diff --git a/.github/workflows/update-size-on-main.yml b/.github/workflows/update-size-on-main.yml new file mode 100644 index 000000000..16bc7dd8a --- /dev/null +++ b/.github/workflows/update-size-on-main.yml @@ -0,0 +1,43 @@ +name: Update size + +on: + push: + branches: main + workflow_dispatch: + # allows triggering from the github UI +jobs: + check-for-doc-changes: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: npm + - uses: google/wireit@setup-github-actions-caching/v1 + + - name: Install Dependencies + run: npm ci + + - name: Update Size + run: npm run update-size + + - name: Check if update-size produces git diff + id: ifChange + run: git diff --exit-code || echo "::set-output name=changed::yes" + + - name: Create or update PR + if: steps.ifChange.outputs.changed == 'yes' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.LIT_ROBOT_ACCESS_TOKEN }} + commit-message: 'chore: update sizes' + author: lit-robot + committer: lit-robot + title: 'chore: update sizes' + body: This PR was auto generated by the update-size-on-main GitHub action. + reviewers: e111077,asyncliz + branch: auto-update-size + # Don't automatically add Ready for Google label until we're ready + # since this will be noisy. diff --git a/README.md b/README.md index 5f0794842..71528ee6e 100644 --- a/README.md +++ b/README.md @@ -16,5 +16,6 @@ Google's open-source design system. - [Introduction](docs/intro.md) - [Roadmap](docs/roadmap.md) - [Quick start](docs/quick-start.md) +- [Bundle sizes](docs/size.md) - [Component docs](docs/components/) - [Browser support and FAQ](docs/support.md) diff --git a/docs/size.md b/docs/size.md new file mode 100644 index 000000000..6f9592249 --- /dev/null +++ b/docs/size.md @@ -0,0 +1,96 @@ +# Sizes + + + + + + + +This doc tracks important size metrics for Material Web Components. + +Sizes are tracked in bundles. A bundle is a single `.js` file for one or more +components that includes all of the JavaScript and CSS needed, minus external +dependencies. We track three metrics: + +- **gzip** - minified and compressed. This impacts download size, which can + take longer over slow networks. + +- **minified** - minified and unpacked. This impacts the time it takes a page + to be interactive, which can take longer on some devices. + +- **% CSS** - the amount of CSS compared to JavaScript. The bundle includes + both JS and CSS, so this helps track changes to JS logic and CSS styles + separately. + + + + +Last updated 2023-12-13. + + + +| Component | gzip | minified | *% CSS* | Import | +| --- | --- | --- | --- | --- | +| **All** | **70.1kb** | 451.9kb | *66% CSS* | `@material/web/all.js` | +| **Common** | **51.5kb** | 282.2kb | *54% CSS* | `@material/web/common.js` | +| **Button** | **8.0kb** | 46.3kb | *66% CSS* | | +| | 6.7kb | 27.4kb | *49% CSS* | `@material/web/button/elevated-button.js` | +| | 6.6kb | 27.3kb | *49% CSS* | `@material/web/button/filled-button.js` | +| | 6.7kb | 27.7kb | *49% CSS* | `@material/web/button/filled-tonal-button.js` | +| | 6.4kb | 25.7kb | *48% CSS* | `@material/web/button/outlined-button.js` | +| | 6.2kb | 24.2kb | *45% CSS* | `@material/web/button/text-button.js` | +| **Checkbox** | **7.0kb** | 28.5kb | *43% CSS* | `@material/web/checkbox/checkbox.js` | +| **Chips** | **10.0kb** | 60.4kb | *64% CSS* | | +| | 4.8kb | 16.4kb | *22% CSS* | `@material/web/chips/chip-set.js` | +| | 6.3kb | 26.8kb | *51% CSS* | `@material/web/chips/assist-chip.js` | +| | 7.9kb | 37.0kb | *55% CSS* | `@material/web/chips/filter-chip.js` | +| | 7.3kb | 33.7kb | *54% CSS* | `@material/web/chips/input-chip.js` | +| | 6.4kb | 27.2kb | *51% CSS* | `@material/web/chips/suggestion-chip.js` | +| **Dialog** | **4.2kb** | 15.2kb | *36% CSS* | `@material/web/dialog/dialog.js` | +| **Divider** | **0.7kb** | 1.4kb | *39% CSS* | `@material/web/divider/divider.js` | +| **Elevation** | **0.7kb** | 1.7kb | *62% CSS* | `@material/web/elevation/elevation.js` | +| **Fab** | **6.9kb** | 37.1kb | *67% CSS* | | +| | 6.6kb | 32.8kb | *64% CSS* | `@material/web/fab/fab.js` | +| | 5.8kb | 24.6kb | *51% CSS* | `@material/web/fab/branded-fab.js` | +| **Field** | **6.0kb** | 40.5kb | *83% CSS* | | +| | 4.6kb | 24.8kb | *75% CSS* | `@material/web/field/filled-field.js` | +| | 5.0kb | 27.1kb | *76% CSS* | `@material/web/field/outlined-field.js` | +| **Focus** | **1.6kb** | 5.2kb | *46% CSS* | `@material/web/focus/md-focus-ring.js` | +| **Icon** | **0.7kb** | 1.3kb | *46% CSS* | `@material/web/icon/icon.js` | +| **Icon button** | **7.3kb** | 42.0kb | *65% CSS* | | +| | 5.8kb | 23.1kb | *42% CSS* | `@material/web/iconbutton/icon-button.js` | +| | 6.0kb | 25.0kb | *45% CSS* | `@material/web/iconbutton/filled-icon-button.js` | +| | 6.0kb | 25.5kb | *46% CSS* | `@material/web/iconbutton/filled-tonal-icon-button.js` | +| | 6.0kb | 24.6kb | *45% CSS* | `@material/web/iconbutton/outlined-icon-button.js` | +| **List** | **6.9kb** | 27.2kb | *35% CSS* | | +| | 1.6kb | 4.5kb | *5% CSS* | `@material/web/list/list.js` | +| | 5.8kb | 23.0kb | *40% CSS* | `@material/web/list/list-item.js` | +| **Menu** | **13.5kb** | 53.9kb | *23% CSS* | | +| | 7.9kb | 28.8kb | *17% CSS* | `@material/web/menu/menu.js` | +| | 6.5kb | 25.6kb | *37% CSS* | `@material/web/menu/menu-item.js` | +| | 8.4kb | 31.9kb | *11% CSS* | `@material/web/menu/sub-menu.js` | +| **Progress** | **3.5kb** | 13.9kb | *70% CSS* | | +| | 2.6kb | 8.6kb | *64% CSS* | `@material/web/progress/linear-progress.js` | +| | 2.2kb | 7.4kb | *57% CSS* | `@material/web/progress/circular-progress.js` | +| **Radio** | **6.9kb** | 26.0kb | *31% CSS* | `@material/web/radio/radio.js` | +| **Ripple** | **2.8kb** | 7.9kb | *14% CSS* | `@material/web/ripple/ripple.js` | +| **Select** | **25.6kb** | 142.5kb | *57% CSS* | | +| | 17.8kb | 89.3kb | *48% CSS* | `@material/web/select/filled-select.js` | +| | 18.1kb | 89.9kb | *48% CSS* | `@material/web/select/outlined-select.js` | +| | 6.6kb | 26.6kb | *36% CSS* | `@material/web/select/select-option.js` | +| **Slider** | **9.7kb** | 45.0kb | *49% CSS* | `@material/web/slider/slider.js` | +| **Switch** | **7.8kb** | 34.8kb | *53% CSS* | `@material/web/switch/switch.js` | +| **Tabs** | **7.9kb** | 35.1kb | *50% CSS* | | +| | 6.2kb | 21.9kb | *25% CSS* | `@material/web/tabs/tabs.js` | +| | 6.3kb | 25.6kb | *48% CSS* | `@material/web/tabs/primary-tab.js` | +| | 6.2kb | 25.2kb | *48% CSS* | `@material/web/tabs/secondary-tab.js` | +| **Text field** | **13.7kb** | 93.0kb | *74% CSS* | | +| | 10.7kb | 60.8kb | *62% CSS* | `@material/web/textfield/filled-text-field.js` | +| | 10.9kb | 61.3kb | *62% CSS* | `@material/web/textfield/outlined-text-field.js` | + + + + diff --git a/package-lock.json b/package-lock.json index f2302a484..be26131a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,14 @@ }, "devDependencies": { "@lit-labs/analyzer": "^0.9.2", + "@rollup/plugin-multi-entry": "^6.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", "@types/jasmine": "^4.0.3", "@web/test-runner": "^0.15.0", "@web/test-runner-playwright": "^0.9.0", "jasmine": "^4.5.0", + "rollup": "^2.79.1", "sass": "^1.52.3", "sass-true": "^6.1.0", "typescript": "5.1.6", @@ -746,6 +750,20 @@ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", "dev": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -755,6 +773,25 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -1448,41 +1485,111 @@ } } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", - "integrity": "sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==", + "node_modules/@rollup/plugin-multi-entry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-multi-entry/-/plugin-multi-entry-6.0.1.tgz", + "integrity": "sha512-AXm6toPyTSfbYZWghQGbom1Uh7dHXlrGa+HoiYNhQtDUE3Q7LqoUYdVQx9E1579QWS1uOiu+cZRSE4okO7ySgw==", "dev": true, "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.1.0", - "is-module": "^1.0.0", - "resolve": "^1.19.0" + "@rollup/plugin-virtual": "^3.0.0", + "matched": "^5.0.1" }, "engines": { - "node": ">= 10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.42.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", "dev": true, "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" }, "engines": { - "node": ">= 8.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, "node_modules/@sindresorhus/slugify": { @@ -1640,9 +1747,9 @@ "dev": true }, "node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/express": { "version": "4.17.17", @@ -1826,13 +1933,10 @@ "dev": true }, "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true }, "node_modules/@types/send": { "version": "0.17.1", @@ -2030,6 +2134,64 @@ "node": ">=10.0.0" } }, + "node_modules/@web/dev-server-rollup/node_modules/@rollup/plugin-node-resolve": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", + "integrity": "sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^2.42.0" + } + }, + "node_modules/@web/dev-server-rollup/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@web/dev-server-rollup/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@web/dev-server-rollup/node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@web/dev-server-rollup/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "node_modules/@web/dev-server-rollup/node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -2553,6 +2715,12 @@ "node": "*" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -3612,9 +3780,9 @@ } }, "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, "node_modules/etag": { @@ -5258,6 +5426,22 @@ "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", "dev": true }, + "node_modules/matched": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/matched/-/matched-5.0.1.tgz", + "integrity": "sha512-E1fhSTPRyhAlNaNvGXAgZQlq1hL0bgYMTk/6bktVlIhzUnX/SZs7296ACdVeNJE8xFNGSuvd9IpI7vSnmcqLvw==", + "dev": true, + "dependencies": { + "glob": "^7.1.6", + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/material-web-catalog": { "resolved": "catalog", "link": true @@ -6416,6 +6600,15 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -6784,6 +6977,15 @@ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6899,6 +7101,12 @@ "node": ">=8.0.0" } }, + "node_modules/smob": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", + "dev": true + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -6928,6 +7136,25 @@ "decode-uri-component": "^0.2.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", @@ -7115,6 +7342,36 @@ "node": ">=6" } }, + "node_modules/terser": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index 9413f1a9e..1e103a369 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,9 @@ "build:sass": "wireit", "test": "wireit", "build:catalog": "wireit", - "build:analyzer": "wireit", - "update-docs": "wireit" + "build:scripts": "wireit", + "update-docs": "wireit", + "update-size": "wireit" }, "type": "module", "files": [ @@ -56,10 +57,14 @@ }, "devDependencies": { "@lit-labs/analyzer": "^0.9.2", + "@rollup/plugin-multi-entry": "^6.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", "@types/jasmine": "^4.0.3", "@web/test-runner": "^0.15.0", "@web/test-runner-playwright": "^0.9.0", "jasmine": "^4.5.0", + "rollup": "^2.79.1", "sass": "^1.52.3", "sass-true": "^6.1.0", "typescript": "5.1.6", @@ -145,19 +150,19 @@ "./catalog:build:prod" ] }, - "build:analyzer": { + "build:scripts": { "command": "tsc -b scripts/tsconfig.json --pretty", "files": [ "scripts/tsconfig.json", - "scripts/analyzer/**/*.ts", + "scripts/**/*.ts", "!**/*.d.ts", "!**/*.css.ts" ], "output": [ "scripts/.tsbuildinfo", - "scripts/analyzer/**/*.js", - "scripts/analyzer/**/*.js.map", - "scripts/analyzer/**/*.d.ts" + "scripts/**/*.js", + "scripts/**/*.js.map", + "scripts/**/*.d.ts" ], "clean": "if-file-deleted" }, @@ -175,7 +180,14 @@ ], "output": [], "dependencies": [ - "build:analyzer" + "build:scripts" + ] + }, + "update-size": { + "command": "node scripts/size/update-size.js", + "dependencies": [ + "build:scripts", + "build" ] } } diff --git a/scripts/analyzer/element-docs-map.ts b/scripts/analyzer/element-docs-map.ts index 622adb0ee..a5f5a7f44 100644 --- a/scripts/analyzer/element-docs-map.ts +++ b/scripts/analyzer/element-docs-map.ts @@ -4,56 +4,31 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {COMPONENT_CUSTOM_ELEMENTS} from '../component-custom-elements.js'; + /** * A map of Markdown documentation file name to element entrypoints associated * with that documentation. */ -export const docsToElementMapping: {[key: string]: string[]} = { - 'button.md': [ - 'button/elevated-button.ts', - 'button/filled-button.ts', - 'button/filled-tonal-button.ts', - 'button/outlined-button.ts', - 'button/text-button.ts', - ], - 'checkbox.md': ['checkbox/checkbox.ts'], - 'chip.md': [ - 'chips/chip-set.ts', - 'chips/assist-chip.ts', - 'chips/filter-chip.ts', - 'chips/input-chip.ts', - 'chips/suggestion-chip.ts', - ], - 'dialog.md': ['dialog/dialog.ts'], - 'divider.md': ['divider/divider.ts'], - 'elevation.md': ['elevation/elevation.ts'], - 'fab.md': ['fab/fab.ts', 'fab/branded-fab.ts'], - 'focus-ring.md': ['focus/md-focus-ring.ts'], - 'icon-button.md': [ - 'iconbutton/icon-button.ts', - 'iconbutton/filled-icon-button.ts', - 'iconbutton/filled-tonal-icon-button.ts', - 'iconbutton/outlined-icon-button.ts', - ], - 'icon.md': ['icon/icon.ts'], - 'list.md': ['list/list.ts', 'list/list-item.ts'], - 'menu.md': ['menu/menu.ts', 'menu/menu-item.ts', 'menu/sub-menu.ts'], - 'progress.md': [ - 'progress/linear-progress.ts', - 'progress/circular-progress.ts', - ], - 'radio.md': ['radio/radio.ts'], - 'ripple.md': ['ripple/ripple.ts'], - 'slider.md': ['slider/slider.ts'], - 'switch.md': ['switch/switch.ts'], - 'tabs.md': ['tabs/tabs.ts', 'tabs/primary-tab.ts', 'tabs/secondary-tab.ts'], - 'text-field.md': [ - 'textfield/filled-text-field.ts', - 'textfield/outlined-text-field.ts', - ], - 'select.md': [ - 'select/filled-select.ts', - 'select/outlined-select.ts', - 'select/select-option.ts', - ], +export const docsToElementMapping: {[key: string]: readonly string[]} = { + 'button.md': COMPONENT_CUSTOM_ELEMENTS.button, + 'checkbox.md': COMPONENT_CUSTOM_ELEMENTS.checkbox, + 'chip.md': COMPONENT_CUSTOM_ELEMENTS.chips, + 'dialog.md': COMPONENT_CUSTOM_ELEMENTS.dialog, + 'divider.md': COMPONENT_CUSTOM_ELEMENTS.divider, + 'elevation.md': COMPONENT_CUSTOM_ELEMENTS.elevation, + 'fab.md': COMPONENT_CUSTOM_ELEMENTS.fab, + 'focus-ring.md': COMPONENT_CUSTOM_ELEMENTS.focus, + 'icon-button.md': COMPONENT_CUSTOM_ELEMENTS.iconButton, + 'icon.md': COMPONENT_CUSTOM_ELEMENTS.icon, + 'list.md': COMPONENT_CUSTOM_ELEMENTS.list, + 'menu.md': COMPONENT_CUSTOM_ELEMENTS.menu, + 'progress.md': COMPONENT_CUSTOM_ELEMENTS.progress, + 'radio.md': COMPONENT_CUSTOM_ELEMENTS.radio, + 'ripple.md': COMPONENT_CUSTOM_ELEMENTS.ripple, + 'slider.md': COMPONENT_CUSTOM_ELEMENTS.slider, + 'switch.md': COMPONENT_CUSTOM_ELEMENTS.switch, + 'tabs.md': COMPONENT_CUSTOM_ELEMENTS.tabs, + 'text-field.md': COMPONENT_CUSTOM_ELEMENTS.textField, + 'select.md': COMPONENT_CUSTOM_ELEMENTS.select, }; diff --git a/scripts/analyzer/markdown-tree-builder.ts b/scripts/analyzer/markdown-tree-builder.ts index 27855ffa1..5a8db7598 100644 --- a/scripts/analyzer/markdown-tree-builder.ts +++ b/scripts/analyzer/markdown-tree-builder.ts @@ -40,7 +40,7 @@ export class MarkdownTable { addRow(row: string[]) { if (row.length !== this.columnsInternal.length) { throw new Error( - `Row length (${row.length}) must match column length (${this.columnsInternal.length})`, + `Row length (${row.length}) must match column length (${this.columnsInternal.length})` ); } @@ -53,10 +53,12 @@ export class MarkdownTable { * @returns A markdown-compatible table. */ toString() { - const headerRow = this.columnsInternal.join(' | '); - const dividerRow = this.columnsInternal.map(() => '---').join(' | '); + const headerRow = `| ${this.columnsInternal.join(' | ')} |`; + const dividerRow = `| ${this.columnsInternal + .map(() => '---') + .join(' | ')} |`; const rows = this.rowsInternal - .map((row) => `${row.join(' | ')}`) + .map((row) => `| ${row.join(' | ')} |`) .join('\n'); return ` diff --git a/scripts/component-custom-elements.ts b/scripts/component-custom-elements.ts new file mode 100644 index 000000000..1f557629a --- /dev/null +++ b/scripts/component-custom-elements.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * A map of components and their custom element TypeScript entrypoints. + */ +export const COMPONENT_CUSTOM_ELEMENTS = { + button: [ + 'button/elevated-button.ts', + 'button/filled-button.ts', + 'button/filled-tonal-button.ts', + 'button/outlined-button.ts', + 'button/text-button.ts', + ], + checkbox: ['checkbox/checkbox.ts'], + chips: [ + 'chips/chip-set.ts', + 'chips/assist-chip.ts', + 'chips/filter-chip.ts', + 'chips/input-chip.ts', + 'chips/suggestion-chip.ts', + ], + dialog: ['dialog/dialog.ts'], + divider: ['divider/divider.ts'], + elevation: ['elevation/elevation.ts'], + fab: ['fab/fab.ts', 'fab/branded-fab.ts'], + field: ['field/filled-field.ts', 'field/outlined-field.ts'], + focus: ['focus/md-focus-ring.ts'], + icon: ['icon/icon.ts'], + iconButton: [ + 'iconbutton/icon-button.ts', + 'iconbutton/filled-icon-button.ts', + 'iconbutton/filled-tonal-icon-button.ts', + 'iconbutton/outlined-icon-button.ts', + ], + list: ['list/list.ts', 'list/list-item.ts'], + menu: ['menu/menu.ts', 'menu/menu-item.ts', 'menu/sub-menu.ts'], + progress: ['progress/linear-progress.ts', 'progress/circular-progress.ts'], + radio: ['radio/radio.ts'], + ripple: ['ripple/ripple.ts'], + select: [ + 'select/filled-select.ts', + 'select/outlined-select.ts', + 'select/select-option.ts', + ], + slider: ['slider/slider.ts'], + switch: ['switch/switch.ts'], + tabs: ['tabs/tabs.ts', 'tabs/primary-tab.ts', 'tabs/secondary-tab.ts'], + textField: [ + 'textfield/filled-text-field.ts', + 'textfield/outlined-text-field.ts', + ], +} as const; diff --git a/scripts/size/bundle-size.ts b/scripts/size/bundle-size.ts new file mode 100644 index 000000000..23e4f3786 --- /dev/null +++ b/scripts/size/bundle-size.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import multiEntry from '@rollup/plugin-multi-entry'; +import nodeResolve from '@rollup/plugin-node-resolve'; +import terser from '@rollup/plugin-terser'; +import {rollup} from 'rollup'; +import {promisify} from 'util'; +import {gzip} from 'zlib'; + +const gzipPromise = promisify(gzip); + +export interface Bundle { + name: string; + inputs: string[]; +} + +export interface BundleSize { + name: string; + size: Size; + inputs: InputSize[]; +} + +export interface InputSize { + input: string; + size: Size; +} + +export interface Size { + raw: number; + css: number; + gzip: number; +} + +export async function getBundleSize(bundle: Bundle): Promise { + const bundleSize = await computeBundleSize(bundle.inputs); + let inputSizes: InputSize[]; + if (bundle.inputs.length === 1) { + // If there's only one input, we don't need to re-generate the bundle. + inputSizes = [{input: bundle.inputs[0], size: bundleSize}]; + } else { + // Include computed bundle size for individual inputs. + inputSizes = await Promise.all( + bundle.inputs.map(async (input) => { + return { + input, + size: await computeBundleSize(input), + }; + }), + ); + } + + return { + name: bundle.name, + size: bundleSize, + inputs: inputSizes, + }; +} + +async function computeBundleSize(input: string | string[]): Promise { + const rollupBundle = await rollup({ + input, + external: [/node_modules/], + plugins: [multiEntry(), nodeResolve(), terser()], + }); + + let code = ''; + try { + const {output} = await rollupBundle.generate({}); + code = output[0].code; + } finally { + rollupBundle.close(); + } + + const litCssTagNameMatch = code.match(/css\s*as\s*(\w+)/); + if (!litCssTagNameMatch) { + throw new Error("Cannot find `import { css as X } from 'lit'`"); + } + + const litCssTagName = litCssTagNameMatch[1]; + const codeWithoutCss = code.replaceAll( + new RegExp(`${litCssTagName}\`([^\`]*)\``, 'g'), + '', + ); + + const encoder = new TextEncoder(); + const codeSize = encoder.encode(code).length; + const codeSizeWithoutCss = encoder.encode(codeWithoutCss).length; + const cssSize = codeSize - codeSizeWithoutCss; + const gzipSize = (await gzipPromise(code)).length; + return { + raw: codeSize, + css: cssSize, + gzip: gzipSize, + }; +} diff --git a/scripts/size/update-size.ts b/scripts/size/update-size.ts new file mode 100644 index 000000000..07f4b4742 --- /dev/null +++ b/scripts/size/update-size.ts @@ -0,0 +1,118 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'fs/promises'; + +import { MarkdownTable } from '../analyzer/markdown-tree-builder.js'; +import { COMPONENT_CUSTOM_ELEMENTS } from '../component-custom-elements.js'; +import { Bundle, Size, getBundleSize } from './bundle-size.js'; + +// The bundles to track sizes for. + +const bundles: Bundle[] = [ + { + name: 'all', + inputs: ['all.js'], + }, + { + name: 'common', + inputs: ['common.js'], + }, + ...( + Object.keys(COMPONENT_CUSTOM_ELEMENTS) as Array< + keyof typeof COMPONENT_CUSTOM_ELEMENTS + > + ).map((component) => { + const tsCustomElementPaths = COMPONENT_CUSTOM_ELEMENTS[component]; + const jsCustomElementPaths = tsCustomElementPaths.map((tsPath) => + tsPath.replace(/\.ts$/, '.js') + ); + + return { + name: component, + inputs: jsCustomElementPaths, + }; + }), +]; + +// Compute bundle sizes. + +const bundleSizes = await Promise.all( + bundles.map((bundle) => getBundleSize(bundle)) +); + +// Create a markdown table with size data. + +const columns = ['Component', 'gzip', 'minified', '*% CSS*', 'Import']; +const rows: string[][] = []; +for (const { name, size, inputs } of bundleSizes) { + rows.push([ + `**${camelToSentenceCase(name)}**`, + `**${bytesToString(size.gzip)}**`, + bytesToString(size.raw), + getCssPercent(size), + inputs.length === 1 ? getImport(inputs[0].input) : '', + ]); + + if (inputs.length > 1) { + rows.push( + ...inputs.map((input) => { + return [ + '', + bytesToString(input.size.gzip), + bytesToString(input.size.raw), + getCssPercent(input.size), + getImport(input.input), + ]; + }) + ); + } +} + +const markdownTable = new MarkdownTable(columns); +for (const row of rows) { + markdownTable.addRow(row); +} + +// Update markdown file. + +const markdownContent = await fs.readFile('docs/size.md', { encoding: 'utf8' }); +const updateTrackingStart = ''; +const updateTrackingEnd = ''; + +const now = new Date(); +const nowString = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`; + +const newMarkdownContent = [ + markdownContent.substring(0, markdownContent.indexOf(updateTrackingStart)), + updateTrackingStart, + '\n\n', + `Last updated ${nowString}.\n\n`, + markdownTable.toString(), + '\n\n', + markdownContent.substring(markdownContent.indexOf(updateTrackingEnd)), +].join(''); + +await fs.writeFile('docs/size.md', newMarkdownContent); + +// Text formatting functions for markdown table. + +function getImport(input: string) { + return `\`@material/web/${input}\``; +} + +function getCssPercent(size: Size) { + return `*${Math.round((size.css / size.raw) * 100)}% CSS*`; +} + +function bytesToString(bytes: number) { + return `${(Math.round(bytes / 100) / 10).toFixed(1)}kb`; +} + +function camelToSentenceCase(value: string) { + const withSpaces = value.replaceAll(/([a-z])([A-Z])/g, '$1 $2'); + return withSpaces[0].toUpperCase() + withSpaces.slice(1).toLowerCase(); +}