├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .size-limit.js ├── .size.json ├── .travis.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ ├── ast.spec.tsx.snap │ └── react.integration.spec.tsx.snap ├── ast.spec.tsx ├── css-stream.spec.tsx ├── css │ ├── file1.css │ ├── file2.css │ ├── file3.css │ └── wrong-file.css ├── edge-cases │ ├── broken-split-46.spec.ts │ ├── missing-1600.spec.tsx │ └── missing-43.spec.ts ├── extraction.spec.tsx ├── index.tsx ├── mapStyles.spec.ts ├── media.spec.ts ├── react-css-stream.spec.tsx ├── react.integration.spec.tsx ├── react.integration2.spec.tsx ├── react.integration3.spec.tsx └── simple-example.spec.tsx ├── example ├── react-18-streaming │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── client.tsx │ │ ├── entry-server.tsx │ │ ├── server.tsx │ │ └── styles.css │ ├── yarn-error.log │ └── yarn.lock ├── ssr-react-streaming-ts │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── server.js │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── Card.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── entry-client.tsx │ │ ├── entry-server.tsx │ │ ├── index.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock ├── ssr-react-streaming │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── server.js │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── Card.jsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── entry-client.jsx │ │ ├── entry-server.jsx │ │ └── index.css │ ├── vite.config.js │ └── yarn.lock ├── ssr-react-ts │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── server.js │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── entry-client.tsx │ │ ├── entry-server.tsx │ │ ├── index.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock └── ssr-react │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── server.js │ ├── src │ ├── App.css │ ├── App.jsx │ ├── assets │ │ └── react.svg │ ├── entry-client.jsx │ ├── entry-server.jsx │ └── index.css │ ├── vite.config.js │ └── yarn.lock ├── jest.config.js ├── moveStyles └── package.json ├── node └── package.json ├── package.json ├── src ├── config.ts ├── createLink.ts ├── getCSS.ts ├── index-node.ts ├── index.ts ├── moveStyles.ts ├── operations.ts ├── operations │ └── prune-selector.ts ├── parser │ ├── ast.ts │ ├── fromAst.ts │ ├── ranges.ts │ ├── toAst.ts │ └── utils.ts ├── reporters │ ├── critical.ts │ └── used.ts ├── serialize.ts ├── style-discovery.ts ├── style-operations.ts ├── types.ts └── utils │ ├── __tests__ │ ├── class-extraction.spec.ts │ └── split-selectors.spec.ts │ ├── async.ts │ ├── cache.ts │ ├── order.ts │ ├── split-selectors.ts │ ├── string.ts │ └── style.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript', 'plugin:react-hooks/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'prettier', 'import'], 5 | rules: { 6 | '@typescript-eslint/ban-ts-comment': 0, 7 | '@typescript-eslint/ban-ts-ignore': 0, 8 | '@typescript-eslint/no-var-requires': 0, 9 | '@typescript-eslint/camelcase': 0, 10 | 'import/order': [ 11 | 'error', 12 | { 13 | 'newlines-between': 'always-and-inside-groups', 14 | alphabetize: { 15 | order: 'asc', 16 | }, 17 | groups: ['builtin', 'external', 'internal', ['parent', 'index', 'sibling']], 18 | }, 19 | ], 20 | 'padding-line-between-statements': [ 21 | 'error', 22 | // IMPORT 23 | { 24 | blankLine: 'always', 25 | prev: 'import', 26 | next: '*', 27 | }, 28 | { 29 | blankLine: 'any', 30 | prev: 'import', 31 | next: 'import', 32 | }, 33 | // EXPORT 34 | { 35 | blankLine: 'always', 36 | prev: '*', 37 | next: 'export', 38 | }, 39 | { 40 | blankLine: 'any', 41 | prev: 'export', 42 | next: 'export', 43 | }, 44 | { 45 | blankLine: 'always', 46 | prev: '*', 47 | next: ['const', 'let'], 48 | }, 49 | { 50 | blankLine: 'any', 51 | prev: ['const', 'let'], 52 | next: ['const', 'let'], 53 | }, 54 | // BLOCKS 55 | { 56 | blankLine: 'always', 57 | prev: ['block', 'block-like', 'class', 'function', 'multiline-expression'], 58 | next: '*', 59 | }, 60 | { 61 | blankLine: 'always', 62 | prev: '*', 63 | next: ['block', 'block-like', 'class', 'function', 'return', 'multiline-expression'], 64 | }, 65 | ], 66 | }, 67 | settings: { 68 | 'import/parsers': { 69 | '@typescript-eslint/parser': ['.ts', '.tsx'], 70 | }, 71 | 'import/resolver': { 72 | typescript: { 73 | alwaysTryTypes: true, 74 | }, 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: ['*'] 6 | pull_request: 7 | branches: ['*'] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x, 16.x, 18.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Run tests [Node.js ${{ matrix.node-version }}] 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: yarn --frozen-lockfile 25 | - run: yarn test:ci -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/ 3 | /dist/ 4 | coverage 5 | .DS_Store 6 | .nyc_output 7 | .yalc 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.size-limit.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // { 3 | // path: ['src/moveStyles.ts'], 4 | // ignore: ['tslib'], 5 | // limit: '0.5 KB', 6 | // }, 7 | ]; 8 | -------------------------------------------------------------------------------- /.size.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.\n - configuration.entry['index'] should be an non-empty array.\n -> A non-empty array of non-empty strings\n at webpack (/Users/akorzunov/dev/github/loaders/used-styles/node_modules/webpack/lib/webpack.js:31:9)\n at /Users/akorzunov/dev/github/loaders/used-styles/node_modules/@size-limit/webpack/run-webpack.js:5:20\n at new Promise ()\n at runWebpack (/Users/akorzunov/dev/github/loaders/used-styles/node_modules/@size-limit/webpack/run-webpack.js:4:10)\n at Object.step40 (/Users/akorzunov/dev/github/loaders/used-styles/node_modules/@size-limit/webpack/index.js:59:38)\n at /Users/akorzunov/dev/github/loaders/used-styles/node_modules/size-limit/calc.js:8:62\n at Array.map ()\n at exec (/Users/akorzunov/dev/github/loaders/used-styles/node_modules/size-limit/calc.js:8:41)\n at calc (/Users/akorzunov/dev/github/loaders/used-styles/node_modules/size-limit/calc.js:14:42)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn test:ci 8 | notifications: 9 | email: false -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/jest/bin/jest.js", "--runInBand"], 9 | "console": "integratedTerminal", 10 | "internalConsoleOptions": "neverOpen" 11 | }, 12 | { 13 | "name": "Debug Jest Tests (watch)", 14 | "type": "node", 15 | "request": "launch", 16 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/jest/bin/jest.js", "--runInBand", "--watch"], 17 | "console": "integratedTerminal", 18 | "internalConsoleOptions": "neverOpen" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.0.0](https://github.com/theKashey/used-styles/compare/v2.6.4...v3.0.0) (2024-06-30) 2 | 3 | # Breaking change - `discoverProjectStyles` moved to `used-styles/node` entrypoint 4 | 5 | ## [2.6.4](https://github.com/theKashey/used-styles/compare/v2.6.3...v2.6.4) (2024-03-29) 6 | 7 | ## [2.6.3](https://github.com/theKashey/used-styles/compare/v2.6.2...v2.6.3) (2024-01-21) 8 | 9 | ## [2.6.2](https://github.com/theKashey/used-styles/compare/v2.6.1...v2.6.2) (2023-11-15) 10 | 11 | ## [2.6.1](https://github.com/theKashey/used-styles/compare/v2.6.0...v2.6.1) (2023-11-14) 12 | 13 | ### Bug Fixes 14 | 15 | - expose moveStyles API. fixes [#52](https://github.com/theKashey/used-styles/issues/52) ([02731fd](https://github.com/theKashey/used-styles/commit/02731fd2958dba2ab83773cca7451a25f3078118)) 16 | 17 | # [2.6.0](https://github.com/theKashey/used-styles/compare/v2.5.2...v2.6.0) (2023-09-16) 18 | 19 | ### Bug Fixes 20 | 21 | - **docs:** typo in style extraction info ([58a704e](https://github.com/theKashey/used-styles/commit/58a704e281f153b4ec65df32c4371a10330e603f)) 22 | 23 | ## [2.5.2](https://github.com/theKashey/used-styles/compare/v2.5.1...v2.5.2) (2023-07-27) 24 | 25 | ## [2.5.1](https://github.com/theKashey/used-styles/compare/v2.5.0...v2.5.1) (2023-07-27) 26 | 27 | # [2.5.0](https://github.com/theKashey/used-styles/compare/v2.4.3...v2.5.0) (2023-07-26) 28 | 29 | ### Bug Fixes 30 | 31 | - use smarter class separation, fixes [#46](https://github.com/theKashey/used-styles/issues/46) ([ee2132d](https://github.com/theKashey/used-styles/commit/ee2132d785c608ea0acdd144470a7293cb95ee5b)) 32 | 33 | ## [2.4.3](https://github.com/theKashey/used-styles/compare/v2.4.2...v2.4.3) (2023-05-13) 34 | 35 | ### Bug Fixes 36 | 37 | - add simple extraction example; ease usage of sync types ([fc75e1a](https://github.com/theKashey/used-styles/commit/fc75e1ae6a43c3b444bfaa4e8bc0b2f576538c57)) 38 | 39 | ## [2.4.2](https://github.com/theKashey/used-styles/compare/v2.4.1...v2.4.2) (2023-02-21) 40 | 41 | ### Bug Fixes 42 | 43 | - add support for class names which are keys from object prototype ([ed2d5f9](https://github.com/theKashey/used-styles/commit/ed2d5f94fa1f20d980b71181c33ae021af5c42fc)) 44 | - correct support for multiple classes, fixes [#43](https://github.com/theKashey/used-styles/issues/43) ([fde1497](https://github.com/theKashey/used-styles/commit/fde1497a1a271ac86e320c3056f25a9ed06bf69a)) 45 | 46 | ## [2.4.1](https://github.com/theKashey/used-styles/compare/v2.4.0...v2.4.1) (2022-03-31) 47 | 48 | ### Bug Fixes 49 | 50 | - refactor class introduction ([b85fd08](https://github.com/theKashey/used-styles/commit/b85fd08c3250b6bfc46ddb80a068c493fe2ee1c8)) 51 | 52 | # [2.4.0](https://github.com/theKashey/used-styles/compare/v2.3.3...v2.4.0) (2022-03-31) 53 | 54 | ### Features 55 | 56 | - do not extract nested selectors without prior parts introduced first, fixes [#30](https://github.com/theKashey/used-styles/issues/30) ([0b83f8e](https://github.com/theKashey/used-styles/commit/0b83f8e88f461e195cc3ce8f9976d324c0471466)) 57 | - selectors pruning. fixes [#38](https://github.com/theKashey/used-styles/issues/38) ([4ce4d18](https://github.com/theKashey/used-styles/commit/4ce4d18cd10474126f7882b395e15522fc2702f9)) 58 | 59 | ## [2.3.3](https://github.com/theKashey/used-styles/compare/v2.3.2...v2.3.3) (2022-02-12) 60 | 61 | ## [2.3.2](https://github.com/theKashey/used-styles/compare/v2.3.1...v2.3.2) (2021-11-09) 62 | 63 | ### Bug Fixes 64 | 65 | - classname can contain escaped sequence of not allowed characters ([894fed6](https://github.com/theKashey/used-styles/commit/894fed618d72bf3ce5bdd072bbdc7f221ee376f3)) 66 | 67 | ## [2.3.1](https://github.com/theKashey/used-styles/compare/v2.3.0...v2.3.1) (2021-11-07) 68 | 69 | # [2.3.0](https://github.com/theKashey/used-styles/compare/v2.2.1...v2.3.0) (2021-10-13) 70 | 71 | ### Features 72 | 73 | - expose extractCriticalRules to handle per-chunk operations ([f038807](https://github.com/theKashey/used-styles/commit/f038807f6668c56adb27107dea01ff06024d7a8d)) 74 | 75 | ## [2.2.1](https://github.com/theKashey/used-styles/compare/v2.2.0...v2.2.1) (2021-09-19) 76 | 77 | ### Bug Fixes 78 | 79 | - selectors from different media targets can collide, fixes [#31](https://github.com/theKashey/used-styles/issues/31) ([64c5f26](https://github.com/theKashey/used-styles/commit/64c5f2681b5d882db46e24fd4f0a372f3b9d902b)) 80 | 81 | # [2.2.0](https://github.com/theKashey/used-styles/compare/v2.1.4...v2.2.0) (2021-07-18) 82 | 83 | ### Features 84 | 85 | - provide controlled discovery API ([65cfc4d](https://github.com/theKashey/used-styles/commit/65cfc4d411669149b4822ebd97c988c65faa8fc6)) 86 | 87 | ## [2.1.4](https://github.com/theKashey/used-styles/compare/v2.1.3...v2.1.4) (2021-03-24) 88 | 89 | ## [2.1.3](https://github.com/theKashey/used-styles/compare/v2.1.2...v2.1.3) (2020-06-08) 90 | 91 | ### Bug Fixes 92 | 93 | - use selector hash instead of weak reference ([2859ac5](https://github.com/theKashey/used-styles/commit/2859ac514c9329c177e0782756fd0f210b68c784)) 94 | 95 | ## [2.1.2](https://github.com/theKashey/used-styles/compare/v2.1.1...v2.1.2) (2020-06-08) 96 | 97 | ### Bug Fixes 98 | 99 | - consider media query when filtering used selectors ([5c4d37b](https://github.com/theKashey/used-styles/commit/5c4d37bfc304c85cce0f92a5d92b07f786996559)) 100 | - readme - lookup usage ([f02ac12](https://github.com/theKashey/used-styles/commit/f02ac12a402af046587cade42e7bff37de7429c4)) 101 | 102 | ## [2.1.1](https://github.com/theKashey/used-styles/compare/v2.1.0...v2.1.1) (2020-04-02) 103 | 104 | ### Bug Fixes 105 | 106 | - unmatched css is not wrapped with styles ([dc7939d](https://github.com/theKashey/used-styles/commit/dc7939dc12e5f7407523b44966732ea2b9c683a8)) 107 | - wrong helper used in readme, fixes [#17](https://github.com/theKashey/used-styles/issues/17) ([2bbb000](https://github.com/theKashey/used-styles/commit/2bbb000af334a1714676c401716a3c6456183f1f)) 108 | 109 | # [2.1.0](https://github.com/theKashey/used-styles/compare/v2.0.4...v2.1.0) (2019-10-05) 110 | 111 | ### Bug Fixes 112 | 113 | - handle !important, fixes [#8](https://github.com/theKashey/used-styles/issues/8) ([da5042a](https://github.com/theKashey/used-styles/commit/da5042a01c2d6c68e0da04c9d59bb9ab411961d4)) 114 | 115 | ### Features 116 | 117 | - alterProjectStyles and hydrid usage ([4012b86](https://github.com/theKashey/used-styles/commit/4012b86a780b54b8cc58b407d753f3aac58ee620)) 118 | 119 | ## [2.0.4](https://github.com/theKashey/used-styles/compare/v2.0.3...v2.0.4) (2019-09-14) 120 | 121 | ## [2.0.3](https://github.com/theKashey/used-styles/compare/v2.0.2...v2.0.3) (2019-09-09) 122 | 123 | ## [2.0.2](https://github.com/theKashey/used-styles/compare/v2.0.1...v2.0.2) (2019-09-05) 124 | 125 | ## [2.0.1](https://github.com/theKashey/used-styles/compare/v2.0.0...v2.0.1) (2019-09-02) 126 | 127 | # [2.0.0](https://github.com/theKashey/used-styles/compare/v1.1.0...v2.0.0) (2019-08-31) 128 | 129 | # 1.1.0 (2019-02-09) 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anton Korzunov 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 |
2 |

used-styles

3 |
4 | Get all the styles, you have used to render a page.
5 | (without any puppeteer involved) 6 |
7 |
8 | 9 | [![Build Status](https://travis-ci.org/theKashey/used-styles.svg?branch=master)](https://travis-ci.org/theKashey/used-styles) 10 | [![NPM version](https://img.shields.io/npm/v/used-styles.svg)](https://www.npmjs.com/package/used-styles) 11 | 12 |
13 | 14 | > 👋**Version 3** migration notice: `import { discoverProjectStyles } from 'used-styles/node'`. That's it 15 | 16 | --- 17 | 18 | > Bundler and framework independent CSS part of SSR-friendly code splitting 19 | 20 | Detects used `css` files from the given HTML, and/or **inlines critical styles**. Supports sync or **stream** rendering. 21 | 22 | Read more about critical style extraction and this library: https://dev.to/thekashey/optimising-css-delivery-57eh 23 | 24 | - 🚀 Super Fast - no browser, no jsdom, no runtime transformations 25 | - 💪 API - it's no more than an API - integrates with everything 26 | - 🤝 Works with `strings` and `streams` 27 | - ⏳ Helps preloading for the "real" style files 28 | 29 | Works in two modes: 30 | 31 | - 🚙 inlines style **rules** required to render given HTML - ideal for the first time visitor 32 | - 🏋️‍♀️inlines style **files** required to render given HTML - ideal for the second time visitor (and code splitting) 33 | 34 | Critical style extraction: 35 | 36 | - 🧱 will _load_ all used styles at the beginning of your page in a **string** mode 37 | - 💉 will _interleave_ HTML and CSS in a **stream** mode. This is the best experience possible 38 | 39 | ## How it works 40 | 41 | 1. Scans all `.css` files, in your `build` directory, extracting all style rules names. 42 | 2. Scans a given `html`, finding all the `classes` used. 43 | 3. Here there are two options: 44 | 3a. Calculate all **style rules** you need to render a given HTML. 3b. Calculate all the style **files** you have 45 | send to a client. 46 | 4. Injects `` or `` 47 | 5. After the page load, hoist or removes critical styles replacing them by the "real" ones. 48 | 49 | ## Limitation 50 | 51 | For the performance sake `used-styles` inlines a bit more styles than it should - it inlines everything it would be "not 52 | fast" to remove. 53 | 54 | - inlines all `@keyframe` animations 55 | - inlines all `html, body` and other tag-based selectors (hello css-reset) 56 | - undefined behavior if `@layer a,b,c` is used multiple times 57 | 58 | ### Speed 59 | 60 | > Speed, I am speed! 61 | 62 | For the 516kb page, which needs **80ms** to `renderToString`(React) resulting time for the `getCriticalRules`(very 63 | expensive operation) would be around **4ms**. 64 | 65 | # API 66 | 67 | ## Discovery API 68 | 69 | Use it to scan your `dist`/`build` folder to create a look up table between classNames and files they are described in. 70 | 71 | 1. `discoverProjectStyles(buildDirrectory, [filter]): StyleDef` - generates class lookup table 72 | > you may use the second argument to control which files should be scanned 73 | 74 | `filter` is very important function here. It takes `fileName` as input, and returns 75 | `false`, `true`, or a `number` as result. `False` value would exclude this file from the set, `true` - add it, 76 | and `number` 77 | would change **the order** of the chunk. Keeping chunk ordered "as expected" is required to preserve style declaration 78 | order, which is important for many existing styles. 79 | 80 | ```js 81 | // with chunk format [chunkhash]_[id] lower ids are potentialy should be defined before higher 82 | const styleData = discoverProjectStyles(resolve('build'), (name) => { 83 | // get ID of a chunk and use it as order hint 84 | const match = name.match(/(\d)_c.css/); 85 | return match && +match[1]; 86 | }); 87 | ``` 88 | 89 | > ⚠️ generally speaking - this approach working only unless there are no order-sensive styles from different chunks applied to a single DOM Element. 90 | > Quite often it never happen, but if you are looking for a better way - follow to [#26](https://github.com/theKashey/used-styles/issues/26) ☣️ 91 | 92 | 1. `loadStyleDefinitions` is a "full control API", and can used to feed `used-styles` with any custom data, for example 93 | providing correct critical css extraction in dev mode (no files written on disk) 94 | 95 | ```ts 96 | return loadStyleDefinitions( 97 | /*list of files*/ async () => cssFiles, 98 | /*data loader*/ (file) => fetchTxt(`http://localhost:${process.env.DEV_SERVER_PORT}/${file}`) 99 | /*filter and order */ // (file) => order.indexOf(cssToChunk[file]) 100 | ); 101 | ``` 102 | 103 | ## Scanners 104 | 105 | Use to get used styled from render result or a stream 106 | 107 | 2. `getUsedStyles(html, StyleDef): string[]` - returns all used **files**, you will need to import them 108 | 3. `getCriticalStyles(html, StyleDef) : string` - returns all used selectors and other applicable rules, wrapped 109 | with `style` 110 | 4. `getCriticalRules(html, StyleDef): string` - **the same**, but without `" 42 | `); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/edge-cases/missing-1600.spec.tsx: -------------------------------------------------------------------------------- 1 | import { fromAst } from '../../src/parser/fromAst'; 2 | import { buildAst } from '../../src/parser/toAst'; 3 | import { createUsedFilter } from '../../src/utils/cache'; 4 | 5 | describe('missing 1600px', () => { 6 | const CSS = ` 7 | .content { 8 | } 9 | 10 | @media screen and (min-width: 768px) { 11 | .content { 12 | grid-column:1/6 13 | } 14 | } 15 | 16 | @media screen and (min-width: 1024px) { 17 | .content { 18 | grid-column:1/7 19 | } 20 | } 21 | 22 | /*this style duplicates 768 one, had the hash before*/ 23 | @media screen and (min-width: 1600px) { 24 | .content { 25 | grid-column:1/6 26 | } 27 | }`; 28 | 29 | it('result should contain 1600', () => { 30 | const ast = buildAst(CSS); 31 | 32 | const test = fromAst(['content'], ast, createUsedFilter()); 33 | 34 | expect(test).toMatchInlineSnapshot(` 35 | ".content { } 36 | 37 | @media screen and (min-width: 768px) { 38 | .content { grid-column: 1/6; } 39 | } 40 | @media screen and (min-width: 1024px) { 41 | .content { grid-column: 1/7; } 42 | } 43 | @media screen and (min-width: 1600px) { 44 | .content { grid-column: 1/6; } 45 | }" 46 | `); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/edge-cases/missing-43.spec.ts: -------------------------------------------------------------------------------- 1 | import { loadStyleDefinitions } from '../../src'; 2 | import { getCriticalStyles } from '../../src/getCSS'; 3 | 4 | describe('missing styles', () => { 5 | it('result should contain 1600', async () => { 6 | const styles = loadStyleDefinitions( 7 | () => ['test.css'], 8 | // // .downloads-1u5ev.downloadsFallback-1btP7 .logo-2Dv5-, 9 | () => ` 10 | .a.b .c { 11 | position: relative; 12 | } 13 | ` 14 | ); 15 | await styles; 16 | 17 | expect(styles.ast['test.css'].selectors).toMatchInlineSnapshot(` 18 | Array [ 19 | Object { 20 | "atrules": Array [], 21 | "declaration": 1, 22 | "hash": ".a.b .c18suomu-1gpll6f0", 23 | "parents": Array [ 24 | "a", 25 | "b", 26 | ], 27 | "pieces": Array [ 28 | "c", 29 | ], 30 | "postfix": ".c", 31 | "selector": ".a.b .c", 32 | }, 33 | ] 34 | `); 35 | 36 | const extracted = getCriticalStyles('
\n', styles); 37 | 38 | expect(extracted).toMatchInlineSnapshot(` 39 | "" 41 | `); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/extraction.spec.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | alterProjectStyles, 3 | loadSerializedLookup, 4 | getCriticalRules, 5 | loadStyleDefinitions, 6 | serializeStylesLookup, 7 | StyleDefinition, 8 | } from '../src'; 9 | 10 | describe('extraction stories', () => { 11 | it('handles duplicated selectors', async () => { 12 | const styles: StyleDefinition = loadStyleDefinitions( 13 | () => ['test.css'], 14 | () => ` 15 | .test { 16 | display: inline-block; 17 | } 18 | 19 | .test { 20 | padding: 10px; 21 | } 22 | 23 | .test:focus { 24 | color: red; 25 | } 26 | 27 | .test:focus { 28 | border: blue; 29 | } 30 | ` 31 | ); 32 | await styles; 33 | 34 | const extracted = getCriticalRules('
', styles); 35 | 36 | expect(extracted).toMatchInlineSnapshot(` 37 | " 38 | /* test.css */ 39 | .test { display: inline-block; } 40 | .test { padding: 10px; } 41 | .test:focus { color: red; } 42 | .test:focus { border: blue; } 43 | " 44 | `); 45 | }); 46 | 47 | it('extract pseudo selectors', async () => { 48 | const styles: StyleDefinition = loadStyleDefinitions( 49 | () => ['test.css'], 50 | () => ` 51 | .test { 52 | display: inline-block; 53 | } 54 | 55 | .test:not(.some) { 56 | padding: 10px; 57 | } 58 | 59 | .test:focus, 60 | .test::hover { 61 | padding: 10px; 62 | } 63 | ` 64 | ); 65 | await styles; 66 | 67 | const extracted = getCriticalRules('
', styles); 68 | 69 | expect(extracted).toMatchInlineSnapshot(` 70 | " 71 | /* test.css */ 72 | .test { display: inline-block; } 73 | .test:not(.some) { padding: 10px; } 74 | .test:focus, 75 | .test::hover { padding: 10px; } 76 | " 77 | `); 78 | }); 79 | 80 | it('handle exotic styles', async () => { 81 | const styles: StyleDefinition = loadStyleDefinitions( 82 | () => ['test.css'], 83 | () => ` 84 | @media screen and (min-width:1350px){.content__L0XJ\\+{color:red}} 85 | .primary__L4\\+dg{ color: blue} 86 | .primary__L4+dg{ color: wrong} 87 | ` 88 | ); 89 | await styles; 90 | 91 | const extracted = getCriticalRules('
', styles); 92 | 93 | expect(extracted).toMatchInlineSnapshot(` 94 | " 95 | /* test.css */ 96 | 97 | @media screen and (min-width:1350px) { 98 | .content__L0XJ\\\\+ { color: red; } 99 | } 100 | 101 | .primary__L4\\\\+dg { color: blue; } 102 | " 103 | `); 104 | }); 105 | 106 | it('reducing styles', async () => { 107 | const styles: StyleDefinition = loadStyleDefinitions( 108 | () => ['test.css'], 109 | () => ` 110 | .button { 111 | display: inline-block; 112 | } 113 | 114 | .button:focus { 115 | padding: 10px; 116 | } 117 | ` 118 | ); 119 | await styles; 120 | 121 | const extracted = getCriticalRules( 122 | '
', 123 | alterProjectStyles(styles, { 124 | pruneSelector: (selector) => selector.includes(':focus'), 125 | }) 126 | ); 127 | 128 | expect(extracted).toMatchInlineSnapshot(` 129 | " 130 | /* test.css */ 131 | .button { display: inline-block; } 132 | " 133 | `); 134 | }); 135 | 136 | it('opening styles styles', async () => { 137 | const styles: StyleDefinition = loadStyleDefinitions( 138 | () => ['test.css'], 139 | () => ` 140 | .parent { 141 | display: inline-block; 142 | } 143 | 144 | .child { 145 | padding: 10px; 146 | } 147 | .parent .child { 148 | padding: 10px; 149 | } 150 | 151 | .grand .child { 152 | padding: 10px; 153 | } 154 | 155 | .top .child { 156 | margin: 10px; 157 | } 158 | 159 | .grand.top .child { 160 | position: correct 161 | } 162 | 163 | .grand.top { 164 | margin: 10px; 165 | } 166 | 167 | .grand.top.incorrect .child { 168 | position: incorrect 169 | } 170 | ` 171 | ); 172 | await styles; 173 | 174 | const extracted = getCriticalRules('
', styles); 175 | 176 | expect(extracted).toMatchInlineSnapshot(` 177 | " 178 | /* test.css */ 179 | .child { padding: 10px; } 180 | .grand .child { padding: 10px; } 181 | .top .child { margin: 10px; } 182 | .grand.top .child { position: correct; } 183 | .grand.top { margin: 10px; } 184 | " 185 | `); 186 | }); 187 | 188 | it('should ignore media rules nested to unknown at rules', async () => { 189 | const styles: StyleDefinition = loadStyleDefinitions( 190 | () => ['test.css'], 191 | () => ` 192 | @supports (display:grid) { 193 | .a { display: grid; } 194 | 195 | @media only print { 196 | .a { color: red; } 197 | } 198 | } 199 | ` 200 | ); 201 | await styles; 202 | 203 | const extracted = getCriticalRules('
', styles); 204 | 205 | expect(extracted).toMatchInlineSnapshot(` 206 | " 207 | /* test.css */ 208 | @supports (display:grid) { 209 | .a { display: grid; } 210 | 211 | @media only print { 212 | .a { color: red; } 213 | } 214 | }" 215 | `); 216 | }); 217 | 218 | describe('Serializable definitions', () => { 219 | test('Serialized defintion is equal to original', async () => { 220 | const styles: StyleDefinition = loadStyleDefinitions( 221 | () => ['test.css'], 222 | () => ` 223 | @media screen and (min-width:1350px){.content__L0XJ\\+{color:red}} 224 | .primary__L4\\+dg{ color: blue} 225 | .primary__L4+dg{ color: wrong} 226 | ` 227 | ); 228 | await styles; 229 | 230 | const serializedDefinition = JSON.stringify(serializeStylesLookup(styles)); 231 | const deserializedDefinition = loadSerializedLookup(JSON.parse(serializedDefinition)); 232 | 233 | expect(deserializedDefinition.lookup).toEqual(styles.lookup); 234 | expect(deserializedDefinition.ast).toEqual(styles.ast); 235 | expect(deserializedDefinition.urlPrefix).toEqual(styles.urlPrefix); 236 | expect(deserializedDefinition.isReady).toEqual(styles.isReady); 237 | expect(typeof deserializedDefinition.then).toEqual(typeof styles.then); 238 | }); 239 | 240 | test('Serializing unready definition throws', async () => { 241 | const styles: StyleDefinition = loadStyleDefinitions( 242 | async () => ['test.css'], 243 | async () => ` 244 | @media screen and (min-width:1350px){.content__L0XJ\\+{color:red}} 245 | .primary__L4\\+dg{ color: blue} 246 | .primary__L4+dg{ color: wrong} 247 | ` 248 | ); 249 | 250 | expect(() => serializeStylesLookup(styles)).toThrowErrorMatchingInlineSnapshot( 251 | `"used-styles: style definitions are not ready yet. You should \`await discoverProjectStyles(...)\`"` 252 | ); 253 | }); 254 | 255 | test('Invalid value in serializers throws', async () => { 256 | expect(() => serializeStylesLookup({} as any)).toThrowErrorMatchingInlineSnapshot( 257 | `"used-styles: style definitions has to be created using discoverProjectStyles or loadStyleDefinitions"` 258 | ); 259 | 260 | expect(() => loadSerializedLookup({} as any)).toThrowErrorMatchingInlineSnapshot( 261 | `"used-styles: serialized style definition should be created with serializeStylesLookup"` 262 | ); 263 | 264 | expect(() => loadSerializedLookup('invalid' as any)).toThrowErrorMatchingInlineSnapshot( 265 | `"used-styles: got a string instead of serialized style definition object, make sure to parse it back to JS object first"` 266 | ); 267 | }); 268 | 269 | test('Serialized defintion is awaitable just like original', async () => { 270 | const styles: StyleDefinition = loadStyleDefinitions( 271 | () => ['test.css'], 272 | () => ` 273 | @media screen and (min-width:1350px){.content__L0XJ\\+{color:red}} 274 | .primary__L4\\+dg{ color: blue} 275 | .primary__L4+dg{ color: wrong} 276 | ` 277 | ); 278 | await styles; 279 | 280 | const serializedDefinition = JSON.stringify(serializeStylesLookup(styles)); 281 | const deserializedDefinition = loadSerializedLookup(JSON.parse(serializedDefinition)); 282 | 283 | await deserializedDefinition; 284 | 285 | const resolve = jest.fn(); 286 | await deserializedDefinition.then(resolve); 287 | 288 | expect(resolve).toBeCalledTimes(1); 289 | }); 290 | }); 291 | 292 | describe('CSS Cascade Layers', () => { 293 | it('handles CSS Cascade Layers', async () => { 294 | const styles = loadStyleDefinitions( 295 | () => ['test.css'], 296 | () => ` 297 | @layer module, state; 298 | 299 | .a { 300 | color: red; 301 | } 302 | 303 | @layer state { 304 | .a { 305 | background-color: brown; 306 | } 307 | .b { 308 | border: medium solid limegreen; 309 | } 310 | } 311 | 312 | @layer module { 313 | .a { 314 | border: medium solid violet; 315 | background-color: yellow; 316 | color: white; 317 | } 318 | } 319 | ` 320 | ); 321 | 322 | await styles; 323 | 324 | const extracted = getCriticalRules('
', styles); 325 | 326 | expect(extracted).toMatchInlineSnapshot(` 327 | " 328 | /* test.css */ 329 | @layer module, state; 330 | /* test.css */ 331 | .a { color: red; } 332 | 333 | @layer state { 334 | .a { background-color: brown; } 335 | } 336 | @layer module { 337 | .a { border: medium solid violet; 338 | background-color: yellow; 339 | color: white; } 340 | }" 341 | `); 342 | }); 343 | 344 | it('handles CSS Cascade Layers across multiple files', async () => { 345 | const CSS = { 346 | 'index.css': ` 347 | @layer module, state; 348 | 349 | .a { 350 | color: red; 351 | } 352 | 353 | @layer state { 354 | .a { 355 | background-color: brown; 356 | } 357 | .b { 358 | border: medium solid limegreen; 359 | } 360 | } 361 | 362 | @layer module { 363 | .a { 364 | border: medium solid violet; 365 | background-color: yellow; 366 | color: white; 367 | } 368 | } 369 | `, 370 | 'chunk.css': ` 371 | @layer state { 372 | .b { 373 | border: medium solid limegreen; 374 | } 375 | } 376 | `, 377 | } as const; 378 | 379 | const styles = loadStyleDefinitions( 380 | () => Object.keys(CSS), 381 | (file) => CSS[file as keyof typeof CSS] 382 | ); 383 | 384 | await styles; 385 | 386 | const extracted = getCriticalRules('
', styles); 387 | 388 | expect(extracted).toMatchInlineSnapshot(` 389 | " 390 | /* index.css */ 391 | @layer module, state; 392 | /* index.css */ 393 | 394 | @layer state { 395 | .b { border: medium solid limegreen; } 396 | } 397 | /* chunk.css */ 398 | " 399 | `); 400 | }); 401 | 402 | it('handles nested CSS Cascade Layers', async () => { 403 | const CSS = { 404 | 'index.css': ` 405 | 406 | .a { 407 | color: red; 408 | } 409 | 410 | @layer state { 411 | .a { 412 | background-color: brown; 413 | } 414 | .b { 415 | border: medium solid limegreen; 416 | } 417 | 418 | @layer module { 419 | .a { 420 | border: medium solid violet; 421 | background-color: yellow; 422 | color: white; 423 | } 424 | } 425 | } 426 | 427 | @layer state.module { 428 | .a { 429 | border-color: blue; 430 | } 431 | } 432 | `, 433 | }; 434 | 435 | const styles = loadStyleDefinitions( 436 | () => Object.keys(CSS), 437 | (file) => CSS[file as keyof typeof CSS] 438 | ); 439 | 440 | await styles; 441 | 442 | const extracted = getCriticalRules('
', styles); 443 | 444 | expect(extracted).toMatchInlineSnapshot(` 445 | " 446 | /* index.css */ 447 | .a { color: red; } 448 | 449 | @layer state { 450 | .a { background-color: brown; } 451 | } 452 | @layer state { 453 | @layer module { 454 | .a { border: medium solid violet; 455 | background-color: yellow; 456 | color: white; } 457 | } 458 | } 459 | @layer state.module { 460 | .a { border-color: blue; } 461 | }" 462 | `); 463 | }); 464 | 465 | test('CSS Cascade Layers definition should be always at top', async () => { 466 | const CSS = { 467 | 'index.css': ` 468 | .a { 469 | color: red; 470 | } 471 | 472 | @layer state { 473 | .a { 474 | background-color: brown; 475 | } 476 | .b { 477 | color: red; 478 | } 479 | } 480 | 481 | @layer module { 482 | .a { 483 | border: medium solid violet; 484 | background-color: yellow; 485 | color: white; 486 | } 487 | } 488 | `, 489 | 'chunk.css': ` 490 | @layer module, state; 491 | 492 | @layer state { 493 | .b { 494 | border: medium solid limegreen; 495 | } 496 | } 497 | `, 498 | } as const; 499 | 500 | const styles = loadStyleDefinitions( 501 | () => Object.keys(CSS), 502 | (file) => CSS[file as keyof typeof CSS] 503 | ); 504 | 505 | await styles; 506 | 507 | const extracted = getCriticalRules('
', styles); 508 | 509 | expect(extracted).toMatchInlineSnapshot(` 510 | " 511 | /* chunk.css */ 512 | @layer module, state; 513 | /* index.css */ 514 | 515 | @layer state { 516 | .b { color: red; } 517 | } 518 | /* chunk.css */ 519 | 520 | @layer state { 521 | .b { border: medium solid limegreen; } 522 | }" 523 | `); 524 | }); 525 | 526 | it('should handle CSS Cascade Layers with @layer at-rule mixed with media rules', async () => { 527 | const CSS = { 528 | 'index.css': ` 529 | .a { 530 | color: red; 531 | } 532 | 533 | @layer state { 534 | .a { 535 | background-color: brown; 536 | } 537 | 538 | @media only print { 539 | .b { 540 | color: red; 541 | } 542 | 543 | .a { 544 | color: red; 545 | } 546 | } 547 | } 548 | 549 | @layer module { 550 | .a { 551 | border: medium solid violet; 552 | background-color: yellow; 553 | color: white; 554 | } 555 | } 556 | `, 557 | 'chunk.css': ` 558 | @layer module, state; 559 | 560 | @media only print { 561 | .a { 562 | width: 42px; 563 | } 564 | 565 | @layer state { 566 | .b { 567 | border: medium solid limegreen; 568 | } 569 | } 570 | } 571 | `, 572 | 'other.css': ` 573 | .b { 574 | background-color: brown; 575 | } 576 | `, 577 | } as const; 578 | 579 | const styles = loadStyleDefinitions( 580 | () => Object.keys(CSS), 581 | (file) => CSS[file as keyof typeof CSS] 582 | ); 583 | 584 | await styles; 585 | 586 | const extracted = getCriticalRules('
', styles); 587 | 588 | expect(extracted).toMatchInlineSnapshot(` 589 | " 590 | /* chunk.css */ 591 | @layer module, state; 592 | /* index.css */ 593 | 594 | @layer state { 595 | @media only print { 596 | .b { color: red; } 597 | } 598 | } 599 | /* chunk.css */ 600 | 601 | @media only print { 602 | @layer state { 603 | .b { border: medium solid limegreen; } 604 | } 605 | } 606 | /* other.css */ 607 | .b { background-color: brown; } 608 | " 609 | `); 610 | }); 611 | }); 612 | }); 613 | -------------------------------------------------------------------------------- /__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import { remapStyles } from '../src/utils/style'; 2 | 3 | describe('scanForStyles', () => { 4 | it('should map simple style', () => { 5 | const styles = {}; 6 | 7 | remapStyles( 8 | { 9 | a: '.a{}, .b .c{}, .d>.e:not(focused){}', 10 | b: '.a {}, .f~.g{}, @media (screen) { .media { } }', 11 | }, 12 | styles 13 | ); 14 | 15 | expect(styles).toEqual({ 16 | a: { 17 | a: true, 18 | b: true, 19 | }, 20 | b: { 21 | a: true, 22 | }, 23 | c: { 24 | a: true, 25 | }, 26 | d: { 27 | a: true, 28 | }, 29 | e: { 30 | a: true, 31 | }, 32 | f: { 33 | b: true, 34 | }, 35 | g: { 36 | b: true, 37 | }, 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/mapStyles.spec.ts: -------------------------------------------------------------------------------- 1 | import { extractParents, mapSelector } from '../src/parser/utils'; 2 | 3 | describe('test map selector', () => { 4 | it('should return the single style', () => { 5 | expect(mapSelector('.a')).toEqual(['a']); 6 | }); 7 | 8 | it('should return the double style', () => { 9 | expect(mapSelector('.a.b')).toEqual(['a', 'b']); 10 | }); 11 | 12 | it('should keep the last style', () => { 13 | expect(mapSelector('.a .b')).toEqual(['b']); 14 | }); 15 | 16 | it('should keep the last style', () => { 17 | expect(mapSelector('.a .b input')).toEqual(['b']); 18 | }); 19 | 20 | it('should keep the last style', () => { 21 | expect(mapSelector('.a .b:focus')).toEqual(['b']); 22 | }); 23 | 24 | it('should keep the last style', () => { 25 | expect(mapSelector('.a input>.b:focus>input')).toEqual(['b']); 26 | expect(mapSelector('.item+.item:before')).toEqual(['item']); 27 | }); 28 | }); 29 | 30 | describe('test parent selector', () => { 31 | it('should return the single style', () => { 32 | expect(extractParents('.a')).toEqual([]); 33 | }); 34 | 35 | it('should return the double style', () => { 36 | expect(extractParents('.a.b c')).toEqual(['a', 'b']); 37 | }); 38 | 39 | it('should keep the first style; drop last', () => { 40 | expect(extractParents('.a .b')).toEqual(['a']); 41 | }); 42 | 43 | it('should drop tag, keep both', () => { 44 | expect(extractParents('.a .b input')).toEqual(['a', 'b']); 45 | }); 46 | 47 | it('should handle pseudo', () => { 48 | expect(extractParents('.a .b:focus .c')).toEqual(['a', 'b']); 49 | }); 50 | 51 | it('edge cases', () => { 52 | expect(extractParents('.a input>.b:focus>input.c')).toEqual(['a']); 53 | expect(extractParents('.item+.item:before')).toEqual([]); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /__tests__/media.spec.ts: -------------------------------------------------------------------------------- 1 | import { extractUnmatchableFromAst } from '../src/getCSS'; 2 | import { fromAst } from '../src/parser/fromAst'; 3 | import { buildAst } from '../src/parser/toAst'; 4 | 5 | describe('media selectors', () => { 6 | const CSS = ` 7 | .a { 8 | border:2px solid; 9 | } 10 | .b { 11 | border:1px solid; 12 | } 13 | .d { position: relative; } 14 | 15 | @media only screen and (max-width: 600px) { 16 | .a { position: relative; } 17 | .b { position: relative; } 18 | .c { position: relative; } 19 | } 20 | 21 | @media only screen and (max-width: 600px) { 22 | .c { color: red; } 23 | } 24 | 25 | @media only print { 26 | .c { position: relative; } 27 | } 28 | 29 | body { color: red } 30 | 31 | .a, .b, input { color: rightColor } 32 | `; 33 | 34 | const ast = buildAst(CSS); 35 | 36 | it('should return nothing if nothing used', () => { 37 | const css = fromAst([], ast); 38 | expect(css).toEqual(''); 39 | }); 40 | 41 | it('should extract unmatchable parts', () => { 42 | const css = extractUnmatchableFromAst({ ast }); 43 | 44 | expect(css[0].css).toEqual(`body { color: red; } 45 | input { color: rightColor; } 46 | `); 47 | }); 48 | 49 | it('should return what was used', () => { 50 | const css = fromAst(['d'], ast); 51 | expect(css.trim()).toEqual('.d { position: relative; }'); 52 | }); 53 | 54 | it('should use media if not used: case a', () => { 55 | const css = fromAst(['a'], ast); 56 | 57 | expect(css.trim()).toEqual(`.a { border: 2px solid; } 58 | 59 | @media only screen and (max-width: 600px) { 60 | .a { position: relative; } 61 | } 62 | 63 | .a { color: rightColor; }`); 64 | }); 65 | 66 | it('should use media if not used: case c', () => { 67 | const css = fromAst(['c'], ast); 68 | 69 | expect(css.trim()).toEqual(`@media only screen and (max-width: 600px) { 70 | .c { position: relative; } 71 | .c { color: red; } 72 | } 73 | @media only print { 74 | .c { position: relative; } 75 | }`); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /__tests__/react-css-stream.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { renderToStaticNodeStream } from 'react-dom/server'; 3 | 4 | import { createStyleStream, enableReactOptimization, getUsedStyles } from '../src'; 5 | import { StylesLookupTable } from '../src/types'; 6 | 7 | describe('React css stream', () => { 8 | const createLookup = (lookup: StylesLookupTable): any => ({ 9 | isReady: true, 10 | ast: Object.keys(lookup).reduce((acc, file) => { 11 | lookup[file].forEach( 12 | (f) => 13 | (acc[f] = { 14 | selectors: [], 15 | }) 16 | ); 17 | 18 | return acc; 19 | }, {} as any), 20 | lookup, 21 | }); 22 | 23 | beforeEach(() => enableReactOptimization()); 24 | 25 | it('simple map', () => { 26 | const map = getUsedStyles( 27 | `
`, 28 | createLookup({ 29 | a: ['1'], 30 | b: ['2'], 31 | d: ['3'], 32 | e: ['4'], 33 | f: ['5', '6'], 34 | }) 35 | ); 36 | expect(map).toEqual(['1', '2', '3', '5', '6']); 37 | }); 38 | 39 | it('React.renderToStream', async () => { 40 | const styles: any = {}; 41 | const cssStream = createStyleStream( 42 | createLookup({ 43 | a: ['file1'], 44 | b: ['file1', 'file2'], 45 | zz: ['file3'], 46 | notused: ['file4'], 47 | }), 48 | (style) => { 49 | styles[style] = (styles[style] || 0) + 1; 50 | } 51 | ); 52 | const output = renderToStaticNodeStream( 53 |
54 |
55 |
56 |
57 | {Array(1000) 58 | .fill(1) 59 | .map((_, index) => ( 60 |
{index}
61 | ))} 62 |
63 |
64 |
65 |
66 |
67 | ); 68 | 69 | const streamString = async (readStream: NodeJS.ReadableStream) => { 70 | const result = []; 71 | 72 | for await (const chunk of readStream) { 73 | result.push(chunk); 74 | } 75 | 76 | return result.join(''); 77 | }; 78 | 79 | const [tr, base] = await Promise.all([streamString(output.pipe(cssStream)), streamString(output)]); 80 | 81 | expect(base).toEqual(tr); 82 | 83 | expect(styles).toEqual({ 84 | file1: 1, 85 | file2: 1, 86 | file3: 1, 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /__tests__/react.integration.spec.tsx: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | import * as React from 'react'; 4 | import { renderToStaticNodeStream, renderToString } from 'react-dom/server'; 5 | 6 | import { 7 | alterProjectStyles, 8 | createCriticalStyleStream, 9 | createLink, 10 | createStyleStream, 11 | enableReactOptimization, 12 | getCriticalStyles, 13 | getUsedStyles, 14 | parseProjectStyles, 15 | serializeStylesLookup, 16 | loadSerializedLookup, 17 | } from '../src'; 18 | import { discoverProjectStyles } from '../src/index-node'; 19 | import { StyleDefinition } from '../src/types'; 20 | 21 | describe('File based css stream', () => { 22 | let styles: StyleDefinition; 23 | 24 | beforeEach(() => { 25 | styles = discoverProjectStyles(resolve(__dirname, 'css'), (name) => { 26 | const match = name.match(/file(\d+).css/); 27 | 28 | return match && +match[1]; 29 | }); 30 | }); 31 | 32 | it('fail: should throw if not ready', () => { 33 | expect(() => getUsedStyles('', styles)).toThrow(); 34 | }); 35 | 36 | it('skip: test', async () => { 37 | await styles; 38 | 39 | const s1 = getCriticalStyles('
', styles); 40 | const s2 = getCriticalStyles( 41 | '
', 42 | alterProjectStyles(styles, { filter: (x) => x.indexOf('file2') !== 0 }) 43 | ); 44 | const s3 = getCriticalStyles( 45 | '
', 46 | alterProjectStyles(alterProjectStyles(styles, { filter: (x) => x.indexOf('file2') !== 0 }), { 47 | filter: (x) => x.indexOf('file1') !== 0, 48 | }) 49 | ); 50 | 51 | expect(s1).not.toBe(s2); 52 | expect(s2).not.toBe(s3); 53 | expect(s1).toMatch(/data-used-styles=\"file1.css,file2.css\"/); 54 | expect(s2).toMatch(/data-used-styles=\"file1.css\"/); 55 | expect(s3).toBe(''); 56 | }); 57 | 58 | it('memoization: test', async () => { 59 | await styles; 60 | 61 | const s1 = getCriticalStyles('
', styles); 62 | // @ts-expect-error 63 | delete styles.ast.file2; 64 | 65 | const s2 = getCriticalStyles('
', styles); 66 | const s3 = getCriticalStyles('
', styles); 67 | 68 | expect(s1).toBe(s2); 69 | expect(s1).not.toBe(s3); 70 | }); 71 | 72 | it('ok: test', async () => { 73 | await styles; 74 | expect(getUsedStyles('', styles)).toEqual(['file1.css']); 75 | expect(getCriticalStyles('', styles)).toMatchSnapshot(); 76 | 77 | const output = renderToString( 78 |
79 |
80 |
81 | {Array(10) 82 | .fill(1) 83 | .map((_, index) => ( 84 |
85 | {index} 86 |
87 | ))} 88 |
89 |
90 |
91 |
92 | ); 93 | 94 | const usedFiles = getUsedStyles(output, styles); 95 | const usedCritical = getCriticalStyles(output, styles); 96 | 97 | expect(usedFiles).toEqual(['file1.css', 'file2.css']); 98 | 99 | expect(usedCritical).toMatch(/selector-11/); 100 | expect(usedCritical).toMatch(/data-from-file1/); 101 | expect(usedCritical).not.toMatch(/data-wrong-file1/); 102 | expect(usedCritical).toMatch(/data-from-file2/); 103 | expect(usedCritical).not.toMatch(/data-wrong-file1/); 104 | 105 | expect(usedCritical).toMatch(/ANIMATION_NAME/); 106 | 107 | expect(usedCritical).toMatch(/htmlRED/); 108 | }); 109 | 110 | test('works with (de)serialized styles definition', async () => { 111 | await styles; 112 | 113 | const lookupAfterSerialization = loadSerializedLookup(JSON.parse(JSON.stringify(serializeStylesLookup(styles)))); 114 | 115 | expect(getUsedStyles('', lookupAfterSerialization)).toEqual(['file1.css']); 116 | expect(getCriticalStyles('', lookupAfterSerialization)).toMatchSnapshot(); 117 | 118 | const output = renderToString( 119 |
120 |
121 |
122 | {Array(10) 123 | .fill(1) 124 | .map((_, index) => ( 125 |
126 | {index} 127 |
128 | ))} 129 |
130 |
131 |
132 |
133 | ); 134 | 135 | const usedFiles = getUsedStyles(output, lookupAfterSerialization); 136 | const usedCritical = getCriticalStyles(output, lookupAfterSerialization); 137 | 138 | expect(usedFiles).toEqual(['file1.css', 'file2.css']); 139 | 140 | expect(usedCritical).toMatch(/selector-11/); 141 | expect(usedCritical).toMatch(/data-from-file1/); 142 | expect(usedCritical).not.toMatch(/data-wrong-file1/); 143 | expect(usedCritical).toMatch(/data-from-file2/); 144 | expect(usedCritical).not.toMatch(/data-wrong-file1/); 145 | 146 | expect(usedCritical).toMatch(/ANIMATION_NAME/); 147 | 148 | expect(usedCritical).toMatch(/htmlRED/); 149 | }); 150 | }); 151 | 152 | describe('React css stream', () => { 153 | const file1 = ` 154 | .a, .b, .input { color: rightColor } 155 | .a2, .b1, .input { color: wrong } 156 | input { color: rightInput } 157 | 158 | .responsive { color: red; } 159 | @media (print) { 160 | .responsive { color: blue; } 161 | } 162 | `; 163 | 164 | const file2 = ` 165 | .c2, .d1, .input { marker: wrong } 166 | .c, .d { marker: blueMark } 167 | .responsive { color: red; } 168 | `; 169 | 170 | const file3 = ` 171 | .somethingOdd { marker: wrong } 172 | `; 173 | 174 | let lookup: any; 175 | 176 | beforeAll(() => { 177 | lookup = parseProjectStyles({ 178 | file1, 179 | file2, 180 | file3, 181 | }); 182 | }); 183 | 184 | enableReactOptimization(); 185 | 186 | describe('React.renderToStream', () => { 187 | let criticalStream: any; 188 | let cssStream: any; 189 | let output: any; 190 | 191 | const streamString = async (readStream: NodeJS.ReadableStream) => { 192 | const result = []; 193 | 194 | for await (const chunk of readStream) { 195 | result.push(chunk); 196 | } 197 | 198 | return result.join(''); 199 | }; 200 | 201 | it('setup', () => { 202 | criticalStream = createCriticalStyleStream(lookup); 203 | cssStream = createStyleStream(lookup, createLink); 204 | 205 | output = renderToStaticNodeStream( 206 |
207 |
208 |
209 |
210 | {Array(1000) 211 | .fill(1) 212 | .map((_, index) => ( 213 |
214 | {index} 215 |
216 | ))} 217 |
218 | datacontent 219 |
220 |
221 |
222 |
223 | ); 224 | }); 225 | 226 | let htmlCritical = ''; 227 | let htmlLink = ''; 228 | let html = ''; 229 | 230 | it('render', async () => { 231 | // tslint:disable variable-name 232 | const htmlCritical_a = streamString(output.pipe(criticalStream)); 233 | const htmlLink_a = streamString(output.pipe(cssStream)); 234 | const html_a = streamString(output); 235 | // tslint:enable 236 | 237 | htmlCritical = await htmlCritical_a; 238 | htmlLink = await htmlLink_a; 239 | html = await html_a; 240 | }); 241 | 242 | it('expectations', () => { 243 | expect(html).toMatch(/datacontent/); 244 | expect(htmlCritical).toMatch(/datacontent/); 245 | expect(htmlLink).toMatch(/datacontent/); 246 | 247 | expect(htmlCritical).toMatch(/rightColor/); 248 | expect(htmlCritical).toMatch(/blueMark/); 249 | expect(htmlCritical).toMatch(/rightInput/); 250 | expect(htmlCritical).not.toMatch(/wrong/); 251 | 252 | expect(htmlLink).toMatch(/file1/); 253 | expect(htmlLink).toMatch(/file2/); 254 | expect(htmlLink).not.toMatch(/file3/); 255 | 256 | expect(htmlCritical).toMatchSnapshot(); 257 | expect(htmlLink).toMatchSnapshot(); 258 | }); 259 | 260 | it('critical', () => { 261 | const critical = getCriticalStyles(html, lookup); 262 | expect(critical).toMatchSnapshot(); 263 | }); 264 | }); 265 | }); 266 | -------------------------------------------------------------------------------- /__tests__/simple-example.spec.tsx: -------------------------------------------------------------------------------- 1 | import { getCriticalRules, parseProjectStyles } from '../src'; 2 | 3 | test('simplest example ever', () => { 4 | const styles = parseProjectStyles({ 5 | style: `body { padding:0 } .button {color:red}`, 6 | }); 7 | 8 | const critical = getCriticalRules('', styles); 9 | 10 | expect(critical).toMatchInlineSnapshot(` 11 | " 12 | /* style */ 13 | body { padding: 0; } 14 | 15 | /* style */ 16 | .button { color: red; } 17 | " 18 | `); 19 | }); 20 | -------------------------------------------------------------------------------- /example/react-18-streaming/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | -------------------------------------------------------------------------------- /example/react-18-streaming/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "used-styles-react-18", 3 | "private": true, 4 | "scripts": { 5 | "start:server": "ts-node ./src/server.tsx", 6 | "start:client": "vite . --port 3001" 7 | }, 8 | "dependencies": { 9 | "express": "^4.19.2", 10 | "multistream": "^4.1.0", 11 | "react": "^18.0.0", 12 | "react-dom": "^18.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/express": "^4.17.21", 16 | "@types/multistream": "^4.1.3", 17 | "@types/react": "^18.2.73", 18 | "@types/react-dom": "^18.2.23", 19 | "ts-node": "^10.9.2", 20 | "vite": "^5.2.6" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/react-18-streaming/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const App = () => ( 4 |
5 | your app
and your styles
6 |
7 | ); 8 | -------------------------------------------------------------------------------- /example/react-18-streaming/src/client.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import { moveStyles } from '../../../moveStyles'; 5 | 6 | import { App } from './App'; 7 | 8 | // Call before `ReactDOM.hydrateRoot` 9 | moveStyles(); 10 | 11 | ReactDOM.hydrateRoot( 12 | document.getElementById('root'), 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /example/react-18-streaming/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | // entry-server.js 2 | import { Readable } from 'node:stream'; 3 | import { Transform } from 'stream'; 4 | 5 | import { Response } from 'express'; 6 | import MultiStream from 'multistream'; 7 | import React from 'react'; 8 | import { renderToPipeableStream } from 'react-dom/server'; 9 | 10 | import { App } from './App'; 11 | 12 | // small utility for "readable" streams 13 | const readableString = (string: string) => { 14 | const s = new Readable(); 15 | s.push(string); 16 | s.push(null); 17 | s._read = () => true; 18 | 19 | return s; 20 | }; 21 | 22 | const ABORT_DELAY = 10000; 23 | 24 | export const renderApp = async (res: Response, styledStream: Transform) => { 25 | let didError = false; 26 | 27 | const { pipe, abort } = renderToPipeableStream( 28 | 29 | 30 | , 31 | { 32 | onShellError() { 33 | res.sendStatus(500); 34 | }, 35 | // wait for all pieces to be ready 36 | onAllReady() { 37 | res.status(didError ? 500 : 200); 38 | res.set({ 'Content-Type': 'text/html' }); 39 | 40 | // allow client to start loading js bundle 41 | res.write(`
`); 42 | 43 | const endStream = readableString('
'); 44 | 45 | // concatenate all streams together 46 | const streams = [ 47 | styledStream, // the main content 48 | endStream, // closing tags 49 | ]; 50 | 51 | new MultiStream(streams).pipe(res); 52 | 53 | // start by piping react and styled transform stream 54 | pipe(styledStream); 55 | }, 56 | onError(error) { 57 | didError = true; 58 | console.error(error); 59 | }, 60 | } 61 | ); 62 | 63 | setTimeout(() => { 64 | abort(); 65 | }, ABORT_DELAY); 66 | }; 67 | -------------------------------------------------------------------------------- /example/react-18-streaming/src/server.tsx: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { 4 | createCriticalStyleStream, 5 | // createStyleStream, 6 | // createLink, 7 | } from '../../../'; 8 | import { discoverProjectStyles } from '../../../node'; 9 | 10 | import { renderApp } from './entry-server'; 11 | 12 | const app = express(); 13 | 14 | // generate lookup table on server start 15 | const stylesLookup = discoverProjectStyles(__dirname); 16 | 17 | app.use('*', async (_req, res) => { 18 | await stylesLookup; 19 | 20 | try { 21 | // create a style steam 22 | // const styledStream = createStyleStream(stylesLookup, (style) => { 23 | // // _return_ link tag, and it will be appended to the stream output 24 | // return createLink(`${style}`) // 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr-react-streaming-ts", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "node server", 8 | "build": "npm run build:client && npm run build:server", 9 | "build:client": "vite build --ssrManifest --outDir dist/client", 10 | "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", 11 | "preview": "cross-env NODE_ENV=production node server" 12 | }, 13 | "dependencies": { 14 | "compression": "^1.7.4", 15 | "express": "^4.18.2", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "sirv": "^2.0.4", 19 | "used-styles": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/express": "^4.17.21", 23 | "@types/node": "^20.10.5", 24 | "@types/react": "^18.2.45", 25 | "@types/react-dom": "^18.2.18", 26 | "@vitejs/plugin-react": "^4.2.1", 27 | "cross-env": "^7.0.3", 28 | "typescript": "^5.3.3", 29 | "vite": "^5.0.10" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/server.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | import express from 'express'; 4 | import { loadStyleDefinitions, createCriticalStyleStream } from 'used-styles'; 5 | import { discoverProjectStyles } from 'used-styles/node'; 6 | 7 | // Constants 8 | const isProduction = process.env.NODE_ENV === 'production'; 9 | const port = process.env.PORT || 5173; 10 | const base = process.env.BASE || '/'; 11 | const ABORT_DELAY = 10000; 12 | 13 | // generate lookup table on server start 14 | const stylesLookup = isProduction 15 | ? discoverProjectStyles('./dist/client') 16 | : // in dev mode vite injects all styles to element 17 | loadStyleDefinitions(async () => []); 18 | 19 | // Cached production assets 20 | const templateHtml = isProduction ? await fs.readFile('./dist/client/index.html', 'utf-8') : ''; 21 | const ssrManifest = isProduction ? await fs.readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8') : undefined; 22 | 23 | // Create http server 24 | const app = express(); 25 | 26 | // Add Vite or respective production middlewares 27 | let vite; 28 | 29 | if (!isProduction) { 30 | const { createServer } = await import('vite'); 31 | 32 | vite = await createServer({ 33 | server: { middlewareMode: true }, 34 | appType: 'custom', 35 | base, 36 | }); 37 | 38 | app.use(vite.middlewares); 39 | } else { 40 | const compression = (await import('compression')).default; 41 | const sirv = (await import('sirv')).default; 42 | app.use(compression()); 43 | app.use(base, sirv('./dist/client', { extensions: [] })); 44 | } 45 | 46 | // Serve HTML 47 | app.use('*', async (req, res) => { 48 | try { 49 | await stylesLookup; 50 | 51 | const url = req.originalUrl.replace(base, ''); 52 | 53 | let template; 54 | let render; 55 | 56 | if (!isProduction) { 57 | // Always read fresh template in development 58 | template = await fs.readFile('./index.html', 'utf-8'); 59 | template = await vite.transformIndexHtml(url, template); 60 | render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render; 61 | } else { 62 | template = templateHtml; 63 | render = (await import('./dist/server/entry-server.js')).render; 64 | } 65 | 66 | const styledStream = createCriticalStyleStream(stylesLookup); 67 | 68 | let didError = false; 69 | 70 | const { pipe, abort } = render(url, ssrManifest, { 71 | onShellError() { 72 | res.status(500); 73 | res.set({ 'Content-Type': 'text/html' }); 74 | res.send('

Something went wrong

'); 75 | }, 76 | // Can use also `onAllReady` callback 77 | onShellReady() { 78 | res.status(didError ? 500 : 200); 79 | res.set({ 'Content-Type': 'text/html' }); 80 | 81 | let [htmlStart, htmlEnd] = template.split(``); 82 | 83 | // React 19 supports document metadata out of box, 84 | // but for react 18 we can use `react-helmet-async` here: 85 | // htmlStart = htmlStart.replace(``, helmet.title.toString()) 86 | 87 | res.write(htmlStart); 88 | 89 | styledStream.pipe(res, { end: false }); 90 | 91 | pipe(styledStream); 92 | 93 | styledStream.on('end', () => { 94 | res.end(htmlEnd); 95 | }); 96 | }, 97 | onError(error) { 98 | didError = true; 99 | console.error(error); 100 | // You can log crash reports here: 101 | // logServerCrashReport(error) 102 | }, 103 | }); 104 | 105 | setTimeout(() => { 106 | abort(); 107 | }, ABORT_DELAY); 108 | } catch (e) { 109 | vite?.ssrFixStacktrace(e); 110 | console.log(e.stack); 111 | res.status(500).end(e.stack); 112 | } 113 | }); 114 | 115 | // Start http server 116 | app.listen(port, () => { 117 | console.log(`Server started at http://localhost:${port}`); 118 | }); 119 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from "react" 2 | import reactLogo from './assets/react.svg' 3 | import './App.css' 4 | 5 | // Works also with SSR as expected 6 | const Card = lazy(() => import("./Card")) 7 | 8 | function App() { 9 | return ( 10 | <> 11 | 19 |

Vite + React

20 | 21 | Loading card component...

}> 22 | 23 |
24 | 25 |

26 | Click on the Vite and React logos to learn more 27 |

28 | 29 | ) 30 | } 31 | 32 | export default App 33 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/Card.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | function Card() { 4 | const [count, setCount] = useState(0) 5 | 6 | return ( 7 |
8 | 11 |

12 | Edit src/App.tsx and save to test HMR 13 |

14 |
15 | ) 16 | } 17 | 18 | export default Card 19 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | import { moveStyles } from 'used-styles/moveStyles' 5 | import App from './App' 6 | 7 | // Call before `ReactDOM.hydrateRoot` 8 | moveStyles() 9 | 10 | ReactDOM.hydrateRoot( 11 | document.getElementById('root') as HTMLElement, 12 | 13 | 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { RenderToPipeableStreamOptions, renderToPipeableStream } from 'react-dom/server' 3 | import App from './App' 4 | 5 | export function render(_url: string, _ssrManifest: any, options: RenderToPipeableStreamOptions) { 6 | return renderToPipeableStream( 7 | 8 | 9 | , 10 | options 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "jsx": "preserve" 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /example/ssr-react-streaming-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/README.md: -------------------------------------------------------------------------------- 1 | # React Streaming SSR demo 2 | 3 | This template is build using `vite` and uses `renderToPipeableStream` for SSR. 4 | To run the demo: 5 | 6 | - dev: `yarn install && yarn dev`. 7 | - prod: `yarn install && yarn build && yarn preview` 8 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr-react-streaming", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "node server", 8 | "build": "npm run build:client && npm run build:server", 9 | "build:client": "vite build --ssrManifest --outDir dist/client", 10 | "build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server", 11 | "preview": "cross-env NODE_ENV=production node server" 12 | }, 13 | "dependencies": { 14 | "compression": "^1.7.4", 15 | "express": "^4.18.2", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "sirv": "^2.0.4", 19 | "used-styles": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@vitejs/plugin-react": "^4.2.1", 23 | "cross-env": "^7.0.3", 24 | "vite": "^5.0.10" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/server.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | import express from 'express'; 4 | import { loadStyleDefinitions, createCriticalStyleStream } from 'used-styles'; 5 | import { discoverProjectStyles } from 'used-styles/node'; 6 | 7 | // Constants 8 | const isProduction = process.env.NODE_ENV === 'production'; 9 | const port = process.env.PORT || 5173; 10 | const base = process.env.BASE || '/'; 11 | const ABORT_DELAY = 10000; 12 | 13 | // generate lookup table on server start 14 | const stylesLookup = isProduction 15 | ? discoverProjectStyles('./dist/client') 16 | : // in dev mode vite injects all styles to element 17 | loadStyleDefinitions(async () => []); 18 | 19 | // Cached production assets 20 | const templateHtml = isProduction ? await fs.readFile('./dist/client/index.html', 'utf-8') : ''; 21 | const ssrManifest = isProduction ? await fs.readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8') : undefined; 22 | 23 | // Create http server 24 | const app = express(); 25 | 26 | // Add Vite or respective production middlewares 27 | let vite; 28 | 29 | if (!isProduction) { 30 | const { createServer } = await import('vite'); 31 | 32 | vite = await createServer({ 33 | server: { middlewareMode: true }, 34 | appType: 'custom', 35 | base, 36 | }); 37 | 38 | app.use(vite.middlewares); 39 | } else { 40 | const compression = (await import('compression')).default; 41 | const sirv = (await import('sirv')).default; 42 | app.use(compression()); 43 | app.use(base, sirv('./dist/client', { extensions: [] })); 44 | } 45 | 46 | // Serve HTML 47 | app.use('*', async (req, res) => { 48 | try { 49 | await stylesLookup; 50 | 51 | const url = req.originalUrl.replace(base, ''); 52 | 53 | let template; 54 | let render; 55 | 56 | if (!isProduction) { 57 | // Always read fresh template in development 58 | template = await fs.readFile('./index.html', 'utf-8'); 59 | template = await vite.transformIndexHtml(url, template); 60 | render = (await vite.ssrLoadModule('/src/entry-server.jsx')).render; 61 | } else { 62 | template = templateHtml; 63 | render = (await import('./dist/server/entry-server.js')).render; 64 | } 65 | 66 | const styledStream = createCriticalStyleStream(stylesLookup); 67 | 68 | let didError = false; 69 | 70 | const { pipe, abort } = render(url, ssrManifest, { 71 | onShellError() { 72 | res.status(500); 73 | res.set({ 'Content-Type': 'text/html' }); 74 | res.send('

Something went wrong

'); 75 | }, 76 | // Can use also `onAllReady` callback 77 | onShellReady() { 78 | res.status(didError ? 500 : 200); 79 | res.set({ 'Content-Type': 'text/html' }); 80 | 81 | let [htmlStart, htmlEnd] = template.split(``); 82 | 83 | // React 19 supports document metadata out of box, 84 | // but for react 18 we can use `react-helmet-async` here: 85 | // htmlStart = htmlStart.replace(``, helmet.title.toString()) 86 | 87 | res.write(htmlStart); 88 | 89 | styledStream.pipe(res, { end: false }); 90 | 91 | pipe(styledStream); 92 | 93 | styledStream.on('end', () => { 94 | res.end(htmlEnd); 95 | }); 96 | }, 97 | onError(error) { 98 | didError = true; 99 | console.error(error); 100 | // You can log crash reports here: 101 | // logServerCrashReport(error) 102 | }, 103 | }); 104 | 105 | setTimeout(() => { 106 | abort(); 107 | }, ABORT_DELAY); 108 | } catch (e) { 109 | vite?.ssrFixStacktrace(e); 110 | console.log(e.stack); 111 | res.status(500).end(e.stack); 112 | } 113 | }); 114 | 115 | // Start http server 116 | app.listen(port, () => { 117 | console.log(`Server started at http://localhost:${port}`); 118 | }); 119 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from "react" 2 | import reactLogo from "./assets/react.svg" 3 | import "./App.css" 4 | 5 | // Works also with SSR as expected 6 | const Card = lazy(() => import("./Card")) 7 | 8 | function App() { 9 | return ( 10 | <> 11 | 19 |

Vite + React

20 | 21 | Loading card component...

}> 22 | 23 |
24 | 25 |

