Skip to content

Commit

Permalink
Add size check to react-northstar build (#16686)
Browse files Browse the repository at this point in the history
  • Loading branch information
petdud authored Feb 16, 2021
1 parent ac1bbd3 commit 6fac663
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/convergence_epic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions apps/test-bundles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 24 additions & 14 deletions apps/test-bundles/webpack.config.js
Original file line number Diff line number Diff line change
@@ -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);
123 changes: 86 additions & 37 deletions apps/test-bundles/webpackUtils.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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,
};
83 changes: 78 additions & 5 deletions azure-pipelines.bundlesize.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ trigger:
- master

jobs:
- job: build
- job: build_react
timeoutInMinutes: 75
pool:
vmImage: 'windows-2019'
Expand All @@ -15,29 +15,102 @@ 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'

- 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'
Expand Down
2 changes: 1 addition & 1 deletion scripts/bundle-size-collect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion scripts/tasks/bundle-size-collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down

0 comments on commit 6fac663

Please sign in to comment.