├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── images │ └── gh-report.png └── workflows │ ├── ci-change.yml │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── .yarn ├── patches │ └── beachball-npm-2.31.5-0e84ec4233.patch └── releases │ └── yarn-4.7.0.cjs ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── azure-pipelines.yml ├── beachball.config.js ├── beachball.hooks.js ├── change ├── monosize-62eebf5a-6557-4e54-bb6c-b53bc6a63341.json ├── monosize-7b73fac9-4b25-47fe-8e47-41b20b43d24d.json ├── monosize-bundler-esbuild-0b5a0dcb-961d-4c3c-9084-9e9b20c7e677.json ├── monosize-bundler-esbuild-10cfe219-525f-4ea9-9427-ce49451c44a9.json ├── monosize-bundler-esbuild-42a2093e-1c68-4f42-a332-aabdd56381a3.json ├── monosize-bundler-esbuild-bd459261-a419-4687-8865-4aa54d229d7d.json ├── monosize-bundler-rsbuild-2096eea7-473a-4369-9e46-28fa89e50b1f.json ├── monosize-bundler-webpack-356a50ef-4116-4dc4-aafe-e9a335c3c457.json ├── monosize-bundler-webpack-a995d54d-6ab4-4746-95fd-7eab8fdc72c0.json ├── monosize-c82e1067-d113-4774-9e03-72a7444829db.json ├── monosize-cda9b3c4-9d4b-4654-b74b-13ff5d0da1ce.json ├── monosize-de38c7d9-ac8f-4dbe-b835-1facd527df16.json ├── monosize-fc0d287e-d8f7-4d2a-ae57-4359f68eaf23.json ├── monosize-storage-azure-e2978f15-7bc1-4a3f-bc41-d7ad9e017af8.json ├── monosize-storage-azure-e7f1a622-f228-4b92-9091-3c679c608696.json ├── monosize-storage-azure-f2dc7b3e-f20a-4ee6-8488-dbdb1d47ce3a.json ├── monosize-storage-upstash-231477ad-e1e8-48a9-8709-edf8c85d13f3.json └── monosize-storage-upstash-32304c4e-8331-4b1a-ab79-277e0aa9e130.json ├── migrations.json ├── nx.json ├── package.json ├── packages ├── monosize-bundler-esbuild │ ├── .eslintrc.json │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── createEsbuildBundler.mts │ │ ├── createEsbuildBundler.test.mts │ │ ├── index.mts │ │ ├── runEsbuild.mts │ │ └── types.mts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.mts ├── monosize-bundler-rsbuild │ ├── .eslintrc.json │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── __snapshots__ │ │ │ └── createRsbuildBundler.test.mts.snap │ │ ├── createRsbuildBundler.mts │ │ ├── createRsbuildBundler.test.mts │ │ └── index.mts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.mts ├── monosize-bundler-webpack │ ├── .eslintrc.json │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── createWebpackBundler.mts │ │ ├── createWebpackBundler.test.mts │ │ ├── index.mts │ │ ├── runTerser.mts │ │ ├── runTerser.test.mts │ │ ├── runWebpack.mts │ │ └── types.mts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.mts ├── monosize-storage-azure │ ├── .eslintrc.json │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── __fixture__ │ │ │ └── sampleReports.mts │ │ ├── createTableClient.mts │ │ ├── createTableClient.test.mts │ │ ├── getRemoteReport.mts │ │ ├── getRemoteReport.test.mts │ │ ├── index.mts │ │ ├── types.mts │ │ ├── uploadReportToRemote.mts │ │ └── uploadReportToRemote.test.mts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.mts ├── monosize-storage-upstash │ ├── .eslintrc.json │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── getRemoteReport.test.mts │ │ └── index.mts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.mts └── monosize │ ├── .eslintrc.json │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ └── monosize.mjs │ ├── package.json │ ├── project.json │ ├── src │ ├── __fixture__ │ │ ├── sampleComparedReport.mts │ │ └── sampleReport.mts │ ├── __mocks__ │ │ ├── find-up.mts │ │ ├── gzip-size.mts │ │ └── pretty-bytes.mts │ ├── commands │ │ ├── compareReports.mts │ │ ├── compareReports.test.mts │ │ ├── measure.mts │ │ ├── measure.test.mts │ │ └── uploadReport.mts │ ├── index.mts │ ├── logger.mts │ ├── reporters │ │ ├── __snapshots__ │ │ │ └── markdownReporter.test.mts.snap │ │ ├── cliReporter.mts │ │ ├── cliReporter.test.mts │ │ ├── markdownReporter.mts │ │ ├── markdownReporter.test.mts │ │ └── shared.mts │ ├── types.mts │ └── utils │ │ ├── calculateDiffByMetric.mts │ │ ├── calculateDiffByMetric.test.mts │ │ ├── collectLocalReport.mts │ │ ├── collectLocalReport.test.mts │ │ ├── compareResultsInReports.mts │ │ ├── compareResultsInReports.test.mts │ │ ├── getChangedEntriesInReport.mts │ │ ├── getChangedEntriesInReport.test.mts │ │ ├── helpers.mts │ │ ├── helpers.test.mts │ │ ├── prepareFixture.mts │ │ ├── prepareFixture.test.mts │ │ ├── readConfig.mts │ │ ├── readConfig.test.mts │ │ ├── sortComparedReport.mts │ │ └── sortComparedReport.test.cts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.mts ├── tsconfig.base.json ├── typings ├── environment │ ├── README.md │ ├── index.d.ts │ └── tsconfig.json └── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx", "import", "unicorn"], 5 | "extends": ["plugin:import/typescript"], 6 | "settings": { 7 | "import/resolver": { 8 | "typescript": { 9 | "alwaysTryTypes": true, 10 | "project": "./tsconfig.base.json" 11 | } 12 | } 13 | }, 14 | "rules": { 15 | "import/no-extraneous-dependencies": ["error", { "devDependencies": false }] 16 | }, 17 | "overrides": [ 18 | { 19 | "files": ["*.ts", "*.tsx", ".mts", "*.js", "*.jsx"], 20 | "rules": { 21 | "@nx/enforce-module-boundaries": [ 22 | "error", 23 | { 24 | "enforceBuildableLibDependency": true, 25 | "allow": [], 26 | "depConstraints": [ 27 | { 28 | "sourceTag": "*", 29 | "onlyDependOnLibsWithTags": ["*"] 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "files": ["*.ts", "*.tsx", "*.mts"], 38 | "extends": ["plugin:@nx/typescript"], 39 | "rules": {} 40 | }, 41 | { 42 | "files": ["*.mts"], 43 | "rules": { 44 | "unicorn/prefer-module": "error", 45 | "unicorn/prefer-node-protocol": "error" 46 | } 47 | }, 48 | { 49 | "files": ["*.js", "*.jsx"], 50 | "extends": ["plugin:@nx/javascript"], 51 | "rules": {} 52 | }, 53 | { 54 | "files": ["**/__fixtures__/**/*", "**/*.test.mts", "**/vite.config.mts"], 55 | "rules": { 56 | "import/no-extraneous-dependencies": "off" 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Catch All Rule 2 | * @layershifter @microsoft/fluentui-react-build 3 | 4 | 5 | # Packages 6 | packages/monosize @layershifter @hotell 7 | packages/monosize-storage-azure @layershifter 8 | packages/monosize-storage-upstash @layershifter 9 | -------------------------------------------------------------------------------- /.github/images/gh-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/monosize/0ccd02477189691e76d44834574236dc07b66f51/.github/images/gh-report.png -------------------------------------------------------------------------------- /.github/workflows/ci-change.yml: -------------------------------------------------------------------------------- 1 | name: Changelog 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | pr: 7 | runs-on: ubuntu-latest 8 | if: ${{ github.event_name == 'pull_request' }} 9 | steps: 10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | with: 12 | fetch-depth: 0 13 | 14 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 15 | with: 16 | node-version: '20' 17 | 18 | - run: npx beachball check --changehint "Run 'yarn change' to generate a change file" 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | env: 9 | NX_PARALLEL: 4 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event_name == 'pull_request' }} 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 21 | uses: nrwl/nx-set-shas@dbe0650947e5f2c81f59190a38512cf49126fe6b # v4.3.0 22 | 23 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 24 | with: 25 | cache: 'yarn' 26 | node-version: '20' 27 | 28 | - run: yarn install --immutable 29 | - run: yarn dedupe --check 30 | - run: yarn check-dependencies 31 | - run: yarn nx affected --target=lint,build,test --nxBail 32 | 33 | ci-windows: 34 | runs-on: windows-latest 35 | if: ${{ github.event_name == 'pull_request' }} 36 | steps: 37 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 38 | with: 39 | fetch-depth: 0 40 | 41 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 42 | uses: nrwl/nx-set-shas@dbe0650947e5f2c81f59190a38512cf49126fe6b # v4.3.0 43 | 44 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 45 | with: 46 | cache: 'yarn' 47 | node-version: '20' 48 | 49 | - run: yarn install --immutable 50 | - run: yarn nx affected --target=test --nxBail 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/releases 12 | /node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | 43 | .nx/cache 44 | vite.config.*.timestamp* 45 | vitest.config.*.timestamp* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | 6 | /.nx/cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.yarn/patches/beachball-npm-2.31.5-0e84ec4233.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/packageManager/packagePublish.js b/lib/packageManager/packagePublish.js 2 | index 6a211b2b472c8d16f776f79a89d5914cabd390b7..f0006dd42e1f8aae581e61c6c5b6dfe495500ed1 100644 3 | --- a/lib/packageManager/packagePublish.js 4 | +++ b/lib/packageManager/packagePublish.js 5 | @@ -9,6 +9,10 @@ const npm_1 = require("./npm"); 6 | function packagePublish(packageInfo, registry, token, access, authType, timeout) { 7 | const packageOptions = packageInfo.combinedOptions; 8 | const packagePath = path_1.default.dirname(packageInfo.packageJsonPath); 9 | + 10 | + const packageDir = require('path').relative(packageInfo.combinedOptions.path, packagePath); 11 | + const artifactsPath = require('path').resolve(packageInfo.combinedOptions.path, 'dist', packageDir); 12 | + 13 | const args = [ 14 | 'publish', 15 | '--registry', 16 | @@ -23,8 +27,9 @@ function packagePublish(packageInfo, registry, token, access, authType, timeout) 17 | args.push('--access'); 18 | args.push(access); 19 | } 20 | + console.log(`publish cwd: ${artifactsPath}`); 21 | console.log(`publish command: ${args.join(' ')}`); 22 | - return npm_1.npmAsync(args, { cwd: packagePath, timeout, all: true }); 23 | + return npm_1.npmAsync(args, { cwd: artifactsPath, timeout, all: true }); 24 | } 25 | exports.packagePublish = packagePublish; 26 | //# sourceMappingURL=packagePublish.js.map 27 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.7.0.cjs 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/monosize/README.md -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). 7 | - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Release pipeline 2 | 3 | pr: none 4 | trigger: none 5 | 6 | variables: 7 | - group: 'Github and NPM secrets' 8 | - name: tags 9 | value: production,externalfacing 10 | 11 | resources: 12 | repositories: 13 | - repository: 1esPipelines 14 | type: git 15 | name: 1ESPipelineTemplates/1ESPipelineTemplates 16 | ref: refs/tags/release 17 | 18 | extends: 19 | template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines 20 | parameters: 21 | pool: 22 | name: Azure-Pipelines-1ESPT-ExDShared 23 | image: windows-latest 24 | os: windows # We need windows because compliance task only run on windows. 25 | stages: 26 | - stage: main 27 | jobs: 28 | - job: Release 29 | pool: 30 | name: '1ES-Host-Ubuntu' 31 | image: '1ES-PT-Ubuntu-20.04' 32 | os: linux 33 | workspace: 34 | clean: all 35 | steps: 36 | # For multiline scripts, we want the whole task to fail if any line of the script fails. 37 | # ADO doesn't have bash configured this way by default. To fix we override the SHELLOPTS built-in variable. 38 | # https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html 39 | # The options below include ADO defaults (braceexpand:hashall:interactive-comments) plus 40 | # errexit:errtrace for better error behavior. 41 | - script: | 42 | echo "##vso[task.setvariable variable=shellopts]braceexpand:hashall:interactive-comments:errexit:errtrace" 43 | displayName: Force exit on error (bash) 44 | 45 | - task: NodeTool@0 46 | inputs: 47 | versionSpec: '16.x' 48 | checkLatest: true 49 | displayName: 'Install Node.js' 50 | 51 | - script: yarn install --frozen-lockfile 52 | displayName: Install dependencies 53 | 54 | - script: | 55 | git config user.name "Fluent UI Build" 56 | git config user.email "fluentui-internal@service.microsoft.com" 57 | git remote set-url origin https://$(githubUser):$(githubPAT)@github.com/microsoft/monosize.git 58 | displayName: Authenticate git for pushes 59 | 60 | - script: | 61 | yarn beachball publish -b origin/main --access public -y -n $(npmToken) 62 | git reset --hard origin/main 63 | env: 64 | GITHUB_PAT: $(githubPAT) 65 | displayName: Publish to NPM & bump versions 66 | -------------------------------------------------------------------------------- /beachball.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @type {import('beachball').BeachballConfig} 5 | */ 6 | module.exports = { 7 | gitTags: false, 8 | ignorePatterns: [ 9 | '**/__fixtures__/**', 10 | '**/*.test.mts', 11 | '**/.eslintrc.json', 12 | '**/vite.config.mts', 13 | '**/project.json', 14 | '**/README.md', 15 | ], 16 | disallowedChangeTypes: ['major'], 17 | hooks: require('./beachball.hooks'), 18 | }; 19 | -------------------------------------------------------------------------------- /beachball.hooks.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const childProcess = require('child_process'); 4 | 5 | /** 6 | * @param {String} command 7 | * 8 | * @returns {Promise} 9 | */ 10 | function sh(command) { 11 | return new Promise((resolve, reject) => { 12 | const [cmd, ...args] = command.split(' '); 13 | 14 | /** @type {import('child_process').SpawnOptions} */ 15 | const options = { 16 | cwd: process.cwd(), 17 | env: process.env, 18 | stdio: 'inherit', 19 | shell: true, 20 | }; 21 | const child = childProcess.spawn(cmd, args, options); 22 | 23 | let stdoutData = ''; 24 | 25 | if (child.stdout) { 26 | child.stdout.on('data', data => { 27 | stdoutData += data; 28 | }); 29 | } 30 | 31 | child.on('close', code => { 32 | if (code === 0) { 33 | resolve(stdoutData); 34 | } 35 | 36 | reject(new Error([`child process exited with code ${code}`, stdoutData].join('\n'))); 37 | }); 38 | }); 39 | } 40 | 41 | let completedPrepublish = false; 42 | 43 | /** 44 | * @type {import('beachball').BeachballConfig['hooks']} 45 | */ 46 | module.exports = { 47 | // Executed after all package versions were bumped -> run build 48 | // If we run build before `beachball publish`, artifacts would have 49 | // old (without bump) versions. 50 | async prepublish() { 51 | // `beachball` runs this hook for every package, we want to run it only once. 52 | if (!completedPrepublish) { 53 | await sh('yarn nx run-many --target=build --all --parallel --max-parallel=3'); 54 | completedPrepublish = true; 55 | } 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /change/monosize-62eebf5a-6557-4e54-bb6c-b53bc6a63341.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minor", 3 | "comment": "chore: remove custom logging, add bundler name", 4 | "packageName": "monosize", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-7b73fac9-4b25-47fe-8e47-41b20b43d24d.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: add repository field to package.json", 4 | "packageName": "monosize", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-bundler-esbuild-0b5a0dcb-961d-4c3c-9084-9e9b20c7e677.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minor", 3 | "comment": "chore: require \"name\" for bundlers", 4 | "packageName": "monosize-bundler-esbuild", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-bundler-esbuild-10cfe219-525f-4ea9-9427-ce49451c44a9.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: add repository field to package.json", 4 | "packageName": "monosize-bundler-esbuild", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-bundler-esbuild-42a2093e-1c68-4f42-a332-aabdd56381a3.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: bump Nx to 20.6.2", 4 | "packageName": "monosize-bundler-esbuild", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-bundler-esbuild-bd459261-a419-4687-8865-4aa54d229d7d.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "patch", 3 | "comment": "chore: bump esbuild requirement", 4 | "packageName": "monosize-bundler-esbuild", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-bundler-rsbuild-2096eea7-473a-4369-9e46-28fa89e50b1f.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minor", 3 | "comment": "feat: initial release", 4 | "packageName": "monosize-bundler-rsbuild", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-bundler-webpack-356a50ef-4116-4dc4-aafe-e9a335c3c457.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: add repository field to package.json", 4 | "packageName": "monosize-bundler-webpack", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-bundler-webpack-a995d54d-6ab4-4746-95fd-7eab8fdc72c0.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minor", 3 | "comment": "chore: remove custom logging, add bundler name", 4 | "packageName": "monosize-bundler-webpack", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-c82e1067-d113-4774-9e03-72a7444829db.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "patch", 3 | "comment": "chore: unify logging", 4 | "packageName": "monosize", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-cda9b3c4-9d4b-4654-b74b-13ff5d0da1ce.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "patch", 3 | "comment": "chore: bump production dependencies", 4 | "packageName": "monosize", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-de38c7d9-ac8f-4dbe-b835-1facd527df16.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: bump Nx to 18.3.5", 4 | "packageName": "monosize", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-fc0d287e-d8f7-4d2a-ae57-4359f68eaf23.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: formating changes", 4 | "packageName": "monosize", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-storage-azure-e2978f15-7bc1-4a3f-bc41-d7ad9e017af8.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: add repository field to package.json", 4 | "packageName": "monosize-storage-azure", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-storage-azure-e7f1a622-f228-4b92-9091-3c679c608696.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "patch", 3 | "comment": "fix: add branch name validation.", 4 | "packageName": "monosize-storage-azure", 5 | "email": "tristan.watanabe@gmail.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-storage-azure-f2dc7b3e-f20a-4ee6-8488-dbdb1d47ce3a.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "patch", 3 | "comment": "chore: bump production dependencies", 4 | "packageName": "monosize-storage-azure", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-storage-upstash-231477ad-e1e8-48a9-8709-edf8c85d13f3.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "none", 3 | "comment": "chore: add repository field to package.json", 4 | "packageName": "monosize-storage-upstash", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "none" 7 | } 8 | -------------------------------------------------------------------------------- /change/monosize-storage-upstash-32304c4e-8331-4b1a-ab79-277e0aa9e130.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "patch", 3 | "comment": "chore: bump production dependencies", 4 | "packageName": "monosize-storage-upstash", 5 | "email": "olfedias@microsoft.com", 6 | "dependentChangeType": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "version": "20.0.0-beta.7", 5 | "description": "Migration for v20.0.0-beta.7", 6 | "implementation": "./src/migrations/update-20-0-0/move-use-daemon-process", 7 | "package": "nx", 8 | "name": "move-use-daemon-process" 9 | }, 10 | { 11 | "version": "20.0.1", 12 | "description": "Set `useLegacyCache` to true for migrating workspaces", 13 | "implementation": "./src/migrations/update-20-0-1/use-legacy-cache", 14 | "x-repair-skip": true, 15 | "package": "nx", 16 | "name": "use-legacy-cache" 17 | }, 18 | { 19 | "version": "20.2.0-beta.5", 20 | "description": "Update TypeScript ESLint packages to v8.13.0 if they are already on v8", 21 | "implementation": "./src/migrations/update-20-2-0/update-typescript-eslint-v8-13-0", 22 | "package": "@nx/eslint", 23 | "name": "update-typescript-eslint-v8.13.0" 24 | }, 25 | { 26 | "version": "20.3.0-beta.1", 27 | "description": "Update ESLint flat config to include .cjs, .mjs, .cts, and .mts files in overrides (if needed)", 28 | "implementation": "./src/migrations/update-20-3-0/add-file-extensions-to-overrides", 29 | "package": "@nx/eslint", 30 | "name": "add-file-extensions-to-overrides" 31 | }, 32 | { 33 | "version": "20.0.4-beta.0", 34 | "description": "Add gitignore entry for temporary vite config files.", 35 | "implementation": "./src/migrations/update-20-0-4/add-vite-temp-files-to-git-ignore", 36 | "package": "@nx/vite", 37 | "name": "update-20-0-4" 38 | }, 39 | { 40 | "version": "20.0.6-beta.0", 41 | "description": "Add gitignore entry for temporary vite config files and remove previous incorrect glob.", 42 | "implementation": "./src/migrations/update-20-0-4/add-vite-temp-files-to-git-ignore", 43 | "package": "@nx/vite", 44 | "name": "update-20-0-6" 45 | }, 46 | { 47 | "version": "20.3.0-beta.2", 48 | "description": "Add gitignore entry for temporary vitest config files.", 49 | "implementation": "./src/migrations/update-20-3-0/add-vitest-temp-files-to-git-ignore", 50 | "package": "@nx/vite", 51 | "name": "update-20-3-0" 52 | }, 53 | { 54 | "version": "20.5.0-beta.2", 55 | "description": "Install jiti as a devDependency to allow vite to parse TS postcss files.", 56 | "implementation": "./src/migrations/update-20-5-0/install-jiti", 57 | "package": "@nx/vite", 58 | "name": "update-20-5-0-install-jiti" 59 | }, 60 | { 61 | "version": "20.5.0-beta.3", 62 | "description": "Update resolve.conditions to include defaults that are no longer provided by Vite.", 63 | "implementation": "./src/migrations/update-20-5-0/update-resolve-conditions", 64 | "package": "@nx/vite", 65 | "name": "update-20-5-0-update-resolve-conditions" 66 | }, 67 | { 68 | "version": "20.5.0-beta.3", 69 | "description": "Add vite config temporary files to the ESLint configuration ignore patterns if ESLint is used.", 70 | "implementation": "./src/migrations/update-20-5-0/eslint-ignore-vite-temp-files", 71 | "package": "@nx/vite", 72 | "name": "eslint-ignore-vite-temp-files" 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetDefaults": { 3 | "build": { 4 | "cache": true 5 | }, 6 | "@nx/eslint:lint": { 7 | "cache": true, 8 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 9 | }, 10 | "@nx/vite:test": { 11 | "cache": true, 12 | "inputs": ["default", "^default"] 13 | } 14 | }, 15 | "extends": "@nx/workspace/presets/core.json", 16 | "pluginsConfig": { 17 | "@nx/js": { 18 | "analyzeSourceFiles": false 19 | } 20 | }, 21 | "useInferencePlugins": false, 22 | "defaultBase": "main", 23 | "useLegacyCache": true 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-monorepo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/microsoft/monosize" 9 | }, 10 | "scripts": { 11 | "build": "nx affected:build", 12 | "change": "beachball change --no-commit", 13 | "check-dependencies": "syncpack list-mismatches --types prod,peer", 14 | "lint": "nx affected:lint", 15 | "test": "nx affected:test" 16 | }, 17 | "nano-staged": { 18 | "**/*.{js,json,ts,tsx}": "prettier --write", 19 | "**/*.{md}": "doctoc -u" 20 | }, 21 | "simple-git-hooks": { 22 | "pre-commit": "./node_modules/.bin/nano-staged" 23 | }, 24 | "packageManager": "yarn@4.7.0", 25 | "devDependencies": { 26 | "@nx/eslint": "20.6.2", 27 | "@nx/eslint-plugin": "20.6.2", 28 | "@nx/js": "20.6.2", 29 | "@nx/vite": "20.6.2", 30 | "@nx/workspace": "20.6.2", 31 | "@swc-node/register": "1.9.2", 32 | "@swc/core": "1.5.29", 33 | "@types/babel__core": "7.20.5", 34 | "@types/node": "20.17.24", 35 | "@types/tmp": "0.2.3", 36 | "@types/yargs": "17.0.32", 37 | "@typescript-eslint/eslint-plugin": "7.18.0", 38 | "@typescript-eslint/parser": "7.18.0", 39 | "@vitest/coverage-v8": "1.6.1", 40 | "@vitest/ui": "1.6.1", 41 | "beachball": "2.31.5", 42 | "doctoc": "2.2.1", 43 | "eslint": "8.57.1", 44 | "eslint-config-prettier": "9.0.0", 45 | "eslint-import-resolver-typescript": "3.6.1", 46 | "eslint-plugin-import": "2.28.1", 47 | "eslint-plugin-unicorn": "48.0.1", 48 | "nano-staged": "0.8.0", 49 | "nx": "20.6.2", 50 | "prettier": "3.5.3", 51 | "simple-git-hooks": "2.12.1", 52 | "syncpack": "13.0.3", 53 | "ts-node": "10.9.1", 54 | "typescript": "5.7.3", 55 | "vite": "6.2.7", 56 | "vitest": "3.0.9" 57 | }, 58 | "dependencies": { 59 | "@azure/data-tables": "^13.2.2", 60 | "@azure/identity": "^4.5.0", 61 | "@rsbuild/core": "^1.3.3", 62 | "@upstash/redis": "^1.34.6", 63 | "acorn": "^8.14.1", 64 | "ci-info": "^4.2.0", 65 | "cli-table3": "^0.6.5", 66 | "esbuild": "^0.25.1", 67 | "find-up": "^7.0.0", 68 | "glob": "^11.0.1", 69 | "gzip-size": "^7.0.0", 70 | "picocolors": "^1.0.0", 71 | "pretty-bytes": "^6.0.0", 72 | "terser": "^5.16.0", 73 | "terser-webpack-plugin": "^5.3.1", 74 | "tslib": "^2.4.1", 75 | "webpack": "^5.94.0", 76 | "yargs": "^17.6.2" 77 | }, 78 | "resolutions": { 79 | "beachball@2.31.5": "patch:beachball@npm:2.31.5#.yarn/patches/beachball-npm-2.31.5-0e84ec4233.patch" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-esbuild", 3 | "entries": [ 4 | { 5 | "date": "Tue, 06 Aug 2024 20:40:08 GMT", 6 | "tag": "monosize-bundler-esbuild_v0.1.5", 7 | "version": "0.1.5", 8 | "comments": { 9 | "patch": [ 10 | { 11 | "author": "beachball", 12 | "package": "monosize-bundler-esbuild", 13 | "comment": "Bump monosize to v0.6.3", 14 | "commit": "e968bcd7b04e2530bcd1d55b9f2e328386d734a5" 15 | } 16 | ] 17 | } 18 | }, 19 | { 20 | "date": "Mon, 20 May 2024 09:09:44 GMT", 21 | "tag": "monosize-bundler-esbuild_v0.1.4", 22 | "version": "0.1.4", 23 | "comments": { 24 | "patch": [ 25 | { 26 | "author": "beachball", 27 | "package": "monosize-bundler-esbuild", 28 | "comment": "Bump monosize to v0.6.2", 29 | "commit": "14bfdbedaf05c02031df79f0523c36a91d3c9b20" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "date": "Fri, 17 May 2024 08:39:46 GMT", 36 | "tag": "monosize-bundler-esbuild_v0.1.3", 37 | "version": "0.1.3", 38 | "comments": { 39 | "patch": [ 40 | { 41 | "author": "beachball", 42 | "package": "monosize-bundler-esbuild", 43 | "comment": "Bump monosize to v0.6.1", 44 | "commit": "957bc0bf6ffb97f970ed165a102c6c08920d736a" 45 | } 46 | ] 47 | } 48 | }, 49 | { 50 | "date": "Thu, 16 May 2024 15:04:43 GMT", 51 | "tag": "monosize-bundler-esbuild_v0.1.2", 52 | "version": "0.1.2", 53 | "comments": { 54 | "patch": [ 55 | { 56 | "author": "beachball", 57 | "package": "monosize-bundler-esbuild", 58 | "comment": "Bump monosize to v0.6.0", 59 | "commit": "e037ff19e68dc2fc725f27b42822ae35729d49d1" 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "date": "Fri, 10 May 2024 09:04:46 GMT", 66 | "tag": "monosize-bundler-esbuild_v0.1.1", 67 | "version": "0.1.1", 68 | "comments": { 69 | "patch": [ 70 | { 71 | "author": "beachball", 72 | "package": "monosize-bundler-esbuild", 73 | "comment": "Bump monosize to v0.5.1", 74 | "commit": "a9fc15428b9a9c5b5a99b432a9d3a691217e8644" 75 | } 76 | ] 77 | } 78 | }, 79 | { 80 | "date": "Fri, 22 Mar 2024 14:27:41 GMT", 81 | "tag": "monosize-bundler-esbuild_v0.1.0", 82 | "version": "0.1.0", 83 | "comments": { 84 | "minor": [ 85 | { 86 | "author": "hochelmartin@gmail.com", 87 | "package": "monosize-bundler-esbuild", 88 | "commit": "93a2578e0f5e4a757a78525fe703d8bc5d6f0e86", 89 | "comment": "feat:BREAKING CHANGE - change create*Bundler api to accept callback handler only" 90 | }, 91 | { 92 | "author": "beachball", 93 | "package": "monosize-bundler-esbuild", 94 | "comment": "Bump monosize to v0.5.0", 95 | "commit": "93a2578e0f5e4a757a78525fe703d8bc5d6f0e86" 96 | } 97 | ] 98 | } 99 | }, 100 | { 101 | "date": "Sat, 16 Mar 2024 11:26:36 GMT", 102 | "tag": "monosize-bundler-esbuild_v0.0.2", 103 | "version": "0.0.2", 104 | "comments": { 105 | "patch": [ 106 | { 107 | "author": "olfedias@microsoft.com", 108 | "package": "monosize-bundler-esbuild", 109 | "commit": "4a10d13398a1f3e6f1f59f9f55d34b295d784dbc", 110 | "comment": "chore: initial release" 111 | }, 112 | { 113 | "author": "beachball", 114 | "package": "monosize-bundler-esbuild", 115 | "comment": "Bump monosize to v0.4.0", 116 | "commit": "4a10d13398a1f3e6f1f59f9f55d34b295d784dbc" 117 | } 118 | ] 119 | } 120 | } 121 | ] 122 | } 123 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - monosize-bundler-esbuild 2 | 3 | This log was last generated on Tue, 06 Aug 2024 20:40:08 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 0.1.5 8 | 9 | Tue, 06 Aug 2024 20:40:08 GMT 10 | 11 | ### Patches 12 | 13 | - Bump monosize to v0.6.3 14 | 15 | ## 0.1.4 16 | 17 | Mon, 20 May 2024 09:09:44 GMT 18 | 19 | ### Patches 20 | 21 | - Bump monosize to v0.6.2 22 | 23 | ## 0.1.3 24 | 25 | Fri, 17 May 2024 08:39:46 GMT 26 | 27 | ### Patches 28 | 29 | - Bump monosize to v0.6.1 30 | 31 | ## 0.1.2 32 | 33 | Thu, 16 May 2024 15:04:43 GMT 34 | 35 | ### Patches 36 | 37 | - Bump monosize to v0.6.0 38 | 39 | ## 0.1.1 40 | 41 | Fri, 10 May 2024 09:04:46 GMT 42 | 43 | ### Patches 44 | 45 | - Bump monosize to v0.5.1 46 | 47 | ## 0.1.0 48 | 49 | Fri, 22 Mar 2024 14:27:41 GMT 50 | 51 | ### Minor changes 52 | 53 | - feat:BREAKING CHANGE - change create*Bundler api to accept callback handler only (hochelmartin@gmail.com) 54 | - Bump monosize to v0.5.0 55 | 56 | ## 0.0.2 57 | 58 | Sat, 16 Mar 2024 11:26:36 GMT 59 | 60 | ### Patches 61 | 62 | - chore: initial release (olfedias@microsoft.com) 63 | - Bump monosize to v0.4.0 64 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/README.md: -------------------------------------------------------------------------------- 1 | # monosize-bundler-esbuild 2 | 3 | ## Installation 4 | 5 | ```sh 6 | # npm 7 | npm install monosize-bundler-esbuild --save-dev 8 | # yarn 9 | yarn add monosize-bundler-esbuild --dev 10 | ``` 11 | 12 | ## Configuration 13 | 14 | You need to update your `monosize.config.mjs` to use `monosize-bundler-esbuild`: 15 | 16 | ```js 17 | // monosize.config.mjs 18 | import esbuildBundler from 'monosize-bundler-esbuild'; 19 | 20 | export default { 21 | // ... 22 | bundler: esbuildBundler(config => { 23 | // customize config here 24 | return config; 25 | }), 26 | }; 27 | ``` 28 | 29 | `esbuildBundler` is a function that accepts a callback to customize the configuration. The callback receives the default esbuild configuration and should return the updated configuration. 30 | 31 | ### Customizing configuration 32 | 33 | You can customize the configuration by modifying the default configuration: 34 | 35 | ```js 36 | // monosize.config.mjs 37 | import esbuildBundler from 'monosize-bundler-esbuild'; 38 | 39 | export default { 40 | // ... 41 | bundler: esbuildBundler(config => { 42 | config.loader = { 43 | ...config.loader, 44 | '.svg': 'file', 45 | }; 46 | 47 | return config; 48 | }), 49 | }; 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-esbuild", 3 | "version": "0.1.5", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/monosize", 8 | "directory": "packages/monosize-bundler-esbuild" 9 | }, 10 | "types": "./src/index.d.mts", 11 | "dependencies": { 12 | "esbuild": "^0.25.1", 13 | "monosize": "^0.6.3", 14 | "tslib": "^2.4.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-esbuild", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/monosize-bundler-esbuild/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "dependsOn": ["^build"], 10 | "options": { 11 | "outputPath": "dist/packages/monosize-bundler-esbuild", 12 | "main": "packages/monosize-bundler-esbuild/src/index.ts", 13 | "tsConfig": "packages/monosize-bundler-esbuild/tsconfig.lib.json", 14 | "outputFileExtensionForEsm": ".mjs", 15 | "assets": [ 16 | "packages/monosize-bundler-esbuild/README.md", 17 | { 18 | "glob": "LICENSE.md", 19 | "input": ".", 20 | "output": "." 21 | } 22 | ] 23 | } 24 | }, 25 | "lint": { 26 | "executor": "@nx/eslint:lint", 27 | "outputs": ["{options.outputFile}"] 28 | }, 29 | "test": { 30 | "executor": "@nx/vite:test", 31 | "outputs": ["{workspaceRoot}/coverage/packages/monosize-bundler-esbuild"], 32 | "options": { 33 | "passWithNoTests": true 34 | } 35 | } 36 | }, 37 | "tags": [] 38 | } 39 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/src/createEsbuildBundler.mts: -------------------------------------------------------------------------------- 1 | import type { BundlerAdapter } from 'monosize'; 2 | 3 | import { runEsbuild } from './runEsbuild.mjs'; 4 | import type { EsbuildBundlerOptions } from './types.mjs'; 5 | 6 | const DEFAULT_CONFIG_ENHANCER: EsbuildBundlerOptions = config => config; 7 | 8 | export function createEsbuildBundler(configEnhancerCallback = DEFAULT_CONFIG_ENHANCER): BundlerAdapter { 9 | return { 10 | buildFixture: async function (options) { 11 | const { fixturePath, quiet } = options; 12 | const outputPath = fixturePath.replace(/\.fixture.js$/, '.output.js'); 13 | 14 | await runEsbuild({ 15 | enhanceConfig: configEnhancerCallback, 16 | fixturePath, 17 | outputPath, 18 | quiet, 19 | }); 20 | 21 | return { 22 | outputPath, 23 | }; 24 | }, 25 | name: 'esbuild', 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/src/createEsbuildBundler.test.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import tmp from 'tmp'; 3 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 4 | 5 | import { createEsbuildBundler } from './createEsbuildBundler.mjs'; 6 | 7 | async function setup(fixtureContent: string): Promise { 8 | const packageDir = tmp.dirSync({ 9 | prefix: 'buildFixture', 10 | unsafeCleanup: true, 11 | }); 12 | 13 | const spy = vitest.spyOn(process, 'cwd'); 14 | spy.mockReturnValue(packageDir.name); 15 | 16 | const fixtureDir = tmp.dirSync({ 17 | dir: packageDir.name, 18 | name: 'monosize', 19 | unsafeCleanup: true, 20 | }); 21 | const fixture = tmp.fileSync({ 22 | dir: fixtureDir.name, 23 | name: 'test.fixture.js', 24 | }); 25 | 26 | await fs.writeFile(fixture.name, fixtureContent); 27 | 28 | return fs.realpath(fixture.name); 29 | } 30 | 31 | const esbuildBundler = createEsbuildBundler(); 32 | 33 | describe('buildFixture', () => { 34 | beforeEach(() => { 35 | vitest.resetAllMocks(); 36 | }); 37 | 38 | it('builds fixtures', async () => { 39 | const fixturePath = await setup(` 40 | const hello = 'Hello world'; 41 | const world = 'World'; 42 | 43 | console.log(hello) 44 | `); 45 | const buildResult = await esbuildBundler.buildFixture({ 46 | debug: false, 47 | fixturePath, 48 | quiet: true, 49 | }); 50 | 51 | expect(buildResult.outputPath).toMatch(/monosize[\\|/]test\.output\.js/); 52 | expect(await fs.readFile(buildResult.outputPath, 'utf-8')).toMatchInlineSnapshot(` 53 | "(()=>{var o="Hello world";console.log(o);})(); 54 | " 55 | `); 56 | }); 57 | 58 | it('should throw on compilation errors', async () => { 59 | const fixturePath = await setup(`import something from 'unknown-pkg'`); 60 | 61 | await expect( 62 | esbuildBundler.buildFixture({ 63 | debug: false, 64 | fixturePath, 65 | quiet: true, 66 | }), 67 | ).rejects.toThrow(/Could not resolve "unknown-pkg"/); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/src/index.mts: -------------------------------------------------------------------------------- 1 | export { createEsbuildBundler as default } from './createEsbuildBundler.mjs'; 2 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/src/runEsbuild.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { build, type BuildOptions } from 'esbuild'; 4 | 5 | import { EsbuildBundlerOptions } from './types.mjs'; 6 | 7 | function createEsbuildConfig(fixturePath: string): BuildOptions { 8 | return { 9 | logLevel: 'silent', 10 | 11 | entryPoints: [fixturePath], 12 | 13 | minify: true, 14 | bundle: true, 15 | 16 | write: false, 17 | }; 18 | } 19 | 20 | type RunEsbuildOptions = { 21 | enhanceConfig: EsbuildBundlerOptions; 22 | 23 | fixturePath: string; 24 | outputPath: string; 25 | 26 | quiet: boolean; 27 | }; 28 | 29 | export async function runEsbuild(options: RunEsbuildOptions): Promise { 30 | const { enhanceConfig, fixturePath, outputPath } = options; 31 | const esbuildConfig = enhanceConfig(createEsbuildConfig(fixturePath)); 32 | 33 | const result = await build(esbuildConfig); 34 | const { outputFiles } = result; 35 | 36 | if (!outputFiles || outputFiles.length !== 1) { 37 | throw new Error('Expected exactly one output file'); 38 | } 39 | 40 | await fs.writeFile(outputPath, outputFiles[0].contents); 41 | 42 | return null; 43 | } 44 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/src/types.mts: -------------------------------------------------------------------------------- 1 | import type { BundlerAdapterFactoryConfig } from 'monosize'; 2 | import type { BuildOptions } from 'esbuild'; 3 | 4 | export type EsbuildBundlerOptions = BundlerAdapterFactoryConfig; 5 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["environment", "node"] 7 | }, 8 | "include": ["**/*.mts"], 9 | "exclude": ["**/*.test.mts", "vite.config.mts", "**/__fixture__/", "**/__mocks__/"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["vitest/importMeta", "vite/client", "environment", "node", "vitest"] 6 | }, 7 | "include": ["vite.config.mts", "src/**/*.test.mts", "src/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/monosize-bundler-esbuild/vite.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | 6 | export default defineConfig({ 7 | cacheDir: '../../node_modules/.vite/monosize-bundler-esbuild', 8 | plugins: [nxViteTsPaths()], 9 | 10 | test: { 11 | reporters: ['default'], 12 | include: ['src/**/*.test.mts'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/README.md: -------------------------------------------------------------------------------- 1 | # monosize-bundler-rsbuild 2 | 3 | ## Installation 4 | 5 | ```sh 6 | # npm 7 | npm install monosize-bundler-rsbuild --save-dev 8 | # yarn 9 | yarn add monosize-bundler-rsbuild --dev 10 | ``` 11 | 12 | ## Configuration 13 | 14 | You need to update your `monosize.config.mjs` to use `monosize-bundler-rsbuild`: 15 | 16 | ```js 17 | // monosize.config.mjs 18 | import rsbuildBundler from 'monosize-bundler-rsbuild'; 19 | 20 | export default { 21 | // ... 22 | bundler: rsbuildBundler(config => { 23 | // customize config here 24 | return config; 25 | }), 26 | }; 27 | ``` 28 | 29 | `rsbuildBundler` is a function that accepts a callback to customize the rsbuild configuration. The callback receives the default rsbuild configuration and should return the updated configuration. 30 | 31 | ### Customizing configuration 32 | 33 | You can customize the configuration by modifying the default configuration: 34 | 35 | ```js 36 | // monosize.config.mjs 37 | import rsbuildBundler from 'monosize-bundler-rsbuild'; 38 | 39 | export default { 40 | // ... 41 | bundler: rsbuildBundler(config => { 42 | config.resolve ??= {}; 43 | config.resolve.alias = { 44 | ...config.resolve.alias, 45 | 'some-package': 'some-package/dist/some-package.esm.js', 46 | }; 47 | 48 | return config; 49 | }), 50 | }; 51 | ``` 52 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-rsbuild", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/monosize", 8 | "directory": "packages/monosize-bundler-rsbuild" 9 | }, 10 | "types": "./src/index.d.mts", 11 | "dependencies": { 12 | "@rsbuild/core": "^1.3.3", 13 | "monosize": "^0.6.3", 14 | "tslib": "^2.4.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-rsbuild", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/monosize-bundler-rsbuild/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "dependsOn": ["^build"], 10 | "options": { 11 | "outputPath": "dist/packages/monosize-bundler-rsbuild", 12 | "main": "packages/monosize-bundler-rsbuild/src/index.ts", 13 | "tsConfig": "packages/monosize-bundler-rsbuild/tsconfig.lib.json", 14 | "outputFileExtensionForEsm": ".mjs", 15 | "assets": [ 16 | "packages/monosize-bundler-rsbuild/README.md", 17 | { 18 | "glob": "LICENSE.md", 19 | "input": ".", 20 | "output": "." 21 | } 22 | ] 23 | } 24 | }, 25 | "lint": { 26 | "executor": "@nx/eslint:lint", 27 | "outputs": ["{options.outputFile}"] 28 | }, 29 | "test": { 30 | "executor": "@nx/vite:test", 31 | "outputs": ["{workspaceRoot}/coverage/packages/monosize-bundler-rsbuild"], 32 | "options": { 33 | "passWithNoTests": true 34 | } 35 | } 36 | }, 37 | "tags": [] 38 | } 39 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/src/__snapshots__/createRsbuildBundler.test.mts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`buildFixture > builds fixtures 1`] = ` 4 | "(() => { 5 | var r = {}, 6 | o = {}; 7 | function e(t) { 8 | var n = o[t]; 9 | if (void 0 !== n) return n.exports; 10 | var s = (o[t] = { exports: {} }); 11 | return r[t](s, s.exports, e), s.exports; 12 | } 13 | (e.rv = () => "0.0.0"), 14 | (e.ruid = "bundler=rspack@0.0.0"), 15 | console.log("Hello"); 16 | })(); 17 | " 18 | `; 19 | 20 | exports[`buildFixture > debug mode > does not output additional files when disabled 1`] = ` 21 | "(() => { 22 | var r = {}, 23 | o = {}; 24 | function e(t) { 25 | var n = o[t]; 26 | if (void 0 !== n) return n.exports; 27 | var u = (o[t] = { exports: {} }); 28 | return r[t](u, u.exports, e), u.exports; 29 | } 30 | (e.rv = () => "0.0.0"), (e.ruid = "bundler=rspack@0.0.0"); 31 | let t = { foo: "foo" }; 32 | console.log(function () { 33 | return t.foo; 34 | }); 35 | })(); 36 | " 37 | `; 38 | 39 | exports[`buildFixture > debug mode > provides partially minified output when enabled 1`] = ` 40 | "(() => { 41 | var r = {}, 42 | o = {}; 43 | function e(t) { 44 | var n = o[t]; 45 | if (void 0 !== n) return n.exports; 46 | var u = (o[t] = { exports: {} }); 47 | return r[t](u, u.exports, e), u.exports; 48 | } 49 | (e.rv = () => "0.0.0"), (e.ruid = "bundler=rspack@0.0.0"); 50 | let t = { foo: "foo" }; 51 | console.log(function () { 52 | return t.foo; 53 | }); 54 | })(); 55 | " 56 | `; 57 | 58 | exports[`buildFixture > debug mode > provides partially minified output when enabled 2`] = ` 59 | "(() => { 60 | var __webpack_modules__ = {}; 61 | var __webpack_module_cache__ = {}; 62 | function __webpack_require__(moduleId) { 63 | var cachedModule = __webpack_module_cache__[moduleId]; 64 | if (cachedModule !== undefined) { 65 | return cachedModule.exports; 66 | } 67 | var module = (__webpack_module_cache__[moduleId] = { exports: {} }); 68 | __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 69 | return module.exports; 70 | } 71 | (() => { 72 | __webpack_require__.rv = () => "0.0.0"; 73 | })(); 74 | (() => { 75 | __webpack_require__.ruid = "bundler=rspack@0.0.0"; 76 | })(); 77 | const tokens = { foo: "foo", bar: "bar" }; 78 | function foo() { 79 | return tokens.foo; 80 | } 81 | const bar = 1; 82 | console.log(foo); 83 | })(); 84 | " 85 | `; 86 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/src/createRsbuildBundler.mts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { createRsbuild, type EnvironmentConfig, type RsbuildConfig, logger } from '@rsbuild/core'; 3 | import type { BundlerAdapter, BundlerAdapterFactoryConfig } from 'monosize'; 4 | 5 | const DEFAULT_CONFIG_ENHANCER: BundlerAdapterFactoryConfig = config => config; 6 | 7 | export function createEnvironmentConfig(params: { 8 | fixturePath: string; 9 | outputPath: string; 10 | debugOutputPath?: string; 11 | }): Record { 12 | const { fixturePath, outputPath, debugOutputPath } = params; 13 | const environmentConfig: EnvironmentConfig = { 14 | source: { 15 | entry: { 16 | index: fixturePath, 17 | }, 18 | }, 19 | 20 | output: { 21 | externals: { 22 | react: 'React', 23 | 'react-dom': 'ReactDOM', 24 | }, 25 | target: 'web', 26 | 27 | emitAssets: false, 28 | 29 | filename: { 30 | js: path.basename(outputPath), 31 | }, 32 | distPath: { 33 | root: path.dirname(outputPath), 34 | js: './', 35 | }, 36 | 37 | minify: true, 38 | }, 39 | 40 | performance: { 41 | chunkSplit: { 42 | strategy: 'all-in-one', 43 | }, 44 | }, 45 | }; 46 | 47 | return { 48 | default: environmentConfig, 49 | 50 | ...(debugOutputPath && { 51 | debug: { 52 | ...environmentConfig, 53 | 54 | output: { 55 | ...environmentConfig.output, 56 | filename: { 57 | js: path.basename(debugOutputPath), 58 | }, 59 | minify: false, 60 | }, 61 | }, 62 | }), 63 | }; 64 | } 65 | 66 | export function createRsbuildBundler(configEnhancerCallback = DEFAULT_CONFIG_ENHANCER): BundlerAdapter { 67 | return { 68 | buildFixture: async function (options) { 69 | const { debug, fixturePath } = options; 70 | 71 | // Silence the default logger 72 | logger.level = 'error'; 73 | 74 | const rootDir = path.dirname(fixturePath); 75 | const artifactsDir = path.join(rootDir, 'dist'); 76 | const fixtureName = path.basename(fixturePath); 77 | 78 | const outputPath = path.join(artifactsDir, fixtureName.replace(/\.fixture\.js$/, '.output.js')); 79 | const debugOutputPath = path.join(artifactsDir, fixtureName.replace(/\.fixture.js$/, '.debug.js')); 80 | 81 | const rsbuild = await createRsbuild({ 82 | loadEnv: false, 83 | rsbuildConfig: configEnhancerCallback({ 84 | root: rootDir, 85 | mode: 'production', 86 | dev: { progressBar: false }, 87 | environments: createEnvironmentConfig({ 88 | fixturePath, 89 | outputPath, 90 | 91 | ...(debug && { debugOutputPath }), 92 | }), 93 | }), 94 | }); 95 | const buildResult = await rsbuild.build({ watch: false }); 96 | 97 | await buildResult.close(); 98 | 99 | return { 100 | outputPath, 101 | ...(debug && { debugOutputPath }), 102 | }; 103 | }, 104 | 105 | name: 'Rsbuild', 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/src/createRsbuildBundler.test.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import prettier from 'prettier'; 3 | import tmp from 'tmp'; 4 | import { rspackVersion } from '@rspack/core'; 5 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 6 | 7 | import { createEnvironmentConfig, createRsbuildBundler } from './createRsbuildBundler.mjs'; 8 | 9 | async function setup(content: string): Promise { 10 | const packageDir = tmp.dirSync({ 11 | prefix: 'buildFixture', 12 | unsafeCleanup: true, 13 | }); 14 | 15 | const spy = vitest.spyOn(process, 'cwd'); 16 | spy.mockReturnValue(packageDir.name); 17 | 18 | const fixtureDir = tmp.dirSync({ 19 | dir: packageDir.name, 20 | name: 'monosize', 21 | unsafeCleanup: true, 22 | }); 23 | const fixtureFile = tmp.fileSync({ 24 | dir: fixtureDir.name, 25 | name: 'test.fixture.js', 26 | }); 27 | 28 | await fs.promises.writeFile(fixtureFile.name, content); 29 | 30 | return fixtureFile.name; 31 | } 32 | 33 | async function prepareOutput(outputPath: string): Promise { 34 | const content = fs.readFileSync(outputPath, 'utf-8'); 35 | const formattedContent = await prettier.format(content, { 36 | filepath: outputPath, 37 | }); 38 | 39 | return formattedContent.replace(new RegExp(rspackVersion, 'g'), '0.0.0'); 40 | } 41 | 42 | describe('createEnvironmentConfig', () => { 43 | it('creates a config with the correct environment', () => { 44 | expect( 45 | createEnvironmentConfig({ 46 | fixturePath: '/workspace/fixtures/my-fixture.js', 47 | outputPath: '/workspace/dist/my-fixture.js', 48 | }), 49 | ).toMatchInlineSnapshot(` 50 | { 51 | "default": { 52 | "output": { 53 | "distPath": { 54 | "js": "./", 55 | "root": "/workspace/dist", 56 | }, 57 | "emitAssets": false, 58 | "externals": { 59 | "react": "React", 60 | "react-dom": "ReactDOM", 61 | }, 62 | "filename": { 63 | "js": "my-fixture.js", 64 | }, 65 | "minify": true, 66 | "target": "web", 67 | }, 68 | "performance": { 69 | "chunkSplit": { 70 | "strategy": "all-in-one", 71 | }, 72 | }, 73 | "source": { 74 | "entry": { 75 | "index": "/workspace/fixtures/my-fixture.js", 76 | }, 77 | }, 78 | }, 79 | } 80 | `); 81 | }); 82 | }); 83 | 84 | describe('buildFixture', () => { 85 | const rsbuildBundler = createRsbuildBundler(config => { 86 | // Specific config to get minification output consistent on *nix/Windows 87 | 88 | for (const environment in config.environments) { 89 | if (environment === 'debug') { 90 | config.environments['debug'] ??= {}; 91 | config.environments['debug'].output ??= {}; 92 | config.environments['debug'].output.minify = { 93 | js: true, 94 | jsOptions: { 95 | minimizerOptions: { 96 | compress: false, 97 | mangle: false, 98 | minify: true, 99 | }, 100 | }, 101 | }; 102 | } 103 | } 104 | 105 | return config; 106 | }); 107 | 108 | beforeEach(() => { 109 | vitest.resetAllMocks(); 110 | }); 111 | 112 | it('builds fixtures', async () => { 113 | const fixturePath = await setup(`const hello = 'Hello'; console.log(hello);`); 114 | const result = await rsbuildBundler.buildFixture({ 115 | debug: false, 116 | fixturePath, 117 | quiet: true, 118 | }); 119 | const output = await prepareOutput(result.outputPath); 120 | 121 | expect(result.outputPath).toMatch(/monosize[\\|/]dist[\\|/]test\.output\.js/); 122 | expect(output).toMatchSnapshot(); 123 | }); 124 | 125 | it('should throw on compilation errors', async () => { 126 | const fixturePath = await setup(`console..log(hello);`); 127 | 128 | await expect( 129 | rsbuildBundler.buildFixture({ 130 | debug: false, 131 | fixturePath, 132 | quiet: true, 133 | }), 134 | ).rejects.toMatchInlineSnapshot(`[Error: Rspack build failed.]`); 135 | }); 136 | 137 | describe('debug mode', () => { 138 | it('does not output additional files when disabled', async () => { 139 | const fixturePath = await setup(` 140 | const tokens = { foo: 'foo', bar: 'bar' }; 141 | function foo () { return tokens.foo; } 142 | const bar = 1; 143 | console.log(foo); 144 | `); 145 | const buildResult = await rsbuildBundler.buildFixture({ 146 | debug: false, 147 | fixturePath, 148 | quiet: true, 149 | }); 150 | const output = await prepareOutput(buildResult.outputPath); 151 | 152 | expect(buildResult.outputPath).toMatch(/monosize[\\|/]dist[\\|/]test\.output\.js/); 153 | expect(buildResult.debugOutputPath).toBeUndefined(); 154 | 155 | expect(output).toMatchSnapshot(); 156 | }); 157 | 158 | it('provides partially minified output when enabled', async () => { 159 | const fixturePath = await setup(` 160 | const tokens = { 161 | foo: 'foo', 162 | bar: 'bar', 163 | }; 164 | function foo () { return tokens.foo; } 165 | const bar = 1; 166 | console.log(foo); 167 | `); 168 | const buildResult = await rsbuildBundler.buildFixture({ 169 | debug: true, 170 | fixturePath, 171 | quiet: true, 172 | }); 173 | 174 | expect(buildResult.outputPath).toMatch(/monosize[\\|/]dist[\\|/]test\.output\.js/); 175 | expect(buildResult.debugOutputPath).toMatch(/monosize[\\|/]dist[\\|/]test\.debug\.js/); 176 | 177 | const output = await prepareOutput(buildResult.outputPath); 178 | const debugOutput = await prepareOutput(buildResult.debugOutputPath as string); 179 | 180 | expect(output).toMatchSnapshot(); 181 | // Output should contain the original variable names 182 | expect(debugOutput).toMatchSnapshot(); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/src/index.mts: -------------------------------------------------------------------------------- 1 | export { createRsbuildBundler as default } from './createRsbuildBundler.mjs'; 2 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["environment", "node"] 7 | }, 8 | "include": ["**/*.mts"], 9 | "exclude": ["**/*.test.mts", "vite.config.mts", "**/__fixture__/", "**/__mocks__/"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["vitest/importMeta", "vite/client", "environment", "node", "vitest"] 6 | }, 7 | "include": ["vite.config.mts", "src/**/*.test.mts", "src/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/monosize-bundler-rsbuild/vite.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | 6 | export default defineConfig({ 7 | cacheDir: '../../node_modules/.vite/monosize-bundler-rspack', 8 | plugins: [nxViteTsPaths()], 9 | 10 | test: { 11 | reporters: ['default'], 12 | include: ['src/**/*.test.mts'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-webpack", 3 | "entries": [ 4 | { 5 | "date": "Thu, 07 Nov 2024 14:43:21 GMT", 6 | "tag": "monosize-bundler-webpack_v0.1.6", 7 | "version": "0.1.6", 8 | "comments": { 9 | "patch": [ 10 | { 11 | "author": "olfedias@microsoft.com", 12 | "package": "monosize-bundler-webpack", 13 | "commit": "3e278dff488fd25b33b4612d645dc1a199c7d62d", 14 | "comment": "chore: bump webpack" 15 | } 16 | ] 17 | } 18 | }, 19 | { 20 | "date": "Tue, 06 Aug 2024 20:40:08 GMT", 21 | "tag": "monosize-bundler-webpack_v0.1.5", 22 | "version": "0.1.5", 23 | "comments": { 24 | "patch": [ 25 | { 26 | "author": "beachball", 27 | "package": "monosize-bundler-webpack", 28 | "comment": "Bump monosize to v0.6.3", 29 | "commit": "e968bcd7b04e2530bcd1d55b9f2e328386d734a5" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "date": "Mon, 20 May 2024 09:09:44 GMT", 36 | "tag": "monosize-bundler-webpack_v0.1.4", 37 | "version": "0.1.4", 38 | "comments": { 39 | "patch": [ 40 | { 41 | "author": "beachball", 42 | "package": "monosize-bundler-webpack", 43 | "comment": "Bump monosize to v0.6.2", 44 | "commit": "14bfdbedaf05c02031df79f0523c36a91d3c9b20" 45 | } 46 | ] 47 | } 48 | }, 49 | { 50 | "date": "Fri, 17 May 2024 08:39:46 GMT", 51 | "tag": "monosize-bundler-webpack_v0.1.3", 52 | "version": "0.1.3", 53 | "comments": { 54 | "patch": [ 55 | { 56 | "author": "beachball", 57 | "package": "monosize-bundler-webpack", 58 | "comment": "Bump monosize to v0.6.1", 59 | "commit": "957bc0bf6ffb97f970ed165a102c6c08920d736a" 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "date": "Thu, 16 May 2024 15:04:43 GMT", 66 | "tag": "monosize-bundler-webpack_v0.1.2", 67 | "version": "0.1.2", 68 | "comments": { 69 | "patch": [ 70 | { 71 | "author": "beachball", 72 | "package": "monosize-bundler-webpack", 73 | "comment": "Bump monosize to v0.6.0", 74 | "commit": "e037ff19e68dc2fc725f27b42822ae35729d49d1" 75 | } 76 | ] 77 | } 78 | }, 79 | { 80 | "date": "Fri, 10 May 2024 09:04:46 GMT", 81 | "tag": "monosize-bundler-webpack_v0.1.1", 82 | "version": "0.1.1", 83 | "comments": { 84 | "patch": [ 85 | { 86 | "author": "beachball", 87 | "package": "monosize-bundler-webpack", 88 | "comment": "Bump monosize to v0.5.1", 89 | "commit": "a9fc15428b9a9c5b5a99b432a9d3a691217e8644" 90 | } 91 | ] 92 | } 93 | }, 94 | { 95 | "date": "Fri, 22 Mar 2024 14:27:41 GMT", 96 | "tag": "monosize-bundler-webpack_v0.1.0", 97 | "version": "0.1.0", 98 | "comments": { 99 | "minor": [ 100 | { 101 | "author": "hochelmartin@gmail.com", 102 | "package": "monosize-bundler-webpack", 103 | "commit": "93a2578e0f5e4a757a78525fe703d8bc5d6f0e86", 104 | "comment": "feat:BREAKING CHANGE - change create*Bundler api to accept callback handler only" 105 | }, 106 | { 107 | "author": "beachball", 108 | "package": "monosize-bundler-webpack", 109 | "comment": "Bump monosize to v0.5.0", 110 | "commit": "93a2578e0f5e4a757a78525fe703d8bc5d6f0e86" 111 | } 112 | ] 113 | } 114 | }, 115 | { 116 | "date": "Sat, 16 Mar 2024 11:26:36 GMT", 117 | "tag": "monosize-bundler-webpack_v0.0.2", 118 | "version": "0.0.2", 119 | "comments": { 120 | "patch": [ 121 | { 122 | "author": "olfedias@microsoft.com", 123 | "package": "monosize-bundler-webpack", 124 | "commit": "4a10d13398a1f3e6f1f59f9f55d34b295d784dbc", 125 | "comment": "chore: initial release" 126 | }, 127 | { 128 | "author": "beachball", 129 | "package": "monosize-bundler-webpack", 130 | "comment": "Bump monosize to v0.4.0", 131 | "commit": "4a10d13398a1f3e6f1f59f9f55d34b295d784dbc" 132 | } 133 | ] 134 | } 135 | } 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - monosize-bundler-webpack 2 | 3 | This log was last generated on Thu, 07 Nov 2024 14:43:21 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 0.1.6 8 | 9 | Thu, 07 Nov 2024 14:43:21 GMT 10 | 11 | ### Patches 12 | 13 | - chore: bump webpack (olfedias@microsoft.com) 14 | 15 | ## 0.1.5 16 | 17 | Tue, 06 Aug 2024 20:40:08 GMT 18 | 19 | ### Patches 20 | 21 | - Bump monosize to v0.6.3 22 | 23 | ## 0.1.4 24 | 25 | Mon, 20 May 2024 09:09:44 GMT 26 | 27 | ### Patches 28 | 29 | - Bump monosize to v0.6.2 30 | 31 | ## 0.1.3 32 | 33 | Fri, 17 May 2024 08:39:46 GMT 34 | 35 | ### Patches 36 | 37 | - Bump monosize to v0.6.1 38 | 39 | ## 0.1.2 40 | 41 | Thu, 16 May 2024 15:04:43 GMT 42 | 43 | ### Patches 44 | 45 | - Bump monosize to v0.6.0 46 | 47 | ## 0.1.1 48 | 49 | Fri, 10 May 2024 09:04:46 GMT 50 | 51 | ### Patches 52 | 53 | - Bump monosize to v0.5.1 54 | 55 | ## 0.1.0 56 | 57 | Fri, 22 Mar 2024 14:27:41 GMT 58 | 59 | ### Minor changes 60 | 61 | - feat:BREAKING CHANGE - change create*Bundler api to accept callback handler only (hochelmartin@gmail.com) 62 | - Bump monosize to v0.5.0 63 | 64 | ## 0.0.2 65 | 66 | Sat, 16 Mar 2024 11:26:36 GMT 67 | 68 | ### Patches 69 | 70 | - chore: initial release (olfedias@microsoft.com) 71 | - Bump monosize to v0.4.0 72 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/README.md: -------------------------------------------------------------------------------- 1 | # monosize-bundler-webpack 2 | 3 | ## Installation 4 | 5 | ```sh 6 | # npm 7 | npm install monosize-bundler-webpack --save-dev 8 | # yarn 9 | yarn add monosize-bundler-webpack --dev 10 | ``` 11 | 12 | ## Configuration 13 | 14 | You need to update your `monosize.config.mjs` to use `monosize-bundler-webpack`: 15 | 16 | ```js 17 | // monosize.config.mjs 18 | import webpackBundler from 'monosize-bundler-webpack'; 19 | 20 | export default { 21 | // ... 22 | bundler: webpackBundler(config => { 23 | // customize config here 24 | return config; 25 | }), 26 | }; 27 | ``` 28 | 29 | `webpackBundler` is a function that accepts a callback to customize the webpack configuration. The callback receives the default webpack configuration and should return the updated configuration. 30 | 31 | ### Customizing configuration 32 | 33 | You can customize the configuration by modifying the default configuration: 34 | 35 | ```js 36 | // monosize.config.mjs 37 | import webpackBundler from 'monosize-bundler-webpack'; 38 | 39 | export default { 40 | // ... 41 | bundler: webpackBundler(config => { 42 | config.resolve.alias = { 43 | ...config.resolve.alias, 44 | 'some-package': 'some-package/dist/some-package.esm.js', 45 | }; 46 | 47 | return config; 48 | }), 49 | }; 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-webpack", 3 | "version": "0.1.6", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/monosize", 8 | "directory": "packages/monosize-bundler-webpack" 9 | }, 10 | "types": "./src/index.d.mts", 11 | "dependencies": { 12 | "monosize": "^0.6.3", 13 | "terser": "^5.16.0", 14 | "terser-webpack-plugin": "^5.3.1", 15 | "tslib": "^2.4.1", 16 | "webpack": "^5.94.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-bundler-webpack", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/monosize-bundler-webpack/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "dependsOn": ["^build"], 10 | "options": { 11 | "outputPath": "dist/packages/monosize-bundler-webpack", 12 | "main": "packages/monosize-bundler-webpack/src/index.ts", 13 | "tsConfig": "packages/monosize-bundler-webpack/tsconfig.lib.json", 14 | "outputFileExtensionForEsm": ".mjs", 15 | "assets": [ 16 | "packages/monosize-bundler-webpack/README.md", 17 | { 18 | "glob": "LICENSE.md", 19 | "input": ".", 20 | "output": "." 21 | } 22 | ] 23 | } 24 | }, 25 | "lint": { 26 | "executor": "@nx/eslint:lint", 27 | "outputs": ["{options.outputFile}"] 28 | }, 29 | "test": { 30 | "executor": "@nx/vite:test", 31 | "outputs": ["{workspaceRoot}/coverage/packages/monosize-bundler-webpack"], 32 | "options": { 33 | "passWithNoTests": true 34 | } 35 | } 36 | }, 37 | "tags": [] 38 | } 39 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/src/createWebpackBundler.mts: -------------------------------------------------------------------------------- 1 | import type { BundlerAdapter } from 'monosize'; 2 | 3 | import { runTerser } from './runTerser.mjs'; 4 | import { runWebpack } from './runWebpack.mjs'; 5 | import type { WebpackBundlerOptions } from './types.mjs'; 6 | 7 | const DEFAULT_CONFIG_ENHANCER: WebpackBundlerOptions = config => config; 8 | 9 | export function createWebpackBundler(configEnhancerCallback = DEFAULT_CONFIG_ENHANCER): BundlerAdapter { 10 | return { 11 | buildFixture: async function (options) { 12 | const { debug, fixturePath, quiet } = options; 13 | 14 | const outputPath = fixturePath.replace(/\.fixture.js$/, '.output.js'); 15 | const debugOutputPath = fixturePath.replace(/\.fixture.js$/, '.debug.js'); 16 | 17 | await runWebpack({ 18 | enhanceConfig: configEnhancerCallback, 19 | fixturePath, 20 | outputPath, 21 | debug, 22 | quiet, 23 | }); 24 | 25 | if (debug) { 26 | await runTerser({ 27 | fixturePath, 28 | sourcePath: outputPath, 29 | outputPath, 30 | debugOutputPath, 31 | quiet, 32 | }); 33 | } 34 | 35 | return { 36 | outputPath, 37 | ...(debug && { 38 | debugOutputPath, 39 | }), 40 | }; 41 | }, 42 | name: 'Webpack', 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/src/createWebpackBundler.test.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import tmp from 'tmp'; 3 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 4 | 5 | import { createWebpackBundler } from './createWebpackBundler.mjs'; 6 | 7 | async function setup(fixtureContent: string): Promise { 8 | const packageDir = tmp.dirSync({ 9 | prefix: 'buildFixture', 10 | unsafeCleanup: true, 11 | }); 12 | 13 | const spy = vitest.spyOn(process, 'cwd'); 14 | spy.mockReturnValue(packageDir.name); 15 | 16 | const fixtureDir = tmp.dirSync({ 17 | dir: packageDir.name, 18 | name: 'monosize', 19 | unsafeCleanup: true, 20 | }); 21 | const fixture = tmp.fileSync({ 22 | dir: fixtureDir.name, 23 | name: 'test.fixture.js', 24 | }); 25 | 26 | await fs.promises.writeFile(fixture.name, fixtureContent); 27 | 28 | return fixture.name; 29 | } 30 | 31 | const webpackBundler = createWebpackBundler(config => { 32 | // Disable pathinfo to make the output deterministic in snapshots 33 | config.output ??= {}; 34 | config.output.pathinfo = false; 35 | 36 | return config; 37 | }); 38 | 39 | describe('buildFixture', () => { 40 | beforeEach(() => { 41 | vitest.resetAllMocks(); 42 | }); 43 | 44 | it('builds fixtures', async () => { 45 | const fixturePath = await setup(` 46 | const hello = 'Hello'; 47 | const world = 'world'; 48 | 49 | console.log(hello); 50 | `); 51 | const buildResult = await webpackBundler.buildFixture({ 52 | debug: false, 53 | fixturePath, 54 | quiet: true, 55 | }); 56 | 57 | expect(buildResult.outputPath).toMatch(/monosize[\\|/]test\.output\.js/); 58 | expect(await fs.promises.readFile(buildResult.outputPath, 'utf-8')).toMatchInlineSnapshot( 59 | `"console.log("Hello");"`, 60 | ); 61 | }); 62 | 63 | it('should throw on compilation errors', async () => { 64 | const fixturePath = await setup(`import something from 'unknown-pkg'`); 65 | await expect( 66 | webpackBundler.buildFixture({ 67 | debug: false, 68 | fixturePath, 69 | quiet: true, 70 | }), 71 | ).rejects.toBeDefined(); 72 | }); 73 | 74 | describe('debug mode', () => { 75 | it('does not output additional files when disabled', async () => { 76 | const fixturePath = await setup(` 77 | const tokens = { 78 | foo: 'foo', 79 | bar: 'bar', 80 | }; 81 | function foo () { return tokens.foo; } 82 | const bar = 1; 83 | 84 | console.log(foo); 85 | `); 86 | const buildResult = await webpackBundler.buildFixture({ 87 | debug: false, 88 | fixturePath, 89 | quiet: true, 90 | }); 91 | const output = await fs.promises.readFile(buildResult.outputPath, 'utf-8'); 92 | 93 | expect(buildResult.outputPath).toMatch(/monosize[\\|/]test\.output\.js/); 94 | expect(buildResult.debugOutputPath).toBeUndefined(); 95 | 96 | expect(output).toMatchInlineSnapshot(`"(()=>{const o="foo";console.log((function(){return o}))})();"`); 97 | }); 98 | 99 | it('provides partially minified output when enabled', async () => { 100 | const fixturePath = await setup(` 101 | const tokens = { 102 | foo: 'foo', 103 | bar: 'bar', 104 | }; 105 | function foo () { return tokens.foo; } 106 | const bar = 1; 107 | 108 | console.log(foo); 109 | `); 110 | const buildResult = await webpackBundler.buildFixture({ 111 | debug: true, 112 | fixturePath, 113 | quiet: true, 114 | }); 115 | 116 | expect(buildResult.outputPath).toMatch(/monosize[\\|/]test\.output\.js/); 117 | expect(buildResult.debugOutputPath).toMatch(/monosize[\\|/]test\.debug\.js/); 118 | 119 | const output = await fs.promises.readFile(buildResult.outputPath, 'utf-8'); 120 | const debugOutput = await fs.promises.readFile(buildResult.debugOutputPath!, 'utf-8'); 121 | 122 | expect(output).toMatchInlineSnapshot(`"(()=>{const o="foo";console.log((function(){return o}))})();"`); 123 | 124 | // Output should contain the original variable names 125 | expect(debugOutput).toMatchInlineSnapshot(` 126 | "/******/ (() => { 127 | // webpackBootstrap 128 | const tokens_foo = "foo"; 129 | console.log((function() { 130 | return tokens_foo; 131 | })); 132 | }) 133 | /******/ ();" 134 | `); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/src/index.mts: -------------------------------------------------------------------------------- 1 | export { createWebpackBundler as default } from './createWebpackBundler.mjs'; 2 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/src/runTerser.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { minify } from 'terser'; 3 | 4 | type RunTerserOptions = { 5 | fixturePath: string; 6 | 7 | sourcePath: string; 8 | outputPath: string; 9 | debugOutputPath: string; 10 | 11 | quiet: boolean; 12 | }; 13 | 14 | export async function runTerser(options: RunTerserOptions) { 15 | const { fixturePath, debugOutputPath, sourcePath, outputPath } = options; 16 | const sourceContent = await fs.promises.readFile(sourcePath, 'utf8'); 17 | 18 | // Performs only dead-code elimination 19 | const debugOutput = await minify( 20 | { [fixturePath]: sourceContent }, 21 | { 22 | compress: {}, 23 | mangle: false, 24 | format: { 25 | beautify: true, 26 | comments: true, 27 | preserve_annotations: true, 28 | }, 29 | sourceMap: false, 30 | }, 31 | ); 32 | // Performs full minification 33 | const minifiedOutput = await minify( 34 | { [fixturePath]: sourceContent }, 35 | { 36 | format: { 37 | comments: false, 38 | }, 39 | sourceMap: false, 40 | }, 41 | ); 42 | 43 | if (!debugOutput.code || !minifiedOutput.code) { 44 | throw new Error('Got an empty output from Terser, this is not expected...'); 45 | } 46 | 47 | await fs.promises.writeFile(debugOutputPath, debugOutput.code); 48 | await fs.promises.writeFile(outputPath, minifiedOutput.code); 49 | } 50 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/src/runTerser.test.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import tmp from 'tmp'; 4 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 5 | 6 | import { runTerser } from './runTerser.mjs'; 7 | 8 | async function setup(fixtureContent: string): Promise[0]> { 9 | const tmpDir = tmp.dirSync({ unsafeCleanup: true }); 10 | const sourcePath = path.resolve(tmpDir.name, 'test.fixture.js'); 11 | 12 | await fs.promises.writeFile(sourcePath, fixtureContent); 13 | 14 | return { 15 | fixturePath: 'test.fixture.js', // This is a placeholder, it's not used in the test 16 | quiet: true, 17 | 18 | sourcePath, 19 | debugOutputPath: path.resolve(tmpDir.name, 'test.debug.js'), 20 | outputPath: path.resolve(tmpDir.name, 'test.output.js'), 21 | }; 22 | } 23 | 24 | describe('runTerser', () => { 25 | beforeEach(() => { 26 | vitest.resetAllMocks(); 27 | }); 28 | 29 | it('performs minification', async () => { 30 | const options = await setup(` 31 | const hello = 'Hello'; 32 | const world = 'world'; 33 | 34 | console.log(hello); 35 | `); 36 | await runTerser(options); 37 | 38 | expect(await fs.promises.readFile(options.outputPath, 'utf-8')).toMatchInlineSnapshot( 39 | `"const hello="Hello",world="world";console.log(hello);"`, 40 | ); 41 | expect(await fs.promises.readFile(options.debugOutputPath, 'utf-8')).toMatchInlineSnapshot( 42 | ` 43 | "const hello = "Hello", world = "world"; 44 | 45 | console.log(hello);" 46 | `, 47 | ); 48 | }); 49 | 50 | it('should throw on compilation errors', async () => { 51 | const options = await setup(`import something from "unknown-pkg'`); 52 | await expect(runTerser(options)).rejects.toMatchInlineSnapshot('[SyntaxError: Unterminated string constant]'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/src/runWebpack.mts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import TerserWebpackPlugin from 'terser-webpack-plugin'; 3 | import webpack from 'webpack'; 4 | import type { Configuration as WebpackConfiguration } from 'webpack'; 5 | 6 | import { WebpackBundlerOptions } from './types.mjs'; 7 | 8 | function createWebpackConfig(fixturePath: string, outputPath: string, debug: boolean): WebpackConfiguration { 9 | return { 10 | name: 'client', 11 | target: 'web', 12 | mode: 'production', 13 | 14 | cache: { 15 | type: 'memory', 16 | }, 17 | externals: { 18 | react: 'React', 19 | 'react-dom': 'ReactDOM', 20 | }, 21 | 22 | entry: fixturePath, 23 | output: { 24 | filename: path.basename(outputPath), 25 | path: path.dirname(outputPath), 26 | 27 | ...(debug && { 28 | pathinfo: true, 29 | }), 30 | }, 31 | performance: { 32 | hints: false, 33 | }, 34 | optimization: { 35 | minimizer: [ 36 | new TerserWebpackPlugin({ 37 | extractComments: false, 38 | terserOptions: { 39 | format: { 40 | comments: false, 41 | }, 42 | }, 43 | }), 44 | ], 45 | 46 | // If debug mode is enabled, we want to disable minification and rely on Terser to produce a partially minified 47 | // file for debugging purposes 48 | ...(debug && { 49 | minimize: false, 50 | minimizer: [], 51 | }), 52 | }, 53 | 54 | ...(debug && { 55 | stats: { 56 | optimizationBailout: true, 57 | }, 58 | }), 59 | }; 60 | } 61 | 62 | type RunWebpackOptions = { 63 | enhanceConfig: WebpackBundlerOptions; 64 | 65 | fixturePath: string; 66 | outputPath: string; 67 | 68 | debug: boolean; 69 | quiet: boolean; 70 | }; 71 | 72 | export async function runWebpack(options: RunWebpackOptions): Promise { 73 | const { enhanceConfig, fixturePath, outputPath, debug } = options; 74 | const webpackConfig = enhanceConfig(createWebpackConfig(fixturePath, outputPath, debug)); 75 | 76 | return new Promise((resolve, reject) => { 77 | const compiler = webpack(webpackConfig); 78 | 79 | compiler.run((err, result) => { 80 | if (err) { 81 | reject(err); 82 | } 83 | if (result && result.hasErrors()) { 84 | reject(result.compilation.errors.join('\n')); 85 | } 86 | 87 | resolve(null); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/src/types.mts: -------------------------------------------------------------------------------- 1 | import type { BundlerAdapterFactoryConfig } from 'monosize'; 2 | import type { Configuration as WebpackConfiguration } from 'webpack'; 3 | 4 | export type WebpackBundlerOptions = BundlerAdapterFactoryConfig; 5 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["environment", "node"] 7 | }, 8 | "include": ["**/*.mts"], 9 | "exclude": ["**/*.test.mts", "vite.config.mts", "**/__fixture__/", "**/__mocks__/"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["vitest/importMeta", "vite/client", "environment", "node", "vitest"] 6 | }, 7 | "include": ["vite.config.mts", "src/**/*.test.mts", "src/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/monosize-bundler-webpack/vite.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | 6 | export default defineConfig({ 7 | cacheDir: '../../node_modules/.vite/monosize-bundler-webpack', 8 | plugins: [nxViteTsPaths()], 9 | 10 | test: { 11 | reporters: ['default'], 12 | include: ['src/**/*.test.mts'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - monosize-storage-azure 2 | 3 | This log was last generated on Thu, 07 Nov 2024 14:43:21 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 0.0.16 8 | 9 | Thu, 07 Nov 2024 14:43:21 GMT 10 | 11 | ### Patches 12 | 13 | - feat(monosize-storage-azure): add support for DefaultAzureCredential (hochelmartin@gmail.com) 14 | 15 | ## 0.0.15 16 | 17 | Tue, 06 Aug 2024 20:40:08 GMT 18 | 19 | ### Patches 20 | 21 | - feat: add support for workload identity authentication with azure pipelines. (tristan.watanabe@gmail.com) 22 | - Bump monosize to v0.6.3 23 | 24 | ## 0.0.14 25 | 26 | Mon, 20 May 2024 09:09:44 GMT 27 | 28 | ### Patches 29 | 30 | - Bump monosize to v0.6.2 31 | 32 | ## 0.0.13 33 | 34 | Fri, 17 May 2024 08:39:46 GMT 35 | 36 | ### Patches 37 | 38 | - Bump monosize to v0.6.1 39 | 40 | ## 0.0.12 41 | 42 | Thu, 16 May 2024 15:04:43 GMT 43 | 44 | ### Patches 45 | 46 | - Bump monosize to v0.6.0 47 | 48 | ## 0.0.11 49 | 50 | Fri, 10 May 2024 09:04:46 GMT 51 | 52 | ### Patches 53 | 54 | - Bump monosize to v0.5.1 55 | 56 | ## 0.0.10 57 | 58 | Fri, 22 Mar 2024 14:27:41 GMT 59 | 60 | ### Patches 61 | 62 | - Bump monosize to v0.5.0 63 | 64 | ## 0.0.9 65 | 66 | Sat, 16 Mar 2024 11:26:36 GMT 67 | 68 | ### Patches 69 | 70 | - Bump monosize to v0.4.0 71 | 72 | ## 0.0.8 73 | 74 | Wed, 21 Feb 2024 16:37:56 GMT 75 | 76 | ### Patches 77 | 78 | - Bump monosize to v0.3.0 79 | 80 | ## 0.0.7 81 | 82 | Tue, 20 Feb 2024 17:06:09 GMT 83 | 84 | ### Patches 85 | 86 | - Bump monosize to v0.2.2 87 | 88 | ## 0.0.6 89 | 90 | Wed, 31 Jan 2024 11:35:39 GMT 91 | 92 | ### Patches 93 | 94 | - feat(azure-adapter): split transactions by chunks (olfedias@microsoft.com) 95 | 96 | ## 0.0.5 97 | 98 | Fri, 22 Dec 2023 14:27:47 GMT 99 | 100 | ### Patches 101 | 102 | - Bump monosize to v0.2.1 103 | 104 | ## 0.0.4 105 | 106 | Fri, 22 Dec 2023 13:44:44 GMT 107 | 108 | ### Patches 109 | 110 | - chore: improve error reporting (olfedias@microsoft.com) 111 | - Bump monosize to v0.2.0 112 | 113 | ## 0.0.3 114 | 115 | Mon, 13 Feb 2023 12:55:07 GMT 116 | 117 | ### Patches 118 | 119 | - chore: enable typings (olfedias@microsoft.com) 120 | 121 | ## 0.0.2 122 | 123 | Mon, 13 Feb 2023 11:26:46 GMT 124 | 125 | ### Patches 126 | 127 | - feat: add adapter for Azure (olfedias@microsoft.com) 128 | - Bump monosize to v0.1.0 129 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/README.md: -------------------------------------------------------------------------------- 1 | # monosize-storage-azure 2 | 3 | This adapter is in alpha stage and is not intended for public usage. 4 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-storage-azure", 3 | "version": "0.0.16", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/monosize", 8 | "directory": "packages/monosize-storage-azure" 9 | }, 10 | "types": "./src/index.d.mts", 11 | "dependencies": { 12 | "@azure/data-tables": "^13.2.2", 13 | "@azure/identity": "^4.5.0", 14 | "monosize": "^0.6.3", 15 | "node-fetch": "^3.3.0", 16 | "picocolors": "^1.0.0", 17 | "tslib": "^2.4.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-storage-azure", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/monosize-storage-azure/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "dependsOn": ["^build"], 10 | "options": { 11 | "outputPath": "dist/packages/monosize-storage-azure", 12 | "main": "packages/monosize-storage-azure/src/index.ts", 13 | "tsConfig": "packages/monosize-storage-azure/tsconfig.lib.json", 14 | "outputFileExtensionForEsm": ".mjs", 15 | "assets": [ 16 | "packages/monosize-storage-azure/README.md", 17 | { 18 | "glob": "LICENSE.md", 19 | "input": ".", 20 | "output": "." 21 | } 22 | ] 23 | } 24 | }, 25 | "lint": { 26 | "executor": "@nx/eslint:lint", 27 | "outputs": ["{options.outputFile}"] 28 | }, 29 | "test": { 30 | "executor": "@nx/vite:test", 31 | "outputs": ["{workspaceRoot}/coverage/packages/monosize-storage-azure"], 32 | "options": { 33 | "passWithNoTests": true 34 | } 35 | } 36 | }, 37 | "tags": [] 38 | } 39 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/__fixture__/sampleReports.mts: -------------------------------------------------------------------------------- 1 | import type { BundleSizeReport } from 'monosize'; 2 | 3 | export const sampleReport: BundleSizeReport = [ 4 | { 5 | packageName: '@scope/foo-package', 6 | name: 'Foo', 7 | path: 'foo.fixture.js', 8 | minifiedSize: 1000, 9 | gzippedSize: 100, 10 | }, 11 | { 12 | packageName: '@scope/bar-package', 13 | name: 'Bar', 14 | path: 'bar.fixture.js', 15 | minifiedSize: 1000, 16 | gzippedSize: 100, 17 | }, 18 | ]; 19 | 20 | export const bigReport: BundleSizeReport = new Array(200).fill(null).map((_, index) => ({ 21 | packageName: '@scope/foo-package', 22 | name: `Entry [${index}]`, 23 | path: `foo-${index}.fixture.js`, 24 | minifiedSize: 1000, 25 | gzippedSize: 100, 26 | })); 27 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/createTableClient.mts: -------------------------------------------------------------------------------- 1 | import { AzureNamedKeyCredential, TableClient } from '@azure/data-tables'; 2 | import { AzurePipelinesCredential, DefaultAzureCredential } from '@azure/identity'; 3 | import type { AzureStorageConfig } from './types.mjs'; 4 | 5 | export function createTableClient(options: Required>): TableClient { 6 | const { authType, tableName } = options; 7 | 8 | const AZURE_STORAGE_TABLE_NAME = tableName; 9 | 10 | if (authType === 'AzureNamedKeyCredential') { 11 | const requiredEnvVars = ['BUNDLESIZE_ACCOUNT_NAME', 'BUNDLESIZE_ACCOUNT_KEY']; 12 | validateRequiredEnvVariables({ 13 | requiredEnvVars, 14 | authType, 15 | }); 16 | 17 | const AZURE_STORAGE_ACCOUNT = process.env['BUNDLESIZE_ACCOUNT_NAME'] as string; 18 | const AZURE_ACCOUNT_KEY = process.env['BUNDLESIZE_ACCOUNT_KEY'] as string; 19 | 20 | return new TableClient( 21 | `https://${AZURE_STORAGE_ACCOUNT}.table.core.windows.net`, 22 | AZURE_STORAGE_TABLE_NAME, 23 | new AzureNamedKeyCredential(AZURE_STORAGE_ACCOUNT, AZURE_ACCOUNT_KEY), 24 | ); 25 | } 26 | 27 | if (authType === 'AzurePipelinesCredential') { 28 | const requiredEnvVars = [ 29 | 'BUNDLESIZE_ACCOUNT_NAME', 30 | 'AZURE_TENANT_ID', 31 | 'AZURE_CLIENT_ID', 32 | 'AZURE_SERVICE_CONNECTION_ID', 33 | 'SYSTEM_ACCESSTOKEN', 34 | ]; 35 | validateRequiredEnvVariables({ 36 | requiredEnvVars, 37 | authType, 38 | }); 39 | 40 | const AZURE_STORAGE_ACCOUNT = process.env['BUNDLESIZE_ACCOUNT_NAME'] as string; 41 | const TENANT_ID = process.env['AZURE_TENANT_ID'] as string; 42 | const CLIENT_ID = process.env['AZURE_CLIENT_ID'] as string; 43 | const SERVICE_CONNECTION_ID = process.env['AZURE_SERVICE_CONNECTION_ID'] as string; 44 | const SYSTEM_ACCESSTOKEN = process.env['SYSTEM_ACCESSTOKEN'] as string; 45 | 46 | return new TableClient( 47 | `https://${AZURE_STORAGE_ACCOUNT}.table.core.windows.net`, 48 | AZURE_STORAGE_TABLE_NAME, 49 | new AzurePipelinesCredential(TENANT_ID, CLIENT_ID, SERVICE_CONNECTION_ID, SYSTEM_ACCESSTOKEN), 50 | ); 51 | } 52 | 53 | if (authType === 'DefaultAzureCredential') { 54 | // DefaultAzureCredential will obtain these from environment variables, thus why we need to assert on them while they are not used directly in code 55 | const requiredEnvVars = [ 56 | 'BUNDLESIZE_ACCOUNT_NAME', 57 | 'AZURE_TENANT_ID', 58 | 'AZURE_CLIENT_ID', 59 | 'AZURE_SERVICE_CONNECTION_ID', 60 | ]; 61 | validateRequiredEnvVariables({ 62 | requiredEnvVars, 63 | authType, 64 | }); 65 | 66 | const AZURE_STORAGE_ACCOUNT = process.env['BUNDLESIZE_ACCOUNT_NAME'] as string; 67 | const TENANT_ID = process.env['AZURE_TENANT_ID'] as string; 68 | 69 | return new TableClient( 70 | `https://${AZURE_STORAGE_ACCOUNT}.table.core.windows.net`, 71 | AZURE_STORAGE_TABLE_NAME, 72 | new DefaultAzureCredential({ tenantId: TENANT_ID }), 73 | ); 74 | } 75 | 76 | throw new Error(`monosize-storage-azure: "authType: ${authType}" is not supported.`); 77 | } 78 | 79 | function validateRequiredEnvVariables(options: { requiredEnvVars: string[]; authType: string }): void { 80 | const { requiredEnvVars, authType } = options; 81 | const missingEnvVars = requiredEnvVars.filter(envParamName => typeof process.env[envParamName] !== 'string'); 82 | 83 | if (missingEnvVars.length > 0) { 84 | throw new Error( 85 | `monosize-storage-azure: Missing required environment variable(s) for authType ${authType}: ${missingEnvVars.join( 86 | ', ', 87 | )} not in your process.env.`, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/createTableClient.test.mts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 2 | import { AzureNamedKeyCredential, TableClient } from '@azure/data-tables'; 3 | import { AzurePipelinesCredential, DefaultAzureCredential } from '@azure/identity'; 4 | import { createTableClient } from './createTableClient.mjs'; 5 | import type { AzureAuthenticationType } from './types.mjs'; 6 | 7 | vitest.mock('@azure/data-tables', () => { 8 | return { 9 | AzureNamedKeyCredential: vitest.fn(), 10 | TableClient: vitest.fn().mockImplementation(() => { 11 | return { 12 | createTable: vitest.fn(), 13 | deleteTable: vitest.fn(), 14 | }; 15 | }), 16 | }; 17 | }); 18 | 19 | vitest.mock('@azure/identity', () => { 20 | return { 21 | AzurePipelinesCredential: vitest.fn(), 22 | DefaultAzureCredential: vitest.fn(), 23 | }; 24 | }); 25 | 26 | describe('createTableClient', () => { 27 | beforeEach(() => { 28 | vitest.resetAllMocks(); 29 | vitest.unstubAllEnvs(); 30 | }); 31 | 32 | it('should create TableClient with AzureNamedKeyCredential', () => { 33 | vitest.stubEnv('BUNDLESIZE_ACCOUNT_NAME', 'test-account-name'); 34 | vitest.stubEnv('BUNDLESIZE_ACCOUNT_KEY', 'test-account-key'); 35 | 36 | const authType = 'AzureNamedKeyCredential'; 37 | const tableName = 'test-table'; 38 | createTableClient({ authType, tableName }); 39 | 40 | expect(AzureNamedKeyCredential).toHaveBeenCalledWith( 41 | process.env['BUNDLESIZE_ACCOUNT_NAME'] as string, 42 | process.env['BUNDLESIZE_ACCOUNT_KEY'] as string, 43 | ); 44 | 45 | expect(TableClient).toHaveBeenCalledWith( 46 | 'https://test-account-name.table.core.windows.net', 47 | tableName, 48 | expect.any(AzureNamedKeyCredential), 49 | ); 50 | }); 51 | 52 | it('should create TableClient with AzurePipelinesCredential', () => { 53 | vitest.stubEnv('BUNDLESIZE_ACCOUNT_NAME', 'test-account-name'); 54 | vitest.stubEnv('AZURE_TENANT_ID', 'test-tenant-id'); 55 | vitest.stubEnv('AZURE_CLIENT_ID', 'test-client-id'); 56 | vitest.stubEnv('AZURE_SERVICE_CONNECTION_ID', 'test-service-connection-id'); 57 | vitest.stubEnv('SYSTEM_ACCESSTOKEN', 'test-system-access-token'); 58 | vitest.stubEnv('SYSTEM_OIDCREQUESTURI', 'test-system-oidc-request-uri'); 59 | 60 | const authType = 'AzurePipelinesCredential'; 61 | const tableName = 'test-table'; 62 | createTableClient({ authType, tableName }); 63 | 64 | expect(AzurePipelinesCredential).toHaveBeenCalledWith( 65 | process.env['AZURE_TENANT_ID'] as string, 66 | process.env['AZURE_CLIENT_ID'] as string, 67 | process.env['AZURE_SERVICE_CONNECTION_ID'] as string, 68 | process.env['SYSTEM_ACCESSTOKEN'] as string, 69 | ); 70 | 71 | expect(TableClient).toHaveBeenCalledWith( 72 | 'https://test-account-name.table.core.windows.net', 73 | tableName, 74 | expect.any(AzurePipelinesCredential), 75 | ); 76 | }); 77 | 78 | it('should create TableClient with DefaultAzureCredential', () => { 79 | vitest.stubEnv('BUNDLESIZE_ACCOUNT_NAME', 'test-account-name'); 80 | vitest.stubEnv('AZURE_TENANT_ID', 'test-tenant-id'); 81 | vitest.stubEnv('AZURE_CLIENT_ID', 'test-client-id'); 82 | vitest.stubEnv('AZURE_SERVICE_CONNECTION_ID', 'test-service-connection-id'); 83 | vitest.stubEnv('SYSTEM_ACCESSTOKEN', 'test-system-access-token'); 84 | 85 | const authType = 'DefaultAzureCredential'; 86 | const tableName = 'test-table'; 87 | createTableClient({ authType, tableName }); 88 | 89 | expect(DefaultAzureCredential).toHaveBeenCalledWith({ tenantId: process.env['AZURE_TENANT_ID'] }); 90 | 91 | expect(TableClient).toHaveBeenCalledWith( 92 | 'https://test-account-name.table.core.windows.net', 93 | tableName, 94 | expect.any(DefaultAzureCredential), 95 | ); 96 | }); 97 | 98 | it('should throw an error for unsupported authType', () => { 99 | const authType = 'AzureNamedKeyCredentail' as AzureAuthenticationType; 100 | const tableName = 'test-table'; 101 | 102 | expect(() => createTableClient({ authType, tableName })).toThrow( 103 | `monosize-storage-azure: "authType: ${authType}" is not supported.`, 104 | ); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/getRemoteReport.mts: -------------------------------------------------------------------------------- 1 | import pc from 'picocolors'; 2 | import type { BundleSizeReportEntry, StorageAdapter } from 'monosize'; 3 | 4 | import type { AzureStorageConfig } from './types.mjs'; 5 | 6 | const MAX_HTTP_ATTEMPT_COUNT = 5; 7 | 8 | export function createGetRemoteReport(config: AzureStorageConfig) { 9 | async function getRemoteReport(branch: string, attempt = 1): ReturnType { 10 | try { 11 | const response = await fetch(`${config.endpoint}?branch=${branch}`); 12 | const result = (await response.json()) as Array; 13 | 14 | const remoteReport = result.map(entity => { 15 | const { commitSHA, ...rest } = entity; 16 | return rest; 17 | }); 18 | const { commitSHA } = result[result.length - 1]; 19 | 20 | return { commitSHA, remoteReport }; 21 | } catch (err) { 22 | console.log([pc.yellow('[w]'), (err as Error).toString()].join(' ')); 23 | console.log([pc.yellow('[w]'), 'Failed to fetch report from the remote. Retrying...'].join(' ')); 24 | 25 | if (attempt >= MAX_HTTP_ATTEMPT_COUNT) { 26 | console.error( 27 | [pc.red('[e]'), 'Exceeded 5 attempts to fetch reports, please check previously reported warnings...'].join( 28 | ' ', 29 | ), 30 | ); 31 | throw err; 32 | } 33 | 34 | return getRemoteReport(branch, attempt + 1); 35 | } 36 | } 37 | 38 | return getRemoteReport; 39 | } 40 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/getRemoteReport.test.mts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 2 | 3 | import { createGetRemoteReport } from './getRemoteReport.mjs'; 4 | import type { AzureStorageConfig } from './types.mjs'; 5 | import { sampleReport } from './__fixture__/sampleReports.mjs'; 6 | 7 | const fetch = vitest.hoisted(() => vitest.fn()); 8 | global.fetch = fetch; 9 | 10 | const testConfig: AzureStorageConfig = { 11 | endpoint: 'https://localhost', 12 | }; 13 | 14 | function noop() { 15 | /* does nothing */ 16 | } 17 | 18 | describe('getRemoteReport', () => { 19 | beforeEach(() => { 20 | vitest.resetAllMocks(); 21 | }); 22 | 23 | it('fetches a remote report', async () => { 24 | const value: Partial = { 25 | json: () => { 26 | return Promise.resolve(sampleReport); 27 | }, 28 | }; 29 | fetch.mockImplementation(() => Promise.resolve(value)); 30 | 31 | const getRemoteReport = createGetRemoteReport(testConfig); 32 | const { remoteReport } = await getRemoteReport('main'); 33 | 34 | expect(fetch).toHaveBeenCalledTimes(1); 35 | expect(remoteReport).toEqual(sampleReport); 36 | }); 37 | 38 | it('retries to fetch a report', async () => { 39 | const value: Partial = { 40 | json: () => { 41 | return Promise.resolve(sampleReport); 42 | }, 43 | }; 44 | fetch 45 | .mockImplementationOnce(() => Promise.reject(new Error('A fetch error'))) 46 | .mockImplementationOnce(() => Promise.reject(new Error('A fetch error'))) 47 | .mockImplementation(() => Promise.resolve(value)); 48 | 49 | vitest.spyOn(console, 'log').mockImplementation(noop); 50 | 51 | const getRemoteReport = createGetRemoteReport(testConfig); 52 | const { remoteReport } = await getRemoteReport('main'); 53 | 54 | expect(fetch).toHaveBeenCalledTimes(3); 55 | expect(remoteReport).toEqual(sampleReport); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/index.mts: -------------------------------------------------------------------------------- 1 | import type { StorageAdapter } from 'monosize'; 2 | 3 | import { createGetRemoteReport } from './getRemoteReport.mjs'; 4 | import { createUploadReportToRemote } from './uploadReportToRemote.mjs'; 5 | import type { AzureStorageConfig } from './types.mjs'; 6 | 7 | function createAzureStorage(config: AzureStorageConfig): StorageAdapter { 8 | return { 9 | getRemoteReport: createGetRemoteReport(config), 10 | uploadReportToRemote: createUploadReportToRemote(config), 11 | }; 12 | } 13 | 14 | export default createAzureStorage; 15 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/types.mts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/identity/identity/samples/AzureIdentityExamples.md#authenticating-azure-hosted-applications 3 | */ 4 | export type AzureAuthenticationType = 'AzureNamedKeyCredential' | 'AzurePipelinesCredential' | 'DefaultAzureCredential'; 5 | 6 | export type AzureStorageConfig = { 7 | endpoint: string; 8 | /** 9 | * @default 'AzureNamedKeyCredential' auth type 10 | */ 11 | authType?: AzureAuthenticationType; 12 | /** 13 | * @default 'latest' table name 14 | */ 15 | tableName?: string; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/uploadReportToRemote.mts: -------------------------------------------------------------------------------- 1 | import { odata, TableTransaction } from '@azure/data-tables'; 2 | import { BundleSizeReportEntry, BundleSizeReport, StorageAdapter } from 'monosize'; 3 | import pc from 'picocolors'; 4 | import { createTableClient } from './createTableClient.mjs'; 5 | import type { AzureStorageConfig } from './types.mjs'; 6 | 7 | export const ENTRIES_PER_CHUNK = 90; 8 | 9 | export function createRowKey(entry: BundleSizeReportEntry): string { 10 | // Azure does not support slashes in "rowKey" 11 | // https://docs.microsoft.com/archive/blogs/jmstall/azure-storage-naming-rules 12 | return `${entry.packageName}${entry.path.replace(/\.fixture\.js$/, '').replace(/\//g, '')}`; 13 | } 14 | 15 | export function splitArrayToChunks(arr: T[], size: number): T[][] { 16 | return [...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(i * size, (i + 1) * size)); 17 | } 18 | 19 | export function createUploadReportToRemote(config: AzureStorageConfig) { 20 | const { authType = 'AzureNamedKeyCredential', tableName = 'latest' } = config; 21 | 22 | async function uploadReportToRemote( 23 | branch: string, 24 | commitSHA: string, 25 | localReport: BundleSizeReport, 26 | ): ReturnType { 27 | // Validate the branch name 28 | if (/[\\/]/.test(branch)) { 29 | throw new Error( 30 | `monosize-storage-azure: invalid branch name "${branch}". Branch names cannot contain forward (/) or backward (\\) slashes.`, 31 | ); 32 | } 33 | 34 | const client = createTableClient({ authType, tableName }); 35 | 36 | if (localReport.length === 0) { 37 | console.log([pc.yellow('[w]'), 'No entries to upload'].join(' ')); 38 | return; 39 | } 40 | 41 | const transaction = new TableTransaction(); 42 | const entitiesIterator = client.listEntities({ 43 | queryOptions: { 44 | filter: odata`PartitionKey eq ${branch}`, 45 | }, 46 | }); 47 | 48 | for await (const entity of entitiesIterator) { 49 | // We can't delete and create entries with the same "rowKey" in the same transaction 50 | // => we delete only entries not present in existing report 51 | const isEntryPresentInExistingReport = Boolean(localReport.find(entry => createRowKey(entry) === entity.rowKey)); 52 | const shouldEntryBeDeleted = !isEntryPresentInExistingReport; 53 | 54 | if (shouldEntryBeDeleted) { 55 | transaction.deleteEntity(entity.partitionKey as string, entity.rowKey as string); 56 | } 57 | } 58 | 59 | localReport.forEach(entry => { 60 | transaction.upsertEntity( 61 | { 62 | partitionKey: branch, 63 | rowKey: createRowKey(entry), 64 | 65 | name: entry.name, 66 | packageName: entry.packageName, 67 | path: entry.path, 68 | 69 | minifiedSize: entry.minifiedSize, 70 | gzippedSize: entry.gzippedSize, 71 | 72 | commitSHA, 73 | }, 74 | 'Replace', 75 | ); 76 | }); 77 | 78 | const chunks = splitArrayToChunks(transaction.actions, ENTRIES_PER_CHUNK); 79 | 80 | for (const chunk of chunks) { 81 | await client.submitTransaction(chunk); 82 | } 83 | } 84 | 85 | return uploadReportToRemote; 86 | } 87 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/src/uploadReportToRemote.test.mts: -------------------------------------------------------------------------------- 1 | import { beforeEach, beforeAll, describe, expect, it, vitest, type Mock } from 'vitest'; 2 | import { 3 | createRowKey, 4 | ENTRIES_PER_CHUNK, 5 | splitArrayToChunks, 6 | createUploadReportToRemote, 7 | } from './uploadReportToRemote.mjs'; 8 | 9 | import { sampleReport, bigReport } from './__fixture__/sampleReports.mjs'; 10 | import { BundleSizeReportEntry } from 'monosize'; 11 | import type { AzureStorageConfig } from './types.mjs'; 12 | 13 | const getRemoteReport = vitest.hoisted( 14 | () => vitest.fn() as Mock>, 15 | ); 16 | const submitTransaction = vitest.hoisted(() => vitest.fn()); 17 | 18 | const testConfig: AzureStorageConfig = { 19 | endpoint: 'https://localhost', 20 | authType: 'AzureNamedKeyCredential', 21 | }; 22 | 23 | vitest.mock('@azure/data-tables', async () => { 24 | const listEntities = () => { 25 | const data = getRemoteReport(); 26 | 27 | return { 28 | [Symbol.asyncIterator]: () => ({ 29 | next: async () => { 30 | const value = data.shift(); 31 | 32 | return { 33 | value, 34 | done: value === undefined, 35 | }; 36 | }, 37 | }), 38 | }; 39 | }; 40 | 41 | const AzureNamedKeyCredential = vitest.fn() as unknown as import('@azure/data-tables').AzureNamedKeyCredential; 42 | const TableClient = vitest.fn().mockImplementation(() => ({ 43 | listEntities, 44 | submitTransaction, 45 | })) as unknown as import('@azure/data-tables').TableClient; 46 | 47 | const azureTables = await vitest.importActual('@azure/data-tables'); 48 | return { 49 | ...azureTables, 50 | 51 | AzureNamedKeyCredential, 52 | TableClient, 53 | }; 54 | }); 55 | 56 | const commitSHA = 'commit-sha'; 57 | const branchName = 'main'; 58 | 59 | describe('createRowKey', () => { 60 | it('creates a row key', () => { 61 | expect(createRowKey(sampleReport[0])).toBe('@scope/foo-packagefoo'); 62 | }); 63 | }); 64 | 65 | describe('splitArrayToChunks', () => { 66 | it('splits an array to chunks', () => { 67 | expect(splitArrayToChunks([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]); 68 | }); 69 | }); 70 | 71 | describe('uploadReportToRemote', () => { 72 | beforeEach(() => { 73 | vitest.clearAllMocks(); 74 | }); 75 | 76 | beforeAll(() => { 77 | Object.assign(process.env, { 78 | BUNDLESIZE_ACCOUNT_KEY: 'account-key', 79 | BUNDLESIZE_ACCOUNT_NAME: 'account-name', 80 | }); 81 | }); 82 | 83 | it('uploads a report to the remote', async () => { 84 | // Remote report contains 2 entries, local report contains 1 entry 85 | // => 1 entry should be deleted, 1 entry should be upserted 86 | 87 | const remoteReport = sampleReport.map(entry => ({ 88 | ...entry, 89 | 90 | partitionKey: 'main', 91 | rowKey: createRowKey(entry), 92 | })); 93 | const localReport = sampleReport.slice(0, 1); 94 | 95 | getRemoteReport.mockReturnValueOnce(remoteReport); 96 | const uploadReportToRemote = createUploadReportToRemote(testConfig); 97 | await uploadReportToRemote(branchName, commitSHA, localReport); 98 | 99 | expect(submitTransaction).toHaveBeenCalledTimes(1); 100 | expect(submitTransaction).toHaveBeenCalledWith([ 101 | ['delete', { partitionKey: 'main', rowKey: createRowKey(sampleReport[1]) }], 102 | [ 103 | 'upsert', 104 | { ...sampleReport[0], commitSHA, partitionKey: 'main', rowKey: createRowKey(sampleReport[0]) }, 105 | 'Replace', 106 | ], 107 | ]); 108 | }); 109 | 110 | it('performs chunked transactions if local report is too big', async () => { 111 | const remoteReport = bigReport.slice(0, 1).map(entry => ({ 112 | ...entry, 113 | 114 | partitionKey: 'main', 115 | rowKey: createRowKey(entry), 116 | })); 117 | const localReport = bigReport; 118 | 119 | getRemoteReport.mockReturnValueOnce(remoteReport); 120 | const uploadReportToRemote = createUploadReportToRemote(testConfig); 121 | await uploadReportToRemote(branchName, commitSHA, localReport); 122 | 123 | expect(submitTransaction).toHaveBeenCalledTimes(Math.ceil(localReport.length / ENTRIES_PER_CHUNK)); 124 | expect(submitTransaction).toHaveBeenCalledWith( 125 | bigReport 126 | .slice(0, ENTRIES_PER_CHUNK) 127 | .map(entry => [ 128 | 'upsert', 129 | { ...entry, commitSHA, partitionKey: 'main', rowKey: createRowKey(entry) }, 130 | 'Replace', 131 | ]), 132 | ); 133 | }); 134 | 135 | it('performs no actions if local report is empty', async () => { 136 | // eslint-disable-next-line @typescript-eslint/no-empty-function 137 | const log = vitest.spyOn(console, 'log').mockImplementation(() => {}); 138 | const uploadReportToRemote = createUploadReportToRemote(testConfig); 139 | await uploadReportToRemote(branchName, commitSHA, []); 140 | 141 | expect(log).toHaveBeenCalledTimes(1); 142 | expect(log).toHaveBeenCalledWith(expect.stringContaining('No entries to upload')); 143 | 144 | expect(submitTransaction).not.toHaveBeenCalled(); 145 | }); 146 | 147 | it('throws an error if branch contains invalid characters', async () => { 148 | const uploadReportToRemote = createUploadReportToRemote(testConfig); 149 | 150 | // Test with invalid branch names 151 | const invalidBranches = ['feature/new-1', 'feature\\new-2', 'refs/heads/feature/new-3']; 152 | 153 | for (const invalidBranch of invalidBranches) { 154 | await expect(uploadReportToRemote(invalidBranch, commitSHA, sampleReport)).rejects.toThrow( 155 | `monosize-storage-azure: invalid branch name "${invalidBranch}". Branch names cannot contain forward (/) or backward (\\) slashes.`, 156 | ); 157 | } 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["environment", "node"] 7 | }, 8 | "include": ["**/*.mts"], 9 | "exclude": ["**/*.test.mts", "vite.config.mts", "**/__fixture__/", "**/__mocks__/"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["vitest/importMeta", "vite/client", "environment", "node", "vitest"] 6 | }, 7 | "include": ["vite.config.mts", "src/**/*.test.mts", "src/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/monosize-storage-azure/vite.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | 6 | export default defineConfig({ 7 | cacheDir: '../../node_modules/.vite/monosize-storage-azure', 8 | plugins: [nxViteTsPaths()], 9 | 10 | test: { 11 | reporters: ['default'], 12 | include: ['src/**/*.test.mts'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - monosize-storage-upstash 2 | 3 | This log was last generated on Tue, 06 Aug 2024 20:40:08 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 0.0.21 8 | 9 | Tue, 06 Aug 2024 20:40:08 GMT 10 | 11 | ### Patches 12 | 13 | - Bump monosize to v0.6.3 14 | 15 | ## 0.0.20 16 | 17 | Mon, 20 May 2024 09:09:44 GMT 18 | 19 | ### Patches 20 | 21 | - Bump monosize to v0.6.2 22 | 23 | ## 0.0.19 24 | 25 | Fri, 17 May 2024 08:39:46 GMT 26 | 27 | ### Patches 28 | 29 | - Bump monosize to v0.6.1 30 | 31 | ## 0.0.18 32 | 33 | Thu, 16 May 2024 15:04:43 GMT 34 | 35 | ### Patches 36 | 37 | - Bump monosize to v0.6.0 38 | 39 | ## 0.0.17 40 | 41 | Fri, 10 May 2024 09:04:46 GMT 42 | 43 | ### Patches 44 | 45 | - Bump monosize to v0.5.1 46 | 47 | ## 0.0.16 48 | 49 | Fri, 22 Mar 2024 14:27:41 GMT 50 | 51 | ### Patches 52 | 53 | - Bump monosize to v0.5.0 54 | 55 | ## 0.0.15 56 | 57 | Sat, 16 Mar 2024 11:26:36 GMT 58 | 59 | ### Patches 60 | 61 | - Bump monosize to v0.4.0 62 | 63 | ## 0.0.14 64 | 65 | Wed, 21 Feb 2024 16:37:56 GMT 66 | 67 | ### Patches 68 | 69 | - Bump monosize to v0.3.0 70 | 71 | ## 0.0.13 72 | 73 | Tue, 20 Feb 2024 17:06:09 GMT 74 | 75 | ### Patches 76 | 77 | - Bump monosize to v0.2.2 78 | 79 | ## 0.0.12 80 | 81 | Fri, 22 Dec 2023 14:27:47 GMT 82 | 83 | ### Patches 84 | 85 | - Bump monosize to v0.2.1 86 | 87 | ## 0.0.11 88 | 89 | Fri, 22 Dec 2023 13:44:44 GMT 90 | 91 | ### Patches 92 | 93 | - Bump monosize to v0.2.0 94 | 95 | ## 0.0.10 96 | 97 | Mon, 13 Feb 2023 12:55:07 GMT 98 | 99 | ### Patches 100 | 101 | - chore: enable typings (olfedias@microsoft.com) 102 | 103 | ## 0.0.9 104 | 105 | Mon, 13 Feb 2023 11:26:46 GMT 106 | 107 | ### Patches 108 | 109 | - Bump monosize to v0.1.0 110 | 111 | ## 0.0.8 112 | 113 | Mon, 12 Dec 2022 17:37:53 GMT 114 | 115 | ### Patches 116 | 117 | - Bump monosize to v0.0.9 118 | 119 | ## 0.0.7 120 | 121 | Sun, 27 Nov 2022 22:40:02 GMT 122 | 123 | ### Patches 124 | 125 | - Bump monosize to v0.0.8 126 | 127 | ## 0.0.6 128 | 129 | Fri, 25 Nov 2022 16:53:18 GMT 130 | 131 | ### Patches 132 | 133 | - Bump monosize to v0.0.7 134 | 135 | ## 0.0.5 136 | 137 | Fri, 25 Nov 2022 16:20:16 GMT 138 | 139 | ### Patches 140 | 141 | - Bump monosize to v0.0.6 142 | 143 | ## 0.0.4 144 | 145 | Fri, 25 Nov 2022 15:17:27 GMT 146 | 147 | ### Patches 148 | 149 | - Bump monosize to v0.0.5 150 | 151 | ## 0.0.3 152 | 153 | Fri, 25 Nov 2022 14:47:08 GMT 154 | 155 | ### Patches 156 | 157 | - chore: bump dependencies (olfedias@microsoft.com) 158 | - Bump monosize to v0.0.4 159 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/README.md: -------------------------------------------------------------------------------- 1 |
2 |

