├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierrc ├── HISTORY.md ├── LICENSE ├── README.md ├── docs ├── demos │ └── basic.md ├── examples │ └── basic.tsx └── index.md ├── jest.config.js ├── now.json ├── package.json ├── src ├── index.tsx └── interface.ts ├── tests ├── index.test.tsx ├── setup.js └── ssr.test.tsx └── tsconfig.json /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | mfsu: false, 5 | favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], 6 | themeConfig: { 7 | name: 'Static-Style-Extract', 8 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | rules: { 4 | 'default-case': 0, 5 | 'import/no-extraneous-dependencies': 0, 6 | 'react-hooks/exhaustive-deps': 0, 7 | 'react/no-find-dom-node': 0, 8 | 'react/no-did-update-set-state': 0, 9 | 'react/no-unused-state': 1, 10 | 'react/sort-comp': 0, 11 | 'jsx-a11y/label-has-for': 0, 12 | 'jsx-a11y/label-has-associated-control': 0, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.fatherrc.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | setup: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@master 15 | 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '16' 19 | 20 | - name: cache package-lock.json 21 | uses: actions/cache@v2 22 | with: 23 | path: package-temp-dir 24 | key: lock-${{ github.sha }} 25 | 26 | - name: create package-lock.json 27 | run: npm i --package-lock-only 28 | 29 | - name: hack for singe file 30 | run: | 31 | if [ ! -d "package-temp-dir" ]; then 32 | mkdir package-temp-dir 33 | fi 34 | cp package-lock.json package-temp-dir 35 | 36 | - name: cache node_modules 37 | id: node_modules_cache_id 38 | uses: actions/cache@v2 39 | with: 40 | path: node_modules 41 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 42 | 43 | - name: install 44 | if: steps.node_modules_cache_id.outputs.cache-hit != 'true' 45 | run: npm i 46 | 47 | lint: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: checkout 51 | uses: actions/checkout@master 52 | 53 | - name: restore cache from package-lock.json 54 | uses: actions/cache@v2 55 | with: 56 | path: package-temp-dir 57 | key: lock-${{ github.sha }} 58 | 59 | - name: restore cache from node_modules 60 | uses: actions/cache@v2 61 | with: 62 | path: node_modules 63 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 64 | 65 | - name: lint 66 | run: npm run lint 67 | 68 | needs: setup 69 | 70 | compile: 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: checkout 74 | uses: actions/checkout@master 75 | 76 | - name: restore cache from package-lock.json 77 | uses: actions/cache@v2 78 | with: 79 | path: package-temp-dir 80 | key: lock-${{ github.sha }} 81 | 82 | - name: restore cache from node_modules 83 | uses: actions/cache@v2 84 | with: 85 | path: node_modules 86 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 87 | 88 | - name: compile 89 | run: npm run compile 90 | 91 | needs: setup 92 | 93 | coverage: 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: checkout 97 | uses: actions/checkout@master 98 | 99 | - name: restore cache from package-lock.json 100 | uses: actions/cache@v2 101 | with: 102 | path: package-temp-dir 103 | key: lock-${{ github.sha }} 104 | 105 | - name: restore cache from node_modules 106 | uses: actions/cache@v2 107 | with: 108 | path: node_modules 109 | key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} 110 | 111 | - name: coverage 112 | run: npm run coverage && bash <(curl -s https://codecov.io/bash) 113 | 114 | needs: setup -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | *.iml 3 | *.log 4 | .idea 5 | .ipr 6 | .iws 7 | *~ 8 | ~* 9 | *.diff 10 | *.patch 11 | *.bak 12 | .DS_Store 13 | Thumbs.db 14 | .project 15 | .*proj 16 | .svn 17 | *.swp 18 | *.swo 19 | *.pyc 20 | *.pyo 21 | node_modules 22 | .cache 23 | *.css 24 | build 25 | lib 26 | es 27 | coverage 28 | yarn.lock 29 | package-lock.json 30 | pnpm-lock.yaml 31 | 32 | # dumi 33 | .umi 34 | .umi-production 35 | .umi-test 36 | .docs 37 | 38 | 39 | # dumi 40 | .dumi/tmp 41 | .dumi/tmp-production -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | ---- 3 | 4 | ## 0.0.1 / 2023-03-07 5 | 6 | - feat: initial commit 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Static-Style-Extract 2 | 3 | Provide a lib like @ant-design/static-style-extract to support generate static css for SSR usage to generate raw css file for caching. 4 | 5 | [![NPM version][npm-image]][npm-url] [![build status][github-actions-image]][github-actions-url] [![Test coverage][coveralls-image]][coveralls-url] [![Dependencies][david-image]][david-url] [![DevDependencies][david-dev-image]][david-dev-url] [![npm download][download-image]][download-url] [![bundle size][bundlephobia-image]][bundlephobia-url] [![dumi][dumi-image]][dumi-url] 6 | 7 | [npm-image]: http://img.shields.io/npm/v/rc-trigger.svg?style=flat-square 8 | [npm-url]: http://npmjs.org/package/rc-trigger 9 | [github-actions-image]: https://github.com/react-component/trigger/workflows/CI/badge.svg 10 | [github-actions-url]: https://github.com/react-component/trigger/actions 11 | [circleci-image]: https://img.shields.io/circleci/react-component/trigger/master?style=flat-square 12 | [circleci-url]: https://circleci.com/gh/react-component/trigger 13 | [coveralls-image]: https://img.shields.io/coveralls/react-component/trigger.svg?style=flat-square 14 | [coveralls-url]: https://coveralls.io/r/react-component/trigger?branch=master 15 | [david-url]: https://david-dm.org/react-component/trigger 16 | [david-image]: https://david-dm.org/react-component/trigger/status.svg?style=flat-square 17 | [david-dev-url]: https://david-dm.org/react-component/trigger?type=dev 18 | [david-dev-image]: https://david-dm.org/react-component/trigger/dev-status.svg?style=flat-square 19 | [download-image]: https://img.shields.io/npm/dm/rc-trigger.svg?style=flat-square 20 | [download-url]: https://npmjs.org/package/rc-trigger 21 | [bundlephobia-url]: https://bundlephobia.com/result?p=rc-trigger 22 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-trigger 23 | [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square 24 | [dumi-url]: https://github.com/umijs/dumi 25 | 26 | ## Install 27 | 28 | ```bash 29 | npm install @ant-design/static-style-extract 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```tsx | pure 35 | import extractStyle from `@ant-design/static-style-extract`; 36 | 37 | const cssText = extractStyle(); // :where(.css-bAMboOo).ant-btn ... 38 | 39 | ``` 40 | 41 | use with custom theme 42 | 43 | ```tsx | pure 44 | import extractStyle from `@ant-design/static-style-extract`; 45 | 46 | const cssText = extractStyle(); // :where(.css-bAMboOo).ant-btn ... 47 | 48 | const cssText = extractStyle((node) => ( 49 | 50 | {node} 51 | 52 | )); 53 | ``` 54 | 55 | ## Example 56 | 57 | http://localhost:8000 58 | 59 | online example: http://react-component.github.io/static-style-extract/ 60 | 61 | ## Development 62 | 63 | ``` 64 | npm install 65 | npm start 66 | ``` 67 | 68 | ## Test Case 69 | 70 | ``` 71 | npm test 72 | ``` 73 | 74 | ## Coverage 75 | 76 | ``` 77 | npm run coverage 78 | ``` 79 | 80 | open coverage/ dir 81 | 82 | ## License 83 | 84 | static-style-extract is released under the MIT license. 85 | -------------------------------------------------------------------------------- /docs/demos/basic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/examples/basic.tsx: -------------------------------------------------------------------------------- 1 | import { extractStyle } from '../../src/index'; 2 | 3 | export default () => { 4 | const cssText = extractStyle(); 5 | 6 | console.log(cssText); 7 | 8 | return ( 9 |
10 |

Basic

11 |

Basic usage

12 |
{cssText}
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: Static-Style-Extract 4 | description: support generate static css for SSR usage to generate raw css file for caching. 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['./tests/setup.js'], 3 | }; 4 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "static-style-extract", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": "dist" } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ant-design/static-style-extract", 3 | "version": "1.0.3", 4 | "description": "support generate static css for SSR usage to generate raw css file for caching.", 5 | "engines": { 6 | "node": ">=8.x" 7 | }, 8 | "keywords": [ 9 | "Static-Style-Extract" 10 | ], 11 | "homepage": "https://github.com/ant-design/static-style-extract", 12 | "author": "kinertang <1127031143@qq.com>", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/ant-design/static-style-extract.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/ant-design/static-style-extract/issues" 19 | }, 20 | "files": [ 21 | "es", 22 | "lib" 23 | ], 24 | "license": "MIT", 25 | "main": "./lib/index", 26 | "module": "./es/index", 27 | "scripts": { 28 | "start": "dumi dev", 29 | "build": "dumi build", 30 | "compile": "father build", 31 | "prepublishOnly": "npm run compile && np --yolo --no-publish", 32 | "lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js", 33 | "test": "rc-test", 34 | "coverage": "rc-test --coverage", 35 | "now-build": "npm run build" 36 | }, 37 | "devDependencies": { 38 | "@rc-component/father-plugin": "^1.0.0", 39 | "@testing-library/jest-dom": "^5.16.4", 40 | "@testing-library/react": "^13.0.0", 41 | "@types/classnames": "^2.2.10", 42 | "@types/jest": "^26.0.15", 43 | "@types/react": "^16.8.19", 44 | "@types/react-dom": "^16.8.4", 45 | "antd": "^5.8.5", 46 | "cross-env": "^7.0.1", 47 | "dumi": "^2.1.0", 48 | "eslint": "^8.0.0", 49 | "father": "^4.0.0", 50 | "less": "^3.10.3", 51 | "np": "^6.2.0", 52 | "rc-test": "^7.0.13", 53 | "react": "^18.0.0", 54 | "react-dom": "^18.0.0", 55 | "regenerator-runtime": "^0.13.7", 56 | "typescript": "^4.0.0" 57 | }, 58 | "dependencies": { 59 | "@ant-design/cssinjs": "^1.8.1", 60 | "@babel/runtime": "^7.18.3", 61 | "@rc-component/portal": "^1.1.0", 62 | "classnames": "^2.3.2", 63 | "rc-align": "^4.0.0", 64 | "rc-motion": "^2.0.0", 65 | "rc-resize-observer": "^1.3.1", 66 | "rc-util": "^5.27.1" 67 | }, 68 | "peerDependencies": { 69 | "antd": "^5.3.0", 70 | "react": ">=16.9.0", 71 | "react-dom": ">=16.9.0" 72 | }, 73 | "resolutions": { 74 | "@types/react": "^16.9.0", 75 | "@types/react-dom": "^16.9.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createCache, 3 | extractStyle as extStyle, 4 | StyleProvider, 5 | } from '@ant-design/cssinjs'; 6 | import * as antd from 'antd'; 7 | import React from 'react'; 8 | import { renderToString } from 'react-dom/server'; 9 | import type { CustomRender } from './interface'; 10 | 11 | const blackList: string[] = [ 12 | 'ConfigProvider', 13 | 'Drawer', 14 | 'Grid', 15 | 'Modal', 16 | 'Popconfirm', 17 | 'Popover', 18 | 'Tooltip', 19 | 'Tour', 20 | ]; 21 | 22 | const ComponentCustomizeRender: Record< 23 | string, 24 | (component: React.ComponentType) => React.ReactNode 25 | > = { 26 | Affix: (Affix) => ( 27 | 28 |
29 | 30 | ), 31 | BackTop: () => , 32 | Dropdown: (Dropdown) => ( 33 | 34 |
35 | 36 | ), 37 | Menu: (Menu) => , 38 | QRCode: (QRCode) => , 39 | Tree: (Tree) => , 40 | }; 41 | 42 | const defaultNode = () => ( 43 | <> 44 | {Object.keys(antd) 45 | .filter( 46 | (name) => 47 | !blackList.includes(name) && name[0] === name[0].toUpperCase(), 48 | ) 49 | .map((compName) => { 50 | const Comp = antd[compName]; 51 | 52 | const renderFunc = ComponentCustomizeRender[compName]; 53 | 54 | if (renderFunc) { 55 | return ( 56 | {renderFunc(Comp)} 57 | ); 58 | } 59 | 60 | return ; 61 | })} 62 | 63 | ); 64 | 65 | export function extractStyle(customTheme?: CustomRender): string { 66 | const cache = createCache(); 67 | renderToString( 68 | 69 | {customTheme ? customTheme(defaultNode()) : defaultNode()} 70 | , 71 | ); 72 | 73 | // Grab style from cache 74 | const styleText = extStyle(cache, true); 75 | 76 | return styleText; 77 | } 78 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | export type CustomRender = (node: JSX.Element) => JSX.Element; -------------------------------------------------------------------------------- /tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { StyleProvider } from '@ant-design/cssinjs'; 2 | import { ConfigProvider } from 'antd'; 3 | import { extractStyle } from '../src/index'; 4 | 5 | const testGreenColor = '#008000'; 6 | describe('Static-Style-Extract', () => { 7 | it('should extract static styles', () => { 8 | const cssText = extractStyle(); 9 | expect(cssText).not.toContain(testGreenColor); 10 | expect(cssText).toContain('.ant-btn'); 11 | }); 12 | 13 | it('should extract static styles with customTheme', () => { 14 | const cssText = extractStyle((node) => ( 15 | 22 | {node} 23 | 24 | )); 25 | expect(cssText).toContain(testGreenColor); 26 | }); 27 | 28 | it('with custom hashPriority', () => { 29 | const cssText = extractStyle((node) => ( 30 | 31 | 38 | {node} 39 | 40 | 41 | )); 42 | expect(cssText).toContain(testGreenColor); 43 | expect(cssText).not.toContain(':where'); 44 | 45 | const cssText2 = extractStyle((node) => ( 46 | 53 | {node} 54 | 55 | )); 56 | expect(cssText2).toContain(':where'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | // jsdom add motion events to test CSSMotion 2 | window.AnimationEvent = window.AnimationEvent || (() => {}); 3 | window.TransitionEvent = window.TransitionEvent || (() => {}); 4 | -------------------------------------------------------------------------------- /tests/ssr.test.tsx: -------------------------------------------------------------------------------- 1 | import { extractStyle } from '../src/index'; 2 | 3 | // Spy useLayoutEffect to avoid warning 4 | jest.mock('react', () => { 5 | const oriReact = jest.requireActual('react'); 6 | return { 7 | ...oriReact, 8 | useLayoutEffect: oriReact.useEffect, 9 | }; 10 | }); 11 | 12 | describe('Static-Style-Extract.SSR', () => { 13 | it('not warning', () => { 14 | const errSpy = jest.spyOn(console, 'error'); 15 | 16 | extractStyle(); 17 | 18 | expect(errSpy).not.toHaveBeenCalled(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "baseUrl": "./", 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "paths": { 12 | "@/*": ["src/*"], 13 | "@@/*": [".dumi/tmp/*"], 14 | "rc-trigger": ["src/index.tsx"] 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------