diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ea94353ef8b14f..5ece5c4ce83a93 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -102,6 +102,7 @@ scripts/update-release-notes @microsoft/fluentui-react-build scripts/utils @microsoft/fluentui-react-build scripts/webpack @microsoft/fluentui-react-build scripts/perf-test-flamegrill @microsoft/fluentui-react-build +scripts/bundle-size-auditor @microsoft/fluentui-react-build #### Fluent UI N* packages/a11y-rules @microsoft/fluentui-northstar @@ -124,7 +125,6 @@ apps/vr-tests @microsoft/fluentui-react apps/vr-tests-react-components @microsoft/fluentui-react apps/ssr-tests @microsoft/fluentui-react apps/pr-deploy-site @microsoft/fluentui-react-build -apps/test-bundles @microsoft/fluentui-react apps/public-docsite-v9 @microsoft/cxe-red @microsoft/cxe-coastal @microsoft/fluentui-react-build apps/theming-designer @microsoft/fluentui-react apps/ssr-tests-v9 @microsoft/fluentui-react-build diff --git a/apps/test-bundles/.eslintrc.json b/apps/test-bundles/.eslintrc.json deleted file mode 100644 index d5ab5e8d3bdee2..00000000000000 --- a/apps/test-bundles/.eslintrc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": ["plugin:@fluentui/eslint-plugin/node"], - "root": true, - "settings": { - "import/resolver": { - "typescript": { - "project": "../../tsconfig.json" - } - } - } -} diff --git a/apps/test-bundles/CHANGELOG.json b/apps/test-bundles/CHANGELOG.json deleted file mode 100644 index 492c83a09e0431..00000000000000 --- a/apps/test-bundles/CHANGELOG.json +++ /dev/null @@ -1,321 +0,0 @@ -{ - "name": "test-bundles", - "entries": [ - { - "date": "Tue, 24 Aug 2021 07:34:48 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "none": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.4.1", - "author": "dzearing@hotmail.com", - "commit": "0abd957c8d4421018e6d792c2a4aa8876967392b", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Fri, 20 Aug 2021 07:37:28 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "none": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.4.0", - "author": "behowell@microsoft.com", - "commit": "21df8406417c5c5c1d053561a498b920ac962b4b", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Tue, 03 Aug 2021 07:39:30 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.3.3", - "author": "behowell@microsoft.com", - "commit": "86476ee0511ad2693c2829b959f93a87ad10f095", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Fri, 09 Jul 2021 07:39:31 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.3.2", - "author": "martinhochel@microsoft.com", - "commit": "18902eb64710aa6253a79781357b8390bb13665c", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Mon, 07 Jun 2021 07:38:15 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.3.1", - "author": "martinhochel@microsoft.com", - "commit": "f856cb3f7fbc3edb3646204c0c7e435fc7678dd1", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Thu, 20 May 2021 07:41:54 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.3.0", - "author": "elcraig@microsoft.com", - "commit": "630b71c415cd40ed0e36773eab99d62cd02a30fb", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Fri, 30 Apr 2021 07:42:23 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.2.0", - "author": "joschect@microsoft.com", - "commit": "2b62c457bb860f6675fae4acae86ee6c0b06c279", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Fri, 23 Apr 2021 07:37:10 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.1.1", - "author": "olfedias@microsoft.com", - "commit": "7d1a9a8aee217022e55b8c39c723b1390b5d8095", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Wed, 31 Mar 2021 00:53:43 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.1.0", - "author": "elcraig@microsoft.com", - "commit": "d41b79242e6b682dfa58fcd76797ecfd9146d4cf", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Fri, 26 Feb 2021 01:16:27 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "patch": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.0.1", - "author": "elcraig@microsoft.com", - "commit": "71f0a43b375b4a932ecbcf6778288422db2dc267", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Thu, 18 Feb 2021 12:27:34 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.0.0-beta.2", - "author": "elcraig@microsoft.com", - "commit": "1072765ed2e0ffda34688d84d376a8bc4bf928f0", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Mon, 15 Feb 2021 12:22:00 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react-button to v1.0.0-beta.32", - "author": "xgao@microsoft.com", - "commit": "d5a5a4f26ce900b3e1b3c0f6c46a17c2e3430439", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Fri, 12 Feb 2021 12:26:20 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react to v8.0.0-beta.54", - "author": "tristan.watanabe@gmail.com", - "commit": "bfc63e446500e60e38ac0ac0a023c7721152a832", - "package": "test-bundles" - } - ], - "none": [ - { - "comment": "Bump @fluentui/react-button to v1.0.0-beta.31", - "author": "xgao@microsoft.com", - "commit": "84f36ddd24db3546e4ccb129a60f7f7f98ceefef", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Thu, 11 Feb 2021 12:18:41 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "none": [ - { - "comment": "Bump @fluentui/eslint-plugin to v1.0.0-beta.1", - "author": "martinhochel@microsoft.com", - "commit": "7566015a7edd355b4fcd3796bc8f44f732ef0877", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Thu, 11 Feb 2021 00:58:10 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react to v8.0.0-beta.53", - "author": "elcraig@microsoft.com", - "commit": "70b63061357489ccc43878d0d0b054953690d5b2", - "package": "test-bundles" - } - ], - "none": [ - { - "comment": "Bump @fluentui/react-button to v1.0.0-beta.30", - "author": "xgao@microsoft.com", - "commit": "69357e7a30d4c282ad82d2ecf4da6fc4b942ab99", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Wed, 10 Feb 2021 12:20:53 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react to v8.0.0-beta.52", - "author": "tristan.watanabe@gmail.com", - "commit": "06e8eda9b5b6eefb3c3c9d34a92998e605a99e8f", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Tue, 09 Feb 2021 12:24:19 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react to v8.0.0-beta.51", - "author": "tristan.watanabe@gmail.com", - "commit": "47b42efbbe9e869864973f3b2cb717e6112b8106", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Tue, 09 Feb 2021 00:56:52 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react-button to v1.0.0-beta.29", - "author": "humbertomakotomorimoto@gmail.com", - "commit": "0f46d91d5de073dc025dc8a137a8caa2eda21a08", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Mon, 08 Feb 2021 12:23:08 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react-button to v1.0.0-beta.28", - "author": "miroslav.stastny@microsoft.com", - "commit": "bca3d0d79accb3675bf3dfd49c0297e646b91d2c", - "package": "test-bundles" - } - ] - } - }, - { - "date": "Fri, 05 Feb 2021 12:20:17 GMT", - "tag": "test-bundles_v1.0.0", - "version": "1.0.0", - "comments": { - "prerelease": [ - { - "comment": "Bump @fluentui/react to v8.0.0-beta.49", - "author": "czearing@outlook.com", - "commit": "0d0fe5677db29dc9c850a013d98fc381bc941f91", - "package": "test-bundles" - } - ] - } - } - ] -} diff --git a/apps/test-bundles/CHANGELOG.md b/apps/test-bundles/CHANGELOG.md deleted file mode 100644 index b5e84cd10f92a3..00000000000000 --- a/apps/test-bundles/CHANGELOG.md +++ /dev/null @@ -1,157 +0,0 @@ -# Change Log - test-bundles - -This log was last generated on Tue, 03 Aug 2021 07:39:30 GMT and should not be manually modified. - - - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Tue, 03 Aug 2021 07:39:30 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.3.3 ([PR #19169](https://github.com/microsoft/fluentui/pull/19169) by behowell@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Fri, 09 Jul 2021 07:39:31 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.3.2 ([PR #18808](https://github.com/microsoft/fluentui/pull/18808) by martinhochel@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Mon, 07 Jun 2021 07:38:15 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.3.1 ([PR #18437](https://github.com/microsoft/fluentui/pull/18437) by martinhochel@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Thu, 20 May 2021 07:41:54 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.3.0 ([PR #18024](https://github.com/microsoft/fluentui/pull/18024) by elcraig@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Fri, 30 Apr 2021 07:42:23 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.2.0 ([PR #17932](https://github.com/microsoft/fluentui/pull/17932) by joschect@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Fri, 23 Apr 2021 07:37:10 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.1.1 ([PR #17894](https://github.com/microsoft/fluentui/pull/17894) by olfedias@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Wed, 31 Mar 2021 00:53:43 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.1.0 ([PR #17568](https://github.com/microsoft/fluentui/pull/17568) by elcraig@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Fri, 26 Feb 2021 01:16:27 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Patches - -- Bump @fluentui/eslint-plugin to v1.0.1 ([PR #17169](https://github.com/microsoft/fluentui/pull/17169) by elcraig@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Thu, 18 Feb 2021 12:27:34 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/eslint-plugin to v1.0.0-beta.2 ([PR #16975](https://github.com/microsoft/fluentui/pull/16975) by elcraig@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Mon, 15 Feb 2021 12:22:00 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/react-button to v1.0.0-beta.32 ([PR #16880](https://github.com/microsoft/fluentui/pull/16880) by xgao@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Fri, 12 Feb 2021 12:26:20 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/react to v8.0.0-beta.54 ([PR #16849](https://github.com/microsoft/fluentui/pull/16849) by tristan.watanabe@gmail.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Thu, 11 Feb 2021 00:58:10 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/react to v8.0.0-beta.53 ([PR #16895](https://github.com/microsoft/fluentui/pull/16895) by elcraig@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Wed, 10 Feb 2021 12:20:53 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/react to v8.0.0-beta.52 ([PR #16873](https://github.com/microsoft/fluentui/pull/16873) by tristan.watanabe@gmail.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Tue, 09 Feb 2021 12:24:19 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/react to v8.0.0-beta.51 ([PR #16832](https://github.com/microsoft/fluentui/pull/16832) by tristan.watanabe@gmail.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Tue, 09 Feb 2021 00:56:52 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/react-button to v1.0.0-beta.29 ([PR #16742](https://github.com/microsoft/fluentui/pull/16742) by humbertomakotomorimoto@gmail.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Mon, 08 Feb 2021 12:23:08 GMT -[Compare changes](https://github.com/microsoft/fluentui/compare/test-bundles_v1.0.0..test-bundles_v1.0.0) - -### Changes - -- Bump @fluentui/react-button to v1.0.0-beta.28 ([PR #16844](https://github.com/microsoft/fluentui/pull/16844) by miroslav.stastny@microsoft.com) - -## [1.0.0](https://github.com/microsoft/fluentui/tree/test-bundles_v1.0.0) - -Fri, 05 Feb 2021 12:20:17 GMT - -### Changes - -- Bump @fluentui/react to v8.0.0-beta.49 ([PR #15707](https://github.com/microsoft/fluentui/pull/15707) by czearing@outlook.com) diff --git a/apps/test-bundles/just.config.ts b/apps/test-bundles/just.config.ts deleted file mode 100644 index 73a68e195f0336..00000000000000 --- a/apps/test-bundles/just.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as path from 'path'; -import { preset, task, series } from '@fluentui/scripts-tasks'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore - parallel-webpack has no types -import { run } from 'parallel-webpack'; - -import { bundleSizeCollect } from './scripts/bundle-size-collect'; -import { mergeBundleSizes } from './scripts/merge-bundlesizes'; - -preset(); - -// This pacakge doesn't currently have any files that are included in the eslint task -// (it only runs on src, not config files) -task('code-style', 'prettier'); - -task('bundle-size-collect', () => { - const packageName = process.env.PACKAGE ?? ''; - bundleSizeCollect({ packageName }); -}); - -task('bundle', done => { - const configPath = path.join(__dirname, 'webpack.config.js'); - const options = {}; - run(configPath, options, done); -}); - -task('bundle-size-merge', () => { - const root = path.join(__dirname, 'dist'); - mergeBundleSizes( - [path.join(root, 'react/bundlesize.json'), path.join(root, 'react-northstar/bundlesize.json')], - path.join(root, 'bundlesizes.json'), - ); -}); - -task('bundle-size', series('bundle', 'bundle-size-collect')); diff --git a/apps/test-bundles/package.json b/apps/test-bundles/package.json deleted file mode 100644 index 6218993d71f9d0..00000000000000 --- a/apps/test-bundles/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@fluentui/test-bundles", - "version": "1.0.0", - "description": "Testing Fluent UI React bundle sizes.", - "private": true, - "license": "MIT", - "scripts": { - "bundle-size-auditor": "just-scripts bundle-size", - "clean": "just-scripts clean", - "code-style": "just-scripts code-style", - "just": "just-scripts" - }, - "devDependencies": { - "@fluentui/eslint-plugin": "*", - "@fluentui/scripts-tasks": "*", - "@fluentui/scripts-webpack": "*" - } -} diff --git a/apps/test-bundles/scripts/bundle-size-collect.ts b/apps/test-bundles/scripts/bundle-size-collect.ts deleted file mode 100644 index 3128e8a100ceb9..00000000000000 --- a/apps/test-bundles/scripts/bundle-size-collect.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -/** - * - * collates bundle size information from minified files in apps/test-bundles/dist/ and writes to apps/test-bundles/dist//bundlesizes.json. - * - * It is uploaded as an artifact by the build definition in Azure Dev Ops and used to compare baseline and PR file size information which gets reported by Size Auditor - */ -export function bundleSizeCollect(config: { packageName: string; filename?: string }) { - const { packageName, filename: outputFilename = 'bundlesize.json' } = config; - - if (!packageName) { - throw new Error('🚨 No packageName provided! Set it via env variable name "PACKAGE"'); - } - - const distRoot = path.join(__dirname, '../dist', packageName.replace('@fluentui/', '')); - - const sizes: Record = {}; - - const items = fs.readdirSync(distRoot); - items.forEach(item => { - const file = path.join(distRoot, item); - - const isMinifiedJavascriptFile = item.match(/.min.js$/); - if (isMinifiedJavascriptFile) { - sizes[getComponentName(item)] = getFilesizeInBytes(file); - } - }); - - fs.writeFileSync(path.join(distRoot, outputFilename), JSON.stringify({ sizes })); - - function getFilesizeInBytes(fileName: string) { - return fs.statSync(fileName).size; - } - - function getComponentName(fileName: string) { - return path.basename(fileName, '.min.js'); - } -} diff --git a/apps/test-bundles/scripts/merge-bundlesizes.ts b/apps/test-bundles/scripts/merge-bundlesizes.ts deleted file mode 100644 index 3c1b484b44f5e2..00000000000000 --- a/apps/test-bundles/scripts/merge-bundlesizes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as fs from 'fs'; - -export function mergeBundleSizes(configPaths: string[], mergeConfigPath: string) { - const result: { sizes: { [entryName: string]: number } } = { - sizes: {}, - }; - - configPaths.forEach(configPath => { - const json = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - Object.assign(result.sizes, json.sizes); - }); - - fs.writeFileSync(mergeConfigPath, JSON.stringify(result)); -} diff --git a/apps/test-bundles/webpack.config.js b/apps/test-bundles/webpack.config.js deleted file mode 100644 index 958f7f341c40d0..00000000000000 --- a/apps/test-bundles/webpack.config.js +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable no-shadow */ - -// @ts-check -const { - buildEntries, - buildEntry, - createWebpackConfig, - createFluentNorthstarFixtures, - createFluentReactFixtures, - createEntry, -} = require('./webpackUtils'); - -/** - * - * @param {string} packageName - * @returns - */ -function getEntries(packageName) { - if (packageName === '@fluentui/react-northstar') { - createFluentNorthstarFixtures(); - const entries = buildEntries('@fluentui/react-northstar'); - return entries; - } - - if (packageName === '@fluentui/react') { - createFluentReactFixtures(); - createEntry('@fluentui/keyboard-key'); - - const entries = buildEntries('@fluentui/react'); - entries['keyboard-key'] = buildEntry('@fluentui/keyboard-key'); - return entries; - } - - console.error('🚨 packageName needs to be one of `@fluentui/react` or `@fluentui/react-northstar`!'); - process.exit(1); -} - -const packageName = process.env.PACKAGE ?? ''; - -if (!packageName) { - console.error('🚨 No packageName provided! Set it via env variable name "PACKAGE"'); - process.exit(1); -} -const entries = getEntries(packageName); -const config = createWebpackConfig(entries, packageName); - -module.exports = config; diff --git a/apps/test-bundles/webpackUtils.js b/apps/test-bundles/webpackUtils.js deleted file mode 100644 index c76868a36092e9..00000000000000 --- a/apps/test-bundles/webpackUtils.js +++ /dev/null @@ -1,214 +0,0 @@ -// @ts-check -const path = require('path'); -const fs = require('fs-extra'); -const { resources } = require('@fluentui/scripts-webpack'); -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -const TerserPlugin = require('terser-webpack-plugin'); - -const FIXTURE_PATH = 'temp/fixtures/'; - -/** - * - * @param {{[entryName:string]:{entryPath:string, includeStats:boolean}}} entries - * @param {string} packageName - * @returns - */ -function createWebpackConfig(entries, packageName) { - const normalizedPkgName = packageName.replace('@fluentui/', ''); - return Object.keys(entries).map(entryName => { - /** @type {BundleAnalyzerPlugin.Options} */ - let anaylizerPluginOptions = { - analyzerMode: 'static', - reportFilename: entryName + '.stats.html', - openAnalyzer: false, - generateStatsFile: false, - logLevel: 'warn', - }; - - const { entryPath, includeStats } = entries[entryName]; - - if (includeStats) { - anaylizerPluginOptions = { - ...anaylizerPluginOptions, - generateStatsFile: true, - statsOptions: { - // https://webpack.js.org/configuration/stats - assets: true, - modules: true, - - builtAt: false, - .../** @type {*} */ ({ outputPath: false }), - namedChunkGroups: false, - logging: false, - children: false, - source: false, - reasons: false, - chunks: false, - cached: false, - cachedAssets: false, - performance: false, - timings: false, - }, - statsFilename: entryName + '.stats.json', - }; - } - - const config = resources.createConfig( - entryName, - true, - { - entry: { - [entryName]: entryPath, - }, - output: { - filename: `[name].min.js`, - path: path.resolve(__dirname, 'dist', normalizedPkgName), - }, - externals: { - react: 'React', - 'react-dom': 'ReactDOM', - }, - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin({ - extractComments: false, - }), - ], - }, - plugins: [new BundleAnalyzerPlugin(anaylizerPluginOptions)], - }, - true, - true, - )[0]; - if (packageName === '@fluentui/react-northstar') { - // This is used most of the configs for IE 11 compat, which northstar doesn't need - delete config.target; - } - return config; - }); -} - -/** - * 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); - } - } - }); -} - -/** - * @param {string} packageName - */ -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 from created fixtures. - * - * @param {string} packageName - * @param {{[entryName:string]:{entryPath:string, includeStats:boolean}}} [entries] - * @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. - */ -function buildEntries(packageName, entries = {}, includeStats = true) { - const folderName = getFolderName(packageName); - const packagePath = path.join(FIXTURE_PATH, folderName); - - fs.readdirSync(packagePath).forEach(itemName => { - const entryName = itemName.replace(/.js$/, ''); - const entryPath = path.resolve(path.join(packagePath, itemName)); - entries[`${packageName.replace('@', '').replace('/', '-')}-${entryName}`] = { - entryPath, - includeStats, - }; - }); - - return entries; -} - -/** - * Build entries for single fixture with top level import. - * @param {string} packageName - * @param {boolean} [includeStats] - */ -function buildEntry(packageName, includeStats = true) { - const folderName = getFolderName(packageName); - const entryPath = path.resolve(path.join(FIXTURE_PATH, folderName)); - return { - entryPath: `${entryPath}/index.js`, - includeStats, - }; -} - -/** - * @param {string} packageName - */ -function getFolderName(packageName) { - return packageName.replace('@fluentui/', ''); -} - -module.exports = { - buildEntries, - buildEntry, - createFluentNorthstarFixtures, - createFluentReactFixtures, - createEntry, - createWebpackConfig, -}; diff --git a/azure-pipelines.bundlesize.yml b/azure-pipelines.bundlesize.yml index bd3a39f59bd167..45bc4ecc29a675 100644 --- a/azure-pipelines.bundlesize.yml +++ b/azure-pipelines.bundlesize.yml @@ -66,31 +66,22 @@ jobs: filePath: yarn-ci.sh displayName: yarn - - script: yarn build --to @fluentui/react @fluentui/keyboard-key @fluentui/react-northstar - displayName: build @fluentui/react, @fluentui/react-northstar - - - script: yarn workspace @fluentui/test-bundles bundle-size-auditor - displayName: create @fluentui/react bundle-size entries - env: - PACKAGE: '@fluentui/react' - - - script: yarn workspace @fluentui/test-bundles bundle-size-auditor - displayName: create @fluentui/react-northstar bundle-size entries - env: - PACKAGE: '@fluentui/react-northstar' + - script: yarn lage bundle-size-auditor --verbose $(sinceArg) + displayName: build packages & create reports - - script: yarn workspace @fluentui/test-bundles just-scripts bundle-size-merge - displayName: 'Merge @fluentui/react, @fluentui/react-northstar to bundlesizes.json' + - script: yarn bundle-size-auditor --create-report --report-path dist/bundle-size-auditor + displayName: 'Merge generated bundlesize.json into bundlesizes.json' - task: PublishBuildArtifacts@1 - displayName: 'Publish Merged Bundle Size information' + displayName: 'Publish Merged Bundle Size information for lightrail processing' inputs: - PathtoPublish: 'apps/test-bundles/dist/bundlesizes.json' + PathtoPublish: 'dist/bundle-size-auditor/bundlesizes.json' + ArtifactName: drop - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact dist folder upon build for debug' + displayName: 'Publish bundled artifacts for debug' inputs: - PathtoPublish: 'apps/test-bundles/dist' + PathtoPublish: 'dist/bundle-size-auditor' ArtifactName: distdrop - template: .devops/templates/cleanup.yml diff --git a/change/@fluentui-react-7c0452ba-90de-488a-b196-6fa727c27fcc.json b/change/@fluentui-react-7c0452ba-90de-488a-b196-6fa727c27fcc.json new file mode 100644 index 00000000000000..c6f22327e779fb --- /dev/null +++ b/change/@fluentui-react-7c0452ba-90de-488a-b196-6fa727c27fcc.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: add bundle-size-auditor task which replaces global test-bundles application approach", + "packageName": "@fluentui/react", + "email": "martinhochel@microsoft.com", + "dependentChangeType": "none" +} diff --git a/lage.config.js b/lage.config.js index 0459542313df59..69d29cc91919d1 100644 --- a/lage.config.js +++ b/lage.config.js @@ -5,6 +5,7 @@ module.exports = { 'build:info': [], bundle: ['build'], 'bundle-size': ['build'], + 'bundle-size-auditor': ['build'], // adding temporary back until import plugin rule is resolved https://github.com/microsoft/fluentui/issues/27727 lint: ['build'], clean: [], diff --git a/packages/fluentui/react-northstar/bundle-size-auditor.config.js b/packages/fluentui/react-northstar/bundle-size-auditor.config.js new file mode 100644 index 00000000000000..d0f0221fa65b30 --- /dev/null +++ b/packages/fluentui/react-northstar/bundle-size-auditor.config.js @@ -0,0 +1,32 @@ +// @ts-check +const path = require('path'); +const fs = require('fs'); + +module.exports = /** @type {import('@fluentui/scripts-bundle-size-auditor').Config} */ ({ + createFixtures: config => { + const packageName = '@fluentui/react-northstar'; + const distPath = path.dirname(require.resolve(packageName).replace('commonjs', 'es')); + const packagePath = path.resolve(distPath, 'components'); + const fixturesEntries = fs.readdirSync(packagePath).map(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 = packageName.replace('@fluentui/', ''); + const entryPath = path.join(folderName, `${itemName}.js`); + config.writeFixture(entryPath, importStatement); + + return entryPath; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + } + + return; + }); + + return fixturesEntries.filter(Boolean); + }, +}); diff --git a/packages/fluentui/react-northstar/package.json b/packages/fluentui/react-northstar/package.json index c1a31c035be992..31c27d3ec40736 100644 --- a/packages/fluentui/react-northstar/package.json +++ b/packages/fluentui/react-northstar/package.json @@ -67,6 +67,7 @@ "scripts": { "build": "gulp bundle:package:no-umd && yarn build:info", "build:info": "gulp build:component-info", + "bundle-size-auditor": "bundle-size-auditor --report-path='../../../dist/bundle-size-auditor/react-northstar'", "clean": "gulp clean:component-info bundle:package:clean", "lint": "eslint --ext .js,.ts,.tsx .", "lint:fix": "yarn lint --fix", diff --git a/packages/react/bundle-size-auditor.config.js b/packages/react/bundle-size-auditor.config.js new file mode 100644 index 00000000000000..e1ce726900d425 --- /dev/null +++ b/packages/react/bundle-size-auditor.config.js @@ -0,0 +1,39 @@ +// @ts-check +const path = require('path'); +const fs = require('fs'); + +module.exports = /** @type {import('@fluentui/scripts-bundle-size-auditor').Config} */ ({ + extraEntries: ['@fluentui/keyboard-key'], + createFixtures: config => { + // Files which should not be considered top-level entries. + const TopLevelEntryFileExclusions = ['index.js', 'version.js', 'index.bundle.js']; + + const packageName = '@fluentui/react'; + const distPath = path.dirname(require.resolve(packageName).replace('lib-commonjs', 'lib')); + const packagePath = path.resolve(distPath); + + const fixturesEntries = fs.readdirSync(packagePath).map(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 = packageName.replace('@fluentui/', ''); + const entryPath = path.join(folderName, `${item}.js`); + config.writeFixture(entryPath, importStatement); + + return entryPath; + } catch (err) { + console.log(err); + } + } + + return; + }); + + return fixturesEntries.filter(Boolean); + }, +}); diff --git a/packages/react/package.json b/packages/react/package.json index 2b192209661925..50e9bd73a5f8c1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -17,6 +17,7 @@ "scripts": { "build": "just-scripts build", "bundle": "just-scripts bundle", + "bundle-size-auditor": "bundle-size-auditor --report-path='../../dist/bundle-size-auditor/react'", "build-storybook": "cross-env NODE_OPTIONS=--max-old-space-size=3072 just-scripts storybook:build", "clean": "just-scripts clean", "code-style": "just-scripts code-style", diff --git a/scripts/bundle-size-auditor/README.md b/scripts/bundle-size-auditor/README.md new file mode 100644 index 00000000000000..4c78261882375f --- /dev/null +++ b/scripts/bundle-size-auditor/README.md @@ -0,0 +1,71 @@ +# bundle-size-auditor + +A CLI utility for getting bundle size information from your package. + +> NOTE: this is a legacy tool which was created to replace global "apps/test-bundles" functionality. +> ATM it is used only for `fluentui/react` and `fluentui/react-northstar` +> In future this will be removed and replaced by more robust `bundle-size` CLI tool(monosize). + +## CLI + +`yarn bundle-size-auditor --help` + +## Configuration + +Add task to your package.json + +```json +{ + "name": "@proj/one", + "scripts": { + "bundle-size-auditor": "bundle-size-auditor --report-path='../dist/bundle-size-auditor/one'" + } +} +``` + +### Options + +#### `--report-path` + +While fixtures and bundles will be created locally within your package `./temp/fixtures/`, +this path should point to same folder for all packages using this tool within monorepo, as we will process that path for CI. + +#### `--create-report` + +> mostly used on CI + +if used `--report-path` is mandatory. this will generate a merged `bundlesizes.json` metadata from all provided bundle within report-path folder. + +## How it works + +1. creates fixtures for your modules within a package +2. bundles those fixtures via webpack in production mode with `webpack-bundle-analyzer` included to generate needed stats. +3. generates `bundlesize.json` which contains bundle size per module within a package +4. (CI) a `bundlesizes.json` is generated (via `yarn bundle-size-auditor --create-report --report-path dist/bundle-size-auditor`), which contains merged `bundlesize.json` files from all projects within monorepo that use `bundle-size-auditor` +5. (CI) `bundlesizes.json` needs to be uploaded as an artifact on your CI from where it will be processed by some auditing service (we use `lightrail`) + +### lightrail + +To get meaningful auditing capabilities on CI, generated output needs to be uploaded to some BLOB storage on CI which is processed by `lightrail service` (which has "sizeauditor" capabilities build-in). + +Ligtrail service is configured within monorepo root [sizeauditor.json](./sizeauditor.json) + +```json +{ + "devopsDropFolderName": "drop", + "devopsAssemblyArtifactName": "drop" +} +``` + +Those `drop` values need to be the same as artifact name being uploaded on CI + +[pipeline config](../../azure-pipelines.bundlesize.yml) + +```yml +- task: PublishBuildArtifacts@1 + displayName: 'Publish Merged Bundle Size information for lightrail processing' + inputs: + PathtoPublish: 'dist/bundle-size-auditor/bundlesizes.json' + # This one needs to be identical to "devopsDropFolderName" and "devopsAssemblyArtifactName" within sizeauditor.json + ArtifactName: drop +``` diff --git a/scripts/bundle-size-auditor/__fixtures__/proj-one/bundle-size-auditor.config.js b/scripts/bundle-size-auditor/__fixtures__/proj-one/bundle-size-auditor.config.js new file mode 100644 index 00000000000000..991359900950e5 --- /dev/null +++ b/scripts/bundle-size-auditor/__fixtures__/proj-one/bundle-size-auditor.config.js @@ -0,0 +1,9 @@ +module.exports = { + createFixtures: config => { + const entryPath = 'proj-one/hello.js'; + const importStatement = "import * as p from '../../../lib-dist/hello'; console.log(p)"; + config.writeFixture(entryPath, importStatement); + const fixturesEntries = [entryPath]; + return fixturesEntries; + }, +}; diff --git a/scripts/bundle-size-auditor/__fixtures__/proj-one/lib-dist/hello.js b/scripts/bundle-size-auditor/__fixtures__/proj-one/lib-dist/hello.js new file mode 100644 index 00000000000000..a79f544fa9f9bd --- /dev/null +++ b/scripts/bundle-size-auditor/__fixtures__/proj-one/lib-dist/hello.js @@ -0,0 +1,3 @@ +export function greet(who, greeting = 'hello') { + return greeting + who + '!!!'; +} diff --git a/scripts/bundle-size-auditor/__fixtures__/proj-one/lib-dist/index.js b/scripts/bundle-size-auditor/__fixtures__/proj-one/lib-dist/index.js new file mode 100644 index 00000000000000..e4a7cd6b541e10 --- /dev/null +++ b/scripts/bundle-size-auditor/__fixtures__/proj-one/lib-dist/index.js @@ -0,0 +1 @@ +export { greet } from './hello'; diff --git a/scripts/bundle-size-auditor/__fixtures__/proj-one/package.json b/scripts/bundle-size-auditor/__fixtures__/proj-one/package.json new file mode 100644 index 00000000000000..3d60463115c83f --- /dev/null +++ b/scripts/bundle-size-auditor/__fixtures__/proj-one/package.json @@ -0,0 +1,5 @@ +{ + "name": "@fluentui/proj-one", + "main": "lib-dist/index.js", + "private": true +} diff --git a/scripts/bundle-size-auditor/bin/bundle-size-auditor.js b/scripts/bundle-size-auditor/bin/bundle-size-auditor.js new file mode 100755 index 00000000000000..4c0851d8be4f6e --- /dev/null +++ b/scripts/bundle-size-auditor/bin/bundle-size-auditor.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +const path = require('path'); +const { registerTsProject } = require('nx/src/utils/register'); + +registerTsProject(path.join(__dirname, '..'), 'tsconfig.lib.json'); + +const { cli } = require('../src/cli'); +cli().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/bundle-size-auditor/jest.config.js b/scripts/bundle-size-auditor/jest.config.js new file mode 100644 index 00000000000000..10369a59749c3e --- /dev/null +++ b/scripts/bundle-size-auditor/jest.config.js @@ -0,0 +1,19 @@ +// @ts-check + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'scripts-bundle-size-auditor', + preset: '../../jest.preset.js', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + isolatedModules: true, + }, + ], + }, + coverageDirectory: './coverage', + testEnvironment: 'node', +}; diff --git a/scripts/bundle-size-auditor/package.json b/scripts/bundle-size-auditor/package.json new file mode 100644 index 00000000000000..eafc9f461d4cd4 --- /dev/null +++ b/scripts/bundle-size-auditor/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fluentui/scripts-bundle-size-auditor", + "version": "0.0.1", + "private": true, + "main": "src/index.dev.js", + "bin": { + "bundle-size-auditor": "./bin/bundle-size-auditor.js" + }, + "scripts": { + "format": "prettier -w --ignore-path ../../.prettierignore .", + "format:check": "yarn format -c", + "lint": "eslint --ext .ts,.js .", + "test": "jest --passWithNoTests", + "type-check": "tsc -b tsconfig.json" + }, + "dependencies": {} +} diff --git a/scripts/bundle-size-auditor/src/bundle-size-collect.ts b/scripts/bundle-size-auditor/src/bundle-size-collect.ts new file mode 100644 index 00000000000000..7718669aef197f --- /dev/null +++ b/scripts/bundle-size-auditor/src/bundle-size-collect.ts @@ -0,0 +1,93 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { run } from 'parallel-webpack'; + +import { + buildEntries, + buildEntry, + createEntry, + createWebpackConfigTemplate, + FIXTURE_PATH, + FIXTURE_BUILD_PATH, + writeFileSync, + Entries, +} from './utils'; +import type { Config } from './types'; + +export async function copyBundles(rootDir: string, reportPath: string) { + const reportFullPath = path.isAbsolute(reportPath) ? reportPath : path.join(rootDir, reportPath); + + return fs.cpSync(path.join(rootDir, FIXTURE_BUILD_PATH), reportFullPath, { recursive: true }); +} + +export function prepareEntries( + options: { + rootDir: string; + packageName: string; + } & Config, +) { + const writeFixture = (entryName: string, content: string) => { + writeFileSync(path.join(rootDir, FIXTURE_PATH, entryName), content, 'utf-8'); + }; + + const { rootDir, packageName, extraEntries = [], createFixtures } = options; + + createFixtures({ writeFixture }); + + extraEntries.forEach(extraEntry => { + createEntry(rootDir, extraEntry); + }); + + const entries = buildEntries(rootDir, packageName); + + extraEntries.forEach(extraEntry => { + const newEntry = buildEntry(rootDir, extraEntry); + Object.assign(entries, newEntry); + }); + + return entries; +} + +export async function bundleEntries(options: { rootDir: string; packageName: string; entries: Entries }) { + const configPath = createWebpackConfigTemplate(options); + + return await run(configPath, {}); +} + +/** + * + * collates bundle size information from minified files in package `temp/fixtures/build/` and writes to `temp/fixtures/build/bundlesizes.json`. + * + * It is uploaded as an artifact by the build definition in Azure Dev Ops and used to compare baseline and PR file size information which gets reported by Size Auditor + */ +export function bundleSizeCollect(config: { rootDir: string; packageName: string; filename: string }) { + const { rootDir, filename: outputFilename } = config; + + const distRoot = path.join(rootDir, FIXTURE_BUILD_PATH); + const bundlesizeJsonPath = path.join(distRoot, outputFilename); + + const sizes: Record = {}; + + const items = fs.readdirSync(distRoot); + items.forEach(item => { + const file = path.join(distRoot, item); + + const isMinifiedJavascriptFile = item.match(/.min.js$/); + if (isMinifiedJavascriptFile) { + sizes[getComponentName(item)] = getFilesizeInBytes(file); + } + }); + + console.log(`CREATE: ${bundlesizeJsonPath} `); + + fs.writeFileSync(bundlesizeJsonPath, JSON.stringify({ sizes })); + + function getFilesizeInBytes(fileName: string) { + return fs.statSync(fileName).size; + } + + function getComponentName(fileName: string) { + return path.basename(fileName, '.min.js'); + } +} diff --git a/scripts/bundle-size-auditor/src/cli.spec.ts b/scripts/bundle-size-auditor/src/cli.spec.ts new file mode 100644 index 00000000000000..57d84ff9ee7837 --- /dev/null +++ b/scripts/bundle-size-auditor/src/cli.spec.ts @@ -0,0 +1,81 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +import { stripIndents } from '@nrwl/devkit'; + +import { bundleSizeAuditor } from './cli'; + +describe(`bundleSizeAuditor`, () => { + // don't spam testing output with logs from webpack etc + const noop = () => {}; + jest.spyOn(console, 'log').mockImplementation(noop); + jest.spyOn(console, 'warn').mockImplementation(noop); + + function setup() { + const fixturesRoot = path.join(__dirname, '../__fixtures__'); + const rootDir = path.join(__dirname, '../temp/test'); + const projOneRoot = path.join(rootDir, 'proj-one'); + + fs.rmSync(rootDir, { recursive: true, force: true }); + fs.cpSync(fixturesRoot, rootDir, { recursive: true }); + + return { + projOneRoot, + rootDir, + }; + } + + it(`should create fixtures, bundle them and collect size within bundlesize.json`, async () => { + const { projOneRoot, rootDir } = setup(); + await bundleSizeAuditor({ rootDir: projOneRoot }); + + const webpackBundleConfig = fs.readFileSync( + path.join(projOneRoot, 'temp/fixtures/webpack.bundle-size-auditor.config.js'), + 'utf-8', + ); + + const fixtureFile = fs.readFileSync(path.join(projOneRoot, 'temp/fixtures/proj-one/hello.js'), 'utf-8'); + const bundleFile = fs.readFileSync( + path.join(projOneRoot, 'temp/fixtures/build/fluentui-proj-one-hello.min.js'), + 'utf-8', + ); + const bundlesizeJSON = JSON.parse( + fs.readFileSync(path.join(projOneRoot, 'temp/fixtures/build/bundlesize.json'), 'utf-8'), + ); + + expect(stripIndents`${webpackBundleConfig}`).toEqual( + stripIndents` + const { createWebpackConfig } = require('@fluentui/scripts-bundle-size-auditor'); + + module.exports = createWebpackConfig({ + "entries": { + "fluentui-proj-one-hello": { + "entryPath": "${rootDir}/proj-one/temp/fixtures/proj-one/hello.js", + "includeStats": true + } + }, + "packageName": "@fluentui/proj-one", + "bundleRootPath": "${rootDir}/proj-one/temp/fixtures/build", + "transpileToEs5": true + }); + `, + ); + expect(fixtureFile).toEqual(`import * as p from '../../../lib-dist/hello'; console.log(p)`); + expect(bundleFile).toEqual( + `!function(){\"use strict\";var e={d:function(o,t){for(var n in t)e.o(t,n)&&!e.o(o,n)&&Object.defineProperty(o,n,{enumerable:!0,get:t[n]})},o:function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},r:function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})}},o={};function t(e,o=\"hello\"){return o+e+\"!!!\"}e.r(o),e.d(o,{greet:function(){return t}}),console.log(o)}();`, + ); + expect(bundlesizeJSON).toEqual({ sizes: { 'fluentui-proj-one-hello': 484 } }); + }); + + it(`should create bundlesizes.json merge`, async () => { + const { projOneRoot, rootDir } = setup(); + const distPathForBundlesizesJson = path.join(rootDir, 'dist_reports'); + + await bundleSizeAuditor({ rootDir: projOneRoot, reportPath: path.join(distPathForBundlesizesJson, 'proj-one') }); + await bundleSizeAuditor({ rootDir: projOneRoot, createReport: true, reportPath: distPathForBundlesizesJson }); + + expect(JSON.parse(fs.readFileSync(path.join(distPathForBundlesizesJson, 'bundlesizes.json'), 'utf-8'))).toEqual({ + sizes: { 'fluentui-proj-one-hello': 484 }, + }); + }); +}); diff --git a/scripts/bundle-size-auditor/src/cli.ts b/scripts/bundle-size-auditor/src/cli.ts new file mode 100644 index 00000000000000..4199a89e5a9e35 --- /dev/null +++ b/scripts/bundle-size-auditor/src/cli.ts @@ -0,0 +1,64 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import * as yargs from 'yargs'; +import { bundleEntries, prepareEntries, bundleSizeCollect, copyBundles } from './bundle-size-collect'; +import { mergeBundleSizes } from './merge-bundlesizes'; +import { Config } from './types'; +import { CONFIG_NAME } from './utils'; + +export async function cli(rootDir = process.cwd()) { + const args = processArgs(); + + await bundleSizeAuditor({ ...args, rootDir }); +} + +export async function bundleSizeAuditor(options: { rootDir: string } & Arguments) { + const { createReport, reportPath, rootDir } = options; + + if (createReport) { + mergeBundleSizes(reportPath as string, 'bundlesizes.json'); + return; + } + + const configPath = path.join(rootDir, CONFIG_NAME); + const pkgJSon = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8')); + const packageName = pkgJSon.name; + + const config: Config = await import(configPath).then(value => value.default); + + const entries = prepareEntries({ rootDir, packageName, ...config }); + await bundleEntries({ rootDir, packageName, entries }); + bundleSizeCollect({ rootDir, packageName, filename: 'bundlesize.json' }); + + if (reportPath) { + copyBundles(rootDir, reportPath); + } +} + +type Arguments = { + createReport?: boolean; + reportPath?: string; +}; +function processArgs(): Arguments { + const args = yargs + .scriptName('bundle-size-auditor') + .usage('$0', 'creates fixtures, bundles them and provides stats output via webpack') + .option('createReport', { + type: 'boolean', + description: + 'generate `bundlesizes.json` metadata from all provided bundles within report-path folder. Useful for CI scenarios.', + }) + .option('reportPath', { + type: 'string', + description: + 'path where project bundles output including bundlesize.json should be copied. This is useful for CI scenario which includes creating report via --create-report', + }) + .version(false) + .help().argv; + + if (args.createReport && !args.reportPath) { + throw new Error('--report-path is mandatory when --create-report is used'); + } + + return args; +} diff --git a/scripts/bundle-size-auditor/src/index.dev.js b/scripts/bundle-size-auditor/src/index.dev.js new file mode 100644 index 00000000000000..03ac7a5f2b2bab --- /dev/null +++ b/scripts/bundle-size-auditor/src/index.dev.js @@ -0,0 +1,10 @@ +const path = require('path'); +const { registerTsProject } = require('nx/src/utils/register'); + +registerTsProject(path.join(__dirname, '..'), 'tsconfig.lib.json'); + +/** + * @typedef {import('./index').Config} Config + */ + +module.exports = require('./index'); diff --git a/scripts/bundle-size-auditor/src/index.ts b/scripts/bundle-size-auditor/src/index.ts new file mode 100644 index 00000000000000..2cceca56b8d7fd --- /dev/null +++ b/scripts/bundle-size-auditor/src/index.ts @@ -0,0 +1,4 @@ +/** public API */ + +export { createWebpackConfig } from './utils'; +export type { Config } from './types'; diff --git a/scripts/bundle-size-auditor/src/merge-bundlesizes.ts b/scripts/bundle-size-auditor/src/merge-bundlesizes.ts new file mode 100644 index 00000000000000..b8250e4aecc1f4 --- /dev/null +++ b/scripts/bundle-size-auditor/src/merge-bundlesizes.ts @@ -0,0 +1,34 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +type Report = { sizes: { [entryName: string]: number } }; + +export function mergeBundleSizes(reportPath: string, mergeFileName: string) { + const mergeFilePath = path.join(reportPath, mergeFileName); + const result: Report = { + sizes: {}, + }; + + if (!fs.existsSync(reportPath)) { + fs.mkdirSync(reportPath, { recursive: true }); + fs.writeFileSync(mergeFilePath, JSON.stringify(result), 'utf-8'); + console.log(`INFO: no bundle-size-auditor reports present!`); + console.log(`CREATE: ${mergeFilePath}`); + return; + } + + const projects = fs.readdirSync(reportPath).filter(value => { + return fs.statSync(path.join(reportPath, value)).isDirectory(); + }); + + projects.forEach(projectName => { + const bundleSizeJson = path.join(reportPath, projectName, 'bundlesize.json'); + if (fs.existsSync(bundleSizeJson)) { + const json: Report = JSON.parse(fs.readFileSync(bundleSizeJson, 'utf-8')); + Object.assign(result.sizes, json.sizes); + } + }); + + console.log(`CREATE: ${mergeFilePath}`); + fs.writeFileSync(mergeFilePath, JSON.stringify(result)); +} diff --git a/scripts/bundle-size-auditor/src/types.ts b/scripts/bundle-size-auditor/src/types.ts new file mode 100644 index 00000000000000..4e702e23ea4108 --- /dev/null +++ b/scripts/bundle-size-auditor/src/types.ts @@ -0,0 +1,10 @@ +export interface Config { + extraEntries?: string[]; + /** + * 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. + */ + createFixtures: (options: { writeFixture: (entryName: string, content: string) => void }) => string[]; +} diff --git a/scripts/bundle-size-auditor/src/typings.d.ts b/scripts/bundle-size-auditor/src/typings.d.ts new file mode 100644 index 00000000000000..fa8260c93365fa --- /dev/null +++ b/scripts/bundle-size-auditor/src/typings.d.ts @@ -0,0 +1,3 @@ +declare module 'parallel-webpack' { + export async function run(configPath: string, options: {}, doneCallback?: () => void): void; +} diff --git a/scripts/bundle-size-auditor/src/utils.ts b/scripts/bundle-size-auditor/src/utils.ts new file mode 100644 index 00000000000000..f1dfb424eb02e9 --- /dev/null +++ b/scripts/bundle-size-auditor/src/utils.ts @@ -0,0 +1,203 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; +import TerserPlugin from 'terser-webpack-plugin'; +import webpack from 'webpack'; + +type Entry = { entryPath: string; includeStats: boolean }; +export type Entries = { [entryName: string]: Entry }; + +export const FIXTURE_PATH = 'temp/fixtures'; +export const FIXTURE_BUILD_PATH = path.join(FIXTURE_PATH, 'build'); +export const CONFIG_NAME = 'bundle-size-auditor.config.js'; + +export function createWebpackConfigTemplate(options: { rootDir: string; entries: Entries; packageName: string }) { + const { entries, packageName, rootDir } = options; + const projectRootDir = path.posix.join(rootDir, FIXTURE_PATH); + const bundleRootPath = path.posix.join(rootDir, FIXTURE_BUILD_PATH); + + const webpackConfig: Parameters[0] = { + entries: entries, + packageName: packageName, + bundleRootPath: bundleRootPath, + transpileToEs5: packageName !== '@fluentui/react-northstar', + }; + const template = ` + const { createWebpackConfig } = require('@fluentui/scripts-bundle-size-auditor'); + + module.exports = createWebpackConfig(${JSON.stringify(webpackConfig, null, 2)}); + `; + + const webpackConfigPath = path.join(projectRootDir, 'webpack.bundle-size-auditor.config.js'); + + fs.writeFileSync(webpackConfigPath, template, 'utf-8'); + + return webpackConfigPath; +} + +export function createWebpackConfig(options: { + entries: Entries; + packageName: string; + bundleRootPath: string; + transpileToEs5: boolean; +}) { + const { bundleRootPath, entries, transpileToEs5 } = options; + + const cssRule = { + test: /\.css$/, + include: /node_modules/, + use: ['style-loader', 'css-loader'], + }; + + /** + * As of webpack 5, you have to add the `es5` target for IE 11 compatibility. + * Otherwise it will output lambdas for smaller bundle size. + * @see https://webpack.js.org/migrate/5/#need-to-support-an-older-browser-like-ie-11 + * + * NOTE: IE 11 compat is still needed? for fluentui/react (v8) ? + */ + const target = ['web', transpileToEs5 ? 'es5' : null].filter(Boolean) as string[]; + + return Object.entries(entries).map(([entryName, entryDefinition]) => { + const { entryPath, includeStats } = entryDefinition; + + const anaylizerPluginOptions: BundleAnalyzerPlugin.Options = { + analyzerMode: 'static', + reportFilename: entryName + '.stats.html', + openAnalyzer: false, + generateStatsFile: false, + logLevel: 'warn', + ...(includeStats + ? { + generateStatsFile: true, + statsFilename: entryName + '.stats.json', + /** + * https://webpack.js.org/configuration/stats + */ + statsOptions: { + assets: true, + modules: true, + + builtAt: false, + outputPath: false, + chunkModules: false, + // namedChunkGroups: false, + chunkGroups: false, + logging: false, + children: false, + source: false, + reasons: false, + chunks: false, + cached: false, + cachedAssets: false, + performance: false, + timings: false, + }, + } + : null), + }; + + const config: webpack.Configuration = { + mode: 'production', + entry: { + [entryName]: entryPath, + }, + output: { + filename: `[name].min.js`, + path: bundleRootPath, + }, + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + module: { + /* TODO: this should be no longer needed ? - https://github.com/webpack/webpack/issues/1721 */ + noParse: [/autoit.js/], + rules: [cssRule], + }, + target, + devtool: undefined, + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + extractComments: false, + }), + ], + }, + plugins: [ + // This is needed because Webpack 5 no longer automatically resolves process.env values. + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production'), + }), + new BundleAnalyzerPlugin(anaylizerPluginOptions), + ], + }; + + return config; + }); +} + +export function createEntry(cwd: string, packageName: string) { + 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(cwd, FIXTURE_PATH, folderName, 'index.js'); + writeFileSync(entryPath, importStatement, 'utf-8'); + + return entryPath; + } catch (err) { + console.log(err); + } +} + +/** + * Build webpack entries from created fixtures. + * + * @param 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. + */ +export function buildEntries(cwd: string, packageName: string, entries: Entries = {}, includeStats = true) { + const folderName = getFolderName(packageName); + const packagePath = path.join(cwd, FIXTURE_PATH, folderName); + + fs.readdirSync(packagePath).forEach(itemName => { + const entryName = itemName.replace(/.js$/, ''); + const entryPath = path.resolve(path.join(packagePath, itemName)); + entries[`${packageName.replace('@', '').replace('/', '-')}-${entryName}`] = { + entryPath, + includeStats, + }; + }); + + return entries; +} + +/** + * Build entries for single fixture with top level import. + */ +export function buildEntry(cwd: string, packageName: string, includeStats = true) { + const folderName = getFolderName(packageName); + const entryPath = path.join(cwd, FIXTURE_PATH, folderName); + const entryKey = `${packageName.replace('@fluentui/', '').replace('/', '-')}`; + + return { + [entryKey]: { + entryPath: `${entryPath}/index.js`, + includeStats, + }, + }; +} + +function getFolderName(packageName: string) { + return packageName.replace('@fluentui/', ''); +} + +export function writeFileSync(filePath: string, contents: string, encoding: BufferEncoding) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, contents, encoding); +} diff --git a/scripts/bundle-size-auditor/tsconfig.json b/scripts/bundle-size-auditor/tsconfig.json new file mode 100644 index 00000000000000..b289e657bc0e53 --- /dev/null +++ b/scripts/bundle-size-auditor/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@tsconfig/node14/tsconfig.json", + "compilerOptions": { + "target": "ES2019", + "pretty": true, + "noEmit": true, + "allowJs": true, + "checkJs": true, + "sourceMap": true, + "noUnusedLocals": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/scripts/bundle-size-auditor/tsconfig.lib.json b/scripts/bundle-size-auditor/tsconfig.lib.json new file mode 100644 index 00000000000000..6c0f8040f80349 --- /dev/null +++ b/scripts/bundle-size-auditor/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019"], + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "exclude": ["**/*.spec.ts", "**/*.test.ts", "src/index.dev.js"], + "include": ["./src/**/*.ts", "./src/**/*.js"] +} diff --git a/scripts/bundle-size-auditor/tsconfig.spec.json b/scripts/bundle-size-auditor/tsconfig.spec.json new file mode 100644 index 00000000000000..4d20fc98b71dd4 --- /dev/null +++ b/scripts/bundle-size-auditor/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] +} diff --git a/scripts/generators/bundle-size-collect.js b/scripts/generators/bundle-size-collect.js deleted file mode 100644 index d5f1944e257b44..00000000000000 --- a/scripts/generators/bundle-size-collect.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @see {@link ../../azure-pipelines.bundlesize.yml} - */ - -// This script collates bundle size information from minified files in apps/test-bundles/dist -// and writes to apps/test-bundles/dist/bundlesizes.json. -// It is uploaded as an artifact by the build definition in Azure DevOps and used to compare -// baseline and PR file size information which gets reported by Size Auditor. - -const fs = require('fs'); -const path = require('path'); - -const distRoot = path.resolve(__dirname, '../../apps/test-bundles/dist'); -/** @type {Record} */ -const sizes = {}; -const outputFilename = 'bundlesize.json'; - -// eslint-disable-next-line no-var -var items = fs.readdirSync(distRoot); -items.forEach(item => { - const file = path.join(distRoot, item); - - const isMinifiedJavascriptFile = item.match(/.min.js$/); - if (isMinifiedJavascriptFile) { - sizes[getComponentName(item)] = getFilesizeInBytes(file); - } -}); - -fs.writeFileSync(path.join(distRoot, outputFilename), JSON.stringify({ sizes })); - -function getFilesizeInBytes(/** @type {string} */ fileName) { - return fs.statSync(fileName).size; -} - -function getComponentName(/** @type {string} */ fileName) { - return path.basename(fileName, '.min.js'); -} diff --git a/workspace.json b/workspace.json index 8aa630b8799ea8..9e392f6465e17d 100644 --- a/workspace.json +++ b/workspace.json @@ -1055,6 +1055,12 @@ "projectType": "library", "tags": ["tools"] }, + "@fluentui/scripts-bundle-size-auditor": { + "root": "scripts/bundle-size-auditor", + "sourceRoot": "scripts/bundle-size-auditor/src", + "projectType": "library", + "tags": ["tools"] + }, "@fluentui/set-version": { "root": "packages/set-version", "projectType": "library", @@ -1097,11 +1103,6 @@ "projectType": "library", "implicitDependencies": [] }, - "@fluentui/test-bundles": { - "root": "apps/test-bundles", - "projectType": "application", - "implicitDependencies": [] - }, "@fluentui/test-utilities": { "root": "packages/test-utilities", "projectType": "library",