├── packages ├── css-render │ ├── .browserslistrc │ ├── src │ │ ├── hash.ts │ │ ├── index.ts │ │ ├── exists.ts │ │ ├── CssRender.ts │ │ ├── utils.ts │ │ ├── c.ts │ │ ├── parse.ts │ │ ├── mount.ts │ │ ├── types.ts │ │ └── render.ts │ ├── .eslintignore │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── index.html │ ├── playground │ │ └── index.ts │ ├── __tests__ │ │ ├── parse │ │ │ ├── index.spec.ts │ │ │ └── pathTestCases.spec.ts │ │ ├── find │ │ │ └── index.spec.ts │ │ ├── render │ │ │ ├── sassSpec.spec.ts │ │ │ └── index.spec.ts │ │ └── mount │ │ │ └── index.spec.ts │ ├── karma.conf.js │ ├── rollup.config.js │ ├── package.json │ ├── CHANGELOG.md │ ├── README.md │ └── CHANGELOG.json ├── plugin-bem │ ├── .browserslistrc │ ├── .eslintignore │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── tsconfig.esm.json │ ├── tsconfig.cjs.json │ ├── package.json │ ├── CHANGELOG.md │ ├── index.ts │ ├── CHANGELOG.json │ └── __tests__ │ │ └── index.spec.ts ├── test-shared │ ├── README.md │ ├── tsconfig.json │ ├── tsconfig.esm.json │ ├── tsconfig.cjs.json │ ├── utils.ts │ └── package.json ├── vue3-ssr │ ├── .browserslistrc │ ├── .eslintignore │ ├── .eslintrc.js │ ├── babel.config.js │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ ├── jest.config.js │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.spec.ts.snap │ │ └── index.spec.ts │ ├── package.json │ ├── README.md │ ├── src │ │ └── index.ts │ ├── CHANGELOG.md │ └── CHANGELOG.json ├── docs │ ├── package.json │ └── docs │ │ ├── installation.md │ │ ├── .vitepress │ │ └── config.js │ │ ├── css-render-instance.md │ │ ├── qa.md │ │ ├── mount.md │ │ ├── get-started.md │ │ ├── plugin-development.md │ │ ├── cnode-and-render.md │ │ └── index.md └── eslint-config │ ├── package.json │ └── base.js ├── common ├── git-hooks │ ├── precommit │ └── commit-msg.sample ├── config │ └── rush │ │ ├── command-line.json │ │ ├── repo-state.json │ │ ├── version-policies.json │ │ ├── common-versions.json │ │ ├── .npmrc │ │ └── .npmrc-publish └── scripts │ ├── install-run-rushx.js │ └── install-run-rush.js ├── playground ├── prototype.ts ├── testHint.ts ├── testHint.js ├── perf-log.js ├── perf.log ├── button.js └── button.perf.js ├── CONTRIBUTING.md ├── package.json ├── tsconfig.json ├── MEMO.md ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── LICENSE ├── .gitignore ├── TODO.md ├── rush.json └── README.md /packages/css-render/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 1 version 2 | > 1% -------------------------------------------------------------------------------- /packages/plugin-bem/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 1 version 2 | > 1% -------------------------------------------------------------------------------- /packages/test-shared/README.md: -------------------------------------------------------------------------------- 1 | # @css-render/test-shared -------------------------------------------------------------------------------- /packages/vue3-ssr/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 1 version 2 | > 1% -------------------------------------------------------------------------------- /packages/css-render/src/hash.ts: -------------------------------------------------------------------------------- 1 | export { default } from '@emotion/hash' 2 | -------------------------------------------------------------------------------- /packages/test-shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.esm.json" 3 | } -------------------------------------------------------------------------------- /packages/css-render/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | **/*/dist 4 | **/*/esm 5 | **/*/lib 6 | -------------------------------------------------------------------------------- /packages/plugin-bem/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | **/*/dist 4 | **/*/esm 5 | **/*/lib 6 | -------------------------------------------------------------------------------- /packages/vue3-ssr/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | **/*/dist 4 | **/*/esm 5 | **/*/lib 6 | -------------------------------------------------------------------------------- /common/git-hooks/precommit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cp README.md packages/css-render/README.md 4 | cp README.md packages/docs/docs/index.md 5 | add . 6 | -------------------------------------------------------------------------------- /packages/plugin-bem/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.esm.json", 3 | "include": [ 4 | "./index.ts", 5 | "./__tests__/**/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/css-render/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@rushstack/eslint-config/patch/modern-module-resolution') 2 | 3 | module.exports = { 4 | extends: ['@css-render/eslint-config/base'] 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugin-bem/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@rushstack/eslint-config/patch/modern-module-resolution') 2 | 3 | module.exports = { 4 | extends: ['@css-render/eslint-config/base'] 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugin-bem/README.md: -------------------------------------------------------------------------------- 1 | # @css-render/plugin-bem 2 | A plugin of css-render that helping generate BEM standard CSS 3 | 4 | See [css-render](https://github.com/07akioni/css-render) -------------------------------------------------------------------------------- /packages/vue3-ssr/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@rushstack/eslint-config/patch/modern-module-resolution') 2 | 3 | module.exports = { 4 | extends: ['@css-render/eslint-config/base'] 5 | } 6 | -------------------------------------------------------------------------------- /packages/vue3-ssr/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/plugin-bem/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /common/config/rush/command-line.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", 3 | "commands": [], 4 | "parameters": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugin-bem/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: [ 4 | './index.ts' 5 | ], 6 | reporters: ['jest-standard-reporter'] 7 | } 8 | -------------------------------------------------------------------------------- /playground/prototype.ts: -------------------------------------------------------------------------------- 1 | interface I { 2 | x: number 3 | y: string 4 | } 5 | 6 | const i: I = Object.create({ 7 | x: 0 8 | }) 9 | 10 | function a (): I { 11 | return i 12 | } 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | css-render use `rush` as monorepo manager. 4 | 5 | `npm i -g @mircosoft/rush` 6 | 7 | Before you start to make some change, run `rush update` then `rush install`. 8 | -------------------------------------------------------------------------------- /common/config/rush/repo-state.json: -------------------------------------------------------------------------------- 1 | // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. 2 | { 3 | "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" 4 | } 5 | -------------------------------------------------------------------------------- /common/config/rush/version-policies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "definitionName": "lockStepVersion", 4 | "policyName": "css-render", 5 | "version": "0.15.14", 6 | "nextBump": "patch" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /packages/css-render/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.esm.json", 3 | "include": [ 4 | "src/**/*", 5 | "__tests__/**/*" 6 | ], 7 | "compilerOptions": { 8 | "rootDir": "." 9 | } 10 | } -------------------------------------------------------------------------------- /common/config/rush/common-versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", 3 | "preferredVersions": { 4 | }, 5 | "allowedAlternativeVersions": {} 6 | } 7 | -------------------------------------------------------------------------------- /packages/plugin-bem/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./esm", 5 | "rootDir": ".", 6 | "module": "ESNext" 7 | }, 8 | "include": [ 9 | "./index.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/test-shared/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./esm", 5 | "rootDir": ".", 6 | "module": "ESNext" 7 | }, 8 | "include": [ 9 | "./utils.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/css-render/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "./src", 6 | "module": "CommonJS" 7 | }, 8 | "include": [ 9 | "./src/**/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/css-render/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./esm", 5 | "rootDir": "./src", 6 | "module": "ESNext" 7 | }, 8 | "include": [ 9 | "./src/**/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/plugin-bem/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": ".", 6 | "module": "CommonJS" 7 | }, 8 | "include": [ 9 | "./index.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/test-shared/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": ".", 6 | "module": "CommonJS" 7 | }, 8 | "include": [ 9 | "./utils.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/vue3-ssr/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "./src", 6 | "module": "CommonJS" 7 | }, 8 | "include": [ 9 | "./src/**/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/vue3-ssr/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./esm", 5 | "rootDir": "./src", 6 | "module": "ESNext" 7 | }, 8 | "include": [ 9 | "./src/**/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/vue3-ssr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.esm.json", 3 | "include": [ 4 | "src/**/*", 5 | "__tests__/**/*" 6 | ], 7 | "compilerOptions": { 8 | "rootDir": ".", 9 | "types": [ 10 | "jest" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /packages/css-render/src/index.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { CssRender } from './CssRender' 3 | 4 | export * from './types' 5 | export { default as hash } from './hash' 6 | export { exists } from './exists' 7 | export { CssRender } 8 | export default CssRender 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "lint": "eslint \"packages/**/*.ts\" \"packages/**/*.js\" --fix", 5 | "perf": "npm run build && node playground/perf-log.js >> playground/perf.log" 6 | }, 7 | "author": "07akioni", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/css-render/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CssRender Test 5 | 6 | 7 |
123
123
8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/css-render/playground/index.ts: -------------------------------------------------------------------------------- 1 | import { CssRender } from '../src/index' 2 | 3 | const { c } = CssRender() 4 | 5 | const style = c([ 6 | c('.a', { 7 | color: 'red' 8 | }, [ 9 | c('.b', { 10 | color: 'green' 11 | }) 12 | ]) 13 | ]) 14 | 15 | style.mount({ 16 | id: 'ab' 17 | }) 18 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@css-render/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build:vercel": "vitepress build docs", 7 | "build": "", 8 | "dev": "vitepress dev docs" 9 | }, 10 | "devDependencies": { 11 | "vitepress": "~0.21.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "ESNext", 5 | "declaration": true, 6 | "declarationMap": false, 7 | "sourceMap": false, 8 | "lib": [ 9 | "ESNext", 10 | "DOM" 11 | ], 12 | "strict": true, 13 | "target": "ES6" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/vue3-ssr/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | module.exports = { 6 | collectCoverage: true, 7 | collectCoverageFrom: [ 8 | 'src/**/*' 9 | ], 10 | reporters: ['jest-standard-reporter'] 11 | } 12 | -------------------------------------------------------------------------------- /playground/testHint.ts: -------------------------------------------------------------------------------- 1 | import CssRender from '@css-render/core/src' 2 | 3 | const cssr = CssRender() 4 | const { c } = cssr 5 | 6 | const target = c('selector', { 7 | background: 'black', 8 | from: { 9 | background: 'black' 10 | } 11 | }, [ 12 | c('selector2', { 13 | background: 'black', 14 | from: { 15 | background: 'black' 16 | } 17 | }) 18 | ]) 19 | -------------------------------------------------------------------------------- /packages/docs/docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Core 4 | The core of css-render 5 | ```bash 6 | npm i -D css-render 7 | ``` 8 | 9 | ## Plugin BEM 10 | css-render plugin for writing bem codes. 11 | ```bash 12 | npm i -D css-render @css-render/plugin-bem 13 | ``` 14 | 15 | ## Vue3 SSR 16 | Vue3 SSR suite for css-render. 17 | ```bash 18 | npm i -D css-render @css-render/vue3-ssr 19 | ``` -------------------------------------------------------------------------------- /packages/test-shared/utils.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | const expect = chai.expect 3 | 4 | export function assertEqual (pattern: string, targetPattern: string): Chai.Assertion { 5 | return expect( 6 | pattern.replace(/\s+/g, ' ').replace(/\{\s+\}/g, '{}').trim() 7 | ) 8 | .to.equal( 9 | targetPattern.replace(/\s+/g, ' ').replace(/\{\s+\}/g, '{}').trim() 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue3-ssr/__tests__/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ssr render to string should work 1`] = ` 4 | "
Child
" 9 | `; 10 | 11 | exports[`ssr useSsrAdapter should work 1`] = ` 12 | "
Child
" 17 | `; 18 | -------------------------------------------------------------------------------- /packages/css-render/src/exists.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 2 | import { MountId, SsrAdapter } from './types' 3 | import { queryElement } from './utils' 4 | 5 | export function exists (id: MountId, ssr?: SsrAdapter): boolean { 6 | if (id === undefined) return false 7 | if (ssr) { 8 | const { 9 | context: { ids } 10 | } = ssr 11 | return ids.has(id) 12 | } 13 | return queryElement(id) !== null 14 | } 15 | -------------------------------------------------------------------------------- /packages/test-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@css-render/test-shared", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json" 6 | }, 7 | "module": "esm/utils.js", 8 | "main": "lib/utils.js", 9 | "devDependencies": { 10 | "typescript": "~4.4.4", 11 | "chai": "^4.2.0", 12 | "@types/chai": "^4.2.11", 13 | "eslint": "~8.5.0", 14 | "@rushstack/eslint-config": "~2.5.1" 15 | }, 16 | "sideEffects": false, 17 | "license": "MIT" 18 | } 19 | -------------------------------------------------------------------------------- /MEMO.md: -------------------------------------------------------------------------------- 1 | ## 为全部的包更新依赖 2 | 3 | rush add -p "typescript@latest" --dev -m 4 | -m 所有包一起更新 5 | 需要目录正确 6 | 7 | ## 清除一切 8 | 9 | rush unlink && git clean -dfx 10 | 11 | ## 重安一遍 12 | 13 | rush update 14 | 15 | ## 构建 16 | 17 | rush rebuild -f from -t to -v -o only 18 | -v verbose 19 | 20 | ## 发包 21 | 先 22 | rush change 23 | rush change --bulk --message xxx --bump-type 24 | 再 25 | rush version --bump // 这玩意有的时候会生成一个 version update only,不知道原因,即使真的有 change,或许和时间有关 26 | 27 | 这个时候就可以提交了 28 | 29 | rush build 30 | rush publish --include-all --publish -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@css-render/eslint-config", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "build": "" 6 | }, 7 | "dependencies": { 8 | "@typescript-eslint/eslint-plugin": "^5.8.1", 9 | "@typescript-eslint/parser": "^5.8.1", 10 | "eslint-config-standard": "^16.0.3", 11 | "eslint-config-standard-with-typescript": "^21.0.1", 12 | "eslint-plugin-import": "^2.25.3", 13 | "eslint-plugin-node": "^11.1.0", 14 | "eslint-plugin-promise": "^6.0.0" 15 | }, 16 | "devDependencies": { 17 | "@rushstack/eslint-config": "~2.5.1" 18 | }, 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /packages/css-render/src/CssRender.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CssRenderConfig, 3 | CssRenderInstance, 4 | createCNode, 5 | CssRenderPlugin 6 | } from './types' 7 | import { 8 | c 9 | } from './c' 10 | import { 11 | queryElement 12 | } from './utils' 13 | 14 | export function CssRender (config: CssRenderConfig = {}): CssRenderInstance { 15 | const cssr: CssRenderInstance = { 16 | c: ( 17 | (...args: any[]) => c(cssr, ...args as [any, any, any]) 18 | ) as createCNode, 19 | use: (plugin: CssRenderPlugin, ...args: any[]) => plugin.install(cssr, ...args), 20 | find: queryElement, 21 | context: {}, 22 | config 23 | } 24 | return cssr 25 | } 26 | -------------------------------------------------------------------------------- /packages/eslint-config/base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | files: ['*.ts'], 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint' 8 | ], 9 | extends: [ 10 | 'standard-with-typescript' 11 | ], 12 | parserOptions: { 13 | project: './tsconfig.json' 14 | }, 15 | rules: { 16 | '@typescript-eslint/no-floating-promises': 0 17 | } 18 | }, 19 | { 20 | files: ['*.js'], 21 | extends: 'standard', 22 | parserOptions: { 23 | ecmaVersion: 2020, 24 | sourceType: 'module' 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=binary 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | -------------------------------------------------------------------------------- /playground/testHint.js: -------------------------------------------------------------------------------- 1 | const { CssRender } = require('css-render') 2 | const { plugin } = require('@css-render/plugin-bem') 3 | 4 | const cssr = CssRender() 5 | const bemPlugin = plugin({ 6 | blockPrefix: '.n-' 7 | }) 8 | const { c } = cssr 9 | cssr.use(bemPlugin) 10 | const { 11 | cB, cE, cM, cNotM 12 | } = bemPlugin 13 | 14 | const style = c('selector', { 15 | background: 'black', 16 | from: { 17 | background: 'black' 18 | } 19 | }, [ 20 | c('selector2', { 21 | background: 'black', 22 | from: { 23 | background: 'black' 24 | } 25 | }) 26 | ]) 27 | 28 | const style2 = c('selector', ({ 29 | context, 30 | props 31 | }) => { 32 | return { 33 | x: '' 34 | } 35 | }) 36 | 37 | const target = style.mount({ 38 | target: '123' 39 | }) 40 | 41 | style.render({ 42 | cool: 'good' 43 | }) 44 | -------------------------------------------------------------------------------- /packages/css-render/__tests__/parse/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import { parseSelectorPath } from '../../src/parse' 3 | import pathTestCases from './pathTestCases.spec' 4 | 5 | const expect = chai.expect 6 | 7 | const _sr = /,(?![^(]*\))/ 8 | 9 | function normalizeSelector (selector: string): string { 10 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare 11 | return selector 12 | .split(_sr) 13 | .map(part => part.trim()) 14 | .sort() 15 | .join(', ') 16 | } 17 | 18 | describe('# parse selector path', () => { 19 | pathTestCases.forEach(testCase => { 20 | it(`parse result of ${testCase.input.toString()} should be ${testCase.output}`, () => { 21 | expect(normalizeSelector(parseSelectorPath(testCase.input))).to.equal(normalizeSelector(testCase.output)) 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/packages/css-render" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: npm 9 | directory: "/packages/vue3-ssr" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | - package-ecosystem: npm 14 | directory: "/packages/plugin-bem" 15 | schedule: 16 | interval: daily 17 | open-pull-requests-limit: 10 18 | - package-ecosystem: npm 19 | directory: "/packages/docs" 20 | schedule: 21 | interval: daily 22 | open-pull-requests-limit: 10 23 | - package-ecosystem: npm 24 | directory: "/packages/eslint-config" 25 | schedule: 26 | interval: daily 27 | open-pull-requests-limit: 10 28 | - package-ecosystem: npm 29 | directory: "/packages/test-shared" 30 | schedule: 31 | interval: daily 32 | open-pull-requests-limit: 10 33 | -------------------------------------------------------------------------------- /packages/css-render/__tests__/find/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import CssRender from '../../src' 3 | 4 | const expect = chai.expect 5 | const cssr = CssRender() 6 | 7 | const { 8 | c, find 9 | } = cssr 10 | 11 | describe('# find', () => { 12 | const style = c('.red-block', { 13 | backgroundColor: 'red' 14 | }) 15 | before(() => { 16 | style.mount({ 17 | id: 'test-id-1' 18 | }) 19 | style.mount({ 20 | id: 'test-id-2' 21 | }) 22 | style.mount({ 23 | id: '14138' 24 | }) 25 | style.mount({ 26 | id: '14139' 27 | }) 28 | }) 29 | after(() => style.unmount()) 30 | it('works', () => { 31 | expect(find('14138')).not.to.equal(null) 32 | expect(find('14139')).not.to.equal(null) 33 | expect(find('test-id-1')).not.to.equal(null) 34 | expect(find('test-id-2')).not.to.equal(null) 35 | expect(find('gogogo')).to.equal(null) 36 | expect(find('kirby')).to.equal(null) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/docs/docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'css-render', 3 | themeConfig: { 4 | sidebar: [ 5 | { 6 | text: 'Introduction', 7 | link: '/' 8 | }, 9 | { 10 | text: 'Installation', 11 | link: '/installation' 12 | }, 13 | { 14 | text: 'Getting Started', 15 | link: '/get-started' 16 | }, 17 | { 18 | text: 'Render Style', 19 | link: '/cnode-and-render' 20 | }, 21 | { 22 | text: 'Mount Style', 23 | link: '/mount' 24 | }, 25 | { 26 | text: 'Plugin Development', 27 | link: '/plugin-development' 28 | }, 29 | { 30 | text: 'CssRender Instance', 31 | link: '/css-render-instance' 32 | }, 33 | { 34 | text: 'Q & A', 35 | link: '/qa' 36 | } 37 | ], 38 | nav: [ 39 | { 40 | text: 'Github', 41 | link: 'https://github.com/07akioni/css-render' 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/vue3-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@css-render/vue3-ssr", 3 | "version": "0.15.14", 4 | "scripts": { 5 | "lint": "eslint --fix *.js src/**/*.ts __tests__/**/*.ts", 6 | "test": "jest", 7 | "build": "npm run lint && rm -rf es lib && npm run test && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json" 8 | }, 9 | "devDependencies": { 10 | "@babel/preset-env": "^7.12.17", 11 | "@babel/preset-typescript": "^7.12.17", 12 | "@css-render/eslint-config": "workspace:*", 13 | "@rushstack/eslint-config": "~2.5.1", 14 | "@types/jest": "^27.0.3", 15 | "@vue/server-renderer": "^3.0.11", 16 | "babel-jest": "^27.4.5", 17 | "css-render": "workspace:~0.15.14", 18 | "eslint": "~8.5.0", 19 | "jest": "^27.4.5", 20 | "jest-standard-reporter": "~2.0.0", 21 | "typescript": "~4.4.4", 22 | "vue": "^3.2.45" 23 | }, 24 | "peerDependencies": { 25 | "vue": "^3.0.11" 26 | }, 27 | "main": "lib/index.js", 28 | "module": "esm/index.js", 29 | "sideEffects": false, 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /common/git-hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is an example Git hook for use with Rush. To enable this hook, rename this file 4 | # to "commit-msg" and then run "rush install", which will copy it from common/git-hooks 5 | # to the .git/hooks folder. 6 | # 7 | # TO LEARN MORE ABOUT GIT HOOKS 8 | # 9 | # The Git documentation is here: https://git-scm.com/docs/githooks 10 | # Some helpful resources: https://githooks.com 11 | # 12 | # ABOUT THIS EXAMPLE 13 | # 14 | # The commit-msg hook is called by "git commit" with one argument, the name of the file 15 | # that has the commit message. The hook should exit with non-zero status after issuing 16 | # an appropriate message if it wants to stop the commit. The hook is allowed to edit 17 | # the commit message file. 18 | 19 | # This example enforces that commit message should contain a minimum amount of 20 | # description text. 21 | if [ `cat $1 | wc -w` -lt 3 ]; then 22 | echo "" 23 | echo "Invalid commit message: The message must contain at least 3 words." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /packages/css-render/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function removeElement (el: HTMLStyleElement | null): void { 2 | /* istanbul ignore if */ 3 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 4 | if (!el) return 5 | const parentElement = el.parentElement 6 | /* istanbul ignore else */ 7 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 8 | if (parentElement) parentElement.removeChild(el) 9 | } 10 | 11 | export function queryElement (id: string, parent?: ParentNode): HTMLStyleElement | null { 12 | return (parent ?? document.head).querySelector(`style[cssr-id="${id}"]`) 13 | } 14 | 15 | export function createElement (id: string): HTMLStyleElement { 16 | const el = document.createElement('style') 17 | el.setAttribute('cssr-id', id) 18 | return el 19 | } 20 | 21 | export function isMediaOrSupports (selector: string | undefined | null): selector is string { 22 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 23 | if (!selector) return false 24 | return /^\s*@(s|m)/.test(selector) 25 | } 26 | -------------------------------------------------------------------------------- /playground/perf-log.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | 3 | const loopTimes = 100 4 | const trimmedLength = 20 5 | 6 | let log = '-----log-start-----\n' 7 | 8 | const gitHash = execSync('git rev-parse HEAD').toString().trim() 9 | log += ('Commit hash : ' + gitHash + '\n') 10 | log += ('Raw logs : ') 11 | 12 | const timeList = [] 13 | 14 | for (let i = 0; i < loopTimes; ++i) { 15 | const output = execSync('node playground/button.perf.js') 16 | const time = Number(Number(output.toString()).toFixed(2)) 17 | timeList.push(time) 18 | log += (time + ',') 19 | } 20 | 21 | const trimmedTimeList = timeList.sort().slice( 22 | trimmedLength / 2 - 1, 23 | loopTimes - trimmedLength / 2 24 | ) 25 | 26 | const sum = trimmedTimeList.reduce((prevValue, currentValue) => prevValue + currentValue, 0) 27 | 28 | log += '\n' 29 | log += ('Max time : ' + Math.max(...timeList) + '\n') 30 | log += ('Min time : ' + Math.min(...timeList) + '\n') 31 | log += ('Average time : ' + (sum / (loopTimes - trimmedLength)).toFixed(4) + '\n') 32 | log += '------log-end------\n' 33 | 34 | console.log(log) 35 | -------------------------------------------------------------------------------- /packages/vue3-ssr/README.md: -------------------------------------------------------------------------------- 1 | # @css-render/vue3-ssr 2 | 3 | ## Example 4 | 5 | ### Server 6 | 7 | ```js 8 | import { createSSRApp } from 'vue' 9 | import { renderToString } from '@vue/server-renderer' 10 | import { setup } = from '@css-render/vue3-ssr' 11 | 12 | // For each request, you need to create a new app 13 | const ssrApp = createSSRApp(App) 14 | const { collect } = setup(ssrApp) 15 | 16 | renderToString(ssrApp).then(appHtml => { 17 | const css = collect() 18 | const page = ` 19 | 20 | ${css} 21 |
${appHtml}
22 | ` 23 | }) 24 | ``` 25 | 26 | ### Component 27 | 28 | ```js 29 | import { defineComponent } from 'vue' 30 | import { useSsrAdapter } from '@css-render/vue3-ssr' 31 | 32 | const Child = defineComponent({ 33 | setup() { 34 | c("div", { 35 | color: "red", 36 | }).mount({ 37 | id: "mount-id", 38 | // It always returns undefined in browser (document === undefined) 39 | ssr: useSsrAdapter(), 40 | }); 41 | }, 42 | render() { 43 | return h("div", null, "Child"); 44 | }, 45 | }); 46 | ``` 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 07akioni 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 | -------------------------------------------------------------------------------- /common/config/rush/.npmrc: -------------------------------------------------------------------------------- 1 | # Rush uses this file to configure the NPM package registry during installation. It is applicable 2 | # to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", 3 | # "rush update", and the "install-run.js" scripts. 4 | # 5 | # NOTE: The "rush publish" command uses .npmrc-publish instead. 6 | # 7 | # Before invoking the package manager, Rush will copy this file to the folder where installation 8 | # is performed. The copied file will omit any config lines that reference environment variables 9 | # that are undefined in that session; this avoids problems that would otherwise result due to 10 | # a missing variable being replaced by an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | registry=https://registry.npmjs.org/ 22 | always-auth=false 23 | -------------------------------------------------------------------------------- /packages/docs/docs/css-render-instance.md: -------------------------------------------------------------------------------- 1 | # `CssRender` Instance 2 | You may wonder why css-render doesn't provide a `c` method from its entry point like `React.createElement` or `Vue.h`. The reason is the `c` method shared a context can be modified by plugins. I can't make `c` a singleton since it may be influenced accidentally by other packages. 3 | 4 | `CssRender` instance can be created by the following codes: 5 | ```js 6 | import CssRender from 'css-render' 7 | 8 | const config = { 9 | // ... 10 | } 11 | const cssr = CssRender(config) // cssr is a CssRender instance 12 | ``` 13 | ## Config 14 | ```ts 15 | interface Config { 16 | // whether to render the CSS of a CNode with empty properties (eg. {}) 17 | // default is false 18 | keepEmptyBlock: boolean 19 | } 20 | ``` 21 | ## Properties on the Instance 22 | the instance(`cssr`) has some public properties: 23 | - `c(...)`: the method to create a `CNode`, see [Create a CNode & Render a CNode Tree](cnode-and-render.md). 24 | - `context`: the context of the instance, default is `{}`. It may be used by a plugin or in the rendering phase of a `CNode` tree. 25 | - `use(plugin)`: bind the instance with a plugin. 26 | 27 | ### Example 28 | ```ts 29 | import CssRender from 'css-render' 30 | 31 | const { 32 | c, context, use 33 | } = CssRender() 34 | ``` 35 | -------------------------------------------------------------------------------- /packages/docs/docs/qa.md: -------------------------------------------------------------------------------- 1 | # Q & A 2 | #### This sounds a bit like styled-components to me. How does this package differ? 3 | 4 | 1. css-render is mainly built for **library builders**. It helps the maintainer ship a library wihout css at a small cost. (gzip < 2kb) 5 | 6 | 2. css-render doesn't do the bindings between components and styles. It is more like a style generator with low level mount and unmount API. So it's not recommend to build style for web-apps. 7 | 8 | 3. As result of `2`, it just generate CSS and mount it to html so doesn't bind with any specific framework. 9 | 10 | 4. css-render is easier to write like a sass mixin or less mixin. 11 | ```ts 12 | // For example 13 | cB('block', [ 14 | cE('element', [ 15 | cM('modifier', {}) 16 | ] 17 | ]) 18 | // generates css: 19 | // block__element--modifier {} 20 | ``` 21 | If you build library with sass or less, you may have a try with css-render. 22 | 23 | 5. Style (CNode) can be reused at any granularity. 24 | 25 | 6. Style can be generated in node side simplelly. 26 | 27 | However, I want to stress that it can be just a supplementary method. If you find everything you are using, for example pure CSS, preprocessors, or other libraries work fine, just keep going, they are doing great jobs. 28 | 29 | When you got some bottleneck and find the mentioned features are useful for you, why not have a try? -------------------------------------------------------------------------------- /common/config/rush/.npmrc-publish: -------------------------------------------------------------------------------- 1 | # This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish 2 | # is used by the "rush publish" command, as publishing often involves different credentials 3 | # and registries than other operations. 4 | # 5 | # Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" 6 | # and then temporarily map that folder as the "home directory" for the current user account. 7 | # This enables the same settings to apply for each project folder that gets published. The copied file 8 | # will omit any config lines that reference environment variables that are undefined in that session; 9 | # this avoids problems that would otherwise result due to a missing variable being replaced by 10 | # an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 22 | -------------------------------------------------------------------------------- /common/scripts/install-run-rushx.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 2 | // 3 | // This script is intended for usage in an automated build environment where the Rush command may not have 4 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 5 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to the 6 | // rushx command. 7 | // 8 | // An example usage would be: 9 | // 10 | // node common/scripts/install-run-rushx.js custom-command 11 | // 12 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 13 | 14 | /******/ (() => { // webpackBootstrap 15 | /******/ "use strict"; 16 | var __webpack_exports__ = {}; 17 | /*!*************************************************!*\ 18 | !*** ./lib-esnext/scripts/install-run-rushx.js ***! 19 | \*************************************************/ 20 | 21 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 22 | // See the @microsoft/rush package's LICENSE file for license information. 23 | require('./install-run-rush'); 24 | //# sourceMappingURL=install-run-rushx.js.map 25 | module.exports = __webpack_exports__; 26 | /******/ })() 27 | ; 28 | //# sourceMappingURL=install-run-rushx.js.map -------------------------------------------------------------------------------- /packages/css-render/karma.conf.js: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require('puppeteer').executablePath() 2 | const pluginKarmaMocha = require('karma-mocha') 3 | const pluginKarmaTypescript = require('karma-typescript') 4 | const pluginKarmaChromeLauncher = require('karma-chrome-launcher') 5 | const pluginKarmaSpecReporter = require('karma-spec-reporter') 6 | 7 | module.exports = function (config) { 8 | config.set({ 9 | singleRun: true, 10 | browsers: ['ChromeHeadless'], 11 | plugins: [ 12 | pluginKarmaMocha, 13 | pluginKarmaTypescript, 14 | pluginKarmaChromeLauncher, 15 | pluginKarmaSpecReporter 16 | ], 17 | frameworks: ['mocha', 'karma-typescript'], 18 | files: [ 19 | 'src/**/*.ts', 20 | '__tests__/**/*.ts' 21 | ], 22 | preprocessors: { 23 | 'src/**/*.ts': 'karma-typescript', 24 | '__tests__/**/*.ts': 'karma-typescript' 25 | }, 26 | reporters: ['spec', 'karma-typescript'], 27 | karmaTypescriptConfig: { 28 | compilerOptions: { 29 | module: 'CommonJS', 30 | inlineSourceMap: true 31 | }, 32 | reports: { 33 | text: '', 34 | html: { 35 | directory: 'coverage', 36 | subdirectory: 'html' 37 | }, 38 | lcovonly: { 39 | directory: 'coverage', 40 | subdirectory: 'lcov' 41 | } 42 | }, 43 | tsconfig: './tsconfig.json' 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # Bower dependency directory (https://bower.io/) 25 | bower_components 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (https://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules/ 35 | jspm_packages/ 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional eslint cache 41 | .eslintcache 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | 46 | # Output of 'npm pack' 47 | *.tgz 48 | 49 | # Yarn Integrity file 50 | .yarn-integrity 51 | 52 | # dotenv environment variables file 53 | .env 54 | 55 | # next.js build output 56 | .next 57 | 58 | # OS X temporary files 59 | .DS_Store 60 | 61 | # Rush temporary files 62 | common/deploy/ 63 | common/temp/ 64 | common/autoinstallers/*/.npmrc 65 | **/.rush/temp/ 66 | 67 | package-lock.json 68 | *.swp 69 | yarn.lock 70 | .yarn 71 | dist 72 | **/*/dist 73 | **/*/esm 74 | **/*/lib 75 | playground/*.css 76 | .vscode 77 | .DS_Store 78 | .pnp.js 79 | *.tsbuildinfo 80 | -------------------------------------------------------------------------------- /packages/css-render/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { terser } = require('rollup-plugin-terser') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | input: { 6 | input: path.resolve(__dirname, 'esm/index.js') 7 | }, 8 | output: [ 9 | { 10 | file: path.resolve(__dirname, 'dist/css-render.iife.js'), 11 | format: 'iife', 12 | name: 'CssRender' 13 | }, 14 | { 15 | file: path.resolve(__dirname, 'dist/css-render.cjs.js'), 16 | format: 'cjs', 17 | exports: 'named' 18 | }, 19 | { 20 | file: path.resolve(__dirname, 'dist/css-render.esm.js'), 21 | format: 'es', 22 | exports: 'named' 23 | }, 24 | { 25 | file: path.resolve(__dirname, 'dist/css-render.esm.min.js'), 26 | format: 'es', 27 | exports: 'named', 28 | plugins: [ 29 | terser({ 30 | compress: true, 31 | mangle: true 32 | }) 33 | ] 34 | }, 35 | { 36 | file: path.resolve(__dirname, 'dist/css-render.cjs.min.js'), 37 | format: 'cjs', 38 | exports: 'named', 39 | plugins: [ 40 | terser({ 41 | compress: true, 42 | mangle: true 43 | }) 44 | ] 45 | }, 46 | { 47 | file: path.resolve(__dirname, 'dist/css-render.iife.min.js'), 48 | format: 'iife', 49 | name: 'CssRender', 50 | plugins: [ 51 | terser({ 52 | compress: true, 53 | mangle: true 54 | }) 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 1. 允许创建多个 CSS Render 的实例,通过 create 方法或者构造函数,现在所有的渲染会共用一个上下文,这显然是有一些问题的✅ 2 | 2. 当多个 CSS Render 存在的时候,Mount 节点需要能被手动控制,否则有的时候可能会产生冲突,主要取决于挂载的时候看不看命名空间让用户去管理 id 吧,我不管了✅ 3 | 3. declarationMap, tsbuildinfo 都是什么玩意?Project Reference 又是啥? 4 | 4. 可能把 render 方法放在 CNode 上比较好✅ 5 | 5. 插件 API 是不是友好?把 selector 换成 $ 可以带来 1% 左右的打包尺寸提升,emmmm,十分困扰,是要维持还说换呢换,API 可以理解,短一点是一点✅ 6 | 6. 对属性的类型得写的严谨一点 通过 `csstype` 包已经解决了这个问题,可是关键是为啥本地 typescript 就不能帮我 resolve 正确的类型呢?我没法在这个文件夹内部测试啊!!!搞的只能 xjb 涨 npm 版本号(这必然也是个错误的行为) 我通过软连接解决这个问题了,似乎是一个说的过去的解决方案... 但是这个软连接会把测试覆盖率搞坏,总是得清掉= =✅ 7 | 7. `karma-typescript` 似乎有个 bug,没法正确的 resolve 一个只包含 index.d.ts 的 npm 包!麻烦 8 | 8. mount 函数的测试✅ 9 | 9. Minify 的 build,使用 rollup 和 terser 10 | 10. 在 render 时候类型的传递我似乎应该用泛型,现在全都是 any,not good虽然对外没啥区别✅ 11 | 11. 感觉我应该允许 mount 和 unmount `null`, `undefined`,因为初始化的时候真的很难避免这两个值 我琢磨了一下,`undefined` 和现有的 API 冲突,里面还要传 props 呢,所以就只让 `null` 什么都不干好了✅ 12 | 12. 然后我还感觉发布的包类型提醒不老对的,是不是重启 vscode 就能好了? 13 | 13. 处理 `:is(a, b, c)` 这种 selector✅ 14 | 14. mount 的返回值类型不能改改吗,某些情况下不可能返回 null 的通过泛型解决✅ 15 | 15. 为 unmount 增加 delay 选项,因为框架的生命周期不能精确的体现 dom 何时被真正的卸载✅ 16 | 16. 使用 webworker 帮助渲染 // 真的能管用么? 17 | 17. 使用 serviceworker 缓存渲染结果这个没有 HTTPS 做不了,要做也应该是拓展 18 | 18. 为函数类型的 selector 和 children 增加文档 19 | 19. 为静态的选择器进行提前解析,稍微提升一丢丢性能 这个目前想来其实没啥用,因为实际使用中经常用动态样式 20 | 20. 经过我的仔细观察,测试覆盖率的问题应该是 source map 出了问题... See https://github.com/istanbuljs/nyc/issues/618#issuecomment-396818724 -------------------------------------------------------------------------------- /packages/plugin-bem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@css-render/plugin-bem", 3 | "main": "lib/index.js", 4 | "module": "esm/index.js", 5 | "license": "MIT", 6 | "description": "A plugin of css-render that helping generate BEM standard CSS", 7 | "keywords": [ 8 | "css-render", 9 | "css", 10 | "style", 11 | "css in js", 12 | "css-in-js", 13 | "bem" 14 | ], 15 | "files": [ 16 | "lib", 17 | "esm" 18 | ], 19 | "author": "07akioni", 20 | "homepage": "https://github.com/07akioni/css-render", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/07akioni/css-render" 24 | }, 25 | "scripts": { 26 | "lint": "eslint --fix index.ts __tests__/**/*.ts", 27 | "test": "jest", 28 | "build": "npm run lint && npm run test && rm -rf es lib && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json" 29 | }, 30 | "version": "0.15.14", 31 | "sideEffects": false, 32 | "peerDependencies": { 33 | "css-render": "~0.15.14" 34 | }, 35 | "devDependencies": { 36 | "@babel/preset-env": "^7.12.17", 37 | "@babel/preset-typescript": "^7.12.17", 38 | "@css-render/eslint-config": "workspace:*", 39 | "@css-render/test-shared": "workspace:*", 40 | "@rushstack/eslint-config": "~2.5.1", 41 | "@types/jest": "^27.0.3", 42 | "@types/node": "~17.0.5", 43 | "babel-jest": "^27.4.5", 44 | "css-render": "workspace:~0.15.14", 45 | "eslint": "~8.5.0", 46 | "jest": "^27.4.5", 47 | "jest-standard-reporter": "~2.0.0", 48 | "typescript": "~4.4.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rush.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", 3 | "rushVersion": "5.88.0", 4 | "pnpmVersion": "7.19.0", 5 | "pnpmOptions": { 6 | "useWorkspaces": true 7 | }, 8 | "nodeSupportedVersionRange": ">=14.15.0 <17.3.0 || >= 18.0.0 < 19.0.0", 9 | "ensureConsistentVersions": true, 10 | "projectFolderMinDepth": 2, 11 | "projectFolderMaxDepth": 2, 12 | "gitPolicy": {}, 13 | "repository": { 14 | "url": "git@github.com:07akioni/css-render.git", 15 | "defaultBranch": "master" 16 | }, 17 | "projects": [ 18 | { 19 | "packageName": "css-render", 20 | "projectFolder": "packages/css-render", 21 | "shouldPublish": true, 22 | "versionPolicyName": "css-render" 23 | }, 24 | { 25 | "packageName": "@css-render/vue3-ssr", 26 | "projectFolder": "packages/vue3-ssr", 27 | "shouldPublish": true, 28 | "versionPolicyName": "css-render" 29 | }, 30 | { 31 | "packageName": "@css-render/plugin-bem", 32 | "projectFolder": "packages/plugin-bem", 33 | "shouldPublish": true, 34 | "versionPolicyName": "css-render" 35 | }, 36 | { 37 | "packageName": "@css-render/test-shared", 38 | "projectFolder": "packages/test-shared", 39 | "shouldPublish": false 40 | }, 41 | { 42 | "packageName": "@css-render/eslint-config", 43 | "projectFolder": "packages/eslint-config", 44 | "shouldPublish": false 45 | }, 46 | { 47 | "packageName": "@css-render/docs", 48 | "projectFolder": "packages/docs", 49 | "shouldPublish": false 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: node common/scripts/install-run-rush.js update 30 | - run: node common/scripts/install-run-rush.js install 31 | - run: node common/scripts/install-run-rush.js build --verbose 32 | - run: export CODECOV_TOKEN="1b50e2b4-d843-48eb-bec7-6665d283b783" 33 | - run: bash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/7877012ffd85dfb7968f91b5992edd627483dfe0/codecov) -f packages/vue3-ssr/coverage/lcov.info -F vue3-ssr -i vue3-ssr 34 | - run: bash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/7877012ffd85dfb7968f91b5992edd627483dfe0/codecov) -f packages/plugin-bem/coverage/lcov.info -F plugin-bem -i packages/plugin-bem 35 | - run: bash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/7877012ffd85dfb7968f91b5992edd627483dfe0/codecov) -f packages/css-render/coverage/lcov/lcov.info -F css-render -i packages/css-render 36 | -------------------------------------------------------------------------------- /packages/vue3-ssr/src/index.ts: -------------------------------------------------------------------------------- 1 | import { inject, InjectionKey, App } from 'vue' 2 | 3 | interface CssrSsrContext { 4 | styles: string[] 5 | ids: Set 6 | } 7 | 8 | const ssrContextKey = 9 | '@css-render/vue3-ssr' as unknown as InjectionKey 10 | 11 | function createStyleString (id: string, style: string): string { 12 | return `` 13 | } 14 | 15 | function ssrAdapter ( 16 | id: string, 17 | style: string, 18 | ssrContext: CssrSsrContext 19 | ): void { 20 | const { styles, ids } = ssrContext 21 | // we need to impl other options to make it behaves the same as the client side 22 | if (ids.has(id)) return 23 | if (styles !== null) { 24 | ids.add(id) 25 | styles.push(createStyleString(id, style)) 26 | } 27 | } 28 | 29 | const isBrowser = typeof document !== 'undefined' 30 | 31 | export function useSsrAdapter (): 32 | | { 33 | adapter: (id: string, style: string) => void 34 | context: CssrSsrContext 35 | } 36 | | undefined { 37 | if (isBrowser) return undefined 38 | const context = inject(ssrContextKey, null) 39 | if (context === null) return undefined 40 | return { 41 | adapter: (id, style) => ssrAdapter(id, style, context), 42 | context 43 | } 44 | } 45 | 46 | interface SsrHandle { 47 | collect: () => string 48 | } 49 | 50 | export function setup (app: App): SsrHandle { 51 | const styles: string[] = [] 52 | const ssrContext: CssrSsrContext = { 53 | styles, 54 | ids: new Set() 55 | } 56 | app.provide(ssrContextKey, ssrContext) 57 | return { 58 | collect () { 59 | const res = styles.join('\n') 60 | styles.length = 0 61 | return res 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/css-render/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-render", 3 | "main": "lib/index.js", 4 | "module": "esm/index.js", 5 | "license": "MIT", 6 | "description": "Generating CSS using JS with considerable flexibility and extensibility, at both server side and client side.", 7 | "keywords": [ 8 | "css-render", 9 | "css", 10 | "style", 11 | "css in js", 12 | "css-in-js" 13 | ], 14 | "files": [ 15 | "lib", 16 | "esm" 17 | ], 18 | "author": "07akioni", 19 | "homepage": "https://github.com/07akioni/css-render", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/07akioni/css-render" 23 | }, 24 | "version": "0.15.14", 25 | "sideEffects": false, 26 | "scripts": { 27 | "lint": "eslint --fix *.js src/*.ts", 28 | "lint:type": "tsc -p tsconfig.esm.json", 29 | "build": "npm run lint && npm run test && rm -rf lib esm && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json", 30 | "test": "rm -rf ./coverage && karma start karma.conf.js", 31 | "test:cov": "npm run test && http-server ./coverage/html" 32 | }, 33 | "dependencies": { 34 | "@emotion/hash": "~0.8.0", 35 | "csstype": "~3.0.5" 36 | }, 37 | "devDependencies": { 38 | "@css-render/eslint-config": "workspace:*", 39 | "@css-render/test-shared": "workspace:*", 40 | "@rushstack/eslint-config": "~2.5.1", 41 | "@types/chai": "^4.2.11", 42 | "@types/mocha": "^9.0.0", 43 | "@types/sinon": "^10.0.6", 44 | "chai": "^4.2.0", 45 | "eslint": "~8.5.0", 46 | "karma": "~6.3.2", 47 | "karma-chrome-launcher": "^3.1.0", 48 | "karma-mocha": "^2.0.1", 49 | "karma-spec-reporter": "0.0.33", 50 | "karma-typescript": "~5.5.1", 51 | "mocha": "^9.1.3", 52 | "puppeteer": "^13.0.1", 53 | "rollup": "^2.19.0", 54 | "rollup-plugin-terser": "^7.0.2", 55 | "sinon": "^12.0.1", 56 | "terser": "^5.3.3", 57 | "typescript": "~4.4.4", 58 | "vite": "~2.7.9" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/css-render/__tests__/render/sassSpec.spec.ts: -------------------------------------------------------------------------------- 1 | import CssRender from '../../src' 2 | import { assertEqual } from '@css-render/test-shared' 3 | 4 | const { 5 | c 6 | } = CssRender() 7 | 8 | describe('#render (some compatible cases from sass-spec)', () => { 9 | it('should render as expected(1)', () => { 10 | assertEqual( 11 | c('div', [ 12 | c('span, p, span', { 13 | color: 'red' 14 | }), 15 | c('a.foo.bar.foo', { 16 | color: 'green' 17 | }), 18 | c('&:nth(-3)', { 19 | color: 'blue' 20 | }) 21 | ]).render(), 22 | ` 23 | div span, div p, div span { 24 | color: red; 25 | } 26 | div a.foo.bar.foo { 27 | color: green; 28 | } 29 | div:nth(-3) { 30 | color: blue; 31 | } 32 | ` 33 | ) 34 | }) 35 | it('should render as expected(2)', () => { 36 | /** 37 | * this case is modified, in the original case keyframe block is nested, 38 | * which doesn't follow CSS spec. 39 | */ 40 | assertEqual( 41 | c('@-webkit-keyframes', { 42 | from: { 43 | left: '0px', 44 | whatever: 'hoo' 45 | }, 46 | to: { 47 | left: '200px' 48 | } 49 | }).render(), 50 | ` 51 | @-webkit-keyframes { 52 | from { 53 | left: 0px; 54 | whatever: hoo; 55 | } 56 | to { 57 | left: 200px; 58 | } 59 | } 60 | ` 61 | ) 62 | }) 63 | it('should render as expected(3)', () => { 64 | assertEqual( 65 | c('a, b', { 66 | color: 'red' 67 | }, [ 68 | c('c, d', { 69 | height: '10px' 70 | }, [ 71 | c('e, f', { 72 | width: '12px' 73 | }) 74 | ]) 75 | ]).render(), 76 | ` 77 | a, b { 78 | color: red; 79 | } 80 | a c, b c, a d, b d { 81 | height: 10px; 82 | } 83 | a c e, b c e, a d e, b d e, a c f, b c f, a d f, b d f { 84 | width: 12px; 85 | } 86 | ` 87 | ) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/docs/docs/mount.md: -------------------------------------------------------------------------------- 1 | # Advanced Mount & Unmount Options 2 | Every [CNode](https://github.com/07akioni/css-render/blob/master/docs/overview.md) has `mount` & `unmount` methods. 3 | 4 | They can help you mount style to the HTML document. 5 | 6 | > Since `CNode` support lazy selector, properties and children evaluation, and some call of the `mount` will render the `CNode` again, the mounted style can be different in different mount. 7 | 8 | ## Mount 9 | Render the `CNode`'s style and mount it to document. 10 | 11 | ```typescript 12 | type mount = ( 13 | options?: { 14 | id?: string, 15 | props?: any, 16 | ssr?: SsrAdapter 17 | anchorMetaName?: string 18 | parent?: ParentNode 19 | } 20 | ) => HTMLStyleElement 21 | ``` 22 | 23 | ### `id` 24 | - If `id` or `options` is `undefined`, every call of mount method will create a new `style` element with rendered style and mount it to `parent`. For example: `style.mount()` or `style.mount({ props: {} })`. 25 | - If `id` is a `string`. It will mount the style on a `style[cssr-id="${id}"]` element to `parent`. For example: ``. If the element already exists, the `mount` method will **not** refresh the content of the element. 26 | ### `props` 27 | The `props` will be used as the render function's `props` during this mount. 28 | ### `ssr` 29 | When mount the style in SSR environment, you should put correct ssr adapter in it. 30 | 31 | ### `anchorMetaName` 32 | 33 | The name of a meta tag as a mount position anchor. 34 | 35 | ### `parent` 36 | The parent element that mounts the style. 37 | Default `document.head`. 38 | This is useful for shadow DOM. 39 | 40 | ### Return Value 41 | In non-ssr environment, the id element for the style to be mounted on. 42 | 43 | 44 | ## Unmount 45 | Unmount the style of the CNode. 46 | 47 | ```typescript 48 | type unmount = ( 49 | options?: { 50 | id?: string 51 | parent?: ParentNode 52 | } 53 | ) => void 54 | ``` 55 | 56 | ### `id` 57 | - If `id` or `options` is `undefined`, every mounted elements of the `CNode` will be unmounted. 58 | - If `id` is a `string`. It will unmount `style[cssr-id="${id}"]` element mounted by the `CNode`. 59 | 60 | ### `parent` 61 | 62 | The parent of mounted style. 63 | Default `document.head`. 64 | -------------------------------------------------------------------------------- /packages/docs/docs/get-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | `css-render` has a concept of `CNode`. 3 | 4 | Basically, a `CNode` is an object with `selector`, `properties` and `children`(child `CNodes`) properties. 5 | 6 | `CNode` has methods to render it to CSS literals, mount to `document` or unmount it from `document`. 7 | 8 | ## Create a `CNode` 9 | ```js 10 | import { CssRender } from 'css-render' 11 | /** 12 | * CommonJS: 13 | * const { CssRender } = require('css-render') 14 | */ 15 | 16 | const { 17 | c 18 | } = CssRender() // create a css-render instance 19 | 20 | // style is a CNode 21 | const style = c('body', ({ props }) => ({ 22 | margin: 0, 23 | backgroundColor: props.backgroundColor 24 | }), [ 25 | c('&.dark', { 26 | backgroundColor: 'black' 27 | }), 28 | c('.container', { 29 | width: '100%' 30 | }) 31 | ]) 32 | ``` 33 | Now you have a `CNode` contains your `style`. 34 | ## Render the `CNode` to String 35 | ```js 36 | console.log(style.render({ backgroundColor: 'white' })) 37 | ``` 38 | Now you have 39 | ```css 40 | body { 41 | margin: 0; 42 | background-color: white; 43 | } 44 | 45 | body.dark { 46 | background-color: black; 47 | } 48 | 49 | body .container { 50 | width: 100%; 51 | } 52 | ``` 53 | ## Mount `CNode` to HTML document 54 | Use `style.mount()` to mount the style. 55 | ```js 56 | style.mount() 57 | 58 | // Style can be mount with id. 59 | // If you provide an id, the style won't be regenerated again and again. 60 | style.mount({ id: 'my-style' }) 61 | ``` 62 | It will create a `HTMLStyleElement` with rendered style and mount it to `document.head`. 63 | ## Unmount style of the `CNode` 64 | Use `style.unmount()` to unmount the style. 65 | ```js 66 | // unmount all the mounted style from the CNode 67 | style.unmount() 68 | 69 | // Style can be unmount with id. 70 | // Only the mounted style with same id will be affected. 71 | style.unmount({ id: 'my-style' }) 72 | ``` 73 | It will remove all the mounted style elemented of the `style` object. 74 | 75 | --- 76 | 77 | - If you want to mount & unmount percisely, see [Advanced Mount & Unmount Options](mount.md). 78 | 79 | - If you want to create `CNode` and render `CNode` tree with more options, see [Create a CNode & Render a CNode Tree](cnode-and-render.md). 80 | 81 | - If you want to know what does the `CssRender` function returns, see [CssRender Instance](css-render-instance.md). 82 | 83 | - If you are interested in developing a plugin, see [Plugin Development](plugin-development.md). 84 | -------------------------------------------------------------------------------- /packages/docs/docs/plugin-development.md: -------------------------------------------------------------------------------- 1 | # Plugin Development 2 | A `css-render` plugin should be a function that return a plugin object. 3 | 4 | The plugin need to follow the following rules: 5 | 1. If you want to access the properties of a [CssRender](css-render-instance.md) instance, the object should has a `install` method. 6 | 2. If you want your plugin has options, you should accept the options from the plugin function's params. 7 | ```js 8 | import CssRender from 'css-render' 9 | 10 | const cssr = CssRender() 11 | 12 | const plugin = function (options) { 13 | return { 14 | install (CssRenderInstance) { 15 | // do something on CssRenderInstance.context as initialization 16 | // or get the c method of the CssRenderInstance 17 | } 18 | } 19 | } 20 | 21 | cssr.use(plugin) // it will make install methods called 22 | ``` 23 | 24 | There is no limitation on what other properties should the plugin object have. But it is recommended that provide the functionality by create some high-order functions of `c` method. (My convention is that the high-order function need to starts with 'c'.) 25 | 26 | Here is an example accumulator plugin (which is literally nonsense). 27 | ```js 28 | import CssRender from 'css-render' 29 | 30 | const cssr = CssRender() 31 | 32 | const Plugin = function ({ 33 | startsFrom 34 | } = { 35 | startsFrom: 1 36 | }) { 37 | let context 38 | let c 39 | return { 40 | install (CssRenderInstance) { 41 | c = CssRenderInstance.c 42 | context = CssRenderInstance.context 43 | context.number = startsFrom 44 | }, 45 | cAcc () { 46 | return c({ 47 | $: '.number', 48 | before: (context) => { 49 | context.number += 1 50 | } 51 | }, ({ context }) => ({ 52 | value: context.number 53 | })) 54 | } 55 | } 56 | } 57 | 58 | const plugin = Plugin({ startsFrom: 0 }) 59 | cssr.use(plugin) // it will make install methods called 60 | 61 | const { cAcc } = plugin 62 | 63 | console.log(cAcc().render()) 64 | console.log() 65 | console.log(cAcc().render()) 66 | console.log() 67 | console.log(cAcc().render()) 68 | ``` 69 | It outputs: 70 | 71 | ```css 72 | .number { 73 | value: 1; 74 | } 75 | 76 | .number { 77 | value: 2; 78 | } 79 | 80 | .number { 81 | value: 3; 82 | } 83 | ``` 84 | 85 | If you still have no idea on how to create a plugin, you may see [@css-render/plugin-bem](https://github.com/07akioni/css-render/tree/master/packages/plugin-bem) for some inspirations. 86 | -------------------------------------------------------------------------------- /packages/css-render/src/c.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CNode, 3 | CProperties, 4 | CContext, 5 | CssRenderInstance, 6 | createCNodeForCssRenderInstance, 7 | baseCreateCNodeForCssRenderInstance, 8 | CRenderProps, 9 | CSelector, 10 | CNodeChildren, 11 | UnmountOption, 12 | MountOption, 13 | SsrAdapter 14 | } from './types' 15 | import { render } from './render' 16 | import { mount, unmount } from './mount' 17 | 18 | function wrappedRender (this: CNode, props?: T): string { 19 | return render(this, this.instance, props) 20 | } 21 | 22 | // do not guard node calling, it should throw an error. 23 | function wrappedMount ( 24 | this: CNode, 25 | options: MountOption = {} 26 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 27 | ): T extends undefined ? HTMLStyleElement : void { 28 | const { id, ssr, props, head = false, force = false, anchorMetaName, parent } = options 29 | const targetElement = mount( 30 | this.instance, 31 | this, 32 | id, 33 | props, 34 | head, 35 | force, 36 | anchorMetaName, 37 | parent, 38 | ssr 39 | ) 40 | return targetElement as any 41 | } 42 | 43 | function wrappedUnmount ( 44 | this: CNode, 45 | options: UnmountOption = {} 46 | ): void { 47 | /* istanbul ignore next */ 48 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 49 | const { 50 | id, 51 | parent 52 | } = options 53 | unmount(this.instance, this, id, parent) 54 | } 55 | 56 | const createCNode: baseCreateCNodeForCssRenderInstance = function ( 57 | instance: CssRenderInstance, 58 | $: CSelector, 59 | props: CProperties, 60 | children: CNodeChildren 61 | ): CNode { 62 | return { 63 | instance, 64 | $, 65 | props, 66 | children, 67 | els: [], 68 | render: wrappedRender, 69 | mount: wrappedMount, 70 | unmount: wrappedUnmount 71 | } 72 | } 73 | 74 | export const c: createCNodeForCssRenderInstance = function ( 75 | instance: CssRenderInstance, 76 | $: any, 77 | props: any, 78 | children: any 79 | ): CNode { 80 | if (Array.isArray($)) { 81 | return createCNode(instance, { $: null }, null, $) 82 | } else if (Array.isArray(props)) { 83 | return createCNode(instance, $, null, props) 84 | } else if (Array.isArray(children)) { 85 | return createCNode(instance, $, props, children) 86 | } else { 87 | return createCNode(instance, $, props, null) 88 | } 89 | } as createCNodeForCssRenderInstance 90 | 91 | export type { 92 | CNode, 93 | CProperties, 94 | CContext 95 | } 96 | -------------------------------------------------------------------------------- /packages/plugin-bem/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - @css-render/plugin-bem 2 | 3 | This log was last generated on Sun, 05 May 2024 06:34:46 GMT and should not be manually modified. 4 | 5 | ## 0.15.14 6 | Sun, 05 May 2024 06:34:46 GMT 7 | 8 | _Version update only_ 9 | 10 | ## 0.15.13 11 | Sun, 05 May 2024 05:43:33 GMT 12 | 13 | _Version update only_ 14 | 15 | ## 0.15.12 16 | Sat, 24 Dec 2022 17:17:08 GMT 17 | 18 | _Version update only_ 19 | 20 | ## 0.15.11 21 | Sun, 21 Aug 2022 07:56:48 GMT 22 | 23 | _Version update only_ 24 | 25 | ## 0.15.10 26 | Wed, 23 Mar 2022 12:33:17 GMT 27 | 28 | _Version update only_ 29 | 30 | ## 0.15.9 31 | Sat, 05 Mar 2022 14:58:36 GMT 32 | 33 | _Version update only_ 34 | 35 | ## 0.15.8 36 | Tue, 28 Dec 2021 18:59:30 GMT 37 | 38 | ### Updates 39 | 40 | - fix peer deps 41 | 42 | ## 0.15.7 43 | Tue, 28 Dec 2021 17:54:23 GMT 44 | 45 | _Version update only_ 46 | 47 | ## 0.15.6 48 | Thu, 02 Sep 2021 15:52:34 GMT 49 | 50 | _Version update only_ 51 | 52 | ## 0.15.5 53 | Sat, 24 Jul 2021 17:34:06 GMT 54 | 55 | _Version update only_ 56 | 57 | ## 0.15.4 58 | Sun, 13 Jun 2021 04:56:14 GMT 59 | 60 | _Version update only_ 61 | 62 | ## 0.15.3 63 | Sat, 12 Jun 2021 09:50:10 GMT 64 | 65 | _Version update only_ 66 | 67 | ## 0.15.2 68 | Wed, 02 Jun 2021 04:15:14 GMT 69 | 70 | _Version update only_ 71 | 72 | ## 0.15.1 73 | Tue, 01 Jun 2021 13:35:47 GMT 74 | 75 | _Version update only_ 76 | 77 | ## 0.15.0 78 | Tue, 01 Jun 2021 13:10:25 GMT 79 | 80 | _Version update only_ 81 | 82 | ## 0.14.1 83 | Wed, 19 May 2021 05:06:20 GMT 84 | 85 | _Version update only_ 86 | 87 | ## 0.14.0 88 | Wed, 19 May 2021 05:03:05 GMT 89 | 90 | _Version update only_ 91 | 92 | ## 0.13.9 93 | Sun, 16 May 2021 19:46:15 GMT 94 | 95 | _Version update only_ 96 | 97 | ## 0.13.8 98 | Wed, 12 May 2021 11:45:08 GMT 99 | 100 | _Version update only_ 101 | 102 | ## 0.13.7 103 | Wed, 12 May 2021 11:35:11 GMT 104 | 105 | _Version update only_ 106 | 107 | ## 0.13.6 108 | Wed, 14 Apr 2021 17:53:56 GMT 109 | 110 | _Version update only_ 111 | 112 | ## 0.13.5 113 | Wed, 14 Apr 2021 17:06:44 GMT 114 | 115 | ### Updates 116 | 117 | - feat(plugin-bem): add bPrefix prop 118 | 119 | ## 0.13.4 120 | Wed, 14 Apr 2021 16:25:11 GMT 121 | 122 | _Version update only_ 123 | 124 | ## 0.13.3 125 | Wed, 14 Apr 2021 16:24:04 GMT 126 | 127 | _Version update only_ 128 | 129 | ## 0.13.2 130 | Fri, 26 Feb 2021 08:24:05 GMT 131 | 132 | _Version update only_ 133 | 134 | ## 0.13.1 135 | Fri, 26 Feb 2021 08:18:28 GMT 136 | 137 | ### Updates 138 | 139 | - No changes 140 | 141 | ## 0.13.0 142 | Wed, 24 Feb 2021 09:35:28 GMT 143 | 144 | ### Minor changes 145 | 146 | - No changes 147 | 148 | -------------------------------------------------------------------------------- /packages/vue3-ssr/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - @css-render/vue3-ssr 2 | 3 | This log was last generated on Sun, 05 May 2024 06:34:46 GMT and should not be manually modified. 4 | 5 | ## 0.15.14 6 | Sun, 05 May 2024 06:34:46 GMT 7 | 8 | _Version update only_ 9 | 10 | ## 0.15.13 11 | Sun, 05 May 2024 05:43:33 GMT 12 | 13 | _Version update only_ 14 | 15 | ## 0.15.12 16 | Sat, 24 Dec 2022 17:17:08 GMT 17 | 18 | _Version update only_ 19 | 20 | ## 0.15.11 21 | Sun, 21 Aug 2022 07:56:48 GMT 22 | 23 | _Version update only_ 24 | 25 | ## 0.15.10 26 | Wed, 23 Mar 2022 12:33:17 GMT 27 | 28 | ### Updates 29 | 30 | - Fix useSsrAdapter may return non-undefined value in browser. 31 | 32 | ## 0.15.9 33 | Sat, 05 Mar 2022 14:58:36 GMT 34 | 35 | _Version update only_ 36 | 37 | ## 0.15.8 38 | Tue, 28 Dec 2021 18:59:30 GMT 39 | 40 | _Version update only_ 41 | 42 | ## 0.15.7 43 | Tue, 28 Dec 2021 17:54:23 GMT 44 | 45 | _Version update only_ 46 | 47 | ## 0.15.6 48 | Thu, 02 Sep 2021 15:52:34 GMT 49 | 50 | _Version update only_ 51 | 52 | ## 0.15.5 53 | Sat, 24 Jul 2021 17:34:06 GMT 54 | 55 | ### Updates 56 | 57 | - Update readme 58 | 59 | ## 0.15.4 60 | Sun, 13 Jun 2021 04:56:14 GMT 61 | 62 | ### Updates 63 | 64 | - (breaking) Remove `ssrAdapter` exports 65 | 66 | ## 0.15.3 67 | Sat, 12 Jun 2021 09:50:10 GMT 68 | 69 | ### Updates 70 | 71 | - Add useSsrAdapter hook 72 | 73 | ## 0.15.2 74 | Wed, 02 Jun 2021 04:15:14 GMT 75 | 76 | _Version update only_ 77 | 78 | ## 0.15.1 79 | Tue, 01 Jun 2021 13:35:47 GMT 80 | 81 | ### Updates 82 | 83 | - no main & module in pkg.json 84 | 85 | ## 0.15.0 86 | Tue, 01 Jun 2021 13:10:25 GMT 87 | 88 | ### Updates 89 | 90 | - support stream ssr 91 | 92 | ## 0.14.1 93 | Wed, 19 May 2021 05:06:20 GMT 94 | 95 | _Version update only_ 96 | 97 | ## 0.14.0 98 | Wed, 19 May 2021 05:03:05 GMT 99 | 100 | _Version update only_ 101 | 102 | ## 0.13.9 103 | Sun, 16 May 2021 19:46:15 GMT 104 | 105 | _Version update only_ 106 | 107 | ## 0.13.8 108 | Wed, 12 May 2021 11:45:08 GMT 109 | 110 | _Version update only_ 111 | 112 | ## 0.13.7 113 | Wed, 12 May 2021 11:35:11 GMT 114 | 115 | _Version update only_ 116 | 117 | ## 0.13.6 118 | Wed, 14 Apr 2021 17:53:56 GMT 119 | 120 | _Version update only_ 121 | 122 | ## 0.13.5 123 | Wed, 14 Apr 2021 17:06:44 GMT 124 | 125 | ### Updates 126 | 127 | - feat(plugin-bem): add bPrefix prop 128 | 129 | ## 0.13.4 130 | Wed, 14 Apr 2021 16:25:11 GMT 131 | 132 | _Version update only_ 133 | 134 | ## 0.13.3 135 | Wed, 14 Apr 2021 16:24:04 GMT 136 | 137 | _Version update only_ 138 | 139 | ## 0.13.2 140 | Fri, 26 Feb 2021 08:24:05 GMT 141 | 142 | _Version update only_ 143 | 144 | ## 0.13.1 145 | Fri, 26 Feb 2021 08:18:28 GMT 146 | 147 | ### Updates 148 | 149 | - No changes 150 | 151 | ## 0.13.0 152 | Wed, 24 Feb 2021 09:35:28 GMT 153 | 154 | ### Minor changes 155 | 156 | - Add SsrContext component & ssrAdapter 157 | 158 | -------------------------------------------------------------------------------- /packages/css-render/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - css-render 2 | 3 | This log was last generated on Sun, 05 May 2024 06:34:46 GMT and should not be manually modified. 4 | 5 | ## 0.15.14 6 | Sun, 05 May 2024 06:34:46 GMT 7 | 8 | _Version update only_ 9 | 10 | ## 0.15.13 11 | Sun, 05 May 2024 05:43:33 GMT 12 | 13 | _Version update only_ 14 | 15 | ## 0.15.12 16 | Sat, 24 Dec 2022 17:17:08 GMT 17 | 18 | _Version update only_ 19 | 20 | ## 0.15.11 21 | Sun, 21 Aug 2022 07:56:48 GMT 22 | 23 | ### Updates 24 | 25 | - Limit scope of style id check 26 | 27 | ## 0.15.10 28 | Wed, 23 Mar 2022 12:33:17 GMT 29 | 30 | _Version update only_ 31 | 32 | ## 0.15.9 33 | Sat, 05 Mar 2022 14:58:36 GMT 34 | 35 | _Version update only_ 36 | 37 | ## 0.15.8 38 | Tue, 28 Dec 2021 18:59:30 GMT 39 | 40 | _Version update only_ 41 | 42 | ## 0.15.7 43 | Tue, 28 Dec 2021 17:54:23 GMT 44 | 45 | ### Updates 46 | 47 | - support metaAnchorName on mount 48 | 49 | ## 0.15.6 50 | Thu, 02 Sep 2021 15:52:34 GMT 51 | 52 | ### Updates 53 | 54 | - fix: mount before link element when head is true 55 | 56 | ## 0.15.5 57 | Sat, 24 Jul 2021 17:34:06 GMT 58 | 59 | _Version update only_ 60 | 61 | ## 0.15.4 62 | Sun, 13 Jun 2021 04:56:14 GMT 63 | 64 | ### Updates 65 | 66 | - Add `exists` function to check if style is mounted 67 | 68 | ## 0.15.3 69 | Sat, 12 Jun 2021 09:50:10 GMT 70 | 71 | _Version update only_ 72 | 73 | ## 0.15.2 74 | Wed, 02 Jun 2021 04:15:14 GMT 75 | 76 | ### Updates 77 | 78 | - fix throwing error in node env 79 | 80 | ## 0.15.1 81 | Tue, 01 Jun 2021 13:35:47 GMT 82 | 83 | _Version update only_ 84 | 85 | ## 0.15.0 86 | Tue, 01 Jun 2021 13:10:25 GMT 87 | 88 | _Version update only_ 89 | 90 | ## 0.14.1 91 | Wed, 19 May 2021 05:06:20 GMT 92 | 93 | _Version update only_ 94 | 95 | ## 0.14.0 96 | Wed, 19 May 2021 05:03:05 GMT 97 | 98 | ### Updates 99 | 100 | - Remove count prop in mount option & unmount option. Rename boost prop in mount option to silent 101 | 102 | ## 0.13.9 103 | Sun, 16 May 2021 19:46:15 GMT 104 | 105 | ### Updates 106 | 107 | - add option.force for mount 108 | 109 | ## 0.13.8 110 | Wed, 12 May 2021 11:45:08 GMT 111 | 112 | ### Updates 113 | 114 | - Fix render multiple times when mount with same id 115 | 116 | ## 0.13.7 117 | Wed, 12 May 2021 11:35:11 GMT 118 | 119 | _Version update only_ 120 | 121 | ## 0.13.6 122 | Wed, 14 Apr 2021 17:53:56 GMT 123 | 124 | _Version update only_ 125 | 126 | ## 0.13.5 127 | Wed, 14 Apr 2021 17:06:44 GMT 128 | 129 | ### Updates 130 | 131 | - feat(plugin-bem): add bPrefix prop 132 | 133 | ## 0.13.4 134 | Wed, 14 Apr 2021 16:25:11 GMT 135 | 136 | _Version update only_ 137 | 138 | ## 0.13.3 139 | Wed, 14 Apr 2021 16:24:04 GMT 140 | 141 | ### Updates 142 | 143 | - feat(mount): add `head` option to insert style before every style node in doc.head 144 | 145 | ## 0.13.2 146 | Fri, 26 Feb 2021 08:24:05 GMT 147 | 148 | _Version update only_ 149 | 150 | ## 0.13.1 151 | Fri, 26 Feb 2021 08:18:28 GMT 152 | 153 | ### Updates 154 | 155 | - Deprecate MountOptions.count & UnmountOptions.count 156 | 157 | ## 0.13.0 158 | Wed, 24 Feb 2021 09:35:28 GMT 159 | 160 | ### Minor changes 161 | 162 | - Support ssrAdapter in mount 163 | 164 | -------------------------------------------------------------------------------- /packages/css-render/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { CSelectorPath } from './types' 2 | 3 | function ampCount (selector: string): number { 4 | let cnt = 0 5 | for (let i = 0; i < selector.length; ++i) { 6 | if (selector[i] === '&') ++cnt 7 | } 8 | return cnt 9 | } 10 | 11 | /** 12 | * Don't just use ',' to separate css selector. For example: 13 | * x:(a, b) {} will be split into 'x:(a' and 'b)', which is not expected. 14 | * Make sure comma doesn't exist inside parentheses. 15 | */ 16 | const separatorRegex = /\s*,(?![^(]*\))\s*/g 17 | const extraSpaceRegex = /\s+/g 18 | 19 | /** 20 | * selector must includes '&' 21 | * selector is trimmed 22 | * every part of amp is trimmed 23 | */ 24 | function resolveSelectorWithAmp (amp: string[], selector: string): string[] { 25 | const nextAmp: string[] = [] 26 | selector.split(separatorRegex).forEach(partialSelector => { 27 | let round = ampCount(partialSelector) 28 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 29 | if (!round) { 30 | amp.forEach(partialAmp => { 31 | nextAmp.push( 32 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 33 | (partialAmp && partialAmp + ' ') + partialSelector 34 | ) 35 | }) 36 | return 37 | } else if (round === 1) { 38 | amp.forEach(partialAmp => { 39 | nextAmp.push(partialSelector.replace('&', partialAmp)) 40 | }) 41 | return 42 | } 43 | let partialNextAmp: string[] = [ 44 | partialSelector 45 | ] 46 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 47 | while (round--) { 48 | const nextPartialNextAmp: string[] = [] 49 | partialNextAmp.forEach(selectorItr => { 50 | amp.forEach( 51 | partialAmp => { 52 | nextPartialNextAmp.push(selectorItr.replace('&', partialAmp)) 53 | } 54 | ) 55 | }) 56 | partialNextAmp = nextPartialNextAmp 57 | } 58 | partialNextAmp.forEach(part => nextAmp.push(part)) 59 | }) 60 | return nextAmp 61 | } 62 | 63 | /** 64 | * selector mustn't includes '&' 65 | * selector is trimmed 66 | */ 67 | function resolveSelector (amp: string[], selector: string): string[] { 68 | const nextAmp: string[] = [] 69 | selector.split(separatorRegex).forEach(partialSelector => { 70 | amp.forEach(partialAmp => { 71 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 72 | nextAmp.push(((partialAmp && partialAmp + ' ') + partialSelector)) 73 | }) 74 | }) 75 | return nextAmp 76 | } 77 | 78 | export function parseSelectorPath ( 79 | selectorPaths: CSelectorPath 80 | ): string { 81 | let amp: string[] = [''] 82 | selectorPaths.forEach(selector => { 83 | // eslint-disable-next-line 84 | selector = selector && selector.trim() 85 | if ( 86 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 87 | !selector 88 | ) { 89 | /** 90 | * if it's a empty selector, do nothing 91 | */ 92 | return 93 | } 94 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 95 | if (selector.includes('&')) { 96 | amp = resolveSelectorWithAmp(amp, selector) 97 | } else { 98 | amp = resolveSelector(amp, selector) 99 | } 100 | }) 101 | return amp.join(', ').replace(extraSpaceRegex, ' ') 102 | } 103 | -------------------------------------------------------------------------------- /packages/vue3-ssr/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { CssRender } from 'css-render' 2 | import { h, createSSRApp, defineComponent } from 'vue' 3 | import { renderToString } from '@vue/server-renderer' 4 | import { useSsrAdapter, setup } from '../src/index' 5 | 6 | const { c } = CssRender() 7 | 8 | describe('ssr', () => { 9 | describe('render to string', () => { 10 | const Child = defineComponent({ 11 | setup () { 12 | c('div', { 13 | color: 'red' 14 | }).mount({ 15 | id: 'mount-id', 16 | ssr: useSsrAdapter() 17 | }) 18 | }, 19 | render () { 20 | return h('div', null, 'Child') 21 | } 22 | }) 23 | const App = defineComponent({ 24 | render () { 25 | return h(Child) 26 | } 27 | }) 28 | const app = createSSRApp(App) 29 | const { collect } = setup(app) 30 | it('should work', (done) => { 31 | renderToString(app).then((v) => { 32 | expect(collect() + v).toMatchSnapshot() 33 | done() 34 | }) 35 | }) 36 | }) 37 | describe('useSsrAdapter', () => { 38 | const Child = defineComponent({ 39 | setup () { 40 | c('div', { 41 | color: 'red' 42 | }).mount({ 43 | id: 'mount-id', 44 | ssr: useSsrAdapter() 45 | }) 46 | }, 47 | render () { 48 | return h('div', null, 'Child') 49 | } 50 | }) 51 | const App = defineComponent({ 52 | render () { 53 | return h(Child) 54 | } 55 | }) 56 | const app = createSSRApp(App) 57 | const { collect } = setup(app) 58 | it('should work', (done) => { 59 | renderToString(app).then((v) => { 60 | expect(collect() + v).toMatchSnapshot() 61 | done() 62 | }) 63 | }) 64 | }) 65 | // uncomment after vue fixes the stream ssr bug 66 | // describe('render to stream', () => { 67 | // it('should work', (done) => { 68 | // const app = createSSRApp({ 69 | // render () { 70 | // return [ 71 | // h(Foo), 72 | // h(Suspense, null, { 73 | // default: () => h(Bar), 74 | // ssFallback: () => 'suspense' 75 | // }), 76 | // h(Foo) 77 | // ] 78 | // } 79 | // }) 80 | // const Foo = { 81 | // setup () { 82 | // c('div', { 83 | // color: 'foo' 84 | // }).mount({ 85 | // id: 'foo', 86 | // ssr: ssrAdapter 87 | // }) 88 | // }, 89 | // render: () => h('div', 'foo') 90 | // } 91 | // const Bar = { 92 | // async setup () { 93 | // c('div', { 94 | // color: 'bar' 95 | // }).mount({ 96 | // id: 'bar', 97 | // ssr: ssrAdapter 98 | // }) 99 | // await new Promise((resolve) => setTimeout(resolve, 1000)) 100 | // }, 101 | // render: () => { 102 | // return [h('div', 'bar'), h(Foo)] 103 | // } 104 | // } 105 | // const { collect } = setup(app) 106 | // const rs = renderToStream(app) 107 | // let html = '' 108 | // rs.on('data', (chunk) => { 109 | // html += `${collect()}\n${chunk.toString() as string}` 110 | // }) 111 | // rs.on('end', () => { 112 | // expect(html).toMatchSnapshot() 113 | // done() 114 | // }) 115 | // }) 116 | // }) 117 | }) 118 | -------------------------------------------------------------------------------- /packages/css-render/src/mount.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/prefer-ts-expect-error */ 2 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 3 | import hash from './hash' 4 | import { 5 | CNode, 6 | CssRenderInstance, 7 | CRenderProps, 8 | MountId, 9 | SsrAdapter 10 | } from './types' 11 | import { createElement, queryElement, removeElement } from './utils' 12 | 13 | if (typeof window !== 'undefined') { 14 | (window as any).__cssrContext = {} 15 | } 16 | 17 | export function unmount ( 18 | instance: CssRenderInstance, 19 | node: CNode, 20 | id: MountId, 21 | parent: ParentNode | undefined 22 | ): void { 23 | const { els } = node 24 | // If id is undefined, unmount all styles 25 | if (id === undefined) { 26 | els.forEach(removeElement) 27 | node.els = [] 28 | } else { 29 | const target = queryElement(id, parent) 30 | // eslint-disable-next-line 31 | if (target && els.includes(target)) { 32 | removeElement(target) 33 | node.els = els.filter((el) => el !== target) 34 | } 35 | } 36 | } 37 | 38 | function addElementToList ( 39 | els: HTMLStyleElement[], 40 | target: HTMLStyleElement 41 | ): void { 42 | els.push(target) 43 | } 44 | 45 | function mount ( 46 | instance: CssRenderInstance, 47 | node: CNode, 48 | id: MountId, 49 | props: CRenderProps, 50 | head: boolean, 51 | force: boolean, 52 | anchorMetaName: string | undefined, 53 | parent: ParentNode | undefined, 54 | ssrAdapter: SsrAdapter 55 | ): void 56 | function mount ( 57 | instance: CssRenderInstance, 58 | node: CNode, 59 | id: MountId, 60 | props: CRenderProps, 61 | head: boolean, 62 | force: boolean, 63 | anchorMetaName: string | undefined, 64 | parent: ParentNode | undefined, 65 | ssrAdapter?: undefined 66 | ): HTMLStyleElement 67 | function mount ( 68 | instance: CssRenderInstance, 69 | node: CNode, 70 | id: MountId, 71 | props: CRenderProps, 72 | head: boolean, 73 | force: boolean, 74 | anchorMetaName: string | undefined, 75 | parent: ParentNode | undefined, 76 | ssrAdapter?: SsrAdapter 77 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 78 | ): HTMLStyleElement | void 79 | function mount ( 80 | instance: CssRenderInstance, 81 | node: CNode, 82 | id: MountId, 83 | props: CRenderProps, 84 | head: boolean, 85 | force: boolean, 86 | anchorMetaName: string | undefined, 87 | parent: ParentNode | undefined, 88 | ssrAdapter?: SsrAdapter 89 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 90 | ): HTMLStyleElement | void { 91 | let style: string | undefined 92 | if (id === undefined) { 93 | style = node.render(props) 94 | id = hash(style) 95 | } 96 | if (ssrAdapter) { 97 | ssrAdapter.adapter(id, style ?? node.render(props)) 98 | return 99 | } 100 | if (parent === undefined) { 101 | parent = document.head 102 | } 103 | const queriedTarget = queryElement(id, parent) 104 | if (queriedTarget !== null && !force) { 105 | return queriedTarget 106 | } 107 | const target = queriedTarget ?? createElement(id) 108 | if (style === undefined) style = node.render(props) 109 | target.textContent = style 110 | if (queriedTarget !== null) return queriedTarget 111 | if (anchorMetaName) { 112 | const anchorMetaEl = parent.querySelector( 113 | `meta[name="${anchorMetaName}"]` 114 | ) 115 | if (anchorMetaEl) { 116 | parent.insertBefore(target, anchorMetaEl) 117 | addElementToList(node.els, target) 118 | return target 119 | } 120 | } 121 | 122 | if (head) { 123 | parent.insertBefore( 124 | target, 125 | parent.querySelector('style, link') 126 | ) 127 | } else { 128 | parent.appendChild(target) 129 | } 130 | addElementToList(node.els, target) 131 | return target 132 | } 133 | 134 | export { mount } 135 | -------------------------------------------------------------------------------- /packages/css-render/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Properties } from 'csstype' 2 | 3 | interface CssrSsrContext { 4 | styles: string[] 5 | ids: Set 6 | } 7 | 8 | export interface SsrAdapter { 9 | adapter: (id: string, style: string) => void 10 | context: CssrSsrContext 11 | } 12 | 13 | export interface CContext { 14 | [key: string]: any 15 | } 16 | 17 | /** render related */ 18 | export type CRenderProps = any 19 | 20 | export interface CRenderOption { 21 | context: CContext 22 | props: CRenderProps 23 | } 24 | 25 | /** mount related */ 26 | export type MountId = string | undefined 27 | export interface UnmountOption { 28 | id?: MountId 29 | parent?: ParentNode 30 | } 31 | 32 | export interface MountOption { 33 | id?: MountId 34 | props?: CRenderProps 35 | ssr?: T 36 | head?: boolean 37 | force?: boolean 38 | anchorMetaName?: string 39 | parent?: ParentNode 40 | } 41 | 42 | /** find related */ 43 | export type CFindTarget = (target: string, parent?: ParentNode) => HTMLStyleElement | null 44 | 45 | /** CNode */ 46 | export interface CNode { 47 | $: CSelector 48 | props: CProperties 49 | children: CNodeChildren 50 | instance: CssRenderInstance 51 | els: HTMLStyleElement[] 52 | render: (props?: T) => string 53 | mount: ( 54 | options?: MountOption 55 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 56 | ) => T extends undefined ? HTMLStyleElement : void 57 | unmount: (options?: UnmountOption) => void 58 | } 59 | 60 | /** Node Children */ 61 | type CNodeLazyChild = ( 62 | option: CRenderOption 63 | ) => CNodePlainChild | CNode | null | undefined 64 | type CNodePlainChild = 65 | | CNode 66 | | string 67 | | Array 68 | export type CNodeChildren = Array< 69 | CNodePlainChild | CNodeLazyChild | null | undefined 70 | > | null 71 | 72 | /** Properties */ 73 | export type CProperty = CPlainProperties | string | number | undefined | null 74 | export interface CPlainProperties extends Properties { 75 | raw?: string 76 | [nonPropertyLiteral: string]: CProperty 77 | } 78 | export type CLazyProperties = (options: { 79 | context?: CContext 80 | props?: CRenderProps 81 | }) => CPlainProperties | string | null | undefined 82 | export type CProperties = 83 | | CPlainProperties 84 | | CLazyProperties 85 | | string 86 | | null 87 | | undefined 88 | 89 | /** Selector */ 90 | export type CStringSelector = string 91 | export type CLazySelector = ( 92 | options: CRenderOption 93 | ) => T 94 | export interface COptionSelector { 95 | $?: CLazySelector | CStringSelector | null 96 | before?: (context: CContext) => any 97 | after?: (context: CContext) => any 98 | } 99 | export type CSelector = 100 | | CStringSelector 101 | | CLazySelector 102 | | COptionSelector 103 | | null 104 | | undefined 105 | export type CSelectorPath = Array 106 | 107 | /** CNode */ 108 | export interface createCNode { 109 | (selector: T, props: CProperties, children: CNodeChildren): CNode 110 | (selector: T, props: CProperties): CNode 111 | (selector: T, children: CNodeChildren): CNode 112 | (children: CNodeChildren): CNode 113 | } 114 | 115 | export type baseCreateCNodeForCssRenderInstance = ( 116 | instance: CssRenderInstance, 117 | selector: CSelector, 118 | props: CProperties, 119 | children: CNodeChildren 120 | ) => CNode 121 | 122 | export interface createCNodeForCssRenderInstance 123 | extends baseCreateCNodeForCssRenderInstance { 124 | (instance: CssRenderInstance, selector: CSelector, props: CProperties): CNode 125 | ( 126 | instance: CssRenderInstance, 127 | selector: CSelector, 128 | children: CNodeChildren 129 | ): CNode 130 | (instance: CssRenderInstance, children: CNodeChildren): CNode 131 | } 132 | 133 | export interface CssRenderInstance { 134 | context: { 135 | [key: string]: any 136 | } 137 | c: createCNode 138 | use: (plugin: CssRenderPlugin, ...args: any[]) => void 139 | find: CFindTarget 140 | config: CssRenderConfig 141 | } 142 | 143 | export interface CssRenderPlugin { 144 | install: (instance: CssRenderInstance, ...args: any[]) => void 145 | } 146 | 147 | export interface CssRenderConfig { 148 | keepEmptyBlock?: boolean 149 | } 150 | -------------------------------------------------------------------------------- /packages/css-render/__tests__/parse/pathTestCases.spec.ts: -------------------------------------------------------------------------------- 1 | interface CSelectorTestCase { input: Array, output: string } 2 | 3 | const testCases: CSelectorTestCase[] = [ 4 | { 5 | input: ['', '.a', '.b'], 6 | output: '.a .b' 7 | }, 8 | { 9 | input: ['.a', '.b'], 10 | output: '.a .b' 11 | }, 12 | { 13 | input: ['.a', '&'], 14 | output: '.a' 15 | }, 16 | { 17 | input: ['.a', '& + a'], 18 | output: '.a + a' 19 | }, 20 | { 21 | input: ['.a', 'b, & c'], 22 | output: '.a b, .a c' 23 | }, 24 | { 25 | input: ['.a > b', '&.c, &.d'], 26 | output: '.a > b.c, .a > b.d' 27 | }, 28 | { 29 | input: ['.a', '> b', '&.c, &.d', '&.e, > .f'], 30 | output: '.a > b.c.e, .a > b.c > .f, .a > b.d.e, .a > b.d > .f' 31 | }, 32 | { 33 | input: ['', '.a', '> b', '&.c, &.d', '&.e, > .f'], 34 | output: '.a > b.c.e, .a > b.c > .f, .a > b.d.e, .a > b.d > .f' 35 | }, 36 | { 37 | input: ['.a > b', '&', '&', '&'], 38 | output: '.a > b' 39 | }, 40 | { 41 | input: ['', '.a > b', '&', '&', '&'], 42 | output: '.a > b' 43 | }, 44 | { 45 | input: ['a, b', 'c, d', 'e, f'], 46 | output: 'a c e, a c f, a d e, a d f, b c e, b c f, b d e, b d f' 47 | }, 48 | { 49 | input: ['a, b', '& + c, d', 'e, & + f'], 50 | output: 'a + c e, a + c + f, a d e, a d + f, b + c e, b + c + f, b d e, b d + f' 51 | }, 52 | { 53 | input: ['@keyframes good-animation'], 54 | output: '@keyframes good-animation' 55 | }, 56 | { 57 | input: ['a, b', '.whatever:is(div, span, p), .freestyle', 'c, d'], 58 | output: 'a .whatever:is(div, span, p) c, a .whatever:is(div, span, p) d, a .freestyle c, a .freestyle d, b .whatever:is(div, span, p) c, b .whatever:is(div, span, p) d, b .freestyle c, b .freestyle d' 59 | }, 60 | { 61 | input: ['body', 'pfx &.dark'], 62 | output: 'pfx body.dark' 63 | }, 64 | { 65 | input: ['body', 'pfx&.dark'], 66 | output: 'pfxbody.dark' 67 | }, 68 | { 69 | input: ['body', '', 'body'], 70 | output: 'body body' 71 | }, 72 | { 73 | input: ['body', '', 'pfx &.dark'], 74 | output: 'pfx body.dark' 75 | }, 76 | { 77 | input: ['body', null, 'pfx &.dark'], 78 | output: 'pfx body.dark' 79 | }, 80 | { 81 | input: ['body', ' ', 'pfx &.dark'], 82 | output: 'pfx body.dark' 83 | }, 84 | { 85 | input: [' body ', ' ', ' pfx &.dark '], 86 | output: 'pfx body.dark' 87 | }, 88 | { 89 | input: [' a, b ', '', ' & + c, d', ' ', 'e, & + f '], 90 | output: 'a + c e, a + c + f, a d e, a d + f, b + c e, b + c + f, b d e, b d + f' 91 | }, 92 | { 93 | input: [' a, b ', '', '', null, ' & + c, d', ' ', 'e, & + f '], 94 | output: 'a + c e, a + c + f, a d e, a d + f, b + c e, b + c + f, b d e, b d + f' 95 | }, 96 | { 97 | input: ['a, b', undefined, '& + c, d', null, 'e, & + f'], 98 | output: 'a + c e, a + c + f, a d e, a d + f, b + c e, b + c + f, b d e, b d + f' 99 | }, 100 | { 101 | input: ['', '&.a'], 102 | output: '.a' 103 | }, 104 | { 105 | input: ['.header', '.menu', '.no-borderradius &'], 106 | output: '.no-borderradius .header .menu' 107 | }, 108 | { 109 | input: ['.grand', '.parent', '& > &'], 110 | output: '.grand .parent > .grand .parent' 111 | }, 112 | { 113 | input: ['.grand', '.parent', '&&'], 114 | output: '.grand .parent.grand .parent' 115 | }, 116 | { 117 | input: ['.grand', '.parent', '&, &ish'], 118 | output: '.grand .parent, .grand .parentish' 119 | }, 120 | { 121 | input: ['a, b', '&.c'], 122 | output: 'a.c, b.c' 123 | }, 124 | { 125 | input: ['a, b', '& &'], 126 | output: 'a a, a b, b a, b b' 127 | }, 128 | { 129 | input: ['p, a, ul, li', '& + &'], 130 | output: 'p + p, p + a, p + ul, p + li, a + p, a + a, a + ul, a + li, ul + p, ul + a, ul + ul, ul + li, li + p, li + a, li + ul, li + li' 131 | }, 132 | { 133 | input: ['div', 'span, p, span'], 134 | output: 'div span, div p, div span' 135 | }, 136 | { 137 | input: ['a, b, c', 'x'], 138 | output: 'a x, b x, c x' 139 | }, 140 | { 141 | input: ['a, b, c', 'x + y'], 142 | output: 'a x + y, b x + y, c x + y' 143 | }, 144 | { 145 | input: [' a ', ' & ', ' &b '], 146 | output: 'ab' 147 | }, 148 | { 149 | input: [' a', 'b&', ''], 150 | output: 'ba' 151 | } 152 | ] 153 | 154 | export default testCases 155 | -------------------------------------------------------------------------------- /packages/plugin-bem/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/restrict-template-expressions */ 2 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 3 | 4 | import { 5 | COptionSelector, 6 | CLazySelector, 7 | CStringSelector, 8 | CssRenderPlugin, 9 | createCNode, 10 | CSelector 11 | } from 'css-render' 12 | 13 | interface BEMPluginOptions { 14 | blockPrefix?: string 15 | elementPrefix?: string 16 | modifierPrefix?: string 17 | } 18 | 19 | type AvailableSelector = CStringSelector | CLazySelector 20 | 21 | interface CssRenderBemPlugin extends CssRenderPlugin { 22 | cB: createCNode 23 | cE: createCNode 24 | cM: createCNode 25 | cNotM: createCNode 26 | } 27 | 28 | function plugin (options?: BEMPluginOptions): CssRenderBemPlugin { 29 | let _bPrefix: string = '.' 30 | let _ePrefix: string = '__' 31 | let _mPrefix: string = '--' 32 | let c: createCNode 33 | if (options) { 34 | let t = options.blockPrefix 35 | if (t) { 36 | _bPrefix = t 37 | } 38 | t = options.elementPrefix 39 | if (t) { 40 | _ePrefix = t 41 | } 42 | t = options.modifierPrefix 43 | if (t) { 44 | _mPrefix = t 45 | } 46 | } 47 | 48 | const _plugin: CssRenderPlugin = { 49 | install (instance) { 50 | c = instance.c 51 | const ctx = instance.context 52 | ctx.bem = {} 53 | ctx.bem.b = null 54 | ctx.bem.els = null 55 | } 56 | } 57 | 58 | function b (arg: AvailableSelector): COptionSelector { 59 | let memorizedB: string | null 60 | let memorizedE: string | null 61 | return { 62 | before (ctx) { 63 | memorizedB = ctx.bem.b 64 | memorizedE = ctx.bem.els 65 | ctx.bem.els = null 66 | }, 67 | after (ctx) { 68 | ctx.bem.b = memorizedB 69 | ctx.bem.els = memorizedE 70 | }, 71 | $ ({ context, props }) { 72 | arg = typeof arg === 'string' ? arg : arg({ context, props }) 73 | context.bem.b = arg 74 | return `${props?.bPrefix || _bPrefix}${context.bem.b as string}` 75 | } 76 | } 77 | } 78 | 79 | function e (arg: AvailableSelector): COptionSelector { 80 | let memorizedE: string | null 81 | return { 82 | before (ctx) { 83 | memorizedE = ctx.bem.els 84 | }, 85 | after (ctx) { 86 | ctx.bem.els = memorizedE 87 | }, 88 | $ ({ context, props }) { 89 | arg = typeof arg === 'string' ? arg : arg({ context, props }) 90 | context.bem.els = arg.split(',').map(v => v.trim()) 91 | return (context.bem.els as string[]) 92 | .map(el => `${props?.bPrefix || _bPrefix}${context.bem.b as string}${_ePrefix}${el}`).join(', ') 93 | } 94 | } 95 | } 96 | 97 | function m (arg: AvailableSelector): COptionSelector { 98 | return { 99 | $ ({ context, props }) { 100 | arg = typeof arg === 'string' ? arg : arg({ context, props }) 101 | const modifiers = arg.split(',').map(v => v.trim()) 102 | function elementToSelector (el?: string): string { 103 | return modifiers.map(modifier => `&${props?.bPrefix || _bPrefix}${context.bem.b as string}${ 104 | el !== undefined ? `${_ePrefix}${el}` : '' 105 | }${_mPrefix}${modifier}`).join(', ') 106 | } 107 | const els = context.bem.els 108 | if (els !== null) { 109 | if (process.env.NODE_ENV !== 'production' && els.length >= 2) { 110 | throw Error( 111 | `[css-render/plugin-bem]: m(${arg}) is invalid, using modifier inside multiple elements is not allowed` 112 | ) 113 | } 114 | return elementToSelector(els[0]) 115 | } else { 116 | return elementToSelector() 117 | } 118 | } 119 | } 120 | } 121 | 122 | function notM (arg: AvailableSelector): COptionSelector { 123 | return { 124 | $ ({ context, props }) { 125 | arg = typeof arg === 'string' ? arg : arg({ context, props }) 126 | const els = context.bem.els as null | string[] 127 | if (process.env.NODE_ENV !== 'production' && els !== null && els.length >= 2) { 128 | throw Error( 129 | `[css-render/plugin-bem]: notM(${arg}) is invalid, using modifier inside multiple elements is not allowed` 130 | ) 131 | } 132 | return `&:not(${props?.bPrefix || _bPrefix}${context.bem.b as string}${ 133 | (els !== null && els.length > 0) ? `${_ePrefix}${els[0]}` : '' 134 | }${_mPrefix}${arg})` 135 | } 136 | } 137 | } 138 | 139 | const cB = ((...args: any[]) => c(b(args[0]), args[1], args[2])) as createCNode 140 | const cE = ((...args: any[]) => c(e(args[0]), args[1], args[2])) as createCNode 141 | const cM = ((...args: any[]) => c(m(args[0]), args[1], args[2])) as createCNode 142 | const cNotM = ((...args: any[]) => c(notM(args[0]), args[1], args[2])) as createCNode 143 | 144 | Object.assign(_plugin, { 145 | cB, cE, cM, cNotM 146 | }) 147 | 148 | return _plugin as CssRenderBemPlugin 149 | } 150 | 151 | export { plugin } 152 | export default plugin 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-render · [![GitHub Liscense](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/07akioni/css-render/blob/master/LICENSE) [![npm](https://img.shields.io/npm/v/css-render)](https://www.npmjs.com/package/css-render) [![Total alerts](https://img.shields.io/lgtm/alerts/g/07akioni/css-render.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/07akioni/css-render/alerts/) [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&precision=2)](https://codecov.io/gh/07akioni/css-render) 2 | 3 | Generating CSS using JS with considerable flexibility and extensibility, at both server side and client side. 4 | 5 | It's mainly built for **library builders** (who wants make their library work without css import at small overhead). It's not recommend to use it in a webapp. 6 | 7 | It is not designed to totally replace other style-related solutions, but to be a progressive tool which can just work as a supplementary of your style files or totally replace your `.css` files. 8 | 9 | ## Docs 10 | [css-render](https://css-render.vercel.app/) 11 | 12 | ## Why Using It 13 | 1. You want to ship a library without css at a small price (gzip < 2kb). 14 | 2. Reduce size compared with static css (which contains duplicate logic). 15 | 3. You can't write `sass-like` or `less-like` css-in-js (eg. `mixin` in sass or less). 16 | 4. You want to write style variables in JS. 17 | 5. Support an simple SSR API (now only for vue3). 18 | 19 | ## Comparasion with other CSS-in-JS framework 20 | 21 | Main differences between css-render and styled-component, jss or emotion: 22 | 1. It doesn't do the bindings between components and styles. It is more like a style generator with low level mount and unmount API. 23 | 2. It's easier to write like a sass mixin or less mixin. 24 | 25 | 26 | ## Examples 27 | ### Realworld Example 28 | - [DataTable](https://github.com/TuSimple/naive-ui/blob/main/src/data-table/src/styles/index.cssr.ts) 29 | - [XScroll](https://github.com/07akioni/vueuc/blob/main/src/x-scroll/src/index.ts) 30 | - [VirtualList](https://github.com/07akioni/vueuc/blob/main/src/virtual-list/src/VirtualList.ts) 31 | 32 | ### Basic Example 33 | ```bash 34 | $ npm install --save-dev css-render 35 | ``` 36 | ```js 37 | import CssRender from 'css-render' 38 | /** 39 | * CommonJS: 40 | * const { CssRender } = require('css-render') 41 | */ 42 | 43 | const { 44 | c 45 | } = CssRender() 46 | 47 | const style = c('body', ({ props }) => ({ 48 | margin: 0, 49 | backgroundColor: props.backgroundColor 50 | }), [ 51 | c('&.dark', { 52 | backgroundColor: 'black' 53 | }), 54 | c('.container', { 55 | width: '100%' 56 | }) 57 | ]) 58 | 59 | /** use it as string */ 60 | console.log(style.render({ backgroundColor: 'white' })) 61 | /** 62 | * or mount on document.head. (the following lines only work in the browser.) 63 | */ 64 | style.mount() 65 | // ... 66 | style.unmount() 67 | ``` 68 | ```css 69 | body { 70 | margin: 0; 71 | background-color: white; 72 | } 73 | 74 | body.dark { 75 | background-color: black; 76 | } 77 | 78 | body .container { 79 | width: 100%; 80 | } 81 | ``` 82 | 83 | ### BEM Plugin Example 84 | ```bash 85 | $ npm install --save-dev css-render @css-render/plugin-bem 86 | ``` 87 | 88 | You can use bem plugin to generate bem CSS like this: 89 | 90 | ```js 91 | import CssRender from 'css-render' 92 | import bem from '@css-render/plugin-bem' 93 | /** 94 | * CommonJS: 95 | * const { CssRender } = require('css-render') 96 | * const { plugin: bem } = require('@css-render/plugin-bem') 97 | */ 98 | 99 | const cssr = CssRender() 100 | const plugin = bem({ 101 | blockPrefix: '.c-' 102 | }) 103 | cssr.use(plugin) // bind the plugin with the cssr instance 104 | const { 105 | cB, cE, cM 106 | } = plugin 107 | 108 | const style = cB( 109 | 'container', 110 | [ 111 | cE( 112 | 'left, right', 113 | { 114 | width: '50%' 115 | } 116 | ), 117 | cM( 118 | 'dark', 119 | [ 120 | cE( 121 | 'left, right', 122 | { 123 | backgroundColor: 'black' 124 | } 125 | ) 126 | ] 127 | ) 128 | ] 129 | ) 130 | 131 | /** use it as string */ 132 | console.log(style.render()) 133 | /** 134 | * or mount on document.head 135 | * the following lines only works in browser, don't call them in node.js 136 | */ 137 | style.mount() 138 | // ... 139 | style.unmount() 140 | ``` 141 | ```css 142 | .c-container .c-container__left, .c-container .c-container__right { 143 | width: 50%; 144 | } 145 | 146 | .c-container.c-container--dark .c-container__left, .c-container.c-container--dark .c-container__right { 147 | background-color: black; 148 | } 149 | ``` 150 | 151 | ## Packages 152 | |Name|Cov| 153 | |-|-| 154 | |css-render|[![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=css-render)](https://codecov.io/gh/07akioni/css-render)| 155 | |@css-render/plugin-bem| [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=plugin-bem)](https://codecov.io/gh/07akioni/css-render)| 156 | |vue3-ssr| [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=vue3-ssr)](https://codecov.io/gh/07akioni/css-render)| -------------------------------------------------------------------------------- /packages/docs/docs/cnode-and-render.md: -------------------------------------------------------------------------------- 1 | # Create a `CNode` & Render a `CNode` Tree 2 | To create a `CNode`, you need to create a [CssRender instance](https://github.com/07akioni/css-render/blob/master/docs/css-render-instance.md) and get the `c` method out of it. For example: 3 | ```js 4 | import CssRender from 'css-render' 5 | 6 | const { 7 | c 8 | } = CssRender() // return a CssRender instance 9 | ``` 10 | The `c` method is use to create a `CNode`. 11 | ## `c` method 12 | It can be use in the following forms: 13 | - `c(selector: string | Function | Object | null, cssProps: Object | string | Function)` 14 | - `c(selector: string | Function | Object | null, cssProps: Object | string | Function, children: Array)` 15 | - `c(selector: string | Function | Object | null, children: Array)` 16 | - `c(children: Array)` 17 | Both of them will return a `CNode`. 18 | ### `selector` 19 | #### String `selector` 20 | selector can be any CSS selector, it also support `&` syntax. 21 | For example: 22 | ```js 23 | c('.button', { 24 | color: 'black' 25 | }, [ 26 | c('.button__icon', { 27 | fill: 'black' 28 | }), 29 | c('&.button--error', { 30 | color: 'red' 31 | }) 32 | ]).render() 33 | ``` 34 | ```css 35 | .button { 36 | color: black; 37 | } 38 | 39 | .button .button__icon { 40 | fill: black; 41 | } 42 | 43 | .button.button--error { 44 | color: red; 45 | } 46 | ``` 47 | #### Function `selector` 48 | Except `string`, `selector` can be a function that returns a string: 49 | ```js 50 | c(({ 51 | context, 52 | props 53 | }) => props.selector, { 54 | color: 'black' 55 | }).render({ 56 | selector: '.selector' 57 | }) 58 | ``` 59 | ```css 60 | .selector { 61 | color: black; 62 | } 63 | ``` 64 | - `props` is passed from render function of itself or its ascendant's `CNode`. For example, in `cnode.render({ color: 'black' })`, `{ color: 'black' }` is the `props`. 65 | - `context` is the `context` of `c` method's corresponding `CssRender` instance. 66 | 67 | If you want to know more about `context`, see [CssRender](https://github.com/07akioni/css-render/blob/master/docs/css-render-instance.md). 68 | 69 | #### Object `selector` 70 | 71 | `selector` can also be a object which has more utilities. The object can be typed as 72 | ```typescript 73 | { 74 | $?: (({ context, props }) => string | null) | string | null, 75 | before?: (context) => void, 76 | after?: (context) => void 77 | } 78 | ``` 79 | - `$` can be a string selector or a function selector or `null`. 80 | - `before` is the hook before the `CNode` tree is rendered. You can setup the `context` in the hook. 81 | - `after` is the hook after the `CNode` tree is rendered. You can do some cleaning in the hook. 82 | 83 | #### Null `selector` 84 | It will work as if the selector does not exist: 85 | ```js 86 | c('div', [ 87 | c(null, [ 88 | c('button', { 89 | color: 'black' 90 | }) 91 | ]) 92 | ]).render() 93 | ``` 94 | ```css 95 | div button { 96 | color: black; 97 | } 98 | ``` 99 | 100 | ### `cssProps` 101 | #### Object `cssProps` & string `cssProps` 102 | You can put just a plain object or string: 103 | ```js 104 | c('.button', ` 105 | background-color: green; 106 | `).render() 107 | 108 | c('@keyframes my-animation', { 109 | from: { 110 | color: 'white' 111 | }, 112 | to: { 113 | color: 'black' 114 | } 115 | }).render() 116 | ``` 117 | ```css 118 | .button { 119 | background-color: green; 120 | } 121 | 122 | @keyframes my-animation { 123 | from { 124 | color: white; 125 | } 126 | to { 127 | color: black; 128 | } 129 | } 130 | ``` 131 | #### Function `cssProps` 132 | If you want to determine the cssProps of a `CNode` at rendering phase. Use a `({ context, props }) => string | Object` typed function as the cssProps of `CNode`. 133 | 134 | ```js 135 | const style = c('.button', ({ 136 | context, 137 | props 138 | }) => ({ 139 | color: props.color 140 | })) 141 | 142 | style.render({ color: 'red' }) 143 | 144 | style.render({ color: 'blue' }) 145 | ``` 146 | ```css 147 | .button { 148 | color: red; 149 | } 150 | 151 | .button { 152 | color: blue; 153 | } 154 | ``` 155 | 156 | Lazy evaluation is good for performance because you can reuse a `CNode` tree rather than rebuild one. 157 | 158 | ### `children` 159 | children is an array of `CNode` and function. If a function is in the array, its need to return a `CNode` or an array of `CNode`. 160 | ```js 161 | c('div', [ 162 | // function returns a CNode 163 | ({ context, props }) => c('button', { 164 | color: props.color 165 | }), 166 | // CNode 167 | c('ul', { 168 | backgroundColor: 'red' 169 | }), 170 | // Nested CNode Array 171 | [ 172 | [c('dl', { 173 | backgroundColor: 'red' 174 | })] 175 | ], 176 | // function returns a CNode Array 177 | () => [ 178 | c('ol', { 179 | backgroundColor: 'red' 180 | }) 181 | ] 182 | ]).render({ 183 | color: 'black' 184 | }) 185 | ``` 186 | ```css 187 | div button { 188 | color: black; 189 | } 190 | 191 | div ul { 192 | background-color: red; 193 | } 194 | 195 | div dl { 196 | background-color: red; 197 | } 198 | 199 | div ol { 200 | background-color: red; 201 | } 202 | ``` 203 | 204 | Also, you can render a style fragment like this: 205 | ```js 206 | c([ 207 | c('.button.button--blue', { 208 | color: 'blue' 209 | }), 210 | c('.button.button--red', { 211 | color: 'red' 212 | }) 213 | ]).render() 214 | ``` 215 | ```css 216 | .button.button--blue { 217 | color: blue; 218 | } 219 | 220 | .button.button--red { 221 | color: red; 222 | } 223 | ``` 224 | 225 | Maybe the only limitation of style generation is your imagination. -------------------------------------------------------------------------------- /packages/plugin-bem/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@css-render/plugin-bem", 3 | "entries": [ 4 | { 5 | "version": "0.15.14", 6 | "tag": "@css-render/plugin-bem_v0.15.14", 7 | "date": "Sun, 05 May 2024 06:34:46 GMT", 8 | "comments": {} 9 | }, 10 | { 11 | "version": "0.15.13", 12 | "tag": "@css-render/plugin-bem_v0.15.13", 13 | "date": "Sun, 05 May 2024 05:43:33 GMT", 14 | "comments": {} 15 | }, 16 | { 17 | "version": "0.15.12", 18 | "tag": "@css-render/plugin-bem_v0.15.12", 19 | "date": "Sat, 24 Dec 2022 17:17:08 GMT", 20 | "comments": {} 21 | }, 22 | { 23 | "version": "0.15.11", 24 | "tag": "@css-render/plugin-bem_v0.15.11", 25 | "date": "Sun, 21 Aug 2022 07:56:48 GMT", 26 | "comments": {} 27 | }, 28 | { 29 | "version": "0.15.10", 30 | "tag": "@css-render/plugin-bem_v0.15.10", 31 | "date": "Wed, 23 Mar 2022 12:33:17 GMT", 32 | "comments": {} 33 | }, 34 | { 35 | "version": "0.15.9", 36 | "tag": "@css-render/plugin-bem_v0.15.9", 37 | "date": "Sat, 05 Mar 2022 14:58:36 GMT", 38 | "comments": {} 39 | }, 40 | { 41 | "version": "0.15.8", 42 | "tag": "@css-render/plugin-bem_v0.15.8", 43 | "date": "Tue, 28 Dec 2021 18:59:30 GMT", 44 | "comments": { 45 | "none": [ 46 | { 47 | "comment": "fix peer deps" 48 | } 49 | ] 50 | } 51 | }, 52 | { 53 | "version": "0.15.7", 54 | "tag": "@css-render/plugin-bem_v0.15.7", 55 | "date": "Tue, 28 Dec 2021 17:54:23 GMT", 56 | "comments": {} 57 | }, 58 | { 59 | "version": "0.15.6", 60 | "tag": "@css-render/plugin-bem_v0.15.6", 61 | "date": "Thu, 02 Sep 2021 15:52:34 GMT", 62 | "comments": {} 63 | }, 64 | { 65 | "version": "0.15.5", 66 | "tag": "@css-render/plugin-bem_v0.15.5", 67 | "date": "Sat, 24 Jul 2021 17:34:06 GMT", 68 | "comments": {} 69 | }, 70 | { 71 | "version": "0.15.4", 72 | "tag": "@css-render/plugin-bem_v0.15.4", 73 | "date": "Sun, 13 Jun 2021 04:56:14 GMT", 74 | "comments": {} 75 | }, 76 | { 77 | "version": "0.15.3", 78 | "tag": "@css-render/plugin-bem_v0.15.3", 79 | "date": "Sat, 12 Jun 2021 09:50:10 GMT", 80 | "comments": {} 81 | }, 82 | { 83 | "version": "0.15.2", 84 | "tag": "@css-render/plugin-bem_v0.15.2", 85 | "date": "Wed, 02 Jun 2021 04:15:14 GMT", 86 | "comments": {} 87 | }, 88 | { 89 | "version": "0.15.1", 90 | "tag": "@css-render/plugin-bem_v0.15.1", 91 | "date": "Tue, 01 Jun 2021 13:35:47 GMT", 92 | "comments": {} 93 | }, 94 | { 95 | "version": "0.15.0", 96 | "tag": "@css-render/plugin-bem_v0.15.0", 97 | "date": "Tue, 01 Jun 2021 13:10:25 GMT", 98 | "comments": {} 99 | }, 100 | { 101 | "version": "0.14.1", 102 | "tag": "@css-render/plugin-bem_v0.14.1", 103 | "date": "Wed, 19 May 2021 05:06:20 GMT", 104 | "comments": {} 105 | }, 106 | { 107 | "version": "0.14.0", 108 | "tag": "@css-render/plugin-bem_v0.14.0", 109 | "date": "Wed, 19 May 2021 05:03:05 GMT", 110 | "comments": {} 111 | }, 112 | { 113 | "version": "0.13.9", 114 | "tag": "@css-render/plugin-bem_v0.13.9", 115 | "date": "Sun, 16 May 2021 19:46:15 GMT", 116 | "comments": {} 117 | }, 118 | { 119 | "version": "0.13.8", 120 | "tag": "@css-render/plugin-bem_v0.13.8", 121 | "date": "Wed, 12 May 2021 11:45:08 GMT", 122 | "comments": {} 123 | }, 124 | { 125 | "version": "0.13.7", 126 | "tag": "@css-render/plugin-bem_v0.13.7", 127 | "date": "Wed, 12 May 2021 11:35:11 GMT", 128 | "comments": {} 129 | }, 130 | { 131 | "version": "0.13.6", 132 | "tag": "@css-render/plugin-bem_v0.13.6", 133 | "date": "Wed, 14 Apr 2021 17:53:56 GMT", 134 | "comments": {} 135 | }, 136 | { 137 | "version": "0.13.5", 138 | "tag": "@css-render/plugin-bem_v0.13.5", 139 | "date": "Wed, 14 Apr 2021 17:06:44 GMT", 140 | "comments": { 141 | "none": [ 142 | { 143 | "comment": "feat(plugin-bem): add bPrefix prop" 144 | } 145 | ] 146 | } 147 | }, 148 | { 149 | "version": "0.13.4", 150 | "tag": "@css-render/plugin-bem_v0.13.4", 151 | "date": "Wed, 14 Apr 2021 16:25:11 GMT", 152 | "comments": {} 153 | }, 154 | { 155 | "version": "0.13.3", 156 | "tag": "@css-render/plugin-bem_v0.13.3", 157 | "date": "Wed, 14 Apr 2021 16:24:04 GMT", 158 | "comments": {} 159 | }, 160 | { 161 | "version": "0.13.2", 162 | "tag": "@css-render/plugin-bem_v0.13.2", 163 | "date": "Fri, 26 Feb 2021 08:24:05 GMT", 164 | "comments": {} 165 | }, 166 | { 167 | "version": "0.13.1", 168 | "tag": "@css-render/plugin-bem_v0.13.1", 169 | "date": "Fri, 26 Feb 2021 08:18:28 GMT", 170 | "comments": { 171 | "none": [ 172 | { 173 | "comment": "No changes" 174 | } 175 | ] 176 | } 177 | }, 178 | { 179 | "version": "0.13.0", 180 | "tag": "@css-render/plugin-bem_v0.13.0", 181 | "date": "Wed, 24 Feb 2021 09:35:28 GMT", 182 | "comments": { 183 | "minor": [ 184 | { 185 | "comment": "No changes" 186 | } 187 | ], 188 | "dependency": [ 189 | { 190 | "comment": "Updating dependency \"css-render\" from `~0.12.0` to `~0.13.0`" 191 | } 192 | ] 193 | } 194 | } 195 | ] 196 | } 197 | -------------------------------------------------------------------------------- /packages/css-render/src/render.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 2 | import { 3 | CNode, 4 | CProperties, 5 | CssRenderInstance, 6 | CProperty, 7 | CPlainProperties, 8 | CRenderProps, 9 | CNodeChildren, 10 | CRenderOption, 11 | CSelectorPath 12 | } from './types' 13 | import { parseSelectorPath } from './parse' 14 | import { isMediaOrSupports } from './utils' 15 | 16 | const kebabRegex = /[A-Z]/g 17 | 18 | function kebabCase (pattern: string): string { 19 | return pattern.replace(kebabRegex, match => '-' + match.toLowerCase()) 20 | } 21 | 22 | /** TODO: refine it to solve nested object */ 23 | function unwrapProperty ( 24 | prop: CProperty, 25 | indent: string = ' ' 26 | ): string { 27 | if (typeof prop === 'object' && prop !== null) { 28 | return ( 29 | ' {\n' + 30 | Object.entries(prop).map(v => { 31 | return indent + ` ${kebabCase(v[0])}: ${v[1] as string};` 32 | }).join('\n') + 33 | '\n' + indent + '}' 34 | ) 35 | } 36 | return `: ${prop as string};` 37 | } 38 | 39 | /** unwrap properties */ 40 | function unwrapProperties ( 41 | props: CProperties, 42 | instance: CssRenderInstance, 43 | params: T 44 | ): CPlainProperties | string | undefined | null { 45 | if (typeof props === 'function') { 46 | return props({ 47 | context: instance.context, 48 | props: params 49 | }) 50 | } 51 | return props 52 | } 53 | 54 | function createStyle ( 55 | selector: string, 56 | props: CProperties, 57 | instance: CssRenderInstance, 58 | params: T 59 | ): string { 60 | if (!props) return '' 61 | // eslint-disable-next-line 62 | const unwrappedProps = unwrapProperties(props, instance, params) 63 | if (!unwrappedProps) return '' 64 | if (typeof unwrappedProps === 'string') { 65 | return `${selector} {\n${unwrappedProps}\n}` 66 | } 67 | const propertyNames = Object.keys(unwrappedProps) 68 | if (propertyNames.length === 0) { 69 | if (instance.config.keepEmptyBlock) return selector + ' {\n}' 70 | return '' 71 | } 72 | const statements = selector 73 | ? [ 74 | selector + ' {' 75 | ] 76 | : [] 77 | propertyNames.forEach(propertyName => { 78 | const property = unwrappedProps[propertyName] 79 | if (propertyName === 'raw') { 80 | statements.push('\n' + (property as string) + '\n') 81 | return 82 | } 83 | propertyName = kebabCase(propertyName) 84 | if (property !== null && property !== undefined) { 85 | statements.push(` ${propertyName}${unwrapProperty(property)}`) 86 | } 87 | }) 88 | if (selector) { 89 | statements.push('}') 90 | } 91 | return statements.join('\n') 92 | } 93 | 94 | function loopCNodeListWithCallback (children: CNodeChildren, options: CRenderOption, callback: (node: CNode | string) => any): void { 95 | /* istanbul ignore if */ 96 | if (!children) return 97 | children.forEach(child => { 98 | if (Array.isArray(child)) { 99 | loopCNodeListWithCallback(child, options, callback) 100 | } else if (typeof child === 'function') { 101 | const grandChildren = child(options) 102 | if (Array.isArray(grandChildren)) { 103 | loopCNodeListWithCallback(grandChildren, options, callback) 104 | } else if (grandChildren) { 105 | callback(grandChildren) 106 | } 107 | } else if (child) { 108 | callback(child) 109 | } 110 | }) 111 | } 112 | 113 | function traverseCNode ( 114 | node: CNode, 115 | selectorPaths: CSelectorPath, 116 | styles: string[], 117 | instance: CssRenderInstance, 118 | params: T 119 | ): void { 120 | const $ = node.$ 121 | let blockSelector = '' 122 | if (!$ || typeof $ === 'string') { 123 | if (isMediaOrSupports($)) { 124 | blockSelector = $ 125 | } else { 126 | // as a string selector 127 | selectorPaths.push($) 128 | } 129 | } else if (typeof $ === 'function') { 130 | const selector = $({ 131 | context: instance.context, 132 | props: params 133 | }) 134 | if (isMediaOrSupports(selector)) { 135 | blockSelector = selector 136 | } else { 137 | // as a lazy selector 138 | selectorPaths.push(selector) 139 | } 140 | } else { // as a option selector 141 | if ($.before) $.before(instance.context) 142 | if (!$.$ || typeof $.$ === 'string') { 143 | if (isMediaOrSupports($.$)) { 144 | blockSelector = $.$ 145 | } else { 146 | // as a string selector 147 | selectorPaths.push($.$) 148 | } 149 | } else /* istanbul ignore else */ if ($.$) { 150 | const selector = $.$({ 151 | context: instance.context, 152 | props: params 153 | }) 154 | if (isMediaOrSupports(selector)) { 155 | blockSelector = selector 156 | } else { 157 | // as a lazy selector 158 | selectorPaths.push(selector) 159 | } 160 | } 161 | } 162 | const selector = parseSelectorPath(selectorPaths) 163 | const style = createStyle(selector, node.props, instance, params) 164 | if (blockSelector) { 165 | styles.push(`${blockSelector} {`) 166 | } else if (style.length) { 167 | styles.push(style) 168 | } 169 | if (node.children) { 170 | loopCNodeListWithCallback(node.children, { 171 | context: instance.context, 172 | props: params 173 | }, childNode => { 174 | if (typeof childNode === 'string') { 175 | const style = createStyle(selector, { raw: childNode }, instance, params) 176 | styles.push(style) 177 | } else { 178 | traverseCNode(childNode, selectorPaths, styles, instance, params) 179 | } 180 | }) 181 | } 182 | selectorPaths.pop() 183 | if (blockSelector) { 184 | styles.push('}') 185 | } 186 | if ($ && ($ as any).after) ($ as any).after(instance.context) 187 | } 188 | 189 | export function render ( 190 | node: CNode, 191 | instance: CssRenderInstance, 192 | props?: T 193 | ): string { 194 | const styles: string[] = [] 195 | traverseCNode( 196 | node, 197 | [], 198 | styles, 199 | instance, 200 | props 201 | ) 202 | return styles.join('\n\n') 203 | } 204 | -------------------------------------------------------------------------------- /packages/docs/docs/index.md: -------------------------------------------------------------------------------- 1 | # css-render · [![GitHub Liscense](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/07akioni/css-render/blob/master/LICENSE) [![npm](https://img.shields.io/npm/v/css-render)](https://www.npmjs.com/package/css-render) [![Total alerts](https://img.shields.io/lgtm/alerts/g/07akioni/css-render.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/07akioni/css-render/alerts/) [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&precision=2)](https://codecov.io/gh/07akioni/css-render) 2 | 3 | Generating CSS using JS with considerable flexibility and extensibility, at both server side and client side. 4 | 5 | It's mainly built for **library builders** (who wants make their library work without css import at small overhead). It's not recommend to use it in a webapp. 6 | 7 | It is not designed to totally replace other style-related solutions, but to be a progressive tool which can just work as a supplementary of your style files or totally replace your `.css` files. 8 | 9 | ## Docs 10 | [css-render](https://css-render.vercel.app/) 11 | 12 | ## Why Using It 13 | 1. You want to ship a library without css at a small price (gzip < 2kb). 14 | 2. Reduce size compared with static css (which contains duplicate logic). 15 | 3. You can't write `sass-like` or `less-like` css-in-js (eg. `mixin` in sass or less). 16 | 4. You want to write style variables in JS. 17 | 5. Support an simple SSR API (now only for vue3). 18 | 19 | ## Comparasion with other CSS-in-JS framework 20 | 21 | Main differences between css-render and styled-component, jss or emotion: 22 | 1. It doesn't do the bindings between components and styles. It is more like a style generator with low level mount and unmount API. 23 | 2. It's easier to write like a sass mixin or less mixin. 24 | 25 | 26 | ## Examples 27 | ### Realword Example 28 | - [XScroll](https://github.com/07akioni/vueuc/blob/main/src/x-scroll/src/index.ts) 29 | - [VirtualList](https://github.com/07akioni/vueuc/blob/main/src/virtual-list/src/VirtualList.ts) 30 | 31 | ### Basic Example 32 | ```bash 33 | $ npm install --save-dev css-render 34 | ``` 35 | ```js 36 | import CssRender from 'css-render' 37 | /** 38 | * CommonJS: 39 | * const { CssRender } = require('css-render') 40 | */ 41 | 42 | const { 43 | c 44 | } = CssRender() 45 | 46 | const style = c('body', ({ props }) => ({ 47 | margin: 0, 48 | backgroundColor: props.backgroundColor 49 | }), [ 50 | c('&.dark', { 51 | backgroundColor: 'black' 52 | }), 53 | c('.container', { 54 | width: '100%' 55 | }) 56 | ]) 57 | 58 | /** use it as string */ 59 | console.log(style.render({ backgroundColor: 'white' })) 60 | /** 61 | * or mount on document.head. (the following lines only work in the browser.) 62 | */ 63 | style.mount() 64 | // ... 65 | style.unmount() 66 | ``` 67 | ```css 68 | body { 69 | margin: 0; 70 | background-color: white; 71 | } 72 | 73 | body.dark { 74 | background-color: black; 75 | } 76 | 77 | body .container { 78 | width: 100%; 79 | } 80 | ``` 81 | 82 | ### BEM Plugin Example 83 | ```bash 84 | $ npm install --save-dev css-render @css-render/plugin-bem 85 | ``` 86 | 87 | You can use bem plugin to generate bem CSS like this: 88 | 89 | ```js 90 | import CssRender from 'css-render' 91 | import bem from '@css-render/plugin-bem' 92 | /** 93 | * CommonJS: 94 | * const { CssRender } = require('css-render') 95 | * const { plugin: bem } = require('@css-render/plugin-bem') 96 | */ 97 | 98 | const cssr = CssRender() 99 | const plugin = bem({ 100 | blockPrefix: '.c-' 101 | }) 102 | cssr.use(plugin) // bind the plugin with the cssr instance 103 | const { 104 | cB, cE, cM 105 | } = plugin 106 | 107 | const style = cB( 108 | 'container', 109 | [ 110 | cE( 111 | 'left, right', 112 | { 113 | width: '50%' 114 | } 115 | ), 116 | cM( 117 | 'dark', 118 | [ 119 | cE( 120 | 'left, right', 121 | { 122 | backgroundColor: 'black' 123 | } 124 | ) 125 | ] 126 | ) 127 | ] 128 | ) 129 | 130 | /** use it as string */ 131 | console.log(style.render()) 132 | /** 133 | * or mount on document.head 134 | * the following lines only works in browser, don't call them in node.js 135 | */ 136 | style.mount() 137 | // ... 138 | style.unmount() 139 | ``` 140 | ```css 141 | .c-container .c-container__left, .c-container .c-container__right { 142 | width: 50%; 143 | } 144 | 145 | .c-container.c-container--dark .c-container__left, .c-container.c-container--dark .c-container__right { 146 | background-color: black; 147 | } 148 | ``` 149 | 150 | ## Vue3 SSR 151 | ```bash 152 | $ npm install --save-dev css-render @css-render/vue3-ssr 153 | ``` 154 | 155 | To make ssr works, you need to make 156 | ```tsx 157 | import { h, createSSRApp, defineComponent } from 'vue' 158 | import { renderToString } from '@vue/server-renderer' 159 | 160 | import { CssRender } from 'css-render' 161 | import { SsrContext, ssrAdapter } from '@css-render/vue3-ssr' 162 | 163 | const Child = defineComponent({ 164 | setup () { 165 | c('div', { 166 | color: 'red' 167 | }).mount({ 168 | id: 'mount-id', 169 | // You need to pass the ssrAdapter to `mount` function 170 | // to make ssr work. 171 | // If you want it work with CSR, just set it to undefined 172 | ssr: ssrAdapter 173 | }) 174 | }, 175 | render () { 176 | return 'Child' 177 | } 178 | }) 179 | 180 | const App = defineComponent({ 181 | render () { 182 | // Wrap the SsrContext at the root of your app 183 | return h(SsrContext, null, { 184 | default: () => h(Child) 185 | }) 186 | } 187 | }) 188 | 189 | const app = createSSRApp(App) 190 | 191 | renderToString(app).then(v => { console.log(v) }) 192 | ``` 193 | 194 | Finally you will find the rendered SSR HTML includes mounted style. 195 | 196 | ## Packages 197 | |Name|Cov| 198 | |-|-| 199 | |css-render|[![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=css-render)](https://codecov.io/gh/07akioni/css-render)| 200 | |@css-render/plugin-bem| [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=plugin-bem)](https://codecov.io/gh/07akioni/css-render)| 201 | |vue3-ssr| [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=vue3-ssr)](https://codecov.io/gh/07akioni/css-render)| -------------------------------------------------------------------------------- /packages/css-render/README.md: -------------------------------------------------------------------------------- 1 | # css-render · [![GitHub Liscense](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/07akioni/css-render/blob/master/LICENSE) [![npm](https://img.shields.io/npm/v/css-render)](https://www.npmjs.com/package/css-render) [![Total alerts](https://img.shields.io/lgtm/alerts/g/07akioni/css-render.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/07akioni/css-render/alerts/) [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&precision=2)](https://codecov.io/gh/07akioni/css-render) 2 | 3 | Generating CSS using JS with considerable flexibility and extensibility, at both server side and client side. 4 | 5 | It's mainly built for **library builders** (who wants make their library work without css import at small overhead). It's not recommend to use it in a webapp. 6 | 7 | It is not designed to totally replace other style-related solutions, but to be a progressive tool which can just work as a supplementary of your style files or totally replace your `.css` files. 8 | 9 | ## Docs 10 | [css-render](https://css-render.vercel.app/) 11 | 12 | ## Why Using It 13 | 1. You want to ship a library without css at a small price (gzip < 2kb). 14 | 2. Reduce size compared with static css (which contains duplicate logic). 15 | 3. You can't write `sass-like` or `less-like` css-in-js (eg. `mixin` in sass or less). 16 | 4. You want to write style variables in JS. 17 | 5. Support an simple SSR API (now only for vue3). 18 | 19 | ## Comparasion with other CSS-in-JS framework 20 | 21 | Main differences between css-render and styled-component, jss or emotion: 22 | 1. It doesn't do the bindings between components and styles. It is more like a style generator with low level mount and unmount API. 23 | 2. It's easier to write like a sass mixin or less mixin. 24 | 25 | 26 | ## Examples 27 | ### Realword Example 28 | - [XScroll](https://github.com/07akioni/vueuc/blob/main/src/x-scroll/src/index.ts) 29 | - [VirtualList](https://github.com/07akioni/vueuc/blob/main/src/virtual-list/src/VirtualList.ts) 30 | 31 | ### Basic Example 32 | ```bash 33 | $ npm install --save-dev css-render 34 | ``` 35 | ```js 36 | import CssRender from 'css-render' 37 | /** 38 | * CommonJS: 39 | * const { CssRender } = require('css-render') 40 | */ 41 | 42 | const { 43 | c 44 | } = CssRender() 45 | 46 | const style = c('body', ({ props }) => ({ 47 | margin: 0, 48 | backgroundColor: props.backgroundColor 49 | }), [ 50 | c('&.dark', { 51 | backgroundColor: 'black' 52 | }), 53 | c('.container', { 54 | width: '100%' 55 | }) 56 | ]) 57 | 58 | /** use it as string */ 59 | console.log(style.render({ backgroundColor: 'white' })) 60 | /** 61 | * or mount on document.head. (the following lines only work in the browser.) 62 | */ 63 | style.mount() 64 | // ... 65 | style.unmount() 66 | ``` 67 | ```css 68 | body { 69 | margin: 0; 70 | background-color: white; 71 | } 72 | 73 | body.dark { 74 | background-color: black; 75 | } 76 | 77 | body .container { 78 | width: 100%; 79 | } 80 | ``` 81 | 82 | ### BEM Plugin Example 83 | ```bash 84 | $ npm install --save-dev css-render @css-render/plugin-bem 85 | ``` 86 | 87 | You can use bem plugin to generate bem CSS like this: 88 | 89 | ```js 90 | import CssRender from 'css-render' 91 | import bem from '@css-render/plugin-bem' 92 | /** 93 | * CommonJS: 94 | * const { CssRender } = require('css-render') 95 | * const { plugin: bem } = require('@css-render/plugin-bem') 96 | */ 97 | 98 | const cssr = CssRender() 99 | const plugin = bem({ 100 | blockPrefix: '.c-' 101 | }) 102 | cssr.use(plugin) // bind the plugin with the cssr instance 103 | const { 104 | cB, cE, cM 105 | } = plugin 106 | 107 | const style = cB( 108 | 'container', 109 | [ 110 | cE( 111 | 'left, right', 112 | { 113 | width: '50%' 114 | } 115 | ), 116 | cM( 117 | 'dark', 118 | [ 119 | cE( 120 | 'left, right', 121 | { 122 | backgroundColor: 'black' 123 | } 124 | ) 125 | ] 126 | ) 127 | ] 128 | ) 129 | 130 | /** use it as string */ 131 | console.log(style.render()) 132 | /** 133 | * or mount on document.head 134 | * the following lines only works in browser, don't call them in node.js 135 | */ 136 | style.mount() 137 | // ... 138 | style.unmount() 139 | ``` 140 | ```css 141 | .c-container .c-container__left, .c-container .c-container__right { 142 | width: 50%; 143 | } 144 | 145 | .c-container.c-container--dark .c-container__left, .c-container.c-container--dark .c-container__right { 146 | background-color: black; 147 | } 148 | ``` 149 | 150 | ## Vue3 SSR 151 | ```bash 152 | $ npm install --save-dev css-render @css-render/vue3-ssr 153 | ``` 154 | 155 | To make ssr works, you need to make 156 | ```tsx 157 | import { h, createSSRApp, defineComponent } from 'vue' 158 | import { renderToString } from '@vue/server-renderer' 159 | 160 | import { CssRender } from 'css-render' 161 | import { SsrContext, ssrAdapter } from '@css-render/vue3-ssr' 162 | 163 | const Child = defineComponent({ 164 | setup () { 165 | c('div', { 166 | color: 'red' 167 | }).mount({ 168 | id: 'mount-id', 169 | // You need to pass the ssrAdapter to `mount` function 170 | // to make ssr work. 171 | // If you want it work with CSR, just set it to undefined 172 | ssr: ssrAdapter 173 | }) 174 | }, 175 | render () { 176 | return 'Child' 177 | } 178 | }) 179 | 180 | const App = defineComponent({ 181 | render () { 182 | // Wrap the SsrContext at the root of your app 183 | return h(SsrContext, null, { 184 | default: () => h(Child) 185 | }) 186 | } 187 | }) 188 | 189 | const app = createSSRApp(App) 190 | 191 | renderToString(app).then(v => { console.log(v) }) 192 | ``` 193 | 194 | Finally you will find the rendered SSR HTML includes mounted style. 195 | 196 | ## Packages 197 | |Name|Cov| 198 | |-|-| 199 | |css-render|[![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=css-render)](https://codecov.io/gh/07akioni/css-render)| 200 | |@css-render/plugin-bem| [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=plugin-bem)](https://codecov.io/gh/07akioni/css-render)| 201 | |vue3-ssr| [![codecov](https://codecov.io/gh/07akioni/css-render/branch/master/graph/badge.svg?token=28OJZAHLK4&flag=vue3-ssr)](https://codecov.io/gh/07akioni/css-render)| -------------------------------------------------------------------------------- /packages/vue3-ssr/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@css-render/vue3-ssr", 3 | "entries": [ 4 | { 5 | "version": "0.15.14", 6 | "tag": "@css-render/vue3-ssr_v0.15.14", 7 | "date": "Sun, 05 May 2024 06:34:46 GMT", 8 | "comments": {} 9 | }, 10 | { 11 | "version": "0.15.13", 12 | "tag": "@css-render/vue3-ssr_v0.15.13", 13 | "date": "Sun, 05 May 2024 05:43:33 GMT", 14 | "comments": {} 15 | }, 16 | { 17 | "version": "0.15.12", 18 | "tag": "@css-render/vue3-ssr_v0.15.12", 19 | "date": "Sat, 24 Dec 2022 17:17:08 GMT", 20 | "comments": {} 21 | }, 22 | { 23 | "version": "0.15.11", 24 | "tag": "@css-render/vue3-ssr_v0.15.11", 25 | "date": "Sun, 21 Aug 2022 07:56:48 GMT", 26 | "comments": {} 27 | }, 28 | { 29 | "version": "0.15.10", 30 | "tag": "@css-render/vue3-ssr_v0.15.10", 31 | "date": "Wed, 23 Mar 2022 12:33:17 GMT", 32 | "comments": { 33 | "none": [ 34 | { 35 | "comment": "Fix useSsrAdapter may return non-undefined value in browser." 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | "version": "0.15.9", 42 | "tag": "@css-render/vue3-ssr_v0.15.9", 43 | "date": "Sat, 05 Mar 2022 14:58:36 GMT", 44 | "comments": {} 45 | }, 46 | { 47 | "version": "0.15.8", 48 | "tag": "@css-render/vue3-ssr_v0.15.8", 49 | "date": "Tue, 28 Dec 2021 18:59:30 GMT", 50 | "comments": {} 51 | }, 52 | { 53 | "version": "0.15.7", 54 | "tag": "@css-render/vue3-ssr_v0.15.7", 55 | "date": "Tue, 28 Dec 2021 17:54:23 GMT", 56 | "comments": {} 57 | }, 58 | { 59 | "version": "0.15.6", 60 | "tag": "@css-render/vue3-ssr_v0.15.6", 61 | "date": "Thu, 02 Sep 2021 15:52:34 GMT", 62 | "comments": {} 63 | }, 64 | { 65 | "version": "0.15.5", 66 | "tag": "@css-render/vue3-ssr_v0.15.5", 67 | "date": "Sat, 24 Jul 2021 17:34:06 GMT", 68 | "comments": { 69 | "none": [ 70 | { 71 | "comment": "Update readme" 72 | } 73 | ] 74 | } 75 | }, 76 | { 77 | "version": "0.15.4", 78 | "tag": "@css-render/vue3-ssr_v0.15.4", 79 | "date": "Sun, 13 Jun 2021 04:56:14 GMT", 80 | "comments": { 81 | "none": [ 82 | { 83 | "comment": "(breaking) Remove `ssrAdapter` exports" 84 | } 85 | ] 86 | } 87 | }, 88 | { 89 | "version": "0.15.3", 90 | "tag": "@css-render/vue3-ssr_v0.15.3", 91 | "date": "Sat, 12 Jun 2021 09:50:10 GMT", 92 | "comments": { 93 | "none": [ 94 | { 95 | "comment": "Add useSsrAdapter hook" 96 | } 97 | ] 98 | } 99 | }, 100 | { 101 | "version": "0.15.2", 102 | "tag": "@css-render/vue3-ssr_v0.15.2", 103 | "date": "Wed, 02 Jun 2021 04:15:14 GMT", 104 | "comments": {} 105 | }, 106 | { 107 | "version": "0.15.1", 108 | "tag": "@css-render/vue3-ssr_v0.15.1", 109 | "date": "Tue, 01 Jun 2021 13:35:47 GMT", 110 | "comments": { 111 | "none": [ 112 | { 113 | "comment": "no main & module in pkg.json" 114 | } 115 | ] 116 | } 117 | }, 118 | { 119 | "version": "0.15.0", 120 | "tag": "@css-render/vue3-ssr_v0.15.0", 121 | "date": "Tue, 01 Jun 2021 13:10:25 GMT", 122 | "comments": { 123 | "none": [ 124 | { 125 | "comment": "support stream ssr" 126 | } 127 | ] 128 | } 129 | }, 130 | { 131 | "version": "0.14.1", 132 | "tag": "@css-render/vue3-ssr_v0.14.1", 133 | "date": "Wed, 19 May 2021 05:06:20 GMT", 134 | "comments": {} 135 | }, 136 | { 137 | "version": "0.14.0", 138 | "tag": "@css-render/vue3-ssr_v0.14.0", 139 | "date": "Wed, 19 May 2021 05:03:05 GMT", 140 | "comments": {} 141 | }, 142 | { 143 | "version": "0.13.9", 144 | "tag": "@css-render/vue3-ssr_v0.13.9", 145 | "date": "Sun, 16 May 2021 19:46:15 GMT", 146 | "comments": {} 147 | }, 148 | { 149 | "version": "0.13.8", 150 | "tag": "@css-render/vue3-ssr_v0.13.8", 151 | "date": "Wed, 12 May 2021 11:45:08 GMT", 152 | "comments": {} 153 | }, 154 | { 155 | "version": "0.13.7", 156 | "tag": "@css-render/vue3-ssr_v0.13.7", 157 | "date": "Wed, 12 May 2021 11:35:11 GMT", 158 | "comments": {} 159 | }, 160 | { 161 | "version": "0.13.6", 162 | "tag": "@css-render/vue3-ssr_v0.13.6", 163 | "date": "Wed, 14 Apr 2021 17:53:56 GMT", 164 | "comments": {} 165 | }, 166 | { 167 | "version": "0.13.5", 168 | "tag": "@css-render/vue3-ssr_v0.13.5", 169 | "date": "Wed, 14 Apr 2021 17:06:44 GMT", 170 | "comments": { 171 | "none": [ 172 | { 173 | "comment": "feat(plugin-bem): add bPrefix prop" 174 | } 175 | ] 176 | } 177 | }, 178 | { 179 | "version": "0.13.4", 180 | "tag": "@css-render/vue3-ssr_v0.13.4", 181 | "date": "Wed, 14 Apr 2021 16:25:11 GMT", 182 | "comments": {} 183 | }, 184 | { 185 | "version": "0.13.3", 186 | "tag": "@css-render/vue3-ssr_v0.13.3", 187 | "date": "Wed, 14 Apr 2021 16:24:04 GMT", 188 | "comments": {} 189 | }, 190 | { 191 | "version": "0.13.2", 192 | "tag": "@css-render/vue3-ssr_v0.13.2", 193 | "date": "Fri, 26 Feb 2021 08:24:05 GMT", 194 | "comments": {} 195 | }, 196 | { 197 | "version": "0.13.1", 198 | "tag": "@css-render/vue3-ssr_v0.13.1", 199 | "date": "Fri, 26 Feb 2021 08:18:28 GMT", 200 | "comments": { 201 | "none": [ 202 | { 203 | "comment": "No changes" 204 | } 205 | ] 206 | } 207 | }, 208 | { 209 | "version": "0.13.0", 210 | "tag": "@css-render/vue3-ssr_v0.13.0", 211 | "date": "Wed, 24 Feb 2021 09:35:28 GMT", 212 | "comments": { 213 | "minor": [ 214 | { 215 | "comment": "Add SsrContext component & ssrAdapter" 216 | } 217 | ], 218 | "dependency": [ 219 | { 220 | "comment": "Updating dependency \"css-render\" from `~0.12.0` to `~0.13.0`" 221 | } 222 | ] 223 | } 224 | } 225 | ] 226 | } 227 | -------------------------------------------------------------------------------- /packages/css-render/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-render", 3 | "entries": [ 4 | { 5 | "version": "0.15.14", 6 | "tag": "css-render_v0.15.14", 7 | "date": "Sun, 05 May 2024 06:34:46 GMT", 8 | "comments": {} 9 | }, 10 | { 11 | "version": "0.15.13", 12 | "tag": "css-render_v0.15.13", 13 | "date": "Sun, 05 May 2024 05:43:33 GMT", 14 | "comments": {} 15 | }, 16 | { 17 | "version": "0.15.12", 18 | "tag": "css-render_v0.15.12", 19 | "date": "Sat, 24 Dec 2022 17:17:08 GMT", 20 | "comments": {} 21 | }, 22 | { 23 | "version": "0.15.11", 24 | "tag": "css-render_v0.15.11", 25 | "date": "Sun, 21 Aug 2022 07:56:48 GMT", 26 | "comments": { 27 | "none": [ 28 | { 29 | "comment": "Limit scope of style id check" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "version": "0.15.10", 36 | "tag": "css-render_v0.15.10", 37 | "date": "Wed, 23 Mar 2022 12:33:17 GMT", 38 | "comments": {} 39 | }, 40 | { 41 | "version": "0.15.9", 42 | "tag": "css-render_v0.15.9", 43 | "date": "Sat, 05 Mar 2022 14:58:36 GMT", 44 | "comments": {} 45 | }, 46 | { 47 | "version": "0.15.8", 48 | "tag": "css-render_v0.15.8", 49 | "date": "Tue, 28 Dec 2021 18:59:30 GMT", 50 | "comments": {} 51 | }, 52 | { 53 | "version": "0.15.7", 54 | "tag": "css-render_v0.15.7", 55 | "date": "Tue, 28 Dec 2021 17:54:23 GMT", 56 | "comments": { 57 | "none": [ 58 | { 59 | "comment": "support metaAnchorName on mount" 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "version": "0.15.6", 66 | "tag": "css-render_v0.15.6", 67 | "date": "Thu, 02 Sep 2021 15:52:34 GMT", 68 | "comments": { 69 | "none": [ 70 | { 71 | "comment": "fix: mount before link element when head is true" 72 | } 73 | ] 74 | } 75 | }, 76 | { 77 | "version": "0.15.5", 78 | "tag": "css-render_v0.15.5", 79 | "date": "Sat, 24 Jul 2021 17:34:06 GMT", 80 | "comments": {} 81 | }, 82 | { 83 | "version": "0.15.4", 84 | "tag": "css-render_v0.15.4", 85 | "date": "Sun, 13 Jun 2021 04:56:14 GMT", 86 | "comments": { 87 | "none": [ 88 | { 89 | "comment": "Add `exists` function to check if style is mounted" 90 | } 91 | ] 92 | } 93 | }, 94 | { 95 | "version": "0.15.3", 96 | "tag": "css-render_v0.15.3", 97 | "date": "Sat, 12 Jun 2021 09:50:10 GMT", 98 | "comments": {} 99 | }, 100 | { 101 | "version": "0.15.2", 102 | "tag": "css-render_v0.15.2", 103 | "date": "Wed, 02 Jun 2021 04:15:14 GMT", 104 | "comments": { 105 | "none": [ 106 | { 107 | "comment": "fix throwing error in node env" 108 | } 109 | ] 110 | } 111 | }, 112 | { 113 | "version": "0.15.1", 114 | "tag": "css-render_v0.15.1", 115 | "date": "Tue, 01 Jun 2021 13:35:47 GMT", 116 | "comments": {} 117 | }, 118 | { 119 | "version": "0.15.0", 120 | "tag": "css-render_v0.15.0", 121 | "date": "Tue, 01 Jun 2021 13:10:25 GMT", 122 | "comments": {} 123 | }, 124 | { 125 | "version": "0.14.1", 126 | "tag": "css-render_v0.14.1", 127 | "date": "Wed, 19 May 2021 05:06:20 GMT", 128 | "comments": {} 129 | }, 130 | { 131 | "version": "0.14.0", 132 | "tag": "css-render_v0.14.0", 133 | "date": "Wed, 19 May 2021 05:03:05 GMT", 134 | "comments": { 135 | "none": [ 136 | { 137 | "comment": "Remove count prop in mount option & unmount option. Rename boost prop in mount option to silent" 138 | } 139 | ] 140 | } 141 | }, 142 | { 143 | "version": "0.13.9", 144 | "tag": "css-render_v0.13.9", 145 | "date": "Sun, 16 May 2021 19:46:15 GMT", 146 | "comments": { 147 | "none": [ 148 | { 149 | "comment": "add option.force for mount" 150 | } 151 | ] 152 | } 153 | }, 154 | { 155 | "version": "0.13.8", 156 | "tag": "css-render_v0.13.8", 157 | "date": "Wed, 12 May 2021 11:45:08 GMT", 158 | "comments": { 159 | "none": [ 160 | { 161 | "comment": "Fix render multiple times when mount with same id" 162 | } 163 | ] 164 | } 165 | }, 166 | { 167 | "version": "0.13.7", 168 | "tag": "css-render_v0.13.7", 169 | "date": "Wed, 12 May 2021 11:35:11 GMT", 170 | "comments": {} 171 | }, 172 | { 173 | "version": "0.13.6", 174 | "tag": "css-render_v0.13.6", 175 | "date": "Wed, 14 Apr 2021 17:53:56 GMT", 176 | "comments": {} 177 | }, 178 | { 179 | "version": "0.13.5", 180 | "tag": "css-render_v0.13.5", 181 | "date": "Wed, 14 Apr 2021 17:06:44 GMT", 182 | "comments": { 183 | "none": [ 184 | { 185 | "comment": "feat(plugin-bem): add bPrefix prop" 186 | } 187 | ] 188 | } 189 | }, 190 | { 191 | "version": "0.13.4", 192 | "tag": "css-render_v0.13.4", 193 | "date": "Wed, 14 Apr 2021 16:25:11 GMT", 194 | "comments": {} 195 | }, 196 | { 197 | "version": "0.13.3", 198 | "tag": "css-render_v0.13.3", 199 | "date": "Wed, 14 Apr 2021 16:24:04 GMT", 200 | "comments": { 201 | "none": [ 202 | { 203 | "comment": "feat(mount): add `head` option to insert style before every style node in doc.head" 204 | } 205 | ] 206 | } 207 | }, 208 | { 209 | "version": "0.13.2", 210 | "tag": "css-render_v0.13.2", 211 | "date": "Fri, 26 Feb 2021 08:24:05 GMT", 212 | "comments": {} 213 | }, 214 | { 215 | "version": "0.13.1", 216 | "tag": "css-render_v0.13.1", 217 | "date": "Fri, 26 Feb 2021 08:18:28 GMT", 218 | "comments": { 219 | "none": [ 220 | { 221 | "comment": "Deprecate MountOptions.count & UnmountOptions.count" 222 | } 223 | ] 224 | } 225 | }, 226 | { 227 | "version": "0.13.0", 228 | "tag": "css-render_v0.13.0", 229 | "date": "Wed, 24 Feb 2021 09:35:28 GMT", 230 | "comments": { 231 | "minor": [ 232 | { 233 | "comment": "Support ssrAdapter in mount" 234 | } 235 | ] 236 | } 237 | } 238 | ] 239 | } 240 | -------------------------------------------------------------------------------- /packages/plugin-bem/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import CssRender from 'css-render' 2 | import { assertEqual } from '@css-render/test-shared' 3 | import CssRenderBEMPlugin from '../index' 4 | 5 | const cssr = CssRender({ 6 | keepEmptyBlock: true 7 | }) 8 | const plugin = CssRenderBEMPlugin({ 9 | blockPrefix: '.c-', 10 | elementPrefix: '__', 11 | modifierPrefix: '--' 12 | }) 13 | cssr.use(plugin) 14 | 15 | const { c } = cssr 16 | 17 | const { 18 | cB, 19 | cE, 20 | cM, 21 | cNotM 22 | } = plugin 23 | 24 | describe('default prefixes', function () { 25 | it('should use default prefixes', function () { 26 | const plugin = CssRenderBEMPlugin({}) 27 | cssr.use(plugin) 28 | const { 29 | cB, 30 | cE, 31 | cM, 32 | cNotM 33 | } = plugin 34 | assertEqual( 35 | cB('b', [ 36 | cM('m', {}), 37 | cNotM('nm', {}), 38 | cE('e', [ 39 | cM('m', {}), 40 | cNotM('nm', {}) 41 | ]) 42 | ]).render(), 43 | ` 44 | .b.b--m {} 45 | .b:not(.b--nm) {} 46 | .b .b__e.b__e--m {} 47 | .b .b__e:not(.b__e--nm) {} 48 | ` 49 | ) 50 | }) 51 | it('should use default prefixes', function () { 52 | const plugin = CssRenderBEMPlugin() 53 | cssr.use(plugin) 54 | const { 55 | cB, 56 | cE, 57 | cM, 58 | cNotM 59 | } = plugin 60 | assertEqual( 61 | cB('b', [ 62 | cM('m', {}), 63 | cNotM('nm', {}), 64 | cE('e', [ 65 | cM('m', {}), 66 | cNotM('nm', {}) 67 | ]) 68 | ]).render(), 69 | ` 70 | .b.b--m {} 71 | .b:not(.b--nm) {} 72 | .b .b__e.b__e--m {} 73 | .b .b__e:not(.b__e--nm) {} 74 | ` 75 | ) 76 | }) 77 | }) 78 | 79 | describe('# bem.b', function () { 80 | it('should use blockPrefix', function () { 81 | assertEqual( 82 | cB('b', {}).render(), 83 | '.c-b {}' 84 | ) 85 | }) 86 | it('should generate correct selector when nested', function () { 87 | assertEqual( 88 | cB( 89 | 'b', 90 | [cB('b2', {})] 91 | ).render(), 92 | '.c-b .c-b2 {}' 93 | ) 94 | }) 95 | }) 96 | 97 | describe('# bem.e', function () { 98 | it('should work with bem.b', function () { 99 | assertEqual( 100 | cB('b', [cE('e', {})]).render(), 101 | '.c-b .c-b__e {}' 102 | ) 103 | }) 104 | it('should work with comma selector', function () { 105 | assertEqual( 106 | cB('b', [cE('e1, e2', {})]).render(), 107 | '.c-b .c-b__e1, .c-b .c-b__e2 {}' 108 | ) 109 | }) 110 | it('should work with nested e', function () { 111 | assertEqual( 112 | cB('b', [ 113 | cE('e1', {}, [ 114 | cM('m1', {}), 115 | cE('e2', {}, [ 116 | cM('m2', {}) 117 | ]) 118 | ]) 119 | ]).render(), 120 | ` 121 | .c-b .c-b__e1 {} 122 | .c-b .c-b__e1.c-b__e1--m1 {} 123 | .c-b .c-b__e1 .c-b__e2 {} 124 | .c-b .c-b__e1 .c-b__e2.c-b__e2--m2 {} 125 | ` 126 | ) 127 | assertEqual( 128 | cB('b', [ 129 | cE('e1', [ 130 | c('& +', [ 131 | cE('e2', [ 132 | cM('m2', {}) 133 | ]) 134 | ]) 135 | ]) 136 | ]).render(), 137 | ` 138 | .c-b .c-b__e1 + .c-b__e2.c-b__e2--m2 {} 139 | ` 140 | ) 141 | }) 142 | }) 143 | 144 | describe('# bem.m', function () { 145 | it('should work with bem.b', function () { 146 | assertEqual( 147 | cB('b', [cM('m', {})]).render(), 148 | '.c-b.c-b--m {}' 149 | ) 150 | }) 151 | it('should work with bem.e', function () { 152 | assertEqual( 153 | cB('b', [cE('e', [cM('m', {})])]).render(), 154 | '.c-b .c-b__e.c-b__e--m {}' 155 | ) 156 | }) 157 | it('should work with comma selector', function () { 158 | assertEqual( 159 | cB('b', [cM('m1, m2', {})]).render(), 160 | '.c-b.c-b--m1, .c-b.c-b--m2 {}' 161 | ) 162 | assertEqual( 163 | cB('b', [cE('e', [cM('m1, m2', {})])]).render(), 164 | '.c-b .c-b__e.c-b__e--m1, .c-b .c-b__e.c-b__e--m2 {}' 165 | ) 166 | }) 167 | }) 168 | 169 | describe('# bem.notM', function () { 170 | it('should work with bem.b', function () { 171 | assertEqual( 172 | cB('b', [cNotM('m', {})]).render(), 173 | '.c-b:not(.c-b--m) {}' 174 | ) 175 | }) 176 | it('should work with bem.e', function () { 177 | assertEqual( 178 | cB('b', [cE('e', [cNotM('m', {})])]).render(), 179 | '.c-b .c-b__e:not(.c-b__e--m) {}' 180 | ) 181 | }) 182 | }) 183 | 184 | describe('# bem', function () { 185 | it('should pass test case#1', function () { 186 | assertEqual( 187 | cB( 188 | 'container', 189 | [ 190 | cE( 191 | 'left, right', 192 | { 193 | width: '50%' 194 | } 195 | ), 196 | cM( 197 | 'dark', 198 | [ 199 | cE( 200 | 'left, right', 201 | { 202 | backgroundColor: 'black' 203 | } 204 | ) 205 | ] 206 | ) 207 | ] 208 | ).render(), 209 | `.c-container .c-container__left, .c-container .c-container__right { 210 | width: 50%; 211 | } 212 | 213 | .c-container.c-container--dark .c-container__left, 214 | .c-container.c-container--dark .c-container__right { 215 | background-color: black; 216 | }` 217 | ) 218 | }) 219 | }) 220 | 221 | describe('# custom bPrefix', function () { 222 | const plugin = CssRenderBEMPlugin() 223 | cssr.use(plugin) 224 | const { 225 | cB, 226 | cE, 227 | cM, 228 | cNotM 229 | } = plugin 230 | assertEqual( 231 | cB('b', [ 232 | cM('m', {}), 233 | cNotM('nm', {}), 234 | cE('e', [ 235 | cM('m', {}), 236 | cNotM('nm', {}) 237 | ]) 238 | ]).render({ 239 | bPrefix: '.x-' 240 | }), 241 | ` 242 | .x-b.x-b--m {} 243 | .x-b:not(.x-b--nm) {} 244 | .x-b .x-b__e.x-b__e--m {} 245 | .x-b .x-b__e:not(.x-b__e--nm) {} 246 | ` 247 | ) 248 | }) 249 | 250 | describe('# function typed selector', function () { 251 | it('should pass test case#1', function () { 252 | assertEqual( 253 | cB(({ props }) => props.value as string + 'block', { 254 | key: 'value' 255 | }, [ 256 | cE(({ props }) => props.value as string + 'el', { 257 | key: 'value' 258 | }, [ 259 | cM(({ props }) => props.value as string + 'm', { 260 | key: 'value' 261 | }), 262 | cNotM(({ props }) => props.value as string + 'nm', { 263 | key: 'value' 264 | }) 265 | ]) 266 | ]).render({ 267 | value: 'propValue' 268 | }), 269 | ` 270 | .c-propValueblock { 271 | key: value; 272 | } 273 | 274 | .c-propValueblock .c-propValueblock__propValueel { 275 | key: value; 276 | } 277 | 278 | .c-propValueblock .c-propValueblock__propValueel.c-propValueblock__propValueel--propValuem { 279 | key: value; 280 | } 281 | 282 | .c-propValueblock .c-propValueblock__propValueel:not(.c-propValueblock__propValueel--propValuenm) { 283 | key: value; 284 | } 285 | `) 286 | }) 287 | }) 288 | 289 | describe('error info', () => { 290 | describe('shouldn\'t allow using modifier inside multiple elements', () => { 291 | it('#case m', (done) => { 292 | try { 293 | cB('b', [cE('e, e', [cM('m', {})])]).render() 294 | } catch (e) { 295 | done() 296 | } 297 | }) 298 | it('#case not m', (done) => { 299 | try { 300 | cB('b', [cE('e, e', [cNotM('m', {})])]).render() 301 | } catch (e) { 302 | done() 303 | } 304 | }) 305 | }) 306 | }) 307 | -------------------------------------------------------------------------------- /packages/css-render/__tests__/mount/index.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import * as chai from 'chai' 3 | import CssRender, { exists } from '../../src' 4 | import { SinonSpy, spy } from 'sinon' 5 | 6 | const expect = chai.expect 7 | const cssr = CssRender() 8 | 9 | const { c } = cssr 10 | 11 | describe('# mount with no id', () => { 12 | let sandbox: HTMLElement 13 | const style = c('.red-block', { 14 | backgroundColor: 'red' 15 | }) 16 | before(() => { 17 | style.mount() 18 | sandbox = document.createElement('div') 19 | document.body.appendChild(sandbox) 20 | }) 21 | beforeEach(() => { 22 | spy(console, 'error') 23 | }) 24 | afterEach(() => { 25 | (console.error as SinonSpy).restore() 26 | sandbox.innerHTML = '' 27 | }) 28 | it('should mount only once', () => { 29 | expect(document.querySelectorAll('[cssr-id]').length).to.equal(1) 30 | style.mount() 31 | expect(document.querySelectorAll('[cssr-id]').length).to.equal(1) 32 | }) 33 | it('should make element styled', () => { 34 | sandbox.innerHTML = '
' 35 | const backgroundColor = getComputedStyle( 36 | sandbox.children[0] 37 | ).backgroundColor 38 | expect(backgroundColor).to.equal('rgb(255, 0, 0)') 39 | }) 40 | it('should support props of render', () => { 41 | sandbox.innerHTML = '' 42 | const style = c('sel1', ({ props }) => { 43 | return { 44 | backgroundColor: props.color 45 | } 46 | }) 47 | style.mount({ 48 | props: { 49 | color: 'red' 50 | } 51 | }) 52 | const backgroundColor = getComputedStyle( 53 | sandbox.children[0] 54 | ).backgroundColor 55 | expect(backgroundColor).to.equal('rgb(255, 0, 0)') 56 | }) 57 | after(() => { 58 | document.body.removeChild(sandbox) 59 | style.unmount() 60 | }) 61 | }) 62 | 63 | describe('# mount & unmount with id (suite 1)', () => { 64 | const style = c('.red-block', { 65 | backgroundColor: 'red' 66 | }) 67 | before(() => { 68 | style.mount({ 69 | id: 'test-id-1' 70 | }) 71 | style.mount({ 72 | id: 'test-id-2' 73 | }) 74 | style.mount({ 75 | id: '14138' 76 | }) 77 | style.mount({ 78 | id: '14139' 79 | }) 80 | }) 81 | beforeEach(() => { 82 | spy(console, 'error') 83 | }) 84 | afterEach(() => { 85 | (console.error as SinonSpy).restore() 86 | }) 87 | it('should work in no-count mode', () => { 88 | expect(exists('jjy')).to.eq(false) 89 | style.mount({ id: 'jjy' }) 90 | expect(exists('jjy')).to.eq(true) 91 | expect(document.head.querySelector('[cssr-id="jjy"]')).not.to.eq(null) 92 | style.unmount({ id: 'jjy' }) 93 | expect(document.head.querySelector('[cssr-id="jjy"]')).to.eq(null) 94 | }) 95 | it('should mount desired style element on head', () => { 96 | expect(document.head.querySelector('[cssr-id="test-id-1"]')).not.to.equal( 97 | null 98 | ) 99 | expect(document.head.querySelector('[cssr-id="14138"]')).not.to.equal(null) 100 | }) 101 | it('should do nothing when unmount with an not exist id', () => { 102 | style.unmount({ 103 | id: 'letitbe' 104 | }) 105 | expect(style.els.length).to.equal(4) 106 | }) 107 | it('should unmount the desired style element', () => { 108 | style.unmount({ 109 | id: 'test-id-1' 110 | }) 111 | style.unmount({ 112 | id: '14138' 113 | }) 114 | expect(document.head.querySelector('[cssr-id="test-id-1"]')).to.equal(null) 115 | expect(document.head.querySelector('[cssr-id="test-id-2"]')).not.to.equal( 116 | null 117 | ) 118 | expect(document.head.querySelector('[cssr-id="14138"]')).to.equal(null) 119 | expect(document.head.querySelector('[cssr-id="14139"]')).not.to.equal(null) 120 | expect(style.els.length).to.equal(2) 121 | }) 122 | it('should unmount all related styles elements', () => { 123 | style.unmount() 124 | expect(style.els.length).to.equal(0) 125 | }) 126 | after(() => { 127 | style.unmount() 128 | }) 129 | }) 130 | 131 | describe('# mount & unmount with id (suite 2)', function () { 132 | const style = c('.red-block', { 133 | backgroundColor: 'red' 134 | }) 135 | beforeEach(() => { 136 | spy(console, 'error') 137 | }) 138 | afterEach(() => { 139 | (console.error as SinonSpy).restore() 140 | style.unmount() 141 | }) 142 | it('should mount same element with same id', function () { 143 | const el = style.mount({ 144 | id: '14141' 145 | }) 146 | const el2 = style.mount({ 147 | id: '14141' 148 | }) 149 | expect(el).to.equal(el2) 150 | }) 151 | }) 152 | 153 | describe('head', () => { 154 | const oldStyle = document.createElement('style') 155 | oldStyle.textContent = '.old-style { color: red; }' 156 | document.head.appendChild(oldStyle) 157 | const style = c('.old-style', 'color: blue;') 158 | const el = document.createElement('div') 159 | el.classList.add('old-style') 160 | document.body.appendChild(el) 161 | it("doesn't affect old style when mount with `head`", () => { 162 | style.mount({ id: 'head-test-head', head: true }) 163 | expect(getComputedStyle(document.querySelector('.old-style')!).color).to.eq( 164 | 'rgb(255, 0, 0)' 165 | ) 166 | style.mount({ id: 'head-test-no-head' }) 167 | expect(getComputedStyle(document.querySelector('.old-style')!).color).to.eq( 168 | 'rgb(0, 0, 255)' 169 | ) 170 | }) 171 | it('mount before link element', () => { 172 | document.head.insertBefore( 173 | document.createElement('link'), 174 | document.head.firstElementChild 175 | ) 176 | style.mount({ id: 'gogogo', head: true }) 177 | expect(document.head.firstElementChild?.getAttribute('cssr-id')).to.equal( 178 | 'gogogo' 179 | ) 180 | }) 181 | }) 182 | 183 | describe('force', () => { 184 | it('works', () => { 185 | const divA = document.createElement('div') 186 | divA.classList.add('a') 187 | document.body.appendChild(divA) 188 | const style = c('.a', ({ props }) => ({ 189 | color: props.color 190 | })) 191 | style.mount({ 192 | id: 'force', 193 | props: { 194 | color: 'red' 195 | }, 196 | force: true 197 | }) 198 | expect(getComputedStyle(document.querySelector('.a')!).color).to.equal( 199 | 'rgb(255, 0, 0)' 200 | ) 201 | style.mount({ 202 | id: 'force', 203 | props: { 204 | color: 'rgb(0, 255, 0)' 205 | }, 206 | force: true 207 | }) 208 | expect(getComputedStyle(document.querySelector('.a')!).color).to.equal( 209 | 'rgb(0, 255, 0)' 210 | ) 211 | }) 212 | }) 213 | 214 | describe('anchorMetaName', () => { 215 | const metaA = document.createElement('meta') 216 | metaA.name = 'metaA' 217 | const metaB = document.createElement('meta') 218 | metaB.name = 'metaB' 219 | const metaC = document.createElement('meta') 220 | metaC.name = 'metaC' 221 | document.head.appendChild(metaA) 222 | document.head.appendChild(metaB) 223 | document.head.appendChild(metaC) 224 | 225 | const style = c('.a', { color: 'green' }) 226 | style.mount({ 227 | id: 'gigigi', 228 | anchorMetaName: 'metaB' 229 | }) 230 | expect(document.head.querySelector('[cssr-id="gigigi"] + *')?.getAttribute('name')).to.equal('metaB') 231 | style.mount({ 232 | id: 'gigigi2', 233 | anchorMetaName: 'metaB' 234 | }) 235 | expect(document.head.querySelector('[cssr-id="gigigi"] + *')?.getAttribute('cssr-id')).to.equal('gigigi2') 236 | expect(document.querySelector('[cssr-id="gigigi2"] + *')?.getAttribute('name')).to.equal('metaB') 237 | style.unmount() 238 | console.log(document.head) 239 | }) 240 | 241 | describe('shadow root', () => { 242 | it('style mounted on head and shadow root not affect each other', () => { 243 | const outerDiv = document.createElement('div') 244 | outerDiv.classList.add('a') 245 | document.body.appendChild(outerDiv) 246 | const innerDiv = outerDiv.cloneNode(true) 247 | const shadow = outerDiv.attachShadow({mode: 'open'}) 248 | shadow.appendChild(innerDiv) 249 | 250 | const style = c('.a', ({ props }) => ({ 251 | backgroundColor: props.backgroundColor 252 | })) 253 | style.mount({ 254 | id: 'outer', 255 | props: { 256 | backgroundColor: 'red' 257 | } 258 | }) 259 | style.mount({ 260 | id: 'inner', 261 | props: { 262 | backgroundColor: 'lime' 263 | }, 264 | parent: shadow 265 | }) 266 | expect(getComputedStyle(document.querySelector('.a')!).backgroundColor).to.equal( 267 | 'rgb(255, 0, 0)' 268 | ) 269 | expect(getComputedStyle(shadow.querySelector('.a')!).backgroundColor).to.equal( 270 | 'rgb(0, 255, 0)' 271 | ) 272 | 273 | style.unmount({ 274 | id: 'outer' 275 | }) 276 | style.unmount({ 277 | id: 'inner', 278 | parent: shadow 279 | }) 280 | expect(style.els.length).to.equal(0) 281 | }) 282 | }) 283 | -------------------------------------------------------------------------------- /playground/perf.log: -------------------------------------------------------------------------------- 1 | -----log-start----- 2 | Commit hash : d13ce297b5a013812a0174e098cd792bb77be915 3 | Raw logs : 13.05,13.09,12.22,14.46,12,13.37,11.8,13.32,13.23,11.99,12.01,13.04,11.9,13.27,12.96,11.81,13.28,14.3,13.31,11.62,11.69,11.72,12.92,13.72,12.06,13.22,11.88,11.95,13.04,11.71,12.43,11.97,11.85,12.95,11.91,13.16,12.87,13.99,14.14,14.99,12.74,11.81,11.89,11.83,12.24,12.11,11.83,12.01,12.87,11.96,12.95,14.96,13.01,11.85,11.87,13.08,12.89,12.4,11.89,11.96,15.83,11.93,11.73,11.89,12.53,13.15,11.76,12.98,12.2,11.85,11.8,12.23,13.3,12.41,13.07,12.01,12.32,13.07,12.3,11.85,12.52,11.78,12.03,12.17,12.19,12.16,11.89,12.91,13.19,15.43,20.87,20.61,19.91,11.98,11.86,13.08,12.59,15.94,15.39,16.9, 4 | Max time : 20.87 5 | Min time : 11.62 6 | Average time : 12.7190 7 | ------log-end------ 8 | 9 | -----log-start----- 10 | Commit hash : d13ce297b5a013812a0174e098cd792bb77be915 11 | Raw logs : 17.4,16.04,11.75,13.72,12.12,12,12.75,12.33,12.01,11.71,12.96,12.03,11.74,13.18,11.87,11.81,11.73,11.87,11.67,12.03,11.77,13.03,11.73,13.14,15.32,11.75,11.74,14.19,15.52,12.33,13.08,13.03,11.8,12.9,11.91,12.42,13.31,12,13.2,13.27,11.68,12.95,13.41,11.9,11.86,13.1,11.96,13.01,13.02,12.22,12.42,14.61,13.01,12.9,12.61,11.72,11.82,11.8,12.24,13.1,13.1,11.72,11.72,11.8,16.17,13.31,12.89,12.88,11.92,12.84,12.01,11.61,11.81,11.74,11.52,12.1,11.92,12.67,11.69,11.75,12.04,11.78,11.78,13.02,12.31,13,12.01,12.92,11.7,11.72,11.92,14.68,13.05,13.08,12.89,11.76,11.73,11.97,12,11.68, 12 | Max time : 17.4 13 | Min time : 11.52 14 | Average time : 12.4959 15 | ------log-end------ 16 | 17 | -----log-start----- 18 | Commit hash : d13ce297b5a013812a0174e098cd792bb77be915 19 | Raw logs : 13.34,13.34,12.08,13.56,12.14,13.36,12.11,13.55,14.61,15.77,14.06,12.82,11.98,11.63,11.64,11.9,14.34,12.21,13.15,12.36,12.03,12.72,12.01,12.78,12.81,14.06,11.97,12.48,13.21,11.58,15.06,13.02,13.07,11.82,11.61,11.7,11.89,16.42,13.88,12.61,16.55,12.98,13.95,13.3,12.56,18.28,13.24,11.95,12.23,12.12,12.49,13.4,12,12.06,13.27,12.22,12.69,12.36,12.51,13.24,13.34,13.12,12.3,11.98,11.96,12.07,11.93,13.56,14.67,13.26,12.12,13.5,12.17,13.56,13.03,12.28,12.11,15.02,12.54,13.11,15.15,14.2,14.06,12.64,12.7,13.19,12.91,12.1,13.09,12.12,12.31,12.45,13.1,11.93,13.82,12.1,12.04,13.22,11.89,11.86, 20 | Max time : 18.28 21 | Min time : 11.58 22 | Average time : 12.9137 23 | ------log-end------ 24 | 25 | -----log-start----- 26 | Commit hash : 4587d662c924ab3b7e6ff196c38f96cb9981fdc6 27 | Raw logs : 13.68,12.64,11.82,11.88,12.93,11.89,12.22,14.38,14.17,11.83,11.63,11.78,11.76,11.74,12.12,11.69,11.83,12.97,12.9,14.84,13.19,13.02,12.67,11.86,11.82,14.49,11.82,11.7,12.06,12.81,11.66,12.92,11.84,12.26,11.85,12.63,11.65,11.92,13,11.62,13.08,11.91,11.77,12.36,13,12.97,11.71,11.66,11.83,16.1,11.71,12.78,11.89,11.74,13.08,11.96,14.82,12.86,12.2,11.78,11.67,11.87,12.22,12.26,12.37,11.63,11.8,11.6,11.74,11.72,11.68,11.78,11.75,12.91,12.99,11.97,11.8,12.93,11.8,11.81,13.1,11.87,11.83,13.03,11.91,11.78,11.86,11.68,11.72,13.54,11.76,13.02,11.7,13.13,11.76,11.78,12.95,12.95,11.64,13.02, 28 | Max time : 16.1 29 | Min time : 11.6 30 | Average time : 12.3491 31 | ------log-end------ 32 | 33 | -----log-start----- 34 | Commit hash : 4587d662c924ab3b7e6ff196c38f96cb9981fdc6 35 | Raw logs : 12.35,11.66,11.62,11.96,11.63,11.63,12.05,12.25,13.56,11.63,11.88,11.82,11.84,12.81,12.04,12.75,11.37,12.1,12,11.7,16.44,11.93,11.75,16.73,11.63,11.92,11.78,12.91,11.74,11.54,13.55,14.29,11.64,13.77,11.69,11.69,12.87,11.58,12.07,11.79,11.74,12.56,11.61,11.58,12.88,11.69,13.08,11.62,12.74,11.65,11.78,12.81,12.14,12.45,12.96,11.87,12.77,12.57,14.7,13.08,12.13,12.66,12.06,12.27,12.09,11.89,11.89,11.84,12.51,12.9,11.77,11.67,11.71,12.85,11.59,11.98,11.83,11.8,12.87,12.17,11.9,11.8,11.64,12.86,11.93,11.61,11.96,12.84,11.92,12.7,12.94,12.2,12.87,11.67,11.93,12.82,11.85,11.65,11.66,11.92, 36 | Max time : 16.73 37 | Min time : 11.37 38 | Average time : 12.2639 39 | ------log-end------ 40 | 41 | -----log-start----- 42 | Commit hash : 4587d662c924ab3b7e6ff196c38f96cb9981fdc6 43 | Raw logs : 11.95,13.04,13.43,11.99,12.06,11.98,13.24,12.88,11.98,11.82,11.74,11.75,12.99,12.94,11.79,13.13,12.97,11.62,12.81,11.69,12.56,12.76,11.49,12.11,13.14,11.41,12.15,14.26,12.11,12.73,13.01,12.82,11.78,11.78,12.88,12.08,13.03,12.12,11.63,12.39,13.07,11.72,13.1,11.75,11.88,11.66,11.61,11.71,11.59,13.54,12.81,12.06,11.65,12.55,13.46,11.82,13.23,16.98,11.75,13.21,11.67,11.68,11.76,11.87,11.57,13.21,11.73,11.56,12.65,12.9,12.25,11.83,12.88,12.67,11.76,11.71,11.71,15.09,19.94,17.7,14.85,14.56,11.66,11.72,11.86,14.01,15.71,12.83,12.92,12.8,11.61,13.23,11.94,11.62,12.66,11.74,14.25,11.65,11.74,11.77, 44 | Max time : 19.94 45 | Min time : 11.41 46 | Average time : 12.4879 47 | ------log-end------ 48 | 49 | -----log-start----- 50 | Commit hash : 4587d662c924ab3b7e6ff196c38f96cb9981fdc6 51 | Raw logs : 12.02,12.06,12.79,11.6,13.04,12.81,11.67,12.1,12.79,11.73,11.56,11.81,12.93,13.08,13.14,11.71,12.95,11.66,11.93,11.72,11.65,11.7,11.81,11.73,12.82,11.74,11.87,11.77,12.84,11.77,11.56,11.77,16.92,11.7,11.72,11.8,12.01,12.28,14.22,17.08,11.75,11.79,11.84,11.76,11.63,15.38,14.94,13.02,11.81,11.99,11.83,11.82,12.12,13.3,11.63,11.69,11.64,11.9,11.72,12.95,11.72,12.35,11.76,12.11,12.83,11.61,11.61,11.81,12.6,12.8,11.64,11.79,11.73,12.81,11.98,12.79,14.16,14.12,12.88,12.81,11.6,12.81,11.7,11.72,11.74,12.9,11.94,11.7,11.72,11.71,11.81,13.45,11.74,11.77,11.68,11.87,11.64,11.64,11.83,11.7, 52 | Max time : 17.08 53 | Min time : 11.56 54 | Average time : 12.2225 55 | ------log-end------ 56 | 57 | -----log-start----- 58 | Commit hash : 67d49f0b5b8e0724e075553d1f2dad88d8142882 59 | Raw logs : 11.75,11.8,11.78,12.38,13.05,12.45,22.71,18.03,15.13,11.75,11.52,13.33,11.4,12.12,12.02,13.56,15.51,13.82,13.36,12.39,12.24,12.14,12.97,13.4,12.3,13.06,12.15,13.14,13.3,11.67,14.6,13.63,11.82,11.76,13.16,12.31,12.25,12.18,11.64,11.81,13.28,12.59,11.72,11.73,13.83,14.88,16.63,14.64,12.46,14.67,14.84,11.79,11.52,11.68,12.12,12.22,13.7,12.84,11.81,12.22,13.05,11.47,11.74,11.46,11.71,12.7,12.8,11.54,11.78,13.29,11.81,12.26,12.12,11.69,11.84,12.53,13.46,15.66,12.08,11.81,13.69,13.71,13.04,12.54,11.44,12.2,11.57,12.67,11.92,11.92,12.02,13.59,11.93,11.97,11.81,12.83,11.86,13.62,12.61,13.51, 60 | Max time : 22.71 61 | Min time : 11.4 62 | Average time : 12.6694 63 | ------log-end------ 64 | 65 | -----log-start----- 66 | Commit hash : 67d49f0b5b8e0724e075553d1f2dad88d8142882 67 | Raw logs : 12.21,12.03,12.44,11.36,12.78,11.49,11.55,12.56,11.66,10.99,11.78,12.36,10.99,11.09,11.71,12.17,11.48,11.52,11.09,11.09,12.51,11.2,11.15,12.06,11.47,12.42,10.98,10.99,11.03,12.23,12.38,11.2,11.49,11.39,11.38,11.59,11.19,11.23,11.64,11.48,11.42,11.79,11.27,12.45,11.55,11.38,12.66,11.57,11.26,11.43,11.58,11.33,11.37,11.84,11.25,11.7,12.13,11.39,11.2,11.41,11.86,11.57,12.56,11.51,13.2,11.67,11.23,11.6,11.54,11.68,11.32,12.47,11.65,11.32,12.52,12.52,11.4,11.64,11.52,12.29,12.46,12.76,11.26,11.45,12.35,11.42,11.2,12.53,11.41,11.85,11.52,11.62,12.78,11.47,12.11,12.38,11.35,12.7,11.52,11.43, 68 | Max time : 13.2 69 | Min time : 10.98 70 | Average time : 11.8191 71 | ------log-end------ 72 | 73 | -----log-start----- 74 | Commit hash : 67d49f0b5b8e0724e075553d1f2dad88d8142882 75 | Raw logs : 11.49,12.84,12.3,11.78,12.63,11.68,12.06,11.8,12.42,11.87,12.82,11.69,19.59,22.09,20.77,12.56,11.67,13.8,11.63,11.51,19.09,17.85,19.68,18.47,11.58,12.7,11.4,11.52,11.34,11.32,11.43,12.88,11.33,11.62,11.72,12.5,12.73,11.66,12.65,12.51,12.56,12.78,11.57,12.14,11.52,11.38,12.57,12.01,11.39,11.45,12.34,11.5,12.84,11.85,11.74,13.46,12.67,11.59,11.49,11.42,12.24,12.89,12.55,11.34,11.37,12.53,11.24,12.07,11.57,11.65,12.62,11.51,11.46,11.35,12.41,12.65,11.45,11.25,12.52,11.5,11.5,11.4,11.34,11.5,12.56,11.33,11.58,12.83,11.72,11.52,12.72,12.01,11.55,11.55,12.85,11.32,11.29,11.7,11.37,11.46, 76 | Max time : 22.09 77 | Min time : 11.24 78 | Average time : 12.1134 79 | ------log-end------ 80 | 81 | -----log-start----- 82 | Commit hash : 67d49f0b5b8e0724e075553d1f2dad88d8142882 83 | Raw logs : 12.53,11.81,12.66,12.65,11.69,11.7,13.25,13.29,12.37,12.65,12.63,11.55,11.44,11.51,12.23,11.43,11.45,11.52,11.73,12.85,11.57,11.53,11.6,11.49,11.36,12.46,13.22,11.33,11.78,11.42,12.72,11.45,12.36,11.19,11.5,11.41,11.68,11.53,11.36,12.31,11.5,11.66,12.36,11.81,13.57,12.57,11.42,11.54,13.79,11.81,11.66,12.58,12.25,11.35,13.49,12.76,11.26,11.63,11.41,11.62,11.86,12.58,11.51,12.6,11.21,12.48,11.38,11.37,11.95,11.97,11.26,12.58,11.54,11.57,12.55,11.31,11.73,11.44,11.99,12.97,11.6,11.99,11.77,11.25,11.24,12.79,12.54,11.48,12.54,11.7,11.47,11.45,11.6,11.36,11.42,11.34,12.38,11.47,11.33,14.96, 84 | Max time : 14.96 85 | Min time : 11.19 86 | Average time : 12.0028 87 | ------log-end------ 88 | 89 | -----log-start----- 90 | Commit hash : 67d49f0b5b8e0724e075553d1f2dad88d8142882 91 | Raw logs : 12.54,11.29,12.52,12.56,12.57,13.17,12.49,11.68,11.42,12.12,11.44,11.28,11.5,11.49,11.66,11.59,11.28,12.61,12.56,11.3,11.36,11.46,11.1,12.6,12.61,12.25,11.85,11.55,11.13,14.54,11.47,12.57,11.45,11.43,12.72,11.33,11.58,11.45,12.52,11.24,11.38,11.36,11.12,11.63,12.57,11.36,12.77,11.3,11.8,12.5,11.25,14.48,11.68,11.49,11.33,12.44,11.57,11.37,12.31,11.63,12.52,11.25,12.75,12.54,11.44,12.24,11.51,12.87,11.34,12.16,11.68,12.45,12.55,11.27,11.32,12.31,11.45,11.25,12.42,11.27,12.24,11.85,11.31,11.26,13.89,11.38,11.4,11.47,11.77,11.33,12.54,12.12,11.48,11.34,12.4,11.48,11.4,11.39,11.94,11.34, 92 | Max time : 14.54 93 | Min time : 11.1 94 | Average time : 11.9407 95 | ------log-end------ 96 | 97 | -------------------------------------------------------------------------------- /common/scripts/install-run-rush.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 2 | // 3 | // This script is intended for usage in an automated build environment where the Rush command may not have 4 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 5 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. 6 | // An example usage would be: 7 | // 8 | // node common/scripts/install-run-rush.js install 9 | // 10 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 11 | 12 | /******/ (() => { // webpackBootstrap 13 | /******/ "use strict"; 14 | /******/ var __webpack_modules__ = ({ 15 | 16 | /***/ 657147: 17 | /*!*********************!*\ 18 | !*** external "fs" ***! 19 | \*********************/ 20 | /***/ ((module) => { 21 | 22 | module.exports = require("fs"); 23 | 24 | /***/ }), 25 | 26 | /***/ 371017: 27 | /*!***********************!*\ 28 | !*** external "path" ***! 29 | \***********************/ 30 | /***/ ((module) => { 31 | 32 | module.exports = require("path"); 33 | 34 | /***/ }) 35 | 36 | /******/ }); 37 | /************************************************************************/ 38 | /******/ // The module cache 39 | /******/ var __webpack_module_cache__ = {}; 40 | /******/ 41 | /******/ // The require function 42 | /******/ function __webpack_require__(moduleId) { 43 | /******/ // Check if module is in cache 44 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 45 | /******/ if (cachedModule !== undefined) { 46 | /******/ return cachedModule.exports; 47 | /******/ } 48 | /******/ // Create a new module (and put it into the cache) 49 | /******/ var module = __webpack_module_cache__[moduleId] = { 50 | /******/ // no module.id needed 51 | /******/ // no module.loaded needed 52 | /******/ exports: {} 53 | /******/ }; 54 | /******/ 55 | /******/ // Execute the module function 56 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 57 | /******/ 58 | /******/ // Return the exports of the module 59 | /******/ return module.exports; 60 | /******/ } 61 | /******/ 62 | /************************************************************************/ 63 | /******/ /* webpack/runtime/compat get default export */ 64 | /******/ (() => { 65 | /******/ // getDefaultExport function for compatibility with non-harmony modules 66 | /******/ __webpack_require__.n = (module) => { 67 | /******/ var getter = module && module.__esModule ? 68 | /******/ () => (module['default']) : 69 | /******/ () => (module); 70 | /******/ __webpack_require__.d(getter, { a: getter }); 71 | /******/ return getter; 72 | /******/ }; 73 | /******/ })(); 74 | /******/ 75 | /******/ /* webpack/runtime/define property getters */ 76 | /******/ (() => { 77 | /******/ // define getter functions for harmony exports 78 | /******/ __webpack_require__.d = (exports, definition) => { 79 | /******/ for(var key in definition) { 80 | /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { 81 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); 82 | /******/ } 83 | /******/ } 84 | /******/ }; 85 | /******/ })(); 86 | /******/ 87 | /******/ /* webpack/runtime/hasOwnProperty shorthand */ 88 | /******/ (() => { 89 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 90 | /******/ })(); 91 | /******/ 92 | /******/ /* webpack/runtime/make namespace object */ 93 | /******/ (() => { 94 | /******/ // define __esModule on exports 95 | /******/ __webpack_require__.r = (exports) => { 96 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 97 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 98 | /******/ } 99 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 100 | /******/ }; 101 | /******/ })(); 102 | /******/ 103 | /************************************************************************/ 104 | var __webpack_exports__ = {}; 105 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 106 | (() => { 107 | /*!************************************************!*\ 108 | !*** ./lib-esnext/scripts/install-run-rush.js ***! 109 | \************************************************/ 110 | __webpack_require__.r(__webpack_exports__); 111 | /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 371017); 112 | /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); 113 | /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147); 114 | /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); 115 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 116 | // See the @microsoft/rush package's LICENSE file for license information. 117 | 118 | 119 | const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); 120 | const PACKAGE_NAME = '@microsoft/rush'; 121 | const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; 122 | const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; 123 | function _getRushVersion(logger) { 124 | const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; 125 | if (rushPreviewVersion !== undefined) { 126 | logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); 127 | return rushPreviewVersion; 128 | } 129 | const rushJsonFolder = findRushJsonFolder(); 130 | const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); 131 | try { 132 | const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8'); 133 | // Use a regular expression to parse out the rushVersion value because rush.json supports comments, 134 | // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. 135 | const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); 136 | return rushJsonMatches[1]; 137 | } 138 | catch (e) { 139 | throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` + 140 | "The 'rushVersion' field is either not assigned in rush.json or was specified " + 141 | 'using an unexpected syntax.'); 142 | } 143 | } 144 | function _run() { 145 | const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; 146 | // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the 147 | // appropriate binary inside the rush package to run 148 | const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); 149 | const bin = scriptName.toLowerCase() === 'install-run-rushx.js' ? 'rushx' : 'rush'; 150 | if (!nodePath || !scriptPath) { 151 | throw new Error('Unexpected exception: could not detect node path or script path'); 152 | } 153 | let commandFound = false; 154 | let logger = { info: console.log, error: console.error }; 155 | for (const arg of packageBinArgs) { 156 | if (arg === '-q' || arg === '--quiet') { 157 | // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress 158 | // any normal informational/diagnostic information printed during startup. 159 | // 160 | // To maintain the same user experience, the install-run* scripts pass along this 161 | // flag but also use it to suppress any diagnostic information normally printed 162 | // to stdout. 163 | logger = { 164 | info: () => { }, 165 | error: console.error 166 | }; 167 | } 168 | else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { 169 | // We either found something that looks like a command (i.e. - doesn't start with a "-"), 170 | // or we found the -h/--help flag, which can be run without a command 171 | commandFound = true; 172 | } 173 | } 174 | if (!commandFound) { 175 | console.log(`Usage: ${scriptName} [args...]`); 176 | if (scriptName === 'install-run-rush.js') { 177 | console.log(`Example: ${scriptName} build --to myproject`); 178 | } 179 | else { 180 | console.log(`Example: ${scriptName} custom-command`); 181 | } 182 | process.exit(1); 183 | } 184 | runWithErrorAndStatusCode(logger, () => { 185 | const version = _getRushVersion(logger); 186 | logger.info(`The rush.json configuration requests Rush version ${version}`); 187 | const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; 188 | if (lockFilePath) { 189 | logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); 190 | } 191 | return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); 192 | }); 193 | } 194 | _run(); 195 | //# sourceMappingURL=install-run-rush.js.map 196 | })(); 197 | 198 | module.exports = __webpack_exports__; 199 | /******/ })() 200 | ; 201 | //# sourceMappingURL=install-run-rush.js.map -------------------------------------------------------------------------------- /playground/button.js: -------------------------------------------------------------------------------- 1 | const { CssRender } = require('css-render') 2 | const { plugin } = require('@css-render/plugin-bem') 3 | 4 | const cssr = CssRender() 5 | const bemPlugin = plugin({ 6 | blockPrefix: '.n-' 7 | }) 8 | const { c } = cssr 9 | cssr.use(bemPlugin) 10 | const { 11 | cB, cE, cM, cNotM 12 | } = bemPlugin 13 | 14 | const buttonType = { 15 | default: 'default-type', 16 | primary: 'primary-type', 17 | info: 'info-type', 18 | success: 'success-type', 19 | warning: 'warning-type', 20 | error: 'error-type' 21 | } 22 | 23 | const buttonSize = { 24 | small: 'small-size', 25 | medium: 'medium-size', 26 | large: 'large-size' 27 | } 28 | 29 | const { 30 | performance 31 | } = require('perf_hooks') 32 | 33 | const start = performance.now() 34 | 35 | function buttonSizeMixin (size) { 36 | const bs = buttonSize[size] 37 | return ( 38 | cM(size + '-size', { 39 | borderRadius: bs, 40 | fontSize: bs, 41 | whiteSpace: 'nowrap' 42 | }, [ 43 | cNotM('text', { 44 | height: bs, 45 | lineHeight: bs, 46 | padding: bs 47 | }), 48 | cM('round', { 49 | padding: bs, 50 | borderRadius: bs 51 | }), 52 | cM('circle', { 53 | width: bs, 54 | borderRadius: bs, 55 | padding: '0 !important' 56 | }), 57 | cM('text', { 58 | padding: 0, 59 | borderRadius: 0 60 | }, [ 61 | cE('icon', { 62 | height: bs, 63 | lineHeight: bs 64 | }) 65 | ]), 66 | cE('content', { 67 | display: 'inline-block', 68 | lineHeight: 'inherit' 69 | }), 70 | cE('icon', { 71 | display: 'inline-block', 72 | position: 'relative', 73 | lineHeight: bs, 74 | height: bs, 75 | width: bs, 76 | maxWidth: bs, 77 | verticalAlign: 'bottom' 78 | }, [ 79 | cB('icon', { 80 | fontSize: bs 81 | }), 82 | cB('base-loading', { 83 | height: bs, 84 | width: bs, 85 | raw: ` 86 | position: absolute; 87 | left: 0; 88 | top: 50%; 89 | transform: translateY(-50%); 90 | display: block; 91 | ` 92 | }), 93 | cM('slot', { 94 | width: bs, 95 | fontSize: bs, 96 | raw: ` 97 | display: inline-block; 98 | align-items: center; 99 | vertical-align: bottom; 100 | ` 101 | }, [ 102 | cB('icon-slot', { 103 | raw: ` 104 | position: absolute; 105 | left: 0; 106 | top: 50%; 107 | transform: translateY(-50%); 108 | display: block; 109 | `, 110 | lineHeight: bs, 111 | height: bs, 112 | width: bs, 113 | fontSize: bs 114 | }) 115 | ]) 116 | ]) 117 | ]) 118 | ) 119 | } 120 | 121 | function buttonTypeMixin (type) { 122 | const bt = buttonType[type] 123 | const commonStyle = { 124 | backgroundColor: bt, 125 | color: bt 126 | } 127 | const borderStyle = { 128 | border: bt 129 | } 130 | const boxShadowStyle = { 131 | boxShadow: bt 132 | } 133 | const borderMaskMixin = cE('border-mask', { 134 | ...boxShadowStyle 135 | }) 136 | const iconMixin = cE('icon', [ 137 | cB('icon', { 138 | fill: bt, 139 | stroke: bt 140 | }), 141 | cB('base-loading', { 142 | fill: bt, 143 | stroke: bt 144 | }) 145 | ]) 146 | return cM(type + '-type', { 147 | ...borderStyle, 148 | ...commonStyle 149 | }, [ 150 | cM('ghost, text', [ 151 | cNotM('disabled', [ 152 | cM('enter-pressed', { 153 | ...commonStyle 154 | }, [ 155 | borderMaskMixin, 156 | iconMixin 157 | ]), 158 | c('&:not(:active):focus', [ 159 | cNotM('enter-pressed', { 160 | ...commonStyle 161 | }, [ 162 | borderMaskMixin, 163 | iconMixin 164 | ]) 165 | ]), 166 | cNotM('enter-pressed', [ 167 | c('&:hover', { 168 | ...commonStyle 169 | }, [ 170 | borderMaskMixin, 171 | iconMixin 172 | ]), 173 | c('&:active', { 174 | ...commonStyle 175 | }, [ 176 | borderMaskMixin, 177 | iconMixin 178 | ]) 179 | ]) 180 | ]), 181 | iconMixin 182 | ]), 183 | cM('text', { 184 | border: 'none', 185 | ...commonStyle 186 | }, [ 187 | borderMaskMixin, 188 | cNotM('disabled', [ 189 | cM('rippling', [ 190 | c('&::after', { 191 | display: 'none' 192 | }) 193 | ]), 194 | cM('enter-pressed', { 195 | ...commonStyle 196 | }), 197 | c('&:not(:active):focus', [ 198 | cNotM('enter-pressed', { 199 | ...commonStyle 200 | }) 201 | ]), 202 | cNotM('enter-pressed', [ 203 | c('&:hover', { 204 | ...commonStyle 205 | }), 206 | c('&:active', { 207 | ...commonStyle 208 | }) 209 | ]) 210 | ]) 211 | ]), 212 | iconMixin, 213 | cNotM('disabled', [ 214 | cM('enter-pressed', { 215 | ...commonStyle 216 | }, [ 217 | borderMaskMixin, 218 | iconMixin 219 | ]), 220 | cM('rippling', [ 221 | c('&::after', { 222 | zIndex: 1, 223 | animationName: bt, 224 | animationDuration: bt, 225 | animationTimingFunction: bt 226 | }), 227 | c('&:not(:active):focus', [ 228 | cNotM('enter-pressed', { 229 | ...commonStyle 230 | }, [ 231 | borderMaskMixin, 232 | iconMixin 233 | ]) 234 | ]), 235 | cNotM('enter-pressed', [ 236 | c('&:hover', { 237 | ...commonStyle 238 | }, [ 239 | borderMaskMixin, 240 | iconMixin 241 | ]) 242 | ]) 243 | ]) 244 | ]) 245 | ]) 246 | } 247 | 248 | function buttonRippleMixin (type) { 249 | const bt = buttonType[type] 250 | return [ 251 | c(`@keyframes ${type}-button-ripple--spread`, { 252 | from: { boxShadow: bt }, 253 | to: { boxShadow: bt } 254 | }), 255 | c(`@keyframes ${type}-button-ripple--opacity`, { 256 | from: { opacity: 0.4 }, 257 | to: { opacity: 0 } 258 | }) 259 | ] 260 | } 261 | 262 | /** ripple */ 263 | const rippleStyle = Object.keys(buttonType).map(type => { 264 | return buttonRippleMixin(type) 265 | }) 266 | 267 | /** button */ 268 | const buttonStyle = cB('button', { 269 | raw: ` 270 | box-sizing: border-box; 271 | outline: none; 272 | position: relative; 273 | z-index: auto; 274 | font-family: inherit; 275 | display: inline-block; 276 | align-items: center; 277 | justify-content: center; 278 | user-select: none; 279 | text-align: center; 280 | transition: 281 | background-color .3s ease-in-out, 282 | opacity .3s ease-in-out, 283 | border-color .3s ease-in-out; 284 | cursor: pointer; 285 | ` 286 | }, [ 287 | c('&::after', { 288 | raw: ` 289 | pointer-events: none; 290 | content: "", 291 | border-radius: inherit; 292 | position: absolute; 293 | left: -1px; 294 | top: -1px; 295 | right: -1px'; 296 | bottom: -1px; 297 | ` 298 | }), 299 | cE('border-mask', { 300 | raw: ` 301 | position: absolute; 302 | left: -1px; 303 | top: -1px; 304 | right: -1px; 305 | bottom: -1px; 306 | border-radius: inherit; 307 | box-shadow: inset 0 0 0 1px transparent; 308 | transition: box-shadow .3s $--n-ease-in-out-cubic-bezier; 309 | pointerEvents: none; 310 | zIndex: 1; 311 | ` 312 | }), 313 | cE('icon', { 314 | transition: 'color .3s $--n-ease-in-out-cubic-bezier' 315 | }), 316 | cE('content', { 317 | whiteSpace: 'nowrap', 318 | transition: 'color .3s $--n-ease-in-out-cubic-bezier' 319 | }), 320 | cM('left-icon', [ 321 | cE('icon', { 322 | marginRight: '6px' 323 | }) 324 | ]), 325 | cM('right-icon', [ 326 | cE('icon', { 327 | marginLeft: '6px' 328 | }) 329 | ]), 330 | cM('block', { 331 | display: 'block', 332 | width: '100%' 333 | }), 334 | cM('loading', { 335 | display: 'block', 336 | width: '100%' 337 | }), 338 | cM('disabled', { 339 | cursor: 'not-allowed' 340 | }), 341 | c('&::-moz-focus-inner', { 342 | border: 0 343 | }), 344 | buttonSizeMixin('tiny'), 345 | buttonSizeMixin('small'), 346 | buttonSizeMixin('medium'), 347 | buttonSizeMixin('large'), 348 | ...Object.keys(buttonType).map(type => buttonTypeMixin(type)) 349 | ]) 350 | 351 | const buttonGroupStyle = cB('button-group', { 352 | whiteSpace: 'nowrap', 353 | display: 'inline-block', 354 | position: 'relative' 355 | }, [ 356 | cNotM('vertical', { 357 | display: 'flex', 358 | flexWrap: 'nowrap' 359 | }, [ 360 | cE('button', { 361 | flexGrow: 1 362 | }), 363 | cB('button', [ 364 | c('&:first-child:not(:last-child)', { 365 | marginRight: '0 !important', 366 | borderTopRightRadius: 0, 367 | borderBottomRightRadius: 0 368 | }), 369 | c('&:last-child:not(:first-child)', { 370 | marginLeft: '0 !important', 371 | borderTopLeftRadius: 0, 372 | borderBottomLeftRadius: 0 373 | }), 374 | c('&:not(first-child):not(:last-child)', { 375 | marginLeft: '0 !important', 376 | marginRight: '0 !important', 377 | borderRadius: '0 !important' 378 | }), 379 | cM('default-type', [ 380 | c('& +', [ 381 | cB('button', [ 382 | cM('default-type', { 383 | borderLeftWidth: 0 384 | }) 385 | ]) 386 | ]) 387 | ]), 388 | cM('ghost', [ 389 | ...[ 390 | 'primary', 391 | 'info', 392 | 'success', 393 | 'warning', 394 | 'error' 395 | ].map(v => cM(v + '-type', [ 396 | c('& +', [ 397 | cB('button', [ 398 | cM('default-type', { 399 | borderLeftWidth: 0 400 | }) 401 | ]) 402 | ]) 403 | ])) 404 | ]) 405 | ]) 406 | ]), 407 | cM('vertical', { 408 | display: 'inline-flex', 409 | flexDirection: 'column' 410 | }, [ 411 | cB('button', [ 412 | c('&:first-child:not(:last-child)', { 413 | raw: ` 414 | margin-bottom: 0 !important; 415 | margin-left: 0 !important; 416 | margin-right: 0 !important; 417 | border-bottom-reft-radius: 0; 418 | border-bottom-right-radius: 0; 419 | ` 420 | }), 421 | c('&:last-child:not(:first-child)', { 422 | raw: ` 423 | margin-top: 0 !important; 424 | margin-left: 0 !important; 425 | margin-right: 0 !important; 426 | border-bottom-left-radius: 0; 427 | border-bottom-left-radius: 0; 428 | ` 429 | }), 430 | c('&:not(first-child):not(:last-child)', { 431 | margin: '0 !important', 432 | borderRadius: '0 !important' 433 | }), 434 | cM('default-type', [ 435 | c('& +', [ 436 | cB('button', [ 437 | cM('default-type', { 438 | borderTopWidth: 0 439 | }) 440 | ]) 441 | ]) 442 | ]), 443 | cM('ghost', [ 444 | ...[ 445 | 'primary', 446 | 'info', 447 | 'success', 448 | 'warning', 449 | 'error' 450 | ].map(v => cM(v + '-type', [ 451 | c('& +', [ 452 | cB('button', [ 453 | cM('default-type', { 454 | borderTopWidth: 0 455 | }) 456 | ]) 457 | ]) 458 | ])) 459 | ]) 460 | ]) 461 | ]) 462 | ]) 463 | 464 | const output = c([ 465 | rippleStyle, 466 | buttonStyle, 467 | buttonGroupStyle 468 | ]).render() 469 | 470 | const end = performance.now() 471 | 472 | // console.log(output) 473 | console.log(end - start) 474 | -------------------------------------------------------------------------------- /playground/button.perf.js: -------------------------------------------------------------------------------- 1 | const { CssRender } = require('css-render') 2 | const { plugin } = require('@css-render/plugin-bem') 3 | 4 | const cssr = CssRender() 5 | const bemPlugin = plugin({ 6 | blockPrefix: '.n-' 7 | }) 8 | const { c } = cssr 9 | cssr.use(bemPlugin) 10 | const { 11 | cB, cE, cM, cNotM 12 | } = bemPlugin 13 | 14 | const buttonType = { 15 | default: 'default-type', 16 | primary: 'primary-type', 17 | info: 'info-type', 18 | success: 'success-type', 19 | warning: 'warning-type', 20 | error: 'error-type' 21 | } 22 | 23 | const buttonSize = { 24 | small: 'small-size', 25 | medium: 'medium-size', 26 | large: 'large-size' 27 | } 28 | 29 | const { 30 | performance 31 | } = require('perf_hooks') 32 | 33 | const start = performance.now() 34 | 35 | function buttonSizeMixin (size) { 36 | const bs = buttonSize[size] 37 | return ( 38 | cM(size + '-size', { 39 | borderRadius: bs, 40 | fontSize: bs, 41 | whiteSpace: 'nowrap' 42 | }, [ 43 | cNotM('text', { 44 | height: bs, 45 | lineHeight: bs, 46 | padding: bs 47 | }), 48 | cM('round', { 49 | padding: bs, 50 | borderRadius: bs 51 | }), 52 | cM('circle', { 53 | width: bs, 54 | borderRadius: bs, 55 | padding: '0 !important' 56 | }), 57 | cM('text', { 58 | padding: 0, 59 | borderRadius: 0 60 | }, [ 61 | cE('icon', { 62 | height: bs, 63 | lineHeight: bs 64 | }) 65 | ]), 66 | cE('content', { 67 | display: 'inline-block', 68 | lineHeight: 'inherit' 69 | }), 70 | cE('icon', { 71 | display: 'inline-block', 72 | position: 'relative', 73 | lineHeight: bs, 74 | height: bs, 75 | width: bs, 76 | maxWidth: bs, 77 | verticalAlign: 'bottom' 78 | }, [ 79 | cB('icon', { 80 | fontSize: bs 81 | }), 82 | cB('base-loading', { 83 | height: bs, 84 | width: bs, 85 | raw: ` 86 | position: absolute; 87 | left: 0; 88 | top: 50%; 89 | transform: translateY(-50%); 90 | display: block; 91 | ` 92 | }), 93 | cM('slot', { 94 | width: bs, 95 | fontSize: bs, 96 | raw: ` 97 | display: inline-block; 98 | align-items: center; 99 | vertical-align: bottom; 100 | ` 101 | }, [ 102 | cB('icon-slot', { 103 | raw: ` 104 | position: absolute; 105 | left: 0; 106 | top: 50%; 107 | transform: translateY(-50%); 108 | display: block; 109 | `, 110 | lineHeight: bs, 111 | height: bs, 112 | width: bs, 113 | fontSize: bs 114 | }) 115 | ]) 116 | ]) 117 | ]) 118 | ) 119 | } 120 | 121 | function buttonTypeMixin (type) { 122 | const bt = buttonType[type] 123 | const commonStyle = { 124 | backgroundColor: bt, 125 | color: bt 126 | } 127 | const borderStyle = { 128 | border: bt 129 | } 130 | const boxShadowStyle = { 131 | boxShadow: bt 132 | } 133 | const borderMaskMixin = cE('border-mask', { 134 | ...boxShadowStyle 135 | }) 136 | const iconMixin = cE('icon', [ 137 | cB('icon', { 138 | fill: bt, 139 | stroke: bt 140 | }), 141 | cB('base-loading', { 142 | fill: bt, 143 | stroke: bt 144 | }) 145 | ]) 146 | return cM(type + '-type', { 147 | ...borderStyle, 148 | ...commonStyle 149 | }, [ 150 | cM('ghost, text', [ 151 | cNotM('disabled', [ 152 | cM('enter-pressed', { 153 | ...commonStyle 154 | }, [ 155 | borderMaskMixin, 156 | iconMixin 157 | ]), 158 | c('&:not(:active):focus', [ 159 | cNotM('enter-pressed', { 160 | ...commonStyle 161 | }, [ 162 | borderMaskMixin, 163 | iconMixin 164 | ]) 165 | ]), 166 | cNotM('enter-pressed', [ 167 | c('&:hover', { 168 | ...commonStyle 169 | }, [ 170 | borderMaskMixin, 171 | iconMixin 172 | ]), 173 | c('&:active', { 174 | ...commonStyle 175 | }, [ 176 | borderMaskMixin, 177 | iconMixin 178 | ]) 179 | ]) 180 | ]), 181 | iconMixin 182 | ]), 183 | cM('text', { 184 | border: 'none', 185 | ...commonStyle 186 | }, [ 187 | borderMaskMixin, 188 | cNotM('disabled', [ 189 | cM('rippling', [ 190 | c('&::after', { 191 | display: 'none' 192 | }) 193 | ]), 194 | cM('enter-pressed', { 195 | ...commonStyle 196 | }), 197 | c('&:not(:active):focus', [ 198 | cNotM('enter-pressed', { 199 | ...commonStyle 200 | }) 201 | ]), 202 | cNotM('enter-pressed', [ 203 | c('&:hover', { 204 | ...commonStyle 205 | }), 206 | c('&:active', { 207 | ...commonStyle 208 | }) 209 | ]) 210 | ]) 211 | ]), 212 | iconMixin, 213 | cNotM('disabled', [ 214 | cM('enter-pressed', { 215 | ...commonStyle 216 | }, [ 217 | borderMaskMixin, 218 | iconMixin 219 | ]), 220 | cM('rippling', [ 221 | c('&::after', { 222 | zIndex: 1, 223 | animationName: bt, 224 | animationDuration: bt, 225 | animationTimingFunction: bt 226 | }), 227 | c('&:not(:active):focus', [ 228 | cNotM('enter-pressed', { 229 | ...commonStyle 230 | }, [ 231 | borderMaskMixin, 232 | iconMixin 233 | ]) 234 | ]), 235 | cNotM('enter-pressed', [ 236 | c('&:hover', { 237 | ...commonStyle 238 | }, [ 239 | borderMaskMixin, 240 | iconMixin 241 | ]) 242 | ]) 243 | ]) 244 | ]) 245 | ]) 246 | } 247 | 248 | function buttonRippleMixin (type) { 249 | const bt = buttonType[type] 250 | return [ 251 | c(`@keyframes ${type}-button-ripple--spread`, { 252 | from: { boxShadow: bt }, 253 | to: { boxShadow: bt } 254 | }), 255 | c(`@keyframes ${type}-button-ripple--opacity`, { 256 | from: { opacity: 0.4 }, 257 | to: { opacity: 0 } 258 | }) 259 | ] 260 | } 261 | 262 | /** ripple */ 263 | const rippleStyle = Object.keys(buttonType).map(type => { 264 | return buttonRippleMixin(type) 265 | }) 266 | 267 | /** button */ 268 | const buttonStyle = cB('button', { 269 | raw: ` 270 | box-sizing: border-box; 271 | outline: none; 272 | position: relative; 273 | z-index: auto; 274 | font-family: inherit; 275 | display: inline-block; 276 | align-items: center; 277 | justify-content: center; 278 | user-select: none; 279 | text-align: center; 280 | transition: 281 | background-color .3s ease-in-out, 282 | opacity .3s ease-in-out, 283 | border-color .3s ease-in-out; 284 | cursor: pointer; 285 | ` 286 | }, [ 287 | c('&::after', { 288 | raw: ` 289 | pointer-events: none; 290 | content: "", 291 | border-radius: inherit; 292 | position: absolute; 293 | left: -1px; 294 | top: -1px; 295 | right: -1px'; 296 | bottom: -1px; 297 | ` 298 | }), 299 | cE('border-mask', { 300 | raw: ` 301 | position: absolute; 302 | left: -1px; 303 | top: -1px; 304 | right: -1px; 305 | bottom: -1px; 306 | border-radius: inherit; 307 | box-shadow: inset 0 0 0 1px transparent; 308 | transition: box-shadow .3s $--n-ease-in-out-cubic-bezier; 309 | pointerEvents: none; 310 | zIndex: 1; 311 | ` 312 | }), 313 | cE('icon', { 314 | transition: 'color .3s $--n-ease-in-out-cubic-bezier' 315 | }), 316 | cE('content', { 317 | whiteSpace: 'nowrap', 318 | transition: 'color .3s $--n-ease-in-out-cubic-bezier' 319 | }), 320 | cM('left-icon', [ 321 | cE('icon', { 322 | marginRight: '6px' 323 | }) 324 | ]), 325 | cM('right-icon', [ 326 | cE('icon', { 327 | marginLeft: '6px' 328 | }) 329 | ]), 330 | cM('block', { 331 | display: 'block', 332 | width: '100%' 333 | }), 334 | cM('loading', { 335 | display: 'block', 336 | width: '100%' 337 | }), 338 | cM('disabled', { 339 | cursor: 'not-allowed' 340 | }), 341 | c('&::-moz-focus-inner', { 342 | border: 0 343 | }), 344 | buttonSizeMixin('tiny'), 345 | buttonSizeMixin('small'), 346 | buttonSizeMixin('medium'), 347 | buttonSizeMixin('large'), 348 | ...Object.keys(buttonType).map(type => buttonTypeMixin(type)) 349 | ]) 350 | 351 | const buttonGroupStyle = cB('button-group', { 352 | whiteSpace: 'nowrap', 353 | display: 'inline-block', 354 | position: 'relative' 355 | }, [ 356 | cNotM('vertical', { 357 | display: 'flex', 358 | flexWrap: 'nowrap' 359 | }, [ 360 | cE('button', { 361 | flexGrow: 1 362 | }), 363 | cB('button', [ 364 | c('&:first-child:not(:last-child)', { 365 | marginRight: '0 !important', 366 | borderTopRightRadius: 0, 367 | borderBottomRightRadius: 0 368 | }), 369 | c('&:last-child:not(:first-child)', { 370 | marginLeft: '0 !important', 371 | borderTopLeftRadius: 0, 372 | borderBottomLeftRadius: 0 373 | }), 374 | c('&:not(first-child):not(:last-child)', { 375 | marginLeft: '0 !important', 376 | marginRight: '0 !important', 377 | borderRadius: '0 !important' 378 | }), 379 | cM('default-type', [ 380 | c('& +', [ 381 | cB('button', [ 382 | cM('default-type', { 383 | borderLeftWidth: 0 384 | }) 385 | ]) 386 | ]) 387 | ]), 388 | cM('ghost', [ 389 | ...[ 390 | 'primary', 391 | 'info', 392 | 'success', 393 | 'warning', 394 | 'error' 395 | ].map(v => cM(v + '-type', [ 396 | c('& +', [ 397 | cB('button', [ 398 | cM('default-type', { 399 | borderLeftWidth: 0 400 | }) 401 | ]) 402 | ]) 403 | ])) 404 | ]) 405 | ]) 406 | ]), 407 | cM('vertical', { 408 | display: 'inline-flex', 409 | flexDirection: 'column' 410 | }, [ 411 | cB('button', [ 412 | c('&:first-child:not(:last-child)', { 413 | raw: ` 414 | margin-bottom: 0 !important; 415 | margin-left: 0 !important; 416 | margin-right: 0 !important; 417 | border-bottom-reft-radius: 0; 418 | border-bottom-right-radius: 0; 419 | ` 420 | }), 421 | c('&:last-child:not(:first-child)', { 422 | raw: ` 423 | margin-top: 0 !important; 424 | margin-left: 0 !important; 425 | margin-right: 0 !important; 426 | border-bottom-left-radius: 0; 427 | border-bottom-left-radius: 0; 428 | ` 429 | }), 430 | c('&:not(first-child):not(:last-child)', { 431 | margin: '0 !important', 432 | borderRadius: '0 !important' 433 | }), 434 | cM('default-type', [ 435 | c('& +', [ 436 | cB('button', [ 437 | cM('default-type', { 438 | borderTopWidth: 0 439 | }) 440 | ]) 441 | ]) 442 | ]), 443 | cM('ghost', [ 444 | ...[ 445 | 'primary', 446 | 'info', 447 | 'success', 448 | 'warning', 449 | 'error' 450 | ].map(v => cM(v + '-type', [ 451 | c('& +', [ 452 | cB('button', [ 453 | cM('default-type', { 454 | borderTopWidth: 0 455 | }) 456 | ]) 457 | ]) 458 | ])) 459 | ]) 460 | ]) 461 | ]) 462 | ]) 463 | 464 | const output = c([ 465 | rippleStyle, 466 | buttonStyle, 467 | buttonGroupStyle 468 | ]).render() 469 | 470 | const end = performance.now() 471 | 472 | // console.log(output) 473 | console.log(end - start) 474 | -------------------------------------------------------------------------------- /packages/css-render/__tests__/render/index.spec.ts: -------------------------------------------------------------------------------- 1 | import CssRender from '../../src' 2 | import { assertEqual } from '@css-render/test-shared' 3 | 4 | const { c, config } = CssRender() 5 | 6 | describe('#render - common cases', () => { 7 | it('should work with nested nodes array', () => { 8 | assertEqual( 9 | c( 10 | 'body', 11 | { 12 | margin: 0, 13 | backgroundColor: 'white' 14 | }, 15 | [ 16 | [ 17 | [ 18 | c('&.dark', { 19 | backgroundColor: 'black' 20 | }) 21 | ], 22 | c('.container', { 23 | width: '100%' 24 | }) 25 | ] 26 | ] 27 | ).render(), 28 | `body { 29 | margin: 0; 30 | background-color: white; 31 | } 32 | 33 | body.dark { 34 | background-color: black; 35 | } 36 | 37 | body .container { 38 | width: 100%; 39 | }` 40 | ) 41 | }) 42 | it('should render as expected(1)', () => { 43 | assertEqual( 44 | c( 45 | 'body', 46 | { 47 | margin: 0, 48 | backgroundColor: 'white' 49 | }, 50 | [ 51 | c('&.dark', { 52 | backgroundColor: 'black' 53 | }), 54 | c('.container', { 55 | width: '100%' 56 | }) 57 | ] 58 | ).render(), 59 | `body { 60 | margin: 0; 61 | background-color: white; 62 | } 63 | 64 | body.dark { 65 | background-color: black; 66 | } 67 | 68 | body .container { 69 | width: 100%; 70 | }` 71 | ) 72 | }) 73 | it('should render as expected(2)', () => { 74 | assertEqual( 75 | c('body', [ 76 | c('&.dark', { 77 | backgroundColor: 'black' 78 | }), 79 | c('.container', { 80 | width: '100%' 81 | }) 82 | ]).render(), 83 | `body.dark { 84 | background-color: black; 85 | } 86 | 87 | body .container { 88 | width: 100%; 89 | }` 90 | ) 91 | }) 92 | it('should render as excepted(3)', () => { 93 | assertEqual( 94 | c('@keyframes what-a-good-animation', { 95 | from: { 96 | color: 'white', 97 | backgroundColor: 'black' 98 | }, 99 | to: { 100 | color: 'black', 101 | backgroundColor: 'white' 102 | } 103 | }).render(), 104 | `@keyframes what-a-good-animation { 105 | from { 106 | color: white; 107 | background-color: black; 108 | } 109 | to { 110 | color: black; 111 | background-color: white; 112 | } 113 | }` 114 | ) 115 | }) 116 | it('should render a function typed prop', () => { 117 | assertEqual( 118 | c('@keyframes what-a-good-animation', () => ({ 119 | from: { 120 | color: 'white', 121 | backgroundColor: 'black' 122 | }, 123 | to: { 124 | color: 'black', 125 | backgroundColor: 'white' 126 | } 127 | })).render(), 128 | `@keyframes what-a-good-animation { 129 | from { 130 | color: white; 131 | background-color: black; 132 | } 133 | to { 134 | color: black; 135 | background-color: white; 136 | } 137 | }` 138 | ) 139 | }) 140 | it('should render an array as root', () => { 141 | assertEqual( 142 | c([ 143 | c( 144 | 'sel1', 145 | { 146 | position: 'relative' 147 | }, 148 | [ 149 | c('&.sel2', { 150 | position: 'relative' 151 | }) 152 | ] 153 | ), 154 | c( 155 | 'sel1', 156 | { 157 | position: 'relative' 158 | }, 159 | [ 160 | c('&.sel2', { 161 | position: 'relative' 162 | }) 163 | ] 164 | ) 165 | ]).render(), 166 | ` 167 | sel1 { 168 | position: relative; 169 | } 170 | sel1.sel2 { 171 | position: relative; 172 | } 173 | sel1 { 174 | position: relative; 175 | } 176 | sel1.sel2 { 177 | position: relative; 178 | } 179 | ` 180 | ) 181 | }) 182 | it('should render with props', () => { 183 | const style = c('sel1', ({ props }) => { 184 | return { 185 | color: props.color 186 | } 187 | }) 188 | assertEqual( 189 | style.render({ 190 | color: 'red' 191 | }), 192 | ` 193 | sel1 { 194 | color: red; 195 | } 196 | ` 197 | ) 198 | }) 199 | it('should work with COptionSelector', () => { 200 | assertEqual( 201 | c( 202 | { 203 | $: 'body' 204 | }, 205 | [ 206 | c( 207 | { 208 | $: () => '&.dark' 209 | }, 210 | { 211 | backgroundColor: 'black' 212 | } 213 | ), 214 | c('.container', { 215 | width: '100%' 216 | }) 217 | ] 218 | ).render(), 219 | `body.dark { 220 | background-color: black; 221 | } 222 | 223 | body .container { 224 | width: 100%; 225 | }` 226 | ) 227 | }) 228 | it('should work with CLazySelector', () => { 229 | assertEqual( 230 | c( 231 | ({ props }) => (props.pfx as string) + 'body', 232 | [ 233 | c(({ props }) => `&.${props.pfx as string}dark`, { 234 | backgroundColor: 'black' 235 | }), 236 | c(() => '.container', { 237 | width: '100%' 238 | }) 239 | ] 240 | ).render({ 241 | pfx: 'pfx' 242 | }), 243 | `pfxbody.pfxdark { 244 | background-color: black; 245 | } 246 | 247 | pfxbody .container { 248 | width: 100%; 249 | }` 250 | ) 251 | }) 252 | it('should work with function typed children', () => { 253 | assertEqual( 254 | c( 255 | () => 'body', 256 | [ 257 | ({ props }) => [ 258 | c((props.prefix as string) + '&.dark', { 259 | backgroundColor: 'black' 260 | }), 261 | c((props.prefix as string) + '.container', { 262 | width: '100%' 263 | }) 264 | ] 265 | ] 266 | ).render({ 267 | prefix: 'pfx' 268 | }), 269 | `pfxbody.dark { 270 | background-color: black; 271 | } 272 | 273 | body pfx.container { 274 | width: 100%; 275 | }` 276 | ) 277 | assertEqual( 278 | c( 279 | () => 'body', 280 | [ 281 | ({ props }) => 282 | c((props.prefix as string) + '&.dark', { 283 | backgroundColor: 'black' 284 | }), 285 | ({ props }) => 286 | c((props.prefix as string) + '.container', { 287 | width: '100%' 288 | }) 289 | ] 290 | ).render({ 291 | prefix: 'pfx' 292 | }), 293 | `pfxbody.dark { 294 | background-color: black; 295 | } 296 | 297 | body pfx.container { 298 | width: 100%; 299 | }` 300 | ) 301 | }) 302 | it('should work with empty selector', () => { 303 | assertEqual( 304 | c('', [ 305 | c('&.a', { 306 | background: 'red' 307 | }) 308 | ]).render(), 309 | `.a { 310 | background: red; 311 | }` 312 | ) 313 | assertEqual( 314 | c( 315 | () => '', 316 | [ 317 | c('&.a', { 318 | background: 'red' 319 | }) 320 | ] 321 | ).render(), 322 | `.a { 323 | background: red; 324 | }` 325 | ) 326 | assertEqual( 327 | c( 328 | () => null, 329 | [ 330 | c('&.a', { 331 | background: 'red' 332 | }) 333 | ] 334 | ).render(), 335 | `.a { 336 | background: red; 337 | }` 338 | ) 339 | assertEqual( 340 | c({}, [ 341 | c('&.a', { 342 | background: 'red' 343 | }) 344 | ]).render(), 345 | `.a { 346 | background: red; 347 | }` 348 | ) 349 | }) 350 | it('should preserve empty block when keepEmptyBlock is false', () => { 351 | config.keepEmptyBlock = true 352 | assertEqual(c('body', {}).render(), 'body {}') 353 | config.keepEmptyBlock = false 354 | assertEqual(c('body', {}).render(), '') 355 | config.keepEmptyBlock = true 356 | }) 357 | it("shouldn't render empty property", () => { 358 | assertEqual( 359 | c('body', { 360 | background: undefined, 361 | myColor: null 362 | }).render(), 363 | 'body {}' 364 | ) 365 | }) 366 | it('should work with empty selector', () => { 367 | assertEqual( 368 | c('body', [ 369 | c( 370 | {}, 371 | { 372 | background: 'red' 373 | } 374 | ) 375 | ]).render(), 376 | `body { 377 | background: red; 378 | }` 379 | ) 380 | assertEqual( 381 | c( 382 | 'body', 383 | { 384 | color: 'black' 385 | }, 386 | [ 387 | c( 388 | {}, 389 | { 390 | background: 'red' 391 | } 392 | ) 393 | ] 394 | ).render(), 395 | ` 396 | body { 397 | color: black; 398 | } 399 | 400 | body { 401 | background: red; 402 | } 403 | ` 404 | ) 405 | }) 406 | it('should work with functional typed option selector.$', () => { 407 | assertEqual( 408 | c( 409 | { 410 | $: ({ props }) => props.x 411 | }, 412 | { 413 | background: 'red' 414 | } 415 | ).render({ 416 | x: 'body' 417 | }), 418 | `body { 419 | background: red; 420 | }` 421 | ) 422 | }) 423 | it('should work with null & void returned props function', () => { 424 | assertEqual( 425 | c('body', () => null, [c('body2', {})]).render(), 426 | 'body body2 {}' 427 | ) 428 | assertEqual( 429 | c('body', () => undefined, [c('body2', {})]).render(), 430 | 'body body2 {}' 431 | ) 432 | }) 433 | }) 434 | 435 | describe('#render - doc cases', () => { 436 | it('#case1', () => { 437 | assertEqual( 438 | c( 439 | '.button', 440 | { 441 | color: 'black' 442 | }, 443 | [ 444 | c('.button__icon', { 445 | fill: 'black' 446 | }), 447 | c('&.button--error', { 448 | color: 'red' 449 | }) 450 | ] 451 | ).render(), 452 | ` 453 | .button { 454 | color: black; 455 | } 456 | 457 | .button .button__icon { 458 | fill: black; 459 | } 460 | 461 | .button.button--error { 462 | color: red; 463 | } 464 | ` 465 | ) 466 | }) 467 | it('#case2', () => { 468 | assertEqual( 469 | c(({ context, props }) => props.selector, { 470 | color: 'black' 471 | }).render({ 472 | selector: '.selector' 473 | }), 474 | ` 475 | .selector { 476 | color: black; 477 | } 478 | ` 479 | ) 480 | }) 481 | it('#case3', () => { 482 | assertEqual( 483 | c('div', [ 484 | c(null, [ 485 | c('button', { 486 | color: 'black' 487 | }) 488 | ]) 489 | ]).render(), 490 | ` 491 | div button { 492 | color: black; 493 | } 494 | ` 495 | ) 496 | }) 497 | it('#case3', () => { 498 | assertEqual( 499 | c('div', [ 500 | ({ context, props }) => 501 | c('button', { 502 | color: props.color 503 | }), 504 | c('ul', { 505 | backgroundColor: 'red' 506 | }), 507 | [ 508 | [ 509 | c('dl', { 510 | backgroundColor: 'red' 511 | }) 512 | ] 513 | ], 514 | () => [ 515 | c('ol', { 516 | backgroundColor: 'red' 517 | }) 518 | ] 519 | ]).render({ 520 | color: 'black' 521 | }), 522 | `div button { 523 | color: black; 524 | } 525 | 526 | div ul { 527 | background-color: red; 528 | } 529 | 530 | div dl { 531 | background-color: red; 532 | } 533 | 534 | div ol { 535 | background-color: red; 536 | }` 537 | ) 538 | }) 539 | }) 540 | 541 | describe('#render - falsy node', () => { 542 | it('#case1', () => { 543 | assertEqual( 544 | c( 545 | '.button', 546 | { 547 | color: 'black' 548 | }, 549 | [ 550 | null, 551 | undefined, 552 | () => null, 553 | () => undefined, 554 | c('.button__icon', { 555 | fill: 'black' 556 | }), 557 | c('&.button--error', { 558 | color: 'red' 559 | }) 560 | ] 561 | ).render(), 562 | ` 563 | .button { 564 | color: black; 565 | } 566 | 567 | .button .button__icon { 568 | fill: black; 569 | } 570 | 571 | .button.button--error { 572 | color: red; 573 | } 574 | ` 575 | ) 576 | }) 577 | }) 578 | 579 | describe('#render - string property', () => { 580 | it('#case1', () => { 581 | assertEqual(c('x', '666').render(), 'x { 666 }') 582 | }) 583 | it('#case2', () => { 584 | assertEqual(c('x', 'color: red;').render(), 'x { color: red; }') 585 | }) 586 | it('#case3', () => { 587 | assertEqual(c('x', () => '666').render(), 'x { 666 }') 588 | }) 589 | it('#case4', () => { 590 | assertEqual(c('x', () => 'color: red;').render(), 'x { color: red; }') 591 | }) 592 | }) 593 | 594 | describe('#render - raw property', () => { 595 | it('#case1', () => { 596 | assertEqual( 597 | c('x', { 598 | raw: '666' 599 | }).render(), 600 | ` 601 | x { 602 | 666 603 | } 604 | ` 605 | ) 606 | assertEqual( 607 | c('x', { 608 | raw: '666', 609 | color: 'white' 610 | }).render(), 611 | ` 612 | x { 613 | 666 614 | color: white; 615 | } 616 | ` 617 | ) 618 | }) 619 | }) 620 | 621 | describe('#render - string child', () => { 622 | it('#case 1', () => { 623 | assertEqual(c(['good { key: value }']).render(), 'good { key: value }') 624 | }) 625 | it('#case 2', () => { 626 | assertEqual( 627 | c('parent', [c('gogogo', { key: 'value' }), 'key: value;']).render(), 628 | ` 629 | parent gogogo { key: value; } 630 | parent { key: value; } 631 | ` 632 | ) 633 | }) 634 | }) 635 | 636 | describe('#render - media query', () => { 637 | it('#case 1', () => { 638 | assertEqual( 639 | c('@media', [c('a', { color: 'red' })]).render(), 640 | '@media { a { color: red; } }' 641 | ) 642 | }) 643 | it('#case 2', () => { 644 | assertEqual( 645 | c('@media', [ 646 | c('a', { color: 'red' }), 647 | c('b', { color: 'red' }) 648 | ]).render(), 649 | '@media { a { color: red; } b { color: red; } }' 650 | ) 651 | }) 652 | it('#case 3', () => { 653 | assertEqual( 654 | c('@supports', [ 655 | c('@media', [c('a', { color: 'red' }), c('b', { color: 'red' })]) 656 | ]).render(), 657 | '@supports { @media { a { color: red; } b { color: red; } } }' 658 | ) 659 | }) 660 | it('#case 4', () => { 661 | assertEqual( 662 | c(' @supports', [ 663 | c(' @media', [c('a', { color: 'red' }), c('b', { color: 'red' })]) 664 | ]).render(), 665 | '@supports { @media { a { color: red; } b { color: red; } } }' 666 | ) 667 | }) 668 | it('#case 4', () => { 669 | assertEqual( 670 | c('@supports', [ 671 | c('a', { color: 'red' }), 672 | c('@media', [c('b', { color: 'red' })]) 673 | ]).render(), 674 | '@supports { a { color: red; } @media { b { color: red; } } }' 675 | ) 676 | }) 677 | it('#case 5', () => { 678 | assertEqual( 679 | c([ 680 | c('@supports', [ 681 | c('a', { color: 'red' }), 682 | c('@media', [c('b', { color: 'red' })]) 683 | ]) 684 | ]).render(), 685 | '@supports { a { color: red; } @media { b { color: red; } } }' 686 | ) 687 | }) 688 | }) 689 | 690 | describe('#render - at rules', () => { 691 | it('@page', () => { 692 | assertEqual( 693 | c('@page', { margin: '1em' }).render(), 694 | '@page { margin: 1em; } ' 695 | ) 696 | }) 697 | it('@font-face', () => { 698 | assertEqual( 699 | c('@font-face', { fontFamily: 'ggg' }).render(), 700 | '@font-face { font-family: ggg; } ' 701 | ) 702 | }) 703 | }) 704 | --------------------------------------------------------------------------------