diff --git a/.github/ISSUE_TEMPLATE/convergence_epic.md b/.github/ISSUE_TEMPLATE/convergence_epic.md index 17384af56d6b1..29ee9fb0f80c8 100644 --- a/.github/ISSUE_TEMPLATE/convergence_epic.md +++ b/.github/ISSUE_TEMPLATE/convergence_epic.md @@ -35,7 +35,7 @@ completed as part of converging a component. More info can be found here: https: - [ ] Conformance tests - [ ] Unit tests - [ ] VR tests - - [ ] Accessibility behavior tests + - [ ] Accessibility behavior tests - [ ] Write README.md covering basic usage - [ ] Write initial MIGRATION.md guide (include v8 and v0) - [ ] **Deliverable:** Experimental component ready for partner use diff --git a/apps/test-bundles/package.json b/apps/test-bundles/package.json index 6e38b3873b52a..ee31dbb3e3266 100644 --- a/apps/test-bundles/package.json +++ b/apps/test-bundles/package.json @@ -12,11 +12,8 @@ }, "dependencies": { "@fluentui/eslint-plugin": "^1.0.0-beta.1", - "@fluentui/keyboard-key": "^0.2.13", - "@fluentui/react": "^8.0.0-beta.55", - "@fluentui/react-button": "^1.0.0-beta.32", - "@fluentui/react-compose": "^1.0.0-beta.13", "@fluentui/scripts": "^1.0.0", + "fs-extra": "^8.1.0", "parallel-webpack": "^2.6.0", "webpack-bundle-analyzer": "^4.4.0", "terser-webpack-plugin": "^5.1.1" diff --git a/apps/test-bundles/webpack.config.js b/apps/test-bundles/webpack.config.js index 63b886d7f8712..8c6803023c984 100644 --- a/apps/test-bundles/webpack.config.js +++ b/apps/test-bundles/webpack.config.js @@ -1,19 +1,29 @@ // @ts-check -const { createWebpackConfig, buildEntries, buildEntry } = require('./webpackUtils'); +const { + buildEntries, + buildEntry, + createWebpackConfig, + createFluentNorthstarFixtures, + createFluentReactFixtures, + createEntry, +} = require('./webpackUtils'); -// Create entries for all top level imports. -const entries = buildEntries('@fluentui/react'); -// If/when we start working in react-next again, the bundle size tests should be set up like this -// so that only the components directly within react-next are tested. -// buildEntries( -// '@fluentui/react-next', -// entries, -// false /* do not include stats for better performance. */, -// true /* onlyOwnComponents */, -// ); +const package = process.env.PACKAGE; -// Create entries for single top level import. -entries['react-compose'] = buildEntry('@fluentui/react-compose'); -entries['keyboard-key'] = buildEntry('@fluentui/keyboard-key'); +let entries; +if (package === '@fluentui/react-northstar') { + createFluentNorthstarFixtures(); + entries = buildEntries('@fluentui/react-northstar'); +} else if (package === '@fluentui/react') { + createFluentReactFixtures(); + createEntry('@fluentui/react-compose'); + createEntry('@fluentui/keyboard-key'); + + entries = buildEntries('@fluentui/react'); + entries['react-compose'] = buildEntry('@fluentui/react-compose'); + entries['keyboard-key'] = buildEntry('@fluentui/keyboard-key'); +} else { + process.exit(1); +} module.exports = createWebpackConfig(entries); diff --git a/apps/test-bundles/webpackUtils.js b/apps/test-bundles/webpackUtils.js index a1a34527c38fe..f5121ee8aee19 100644 --- a/apps/test-bundles/webpackUtils.js +++ b/apps/test-bundles/webpackUtils.js @@ -1,10 +1,12 @@ // @ts-check const path = require('path'); -const fs = require('fs'); +const fs = require('fs-extra'); const resources = require('@fluentui/scripts/webpack/webpack-resources'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const TerserPlugin = require('terser-webpack-plugin'); +const FIXTURE_PATH = 'temp/fixtures/'; + function createWebpackConfig(entries) { return Object.keys(entries).map(entryName => { let anaylizerPluginOptions = { @@ -70,69 +72,116 @@ function createWebpackConfig(entries) { }); } +/** + * Webpack will remove any unused import as a dead code (tree shaking). + * Thus we are creating temporary JS files with top-level component imports + * and console logging them. This will ensure that the code is active + * and that webpack bundles it correctly. + */ +function createFluentNorthstarFixtures() { + const packageName = '@fluentui/react-northstar'; + const distPath = path.dirname(require.resolve(packageName).replace('commonjs', 'es')); + const packagePath = path.resolve(distPath, 'components'); + fs.readdirSync(packagePath).forEach(itemName => { + const isFolder = fs.statSync(path.join(packagePath, itemName)).isDirectory(); + + if (isFolder && itemName) { + const importStatement = `import { ${itemName} } from '${packageName}'; console.log(${itemName})`; + try { + const folderName = getFolderName(packageName); + const entryPath = path.join(FIXTURE_PATH, folderName, `${itemName}.js`); + fs.outputFileSync(entryPath, importStatement, 'utf-8'); + } catch (err) { + console.log(err); + } + } + }); +} + // Files which should not be considered top-level entries. const TopLevelEntryFileExclusions = ['index.js', 'version.js', 'index.bundle.js']; +function createFluentReactFixtures() { + const packageName = '@fluentui/react'; + const distPath = path.dirname(require.resolve(packageName).replace('lib-commonjs', 'lib')); + const packagePath = path.resolve(distPath); + fs.readdirSync(packagePath).forEach(itemName => { + const isFolder = fs.statSync(path.join(packagePath, itemName)).isDirectory(); + const isAllowedFile = itemName && itemName.match(/.js$/) && !TopLevelEntryFileExclusions.includes(itemName); + + if (isAllowedFile && !isFolder) { + const item = isFolder ? itemName : itemName.replace(/.js$/, ''); + // import everything from package/item path + const importStatement = `import * as p from '${packageName}/lib/${item}'; console.log(p)`; + try { + const folderName = getFolderName(packageName); + const entryPath = path.join(FIXTURE_PATH, folderName, `${item}.js`); + fs.outputFileSync(entryPath, importStatement, 'utf-8'); + } catch (err) { + console.log(err); + } + } + }); +} + +function createEntry(packageName) { + try { + // import everything from a single package + const importStatement = `import * as p from '${packageName}'; console.log(p)`; + const folderName = getFolderName(packageName); + const entryPath = path.join(FIXTURE_PATH, folderName, 'index.js'); + fs.outputFileSync(entryPath, importStatement, 'utf-8'); + } catch (err) { + console.log(err); + } +} + /** - * Build webpack entries based on top level imports available in a package. + * Build webpack entries from created fixtures. * * @param {boolean} [includeStats] - Stats are generated and used by the size auditor report * to check more details on what caused the bundle size change. Due to stats generation being slow, * and therefore slowing down CI significantly, setting this to true to avoid stats generation. * If bundle size is changed unexpectedly, developers can drill down deeper on the problem by * locally running bundle tests. - * @param {boolean} [onlyOwnComponents] - If true, only run the tests for an entry point file if it - * has a corresponding folder under `lib/components`. This eliminates duplicate bundle size tests - * for components which are just re-exported. */ -function buildEntries(packageName, entries = {}, includeStats = true, onlyOwnComponents = false) { - let packagePath = ''; - - try { - packagePath = path.dirname(require.resolve(packageName)).replace('lib-commonjs', 'lib'); - } catch (e) { - console.log(`The package "${packageName}" could not be resolved. Add it as a dependency to this project.`); - console.log(e); - return; - } +function buildEntries(packageName, entries = {}, includeStats = true) { + const folderName = getFolderName(packageName); + const packagePath = path.join(FIXTURE_PATH, folderName); fs.readdirSync(packagePath).forEach(itemName => { - const isAllowedFile = - // is JS - itemName.match(/.js$/) && - // not excluded - !TopLevelEntryFileExclusions.includes(itemName) && - // if requested, has component implementation within this package (not re-export) - (!onlyOwnComponents || fs.existsSync(path.join(packagePath, 'components', itemName.replace('.js', '')))); - - if (isAllowedFile) { - const entryName = itemName.replace(/.js$/, ''); - - // Replace commonjs paths with lib paths. - const entryPath = path.join(packagePath, itemName); - - entries[`${packageName.replace('@', '').replace('/', '-')}-${entryName}`] = { - entryPath, - includeStats, - }; - } + const entryName = itemName.replace(/.js$/, ''); + const entryPath = path.resolve(path.join(packagePath, itemName)); + entries[`${packageName.replace('@', '').replace('/', '-')}-${entryName}`] = { + entryPath: entryPath, + includeStats, + }; }); return entries; } /** - * Create entries for single top level import. + * Build entries for single fixture with top level import. */ function buildEntry(packageName, includeStats = true) { + const folderName = getFolderName(packageName); + const entryPath = path.resolve(path.join(FIXTURE_PATH, folderName)); return { - entryPath: path.join(path.dirname(require.resolve(packageName)).replace('lib-commonjs', 'lib'), 'index.js'), + entryPath: `${entryPath}/index.js`, includeStats, }; } +function getFolderName(packageName) { + return packageName.replace('@fluentui/', ''); +} + module.exports = { - createWebpackConfig, buildEntries, buildEntry, + createFluentReactFixtures, + createFluentNorthstarFixtures, + createEntry, + createWebpackConfig, }; diff --git a/azure-pipelines.bundlesize.yml b/azure-pipelines.bundlesize.yml index bb219baf387cc..092b9b92bc411 100644 --- a/azure-pipelines.bundlesize.yml +++ b/azure-pipelines.bundlesize.yml @@ -5,7 +5,7 @@ trigger: - master jobs: - - job: build + - job: build_react timeoutInMinutes: 75 pool: vmImage: 'windows-2019' @@ -15,11 +15,13 @@ jobs: - script: npx midgard-yarn install displayName: yarn - - script: yarn build --to test-bundles --no-cache - displayName: yarn build to test-bundles + - script: yarn build --to @fluentui/react @fluentui/react-button @fluentui/react-compose @fluentui/keyboard-key --no-cache + displayName: yarn build to @fluentui/react - script: yarn workspace test-bundles bundle:size displayName: yarn bundle test-bundles + env: + PACKAGE: '@fluentui/react' - script: yarn bundlesizecollect displayName: 'Collate Bundle Size Information' @@ -27,17 +29,88 @@ jobs: - task: PublishBuildArtifacts@1 displayName: 'Publish Bundle Size information to Azure Dev Ops Artifacts' inputs: - PathtoPublish: 'apps/test-bundles/dist/bundlesizes.json' + PathtoPublish: 'apps/test-bundles/dist/bundlesize.json' + ArtifactName: bundlesize-react - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact dist folder upon build for debug' inputs: PathtoPublish: 'apps/test-bundles/dist' + ArtifactName: distdrop-react + + - job: build_northstar + timeoutInMinutes: 75 + pool: + vmImage: 'windows-2019' + steps: + - template: .devops/templates/tools.yml + + - script: npx midgard-yarn install + displayName: yarn + + - script: yarn build --to @fluentui/react-northstar --no-cache + displayName: yarn build to @fluentui/react-northstar + + - script: yarn workspace test-bundles bundle:size + displayName: yarn bundle test-bundles + env: + PACKAGE: '@fluentui/react-northstar' + + - script: yarn bundlesizecollect + displayName: 'Collate Bundle Size Information' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Bundle Size information to Azure Dev Ops Artifacts' + inputs: + PathtoPublish: 'apps/test-bundles/dist/bundlesize.json' + ArtifactName: bundlesize-northstar + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact dist folder upon build for debug' + inputs: + PathtoPublish: 'apps/test-bundles/dist' + ArtifactName: distdrop-northstar + + - job: merge + pool: + vmImage: 'windows-2019' + dependsOn: + - build_react + - build_northstar + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Pipeline Artifact React' + inputs: + artifactName: 'bundlesize-react' + targetPath: '$(Build.ArtifactStagingDirectory)/react' + + - task: DownloadPipelineArtifact@2 + displayName: 'Download Pipeline Artifact N*' + inputs: + artifactName: 'bundlesize-northstar' + targetPath: '$(Build.ArtifactStagingDirectory)/react-northstar' + + - script: 'chocolatey install jq' + displayName: 'Install jq' + + - script: jq -c -s "reduce .[] as $item ({}; . * $item)" $(Build.ArtifactStagingDirectory)/react-northstar/bundlesize.json $(Build.ArtifactStagingDirectory)/react/bundlesize.json > $(Build.ArtifactStagingDirectory)/bundlesizes.json + displayName: 'Merge React and React-Northstar to bundlesizes.json' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Merged Bundle Size information' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/bundlesizes.json' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact dist folder upon build for debug' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: distdrop - job: lightrail pool: server - dependsOn: build + dependsOn: + - merge steps: - task: odefun.odsp-lightrail-tasks-partner.odsp-lightrail-tasks-SizeAuditorWorker.SizeAuditorWorker@0 displayName: 'Size Auditor Check on LightRail' diff --git a/scripts/bundle-size-collect.js b/scripts/bundle-size-collect.js index 6da450dcd35ea..d2266082e53e2 100644 --- a/scripts/bundle-size-collect.js +++ b/scripts/bundle-size-collect.js @@ -10,7 +10,7 @@ const path = require('path'); const distRoot = path.resolve(__dirname, '../apps/test-bundles/dist'); const sizes = {}; -const outputFilename = 'bundlesizes.json'; +const outputFilename = 'bundlesize.json'; var items = fs.readdirSync(distRoot); items.forEach(item => { diff --git a/scripts/tasks/bundle-size-collect.ts b/scripts/tasks/bundle-size-collect.ts index c0d3521e07d48..6b1ea46dbed94 100644 --- a/scripts/tasks/bundle-size-collect.ts +++ b/scripts/tasks/bundle-size-collect.ts @@ -11,7 +11,7 @@ export function bundleSizeCollect() { const distRoot = path.join(__dirname, '../../apps/test-bundles/dist'); const sizes = {}; - const outputFilename = 'bundlesizes.json'; + const outputFilename = 'bundlesize.json'; var items = fs.readdirSync(distRoot); items.forEach(item => { diff --git a/sizeauditor.json b/sizeauditor.json index e5525cd2e8aa8..db19f6ce13e84 100644 --- a/sizeauditor.json +++ b/sizeauditor.json @@ -1,4 +1,4 @@ { "devopsDropFolderName": "drop", - "devopsWebpackStatsArtifactName": "bundlesizes.json" + "devopsAssemblyArtifactName": "drop" }