monosize-storage-upstash

3 |
4 | 5 | ## Install 6 | 7 | ```sh 8 | # yarn 9 | yarn add --dev monosize-storage-upstash 10 | 11 | # npm 12 | npm install --save-dev monosize-storage-upstash 13 | ``` 14 | 15 | ## Usage 16 | 17 | - Create an account on [Upstash](https://upstash.com/) 18 | - Create a Redis database 19 | - Collect REST URL (`UPSTASH_REDIS_REST_URL`) and a **read-only** token (`UPSTASH_REDIS_REST_TOKEN`) 20 | - Update `monosize.config.mjs` 21 | 22 | ### Configuration 23 | 24 | ```js 25 | import upstashStorage from 'monosize-storage-upstash'; 26 | 27 | export default { 28 | repository: 'https://github.com/ORG/REPO', 29 | storage: upstashStorage({ 30 | url: 'REST URL (UPSTASH_REDIS_REST_URL)', 31 | readonlyToken: 'Readonly token (UPSTASH_REDIS_REST_TOKEN)', 32 | }), 33 | }; 34 | ``` 35 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-storage-upstash", 3 | "version": "0.0.21", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/monosize", 8 | "directory": "packages/monosize-storage-upstash" 9 | }, 10 | "types": "./src/index.d.mts", 11 | "dependencies": { 12 | "@upstash/redis": "^1.34.6", 13 | "monosize": "^0.6.3", 14 | "tslib": "^2.4.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize-storage-upstash", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/monosize-storage-upstash/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "dependsOn": ["^build"], 10 | "options": { 11 | "outputPath": "dist/packages/monosize-storage-upstash", 12 | "main": "packages/monosize-storage-upstash/src/index.ts", 13 | "tsConfig": "packages/monosize-storage-upstash/tsconfig.lib.json", 14 | "outputFileExtensionForEsm": ".mjs", 15 | "assets": [ 16 | "packages/monosize-storage-upstash/README.md", 17 | { 18 | "glob": "LICENSE.md", 19 | "input": ".", 20 | "output": "." 21 | } 22 | ] 23 | } 24 | }, 25 | "lint": { 26 | "executor": "@nx/eslint:lint", 27 | "outputs": ["{options.outputFile}"] 28 | }, 29 | "test": { 30 | "executor": "@nx/vite:test", 31 | "outputs": ["{workspaceRoot}/coverage/packages/monosize-storage-upstash"], 32 | "options": { 33 | "passWithNoTests": true 34 | } 35 | } 36 | }, 37 | "tags": [] 38 | } 39 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/src/getRemoteReport.test.mts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, it, vitest } from 'vitest'; 2 | 3 | // import { sampleReport } from '../__fixture__/sampleReport'; 4 | // import { getRemoteReport } from './getRemoteReport'; 5 | 6 | const fetch = vitest.hoisted(() => vitest.fn()); 7 | 8 | function noop() { 9 | /* does nothing */ 10 | } 11 | 12 | describe('getRemoteReport', () => { 13 | beforeEach(() => { 14 | vitest.resetAllMocks(); 15 | }); 16 | 17 | it('fetches a remote report', async () => { 18 | // const value: Partial = { 19 | // json: () => { 20 | // return Promise.resolve(sampleReport); 21 | // }, 22 | // }; 23 | // fetch.mockImplementation(() => Promise.resolve(value)); 24 | // 25 | // const { remoteReport } = await getRemoteReport('main'); 26 | // 27 | // expect(fetch).toHaveBeenCalledTimes(1); 28 | // expect(remoteReport).toEqual(sampleReport); 29 | // }); 30 | // 31 | // it('retries to fetch a report', async () => { 32 | // const value: Partial = { 33 | // json: () => { 34 | // return Promise.resolve(sampleReport); 35 | // }, 36 | // }; 37 | // fetch 38 | // .mockImplementationOnce(() => Promise.reject(new Error('A fetch error'))) 39 | // .mockImplementationOnce(() => Promise.reject(new Error('A fetch error'))) 40 | // .mockImplementation(() => Promise.resolve(value)); 41 | // 42 | // jest.spyOn(console, 'log').mockImplementation(noop); 43 | // 44 | // const { remoteReport } = await getRemoteReport('main'); 45 | // 46 | // expect(fetch).toHaveBeenCalledTimes(3); 47 | // expect(remoteReport).toEqual(sampleReport); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/src/index.mts: -------------------------------------------------------------------------------- 1 | import { Redis } from '@upstash/redis'; 2 | import type { BundleSizeReport, StorageAdapter } from 'monosize'; 3 | 4 | type UpstashStorageConfig = { 5 | url: string; 6 | readonlyToken: string; 7 | }; 8 | 9 | function createUpstashStorage(config: UpstashStorageConfig): StorageAdapter { 10 | const getRemoteReport: StorageAdapter['getRemoteReport'] = async (branch: string) => { 11 | const redis = new Redis({ 12 | url: config.url, 13 | token: config.readonlyToken, 14 | }); 15 | 16 | const result = await redis.get<{ commitSHA: string; data: BundleSizeReport } | null>(branch); 17 | 18 | if (result === null) { 19 | throw new Error('monosize-storage-upstash: Failed to get data from a remote host'); 20 | } 21 | 22 | // TODO: validate data with schema 23 | // TODO: repeat queries 24 | 25 | return { 26 | commitSHA: result.commitSHA, 27 | remoteReport: result.data, 28 | }; 29 | }; 30 | 31 | const uploadReportToRemote: StorageAdapter['uploadReportToRemote'] = async (branch, commitSHA, localReport) => { 32 | if (typeof process.env['UPSTASH_WRITE_TOKEN'] !== 'string') { 33 | throw new Error('monosize-storage-upstash: "UPSTASH_WRITE_TOKEN" is not defined in your process.env'); 34 | } 35 | 36 | const redis = new Redis({ 37 | url: config.url, 38 | token: process.env['UPSTASH_WRITE_TOKEN'], 39 | }); 40 | const data = JSON.stringify({ 41 | commitSHA, 42 | data: localReport, 43 | }); 44 | 45 | // TODO: repeat queries 46 | await redis.set(branch, data); 47 | }; 48 | 49 | return { 50 | getRemoteReport, 51 | uploadReportToRemote, 52 | }; 53 | } 54 | 55 | export default createUpstashStorage; 56 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["environment", "node"] 7 | }, 8 | "include": ["**/*.mts"], 9 | "exclude": ["**/*.test.mts", "vite.config.mts", "**/__fixture__/", "**/__mocks__/"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["vitest/importMeta", "vite/client", "environment", "node", "vitest"] 6 | }, 7 | "include": ["vite.config.mts", "src/**/*.test.mts", "src/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/monosize-storage-upstash/vite.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | 6 | export default defineConfig({ 7 | cacheDir: '../../node_modules/.vite/monosize-storage-upstash', 8 | plugins: [nxViteTsPaths()], 9 | 10 | test: { 11 | reporters: ['default'], 12 | include: ['src/**/*.test.mts'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/monosize/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"], 4 | "rules": { 5 | "no-console": "error" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/monosize/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - monosize 2 | 3 | This log was last generated on Tue, 06 Aug 2024 20:40:08 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 0.6.3 8 | 9 | Tue, 06 Aug 2024 20:40:08 GMT 10 | 11 | ### Patches 12 | 13 | - feat: implement --fixtures argument for measure CLI command (ben.keen@gmail.com) 14 | 15 | ## 0.6.2 16 | 17 | Mon, 20 May 2024 09:09:44 GMT 18 | 19 | ### Patches 20 | 21 | - feat: inline findGitRoot func and remove workspace-tools from deps in order to ship less JS (hochelmartin@gmail.com) 22 | - fix: enable strict CLI mode in order to provide proper feedback when invalid commands/flags are used (hochelmartin@gmail.com) 23 | 24 | ## 0.6.1 25 | 26 | Fri, 17 May 2024 08:39:46 GMT 27 | 28 | ### Patches 29 | 30 | - fix: resolve default argument resolution bug within collectLocalReport (hochelmartin@gmail.com) 31 | 32 | ## 0.6.0 33 | 34 | Thu, 16 May 2024 15:04:43 GMT 35 | 36 | ### Minor changes 37 | 38 | - feat: create packageName from package.json or project.json by default and add packageName,packageRoot global configuration options overrides. [BREAKING-CHANGE] (hochelmartin@gmail.com) 39 | 40 | ## 0.5.1 41 | 42 | Fri, 10 May 2024 09:04:46 GMT 43 | 44 | ### Patches 45 | 46 | - feat: add `--artifacts-location` option to `monosize measure` (olfedias@microsoft.com) 47 | 48 | ## 0.5.0 49 | 50 | Fri, 22 Mar 2024 14:27:41 GMT 51 | 52 | ### Minor changes 53 | 54 | - feat: implement shared interface for createBunlder api (hochelmartin@gmail.com) 55 | 56 | ## 0.4.0 57 | 58 | Sat, 16 Mar 2024 11:26:36 GMT 59 | 60 | ### Minor changes 61 | 62 | - feat(monosize): unify reporters API and behaviours (hochelmartin@gmail.com) 63 | - feat: make bundlers configurable [BREAKING] (olfedias@microsoft.com) 64 | - feat: add deltaFormat to reporters API configurable via CLI (hochelmartin@gmail.com) 65 | 66 | ### Patches 67 | 68 | - chore: replace Babel with Acorn (olfedias@microsoft.com) 69 | - fix: delete proper directory in measure() (olfedias@microsoft.com) 70 | 71 | ## 0.3.0 72 | 73 | Wed, 21 Feb 2024 16:37:56 GMT 74 | 75 | ### Minor changes 76 | 77 | - breaking: output markdown reports directly to console (olfedias@microsoft.com) 78 | 79 | ### Patches 80 | 81 | - feat: add debug mode to "measure" command (olfedias@microsoft.com) 82 | - fix: do not parallel builds too much (olfedias@microsoft.com) 83 | 84 | ## 0.2.2 85 | 86 | Tue, 20 Feb 2024 17:06:09 GMT 87 | 88 | ### Patches 89 | 90 | - fix: improve Windows compat (olfedias@microsoft.com) 91 | 92 | ## 0.2.1 93 | 94 | Fri, 22 Dec 2023 14:27:47 GMT 95 | 96 | ### Patches 97 | 98 | - fix: add "--report-files-glob" to "compare-reports" (olfedias@microsoft.com) 99 | 100 | ## 0.2.0 101 | 102 | Fri, 22 Dec 2023 13:44:44 GMT 103 | 104 | ### Minor changes 105 | 106 | - feat: support repositories that contain a single package (olfedias@microsoft.com) 107 | 108 | ### Patches 109 | 110 | - chore: bump Babel dependencies (olfedias@microsoft.com) 111 | 112 | ## 0.1.0 113 | 114 | Mon, 13 Feb 2023 11:26:46 GMT 115 | 116 | ### Minor changes 117 | 118 | - feat: export BundleSizeReportEntry type (olfedias@microsoft.com) 119 | 120 | ## 0.0.9 121 | 122 | Mon, 12 Dec 2022 17:37:53 GMT 123 | 124 | ### Patches 125 | 126 | - fix: update glob in collectLocalReport (olfedias@microsoft.com) 127 | 128 | ## 0.0.8 129 | 130 | Sun, 27 Nov 2022 22:40:02 GMT 131 | 132 | ### Patches 133 | 134 | - fix: remove usage of CJS vars, add linting (olfedias@microsoft.com) 135 | 136 | ## 0.0.7 137 | 138 | Fri, 25 Nov 2022 16:53:18 GMT 139 | 140 | ### Patches 141 | 142 | - fix: handle ESM configs, export config type (olfedias@microsoft.com) 143 | 144 | ## 0.0.6 145 | 146 | Fri, 25 Nov 2022 16:20:16 GMT 147 | 148 | ### Patches 149 | 150 | - fix: use esModuleInterop and "import *" (olfedias@microsoft.com) 151 | 152 | ## 0.0.5 153 | 154 | Fri, 25 Nov 2022 15:17:27 GMT 155 | 156 | ### Patches 157 | 158 | - fix: change a path to bin (olfedias@microsoft.com) 159 | 160 | ## 0.0.4 161 | 162 | Fri, 25 Nov 2022 14:47:08 GMT 163 | 164 | ### Patches 165 | 166 | - chore: bump dependencies (olfedias@microsoft.com) 167 | -------------------------------------------------------------------------------- /packages/monosize/bin/monosize.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | await import('../src/index.mjs'); 3 | -------------------------------------------------------------------------------- /packages/monosize/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize", 3 | "version": "0.6.3", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/monosize", 8 | "directory": "packages/monosize" 9 | }, 10 | "types": "./src/index.d.mts", 11 | "bin": { 12 | "monosize": "./bin/monosize.mjs" 13 | }, 14 | "dependencies": { 15 | "acorn": "^8.14.1", 16 | "ci-info": "^4.2.0", 17 | "cli-table3": "^0.6.5", 18 | "find-up": "^7.0.0", 19 | "glob": "^11.0.1", 20 | "gzip-size": "^7.0.0", 21 | "picocolors": "^1.0.0", 22 | "pretty-bytes": "^6.0.0", 23 | "tslib": "^2.4.1", 24 | "yargs": "^17.6.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/monosize/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monosize", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/monosize/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/packages/monosize", 11 | "main": "packages/monosize/src/index.ts", 12 | "tsConfig": "packages/monosize/tsconfig.lib.json", 13 | "outputFileExtensionForEsm": ".mjs", 14 | "assets": [ 15 | "packages/monosize/README.md", 16 | { 17 | "glob": "LICENSE.md", 18 | "input": ".", 19 | "output": "." 20 | }, 21 | { 22 | "glob": "*.mjs", 23 | "input": "packages/monosize/bin", 24 | "output": "bin" 25 | } 26 | ] 27 | } 28 | }, 29 | "lint": { 30 | "executor": "@nx/eslint:lint", 31 | "outputs": ["{options.outputFile}"] 32 | }, 33 | "test": { 34 | "executor": "@nx/vite:test", 35 | "outputs": ["{workspaceRoot}/coverage/packages/monosize"], 36 | "options": { 37 | "passWithNoTests": true 38 | } 39 | } 40 | }, 41 | "tags": [] 42 | } 43 | -------------------------------------------------------------------------------- /packages/monosize/src/__fixture__/sampleComparedReport.mts: -------------------------------------------------------------------------------- 1 | import { ComparedReport, emptyDiff } from '../utils/compareResultsInReports.mjs'; 2 | 3 | export const sampleComparedReport: ComparedReport = [ 4 | { 5 | packageName: 'foo-package', 6 | name: 'New entry', 7 | path: 'foo.fixture.js', 8 | minifiedSize: 1000, 9 | gzippedSize: 100, 10 | diff: emptyDiff, 11 | }, 12 | { 13 | packageName: 'bar-package', 14 | name: 'An entry without diff', 15 | path: 'bar.fixture.js', 16 | minifiedSize: 1000, 17 | gzippedSize: 100, 18 | diff: { 19 | empty: false, 20 | 21 | minified: { delta: 0, percent: '0%' }, 22 | gzip: { delta: 0, percent: '0%' }, 23 | }, 24 | }, 25 | { 26 | packageName: 'baz-package', 27 | name: 'An entry with diff', 28 | path: 'baz.fixture.js', 29 | minifiedSize: 1000, 30 | gzippedSize: 100, 31 | diff: { 32 | empty: false, 33 | 34 | minified: { delta: 1000, percent: '100%' }, 35 | gzip: { delta: 100, percent: '100%' }, 36 | }, 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /packages/monosize/src/__fixture__/sampleReport.mts: -------------------------------------------------------------------------------- 1 | import type { BundleSizeReport } from '../types.mjs'; 2 | 3 | export const sampleReport: BundleSizeReport = [ 4 | { 5 | packageName: 'foo-package', 6 | name: 'New entry', 7 | path: 'foo.fixture.js', 8 | minifiedSize: 1000, 9 | gzippedSize: 100, 10 | }, 11 | { 12 | packageName: 'bar-package', 13 | name: 'An entry without diff', 14 | path: 'bar.fixture.js', 15 | minifiedSize: 1000, 16 | gzippedSize: 100, 17 | }, 18 | { 19 | packageName: 'baz-package', 20 | name: 'An entry with diff', 21 | path: 'baz.fixture.js', 22 | minifiedSize: 1000, 23 | gzippedSize: 100, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /packages/monosize/src/__mocks__/find-up.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | export const findUp: typeof import('find-up').findUp = async (configName, options) => { 5 | const filePath = path.resolve( 6 | (options?.cwd as string | undefined) || '', 7 | Array.isArray(configName) ? configName[0] : configName, 8 | ); 9 | 10 | if (fs.existsSync(filePath)) { 11 | return filePath; 12 | } 13 | 14 | return undefined; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/monosize/src/__mocks__/gzip-size.mts: -------------------------------------------------------------------------------- 1 | export const gzipSizeFromFile: typeof import('gzip-size').gzipSizeFromFile = async () => 1000; 2 | -------------------------------------------------------------------------------- /packages/monosize/src/__mocks__/pretty-bytes.mts: -------------------------------------------------------------------------------- 1 | const prettyBytes: typeof import('pretty-bytes').default = input => input.toString(); 2 | 3 | export default prettyBytes; 4 | -------------------------------------------------------------------------------- /packages/monosize/src/commands/compareReports.mts: -------------------------------------------------------------------------------- 1 | import { CommandModule } from 'yargs'; 2 | 3 | import { CliOptions } from '../index.mjs'; 4 | import { cliReporter } from '../reporters/cliReporter.mjs'; 5 | import { markdownReporter } from '../reporters/markdownReporter.mjs'; 6 | import { collectLocalReport } from '../utils/collectLocalReport.mjs'; 7 | import { compareResultsInReports } from '../utils/compareResultsInReports.mjs'; 8 | import { readConfig } from '../utils/readConfig.mjs'; 9 | import type { DiffByMetric } from '../utils/calculateDiffByMetric.mjs'; 10 | import { logger, timestamp } from '../logger.mjs'; 11 | 12 | export type CompareReportsOptions = CliOptions & { 13 | branch: string; 14 | 'report-files-glob'?: string; 15 | output: 'cli' | 'markdown'; 16 | deltaFormat: keyof DiffByMetric; 17 | }; 18 | 19 | async function compareReports(options: CompareReportsOptions) { 20 | const { branch, output, quiet, deltaFormat } = options; 21 | const startTime = timestamp(); 22 | 23 | const config = await readConfig(quiet); 24 | 25 | const localReportStartTime = timestamp(); 26 | const localReport = await collectLocalReport({ 27 | ...config, 28 | reportFilesGlob: options['report-files-glob'], 29 | }); 30 | 31 | if (!quiet) { 32 | logger.info(`Local report prepared`, localReportStartTime); 33 | } 34 | 35 | const remoteReportStartTime = timestamp(); 36 | const { commitSHA, remoteReport } = await config.storage.getRemoteReport(branch); 37 | 38 | if (!quiet) { 39 | if (commitSHA === '') { 40 | logger.info(`Remote report for "${branch}" branch was not found`); 41 | } else { 42 | logger.info(`Remote report for "${commitSHA}" commit fetched `, remoteReportStartTime); 43 | } 44 | } 45 | 46 | const reportsComparisonResult = compareResultsInReports(localReport, remoteReport); 47 | 48 | switch (output) { 49 | case 'cli': 50 | cliReporter(reportsComparisonResult, { 51 | commitSHA, 52 | repository: config.repository, 53 | showUnchanged: false, 54 | deltaFormat: deltaFormat ?? 'percent', 55 | }); 56 | break; 57 | case 'markdown': 58 | markdownReporter(reportsComparisonResult, { 59 | commitSHA, 60 | repository: config.repository, 61 | showUnchanged: true, 62 | deltaFormat: deltaFormat ?? 'delta', 63 | }); 64 | break; 65 | } 66 | 67 | if (!quiet) { 68 | logger.finish(`Completed`, startTime); 69 | } 70 | } 71 | 72 | // --- 73 | 74 | const api: CommandModule, CompareReportsOptions> = { 75 | command: 'compare-reports', 76 | describe: 'compares local and remote results', 77 | builder: { 78 | branch: { 79 | alias: 'b', 80 | type: 'string', 81 | description: 'A branch to compare against', 82 | default: 'main', 83 | }, 84 | 'report-files-glob': { 85 | type: 'string', 86 | description: 'A glob pattern to search for report files in JSON format', 87 | required: false, 88 | }, 89 | output: { 90 | alias: 'o', 91 | type: 'string', 92 | choices: ['cli', 'markdown'], 93 | description: 'Defines a reporter to produce output', 94 | default: 'cli', 95 | }, 96 | deltaFormat: { 97 | type: 'string', 98 | choices: ['delta', 'percent'], 99 | description: 'Defines format of delta output', 100 | }, 101 | }, 102 | handler: compareReports, 103 | }; 104 | 105 | export default api; 106 | -------------------------------------------------------------------------------- /packages/monosize/src/commands/compareReports.test.mts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vitest } from 'vitest'; 2 | 3 | import { sampleReport } from '../__fixture__/sampleReport.mjs'; 4 | import { sampleComparedReport } from '../__fixture__/sampleComparedReport.mjs'; 5 | import api, { CompareReportsOptions } from './compareReports.mjs'; 6 | 7 | const getRemoteReport = vitest.hoisted(() => vitest.fn()); 8 | const cliReporter = vitest.hoisted(() => vitest.fn()); 9 | const collectLocalReport = vitest.hoisted(() => vitest.fn()); 10 | const compareResultsInReports = vitest.hoisted(() => vitest.fn()); 11 | 12 | vitest.mock('../utils/readConfig.mts', () => ({ 13 | readConfig: vitest.fn().mockResolvedValue({ 14 | storage: { getRemoteReport }, 15 | }), 16 | })); 17 | vitest.mock('../reporters/cliReporter.mts', () => ({ cliReporter })); 18 | vitest.mock('../utils/collectLocalReport.mts', () => ({ collectLocalReport })); 19 | vitest.mock('../utils/compareResultsInReports.mts', () => ({ emptyDiff: {}, compareResultsInReports })); 20 | 21 | describe('compareReports', () => { 22 | it('fetches remote report and compares it with a local data', async () => { 23 | const branchName = 'master'; 24 | 25 | getRemoteReport.mockResolvedValue({ commitSHA: 'test', remoteReport: sampleReport }); 26 | collectLocalReport.mockImplementation(() => sampleReport); 27 | compareResultsInReports.mockImplementation(() => sampleComparedReport); 28 | 29 | const options: CompareReportsOptions = { quiet: true, branch: branchName, output: 'cli', deltaFormat: 'percent' }; 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | await api.handler(options as any); 32 | 33 | expect(getRemoteReport).toHaveBeenCalledWith(branchName); 34 | expect(compareResultsInReports).toHaveBeenCalledWith(sampleReport, sampleReport); 35 | expect(cliReporter).toHaveBeenCalledWith(sampleComparedReport, { 36 | commitSHA: 'test', 37 | deltaFormat: 'percent', 38 | repository: undefined, 39 | showUnchanged: false, 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/monosize/src/commands/measure.mts: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table3'; 2 | import { glob } from 'glob'; 3 | import { gzipSizeFromFile } from 'gzip-size'; 4 | import fs from 'node:fs'; 5 | import path from 'node:path'; 6 | import type { CommandModule } from 'yargs'; 7 | 8 | import { formatBytes } from '../utils/helpers.mjs'; 9 | import { prepareFixture } from '../utils/prepareFixture.mjs'; 10 | import { readConfig } from '../utils/readConfig.mjs'; 11 | import type { CliOptions } from '../index.mjs'; 12 | import type { BuildResult } from '../types.mjs'; 13 | import { logger, timestamp } from '../logger.mjs'; 14 | 15 | export type MeasureOptions = CliOptions & { 16 | debug: boolean; 17 | 'artifacts-location': string; 18 | fixtures: string; 19 | }; 20 | 21 | async function measure(options: MeasureOptions) { 22 | const { debug = false, quiet, 'artifacts-location': artifactsLocation, fixtures: fixturesGlob } = options; 23 | 24 | const startTime = timestamp(); 25 | const artifactsDir = path.resolve(process.cwd(), artifactsLocation); 26 | 27 | // thrown error if cwd is set as artifactsLocation is set to '.' since next step is to rm everything 28 | if (artifactsDir === process.cwd()) { 29 | throw new Error("'--artifacts-location' cannot be the same as current working directory"); 30 | } 31 | 32 | await fs.promises.rm(artifactsDir, { recursive: true, force: true }); 33 | await fs.promises.mkdir(artifactsDir, { recursive: true }); 34 | 35 | if (!quiet) { 36 | if (debug) { 37 | logger.info('Running in debug mode...'); 38 | } 39 | 40 | logger.info('Artifacts dir is cleared'); 41 | } 42 | 43 | const fixtures = await glob(`bundle-size/${fixturesGlob}`, { 44 | absolute: true, 45 | cwd: process.cwd(), 46 | }); 47 | 48 | if (!fixtures.length && fixturesGlob) { 49 | logger.error(`No matching fixtures found for globbing pattern '${fixturesGlob}'`); 50 | process.exit(1); 51 | } 52 | 53 | const config = await readConfig(quiet); 54 | const measurements: BuildResult[] = []; 55 | 56 | if (!quiet) { 57 | logger.info(`Measuring bundle size for ${fixtures.length} fixture(s)...`); 58 | logger.raw(fixtures.map(fixture => ` - ${fixture}`).join('\n')); 59 | logger.info(`Using ${config.bundler.name} as a bundler...`); 60 | } 61 | 62 | for (const fixturePath of fixtures) { 63 | const fixtureStartTime = process.hrtime(); 64 | 65 | const { artifactPath, name } = await prepareFixture(artifactsDir, fixturePath); 66 | const { outputPath } = await config.bundler.buildFixture({ 67 | debug, 68 | fixturePath: artifactPath, 69 | quiet, 70 | }); 71 | 72 | const minifiedSize = (await fs.promises.stat(outputPath)).size; 73 | const gzippedSize = await gzipSizeFromFile(outputPath); 74 | 75 | measurements.push({ 76 | name, 77 | path: path.relative(process.cwd(), fixturePath).replaceAll(path.sep, '/'), 78 | minifiedSize, 79 | gzippedSize, 80 | }); 81 | 82 | if (!quiet) { 83 | logger.info(`Fixture "${path.basename(fixturePath)}" built`, fixtureStartTime); 84 | } 85 | } 86 | 87 | measurements.sort((a, b) => a.path.localeCompare(b.path, 'en')); 88 | 89 | await fs.promises.writeFile(path.resolve(artifactsDir, 'monosize.json'), JSON.stringify(measurements)); 90 | 91 | if (!quiet) { 92 | const table = new Table({ 93 | head: ['Fixture', 'Minified size', 'GZIP size'], 94 | }); 95 | const sortedMeasurements = [...measurements].sort((a, b) => a.path.localeCompare(b.path)); 96 | 97 | sortedMeasurements.forEach(r => { 98 | table.push([r.name, formatBytes(r.minifiedSize), formatBytes(r.gzippedSize)]); 99 | }); 100 | 101 | logger.raw(table.toString()); 102 | logger.finish(`Completed`, startTime); 103 | } 104 | } 105 | 106 | // --- 107 | 108 | const api: CommandModule, MeasureOptions> = { 109 | command: 'measure', 110 | describe: 'builds bundle size fixtures and generates JSON report', 111 | handler: measure, 112 | builder: { 113 | debug: { 114 | type: 'boolean', 115 | description: 'If true, will output additional artifacts for debugging', 116 | }, 117 | 'artifacts-location': { 118 | type: 'string', 119 | description: 120 | 'Relative path to the package root where the artifact files will be stored (monosize.json & bundler output). If specified, "--report-files-glob" in "monosize collect-reports" & "monosize upload-reports" should be set accordingly.', 121 | default: 'dist/bundle-size', 122 | }, 123 | fixtures: { 124 | type: 'string', 125 | description: 'Filename glob pattern to target whatever fixture files you want to measure.', 126 | default: '*.fixture.js', 127 | }, 128 | }, 129 | }; 130 | 131 | export default api; 132 | -------------------------------------------------------------------------------- /packages/monosize/src/commands/measure.test.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import tmp from 'tmp'; 4 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 5 | import api, { type MeasureOptions } from './measure.mjs'; 6 | import { logger } from '../logger.mjs'; 7 | 8 | const buildFixture = vitest.hoisted(() => 9 | vitest.fn().mockImplementation(async ({ fixturePath }) => { 10 | const outputPath = path.resolve( 11 | path.dirname(fixturePath), 12 | path.basename(fixturePath).replace('.fixture.js', '.output.js'), 13 | ); 14 | 15 | fs.cpSync(fixturePath, outputPath); 16 | 17 | return { outputPath }; 18 | }), 19 | ); 20 | 21 | vitest.mock('../utils/readConfig.mts', () => ({ 22 | readConfig: vitest.fn().mockResolvedValue({ 23 | bundler: { buildFixture }, 24 | }), 25 | })); 26 | 27 | async function setup(fixtures: { [key: string]: string }) { 28 | const packageDir = tmp.dirSync({ unsafeCleanup: true }); 29 | 30 | const cwd = vitest.spyOn(process, 'cwd'); 31 | cwd.mockReturnValue(packageDir.name); 32 | 33 | const fixturesDir = path.resolve(packageDir.name, 'bundle-size'); 34 | fs.mkdirSync(fixturesDir); 35 | 36 | for (const [fixture, content] of Object.entries(fixtures)) { 37 | fs.writeFileSync(path.resolve(fixturesDir, fixture), content); 38 | } 39 | 40 | return { 41 | packageDir: packageDir.name, 42 | }; 43 | } 44 | const getMockedFixtures = (...fixtureNames: string[]) => 45 | fixtureNames.reduce( 46 | (acc, item) => ({ 47 | ...acc, 48 | [`${item}.fixture.js`]: ` 49 | console.log("${item}"); 50 | export default { name: '${item}' }; 51 | `, 52 | }), 53 | {}, 54 | ); 55 | 56 | // eslint-disable-next-line @typescript-eslint/no-empty-function 57 | const noop = () => {}; 58 | 59 | describe('measure', () => { 60 | beforeEach(() => { 61 | vitest.clearAllMocks(); 62 | }); 63 | 64 | it('builds fixtures and created a report', async () => { 65 | const { packageDir } = await setup(getMockedFixtures('foo', 'bar')); 66 | const options: MeasureOptions = { 67 | quiet: true, 68 | debug: false, 69 | 'artifacts-location': 'output', 70 | fixtures: '*.fixture.js', 71 | }; 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 74 | await api.handler(options as any); 75 | 76 | // Fixtures 77 | 78 | expect(fs.readdirSync(path.resolve(packageDir, 'output'))).toEqual([ 79 | 'bar.fixture.js', 80 | 'bar.output.js', 81 | 'foo.fixture.js', 82 | 'foo.output.js', 83 | 'monosize.json', 84 | ]); 85 | 86 | // Report 87 | 88 | const report = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), 'output', 'monosize.json'), 'utf-8')); 89 | 90 | expect(report).toEqual([ 91 | { 92 | name: 'bar', 93 | path: 'bundle-size/bar.fixture.js', 94 | minifiedSize: expect.any(Number), 95 | gzippedSize: expect.any(Number), 96 | }, 97 | { 98 | name: 'foo', 99 | path: 'bundle-size/foo.fixture.js', 100 | minifiedSize: expect.any(Number), 101 | gzippedSize: expect.any(Number), 102 | }, 103 | ]); 104 | }); 105 | 106 | it('builds single targeted fixture when full filename passed', async () => { 107 | const { packageDir } = await setup(getMockedFixtures('foo', 'bar', 'baz')); 108 | const options: MeasureOptions = { 109 | quiet: true, 110 | debug: false, 111 | 'artifacts-location': 'output', 112 | fixtures: 'foo.fixture.js', 113 | }; 114 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 115 | await api.handler(options as any); 116 | 117 | // Fixtures 118 | 119 | expect(fs.readdirSync(path.resolve(packageDir, 'output'))).toEqual([ 120 | 'foo.fixture.js', 121 | 'foo.output.js', 122 | 'monosize.json', 123 | ]); 124 | }); 125 | 126 | it('builds only targeted fixtures with pattern passed', async () => { 127 | const { packageDir } = await setup(getMockedFixtures('foo', 'bar', 'baz')); 128 | const options: MeasureOptions = { quiet: true, debug: false, 'artifacts-location': 'output', fixtures: 'ba*' }; 129 | 130 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 131 | await api.handler(options as any); 132 | 133 | // Fixtures 134 | 135 | expect(fs.readdirSync(path.resolve(packageDir, 'output'))).toEqual([ 136 | 'bar.fixture.js', 137 | 'bar.output.js', 138 | 'baz.fixture.js', 139 | 'baz.output.js', 140 | 'monosize.json', 141 | ]); 142 | }); 143 | 144 | it('returns exit code of 1 and displays message when fixtures argument fails to match any fixture filename', async () => { 145 | const errorLog = vitest.spyOn(logger, 'error').mockImplementation(noop); 146 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 147 | const mockExit = vitest.spyOn(process, 'exit').mockImplementation(noop as any); 148 | 149 | await setup({}); 150 | const options: MeasureOptions = { 151 | quiet: true, 152 | debug: false, 153 | 'artifacts-location': 'output', 154 | fixtures: 'invalid-filename.js', 155 | }; 156 | 157 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 158 | await api.handler(options as any); 159 | 160 | expect(errorLog.mock.calls[0][0]).toMatch(/No matching fixtures found for globbing pattern 'invalid-filename.js'/); 161 | expect(mockExit).toHaveBeenCalledWith(1); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /packages/monosize/src/commands/uploadReport.mts: -------------------------------------------------------------------------------- 1 | import { isCI } from 'ci-info'; 2 | import type { CommandModule } from 'yargs'; 3 | 4 | import { collectLocalReport } from '../utils/collectLocalReport.mjs'; 5 | import { readConfig } from '../utils/readConfig.mjs'; 6 | import type { CliOptions } from '../index.mjs'; 7 | import { logger, timestamp } from '../logger.mjs'; 8 | 9 | type UploadOptions = CliOptions & { branch: string; 'report-files-glob'?: string; 'commit-sha': string }; 10 | 11 | async function uploadReport(options: UploadOptions) { 12 | if (!isCI) { 13 | logger.error('This is command can be executed only in CI'); 14 | process.exit(1); 15 | } 16 | 17 | const { branch, 'commit-sha': commitSHA, quiet } = options; 18 | const startTime = timestamp(); 19 | 20 | const config = await readConfig(quiet); 21 | 22 | const localReportStartTime = timestamp(); 23 | const localReport = await collectLocalReport({ 24 | ...config, 25 | reportFilesGlob: options['report-files-glob'], 26 | }); 27 | 28 | if (!quiet) { 29 | logger.info(`Local report prepared`, localReportStartTime); 30 | } 31 | 32 | const uploadStartTime = timestamp(); 33 | 34 | try { 35 | await config.storage.uploadReportToRemote(branch, commitSHA, localReport); 36 | } catch (e) { 37 | logger.error('Upload of the report to a remote host failed...'); 38 | logger.error(e); 39 | process.exit(1); 40 | } 41 | 42 | if (!quiet) { 43 | logger.info('Report uploaded', uploadStartTime); 44 | logger.finish(`Completed`, startTime); 45 | } 46 | } 47 | 48 | // --- 49 | 50 | const api: CommandModule, UploadOptions> = { 51 | command: 'upload-report', 52 | describe: 'uploads local results to Azure Table Storage', 53 | builder: { 54 | branch: { 55 | type: 'string', 56 | description: 'A branch to associate a report', 57 | required: true, 58 | }, 59 | 'report-files-glob': { 60 | type: 'string', 61 | description: 'A glob pattern to search for report files in JSON format', 62 | required: false, 63 | }, 64 | 'commit-sha': { 65 | type: 'string', 66 | description: 'Defines a commit sha for a report', 67 | required: true, 68 | }, 69 | }, 70 | handler: uploadReport, 71 | }; 72 | 73 | export default api; 74 | -------------------------------------------------------------------------------- /packages/monosize/src/index.mts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import { hideBin } from 'yargs/helpers'; 3 | 4 | import compareReports from './commands/compareReports.mjs'; 5 | import measure from './commands/measure.mjs'; 6 | import uploadReport from './commands/uploadReport.mjs'; 7 | 8 | const cliSetup = yargs(hideBin(process.argv)) 9 | .command(compareReports) 10 | .command(measure) 11 | .command(uploadReport) 12 | .option('quiet', { 13 | alias: 'q', 14 | type: 'boolean', 15 | description: 'Suppress verbose build output', 16 | default: false, 17 | }) 18 | .strict() 19 | .help() 20 | .scriptName('monosize') 21 | .version(false).argv; 22 | 23 | export type CliOptions = { quiet: boolean }; 24 | 25 | export type { 26 | BundleSizeReportEntry, 27 | BundleSizeReport, 28 | MonoSizeConfig, 29 | BundlerAdapter, 30 | StorageAdapter, 31 | BundlerAdapterFactoryConfig, 32 | BundleAdapterFactory, 33 | } from './types.mjs'; 34 | 35 | export default cliSetup; 36 | -------------------------------------------------------------------------------- /packages/monosize/src/logger.mts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import pc from 'picocolors'; 3 | 4 | import { formatHrTime } from './utils/helpers.mjs'; 5 | 6 | type LogFunction = (message: unknown, timestamp?: ReturnType) => void; 7 | type LogTypes = 'error' | 'info' | 'success' | 'finish'; 8 | 9 | export const timestamp = () => process.hrtime(); 10 | 11 | function toFriendlyTime(time?: ReturnType) { 12 | if (!time) { 13 | return ''; 14 | } 15 | 16 | return pc.dim(`(${formatHrTime(process.hrtime(time))})`); 17 | } 18 | 19 | /* eslint-disable no-console */ 20 | 21 | export const logger: Record & { raw: (...args: unknown[]) => void } = { 22 | // Logging functions 23 | // These functions are used to log messages to the console with different styles and colors 24 | error: (message, time) => { 25 | console.error(pc.red('[e]'), message, toFriendlyTime(time)); 26 | }, 27 | info: (message, time) => { 28 | if (time) { 29 | console.info(pc.blue('[i]'), message, toFriendlyTime(time)); 30 | return; 31 | } 32 | 33 | console.info(pc.blue('[i]'), message); 34 | }, 35 | success: (message, time) => { 36 | console.log(pc.green('[✔]'), message, toFriendlyTime(time)); 37 | }, 38 | 39 | // Special logging for the end of the process 40 | finish: (message, time) => { 41 | console.log(pc.bgGreenBright(` 🏁 ${pc.black(message as string)} `), toFriendlyTime(time)); 42 | }, 43 | 44 | // Raw logging function i.e. console.log() 45 | raw: (...args) => { 46 | console.log(...args); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/monosize/src/reporters/__snapshots__/markdownReporter.test.mts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`markdownReporter > renders a report to a file 1`] = ` 4 | "## 📊 Bundle size report 5 | 6 | | Package & Exports | Baseline (minified/GZIP) | PR | Change | 7 | | :------------------------------------------------------------------------------------- | -----------------------: | ------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 8 | | baz-package
An entry with diff | \`0 B\`
\`0 B\` | \`1 kB\`
\`100 B\` | \`1 kB\`
\`100 B\` | 9 | | foo-package
New entry | \`0 B\`
\`0 B\` | \`1 kB\`
\`100 B\` | 🆕 New entry | 10 | 11 |
12 | Unchanged fixtures 13 | 14 | | Package & Exports | Size (minified/GZIP) | 15 | | ----------------------------------------------------------------------------------------- | -------------------: | 16 | | bar-package
An entry without diff | \`1 kB\`
\`100 B\` | 17 | 18 |
19 | 🤖 This report was generated against commit-hash 20 | " 21 | `; 22 | 23 | exports[`markdownReporter > renders a report to a file with specified "deltaFormat" 1`] = ` 24 | "## 📊 Bundle size report 25 | 26 | | Package & Exports | Baseline (minified/GZIP) | PR | Change | 27 | | :------------------------------------------------------------------------------------- | -----------------------: | ------------------: | -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 28 | | baz-package
An entry with diff | \`0 B\`
\`0 B\` | \`1 kB\`
\`100 B\` | \`100%\`
\`100%\` | 29 | | foo-package
New entry | \`0 B\`
\`0 B\` | \`1 kB\`
\`100 B\` | 🆕 New entry | 30 | 31 |
32 | Unchanged fixtures 33 | 34 | | Package & Exports | Size (minified/GZIP) | 35 | | ----------------------------------------------------------------------------------------- | -------------------: | 36 | | bar-package
An entry without diff | \`1 kB\`
\`100 B\` | 37 | 38 |
39 | 🤖 This report was generated against commit-hash 40 | " 41 | `; 42 | -------------------------------------------------------------------------------- /packages/monosize/src/reporters/cliReporter.mts: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table3'; 2 | import pc from 'picocolors'; 3 | 4 | import { getChangedEntriesInReport } from '../utils/getChangedEntriesInReport.mjs'; 5 | import { formatBytes } from '../utils/helpers.mjs'; 6 | import type { DiffByMetric } from '../utils/calculateDiffByMetric.mjs'; 7 | import { logger } from '../logger.mjs'; 8 | import { formatDeltaFactory, type Reporter } from './shared.mjs'; 9 | 10 | function getDirectionSymbol(value: number): string { 11 | if (value < 0) { 12 | return '↓'; 13 | } 14 | 15 | if (value > 0) { 16 | return '↑'; 17 | } 18 | 19 | return ''; 20 | } 21 | 22 | function formatDelta(diff: DiffByMetric, deltaFormat: keyof DiffByMetric): string { 23 | const output = formatDeltaFactory(diff, { deltaFormat, directionSymbol: getDirectionSymbol }); 24 | 25 | const colorFn = diff.delta > 0 ? pc.red : pc.green; 26 | 27 | return typeof output === 'string' ? output : colorFn(output.deltaOutput + output.dirSymbol); 28 | } 29 | 30 | export const cliReporter: Reporter = (report, options) => { 31 | const { commitSHA, repository, deltaFormat } = options; 32 | const footer = `🤖 This report was generated against '${repository}/commit/${commitSHA}'`; 33 | 34 | const { changedEntries } = getChangedEntriesInReport(report); 35 | 36 | const reportOutput = new Table({ 37 | colAligns: ['left', 'right', 'right'], 38 | head: ['Fixture', 'Before', 'After (minified/GZIP)'], 39 | }); 40 | 41 | if (changedEntries.length === 0) { 42 | logger.success('No changes found'); 43 | return; 44 | } 45 | 46 | changedEntries.forEach(entry => { 47 | const { diff, gzippedSize, minifiedSize, name, packageName } = entry; 48 | const fixtureColumn = pc.bold(packageName) + '\n' + name + (diff.empty ? pc.cyan(' (new)') : ''); 49 | 50 | const minifiedBefore = diff.empty ? 'N/A' : formatBytes(minifiedSize - diff.minified.delta); 51 | const gzippedBefore = diff.empty ? 'N/A' : formatBytes(gzippedSize - diff.gzip.delta); 52 | 53 | const minifiedAfter = formatBytes(minifiedSize); 54 | const gzippedAfter = formatBytes(gzippedSize); 55 | 56 | const beforeColumn = minifiedBefore + '\n' + gzippedBefore; 57 | const afterColumn = 58 | formatDelta(diff.minified, deltaFormat) + 59 | ' ' + 60 | minifiedAfter + 61 | '\n' + 62 | formatDelta(diff.gzip, deltaFormat) + 63 | ' ' + 64 | gzippedAfter; 65 | 66 | reportOutput.push([fixtureColumn, beforeColumn, afterColumn]); 67 | }); 68 | 69 | logger.raw(reportOutput.toString()); 70 | logger.raw(''); 71 | logger.raw(footer); 72 | }; 73 | -------------------------------------------------------------------------------- /packages/monosize/src/reporters/cliReporter.test.mts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi'; 2 | import { describe, it, expect, vitest } from 'vitest'; 3 | 4 | import { cliReporter } from './cliReporter.mjs'; 5 | import { sampleComparedReport } from '../__fixture__/sampleComparedReport.mjs'; 6 | import { logger } from '../logger.mjs'; 7 | 8 | function noop() { 9 | /* does nothing */ 10 | } 11 | 12 | // We are using "chalk" and "cli-table3" in this reporter, they are adding colors to the output via escape codes that 13 | // makes snapshots look ugly. 14 | // 15 | // It could be disabled for "chalk" but "colors" that is used "cli-table3" is not our dependency. 16 | expect.addSnapshotSerializer({ 17 | test(val) { 18 | return typeof val === 'string'; 19 | }, 20 | print(val) { 21 | return stripAnsi(val as string); 22 | }, 23 | }); 24 | 25 | describe('cliReporter', () => { 26 | const options = { 27 | repository: 'https://github.com/microsoft/monosize', 28 | commitSHA: 'commit-hash', 29 | showUnchanged: false, 30 | deltaFormat: 'percent' as const, 31 | }; 32 | 33 | it('wont render anything if there is nothing to compare', () => { 34 | const logSpy = vitest.spyOn(logger, 'success').mockImplementation(noop); 35 | 36 | cliReporter([], options); 37 | 38 | expect(logSpy.mock.calls[0][0]).toMatchInlineSnapshot('No changes found'); 39 | }); 40 | 41 | it('renders a report to CLI output', () => { 42 | const logSpy = vitest.spyOn(console, 'log').mockImplementation(noop); 43 | 44 | cliReporter(sampleComparedReport, options); 45 | 46 | expect(logSpy.mock.calls[0][0]).toMatchInlineSnapshot(` 47 | ┌────────────────────┬────────┬───────────────────────┐ 48 | │ Fixture │ Before │ After (minified/GZIP) │ 49 | ├────────────────────┼────────┼───────────────────────┤ 50 | │ baz-package │ 0 B │ 100%↑ 1 kB │ 51 | │ An entry with diff │ 0 B │ 100%↑ 100 B │ 52 | ├────────────────────┼────────┼───────────────────────┤ 53 | │ foo-package │ N/A │ 100%↑ 1 kB │ 54 | │ New entry (new) │ N/A │ 100%↑ 100 B │ 55 | └────────────────────┴────────┴───────────────────────┘ 56 | `); 57 | }); 58 | 59 | it('renders a report to CLI output with specified "deltaFormat"', () => { 60 | const logSpy = vitest.spyOn(logger, 'raw').mockImplementation(noop); 61 | 62 | cliReporter(sampleComparedReport, { ...options, deltaFormat: 'delta' }); 63 | 64 | expect(logSpy.mock.calls[0][0]).toMatchInlineSnapshot(` 65 | ┌────────────────────┬────────┬───────────────────────┐ 66 | │ Fixture │ Before │ After (minified/GZIP) │ 67 | ├────────────────────┼────────┼───────────────────────┤ 68 | │ baz-package │ 0 B │ 1 kB↑ 1 kB │ 69 | │ An entry with diff │ 0 B │ 100 B↑ 100 B │ 70 | ├────────────────────┼────────┼───────────────────────┤ 71 | │ foo-package │ N/A │ 1 B↑ 1 kB │ 72 | │ New entry (new) │ N/A │ 1 B↑ 100 B │ 73 | └────────────────────┴────────┴───────────────────────┘ 74 | `); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/monosize/src/reporters/markdownReporter.mts: -------------------------------------------------------------------------------- 1 | import { getChangedEntriesInReport } from '../utils/getChangedEntriesInReport.mjs'; 2 | import { formatBytes } from '../utils/helpers.mjs'; 3 | import type { DiffByMetric } from '../utils/calculateDiffByMetric.mjs'; 4 | import { formatDeltaFactory, type Reporter } from './shared.mjs'; 5 | import { logger } from '../logger.mjs'; 6 | 7 | const icons = { increase: 'increase.png', decrease: 'decrease.png' }; 8 | 9 | function getDirectionSymbol(value: number): string { 10 | const img = (iconName: string) => 11 | ``; 12 | 13 | if (value < 0) { 14 | return img(icons.decrease); 15 | } 16 | 17 | if (value > 0) { 18 | return img(icons.increase); 19 | } 20 | 21 | return ''; 22 | } 23 | 24 | function formatDelta(diff: DiffByMetric, deltaFormat: keyof DiffByMetric): string { 25 | const output = formatDeltaFactory(diff, { deltaFormat, directionSymbol: getDirectionSymbol }); 26 | 27 | return typeof output === 'string' ? output : `\`${output.deltaOutput}\` ${output.dirSymbol}`; 28 | } 29 | 30 | export const markdownReporter: Reporter = (report, options) => { 31 | const { commitSHA, repository, showUnchanged, deltaFormat } = options; 32 | const footer = `🤖 This report was generated against ${commitSHA}`; 33 | 34 | const { changedEntries, unchangedEntries } = getChangedEntriesInReport(report); 35 | 36 | const reportOutput = ['## 📊 Bundle size report', '']; 37 | 38 | if (changedEntries.length === 0) { 39 | reportOutput.push(`✅ No changes found`); 40 | logger.raw(reportOutput.join('\n')); 41 | return; 42 | } 43 | 44 | if (changedEntries.length > 0) { 45 | reportOutput.push('| Package & Exports | Baseline (minified/GZIP) | PR | Change |'); 46 | reportOutput.push('| :---------------- | -----------------------: | ----: | ---------: |'); 47 | 48 | changedEntries.forEach(entry => { 49 | const title = `${entry.packageName}
${entry.name}`; 50 | const before = entry.diff.empty 51 | ? [`\`${formatBytes(0)}\``, '
', `\`${formatBytes(0)}\``].join('') 52 | : [ 53 | `\`${formatBytes(entry.minifiedSize - entry.diff.minified.delta)}\``, 54 | '
', 55 | `\`${formatBytes(entry.gzippedSize - entry.diff.gzip.delta)}\``, 56 | ].join(''); 57 | const after = [`\`${formatBytes(entry.minifiedSize)}\``, '
', `\`${formatBytes(entry.gzippedSize)}\``].join( 58 | '', 59 | ); 60 | const difference = entry.diff.empty 61 | ? '🆕 New entry' 62 | : [ 63 | `${formatDelta(entry.diff.minified, deltaFormat)}`, 64 | '
', 65 | `${formatDelta(entry.diff.gzip, deltaFormat)}`, 66 | ].join(''); 67 | 68 | reportOutput.push(`| ${title} | ${before} | ${after} | ${difference}|`); 69 | }); 70 | 71 | reportOutput.push(''); 72 | } 73 | 74 | if (showUnchanged && unchangedEntries.length > 0) { 75 | reportOutput.push('
'); 76 | reportOutput.push('Unchanged fixtures'); 77 | reportOutput.push(''); 78 | 79 | reportOutput.push('| Package & Exports | Size (minified/GZIP) |'); 80 | reportOutput.push('| ----------------- | -------------------: |'); 81 | 82 | unchangedEntries.forEach(entry => { 83 | const title = `${entry.packageName}
${entry.name}`; 84 | const size = [`\`${formatBytes(entry.minifiedSize)}\``, '
', `\`${formatBytes(entry.gzippedSize)}\``].join( 85 | '', 86 | ); 87 | 88 | reportOutput.push(`| ${title} | ${size} |`); 89 | }); 90 | 91 | reportOutput.push('
'); 92 | } 93 | 94 | // TODO: use repo settings 95 | reportOutput.push(footer); 96 | 97 | logger.raw(reportOutput.join('\n')); 98 | }; 99 | -------------------------------------------------------------------------------- /packages/monosize/src/reporters/markdownReporter.test.mts: -------------------------------------------------------------------------------- 1 | import prettier from 'prettier'; 2 | import { describe, expect, it, vitest } from 'vitest'; 3 | 4 | import { sampleComparedReport } from '../__fixture__/sampleComparedReport.mjs'; 5 | import { logger } from '../logger.mjs'; 6 | import { markdownReporter } from './markdownReporter.mjs'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | const noop = () => {}; 10 | 11 | describe('markdownReporter', () => { 12 | const options = { 13 | repository: 'https://github.com/microsoft/monosize', 14 | commitSHA: 'commit-hash', 15 | showUnchanged: true, 16 | deltaFormat: 'delta' as const, 17 | }; 18 | 19 | it('wont render anything if there is nothing to compare', async () => { 20 | const log = vitest.spyOn(logger, 'raw').mockImplementation(noop); 21 | 22 | markdownReporter([], options); 23 | const output = await prettier.format(log.mock.calls[0][0] as string, { parser: 'markdown' }); 24 | 25 | expect(output).toMatchInlineSnapshot(` 26 | "## 📊 Bundle size report 27 | 28 | ✅ No changes found 29 | " 30 | `); 31 | }); 32 | 33 | it('renders a report to a file', async () => { 34 | const rawLog = vitest.spyOn(logger, 'raw').mockImplementation(noop); 35 | 36 | markdownReporter(sampleComparedReport, options); 37 | const output = await prettier.format(rawLog.mock.calls[0][0] as string, { parser: 'markdown' }); 38 | 39 | expect(output).toMatchSnapshot(); 40 | }); 41 | 42 | it('renders a report to a file with specified "deltaFormat"', async () => { 43 | const log = vitest.spyOn(logger, 'raw').mockImplementation(noop); 44 | 45 | markdownReporter(sampleComparedReport, { ...options, deltaFormat: 'percent' }); 46 | const output = await prettier.format(log.mock.calls[0][0] as string, { parser: 'markdown' }); 47 | 48 | expect(output).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/monosize/src/reporters/shared.mts: -------------------------------------------------------------------------------- 1 | import type { DiffByMetric } from '../utils/calculateDiffByMetric.mjs'; 2 | import type { ComparedReport } from '../utils/compareResultsInReports.mjs'; 3 | import { formatBytes } from '../utils/helpers.mjs'; 4 | 5 | export type Reporter = ( 6 | report: ComparedReport, 7 | options: { commitSHA: string; repository: string; showUnchanged: boolean; deltaFormat: keyof DiffByMetric }, 8 | ) => void; 9 | 10 | export function formatDeltaFactory( 11 | diff: DiffByMetric, 12 | options: { deltaFormat: keyof DiffByMetric; directionSymbol: (value: number) => string }, 13 | ) { 14 | const { deltaFormat, directionSymbol } = options; 15 | 16 | if (diff.delta === 0) { 17 | return ''; 18 | } 19 | 20 | const deltaOutput = deltaFormat === 'delta' ? formatBytes(diff[deltaFormat]) : diff[deltaFormat]; 21 | 22 | return { deltaOutput, dirSymbol: directionSymbol(diff.delta) }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/monosize/src/types.mts: -------------------------------------------------------------------------------- 1 | export type BuildResult = { 2 | name: string; 3 | path: string; 4 | minifiedSize: number; 5 | gzippedSize: number; 6 | }; 7 | 8 | export type BundleSizeReportEntry = Pick & { 9 | packageName: string; 10 | }; 11 | export type BundleSizeReport = BundleSizeReportEntry[]; 12 | 13 | // 14 | // Storage 15 | 16 | export type StorageAdapter = { 17 | getRemoteReport(branch: string): Promise<{ commitSHA: string; remoteReport: BundleSizeReport }>; 18 | uploadReportToRemote(branch: string, commitSHA: string, localReport: BundleSizeReport): Promise; 19 | }; 20 | 21 | // 22 | // Bundlers 23 | 24 | export type BundlerAdapter = { 25 | buildFixture: (options: { fixturePath: string; debug: boolean; quiet: boolean }) => Promise<{ 26 | outputPath: string; 27 | debugOutputPath?: string; 28 | }>; 29 | 30 | /** A friendly name of the bundler used for logging. */ 31 | name: string; 32 | }; 33 | 34 | /** 35 | * @kind shared 36 | */ 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | export type BundlerAdapterFactoryConfig> = (config: T) => T; 39 | 40 | /** 41 | * @kind shared 42 | */ 43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 44 | export type BundleAdapterFactory> = ( 45 | options: BundlerAdapterFactoryConfig, 46 | ) => BundlerAdapter; 47 | 48 | type ReportResolvers = { 49 | /** 50 | * Override package name resolution used within compare-reports and upload-report. 51 | * 52 | * By default we try to read package name from "package.json" or "project.json" files. 53 | * You can override this behavior by providing your own implementation. 54 | */ 55 | packageName?: (packageRoot: string) => Promise; 56 | /** 57 | * 58 | * Override package root resolution used within compare-reports and upload-report. 59 | * 60 | * By default we try to resolve package root by traversing up the directory tree until we find "package.json" or "project.json" files. 61 | * You can override this behavior by providing your own implementation. 62 | * 63 | * @param reportFilePath - absolute path to the report file (monosize.json) 64 | */ 65 | packageRoot?: (reportFilePath: string) => Promise; 66 | }; 67 | 68 | export type MonoSizeConfig = { 69 | repository: string; 70 | storage: StorageAdapter; 71 | bundler: BundlerAdapter; 72 | /** 73 | * Report Commands Configuration Overrides 74 | * Use this if you need to customize package name or package root resolution logic within bundle reports. 75 | */ 76 | reportResolvers?: ReportResolvers; 77 | }; 78 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/calculateDiffByMetric.mts: -------------------------------------------------------------------------------- 1 | import type { BundleSizeReportEntry } from '../types.mjs'; 2 | 3 | export type DiffByMetric = { delta: number; percent: string }; 4 | 5 | const formatter = new Intl.NumberFormat([], { style: 'percent', maximumSignificantDigits: 3 }); 6 | 7 | function roundNumber(value: number, fractionDigits: number): number { 8 | return Number(value.toFixed(fractionDigits)); 9 | } 10 | 11 | function formatPercent(fraction: number): string { 12 | if (fraction < 0.001) { 13 | return formatter.format(roundNumber(fraction, 4)); 14 | } 15 | 16 | if (fraction < 0.01) { 17 | return formatter.format(roundNumber(fraction, 3)); 18 | } 19 | 20 | return formatter.format(roundNumber(fraction, 2)); 21 | } 22 | 23 | export function calculateDiffByMetric( 24 | local: BundleSizeReportEntry, 25 | remote: BundleSizeReportEntry, 26 | property: 'minifiedSize' | 'gzippedSize', 27 | ): DiffByMetric { 28 | const delta = local[property] - remote[property]; 29 | const percent = remote[property] === 0 ? 0 : delta / remote[property]; 30 | 31 | return { delta, percent: formatPercent(percent) }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/calculateDiffByMetric.test.mts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { calculateDiffByMetric } from './calculateDiffByMetric.mjs'; 4 | import type { BundleSizeReportEntry } from '../types.mjs'; 5 | 6 | describe('calculateDiffByMetric', () => { 7 | it('calculates difference deltas and percents', () => { 8 | const remoteEntry: BundleSizeReportEntry = { 9 | packageName: 'test-package', 10 | name: 'Test', 11 | path: 'test.fixture.js', 12 | minifiedSize: 1000, 13 | gzippedSize: 100, 14 | }; 15 | const localEntry: BundleSizeReportEntry = { 16 | packageName: 'test-package', 17 | name: 'Test', 18 | path: 'test.fixture.js', 19 | minifiedSize: 1500, 20 | gzippedSize: 150, 21 | }; 22 | 23 | expect(calculateDiffByMetric(localEntry, remoteEntry, 'minifiedSize')).toEqual({ 24 | delta: 500, 25 | percent: '50%', 26 | }); 27 | expect(calculateDiffByMetric(localEntry, remoteEntry, 'gzippedSize')).toEqual({ 28 | delta: 50, 29 | percent: '50%', 30 | }); 31 | }); 32 | 33 | it('handles zero values', () => { 34 | const remoteEntry: BundleSizeReportEntry = { 35 | packageName: 'test-package', 36 | name: 'Test', 37 | path: 'test.fixture.js', 38 | minifiedSize: 0, 39 | gzippedSize: 0, 40 | }; 41 | const localEntry: BundleSizeReportEntry = { 42 | packageName: 'test-package', 43 | name: 'Test', 44 | path: 'test.fixture.js', 45 | minifiedSize: 0, 46 | gzippedSize: 0, 47 | }; 48 | 49 | expect(calculateDiffByMetric(localEntry, remoteEntry, 'minifiedSize')).toEqual({ 50 | delta: 0, 51 | percent: '0%', 52 | }); 53 | expect(calculateDiffByMetric(localEntry, remoteEntry, 'gzippedSize')).toEqual({ 54 | delta: 0, 55 | percent: '0%', 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/collectLocalReport.mts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import { execSync } from 'node:child_process'; 5 | import { findUp } from 'find-up'; 6 | 7 | import type { BuildResult, BundleSizeReport, MonoSizeConfig } from '../types.mjs'; 8 | 9 | async function getPackageRoot(reportFilePath: string): Promise { 10 | const rootConfig = await findUp(['package.json', 'project.json'], { cwd: path.dirname(reportFilePath) }); 11 | 12 | if (!rootConfig) { 13 | throw new Error( 14 | [ 15 | 'Failed to find a package root (directory that contains "package.json" or "project.json" file)', 16 | `Report file location: ${reportFilePath}`, 17 | `Tip: You can override package root resolution by providing "packageRoot" function in the configuration`, 18 | ].join('\n'), 19 | ); 20 | } 21 | 22 | return path.dirname(rootConfig); 23 | } 24 | 25 | async function getPackageName(packageRoot: string): Promise { 26 | const paths = { 27 | packageJson: path.join(packageRoot, 'package.json'), 28 | projectJson: path.join(packageRoot, 'project.json'), 29 | }; 30 | let getPackageNameFromConfigFile; 31 | 32 | if (fs.existsSync(paths.packageJson)) { 33 | getPackageNameFromConfigFile = async () => JSON.parse(fs.readFileSync(paths.packageJson, 'utf8')).name; 34 | } 35 | if (fs.existsSync(paths.projectJson)) { 36 | getPackageNameFromConfigFile = async () => JSON.parse(fs.readFileSync(paths.projectJson, 'utf8')).name; 37 | } 38 | 39 | if (!getPackageNameFromConfigFile) { 40 | throw new Error( 41 | [ 42 | 'Package root does not contain "package.json" or "project.json" file', 43 | `Package root location: ${packageRoot}`, 44 | `Tip: If you use 'packageRoot' config override make sure that it returns one of 'package.json' | 'project.json' file paths or provide also 'packageName' config override that accommodates your packageRoot resolution logic.`, 45 | ].join('\n'), 46 | ); 47 | } 48 | 49 | try { 50 | const packageName = await getPackageNameFromConfigFile(); 51 | 52 | return packageName; 53 | } catch (err) { 54 | throw new Error( 55 | [`Failed to read/parse package name from "${packageRoot}" file`, 'Original Error:', err].join('\n'), 56 | ); 57 | } 58 | } 59 | 60 | /** 61 | * 62 | * @param reportFile - absolute path to the report file 63 | */ 64 | async function readReportForPackage( 65 | reportFile: string, 66 | resolvers: typeof defaultResolvers, 67 | ): Promise<{ packageName: string; packageReport: BuildResult[] }> { 68 | const packageRoot = await resolvers.packageRoot(reportFile); 69 | const packageName = await resolvers.packageName(packageRoot); 70 | 71 | try { 72 | const packageReport: BuildResult[] = JSON.parse(fs.readFileSync(reportFile, 'utf8')); 73 | 74 | return { packageName, packageReport }; 75 | } catch (e) { 76 | throw new Error([`Failed to read JSON from "${reportFile}":`, (e as Error).toString()].join('\n')); 77 | } 78 | } 79 | 80 | type CollectLocalReportOptions = { 81 | root: string | undefined; 82 | reportFilesGlob: string; 83 | }; 84 | 85 | type Resolvers = Pick; 86 | const defaultResolvers = { packageName: getPackageName, packageRoot: getPackageRoot }; 87 | 88 | interface Options extends Partial, Resolvers {} 89 | 90 | /** 91 | * Collects all reports for packages to a single one. 92 | */ 93 | export async function collectLocalReport(options: Options): Promise { 94 | const { 95 | reportResolvers, 96 | reportFilesGlob = 'packages/**/dist/bundle-size/monosize.json', 97 | root = findGitRoot(process.cwd()), 98 | } = options; 99 | 100 | const resolvers = { ...defaultResolvers, ...reportResolvers }; 101 | 102 | const reportFiles = await glob(reportFilesGlob, { absolute: true, cwd: root }); 103 | const reports = await Promise.all(reportFiles.map(reportFile => readReportForPackage(reportFile, resolvers))); 104 | 105 | return reports.reduce((acc, { packageName, packageReport }) => { 106 | const processedReport = packageReport.map(reportEntry => ({ packageName, ...reportEntry })); 107 | 108 | return [...acc, ...processedReport]; 109 | }, []) 110 | .sort((a, b) => a.path.localeCompare(b.path, 'en')); 111 | } 112 | 113 | function findGitRoot(cwd: string) { 114 | const output = execSync('git rev-parse --show-toplevel', { cwd }); 115 | 116 | return output.toString().trim(); 117 | } 118 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/collectLocalReport.test.mts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 2 | 3 | import fs from 'node:fs'; 4 | import path from 'node:path'; 5 | import tmp from 'tmp'; 6 | import { findUp } from 'find-up'; 7 | 8 | import { collectLocalReport } from './collectLocalReport.mjs'; 9 | import type { BuildResult } from '../types.mjs'; 10 | 11 | function mkPackagesDir() { 12 | const projectDir = tmp.dirSync({ prefix: 'collectLocalReport', unsafeCleanup: true }); 13 | const packagesDir = tmp.dirSync({ dir: projectDir.name, name: 'packages', unsafeCleanup: true }); 14 | 15 | // is required as root directory is determined based on Git project 16 | tmp.dirSync({ dir: projectDir.name, name: '.git', unsafeCleanup: true }); 17 | 18 | return { packagesDir: packagesDir.name, rootDir: projectDir.name }; 19 | } 20 | 21 | function mkReportDir(packagesDir: string, packageName: string, packageRootConfigName: string) { 22 | const packageRoot = tmp.dirSync({ dir: packagesDir, name: packageName, unsafeCleanup: true }).name; 23 | const distDir = tmp.dirSync({ dir: packageRoot, name: 'dist', unsafeCleanup: true }).name; 24 | const monosizeDir = tmp.dirSync({ dir: distDir, name: 'bundle-size', unsafeCleanup: true }); 25 | 26 | const packageRootConfigPath = tmp.fileSync({ dir: packageRoot, name: packageRootConfigName }).name; 27 | fs.writeFileSync(packageRootConfigPath, JSON.stringify({ name: packageName })); 28 | 29 | const reportPath = tmp.fileSync({ dir: monosizeDir.name, name: 'monosize.json' }).name; 30 | 31 | return reportPath; 32 | } 33 | 34 | describe('collectLocalReport', () => { 35 | beforeEach(() => { 36 | vitest.resetAllMocks(); 37 | }); 38 | 39 | it('aggregates all local reports to a single one', async () => { 40 | const { packagesDir, rootDir } = mkPackagesDir(); 41 | 42 | const reportAPath = mkReportDir(packagesDir, 'package-a', 'package.json'); 43 | const reportBPath = mkReportDir(packagesDir, 'package-b', 'project.json'); 44 | 45 | const reportA: BuildResult[] = [ 46 | { name: 'fixtureA1', path: 'path/fixtureA1.js', minifiedSize: 100, gzippedSize: 50 }, 47 | { name: 'fixtureA2', path: 'path/fixtureA2.js', minifiedSize: 200, gzippedSize: 100 }, 48 | ]; 49 | const reportB: BuildResult[] = [{ name: 'fixtureB', path: 'path/fixtureB.js', minifiedSize: 10, gzippedSize: 5 }]; 50 | 51 | await fs.promises.writeFile(reportAPath, JSON.stringify(reportA)); 52 | await fs.promises.writeFile(reportBPath, JSON.stringify(reportB)); 53 | 54 | expect(await collectLocalReport({ root: rootDir, reportFilesGlob: undefined })).toMatchInlineSnapshot(` 55 | [ 56 | { 57 | "gzippedSize": 50, 58 | "minifiedSize": 100, 59 | "name": "fixtureA1", 60 | "packageName": "package-a", 61 | "path": "path/fixtureA1.js", 62 | }, 63 | { 64 | "gzippedSize": 100, 65 | "minifiedSize": 200, 66 | "name": "fixtureA2", 67 | "packageName": "package-a", 68 | "path": "path/fixtureA2.js", 69 | }, 70 | { 71 | "gzippedSize": 5, 72 | "minifiedSize": 10, 73 | "name": "fixtureB", 74 | "packageName": "package-b", 75 | "path": "path/fixtureB.js", 76 | }, 77 | ] 78 | `); 79 | }); 80 | 81 | it('throws an error if a report file contains invalid JSON', async () => { 82 | const { packagesDir, rootDir } = mkPackagesDir(); 83 | 84 | const reportAPath = mkReportDir(packagesDir, 'package-a', 'package.json'); 85 | const reportBPath = mkReportDir(packagesDir, 'package-b', 'project.json'); 86 | 87 | const reportB: BuildResult[] = [{ name: 'fixtureB', path: 'path/fixtureB.js', minifiedSize: 10, gzippedSize: 5 }]; 88 | 89 | await fs.promises.writeFile(reportAPath, '{ name: "fixture", }'); 90 | await fs.promises.writeFile(reportBPath, JSON.stringify(reportB)); 91 | 92 | await expect(collectLocalReport({ root: rootDir })).rejects.toThrow(/Failed to read JSON/); 93 | }); 94 | 95 | describe('resolver overrides', () => { 96 | it('should create local report based on packageName config override', async () => { 97 | const { packagesDir, rootDir } = mkPackagesDir(); 98 | 99 | const reportAPath = mkReportDir(packagesDir, 'package-a', 'package.json'); 100 | const reportBPath = mkReportDir(packagesDir, 'package-b', 'project.json'); 101 | 102 | const reportA: BuildResult[] = [ 103 | { name: 'fixtureA1', path: 'path/fixtureA1.js', minifiedSize: 100, gzippedSize: 50 }, 104 | ]; 105 | const reportB: BuildResult[] = [{ name: 'fixtureB', path: 'path/fixtureB.js', minifiedSize: 10, gzippedSize: 5 }]; 106 | 107 | await fs.promises.writeFile(reportAPath, JSON.stringify(reportA)); 108 | await fs.promises.writeFile(reportBPath, JSON.stringify(reportB)); 109 | 110 | const actual = ( 111 | await collectLocalReport({ 112 | root: rootDir, 113 | reportResolvers: { 114 | packageName: async packageRoot => { 115 | if (fs.existsSync(path.join(packageRoot, 'package.json'))) { 116 | return ( 117 | JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')).name + '-overridden-pkg' 118 | ); 119 | } 120 | if (fs.existsSync(path.join(packageRoot, 'project.json'))) { 121 | return ( 122 | JSON.parse(fs.readFileSync(path.join(packageRoot, 'project.json'), 'utf-8')).name + 123 | '-overridden-project' 124 | ); 125 | } 126 | 127 | return 'unknown'; 128 | }, 129 | }, 130 | }) 131 | ).map(({ packageName }) => ({ packageName })); 132 | 133 | expect(actual).toMatchInlineSnapshot(` 134 | [ 135 | { 136 | "packageName": "package-a-overridden-pkg", 137 | }, 138 | { 139 | "packageName": "package-b-overridden-project", 140 | }, 141 | ] 142 | `); 143 | }); 144 | 145 | it('should local report based on packageRoot and packageName config overrides', async () => { 146 | const { packagesDir, rootDir } = mkPackagesDir(); 147 | 148 | const reportAPath = mkReportDir(packagesDir, 'package-a', 'johny5.json'); 149 | const reportBPath = mkReportDir(packagesDir, 'package-b', 'johny5.json'); 150 | 151 | const reportA: BuildResult[] = [ 152 | { name: 'fixtureA1', path: 'path/fixtureA1.js', minifiedSize: 100, gzippedSize: 50 }, 153 | ]; 154 | const reportB: BuildResult[] = [{ name: 'fixtureB', path: 'path/fixtureB.js', minifiedSize: 10, gzippedSize: 5 }]; 155 | 156 | await fs.promises.writeFile(reportAPath, JSON.stringify(reportA)); 157 | await fs.promises.writeFile(reportBPath, JSON.stringify(reportB)); 158 | 159 | const actual = ( 160 | await collectLocalReport({ 161 | root: rootDir, 162 | reportResolvers: { 163 | packageRoot: async reportFile => { 164 | const rootConfig = await findUp('johny5.json', { cwd: path.dirname(reportFile) }); 165 | 166 | if (rootConfig) { 167 | return path.dirname(rootConfig); 168 | } 169 | 170 | return 'unknown'; 171 | }, 172 | packageName: async packageRoot => { 173 | return ( 174 | JSON.parse(fs.readFileSync(path.join(packageRoot, 'johny5.json'), 'utf-8')).name + '-not-disassembled' 175 | ); 176 | }, 177 | }, 178 | }) 179 | ).map(({ packageName }) => ({ packageName })); 180 | 181 | expect(actual).toMatchInlineSnapshot(` 182 | [ 183 | { 184 | "packageName": "package-a-not-disassembled", 185 | }, 186 | { 187 | "packageName": "package-b-not-disassembled", 188 | }, 189 | ] 190 | `); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/compareResultsInReports.mts: -------------------------------------------------------------------------------- 1 | import { calculateDiffByMetric, DiffByMetric } from './calculateDiffByMetric.mjs'; 2 | import type { BundleSizeReport, BundleSizeReportEntry } from '../types.mjs'; 3 | 4 | type DiffForEntry = { empty: boolean; minified: DiffByMetric; gzip: DiffByMetric }; 5 | 6 | export type ComparedReportEntry = BundleSizeReportEntry & { diff: DiffForEntry }; 7 | export type ComparedReport = ComparedReportEntry[]; 8 | 9 | export const emptyDiff: DiffForEntry = Object.freeze({ 10 | empty: true, 11 | 12 | minified: { delta: 1, percent: '100%' }, 13 | gzip: { delta: 1, percent: '100%' }, 14 | }); 15 | 16 | export function compareResultsInReports(localReport: BundleSizeReport, remoteReport: BundleSizeReport): ComparedReport { 17 | return localReport.map(localEntry => { 18 | const remoteEntry = remoteReport.find( 19 | entry => localEntry.packageName === entry.packageName && localEntry.path === entry.path, 20 | ); 21 | const diff = remoteEntry 22 | ? { 23 | empty: false, 24 | minified: calculateDiffByMetric(localEntry, remoteEntry, 'minifiedSize'), 25 | gzip: calculateDiffByMetric(localEntry, remoteEntry, 'gzippedSize'), 26 | } 27 | : emptyDiff; 28 | 29 | return { 30 | ...localEntry, 31 | diff, 32 | }; 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/compareResultsInReports.test.mts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { compareResultsInReports } from './compareResultsInReports.mjs'; 4 | import type { BundleSizeReport } from '../types.mjs'; 5 | 6 | describe('compareResultsInReports', () => { 7 | it('compares local and remote reports', () => { 8 | const localReport: BundleSizeReport = [ 9 | { packageName: 'abc', name: 'abc-a', path: 'abc-b.js', minifiedSize: 10, gzippedSize: 5 }, 10 | { packageName: 'abc', name: 'abc-b', path: 'abc-a.js', minifiedSize: 10, gzippedSize: 5 }, 11 | { packageName: 'xyz', name: 'xyz', path: 'xyz.js', minifiedSize: 10, gzippedSize: 5 }, 12 | ]; 13 | const remoteReport: BundleSizeReport = [ 14 | { packageName: 'abc', name: 'abc-a', path: 'abc-b.js', minifiedSize: 12, gzippedSize: 7 }, 15 | { packageName: 'xyz', name: 'xyz', path: 'xyz.js', minifiedSize: 10, gzippedSize: 5 }, 16 | ]; 17 | 18 | const actual = compareResultsInReports(localReport, remoteReport); 19 | const packageAbcReport = { 20 | fileAbcA: actual[0], 21 | fileAbcB: actual[1], 22 | }; 23 | const packageXyzReport = actual[2]; 24 | 25 | expect(packageAbcReport.fileAbcA).toMatchInlineSnapshot(` 26 | { 27 | "diff": { 28 | "empty": false, 29 | "gzip": { 30 | "delta": -2, 31 | "percent": "-28.6%", 32 | }, 33 | "minified": { 34 | "delta": -2, 35 | "percent": "-16.7%", 36 | }, 37 | }, 38 | "gzippedSize": 5, 39 | "minifiedSize": 10, 40 | "name": "abc-a", 41 | "packageName": "abc", 42 | "path": "abc-b.js", 43 | } 44 | `); 45 | expect(packageAbcReport.fileAbcB).toMatchInlineSnapshot(` 46 | { 47 | "diff": { 48 | "empty": true, 49 | "gzip": { 50 | "delta": 1, 51 | "percent": "100%", 52 | }, 53 | "minified": { 54 | "delta": 1, 55 | "percent": "100%", 56 | }, 57 | }, 58 | "gzippedSize": 5, 59 | "minifiedSize": 10, 60 | "name": "abc-b", 61 | "packageName": "abc", 62 | "path": "abc-a.js", 63 | } 64 | `); 65 | expect(packageXyzReport).toMatchInlineSnapshot(` 66 | { 67 | "diff": { 68 | "empty": false, 69 | "gzip": { 70 | "delta": 0, 71 | "percent": "0%", 72 | }, 73 | "minified": { 74 | "delta": 0, 75 | "percent": "0%", 76 | }, 77 | }, 78 | "gzippedSize": 5, 79 | "minifiedSize": 10, 80 | "name": "xyz", 81 | "packageName": "xyz", 82 | "path": "xyz.js", 83 | } 84 | `); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/getChangedEntriesInReport.mts: -------------------------------------------------------------------------------- 1 | import { sortComparedReport } from './sortComparedReport.mjs'; 2 | import { ComparedReport } from './compareResultsInReports.mjs'; 3 | 4 | type EntriesInReport = { 5 | changedEntries: ComparedReport; 6 | unchangedEntries: ComparedReport; 7 | }; 8 | 9 | export function getChangedEntriesInReport(report: ComparedReport): EntriesInReport { 10 | const { changedEntries, unchangedEntries } = report.reduce( 11 | (acc, reportEntry) => { 12 | if (reportEntry.diff.gzip.delta === 0 && reportEntry.diff.minified.delta === 0) { 13 | acc.unchangedEntries.push(reportEntry); 14 | return acc; 15 | } 16 | 17 | acc.changedEntries.push(reportEntry); 18 | return acc; 19 | }, 20 | { changedEntries: [], unchangedEntries: [] }, 21 | ); 22 | 23 | return { 24 | changedEntries: sortComparedReport(changedEntries), 25 | unchangedEntries: sortComparedReport(unchangedEntries), 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/getChangedEntriesInReport.test.mts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { ComparedReport, emptyDiff } from './compareResultsInReports.mjs'; 4 | import { getChangedEntriesInReport } from './getChangedEntriesInReport.mjs'; 5 | 6 | describe('getChangedEntriesInReport', () => { 7 | it('splits entries to changed an unchanged', () => { 8 | const report: ComparedReport = [ 9 | { packageName: 'abc', name: 'abc-a', path: 'abc-a.js', minifiedSize: 0, gzippedSize: 0, diff: emptyDiff }, 10 | { 11 | packageName: 'abc', 12 | name: 'abc-b', 13 | path: 'abc-b.js', 14 | minifiedSize: 0, 15 | gzippedSize: 0, 16 | diff: { 17 | empty: false, 18 | 19 | minified: { delta: 0, percent: '0%' }, 20 | gzip: { delta: 0, percent: '0%' }, 21 | }, 22 | }, 23 | { packageName: 'xyz', name: 'xyz', path: 'xyz.js', minifiedSize: 0, gzippedSize: 0, diff: emptyDiff }, 24 | ]; 25 | const actual = getChangedEntriesInReport(report); 26 | 27 | expect(actual.changedEntries).toHaveLength(2); 28 | expect(actual.changedEntries[0]).toEqual(report[0]); 29 | expect(actual.changedEntries[1]).toEqual(report[2]); 30 | 31 | expect(actual.unchangedEntries).toHaveLength(1); 32 | expect(actual.unchangedEntries[0]).toEqual(report[1]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/helpers.mts: -------------------------------------------------------------------------------- 1 | import prettyBytes from 'pretty-bytes'; 2 | import process from 'node:process'; 3 | 4 | /** 5 | * Formats a number of bytes into a human-readable string. 6 | * 7 | * @param value - The number of bytes to format. 8 | */ 9 | export function formatBytes(value: number): string { 10 | return prettyBytes(value, { maximumFractionDigits: 3 }); 11 | } 12 | 13 | export function formatHrTime(hrtime: ReturnType): string { 14 | const number = hrtime[0] * 1e9 + hrtime[1]; 15 | 16 | if (number >= 1e9) { 17 | return (number / 1e9).toFixed(2) + 's'; 18 | } 19 | 20 | return (number / 1e6).toFixed(0) + 'ms'; 21 | } 22 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/helpers.test.mts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { formatBytes, formatHrTime } from './helpers.mjs'; 3 | 4 | describe('formatBytes', () => { 5 | test('formats bytes to human-readable string', () => { 6 | expect(formatBytes(124)).toBe('124 B'); 7 | expect(formatBytes(1536)).toBe('1.536 kB'); 8 | expect(formatBytes(1624857)).toBe('1.625 MB'); 9 | }); 10 | }); 11 | 12 | describe('formatHrTime', () => { 13 | test('formats hrtime to seconds', () => { 14 | const hrtime = [1, 500000000] satisfies [number, number]; // 1.5 seconds 15 | expect(formatHrTime(hrtime)).toBe('1.50s'); 16 | }); 17 | 18 | test('formats hrtime to milliseconds', () => { 19 | const hrtime = [0, 150000000] satisfies [number, number]; // 0.15 seconds 20 | expect(formatHrTime(hrtime)).toBe('150ms'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/prepareFixture.mts: -------------------------------------------------------------------------------- 1 | import { parse } from 'acorn'; 2 | import type ES from 'acorn'; 3 | import fs from 'node:fs'; 4 | import path from 'node:path'; 5 | 6 | export type PreparedFixture = { 7 | artifactPath: string; 8 | name: string; 9 | }; 10 | 11 | /** 12 | * Prepares a fixture file to be compiled with a bundler, grabs data from a default export and removes it. 13 | */ 14 | export async function prepareFixture(artifactDir: string, sourcePath: string): Promise { 15 | const sourceFixtureCode = fs.readFileSync(sourcePath, 'utf8'); 16 | 17 | // A transform that: 18 | // - reads metadata (name, threshold, etc.) 19 | // - removes a default export with metadata 20 | 21 | const program = parse(sourceFixtureCode, { 22 | ecmaVersion: 2020, 23 | sourceType: 'module', 24 | }); 25 | const defaultExport = program.body.find(node => node.type === 'ExportDefaultDeclaration') as 26 | | ES.ExportDefaultDeclaration 27 | | undefined; 28 | 29 | if (!defaultExport) { 30 | throw new Error( 31 | [ 32 | 'A fixture file should contain a default export with metadata.', 33 | "For example: export default { name: 'Test fixture' }", 34 | ].join('\n'), 35 | ); 36 | } 37 | 38 | if (defaultExport.declaration.type !== 'ObjectExpression') { 39 | throw new Error( 40 | ['A default export should be an object expression.', "For example: export default { name: 'Test fixture' }"].join( 41 | '\n', 42 | ), 43 | ); 44 | } 45 | 46 | const exportProperties = defaultExport.declaration.properties; 47 | const name = exportProperties.find( 48 | property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === 'name', 49 | ) as ES.Property | undefined; 50 | 51 | if (!name) { 52 | throw new Error( 53 | [ 54 | 'A default export should contain a property "name".', 55 | "For example: export default { name: 'Test fixture' }", 56 | ].join('\n'), 57 | ); 58 | } 59 | 60 | if (name.value.type !== 'Literal' || typeof name.value.value !== 'string') { 61 | throw new Error( 62 | ['A property "name" should be a string literal.', "For example: export default { name: 'Test fixture' }"].join( 63 | '\n', 64 | ), 65 | ); 66 | } 67 | 68 | const modifiedCode = sourceFixtureCode.slice(0, defaultExport.start) + sourceFixtureCode.slice(defaultExport.end); 69 | const outputFixturePath = path.resolve(artifactDir, path.basename(sourcePath)); 70 | 71 | await fs.promises.mkdir(path.dirname(outputFixturePath), { recursive: true }); 72 | await fs.promises.writeFile(outputFixturePath, modifiedCode); 73 | 74 | return { 75 | artifactPath: outputFixturePath, 76 | name: name.value.value, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/prepareFixture.test.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import tmp from 'tmp'; 4 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 5 | 6 | import { prepareFixture } from './prepareFixture.mjs'; 7 | 8 | async function setup(fixtureContent: string) { 9 | const packageDir = tmp.dirSync({ 10 | prefix: 'prepareFixture', 11 | unsafeCleanup: true, 12 | }); 13 | const artifactsDir = tmp.dirSync({ 14 | prefix: 'prepareFixture-artifacts', 15 | unsafeCleanup: true, 16 | }); 17 | 18 | const spy = vitest.spyOn(process, 'cwd'); 19 | spy.mockReturnValue(packageDir.name); 20 | 21 | const fixtureDir = tmp.dirSync({ 22 | dir: packageDir.name, 23 | name: 'monosize', 24 | unsafeCleanup: true, 25 | }); 26 | const fixture = tmp.fileSync({ 27 | dir: fixtureDir.name, 28 | name: 'test-fixture.js', 29 | }); 30 | 31 | await fs.promises.writeFile(fixture.name, fixtureContent); 32 | 33 | return { 34 | artifactsDir: artifactsDir.name, 35 | fixturePath: fixture.name, 36 | }; 37 | } 38 | 39 | describe('prepareFixture', () => { 40 | beforeEach(() => { 41 | vitest.resetAllMocks(); 42 | }); 43 | 44 | it('reads & removes metadata from a fixture file, writes it to "/dist"', async () => { 45 | const { artifactsDir, fixturePath } = await setup(`import Component from '@react-component'; 46 | export default { name: 'Test fixture' } 47 | `); 48 | const fixtureData = await prepareFixture(artifactsDir, fixturePath); 49 | 50 | expect(fixtureData.artifactPath).toBe(path.resolve(artifactsDir, 'test-fixture.js')); 51 | expect(fixtureData.name).toBe('Test fixture'); 52 | 53 | expect(fs.readFileSync(fixtureData.artifactPath, 'utf8')).toMatchInlineSnapshot( 54 | ` 55 | "import Component from '@react-component'; 56 | 57 | " 58 | `, 59 | ); 60 | }); 61 | 62 | it('throws when metadata is not valid', async () => { 63 | const { artifactsDir, fixturePath } = await setup(`import Component from '@react-component'; 64 | export default { foo: 'bar' } 65 | `); 66 | 67 | await expect(prepareFixture(artifactsDir, fixturePath)).rejects.toMatchInlineSnapshot( 68 | ` 69 | [Error: A default export should contain a property "name". 70 | For example: export default { name: 'Test fixture' }] 71 | `, 72 | ); 73 | }); 74 | 75 | it('throws when metadata is missing', async () => { 76 | const { artifactsDir, fixturePath } = await setup(`import Component from '@fluentui/react-component';`); 77 | 78 | await expect(prepareFixture(artifactsDir, fixturePath)).rejects.toMatchInlineSnapshot(` 79 | [Error: A fixture file should contain a default export with metadata. 80 | For example: export default { name: 'Test fixture' }] 81 | `); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/readConfig.mts: -------------------------------------------------------------------------------- 1 | import { findUp } from 'find-up'; 2 | import { pathToFileURL } from 'node:url'; 3 | 4 | import { logger } from '../logger.mjs'; 5 | import type { MonoSizeConfig } from '../types.mjs'; 6 | 7 | const CONFIG_FILE_NAME = ['monosize.config.js', 'monosize.config.mjs']; 8 | 9 | let cache: MonoSizeConfig | undefined; 10 | 11 | export function resetConfigCache() { 12 | cache = undefined; 13 | } 14 | 15 | export async function readConfig(quiet = true): Promise { 16 | // don't use the cache in tests 17 | if (cache && process.env.NODE_ENV !== 'test') { 18 | return cache; 19 | } 20 | 21 | const configPath = await findUp(CONFIG_FILE_NAME, { cwd: process.cwd() }); 22 | 23 | if (!configPath) { 24 | logger.error(`No config file found in ${configPath}`); 25 | process.exit(1); 26 | } 27 | 28 | if (!quiet) { 29 | logger.info(`Using following config ${configPath}`); 30 | } 31 | 32 | const configFile = await import(pathToFileURL(configPath).toString()); 33 | // TODO: config validation via schema 34 | const userConfig = configFile.default; 35 | 36 | cache = { ...userConfig } as MonoSizeConfig; 37 | 38 | return cache; 39 | } 40 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/readConfig.test.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import os from 'node:os'; 4 | import { workspaceRoot } from 'nx/src/devkit-exports'; 5 | import tmp from 'tmp'; 6 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 7 | 8 | import { readConfig, resetConfigCache } from './readConfig.mjs'; 9 | import type { MonoSizeConfig } from '../types.mjs'; 10 | 11 | vitest.mock('../output.mjs', () => ({ 12 | log: { 13 | error: vitest.fn(), 14 | }, 15 | })); 16 | 17 | async function setup(config: Partial) { 18 | // Heads up! 19 | // GH actions has a weird naming of the temp directory that breaks path resolution: 20 | // "C:\Users\RUNNER~1\AppData" gets transformed to "file:///C:/Users/RUNNER%7E1/AppData" 21 | const tmpDir = path.join(workspaceRoot, 'node_modules', '.tmp'); 22 | await fs.promises.mkdir(tmpDir, { recursive: true }); 23 | 24 | const packageDir = tmp.dirSync({ prefix: 'test-package', unsafeCleanup: true, tmpdir: tmpDir }); 25 | const configFile = tmp.fileSync({ dir: packageDir.name, name: 'monosize.config.js', tmpdir: tmpDir }); 26 | 27 | const spy = vitest.spyOn(process, 'cwd'); 28 | spy.mockReturnValue(packageDir.name); 29 | 30 | await fs.promises.writeFile(configFile.name, `export default ${JSON.stringify(config)}`); 31 | } 32 | 33 | describe('readConfig', () => { 34 | beforeEach(() => { 35 | process.env.NODE_ENV = 'test'; 36 | 37 | vitest.resetAllMocks(); 38 | vitest.resetModules(); 39 | 40 | resetConfigCache(); 41 | }); 42 | 43 | it('should read config from package', async () => { 44 | await setup({ repository: '@microsoft/monosize' }); 45 | 46 | expect(await readConfig(true)).toMatchObject({ 47 | repository: '@microsoft/monosize', 48 | }); 49 | }); 50 | 51 | it('should return default webpack config if no config file defined', async () => { 52 | // eslint-disable-next-line @typescript-eslint/no-empty-function 53 | vitest.spyOn(console, 'log').mockImplementation(() => {}); 54 | 55 | const exit = vitest.spyOn(process, 'exit').mockImplementation(() => { 56 | throw new Error('TEST-MOCK: process.exit() was called'); 57 | }); 58 | const spy = vitest.spyOn(process, 'cwd'); 59 | 60 | spy.mockReturnValue(os.tmpdir()); 61 | 62 | await expect(readConfig(true)).rejects.toThrow('TEST-MOCK: process.exit() was called'); 63 | expect(exit).toHaveBeenCalledWith(1); 64 | }); 65 | 66 | it('should cache config', async () => { 67 | process.env.NODE_ENV = 'production'; 68 | 69 | await setup({ repository: '@microsoft/monosize' }); 70 | const firstConfig = await readConfig(true); 71 | 72 | await setup({ repository: '@microsoft/fluentui' }); 73 | const secondConfig = await readConfig(); 74 | 75 | expect(firstConfig).toBe(secondConfig); 76 | expect(secondConfig).toMatchObject({ 77 | repository: '@microsoft/monosize', 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/sortComparedReport.mts: -------------------------------------------------------------------------------- 1 | import type { ComparedReport, ComparedReportEntry } from './compareResultsInReports.mjs'; 2 | 3 | function compareReports(a: ComparedReportEntry, b: ComparedReportEntry) { 4 | return a.packageName.localeCompare(b.packageName) || a.path.localeCompare(b.path); 5 | } 6 | 7 | /** 8 | * Sorts entries in a report by "packageName" & "path". 9 | */ 10 | export function sortComparedReport(report: ComparedReport) { 11 | return report.slice().sort(compareReports); 12 | } 13 | -------------------------------------------------------------------------------- /packages/monosize/src/utils/sortComparedReport.test.cts: -------------------------------------------------------------------------------- 1 | import { ComparedReport, emptyDiff } from './compareResultsInReports'; 2 | import { sortComparedReport } from './sortComparedReport'; 3 | 4 | describe('sortComparedReport', () => { 5 | it('sorts a report by "packageName" & "path', () => { 6 | const report: ComparedReport = [ 7 | { packageName: 'bcd', name: 'BCD-B', path: 'bcd-b.js', minifiedSize: 0, gzippedSize: 0, diff: emptyDiff }, 8 | { packageName: 'bcd', name: 'BCD-A', path: 'bcd-a.js', minifiedSize: 0, gzippedSize: 0, diff: emptyDiff }, 9 | { packageName: 'abc', name: 'ABC', path: 'abc.js', minifiedSize: 0, gzippedSize: 0, diff: emptyDiff }, 10 | ]; 11 | const actual = sortComparedReport(report); 12 | 13 | expect(actual[0]).toEqual(report[2]); 14 | expect(actual[1]).toEqual(report[1]); 15 | expect(actual[2]).toEqual(report[0]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/monosize/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/monosize/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["environment"] 7 | }, 8 | "include": ["**/*.mts"], 9 | "exclude": ["vite.config.mts", "**/*.test.mts", "**/__fixture__/", "**/__mocks__/"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/monosize/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["vitest/importMeta", "vite/client", "environment", "node", "vitest"] 6 | }, 7 | "include": ["vite.config.mts", "src/**/*.test.mts", "src/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/monosize/vite.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | 6 | export default defineConfig({ 7 | cacheDir: '../../node_modules/.vite/monosize', 8 | plugins: [nxViteTsPaths()], 9 | 10 | test: { 11 | reporters: ['default'], 12 | include: ['src/**/*.test.mts'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "moduleResolution": "Node", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "noImplicitOverride": true, 12 | "noPropertyAccessFromIndexSignature": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "importHelpers": true, 16 | "target": "ES2021", 17 | "module": "ESNext", 18 | "lib": ["ES2021"], 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true, 21 | "baseUrl": ".", 22 | "paths": { 23 | "monosize": ["packages/monosize/src/index.mts"], 24 | "monosize-bundler-esbuild": ["packages/monosize-bundler-esbuild/src/index.mts"], 25 | "monosize-bundler-rsbuild": ["packages/monosize-bundler-rsbuild/src/index.mts"], 26 | "monosize-bundler-webpack": ["packages/monosize-bundler-webpack/src/index.mts"], 27 | "monosize-storage-azure": ["packages/monosize-storage-azure/src/index.mts"], 28 | "monosize-storage-upstash": ["packages/monosize-storage-upstash/src/index.mts"] 29 | }, 30 | "typeRoots": ["node_modules/@types", "./typings"] 31 | }, 32 | "exclude": ["node_modules", "tmp"] 33 | } 34 | -------------------------------------------------------------------------------- /typings/environment/README.md: -------------------------------------------------------------------------------- 1 | # environment 2 | 3 | Enables access to `process.env` in both node and browser packages (your code needs to rely on wepback or bundler that understands node environment). 4 | 5 | This definition list of env variables is maintained manually and should be extended as needed. 6 | 7 | ## Usage 8 | 9 | ```json 10 | { 11 | "compilerOptions": { 12 | "types": ["environment"] 13 | } 14 | } 15 | ``` 16 | 17 | Now you can use `process.env` global with strict type checking: 18 | 19 | ```ts 20 | // @ExpectType string 21 | export function log(...messages: Array) { 22 | if (process.env.NODE_ENV === 'development') { 23 | console.log(...messages); 24 | } 25 | 26 | // $ExpectError - 'prod' is not defined, did you mean to 'production' ? 27 | if (process.env.NODE_ENV === 'prod') { 28 | // do something 29 | } 30 | } 31 | ``` 32 | 33 | ## Adding env variables 34 | 35 | Add new env variables as needed 36 | 37 | **Example:** 38 | 39 | ```diff 40 | // Adding NX_ENV env variable 41 | // ↓↓↓ 42 | interface ExtendedProcessEnv { 43 | NODE_ENV?: 'production' | 'development' | 'test'; 44 | + NX_ENV?: string 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /typings/environment/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ExtendedProcessEnv { 3 | NODE_ENV?: 'production' | 'development' | 'test'; 4 | } 5 | 6 | /** 7 | * extending/creating ProcessEnv interface which is used in @types/node to define `process.env` 8 | * 9 | * NOTE: 10 | * To make it work with and without node globals it need to use same token name 11 | * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v12/globals.d.ts#L764 12 | */ 13 | export interface ProcessEnv extends ExtendedProcessEnv {} 14 | 15 | /** 16 | * extending/creating `Process` interface which is used in @types/node to define `process` global 17 | * 18 | * NOTE: 19 | * To make it work with and without node globals it need to use same token name 20 | * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v12/globals.d.ts#L883 21 | */ 22 | export interface Process { 23 | env: ProcessEnv; 24 | } 25 | } 26 | 27 | declare var process: NodeJS.Process; 28 | -------------------------------------------------------------------------------- /typings/environment/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["*.d.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /typings/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "isolatedModules": true, 5 | "allowSyntheticDefaultImports": true, 6 | "skipLibCheck": false, 7 | "noEmit": true, 8 | "types": [] 9 | }, 10 | "include": [], 11 | "files": [], 12 | "references": [ 13 | { 14 | "path": "./environment/tsconfig.json" 15 | } 16 | ] 17 | } 18 | --------------------------------------------------------------------------------