26 | Click on the Vite and React logos to learn more 27 |

28 | 29 | ) 30 | } 31 | 32 | export default App 33 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/src/Card.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | function Card() { 4 | const [count, setCount] = useState(0) 5 | 6 | return ( 7 |
8 | 11 |

12 | Edit src/App.jsx and save to test HMR 13 |

14 |
15 | ) 16 | } 17 | 18 | export default Card 19 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/src/entry-client.jsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | import { moveStyles } from 'used-styles/moveStyles' 5 | import App from './App' 6 | 7 | // Call before `ReactDOM.hydrateRoot` 8 | moveStyles() 9 | 10 | ReactDOM.hydrateRoot( 11 | document.getElementById('root'), 12 | 13 | 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/src/entry-server.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToPipeableStream } from 'react-dom/server' 3 | import App from './App' 4 | 5 | export function render(url, ssrManifest, options) { 6 | return renderToPipeableStream( 7 | 8 | 9 | , 10 | options 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/ssr-react-streaming/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /example/ssr-react-ts/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/ssr-react-ts/README.md: -------------------------------------------------------------------------------- 1 | # React SSR + TS demo 2 | 3 | This template is build using `vite` and uses `renderToString` for SSR. 4 | To run the demo: 5 | 6 | - dev: `yarn install && yarn dev`. 7 | - prod: `yarn install && yarn build && yarn preview` 8 | -------------------------------------------------------------------------------- /example/ssr-react-ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/ssr-react-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr-react-ts", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "node server", 8 | "build": "npm run build:client && npm run build:server", 9 | "build:client": "vite build --ssrManifest --outDir dist/client", 10 | "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", 11 | "preview": "cross-env NODE_ENV=production node server" 12 | }, 13 | "dependencies": { 14 | "compression": "^1.7.4", 15 | "express": "^4.18.2", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "sirv": "^2.0.4", 19 | "used-styles": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/express": "^4.17.21", 23 | "@types/node": "^20.10.5", 24 | "@types/react": "^18.2.45", 25 | "@types/react-dom": "^18.2.18", 26 | "@vitejs/plugin-react": "^4.2.1", 27 | "cross-env": "^7.0.3", 28 | "typescript": "^5.3.3", 29 | "vite": "^5.0.10" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/ssr-react-ts/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react-ts/server.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | import express from 'express'; 4 | import { loadStyleDefinitions, getCriticalStyles } from 'used-styles'; 5 | import { discoverProjectStyles } from 'used-styles/node'; 6 | 7 | // Constants 8 | const isProduction = process.env.NODE_ENV === 'production'; 9 | const port = process.env.PORT || 5173; 10 | const base = process.env.BASE || '/'; 11 | 12 | // generate lookup table on server start 13 | const stylesLookup = isProduction 14 | ? discoverProjectStyles('./dist/client') 15 | : // in dev mode vite injects all styles to element 16 | loadStyleDefinitions(async () => []); 17 | 18 | // Cached production assets 19 | const templateHtml = isProduction ? await fs.readFile('./dist/client/index.html', 'utf-8') : ''; 20 | const ssrManifest = isProduction ? await fs.readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8') : undefined; 21 | 22 | // Create http server 23 | const app = express(); 24 | 25 | // Add Vite or respective production middlewares 26 | let vite; 27 | 28 | if (!isProduction) { 29 | const { createServer } = await import('vite'); 30 | 31 | vite = await createServer({ 32 | server: { middlewareMode: true }, 33 | appType: 'custom', 34 | base, 35 | }); 36 | 37 | app.use(vite.middlewares); 38 | } else { 39 | const compression = (await import('compression')).default; 40 | const sirv = (await import('sirv')).default; 41 | app.use(compression()); 42 | app.use(base, sirv('./dist/client', { extensions: [] })); 43 | } 44 | 45 | // Serve HTML 46 | app.use('*', async (req, res) => { 47 | try { 48 | await stylesLookup; 49 | 50 | const url = req.originalUrl.replace(base, ''); 51 | 52 | let template; 53 | let render; 54 | 55 | if (!isProduction) { 56 | // Always read fresh template in development 57 | template = await fs.readFile('./index.html', 'utf-8'); 58 | template = await vite.transformIndexHtml(url, template); 59 | render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render; 60 | } else { 61 | template = templateHtml; 62 | render = (await import('./dist/server/entry-server.js')).render; 63 | } 64 | 65 | const rendered = await render(url, ssrManifest); 66 | 67 | const criticalCSS = getCriticalStyles(rendered.html, stylesLookup); 68 | 69 | const appHead = (rendered.head ?? '') + criticalCSS; 70 | const appHtml = rendered.html ?? ''; 71 | 72 | const html = template.replace(``, appHead).replace(``, appHtml); 73 | 74 | res.status(200).set({ 'Content-Type': 'text/html' }).send(html); 75 | } catch (e) { 76 | vite?.ssrFixStacktrace(e); 77 | console.log(e.stack); 78 | res.status(500).end(e.stack); 79 | } 80 | }); 81 | 82 | // Start http server 83 | app.listen(port, () => { 84 | console.log(`Server started at http://localhost:${port}`); 85 | }); 86 | -------------------------------------------------------------------------------- /example/ssr-react-ts/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /example/ssr-react-ts/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import reactLogo from './assets/react.svg' 3 | import './App.css' 4 | 5 | function App() { 6 | const [count, setCount] = useState(0) 7 | 8 | return ( 9 | <> 10 | 18 |

Vite + React

19 |
20 | 23 |

24 | Edit src/App.tsx and save to test HMR 25 |

26 |
27 |

28 | Click on the Vite and React logos to learn more 29 |

30 | 31 | ) 32 | } 33 | 34 | export default App 35 | -------------------------------------------------------------------------------- /example/ssr-react-ts/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react-ts/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | import App from './App' 5 | 6 | ReactDOM.hydrateRoot( 7 | document.getElementById('root') as HTMLElement, 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /example/ssr-react-ts/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | import App from './App' 4 | 5 | export function render() { 6 | const html = ReactDOMServer.renderToString( 7 | 8 | 9 | 10 | ) 11 | return { html } 12 | } 13 | -------------------------------------------------------------------------------- /example/ssr-react-ts/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/ssr-react-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/ssr-react-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /example/ssr-react-ts/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "jsx": "preserve" 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /example/ssr-react-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /example/ssr-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/ssr-react/README.md: -------------------------------------------------------------------------------- 1 | # React SSR demo 2 | 3 | This template is build using `vite` and uses `renderToString` for SSR. 4 | To run the demo: 5 | 6 | - dev: `yarn install && yarn dev`. 7 | - prod: `yarn install && yarn build && yarn preview` 8 | -------------------------------------------------------------------------------- /example/ssr-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/ssr-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "node server", 8 | "build": "npm run build:client && npm run build:server", 9 | "build:client": "vite build --ssrManifest --outDir dist/client", 10 | "build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server", 11 | "preview": "cross-env NODE_ENV=production node server" 12 | }, 13 | "dependencies": { 14 | "compression": "^1.7.4", 15 | "express": "^4.18.2", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "sirv": "^2.0.4", 19 | "used-styles": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.45", 23 | "@types/react-dom": "^18.2.18", 24 | "@vitejs/plugin-react": "^4.2.1", 25 | "cross-env": "^7.0.3", 26 | "ts-node": "^10.9.2", 27 | "vite": "^5.0.10" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/ssr-react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react/server.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { readFile } from 'node:fs/promises'; 3 | 4 | import express from 'express'; 5 | import { loadStyleDefinitions, getCriticalStyles } from 'used-styles'; 6 | // import { discoverProjectStyles } from 'used-styles/node'; 7 | // FIXME: ESM is not fully supported 8 | import { discoverProjectStyles } from 'used-styles/dist/es5/index-node.js'; 9 | 10 | // Constants 11 | const isProduction = process.env.NODE_ENV === 'production'; 12 | const port = process.env.PORT || 5173; 13 | const base = process.env.BASE || '/'; 14 | 15 | // generate lookup table on server start 16 | const stylesLookup = isProduction 17 | ? discoverProjectStyles('./dist/client') 18 | : // in dev mode vite injects all styles to element 19 | loadStyleDefinitions( 20 | async () => [], 21 | () => '', 22 | () => true 23 | ); 24 | 25 | // Cached production assets 26 | const templateHtml = isProduction ? await readFile('./dist/client/index.html', 'utf-8') : ''; 27 | const ssrManifest = isProduction ? await readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8') : undefined; 28 | 29 | // Create http server 30 | const app = express(); 31 | 32 | // Add Vite or respective production middlewares 33 | let vite; 34 | 35 | if (!isProduction) { 36 | const { createServer } = await import('vite'); 37 | 38 | vite = await createServer({ 39 | server: { middlewareMode: true }, 40 | appType: 'custom', 41 | base, 42 | }); 43 | 44 | app.use(vite.middlewares); 45 | } else { 46 | const compression = (await import('compression')).default; 47 | const sirv = (await import('sirv')).default; 48 | app.use(compression()); 49 | app.use(base, sirv('./dist/client', { extensions: [] })); 50 | } 51 | 52 | // Serve HTML 53 | app.use('*', async (req, res) => { 54 | try { 55 | await stylesLookup; 56 | 57 | const url = req.originalUrl.replace(base, ''); 58 | 59 | let template; 60 | let render; 61 | 62 | if (!isProduction) { 63 | // Always read fresh template in development 64 | template = await fs.readFile('./index.html', 'utf-8'); 65 | template = await vite.transformIndexHtml(url, template); 66 | render = (await vite.ssrLoadModule('/src/entry-server.jsx')).render; 67 | } else { 68 | template = templateHtml; 69 | render = (await import('./dist/server/entry-server.js')).render; 70 | } 71 | 72 | const rendered = await render(url, ssrManifest); 73 | 74 | const criticalCSS = getCriticalStyles(rendered.html, stylesLookup); 75 | 76 | const appHead = (rendered.head ?? '') + criticalCSS; 77 | const appHtml = rendered.html ?? ''; 78 | 79 | const html = template.replace(``, appHead).replace(``, appHtml); 80 | 81 | res.status(200).set({ 'Content-Type': 'text/html' }).send(html); 82 | } catch (e) { 83 | vite?.ssrFixStacktrace(e); 84 | console.log(e.stack); 85 | res.status(500).end(e.stack); 86 | } 87 | }); 88 | 89 | // Start http server 90 | app.listen(port, () => { 91 | console.log(`Server started at http://localhost:${port}`); 92 | }); 93 | -------------------------------------------------------------------------------- /example/ssr-react/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /example/ssr-react/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import reactLogo from './assets/react.svg' 3 | import './App.css' 4 | 5 | function App() { 6 | const [count, setCount] = useState(0) 7 | 8 | return ( 9 | <> 10 | 18 |

Vite + React

19 |
20 | 23 |

24 | Edit src/App.jsx and save to test HMR 25 |

26 |
27 |

28 | Click on the Vite and React logos to learn more 29 |

30 | 31 | ) 32 | } 33 | 34 | export default App 35 | -------------------------------------------------------------------------------- /example/ssr-react/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ssr-react/src/entry-client.jsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | import App from './App' 5 | 6 | ReactDOM.hydrateRoot( 7 | document.getElementById('root'), 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /example/ssr-react/src/entry-server.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | import App from './App' 4 | 5 | export function render() { 6 | const html = ReactDOMServer.renderToString( 7 | 8 | 9 | 10 | ) 11 | return { html } 12 | } 13 | -------------------------------------------------------------------------------- /example/ssr-react/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/ssr-react/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | modulePathIgnorePatterns: ['/dist/'], 4 | }; 5 | -------------------------------------------------------------------------------- /moveStyles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/moveStyles.js", 4 | "jsnext:main": "../dist/es2015/moveStyles.js", 5 | "module": "../dist/es2015/moveStyles.js", 6 | "types": "../dist/es2015/moveStyles.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/index-node.js", 4 | "jsnext:main": "../dist/es2015/index-node.js", 5 | "module": "../dist/es2015/index-node.js", 6 | "types": "../dist/es2015/index-node.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "used-styles", 3 | "version": "3.0.0", 4 | "description": "Collect styles used on to create a page", 5 | "main": "dist/es5/index.js", 6 | "jsnext:main": "dist/es2015/index.js", 7 | "module": "dist/es2015/index.js", 8 | "types": "dist/es5/index.d.ts", 9 | "scripts": { 10 | "build": "lib-builder build", 11 | "test": "jest", 12 | "prepublish-only": "yarn changelog && yarn build", 13 | "lint": "lib-builder lint", 14 | "dev": "lib-builder dev", 15 | "test:ci": "jest --runInBand --coverage", 16 | "release": "yarn build && yarn test", 17 | "format": "lib-builder format", 18 | "size": "npx size-limit", 19 | "size:report": "npx size-limit --json > .size.json", 20 | "update": "lib-builder update", 21 | "typecheck": "tsc --noEmit", 22 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 23 | "changelog:rewrite": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 24 | }, 25 | "repository": "https://github.com/theKashey/used-styles/", 26 | "author": "theKashey ", 27 | "license": "MIT", 28 | "devDependencies": { 29 | "@size-limit/preset-small-lib": "^2.1.6", 30 | "@theuiteam/lib-builder": "0.1.4" 31 | }, 32 | "engines": { 33 | "node": ">=11" 34 | }, 35 | "files": [ 36 | "dist", 37 | "moveStyles", 38 | "node" 39 | ], 40 | "keywords": [ 41 | "nodejs", 42 | "SSR", 43 | "CSS", 44 | "webpack", 45 | "code splitting" 46 | ], 47 | "dependencies": { 48 | "crc-32": "^1.2.0", 49 | "kashe": "^1.0.4", 50 | "memoize-one": "^5.2.1", 51 | "postcss": "^8.0.0", 52 | "scan-directory": "^1.0.0", 53 | "tslib": "^2.3.1" 54 | }, 55 | "husky": { 56 | "hooks": { 57 | "pre-commit": "lint-staged" 58 | } 59 | }, 60 | "lint-staged": { 61 | "*.{ts,tsx}": [ 62 | "prettier --write", 63 | "eslint --fix", 64 | "git add" 65 | ], 66 | "*.{js,css,json,md}": [ 67 | "prettier --write", 68 | "git add" 69 | ] 70 | }, 71 | "prettier": { 72 | "printWidth": 120, 73 | "trailingComma": "es5", 74 | "tabWidth": 2, 75 | "semi": true, 76 | "singleQuote": true 77 | }, 78 | "module:es2019": "dist/es2019/index.js" 79 | } 80 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | let mode = 'plain'; 2 | 3 | export const isReact = () => mode === 'react'; 4 | 5 | export const enableReactOptimization = () => mode = 'react'; -------------------------------------------------------------------------------- /src/createLink.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * creates a style sheet link 3 | * @param styleFile 4 | */ 5 | export const createLink = (styleFile: string) => ``; 6 | -------------------------------------------------------------------------------- /src/getCSS.ts: -------------------------------------------------------------------------------- 1 | import { kashe } from 'kashe'; 2 | 3 | import { StyleAst } from './parser/ast'; 4 | import { extractUnmatchable, fromAst, getUnmatchableRules } from './parser/fromAst'; 5 | import { 6 | AbstractStyleDefinition, 7 | FlagType, 8 | SelectionFilter, 9 | StyleChunk, 10 | StyleDefinition, 11 | SyncStyleDefinition, 12 | UsedTypes, 13 | UsedTypesRef, 14 | } from './types'; 15 | import { assertIsReady } from './utils/async'; 16 | import { createUsedFilter } from './utils/cache'; 17 | import { unique } from './utils/order'; 18 | import { flattenClasses, getStylesInText } from './utils/string'; 19 | 20 | export const getUnusableStyles = kashe( 21 | (def: StyleDefinition): UsedTypesRef => 22 | Object.keys(def.ast || {}) 23 | .filter((key) => getUnmatchableRules(def.ast[key]).length > 0) 24 | .reduce((acc, file) => { 25 | acc[file] = true; 26 | 27 | return acc; 28 | }, {} as UsedTypesRef) 29 | ); 30 | 31 | export const astToUsedStyles = kashe((styles: string[], def: AbstractStyleDefinition) => { 32 | const { lookup, ast } = def; 33 | const fetches: Record = {}; 34 | const visitedStyles = new Set(); 35 | 36 | styles.forEach((className) => { 37 | if (visitedStyles.has(className)) { 38 | return; 39 | } 40 | 41 | visitedStyles.add(className); 42 | 43 | const classes = className.split(' '); 44 | 45 | classes.forEach((singleClass) => { 46 | if (lookup.hasOwnProperty(singleClass)) { 47 | const files = lookup[singleClass]; 48 | 49 | files.forEach((file) => { 50 | if (!fetches[file]) { 51 | fetches[file] = {}; 52 | } 53 | 54 | fetches[file][singleClass] = true; 55 | }); 56 | } 57 | }); 58 | }); 59 | 60 | return { 61 | fetches, 62 | usage: Object.keys(ast).filter((file) => !!fetches[file]), 63 | }; 64 | }); 65 | 66 | const getUsedStylesIn = kashe((styles: string[], def: StyleDefinition): UsedTypes => { 67 | assertIsReady(def); 68 | 69 | const { usage } = astToUsedStyles(styles, def); 70 | const flags: FlagType = { 71 | ...getUnusableStyles(def), 72 | ...usage.reduce((acc, file) => { 73 | acc[file] = true; 74 | 75 | return acc; 76 | }, {} as FlagType), 77 | }; 78 | 79 | return Object.keys( 80 | Object.keys(def.ast).reduce((acc, file) => { 81 | if (flags[file]) { 82 | acc[file] = true; 83 | } 84 | 85 | return acc; 86 | }, {} as FlagType) 87 | ); 88 | }); 89 | 90 | /** 91 | * returns names of the style files for a given HTML and style definitions 92 | */ 93 | export const getUsedStyles = (htmlCode: string, def: StyleDefinition): UsedTypes => { 94 | assertIsReady(def); 95 | 96 | return getUsedStylesIn(getStylesInText(htmlCode), def); 97 | }; 98 | 99 | const astToStyles = kashe((styles: string[], def: AbstractStyleDefinition, filter?: SelectionFilter): StyleChunk[] => { 100 | const { ast } = def; 101 | const { fetches, usage } = astToUsedStyles(styles, def); 102 | 103 | if (filter && filter.introduceClasses) { 104 | filter.introduceClasses(flattenClasses(styles)); 105 | } 106 | 107 | return usage.map((file) => ({ 108 | file, 109 | css: fromAst(Object.keys(fetches[file]), ast[file], filter), 110 | })); 111 | }); 112 | 113 | export const wrapInStyle = (styles: string, usedStyles: string[] = []) => 114 | styles 115 | ? `` 118 | : ''; 119 | 120 | export const extractUnmatchableFromAst = kashe((ast: StyleAst, filter?: SelectionFilter | undefined): StyleChunk[] => 121 | Object.keys(ast || {}) 122 | .map((file) => { 123 | const css = extractUnmatchable(ast[file], filter); 124 | 125 | if (css) { 126 | return { 127 | file, 128 | css, 129 | } as StyleChunk; 130 | } 131 | 132 | return undefined; 133 | }) 134 | .filter((x) => !!x) 135 | .map((x) => x as StyleChunk) 136 | ); 137 | 138 | export const extractAllUnmatchable = (def: Pick, filter?: SelectionFilter): StyleChunk[] => 139 | extractUnmatchableFromAst(def.ast, filter); 140 | 141 | export const extractAllUnmatchableAsString = kashe((def: StyleDefinition) => 142 | wrapInStyle( 143 | extractAllUnmatchable(def).reduce((acc, { css }) => acc + css, ''), 144 | ['_unmatched'] 145 | ) 146 | ); 147 | 148 | /** 149 | * just wraps with