From 489d6ab22f41427643dc5e2f97fbbb2ee115bf8e Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 00:41:55 +0900 Subject: [PATCH 01/11] Implement reset css --- .changeset/breezy-ghosts-kiss.md | 5 + apps/landing/next.config.ts | 2 +- apps/landing/package.json | 4 +- apps/landing/src/app/layout.tsx | 5 +- libs/extractor/src/extract_style/constant.rs | 4 +- libs/extractor/src/lib.rs | 2 +- .../extractor__tests__maintain_value.snap | 22 ++- packages/react/src/__tests__/index.test.ts | 1 + packages/react/src/index.ts | 1 + .../src/utils/__tests__/keyframes.test.ts | 19 +++ packages/react/src/utils/keyframes.ts | 16 ++ packages/react/src/utils/reset-css.ts | 144 +++++++++++++++++ packages/reset-css/README.md | 145 +++++++++++++++++ packages/reset-css/package.json | 52 +++++++ packages/reset-css/src/index.ts | 146 ++++++++++++++++++ packages/reset-css/tsconfig.json | 30 ++++ packages/reset-css/vite.config.ts | 65 ++++++++ pnpm-lock.yaml | 27 +++- vitest.config.ts | 46 ------ 19 files changed, 673 insertions(+), 63 deletions(-) create mode 100644 .changeset/breezy-ghosts-kiss.md create mode 100644 packages/react/src/utils/__tests__/keyframes.test.ts create mode 100644 packages/react/src/utils/keyframes.ts create mode 100644 packages/react/src/utils/reset-css.ts create mode 100644 packages/reset-css/README.md create mode 100644 packages/reset-css/package.json create mode 100644 packages/reset-css/src/index.ts create mode 100644 packages/reset-css/tsconfig.json create mode 100644 packages/reset-css/vite.config.ts delete mode 100644 vitest.config.ts diff --git a/.changeset/breezy-ghosts-kiss.md b/.changeset/breezy-ghosts-kiss.md new file mode 100644 index 00000000..649c1967 --- /dev/null +++ b/.changeset/breezy-ghosts-kiss.md @@ -0,0 +1,5 @@ +--- +"@devup-ui/reset-css": major +--- + +reset css diff --git a/apps/landing/next.config.ts b/apps/landing/next.config.ts index fff27523..b676aed5 100644 --- a/apps/landing/next.config.ts +++ b/apps/landing/next.config.ts @@ -15,6 +15,6 @@ export default withMDX( pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], output: 'export', }, - { include: ['@devup-ui/components'] }, + { include: ['@devup-ui/components', '@devup-ui/reset-css'] }, ), ) diff --git a/apps/landing/package.json b/apps/landing/package.json index cfeb619a..5255704d 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -13,6 +13,7 @@ "dependencies": { "@devup-ui/components": "workspace:*", "@devup-ui/react": "workspace:*", + "@devup-ui/reset-css": "workspace:*", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "^15.3.5", @@ -24,8 +25,7 @@ "react-dom": "^19.1.0", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.1", - "remark-gfm": "^4.0.1", - "sanitize.css": "^13.0.0" + "remark-gfm": "^4.0.1" }, "devDependencies": { "@devup-ui/next-plugin": "workspace:*", diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index 0962c4b1..5229a23e 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -1,6 +1,5 @@ -import 'sanitize.css' - import { css, globalCss, ThemeScript } from '@devup-ui/react' +import { resetCss } from '@devup-ui/reset-css' import type { Metadata } from 'next' import { Footer } from '../components/Footer' @@ -24,6 +23,8 @@ export const metadata: Metadata = { }, } +resetCss() + globalCss({ imports: ['https://cdn.jsdelivr.net/gh/joungkyun/font-d2coding/d2coding.css'], table: { diff --git a/libs/extractor/src/extract_style/constant.rs b/libs/extractor/src/extract_style/constant.rs index 51a0a0c8..040411aa 100644 --- a/libs/extractor/src/extract_style/constant.rs +++ b/libs/extractor/src/extract_style/constant.rs @@ -18,5 +18,7 @@ pub(super) static MAINTAIN_VALUE_PROPERTIES: phf::Set<&str> = phf_set! { "gridRow", "gridRowStart", "gridRowEnd", - "animationIterationCount" + "animationIterationCount", + "tabSize", + "MozTabSize" }; diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 740fc514..d00cf7ff 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -2531,7 +2531,7 @@ e(o, { className: "a", bg: variable, style: { color: "blue" }, ...props }) extract( "test.jsx", r#"import {Flex} from '@devup-ui/core' - + "#, ExtractOption { package: "@devup-ui/core".to_string(), diff --git a/libs/extractor/src/snapshots/extractor__tests__maintain_value.snap b/libs/extractor/src/snapshots/extractor__tests__maintain_value.snap index af7a1175..98cf60b1 100644 --- a/libs/extractor/src/snapshots/extractor__tests__maintain_value.snap +++ b/libs/extractor/src/snapshots/extractor__tests__maintain_value.snap @@ -1,9 +1,18 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Flex} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Flex} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" --- ToBTreeSet { styles: { + Static( + ExtractStaticStyle { + property: "MozTabSize", + value: "4", + level: 0, + selector: None, + style_order: None, + }, + ), Static( ExtractStaticStyle { property: "display", @@ -60,6 +69,15 @@ ToBTreeSet { style_order: None, }, ), + Static( + ExtractStaticStyle { + property: "tabSize", + value: "4", + level: 0, + selector: None, + style_order: None, + }, + ), Static( ExtractStaticStyle { property: "zIndex", @@ -70,5 +88,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", } diff --git a/packages/react/src/__tests__/index.test.ts b/packages/react/src/__tests__/index.test.ts index 4518d54f..48a556ac 100644 --- a/packages/react/src/__tests__/index.test.ts +++ b/packages/react/src/__tests__/index.test.ts @@ -14,6 +14,7 @@ describe('export', () => { css: expect.any(Function), globalCss: expect.any(Function), + keyframes: expect.any(Function), ThemeScript: expect.any(Function), diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 40ba543a..4c7f3a96 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -16,4 +16,5 @@ export { css } from './utils/css' export { getTheme } from './utils/get-theme' export { globalCss } from './utils/global-css' export { initTheme } from './utils/init-theme' +export { keyframes } from './utils/keyframes' export { setTheme } from './utils/set-theme' diff --git a/packages/react/src/utils/__tests__/keyframes.test.ts b/packages/react/src/utils/__tests__/keyframes.test.ts new file mode 100644 index 00000000..46de34dc --- /dev/null +++ b/packages/react/src/utils/__tests__/keyframes.test.ts @@ -0,0 +1,19 @@ +import { keyframes } from '../keyframes' + +describe('keyframes', () => { + it('should return animation', () => { + expect(() => + keyframes({ + from: { + opacity: 0, + }, + to: { + opacity: 1, + }, + }), + ).toThrowError('Cannot run on the runtime') + expect(() => keyframes`from{opacity:0}to{opacity:1}`).toThrowError( + 'Cannot run on the runtime', + ) + }) +}) diff --git a/packages/react/src/utils/keyframes.ts b/packages/react/src/utils/keyframes.ts new file mode 100644 index 00000000..79d8b20e --- /dev/null +++ b/packages/react/src/utils/keyframes.ts @@ -0,0 +1,16 @@ +import type { DevupCommonProps } from '../types/props' + +export function keyframes( + props: Record<(string & {}) | 'from' | 'to', DevupCommonProps>, +): string +export function keyframes(strings: TemplateStringsArray): string +export function keyframes(): string + +export function keyframes( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + strings?: + | TemplateStringsArray + | Record<(string & {}) | 'from' | 'to', DevupCommonProps>, +): string { + throw new Error('Cannot run on the runtime') +} diff --git a/packages/react/src/utils/reset-css.ts b/packages/react/src/utils/reset-css.ts new file mode 100644 index 00000000..b19ce675 --- /dev/null +++ b/packages/react/src/utils/reset-css.ts @@ -0,0 +1,144 @@ +import { globalCss } from '@devup-ui/react' + +globalCss({ + '*,:after,:before': { + boxSizing: 'border-box', + backgroundRepeat: 'no-repeat', + }, + ':after,:before': { + textDecoration: 'inherit', + verticalAlign: 'inherit', + }, + ':where(:root)': { + cursor: 'default', + lineHeight: 1.5, + overflowWrap: 'break-word', + MozTabSize: 4, + tabSize: 4, + WebkitTapHighlightColor: 'transparent', + WebkitTextSizeAdjust: '100%', + }, + ':where(body)': { + m: 0, + }, + ':where(h1)': { + fontSize: '2em', + m: '.67em 0', + }, + ':where(dl,ol,ul) :where(dl,ol,ul)': { + m: 0, + }, + ':where(hr)': { + color: 'inherit', + h: 0, + }, + ':where(nav) :where(ol,ul)': { + listStyleType: 'none', + p: 0, + }, + ':where(nav li):before': { + content: '"\\200B"', + float: 'left', + }, + ':where(pre)': { + fontFamily: 'monospace,monospace', + fontSize: '1em', + overflow: 'auto', + }, + ':where(abbr[title])': { + textDecoration: 'underline dotted', + WebkitTextDecorationStyle: 'dotted', + WebkitTextDecorationLine: 'underline', + }, + ':where(b,strong)': { + fontWeight: 'bolder', + }, + ':where(code,kbd,samp)': { + fontFamily: 'monospace,monospace', + fontSize: '1em', + }, + ':where(small)': { + fontSize: '80%', + }, + ':where(audio,canvas,iframe,img,svg,video)': { + verticalAlign: 'middle', + }, + ':where(iframe)': { + borderStyle: 'none', + }, + ':where(svg:not([fill]))': { + fill: 'currentColor', + }, + ':where(table)': { + borderCollapse: 'collapse', + borderColor: 'inherit', + textIndent: 0, + }, + ':where(button,input,select)': { + m: 0, + }, + ':where(button,[type=button i],[type=reset i],[type=submit i])': { + WebkitAppearance: 'button', + }, + ':where(fieldset)': { + border: '1px solid #a0a0a0', + }, + ':where(progress)': { + verticalAlign: 'baseline', + }, + ':where(textarea)': { + m: 0, + resize: 'vertical', + }, + ':where([type=search i])': { + WebkitAppearance: 'textfield', + outlineOffset: '-2px', + }, + '::-webkit-inner-spin-button,::-webkit-outer-spin-button': { + height: 'auto', + }, + '::-webkit-input-placeholder': { + color: 'inherit', + opacity: 0.54, + }, + '::-webkit-search-decoration': { + WebkitAppearance: 'none', + }, + '::-webkit-file-upload-button': { + WebkitAppearance: 'button', + font: 'inherit', + }, + ':where(dialog)': { + bgColor: 'white', + border: 'solid', + color: 'black', + left: 0, + m: 'auto', + p: '1em', + pos: 'absolute', + right: 0, + w: 'fit-content', + h: 'fit-content', + }, + ':where(dialog:not([open]))': { + display: 'none', + }, + ':where(details>summary:first-of-type)': { + display: 'list-item', + }, + ':where([aria-busy=true i])': { + cursor: 'progress', + }, + ':where([aria-controls])': { + cursor: 'pointer', + }, + ':where([aria-disabled=true i],[disabled])': { + cursor: 'not-allowed', + }, + ':where([aria-hidden=false i][hidden])': { + display: 'initial', + }, + ':where([aria-hidden=false i][hidden]:not(:focus))': { + pos: 'absolute', + }, +}) diff --git a/packages/reset-css/README.md b/packages/reset-css/README.md new file mode 100644 index 00000000..fa2f0763 --- /dev/null +++ b/packages/reset-css/README.md @@ -0,0 +1,145 @@ +
+ Devup UI logo +
+ + +

+ Zero Config, Zero FOUC, Zero Runtime, CSS in JS Preprocessor +

+ +--- + +
+ + +Github Checks +Apache-2.0 License + +NPM Downloads + + +Github Stars + + +Discord + + + + +
+ +--- + +English | [한국어](README_ko.md) + +## Install + +```sh +npm install @devup-ui/react + +# on next.js +npm install @devup-ui/next-plugin + +# on vite +npm install @devup-ui/vite-plugin +``` + +## Features + +- Preprocessor +- Zero Config +- Zero FOUC +- Zero Runtime +- RSC Support +- Must not use JavaScript, client-side logic, or hybrid solutions +- Support Library mode +- Zero Cost Dynamic Theme Support based on CSS Variables +- Theme with Typing +- Smallest size, fastest speed + +## Inspirations + +- Styled System +- Chakra UI +- Theme UI +- Vanilla Extract +- Rainbow Sprinkles +- Kuma UI + +## Comparison Benchmarks + +Next.js Build Time and Build Size (AMD Ryzen 9 9950X, 128GB RAM, Windows 11) + +| Library | Build Time | Build Size | +|-----------|------------|--------------| +| kuma-ui | 20.933s | 57,295,073b | +| chakra-ui | 36.961s | 129,527,610b | +| devup-ui | 15.162s | 48,047,678b | + +## How it works + +Devup UI is a CSS in JS preprocessor that does not require runtime. +Devup UI eliminates the performance degradation of the browser through the CSS in JS preprocessor. +We develop a preprocessor that considers all grammatical cases. + +```typescript +const before = + +const after =
+``` + +Variables are fully supported. + +```typescript +const before = + +const after =
+``` + +Various expressions and responsiveness are also fully supported. + +```typescript +const before = b ? "yellow" : variable]}/> + +const after =
b ? "d2" : "d3"}`} style={{ + "--d2": variable +}}/> +``` + +Support Theme with Typing + +`devup.json` + +```json +{ + "theme": { + "colors": { + "default": { + "text": "#000" + }, + "dark": { + "text": "white" + } + } + } +} +``` + +```typescript +// Type Safe + +``` + +Support Responsive And Pseudo Selector + +You can use responsive and pseudo selector. + +```typescript +// Responsive with Selector +const box = + +// Same +const box = +``` diff --git a/packages/reset-css/package.json b/packages/reset-css/package.json new file mode 100644 index 00000000..4aecaa37 --- /dev/null +++ b/packages/reset-css/package.json @@ -0,0 +1,52 @@ +{ + "name": "@devup-ui/reset-css", + "description": "Zero Config, Zero FOUC, Zero Runtime, CSS in JS Preprocessor", + "repository": "https://github.com/dev-five-git/devup-ui", + "author": "devfive", + "license": "Apache-2.0", + "homepage": "https://devup-ui.com", + "bugs": { + "url": "https://github.com/dev-five-git/devup-ui/issues", + "email": "contact@devfive.kr" + }, + "keywords": [ + "css", + "css-in-js", + "css-in-js-preprocessor", + "css-in-js-framework", + "react" + ], + "version": "0.1.0", + "type": "module", + "scripts": { + "lint": "eslint", + "build": "tsc && vite build" + }, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist" + ], + "types": "./dist/index.d.ts", + "dependencies": { + "@devup-ui/react": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vite": "^7.0.3", + "vite-plugin-dts": "^4.5.4" + }, + "peerDependencies": { + "@devup-ui/react": "workspace:*" + } +} \ No newline at end of file diff --git a/packages/reset-css/src/index.ts b/packages/reset-css/src/index.ts new file mode 100644 index 00000000..643b5cfb --- /dev/null +++ b/packages/reset-css/src/index.ts @@ -0,0 +1,146 @@ +import { globalCss } from '@devup-ui/react' + +globalCss({ + '*,:after,:before': { + boxSizing: 'border-box', + backgroundRepeat: 'no-repeat', + }, + ':after,:before': { + textDecoration: 'inherit', + verticalAlign: 'inherit', + }, + ':where(:root)': { + cursor: 'default', + lineHeight: 1.5, + overflowWrap: 'break-word', + MozTabSize: 4, + tabSize: 4, + WebkitTapHighlightColor: 'transparent', + WebkitTextSizeAdjust: '100%', + }, + ':where(body)': { + m: 0, + }, + ':where(h1)': { + fontSize: '2em', + m: '.67em 0', + }, + ':where(dl,ol,ul) :where(dl,ol,ul)': { + m: 0, + }, + ':where(hr)': { + color: 'inherit', + h: 0, + }, + ':where(nav) :where(ol,ul)': { + listStyleType: 'none', + p: 0, + }, + ':where(nav li):before': { + content: '"\\200B"', + float: 'left', + }, + ':where(pre)': { + fontFamily: 'monospace,monospace', + fontSize: '1em', + overflow: 'auto', + }, + ':where(abbr[title])': { + textDecoration: 'underline dotted', + WebkitTextDecorationStyle: 'dotted', + WebkitTextDecorationLine: 'underline', + }, + ':where(b,strong)': { + fontWeight: 'bolder', + }, + ':where(code,kbd,samp)': { + fontFamily: 'monospace,monospace', + fontSize: '1em', + }, + ':where(small)': { + fontSize: '80%', + }, + ':where(audio,canvas,iframe,img,svg,video)': { + verticalAlign: 'middle', + }, + ':where(iframe)': { + borderStyle: 'none', + }, + ':where(svg:not([fill]))': { + fill: 'currentColor', + }, + ':where(table)': { + borderCollapse: 'collapse', + borderColor: 'inherit', + textIndent: 0, + }, + ':where(button,input,select)': { + m: 0, + }, + ':where(button,[type=button i],[type=reset i],[type=submit i])': { + WebkitAppearance: 'button', + }, + ':where(fieldset)': { + border: '1px solid #a0a0a0', + }, + ':where(progress)': { + verticalAlign: 'baseline', + }, + ':where(textarea)': { + m: 0, + resize: 'vertical', + }, + ':where([type=search i])': { + WebkitAppearance: 'textfield', + outlineOffset: '-2px', + }, + '::-webkit-inner-spin-button,::-webkit-outer-spin-button': { + height: 'auto', + }, + '::-webkit-input-placeholder': { + color: 'inherit', + opacity: 0.54, + }, + '::-webkit-search-decoration': { + WebkitAppearance: 'none', + }, + '::-webkit-file-upload-button': { + WebkitAppearance: 'button', + font: 'inherit', + }, + ':where(dialog)': { + bgColor: 'white', + border: 'solid', + color: 'black', + left: 0, + m: 'auto', + p: '1em', + pos: 'absolute', + right: 0, + w: 'fit-content', + h: 'fit-content', + }, + ':where(dialog:not([open]))': { + display: 'none', + }, + ':where(details>summary:first-of-type)': { + display: 'list-item', + }, + ':where([aria-busy=true i])': { + cursor: 'progress', + }, + ':where([aria-controls])': { + cursor: 'pointer', + }, + ':where([aria-disabled=true i],[disabled])': { + cursor: 'not-allowed', + }, + ':where([aria-hidden=false i][hidden])': { + display: 'initial', + }, + ':where([aria-hidden=false i][hidden]:not(:focus))': { + pos: 'absolute', + }, +}) + +export function resetCss(): void {} diff --git a/packages/reset-css/tsconfig.json b/packages/reset-css/tsconfig.json new file mode 100644 index 00000000..8b53b487 --- /dev/null +++ b/packages/reset-css/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "types": [ + "vite/client", + "vitest/importMeta", + "vitest/globals" + ], + "strict": true, + "target": "ESNext", + "declaration": true, + "declarationMap": true, + "removeComments": true, + "sourceMap": true, + "useDefineForClassFields": true, + "allowJs": false, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strictFunctionTypes": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "baseUrl": ".", + "jsx": "react-jsx" + } +} \ No newline at end of file diff --git a/packages/reset-css/vite.config.ts b/packages/reset-css/vite.config.ts new file mode 100644 index 00000000..6893a9b2 --- /dev/null +++ b/packages/reset-css/vite.config.ts @@ -0,0 +1,65 @@ +import preserveDirectives from 'rollup-plugin-preserve-directives' +import dts from 'vite-plugin-dts' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + coverage: { + all: true, + thresholds: { + '100': true, + }, + }, + }, + plugins: [ + dts({ + entryRoot: 'src', + staticImport: true, + pathsToAliases: false, + exclude: [ + '**/__tests__/**/*', + '**/*.test.(tsx|ts|js|jsx)', + '**/*.test-d.(tsx|ts|js|jsx)', + 'vite.config.ts', + ], + include: ['**/src/**/*.ts', '**/src/**/*.tsx'], + copyDtsFiles: true, + compilerOptions: { + isolatedModules: false, + declaration: true, + }, + }) as any, + ], + build: { + rollupOptions: { + onwarn: (warning) => { + if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { + return + } + }, + plugins: [preserveDirectives()], + external: (source) => { + return !(source.includes('src') || source.startsWith('.')) + }, + + output: { + dir: 'dist', + preserveModules: true, + preserveModulesRoot: 'src', + + exports: 'named', + assetFileNames({ name }) { + return name?.replace(/^src\//g, '') ?? '' + }, + }, + }, + lib: { + formats: ['es', 'cjs'], + entry: { + index: 'src/index.ts', + }, + }, + outDir: 'dist', + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 131a072a..55858fd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@devup-ui/react': specifier: workspace:* version: link:../../packages/react + '@devup-ui/reset-css': + specifier: workspace:* + version: link:../../packages/reset-css '@mdx-js/loader': specifier: ^3.1.0 version: 3.1.0(acorn@8.15.0)(webpack@5.99.9) @@ -86,9 +89,6 @@ importers: remark-gfm: specifier: ^4.0.1 version: 4.0.1 - sanitize.css: - specifier: ^13.0.0 - version: 13.0.0 devDependencies: '@devup-ui/next-plugin': specifier: workspace:* @@ -445,6 +445,22 @@ importers: specifier: ^4.5.4 version: 4.5.4(@types/node@24.0.12)(rollup@4.44.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.12)(jiti@2.4.2)(terser@5.43.1)) + packages/reset-css: + dependencies: + '@devup-ui/react': + specifier: workspace:* + version: link:../react + devDependencies: + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vite: + specifier: ^6 + version: 6.3.5(@types/node@24.0.12)(jiti@2.4.2)(terser@5.43.1) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@24.0.12)(rollup@4.44.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.12)(jiti@2.4.2)(terser@5.43.1)) + packages/rsbuild-plugin: dependencies: '@devup-ui/wasm': @@ -4820,9 +4836,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sanitize.css@13.0.0: - resolution: {integrity: sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==} - scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -11052,8 +11065,6 @@ snapshots: safer-buffer@2.1.2: {} - sanitize.css@13.0.0: {} - scheduler@0.26.0: {} schema-utils@4.3.2: diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 33481a20..00000000 --- a/vitest.config.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { DevupUI } from '@devup-ui/vite-plugin' -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - coverage: { - provider: 'v8', - include: ['packages/*/src/**'], - exclude: [ - 'packages/*/src/types', - 'packages/*/src/**/__tests__', - '**/*.stories.{ts,tsx}', - ], - cleanOnRerun: true, - reporter: ['text', 'json', 'html'], - }, - projects: [ - { - test: { - name: 'node', - include: ['packages/*/src/**/__tests__/**/*.test.{ts,tsx}'], - exclude: ['packages/*/src/**/__tests__/**/*.browser.test.{ts,tsx}'], - globals: true, - environment: 'node', - }, - }, - - { - test: { - name: 'happy-dom', - include: ['packages/*/src/**/__tests__/**/*.browser.test.{ts,tsx}'], - environment: 'happy-dom', - globals: true, - css: true, - setupFiles: ['@testing-library/jest-dom/vitest'], - }, - plugins: [ - DevupUI({ - debug: true, - }), - ], - }, - ], - cache: false, - }, -}) From 62bac54dad65ddceb21d1ca2b069343af9131640 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 13:13:31 +0900 Subject: [PATCH 02/11] Refactor --- libs/extractor/src/css_type.rs | 34 - .../extract_global_style_from_expression.rs | 104 ++ .../extract_style_from_expression.rs | 524 ++++++++++ .../src/extractor/extract_style_from_jsx.rs | 40 + .../extract_style_from_member_expression.rs | 232 +++++ libs/extractor/src/extractor/mod.rs | 30 + libs/extractor/src/lib.rs | 49 +- libs/extractor/src/prop_modify_utils.rs | 24 +- ...ts__extract_conditional_style_props-7.snap | 2 +- ...ts__extract_conditional_style_props-8.snap | 2 +- ...ts__extract_conditional_style_props-9.snap | 2 +- ...xtractor__tests__extract_global_css-2.snap | 2 +- ...xtractor__tests__extract_global_css-3.snap | 2 +- ...xtractor__tests__extract_global_css-4.snap | 2 +- .../extractor__tests__extract_global_css.snap | 2 +- ...ests__extract_global_css_with_empty-2.snap | 2 +- ...ests__extract_global_css_with_empty-3.snap | 2 +- ...ests__extract_global_css_with_empty-4.snap | 2 +- ...ests__extract_global_css_with_empty-5.snap | 2 +- ..._tests__extract_global_css_with_empty.snap | 2 +- ...ts__extract_global_css_with_imports-2.snap | 2 +- ...ts__extract_global_css_with_imports-3.snap | 2 +- ...ests__extract_global_css_with_imports.snap | 2 +- ...s__extract_global_css_with_selector-2.snap | 2 +- ...s__extract_global_css_with_selector-3.snap | 2 +- ...s__extract_global_css_with_selector-4.snap | 2 +- ...sts__extract_global_css_with_selector.snap | 2 +- ...ract_global_css_with_template_literal.snap | 2 +- ...extract_global_css_with_wrong_imports.snap | 15 + ...extractor__tests__extract_style_props.snap | 2 +- ...ctor__tests__extract_wrong_global_css.snap | 8 + ..._extract_wrong_responsive_style_props.snap | 2 +- .../extractor__tests__style_order3.snap | 71 ++ ...actor__tests__support_transpile_mjs-5.snap | 2 +- libs/extractor/src/style_extractor.rs | 972 ------------------ libs/extractor/src/util_type.rs | 38 + libs/extractor/src/visit.rs | 148 +-- 37 files changed, 1223 insertions(+), 1112 deletions(-) delete mode 100644 libs/extractor/src/css_type.rs create mode 100644 libs/extractor/src/extractor/extract_global_style_from_expression.rs create mode 100644 libs/extractor/src/extractor/extract_style_from_expression.rs create mode 100644 libs/extractor/src/extractor/extract_style_from_jsx.rs create mode 100644 libs/extractor/src/extractor/extract_style_from_member_expression.rs create mode 100644 libs/extractor/src/extractor/mod.rs create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__style_order3.snap delete mode 100644 libs/extractor/src/style_extractor.rs create mode 100644 libs/extractor/src/util_type.rs diff --git a/libs/extractor/src/css_type.rs b/libs/extractor/src/css_type.rs deleted file mode 100644 index edfa00da..00000000 --- a/libs/extractor/src/css_type.rs +++ /dev/null @@ -1,34 +0,0 @@ -#[derive(Debug, PartialEq)] -pub enum CssType { - Css, - GlobalCss, -} - -impl TryFrom for CssType { - type Error = String; - - fn try_from(value: String) -> Result { - if value == "css" { - Ok(CssType::Css) - } else if value == "globalCss" { - Ok(CssType::GlobalCss) - } else { - Err(value) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - - #[rstest] - #[case("css".to_string(), Ok(CssType::Css))] - #[case("globalCss".to_string(), Ok(CssType::GlobalCss))] - #[case("unknown".to_string(), Err("unknown".to_string()))] - #[case("".to_string(), Err("".to_string()))] - fn test_css_type_try_from(#[case] input: String, #[case] expected: Result) { - assert_eq!(CssType::try_from(input), expected); - } -} diff --git a/libs/extractor/src/extractor/extract_global_style_from_expression.rs b/libs/extractor/src/extractor/extract_global_style_from_expression.rs new file mode 100644 index 00000000..9b4ce34e --- /dev/null +++ b/libs/extractor/src/extractor/extract_global_style_from_expression.rs @@ -0,0 +1,104 @@ +use crate::{ + ExtractStyleProp, + extract_style::{extract_import::ExtractImport, extract_style_value::ExtractStyleValue}, + extractor::{ + ExtractResult, GlobalExtractResult, + extract_style_from_expression::extract_style_from_expression, + }, +}; +use css::style_selector::StyleSelector; +use oxc_ast::{ + AstBuilder, + ast::{ArrayExpressionElement, Expression, ObjectPropertyKind, PropertyKey}, +}; + +pub fn extract_global_style_from_expression<'a>( + ast_builder: &AstBuilder<'a>, + expression: &mut Expression<'a>, + file: &str, +) -> GlobalExtractResult<'a> { + let mut styles = vec![]; + + if let Expression::ObjectExpression(obj) = expression { + for p in obj.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(o) = p { + let name = if let PropertyKey::StaticIdentifier(ident) = &o.key { + ident.name.to_string() + } else if let PropertyKey::StringLiteral(s) = &o.key { + s.value.to_string() + } else if let PropertyKey::TemplateLiteral(t) = &o.key { + t.quasis + .iter() + .map(|q| q.value.raw.as_str()) + .collect::>() + .join("") + } else { + continue; + }; + + if name == "imports" { + if let Expression::ArrayExpression(arr) = &o.value { + for p in arr.elements.iter() { + styles.push(ExtractStyleProp::Static(ExtractStyleValue::Import( + ExtractImport { + url: if let ArrayExpressionElement::StringLiteral(s) = p { + s.value.trim().to_string() + } else if let ArrayExpressionElement::TemplateLiteral(t) = p { + t.quasis + .iter() + .map(|q| q.value.raw.as_str()) + .collect::>() + .join("") + .trim() + .to_string() + } else { + continue; + }, + file: file.to_string(), + }, + ))); + } + } + continue; + } + styles.extend( + extract_style_from_expression( + ast_builder, + None, + &mut o.value, + 0, + Some(&StyleSelector::Global(name.clone(), file.to_string())), + ) + .styles, + ); + + // if let ExtractResult::Extract { + // styles: Some(_styles), + // .. + // } = extract_style_from_expression( + // ast_builder, + // None, + // &mut o.value, + // 0, + // Some(&StyleSelector::Global(name.clone(), file.to_string())), + // ) { + // styles.extend( + // extract_style_from_expression( + // ast_builder, + // None, + // &mut o.value, + // 0, + // Some(&StyleSelector::Global(name.clone(), file.to_string())), + // ) + // .styles + // .iter() + // ); + // } + } + } + } + GlobalExtractResult { + styles, + style_order: None, + } +} diff --git a/libs/extractor/src/extractor/extract_style_from_expression.rs b/libs/extractor/src/extractor/extract_style_from_expression.rs new file mode 100644 index 00000000..7df0a965 --- /dev/null +++ b/libs/extractor/src/extractor/extract_style_from_expression.rs @@ -0,0 +1,524 @@ +use crate::{ + ExtractStyleProp, + css_utils::css_to_style, + extract_style::{ + extract_dynamic_style::ExtractDynamicStyle, extract_static_style::ExtractStaticStyle, + extract_style_value::ExtractStyleValue, + }, + extractor::{ + ExtractResult, extract_style_from_member_expression::extract_style_from_member_expression, + }, + utils::{ + expression_to_code, get_number_by_literal_expression, get_string_by_literal_expression, + is_same_expression, is_special_property, + }, +}; +use css::style_selector::StyleSelector; +use oxc_allocator::CloneIn; +use oxc_ast::{ + AstBuilder, + ast::{ + BinaryOperator, Expression, LogicalOperator, ObjectPropertyKind, PropertyKey, + TemplateElementValue, + }, +}; +use oxc_span::SPAN; + +const IGNORED_IDENTIFIERS: [&str; 3] = ["undefined", "NaN", "Infinity"]; + +pub fn extract_style_from_expression<'a>( + ast_builder: &AstBuilder<'a>, + name: Option<&str>, + expression: &mut Expression<'a>, + level: u8, + selector: Option<&StyleSelector>, +) -> ExtractResult<'a> { + let mut typo = false; + + if name.is_none() && selector.is_none() { + let mut style_order = None; + let mut style_vars = None; + return match expression { + Expression::ObjectExpression(obj) => { + let mut props_styles: Vec> = vec![]; + let mut tag = None; + for idx in (0..obj.properties.len()).rev() { + let mut prop = obj.properties.remove(idx); + if !match &mut prop { + ObjectPropertyKind::ObjectProperty(prop) => { + if let PropertyKey::StaticIdentifier(ident) = &prop.key + && let name = ident.name.as_str() + && !is_special_property(name) + { + if name == "styleOrder" { + style_order = get_number_by_literal_expression(&prop.value) + .map(|v| v as u8); + continue; + } + if name == "styleVars" { + style_vars = Some(prop.value.clone_in(ast_builder.allocator)); + continue; + } + + let ExtractResult { + styles, tag: _tag, .. + } = extract_style_from_expression( + ast_builder, + Some(name), + &mut prop.value, + 0, + None, + ); + props_styles.extend(styles); + tag = _tag.or(tag); + true + } else { + false + } + } + ObjectPropertyKind::SpreadProperty(prop) => { + let ExtractResult { + styles, tag: _tag, .. + } = extract_style_from_expression( + ast_builder, + None, + &mut prop.argument, + 0, + None, + ); + props_styles.extend(styles); + tag = _tag.or(tag); + false + } + } { + obj.properties.insert(idx, prop); + } + } + ExtractResult { + styles: props_styles, + tag, + style_order, + style_vars, + } + } + Expression::ConditionalExpression(conditional) => ExtractResult { + styles: vec![ExtractStyleProp::Conditional { + condition: conditional.test.clone_in(ast_builder.allocator), + consequent: Some(Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + None, + &mut conditional.consequent, + level, + None, + ) + .styles, + ))), + alternate: Some(Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + None, + &mut conditional.alternate, + level, + selector, + ) + .styles, + ))), + }], + tag: None, + style_order, + style_vars, + }, + Expression::ParenthesizedExpression(parenthesized) => extract_style_from_expression( + ast_builder, + None, + &mut parenthesized.expression, + level, + None, + ), + _ => ExtractResult::default(), + }; + } + + if let Some(name) = name { + if is_special_property(name) { + return ExtractResult::default(); + } + + if name == "as" { + return ExtractResult { + tag: Some(expression.clone_in(ast_builder.allocator)), + ..ExtractResult::default() + }; + } + if name == "selectors" + && let Expression::ObjectExpression(obj) = expression + { + let mut props = vec![]; + for p in obj.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(o) = p { + let name = o.key.name().unwrap().to_string(); + props.extend( + extract_style_from_expression( + ast_builder, + None, + &mut o.value, + level, + Some( + &if let Some(selector) = selector { + name.replace("&", &selector.to_string()) + } else { + name + } + .as_str() + .into(), + ), + ) + .styles, + ); + } + } + return ExtractResult { + styles: props, + ..ExtractResult::default() + }; + } + + if let Some(new_selector) = name.strip_prefix("_") { + return extract_style_from_expression( + ast_builder, + None, + expression, + level, + Some(&if let Some(selector) = selector { + (selector, new_selector).into() + } else { + new_selector.into() + }), + ); + } + typo = name == "typography"; + } + if let Some(value) = get_string_by_literal_expression(expression) { + if let Some(name) = name { + ExtractResult { + styles: vec![ExtractStyleProp::Static(if typo { + ExtractStyleValue::Typography(value.to_string()) + } else { + ExtractStyleValue::Static(ExtractStaticStyle::new( + name, + &value, + level, + selector.cloned(), + )) + })], + ..ExtractResult::default() + } + } else { + ExtractResult { + styles: css_to_style(&value, level, &selector), + ..ExtractResult::default() + } + } + } else { + match expression { + Expression::UnaryExpression(_) + | Expression::BinaryExpression(_) + | Expression::StaticMemberExpression(_) + | Expression::CallExpression(_) => ExtractResult { + styles: vec![ExtractStyleProp::Static(ExtractStyleValue::Dynamic( + ExtractDynamicStyle::new( + name.unwrap(), + level, + &expression_to_code(expression), + selector.cloned(), + ), + ))], + ..ExtractResult::default() + }, + Expression::TSAsExpression(exp) => extract_style_from_expression( + ast_builder, + name, + &mut exp.expression, + level, + selector, + ), + Expression::ComputedMemberExpression(mem) => { + extract_style_from_member_expression(ast_builder, name, mem, level, selector) + } + Expression::TemplateLiteral(tmp) => { + if let Some(name) = name { + if tmp.quasis.len() == 1 { + ExtractResult { + styles: vec![ExtractStyleProp::Static(if typo { + ExtractStyleValue::Typography(tmp.quasis[0].value.raw.to_string()) + } else { + ExtractStyleValue::Static(ExtractStaticStyle::new( + name, + &tmp.quasis[0].value.raw, + level, + selector.cloned(), + )) + })], + ..ExtractResult::default() + } + } else if typo { + ExtractResult { + styles: vec![ExtractStyleProp::Expression { + expression: ast_builder.expression_template_literal( + SPAN, + ast_builder.vec_from_array([ + ast_builder.template_element( + SPAN, + TemplateElementValue { + raw: ast_builder.atom("typo-"), + cooked: None, + }, + false, + ), + ast_builder.template_element( + SPAN, + TemplateElementValue { + raw: ast_builder.atom(""), + cooked: None, + }, + true, + ), + ]), + ast_builder.vec_from_array([ + expression.clone_in(ast_builder.allocator) + ]), + ), + styles: vec![], + }], + ..ExtractResult::default() + } + } else { + ExtractResult { + styles: vec![ExtractStyleProp::Static(ExtractStyleValue::Dynamic( + ExtractDynamicStyle::new( + name, + level, + &expression_to_code(expression), + selector.cloned(), + ), + ))], + ..ExtractResult::default() + } + } + } else { + ExtractResult::default() + } + } + Expression::Identifier(identifier) => { + if IGNORED_IDENTIFIERS.contains(&identifier.name.as_str()) { + ExtractResult::default() + } else if let Some(name) = name { + if typo { + ExtractResult { + styles: vec![ExtractStyleProp::Expression { + expression: ast_builder.expression_conditional( + SPAN, + ast_builder + .expression_identifier(SPAN, identifier.name.as_str()), + ast_builder.expression_template_literal( + SPAN, + ast_builder.vec_from_array([ + ast_builder.template_element( + SPAN, + TemplateElementValue { + raw: ast_builder.atom("typo-"), + cooked: None, + }, + false, + ), + ast_builder.template_element( + SPAN, + TemplateElementValue { + raw: ast_builder.atom(""), + cooked: None, + }, + true, + ), + ]), + ast_builder.vec_from_array([ + expression.clone_in(ast_builder.allocator) + ]), + ), + ast_builder.expression_string_literal(SPAN, "", None), + ), + styles: vec![], + }], + ..ExtractResult::default() + } + } else { + ExtractResult { + styles: vec![ExtractStyleProp::Static(ExtractStyleValue::Dynamic( + ExtractDynamicStyle::new( + name, + level, + &identifier.name, + selector.cloned(), + ), + ))], + ..ExtractResult::default() + } + } + } else { + ExtractResult::default() + } + } + Expression::LogicalExpression(logical) => { + let res = Some(Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + name, + &mut logical.right, + level, + selector, + ) + .styles, + ))); + match logical.operator { + LogicalOperator::Or => ExtractResult { + styles: vec![ExtractStyleProp::Conditional { + condition: logical.left.clone_in(ast_builder.allocator), + consequent: None, + alternate: res, + }], + ..ExtractResult::default() + }, + LogicalOperator::And => ExtractResult { + styles: vec![ExtractStyleProp::Conditional { + condition: logical.left.clone_in(ast_builder.allocator), + consequent: res, + alternate: None, + }], + ..ExtractResult::default() + }, + LogicalOperator::Coalesce => ExtractResult { + styles: vec![ExtractStyleProp::Conditional { + condition: ast_builder.expression_logical( + SPAN, + ast_builder.expression_binary( + SPAN, + logical.left.clone_in(ast_builder.allocator), + BinaryOperator::StrictInequality, + ast_builder.expression_null_literal(SPAN), + ), + LogicalOperator::And, + ast_builder.expression_binary( + SPAN, + logical.left.clone_in(ast_builder.allocator), + BinaryOperator::StrictInequality, + ast_builder.expression_identifier(SPAN, "undefined"), + ), + ), + consequent: Some(Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + name, + &mut logical.left, + level, + selector, + ) + .styles, + ))), + alternate: res, + }], + ..ExtractResult::default() + }, + } + } + Expression::ParenthesizedExpression(parenthesized) => extract_style_from_expression( + ast_builder, + name, + &mut parenthesized.expression, + level, + selector, + ), + Expression::ArrayExpression(array) => { + let mut props = vec![]; + + for (idx, element) in array.elements.iter_mut().enumerate() { + props.extend( + extract_style_from_expression( + ast_builder, + name, + element.to_expression_mut(), + idx as u8, + selector, + ) + .styles, + ); + } + ExtractResult { + styles: vec![ExtractStyleProp::StaticArray(props)], + tag: None, + style_order: None, + style_vars: None, + } + } + Expression::ConditionalExpression(conditional) => { + if is_same_expression(&conditional.consequent, &conditional.alternate) { + extract_style_from_expression( + ast_builder, + name, + &mut conditional.consequent, + level, + selector, + ) + } else { + ExtractResult { + styles: vec![ExtractStyleProp::Conditional { + condition: conditional.test.clone_in(ast_builder.allocator), + consequent: Some(Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + name, + &mut conditional.consequent, + level, + selector, + ) + .styles, + ))), + alternate: Some(Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + name, + &mut conditional.alternate, + level, + selector, + ) + .styles, + ))), + }], + ..ExtractResult::default() + } + } + } + Expression::ObjectExpression(obj) => { + let mut props = vec![]; + for p in obj.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(o) = p { + props.extend( + extract_style_from_expression( + ast_builder, + Some(&o.key.name().unwrap()), + &mut o.value, + level, + selector, + ) + .styles, + ); + } + } + ExtractResult { + styles: props, + ..ExtractResult::default() + } + } + _ => ExtractResult::default(), + } + } +} diff --git a/libs/extractor/src/extractor/extract_style_from_jsx.rs b/libs/extractor/src/extractor/extract_style_from_jsx.rs new file mode 100644 index 00000000..a44c03f5 --- /dev/null +++ b/libs/extractor/src/extractor/extract_style_from_jsx.rs @@ -0,0 +1,40 @@ +use crate::extractor::{ + ExtractResult, extract_style_from_expression::extract_style_from_expression, +}; +use css::style_selector::StyleSelector; +use oxc_allocator::CloneIn; +use oxc_ast::{ + AstBuilder, + ast::{Expression, JSXAttributeValue}, +}; + +pub fn extract_style_from_jsx<'a>( + ast_builder: &AstBuilder<'a>, + name: &str, + value: &mut JSXAttributeValue<'a>, + selector: Option<&StyleSelector>, +) -> ExtractResult<'a> { + match value { + JSXAttributeValue::ExpressionContainer(expression) => { + if expression.expression.is_expression() { + extract_style_from_expression( + ast_builder, + Some(name), + expression.expression.to_expression_mut(), + 0, + selector, + ) + } else { + ExtractResult::default() + } + } + JSXAttributeValue::StringLiteral(literal) => extract_style_from_expression( + ast_builder, + Some(name), + &mut Expression::StringLiteral(literal.clone_in(ast_builder.allocator)), + 0, + selector, + ), + _ => ExtractResult::default(), + } +} diff --git a/libs/extractor/src/extractor/extract_style_from_member_expression.rs b/libs/extractor/src/extractor/extract_style_from_member_expression.rs new file mode 100644 index 00000000..5e872c58 --- /dev/null +++ b/libs/extractor/src/extractor/extract_style_from_member_expression.rs @@ -0,0 +1,232 @@ +use crate::{ + ExtractStyleProp, + extract_style::{ + extract_dynamic_style::ExtractDynamicStyle, extract_style_value::ExtractStyleValue, + }, + extractor::{ExtractResult, extract_style_from_expression::extract_style_from_expression}, + utils::{ + expression_to_code, get_number_by_literal_expression, get_string_by_literal_expression, + }, +}; +use css::style_selector::StyleSelector; +use oxc_allocator::CloneIn; +use oxc_ast::{ + AstBuilder, + ast::{ + ArrayExpressionElement, ComputedMemberExpression, Expression, ObjectPropertyKind, + PropertyKey, + }, +}; +use oxc_span::SPAN; +use std::collections::BTreeMap; + +pub(super) fn extract_style_from_member_expression<'a>( + ast_builder: &AstBuilder<'a>, + name: Option<&str>, + mem: &mut ComputedMemberExpression<'a>, + level: u8, + selector: Option<&StyleSelector>, +) -> ExtractResult<'a> { + let mem_expression = &mem.expression.clone_in(ast_builder.allocator); + let mut ret: Vec = vec![]; + + match &mut mem.object { + Expression::ArrayExpression(array) => { + if array.elements.is_empty() { + return ExtractResult::default(); + } + + if let Some(num) = get_number_by_literal_expression(mem_expression) { + if num < 0f64 { + return ExtractResult::default(); + } + let mut etc = None; + for (idx, p) in array.elements.iter_mut().enumerate() { + if let ArrayExpressionElement::SpreadElement(sp) = p { + etc = Some(sp.argument.clone_in(ast_builder.allocator)); + continue; + } + if idx as f64 == num { + return extract_style_from_expression( + ast_builder, + name, + p.to_expression_mut(), + level, + selector, + ); + } + } + return ExtractResult { + styles: etc + .map(|etc| { + vec![ExtractStyleProp::Static(ExtractStyleValue::Dynamic( + ExtractDynamicStyle::new( + name.unwrap(), + level, + &expression_to_code(&Expression::ComputedMemberExpression( + ast_builder.alloc_computed_member_expression( + SPAN, + etc, + mem_expression.clone_in(ast_builder.allocator), + false, + ), + )), + selector.cloned(), + ), + ))] + }) + .unwrap_or_default(), + tag: None, + style_order: None, + style_vars: None, + }; + } + + let mut map = BTreeMap::new(); + for (idx, p) in array.elements.iter_mut().enumerate() { + if let ArrayExpressionElement::SpreadElement(sp) = p { + map.insert( + idx.to_string(), + Box::new(ExtractStyleProp::Static(ExtractStyleValue::Dynamic( + ExtractDynamicStyle::new( + name.unwrap(), + level, + &expression_to_code(&Expression::ComputedMemberExpression( + ast_builder.alloc_computed_member_expression( + SPAN, + sp.argument.clone_in(ast_builder.allocator), + mem_expression.clone_in(ast_builder.allocator), + false, + ), + )), + selector.cloned(), + ), + ))), + ); + } else { + map.insert( + idx.to_string(), + Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + name, + p.to_expression_mut(), + level, + selector, + ) + .styles, + )), + ); + } + } + + ret.push(ExtractStyleProp::MemberExpression { + expression: mem_expression.clone_in(ast_builder.allocator), + map, + }); + } + Expression::ObjectExpression(obj) => { + if obj.properties.is_empty() { + return ExtractResult::default(); + } + + let mut map = BTreeMap::new(); + if let Some(k) = get_string_by_literal_expression(mem_expression) { + let mut etc = None; + for p in obj.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(o) = p { + if let PropertyKey::StaticIdentifier(ref pk) = o.key + && pk.name == k + { + return ExtractResult { + styles: extract_style_from_expression( + ast_builder, + name, + &mut o.value, + level, + selector, + ) + .styles, + ..ExtractResult::default() + }; + } + } else if let ObjectPropertyKind::SpreadProperty(sp) = p { + etc = Some(sp.argument.clone_in(ast_builder.allocator)); + } + } + + match etc { + None => { + return ExtractResult::default(); + } + Some(etc) => ret.push(ExtractStyleProp::Static(ExtractStyleValue::Dynamic( + ExtractDynamicStyle::new( + name.unwrap(), + level, + &expression_to_code(&Expression::ComputedMemberExpression( + ast_builder.alloc_computed_member_expression( + SPAN, + etc, + mem_expression.clone_in(ast_builder.allocator), + false, + ), + )), + selector.cloned(), + ), + ))), + } + } + + for p in obj.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(o) = p + && let PropertyKey::StaticIdentifier(_) + | PropertyKey::NumericLiteral(_) + | PropertyKey::StringLiteral(_) = o.key + { + map.insert( + o.key.name().unwrap().to_string(), + Box::new(ExtractStyleProp::StaticArray( + extract_style_from_expression( + ast_builder, + name, + &mut o.value, + level, + selector, + ) + .styles, + )), + ); + } + } + ret.push(ExtractStyleProp::MemberExpression { + expression: mem_expression.clone_in(ast_builder.allocator), + map, + }); + } + Expression::Identifier(_) => { + if let Some(name) = name { + ret.push(ExtractStyleProp::Static(ExtractStyleValue::Dynamic( + ExtractDynamicStyle::new( + name, + level, + &expression_to_code(&Expression::ComputedMemberExpression( + ast_builder.alloc_computed_member_expression( + SPAN, + mem.object.clone_in(ast_builder.allocator), + mem_expression.clone_in(ast_builder.allocator), + false, + ), + )), + selector.cloned(), + ), + ))) + } + } + _ => {} + }; + + ExtractResult { + styles: ret, + ..ExtractResult::default() + } +} diff --git a/libs/extractor/src/extractor/mod.rs b/libs/extractor/src/extractor/mod.rs new file mode 100644 index 00000000..a69c7234 --- /dev/null +++ b/libs/extractor/src/extractor/mod.rs @@ -0,0 +1,30 @@ +use oxc_ast::ast::Expression; + +use crate::ExtractStyleProp; + +pub(super) mod extract_global_style_from_expression; +pub(super) mod extract_style_from_expression; +pub(super) mod extract_style_from_jsx; +pub(super) mod extract_style_from_member_expression; + +/** + * type + * 1. jsx -> + * 2. object -> createElement('div', {a: 1}) + * 3. object with select -> createElement('div', {a: 1}) + */ + +#[derive(Debug, Default)] +pub struct ExtractResult<'a> { + // attribute will be maintained + pub styles: Vec>, + pub tag: Option>, + pub style_order: Option, + pub style_vars: Option>, +} + +#[derive(Debug)] +pub struct GlobalExtractResult<'a> { + pub styles: Vec>, + pub style_order: Option, +} diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index d00cf7ff..eb87a911 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -1,11 +1,11 @@ mod component; -mod css_type; mod css_utils; pub mod extract_style; +mod extractor; mod gen_class_name; mod gen_style; mod prop_modify_utils; -mod style_extractor; +mod util_type; mod utils; mod visit; use crate::extract_style::extract_style_value::ExtractStyleValue; @@ -4202,6 +4202,30 @@ globalCss({ .unwrap() )); } + + #[test] + #[serial] + fn extract_wrong_global_css() { + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { globalCss } from "@devup-ui/core"; +globalCss({ + [1]: { + bg: "red" + } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + } + #[test] #[serial] fn extract_global_css_with_selector() { @@ -4432,6 +4456,27 @@ globalCss({ )); } + #[test] + #[serial] + fn extract_global_css_with_wrong_imports() { + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { globalCss } from "@devup-ui/core"; +globalCss({ + imports: [1, 2, "./test.css"] +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + } + #[test] #[serial] fn extract_global_css_with_empty() { diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs index a8a8fbb0..2d58f0ca 100644 --- a/libs/extractor/src/prop_modify_utils.rs +++ b/libs/extractor/src/prop_modify_utils.rs @@ -19,6 +19,7 @@ pub fn modify_prop_object<'a>( style_order: Option, style_vars: Option>, ) { + println!("modify_prop_object: {:?}", props); let mut class_name_prop = None; let mut style_prop = None; let mut spread_props = vec![]; @@ -222,14 +223,21 @@ pub fn get_style_expression<'a>( ] .into_iter() .flatten() - .chain(spread_props.iter().map(|ex| { - Expression::StaticMemberExpression(ast_builder.alloc_static_member_expression( - SPAN, - ex.clone_in(ast_builder.allocator), - ast_builder.identifier_name(SPAN, ast_builder.atom("style")), - true, - )) - })) + .chain(if style_prop.is_some() { + vec![] + } else { + spread_props + .iter() + .map(|ex| { + Expression::StaticMemberExpression(ast_builder.alloc_static_member_expression( + SPAN, + ex.clone_in(ast_builder.allocator), + ast_builder.identifier_name(SPAN, ast_builder.atom("style")), + true, + )) + }) + .collect::>() + }) .collect::>() .as_slice(), ) diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-7.snap b/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-7.snap index aa2109e4..0d63fb8f 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-7.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-7.snap @@ -14,5 +14,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-8.snap b/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-8.snap index f820cde0..6a1f2395 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-8.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-8.snap @@ -14,5 +14,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-9.snap b/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-9.snap index 786569f1..e1bff91c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-9.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_conditional_style_props-9.snap @@ -14,5 +14,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css-2.snap index a0ff5e10..0adc1713 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css-2.snap @@ -21,5 +21,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css-3.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css-3.snap index a1f31f08..2869dbde 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css-3.snap @@ -21,5 +21,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css-4.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css-4.snap index 7414bf61..31127126 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css-4.snap @@ -21,5 +21,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css.snap index 29617ddd..e304710f 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css.snap @@ -21,5 +21,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-2.snap index a91d8622..6cf165a7 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-2.snap @@ -4,5 +4,5 @@ expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } fr --- ToBTreeSet { styles: {}, - code: "(\"\");\n", + code: ";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-3.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-3.snap index 86f8d5f7..38a66789 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-3.snap @@ -4,5 +4,5 @@ expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } fr --- ToBTreeSet { styles: {}, - code: "(\"\");\n", + code: ";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-4.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-4.snap index 8d0efe5a..1abae1d9 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-4.snap @@ -4,5 +4,5 @@ expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } fr --- ToBTreeSet { styles: {}, - code: "(\"\");\n", + code: ";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-5.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-5.snap index c75b80a7..31d382a6 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-5.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty-5.snap @@ -4,5 +4,5 @@ expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } fr --- ToBTreeSet { styles: {}, - code: "(\"\");\n", + code: ";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty.snap index 8379b4c5..6f3f7b7b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_empty.snap @@ -4,5 +4,5 @@ expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } fr --- ToBTreeSet { styles: {}, - code: "(\"\");\n", + code: ";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-2.snap index e0ee7a19..87460642 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-2.snap @@ -17,5 +17,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-3.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-3.snap index 297babeb..6f5c2b28 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports-3.snap @@ -17,5 +17,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports.snap index 65d9d011..f7cad842 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_imports.snap @@ -11,5 +11,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-2.snap index 1730a1e3..d514d079 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-2.snap @@ -133,5 +133,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-3.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-3.snap index 1730a1e3..d514d079 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-3.snap @@ -133,5 +133,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-4.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-4.snap index 6faa2d72..4f08100b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector-4.snap @@ -197,5 +197,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector.snap index f6e2a2cf..9adc581c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_selector.snap @@ -69,5 +69,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_template_literal.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_template_literal.snap index b8b13e50..68035513 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_template_literal.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_template_literal.snap @@ -21,5 +21,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n\"\";\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports.snap new file mode 100644 index 00000000..1dbc9048 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports.snap @@ -0,0 +1,15 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } from \"@devup-ui/core\";\nglobalCss({\n imports: [1, 2, \"./test.css\"]\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Import( + ExtractImport { + url: "./test.css", + file: "test.tsx", + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_style_props.snap b/libs/extractor/src/snapshots/extractor__tests__extract_style_props.snap index 87968071..900aeb32 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_style_props.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_style_props.snap @@ -23,5 +23,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n
className=\"d0 d1\" />;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css.snap b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css.snap new file mode 100644 index 00000000..826bfb86 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } from \"@devup-ui/core\";\nglobalCss({\n [1]: {\n bg: \"red\"\n }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: ";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_wrong_responsive_style_props.snap b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_responsive_style_props.snap index ac7f68e2..a4069868 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_wrong_responsive_style_props.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_responsive_style_props.snap @@ -4,5 +4,5 @@ expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { Box } from \"@ --- ToBTreeSet { styles: {}, - code: "
;\n", + code: "
;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order3.snap b/libs/extractor/src/snapshots/extractor__tests__style_order3.snap new file mode 100644 index 00000000..bed99567 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__style_order3.snap @@ -0,0 +1,71 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsxs as r, jsx as e } from \"react/jsx-runtime\";\nimport { Box as o, Text as t, Flex as i } from \"@devup-ui/react\";\nfunction c() {\n return r(\"div\", { children: [\n e(\n o,\n {\n _hover: {\n bg: \"blue\"\n },\n bg: \"$text\",\n color: \"red\",\n children: \"hello\",\n styleOrder: 10\n }\n ),\n e(t, { typography: \"header\", children: \"typo\", styleOrder:20 }),\n e(i, { as: \"section\", mt: 2, children: \"section\",styleOrder:30 })\n ] });\n}\nexport {\n c as Lib\n};\"#,\nExtractOption\n{ package: \"@devup-ui/react\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "$text", + level: 0, + selector: None, + style_order: Some( + 10, + ), + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: Some( + Selector( + "&:hover", + ), + ), + style_order: Some( + 10, + ), + }, + ), + Static( + ExtractStaticStyle { + property: "color", + value: "red", + level: 0, + selector: None, + style_order: Some( + 10, + ), + }, + ), + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: None, + style_order: Some( + 0, + ), + }, + ), + Static( + ExtractStaticStyle { + property: "marginTop", + value: "8px", + level: 0, + selector: None, + style_order: Some( + 30, + ), + }, + ), + Typography( + "header", + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsxs as r, jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn r(\"div\", { children: [\n\t\te(\"div\", {\n\t\t\tchildren: \"hello\",\n\t\t\tclassName: \"d0 d1 d2\"\n\t\t}),\n\t\te(\"span\", {\n\t\t\tchildren: \"typo\",\n\t\t\tclassName: \"typo-header\"\n\t\t}),\n\t\te(\"section\", {\n\t\t\tchildren: \"section\",\n\t\t\tclassName: \"d3 d4\"\n\t\t})\n\t] });\n}\nexport { c as Lib };\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap b/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap index c72dd920..dfd2cf8b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap +++ b/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap @@ -14,5 +14,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\ne(\"div\", {\n\t...props,\n\tclassName: \"a d0\",\n\tstyle: {\n\t\t...{ \"--d1\": variable },\n\t\t...{ color: \"blue\" },\n\t\t...props?.style\n\t}\n});\n", + code: "import \"@devup-ui/core/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\ne(\"div\", {\n\t...props,\n\tclassName: \"a d0\",\n\tstyle: {\n\t\t...{ \"--d1\": variable },\n\t\t...{ color: \"blue\" }\n\t}\n});\n", } diff --git a/libs/extractor/src/style_extractor.rs b/libs/extractor/src/style_extractor.rs deleted file mode 100644 index 6bd7db5e..00000000 --- a/libs/extractor/src/style_extractor.rs +++ /dev/null @@ -1,972 +0,0 @@ -use crate::css_utils::css_to_style; -use crate::extract_style::extract_dynamic_style::ExtractDynamicStyle; -use crate::extract_style::extract_import::ExtractImport; -use crate::extract_style::extract_static_style::ExtractStaticStyle; -use crate::extract_style::extract_style_value::ExtractStyleValue; -use crate::utils::{ - expression_to_code, get_number_by_literal_expression, is_same_expression, is_special_property, -}; -use crate::{ExtractStyleProp, utils}; -use oxc_allocator::CloneIn; -use oxc_ast::ast::{ - ArrayExpressionElement, ComputedMemberExpression, Expression, JSXAttributeValue, - ObjectPropertyKind, PropertyKey, TemplateElementValue, -}; -use std::collections::BTreeMap; - -use css::style_selector::StyleSelector; -use oxc_ast::AstBuilder; -use oxc_span::SPAN; -use oxc_syntax::operator::{BinaryOperator, LogicalOperator}; - -const IGNORED_IDENTIFIERS: [&str; 3] = ["undefined", "NaN", "Infinity"]; - -/** - * type - * 1. jsx -> - * 2. object -> createElement('div', {a: 1}) - * 3. object with select -> createElement('div', {a: 1}) - */ - -#[derive(Debug)] -pub enum ExtractResult<'a> { - // attribute will be maintained - Maintain, - Extract { - styles: Option>>, - tag: Option>, - style_order: Option, - style_vars: Option>, - }, -} - -#[derive(Debug)] -pub struct GlobalExtractResult<'a> { - pub styles: Vec>, - pub style_order: Option, -} - -pub fn extract_style_from_jsx_attr<'a>( - ast_builder: &AstBuilder<'a>, - name: &str, - value: &mut JSXAttributeValue<'a>, - selector: Option<&StyleSelector>, -) -> ExtractResult<'a> { - match value { - JSXAttributeValue::ExpressionContainer(expression) => { - if expression.expression.is_expression() { - extract_style_from_expression( - ast_builder, - Some(name), - expression.expression.to_expression_mut(), - 0, - selector, - ) - } else { - ExtractResult::Maintain - } - } - JSXAttributeValue::StringLiteral(literal) => extract_style_from_expression( - ast_builder, - Some(name), - &mut Expression::StringLiteral(literal.clone_in(ast_builder.allocator)), - 0, - selector, - ), - _ => ExtractResult::Maintain, - } -} - -pub fn extract_style_from_expression<'a>( - ast_builder: &AstBuilder<'a>, - name: Option<&str>, - expression: &mut Expression<'a>, - level: u8, - selector: Option<&StyleSelector>, -) -> ExtractResult<'a> { - let mut typo = false; - - if name.is_none() && selector.is_none() { - let mut style_order = None; - let mut style_vars = None; - return match expression { - Expression::ObjectExpression(obj) => { - let mut props_styles: Vec> = vec![]; - let mut tag = None; - for idx in (0..obj.properties.len()).rev() { - let mut prop = obj.properties.remove(idx); - if !match &mut prop { - ObjectPropertyKind::ObjectProperty(prop) => { - if let PropertyKey::StaticIdentifier(ident) = &prop.key { - let name = ident.name.as_str(); - - if name == "styleOrder" { - style_order = get_number_by_literal_expression(&prop.value) - .map(|v| v as u8); - continue; - } - if name == "styleVars" { - style_vars = Some(prop.value.clone_in(ast_builder.allocator)); - continue; - } - - match extract_style_from_expression( - ast_builder, - Some(name), - &mut prop.value, - 0, - None, - ) { - ExtractResult::Maintain => false, - ExtractResult::Extract { - styles, tag: _tag, .. - } => { - styles.into_iter().for_each(|mut styles| { - props_styles.append(&mut styles) - }); - tag = _tag.or(tag); - true - } - } - } else { - false - } - } - ObjectPropertyKind::SpreadProperty(prop) => { - match extract_style_from_expression( - ast_builder, - None, - &mut prop.argument, - 0, - None, - ) { - ExtractResult::Maintain => false, - ExtractResult::Extract { - styles, tag: _tag, .. - } => { - styles - .into_iter() - .for_each(|mut styles| props_styles.append(&mut styles)); - tag = _tag.or(tag); - true - } - } - } - } { - obj.properties.insert(idx, prop); - } - } - if props_styles.is_empty() && style_vars.is_none() { - ExtractResult::Maintain - } else { - ExtractResult::Extract { - styles: Some(props_styles), - tag, - style_order, - style_vars, - } - } - } - Expression::ConditionalExpression(conditional) => ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Conditional { - condition: conditional.test.clone_in(ast_builder.allocator), - consequent: if let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - None, - &mut conditional.consequent, - level, - None, - ) { - Some(Box::new(ExtractStyleProp::StaticArray(styles))) - } else { - None - }, - alternate: if let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - None, - &mut conditional.alternate, - level, - selector, - ) { - Some(Box::new(ExtractStyleProp::StaticArray(styles))) - } else { - None - }, - }]), - tag: None, - style_order, - style_vars, - }, - Expression::ParenthesizedExpression(parenthesized) => extract_style_from_expression( - ast_builder, - None, - &mut parenthesized.expression, - level, - None, - ), - _ => ExtractResult::Maintain, - }; - } - - if let Some(name) = name { - if is_special_property(name) { - return ExtractResult::Maintain; - } - - if name == "as" { - return ExtractResult::Extract { - styles: None, - tag: Some(expression.clone_in(ast_builder.allocator)), - style_order: None, - style_vars: None, - }; - } - if name == "selectors" - && let Expression::ObjectExpression(obj) = expression - { - let mut props = vec![]; - for p in obj.properties.iter_mut() { - if let ObjectPropertyKind::ObjectProperty(o) = p { - let name = o.key.name().unwrap().to_string(); - if let ExtractResult::Extract { - styles: Some(mut styles), - .. - } = extract_style_from_expression( - ast_builder, - None, - &mut o.value, - level, - Some( - &if let Some(selector) = selector { - name.replace("&", &selector.to_string()) - } else { - name - } - .as_str() - .into(), - ), - ) { - props.append(&mut styles); - } - } - } - return ExtractResult::Extract { - styles: Some(props), - tag: None, - style_order: None, - style_vars: None, - }; - } - - if let Some(new_selector) = name.strip_prefix("_") { - return extract_style_from_expression( - ast_builder, - None, - expression, - level, - Some(&if let Some(selector) = selector { - (selector, new_selector).into() - } else { - new_selector.into() - }), - ); - } - typo = name == "typography"; - } - if let Some(value) = utils::get_string_by_literal_expression(expression) { - if let Some(name) = name { - ExtractResult::Extract { - style_order: None, - style_vars: None, - tag: None, - styles: Some(vec![ExtractStyleProp::Static(if typo { - ExtractStyleValue::Typography(value.to_string()) - } else { - ExtractStyleValue::Static(ExtractStaticStyle::new( - name, - &value, - level, - selector.cloned(), - )) - })]), - } - } else { - ExtractResult::Extract { - style_order: None, - style_vars: None, - tag: None, - styles: Some(css_to_style(&value, level, &selector)), - } - } - } else { - match expression { - Expression::UnaryExpression(_) - | Expression::BinaryExpression(_) - | Expression::StaticMemberExpression(_) - | Expression::CallExpression(_) => ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Static(ExtractStyleValue::Dynamic( - ExtractDynamicStyle::new( - name.unwrap(), - level, - &expression_to_code(expression), - selector.cloned(), - ), - ))]), - tag: None, - style_order: None, - style_vars: None, - }, - Expression::TSAsExpression(exp) => extract_style_from_expression( - ast_builder, - name, - &mut exp.expression, - level, - selector, - ), - Expression::ComputedMemberExpression(mem) => { - extract_style_from_member_expression(ast_builder, name, mem, level, selector) - } - Expression::TemplateLiteral(tmp) => { - if let Some(name) = name { - if tmp.quasis.len() == 1 { - ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Static(if typo { - ExtractStyleValue::Typography(tmp.quasis[0].value.raw.to_string()) - } else { - ExtractStyleValue::Static(ExtractStaticStyle::new( - name, - &tmp.quasis[0].value.raw, - level, - selector.cloned(), - )) - })]), - tag: None, - style_order: None, - style_vars: None, - } - } else if typo { - ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Expression { - expression: ast_builder.expression_template_literal( - SPAN, - ast_builder.vec_from_array([ - ast_builder.template_element( - SPAN, - TemplateElementValue { - raw: ast_builder.atom("typo-"), - cooked: None, - }, - false, - ), - ast_builder.template_element( - SPAN, - TemplateElementValue { - raw: ast_builder.atom(""), - cooked: None, - }, - true, - ), - ]), - ast_builder.vec_from_array([ - expression.clone_in(ast_builder.allocator) - ]), - ), - styles: vec![], - }]), - tag: None, - style_order: None, - style_vars: None, - } - } else { - ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Static( - ExtractStyleValue::Dynamic(ExtractDynamicStyle::new( - name, - level, - &expression_to_code(expression), - selector.cloned(), - )), - )]), - tag: None, - style_order: None, - style_vars: None, - } - } - } else { - ExtractResult::Maintain - } - } - Expression::Identifier(identifier) => { - if IGNORED_IDENTIFIERS.contains(&identifier.name.as_str()) { - ExtractResult::Maintain - } else if let Some(name) = name { - if typo { - ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Expression { - expression: ast_builder.expression_conditional( - SPAN, - ast_builder - .expression_identifier(SPAN, identifier.name.as_str()), - ast_builder.expression_template_literal( - SPAN, - ast_builder.vec_from_array([ - ast_builder.template_element( - SPAN, - TemplateElementValue { - raw: ast_builder.atom("typo-"), - cooked: None, - }, - false, - ), - ast_builder.template_element( - SPAN, - TemplateElementValue { - raw: ast_builder.atom(""), - cooked: None, - }, - true, - ), - ]), - ast_builder.vec_from_array([ - expression.clone_in(ast_builder.allocator) - ]), - ), - ast_builder.expression_string_literal(SPAN, "", None), - ), - styles: vec![], - }]), - tag: None, - style_order: None, - style_vars: None, - } - } else { - ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Static( - ExtractStyleValue::Dynamic(ExtractDynamicStyle::new( - name, - level, - &identifier.name, - selector.cloned(), - )), - )]), - tag: None, - style_order: None, - style_vars: None, - } - } - } else { - ExtractResult::Maintain - } - } - Expression::LogicalExpression(logical) => { - let res = match extract_style_from_expression( - ast_builder, - name, - &mut logical.right, - level, - selector, - ) { - ExtractResult::Extract { - styles: Some(styles), - .. - } => Some(Box::new(ExtractStyleProp::StaticArray(styles))), - _ => None, - }; - match logical.operator { - LogicalOperator::Or => ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Conditional { - condition: logical.left.clone_in(ast_builder.allocator), - consequent: None, - alternate: res, - }]), - tag: None, - style_order: None, - style_vars: None, - }, - LogicalOperator::And => ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Conditional { - condition: logical.left.clone_in(ast_builder.allocator), - consequent: res, - alternate: None, - }]), - tag: None, - style_order: None, - style_vars: None, - }, - LogicalOperator::Coalesce => ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Conditional { - condition: ast_builder.expression_logical( - SPAN, - ast_builder.expression_binary( - SPAN, - logical.left.clone_in(ast_builder.allocator), - BinaryOperator::StrictInequality, - ast_builder.expression_null_literal(SPAN), - ), - LogicalOperator::And, - ast_builder.expression_binary( - SPAN, - logical.left.clone_in(ast_builder.allocator), - BinaryOperator::StrictInequality, - ast_builder.expression_identifier(SPAN, "undefined"), - ), - ), - consequent: match extract_style_from_expression( - ast_builder, - name, - &mut logical.left, - level, - selector, - ) { - ExtractResult::Extract { - styles: Some(styles), - .. - } => Some(Box::new(ExtractStyleProp::StaticArray(styles))), - _ => None, - }, - alternate: res, - }]), - tag: None, - style_order: None, - style_vars: None, - }, - } - } - Expression::ParenthesizedExpression(parenthesized) => extract_style_from_expression( - ast_builder, - name, - &mut parenthesized.expression, - level, - selector, - ), - Expression::ArrayExpression(array) => { - let mut props = vec![]; - - for (idx, element) in array.elements.iter_mut().enumerate() { - if let ExtractResult::Extract { - styles: Some(mut styles), - .. - } = extract_style_from_expression( - ast_builder, - name, - element.to_expression_mut(), - idx as u8, - selector, - ) { - props.append(&mut styles); - } - } - - if props.is_empty() { - ExtractResult::Maintain - } else { - ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::StaticArray(props)]), - tag: None, - style_order: None, - style_vars: None, - } - } - } - Expression::ConditionalExpression(conditional) => { - if is_same_expression(&conditional.consequent, &conditional.alternate) { - extract_style_from_expression( - ast_builder, - name, - &mut conditional.consequent, - level, - selector, - ) - } else { - ExtractResult::Extract { - styles: Some(vec![ExtractStyleProp::Conditional { - condition: conditional.test.clone_in(ast_builder.allocator), - consequent: if let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - name, - &mut conditional.consequent, - level, - selector, - ) { - Some(Box::new(ExtractStyleProp::StaticArray(styles))) - } else { - None - }, - alternate: if let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - name, - &mut conditional.alternate, - level, - selector, - ) { - Some(Box::new(ExtractStyleProp::StaticArray(styles))) - } else { - None - }, - }]), - tag: None, - style_order: None, - style_vars: None, - } - } - } - Expression::ObjectExpression(obj) => { - let mut props = vec![]; - for p in obj.properties.iter_mut() { - if let ObjectPropertyKind::ObjectProperty(o) = p - && let ExtractResult::Extract { - styles: Some(mut styles), - .. - } = extract_style_from_expression( - ast_builder, - Some(&o.key.name().unwrap()), - &mut o.value, - level, - selector, - ) - { - props.append(&mut styles); - } - } - ExtractResult::Extract { - styles: Some(props), - tag: None, - style_order: None, - style_vars: None, - } - } - // val if let Some(value) = get_number_by_literal_expression(val) => {} - _ => ExtractResult::Maintain, - } - } -} - -fn extract_style_from_member_expression<'a>( - ast_builder: &AstBuilder<'a>, - name: Option<&str>, - mem: &mut ComputedMemberExpression<'a>, - level: u8, - selector: Option<&StyleSelector>, -) -> ExtractResult<'a> { - let mem_expression = &mem.expression.clone_in(ast_builder.allocator); - let mut ret: Vec = vec![]; - - match &mut mem.object { - Expression::ArrayExpression(array) => { - if array.elements.is_empty() { - return ExtractResult::Extract { - styles: None, - tag: None, - style_order: None, - style_vars: None, - }; - } - - if let Some(num) = utils::get_number_by_literal_expression(mem_expression) { - if num < 0f64 { - return ExtractResult::Extract { - styles: None, - tag: None, - style_order: None, - style_vars: None, - }; - } - let mut etc = None; - for (idx, p) in array.elements.iter_mut().enumerate() { - if let ArrayExpressionElement::SpreadElement(sp) = p { - etc = Some(sp.argument.clone_in(ast_builder.allocator)); - continue; - } - if idx as f64 == num - && let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - name, - p.to_expression_mut(), - level, - selector, - ) - { - return ExtractResult::Extract { - styles: Some(styles), - tag: None, - style_order: None, - style_vars: None, - }; - } - } - return ExtractResult::Extract { - styles: etc.map(|etc| { - vec![ExtractStyleProp::Static(ExtractStyleValue::Dynamic( - ExtractDynamicStyle::new( - name.unwrap(), - level, - &expression_to_code(&Expression::ComputedMemberExpression( - ast_builder.alloc_computed_member_expression( - SPAN, - etc, - mem_expression.clone_in(ast_builder.allocator), - false, - ), - )), - selector.cloned(), - ), - ))] - }), - tag: None, - style_order: None, - style_vars: None, - }; - } - - let mut map = BTreeMap::new(); - for (idx, p) in array.elements.iter_mut().enumerate() { - if let ArrayExpressionElement::SpreadElement(sp) = p { - map.insert( - idx.to_string(), - Box::new(ExtractStyleProp::Static(ExtractStyleValue::Dynamic( - ExtractDynamicStyle::new( - name.unwrap(), - level, - &expression_to_code(&Expression::ComputedMemberExpression( - ast_builder.alloc_computed_member_expression( - SPAN, - sp.argument.clone_in(ast_builder.allocator), - mem_expression.clone_in(ast_builder.allocator), - false, - ), - )), - selector.cloned(), - ), - ))), - ); - } else if let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - name, - p.to_expression_mut(), - level, - selector, - ) { - map.insert( - idx.to_string(), - Box::new(ExtractStyleProp::StaticArray(styles)), - ); - } - } - - ret.push(ExtractStyleProp::MemberExpression { - expression: mem_expression.clone_in(ast_builder.allocator), - map, - }); - } - Expression::ObjectExpression(obj) => { - if obj.properties.is_empty() { - return ExtractResult::Extract { - styles: None, - tag: None, - style_order: None, - style_vars: None, - }; - } - - let mut map = BTreeMap::new(); - if let Some(k) = utils::get_string_by_literal_expression(mem_expression) { - let mut etc = None; - for p in obj.properties.iter_mut() { - if let ObjectPropertyKind::ObjectProperty(o) = p { - if let PropertyKey::StaticIdentifier(ref pk) = o.key - && pk.name == k - && let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - name, - &mut o.value, - level, - selector, - ) - { - return ExtractResult::Extract { - styles: Some(styles), - tag: None, - style_order: None, - style_vars: None, - }; - } - } else if let ObjectPropertyKind::SpreadProperty(sp) = p { - etc = Some(sp.argument.clone_in(ast_builder.allocator)); - } - } - - match etc { - None => { - return ExtractResult::Extract { - styles: None, - tag: None, - style_order: None, - style_vars: None, - }; - } - Some(etc) => ret.push(ExtractStyleProp::Static(ExtractStyleValue::Dynamic( - ExtractDynamicStyle::new( - name.unwrap(), - level, - &expression_to_code(&Expression::ComputedMemberExpression( - ast_builder.alloc_computed_member_expression( - SPAN, - etc, - mem_expression.clone_in(ast_builder.allocator), - false, - ), - )), - selector.cloned(), - ), - ))), - } - } - - for p in obj.properties.iter_mut() { - if let ObjectPropertyKind::ObjectProperty(o) = p - && let PropertyKey::StaticIdentifier(_) - | PropertyKey::NumericLiteral(_) - | PropertyKey::StringLiteral(_) = o.key - && let ExtractResult::Extract { - styles: Some(styles), - .. - } = extract_style_from_expression( - ast_builder, - name, - &mut o.value, - level, - selector, - ) - { - map.insert( - o.key.name().unwrap().to_string(), - Box::new(ExtractStyleProp::StaticArray(styles)), - ); - } - } - ret.push(ExtractStyleProp::MemberExpression { - expression: mem_expression.clone_in(ast_builder.allocator), - map, - }); - } - Expression::Identifier(_) => { - if let Some(name) = name { - ret.push(ExtractStyleProp::Static(ExtractStyleValue::Dynamic( - ExtractDynamicStyle::new( - name, - level, - &expression_to_code(&Expression::ComputedMemberExpression( - ast_builder.alloc_computed_member_expression( - SPAN, - mem.object.clone_in(ast_builder.allocator), - mem_expression.clone_in(ast_builder.allocator), - false, - ), - )), - selector.cloned(), - ), - ))) - } - } - _ => {} - }; - - ExtractResult::Extract { - styles: Some(ret), - tag: None, - style_order: None, - style_vars: None, - } -} - -pub fn extract_global_style_from_expression<'a>( - ast_builder: &AstBuilder<'a>, - expression: &mut Expression<'a>, - file: &str, -) -> GlobalExtractResult<'a> { - let mut styles = vec![]; - - if let Expression::ObjectExpression(obj) = expression { - for p in obj.properties.iter_mut() { - if let ObjectPropertyKind::ObjectProperty(o) = p { - let name = if let PropertyKey::StaticIdentifier(ident) = &o.key { - ident.name.to_string() - } else if let PropertyKey::StringLiteral(s) = &o.key { - s.value.to_string() - } else if let PropertyKey::TemplateLiteral(t) = &o.key { - t.quasis - .iter() - .map(|q| q.value.raw.as_str()) - .collect::>() - .join("") - } else { - continue; - }; - - if name == "imports" { - if let Expression::ArrayExpression(arr) = &o.value { - for p in arr.elements.iter() { - styles.push(ExtractStyleProp::Static(ExtractStyleValue::Import( - ExtractImport { - url: if let ArrayExpressionElement::StringLiteral(s) = p { - s.value.trim().to_string() - } else if let ArrayExpressionElement::TemplateLiteral(t) = p { - t.quasis - .iter() - .map(|q| q.value.raw.as_str()) - .collect::>() - .join("") - .trim() - .to_string() - } else { - continue; - }, - file: file.to_string(), - }, - ))); - } - } - continue; - } - - if let ExtractResult::Extract { - styles: Some(_styles), - .. - } = extract_style_from_expression( - ast_builder, - None, - &mut o.value, - 0, - Some(&StyleSelector::Global(name.clone(), file.to_string())), - ) { - styles.extend(_styles); - } - } - } - } - GlobalExtractResult { - styles, - style_order: None, - } -} diff --git a/libs/extractor/src/util_type.rs b/libs/extractor/src/util_type.rs new file mode 100644 index 00000000..c936a316 --- /dev/null +++ b/libs/extractor/src/util_type.rs @@ -0,0 +1,38 @@ +#[derive(Debug, PartialEq)] +pub enum UtilType { + Css, + GlobalCss, + Keyframes, +} + +impl TryFrom for UtilType { + type Error = String; + + fn try_from(value: String) -> Result { + if value == "css" { + Ok(UtilType::Css) + } else if value == "globalCss" { + Ok(UtilType::GlobalCss) + } else if value == "keyframes" { + Ok(UtilType::Keyframes) + } else { + Err(value) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("css".to_string(), Ok(UtilType::Css))] + #[case("globalCss".to_string(), Ok(UtilType::GlobalCss))] + #[case("keyframes".to_string(), Ok(UtilType::Keyframes))] + #[case("unknown".to_string(), Err("unknown".to_string()))] + #[case("".to_string(), Err("".to_string()))] + fn test_util_type_try_from(#[case] input: String, #[case] expected: Result) { + assert_eq!(UtilType::try_from(input), expected); + } +} diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index 3ffdc5dc..bc5a9b92 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -1,13 +1,15 @@ use crate::component::ExportVariableKind; -use crate::css_type::CssType; use crate::css_utils::{css_to_style, optimize_css_block}; use crate::extract_style::extract_css::ExtractCss; +use crate::extractor::{ + ExtractResult, GlobalExtractResult, + extract_global_style_from_expression::extract_global_style_from_expression, + extract_style_from_expression::extract_style_from_expression, + extract_style_from_jsx::extract_style_from_jsx, +}; use crate::gen_class_name::gen_class_names; use crate::prop_modify_utils::{modify_prop_object, modify_props}; -use crate::style_extractor::{ - ExtractResult, GlobalExtractResult, extract_global_style_from_expression, - extract_style_from_expression, extract_style_from_jsx_attr, -}; +use crate::util_type::UtilType; use crate::{ExtractStyleProp, ExtractStyleValue}; use css::short_to_long; use oxc_allocator::{Allocator, CloneIn}; @@ -26,7 +28,7 @@ use oxc_ast_visit::walk_mut::{ }; use strum::IntoEnumIterator; -use crate::utils::jsx_expression_to_number; +use crate::utils::{is_special_property, jsx_expression_to_number}; use oxc_ast::AstBuilder; use oxc_span::SPAN; use std::collections::{HashMap, HashSet}; @@ -38,7 +40,7 @@ pub struct DevupVisitor<'a> { imports: HashMap, import_object: Option, jsx_imports: HashMap, - css_imports: HashMap>, + util_imports: HashMap>, jsx_object: Option, package: String, pub css_file: String, @@ -57,7 +59,7 @@ impl<'a> DevupVisitor<'a> { styles: HashSet::new(), import_object: None, jsx_object: None, - css_imports: HashMap::new(), + util_imports: HashMap::new(), } } } @@ -73,10 +75,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { init: Some(Expression::Identifier(ident)), .. } = v - && let Some(css_import_key) = self.css_imports.get(ident.name.as_str()) + && let Some(css_import_key) = self.util_imports.get(ident.name.as_str()) && let Some(name) = id.get_binding_identifier().map(|id| id.name.to_string()) { - self.css_imports.insert(name, css_import_key.clone()); + self.util_imports.insert(name, css_import_key.clone()); } } walk_variable_declarators(self, it); @@ -114,7 +116,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { walk_expression(self, it); if let Expression::CallExpression(call) = it { - let css_import_key = if let Expression::Identifier(ident) = &call.callee { + let util_import_key = if let Expression::Identifier(ident) = &call.callee { Some(ident.name.to_string()) } else if let Expression::StaticMemberExpression(member) = &call.callee && let Expression::Identifier(ident) = &member.object @@ -124,24 +126,24 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { None }; - if let Some(css_import_key) = css_import_key - && let Some(css_type) = self.css_imports.get(&css_import_key) + if let Some(util_import_key) = util_import_key + && let Some(util_type) = self.util_imports.get(&util_import_key) { if call.arguments.len() != 1 { - *it = match css_type.as_ref() { - CssType::Css => { + *it = match util_type.as_ref() { + UtilType::Css | UtilType::Keyframes => { self.ast .expression_string_literal(SPAN, self.ast.atom(""), None) } - CssType::GlobalCss => { + UtilType::GlobalCss => { self.ast.expression_identifier(SPAN, self.ast.atom("")) } }; } else { - *it = match css_type.as_ref() { - CssType::Css => { - if let ExtractResult::Extract { - styles: Some(mut styles), + *it = match util_type.as_ref() { + UtilType::Css => { + let ExtractResult { + mut styles, style_order, .. } = extract_style_from_expression( @@ -150,7 +152,12 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { call.arguments[0].to_expression_mut(), 0, None, - ) { + ); + + if styles.is_empty() { + self.ast + .expression_string_literal(SPAN, self.ast.atom(""), None) + } else { // css can not reachable // ExtractResult::ExtractStyleWithChangeTag(styles, _) let class_name = @@ -171,12 +178,14 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { None, ) } - } else { - self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) } } - CssType::GlobalCss => { + UtilType::Keyframes => { + // TODO: implement keyframes + self.ast + .expression_string_literal(SPAN, self.ast.atom(""), None) + } + UtilType::GlobalCss => { let GlobalExtractResult { styles, style_order, @@ -196,15 +205,14 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { ex.extract() }), ); - self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) + self.ast.expression_identifier(SPAN, self.ast.atom("")) } } } } } else if let Expression::TaggedTemplateExpression(tag) = it { if let Expression::Identifier(ident) = &tag.tag - && let Some(css_type) = self.css_imports.get(ident.name.as_str()) + && let Some(css_type) = self.util_imports.get(ident.name.as_str()) { let css_str = tag .quasi @@ -214,7 +222,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .collect::>() .join(""); match css_type.as_ref() { - CssType::Css => { + UtilType::Css => { let mut styles = css_to_style(&css_str, 0, &None); let class_name = gen_class_names(&self.ast, &mut styles, None); @@ -228,7 +236,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .flat_map(|ex| ex.extract()), ); } - CssType::GlobalCss => { + UtilType::Keyframes => { + // TODO: implement keyframes + } + UtilType::GlobalCss => { let css = ExtractStyleValue::Css(ExtractCss { css: optimize_css_block(&css_str), file: self.filename.clone(), @@ -277,42 +288,39 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { None, ); let mut props_styles = vec![]; - let mut style_order = None; - let mut style_vars = None; - if let ExtractResult::Extract { + let ExtractResult { styles, tag: _tag, - style_order: _style_order, - style_vars: _style_vars, + style_order, + style_vars, } = extract_style_from_expression( &self.ast, None, it.arguments[1].to_expression_mut(), 0, None, - ) { - style_order = _style_order; - styles.into_iter().for_each(|mut ex| { - props_styles.append(&mut ex); - }); - if let Some(t) = _tag { - tag = t; - } - style_vars = _style_vars; - } + ); + props_styles.extend(styles); - for ex in kind.extract().into_iter().rev() { - props_styles.push(ExtractStyleProp::Static(ex)); + if let Some(t) = _tag { + tag = t; } - for style in props_styles.iter().rev() { + props_styles.extend( + kind.extract() + .into_iter() + .rev() + .map(|ex| ExtractStyleProp::Static(ex)), + ); + props_styles.iter().rev().for_each(|style| { self.styles.extend(style.extract().into_iter().map(|mut s| { style_order.into_iter().for_each(|order| { s.set_style_order(order); }); s - })); - } + })) + }); + if let Expression::ObjectExpression(obj) = it.arguments[1].to_expression_mut() { modify_prop_object( &self.ast, @@ -399,8 +407,8 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { { self.imports.insert(import.local.to_string(), kind); specifiers.remove(i); - } else if let Ok(kind) = CssType::try_from(import.imported.to_string()) { - self.css_imports + } else if let Ok(kind) = UtilType::try_from(import.imported.to_string()) { + self.util_imports .insert(import.local.to_string(), Rc::new(kind)); specifiers.remove(i); } @@ -414,14 +422,14 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { kind, ); } - self.css_imports.insert( + self.util_imports.insert( format!("{}.{}", import_default_specifier.local, "css"), - Rc::new(CssType::Css), + Rc::new(UtilType::Css), ); - self.css_imports.insert( + self.util_imports.insert( format!("{}.{}", import_default_specifier.local, "globalCss"), - Rc::new(CssType::GlobalCss), + Rc::new(UtilType::GlobalCss), ); } ImportDeclarationSpecifier::ImportNamespaceSpecifier( @@ -433,13 +441,13 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { kind, ); } - self.css_imports.insert( + self.util_imports.insert( format!("{}.{}", import_namespace_specifier.local, "css"), - Rc::new(CssType::Css), + Rc::new(UtilType::Css), ); - self.css_imports.insert( + self.util_imports.insert( format!("{}.{}", import_namespace_specifier.local, "globalCss"), - Rc::new(CssType::GlobalCss), + Rc::new(UtilType::GlobalCss), ); } } @@ -470,8 +478,9 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { let mut attr = attrs.remove(i); if let Attribute(attr) = &mut attr && let Identifier(name) = &attr.name + && let name = short_to_long(&name.name) + && !is_special_property(&name) { - let name = short_to_long(&name.name); if duplicate_set.contains(&name) { continue; } @@ -492,18 +501,11 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } if let Some(at) = &mut attr.value { - if let ExtractResult::Extract { styles, tag, .. } = - extract_style_from_jsx_attr(&self.ast, &name, at, None) - { - styles.into_iter().for_each(|mut ex| { - ex.reverse(); - props_styles.append(&mut ex); - }); - if let Some(t) = tag { - tag_name = t; - } - continue; - } + let ExtractResult { styles, tag, .. } = + extract_style_from_jsx(&self.ast, &name, at, None); + props_styles.extend(styles.into_iter().rev()); + tag_name = tag.unwrap_or(tag_name); + continue; } } attrs.insert(i, attr); From 9466964b035f64925de939c25edc719da36f782a Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 13:15:10 +0900 Subject: [PATCH 03/11] Rm comment --- .../extract_global_style_from_expression.rs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/libs/extractor/src/extractor/extract_global_style_from_expression.rs b/libs/extractor/src/extractor/extract_global_style_from_expression.rs index 9b4ce34e..6e10772f 100644 --- a/libs/extractor/src/extractor/extract_global_style_from_expression.rs +++ b/libs/extractor/src/extractor/extract_global_style_from_expression.rs @@ -71,29 +71,6 @@ pub fn extract_global_style_from_expression<'a>( ) .styles, ); - - // if let ExtractResult::Extract { - // styles: Some(_styles), - // .. - // } = extract_style_from_expression( - // ast_builder, - // None, - // &mut o.value, - // 0, - // Some(&StyleSelector::Global(name.clone(), file.to_string())), - // ) { - // styles.extend( - // extract_style_from_expression( - // ast_builder, - // None, - // &mut o.value, - // 0, - // Some(&StyleSelector::Global(name.clone(), file.to_string())), - // ) - // .styles - // .iter() - // ); - // } } } } From 824f2dc433310d29a55fc49b538d7eab204fd46e Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 13:16:05 +0900 Subject: [PATCH 04/11] Rm print --- libs/extractor/src/prop_modify_utils.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs index 2d58f0ca..f26b9fc8 100644 --- a/libs/extractor/src/prop_modify_utils.rs +++ b/libs/extractor/src/prop_modify_utils.rs @@ -19,7 +19,6 @@ pub fn modify_prop_object<'a>( style_order: Option, style_vars: Option>, ) { - println!("modify_prop_object: {:?}", props); let mut class_name_prop = None; let mut style_prop = None; let mut spread_props = vec![]; From 0ed688bc6cf9d77835bd336064a3bf680b2b3929 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 13:35:23 +0900 Subject: [PATCH 05/11] Rm snapshot --- .../extract_global_style_from_expression.rs | 3 +- .../extractor__tests__style_order3.snap | 71 ------------------- 2 files changed, 1 insertion(+), 73 deletions(-) delete mode 100644 libs/extractor/src/snapshots/extractor__tests__style_order3.snap diff --git a/libs/extractor/src/extractor/extract_global_style_from_expression.rs b/libs/extractor/src/extractor/extract_global_style_from_expression.rs index 6e10772f..73dad72e 100644 --- a/libs/extractor/src/extractor/extract_global_style_from_expression.rs +++ b/libs/extractor/src/extractor/extract_global_style_from_expression.rs @@ -2,8 +2,7 @@ use crate::{ ExtractStyleProp, extract_style::{extract_import::ExtractImport, extract_style_value::ExtractStyleValue}, extractor::{ - ExtractResult, GlobalExtractResult, - extract_style_from_expression::extract_style_from_expression, + GlobalExtractResult, extract_style_from_expression::extract_style_from_expression, }, }; use css::style_selector::StyleSelector; diff --git a/libs/extractor/src/snapshots/extractor__tests__style_order3.snap b/libs/extractor/src/snapshots/extractor__tests__style_order3.snap deleted file mode 100644 index bed99567..00000000 --- a/libs/extractor/src/snapshots/extractor__tests__style_order3.snap +++ /dev/null @@ -1,71 +0,0 @@ ---- -source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.mjs\",\nr#\"import { jsxs as r, jsx as e } from \"react/jsx-runtime\";\nimport { Box as o, Text as t, Flex as i } from \"@devup-ui/react\";\nfunction c() {\n return r(\"div\", { children: [\n e(\n o,\n {\n _hover: {\n bg: \"blue\"\n },\n bg: \"$text\",\n color: \"red\",\n children: \"hello\",\n styleOrder: 10\n }\n ),\n e(t, { typography: \"header\", children: \"typo\", styleOrder:20 }),\n e(i, { as: \"section\", mt: 2, children: \"section\",styleOrder:30 })\n ] });\n}\nexport {\n c as Lib\n};\"#,\nExtractOption\n{ package: \"@devup-ui/react\".to_string(), css_file: None }).unwrap())" ---- -ToBTreeSet { - styles: { - Static( - ExtractStaticStyle { - property: "background", - value: "$text", - level: 0, - selector: None, - style_order: Some( - 10, - ), - }, - ), - Static( - ExtractStaticStyle { - property: "background", - value: "blue", - level: 0, - selector: Some( - Selector( - "&:hover", - ), - ), - style_order: Some( - 10, - ), - }, - ), - Static( - ExtractStaticStyle { - property: "color", - value: "red", - level: 0, - selector: None, - style_order: Some( - 10, - ), - }, - ), - Static( - ExtractStaticStyle { - property: "display", - value: "flex", - level: 0, - selector: None, - style_order: Some( - 0, - ), - }, - ), - Static( - ExtractStaticStyle { - property: "marginTop", - value: "8px", - level: 0, - selector: None, - style_order: Some( - 30, - ), - }, - ), - Typography( - "header", - ), - }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { jsxs as r, jsx as e } from \"react/jsx-runtime\";\nfunction c() {\n\treturn r(\"div\", { children: [\n\t\te(\"div\", {\n\t\t\tchildren: \"hello\",\n\t\t\tclassName: \"d0 d1 d2\"\n\t\t}),\n\t\te(\"span\", {\n\t\t\tchildren: \"typo\",\n\t\t\tclassName: \"typo-header\"\n\t\t}),\n\t\te(\"section\", {\n\t\t\tchildren: \"section\",\n\t\t\tclassName: \"d3 d4\"\n\t\t})\n\t] });\n}\nexport { c as Lib };\n", -} From 2fa59bcbe639961d92ece2a1956b02c12325638b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 14:25:20 +0900 Subject: [PATCH 06/11] Add snapshot --- libs/extractor/src/lib.rs | 47 +++++++++++++++++++ ...tract_global_css_with_wrong_imports-2.snap | 8 ++++ ...or__tests__extract_wrong_global_css-2.snap | 8 ++++ ...or__tests__extract_wrong_global_css-3.snap | 8 ++++ 4 files changed, 71 insertions(+) create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-3.snap diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index eb87a911..4dae94e5 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -4216,6 +4216,36 @@ globalCss({ bg: "red" } }) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { globalCss } from "@devup-ui/core"; +globalCss() +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { globalCss } from "@devup-ui/core"; +globalCss(1) "#, ExtractOption { package: "@devup-ui/core".to_string(), @@ -4467,6 +4497,23 @@ globalCss({ globalCss({ imports: [1, 2, "./test.css"] }) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { globalCss } from "@devup-ui/core"; +globalCss({ + imports: {} +}) "#, ExtractOption { package: "@devup-ui/core".to_string(), diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports-2.snap new file mode 100644 index 00000000..a4c39747 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_global_css_with_wrong_imports-2.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } from \"@devup-ui/core\";\nglobalCss({\n imports: {}\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: ";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-2.snap new file mode 100644 index 00000000..4d70c073 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-2.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } from \"@devup-ui/core\";\nglobalCss()\n\"#, ExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: ";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-3.snap b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-3.snap new file mode 100644 index 00000000..4001bb83 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_wrong_global_css-3.snap @@ -0,0 +1,8 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { globalCss } from \"@devup-ui/core\";\nglobalCss(1)\n\"#, ExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: {}, + code: ";\n", +} From 946dbc27c0ad8e1672e3a2595a2cf629826199fe Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 19:24:03 +0900 Subject: [PATCH 07/11] Impl keyframe --- .changeset/yummy-brooms-appear.md | 5 + Cargo.lock | 1 + apps/landing/src/app/StarButton.css | 9 - apps/landing/src/app/StarButton.tsx | 16 +- bindings/devup-ui-wasm/src/lib.rs | 30 +- libs/css/src/class_map.rs | 24 ++ libs/css/src/lib.rs | 28 ++ libs/extractor/src/css_utils.rs | 101 ++++++ .../extract_style/extract_dynamic_style.rs | 2 +- .../src/extract_style/extract_keyframes.rs | 24 ++ .../src/extract_style/extract_static_style.rs | 5 +- .../src/extract_style/extract_style_value.rs | 45 ++- libs/extractor/src/extract_style/mod.rs | 4 +- .../src/extract_style/style_property.rs | 36 +++ .../extract_keyframes_from_expression.rs | 56 ++++ .../extract_style_from_expression.rs | 12 + .../src/extractor/extract_style_from_jsx.rs | 6 +- libs/extractor/src/extractor/mod.rs | 8 +- libs/extractor/src/gen_class_name.rs | 3 +- libs/extractor/src/gen_style.rs | 3 +- libs/extractor/src/lib.rs | 287 +++++++++++++++++- .../extractor__tests__extract_keyframs-2.snap | 42 +++ .../extractor__tests__extract_keyframs-3.snap | 42 +++ .../extractor__tests__extract_keyframs-4.snap | 42 +++ .../extractor__tests__extract_keyframs-5.snap | 42 +++ .../extractor__tests__extract_keyframs-6.snap | 42 +++ .../extractor__tests__extract_keyframs-7.snap | 42 +++ .../extractor__tests__extract_keyframs-8.snap | 42 +++ .../extractor__tests__extract_keyframs-9.snap | 108 +++++++ .../extractor__tests__extract_keyframs.snap | 33 ++ ...or__tests__extract_keyframs_literal-2.snap | 47 +++ ...ctor__tests__extract_keyframs_literal.snap | 33 ++ libs/extractor/src/utils.rs | 79 +++++ libs/extractor/src/visit.rs | 48 ++- libs/sheet/Cargo.toml | 1 + libs/sheet/src/lib.rs | 42 ++- .../snapshots/sheet__tests__deserialize.snap | 1 + packages/react/src/utils/keyframes.ts | 15 +- 38 files changed, 1354 insertions(+), 52 deletions(-) create mode 100644 .changeset/yummy-brooms-appear.md delete mode 100644 apps/landing/src/app/StarButton.css create mode 100644 libs/extractor/src/extract_style/extract_keyframes.rs create mode 100644 libs/extractor/src/extract_style/style_property.rs create mode 100644 libs/extractor/src/extractor/extract_keyframes_from_expression.rs create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-4.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-5.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-6.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-7.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-8.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs-9.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal.snap diff --git a/.changeset/yummy-brooms-appear.md b/.changeset/yummy-brooms-appear.md new file mode 100644 index 00000000..dff95dbe --- /dev/null +++ b/.changeset/yummy-brooms-appear.md @@ -0,0 +1,5 @@ +--- +"@devup-ui/wasm": patch +--- + +Implement keyframe diff --git a/Cargo.lock b/Cargo.lock index a48f69f8..6c0d31b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,6 +1290,7 @@ version = "0.1.0" dependencies = [ "criterion", "css", + "extractor", "insta", "once_cell", "regex", diff --git a/apps/landing/src/app/StarButton.css b/apps/landing/src/app/StarButton.css deleted file mode 100644 index 23331d25..00000000 --- a/apps/landing/src/app/StarButton.css +++ /dev/null @@ -1,9 +0,0 @@ -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - diff --git a/apps/landing/src/app/StarButton.tsx b/apps/landing/src/app/StarButton.tsx index 71f6dee7..a8d9c77f 100644 --- a/apps/landing/src/app/StarButton.tsx +++ b/apps/landing/src/app/StarButton.tsx @@ -1,11 +1,18 @@ 'use client' -import './StarButton.css' - -import { Center, css, Flex, Image, Text } from '@devup-ui/react' +import { Center, css, Flex, Image, keyframes, Text } from '@devup-ui/react' import Link from 'next/link' import { useEffect, useState } from 'react' +const spin = keyframes({ + '0%': { + transform: 'rotate(0deg)', + }, + '100%': { + transform: 'rotate(360deg)', + }, +}) + export default function StarButton() { const [starCount, setStarCount] = useState(null) @@ -88,7 +95,8 @@ export default function StarButton() { {starCount === null ? ( Loading diff --git a/bindings/devup-ui-wasm/src/lib.rs b/bindings/devup-ui-wasm/src/lib.rs index 7a4014fe..2c68c892 100644 --- a/bindings/devup-ui-wasm/src/lib.rs +++ b/bindings/devup-ui-wasm/src/lib.rs @@ -1,6 +1,8 @@ use css::class_map::{get_class_map, set_class_map}; +use extractor::extract_style::ExtractStyleProperty; use extractor::extract_style::extract_style_value::ExtractStyleValue; -use extractor::{ExtractOption, StyleProperty, extract}; +use extractor::extract_style::style_property::StyleProperty; +use extractor::{ExtractOption, extract}; use once_cell::sync::Lazy; use sheet::StyleSheet; use std::collections::HashSet; @@ -90,6 +92,32 @@ impl Output { collected = true; } } + + ExtractStyleValue::Keyframes(keyframes) => { + if sheet.add_keyframes( + &keyframes.extract().to_string(), + keyframes + .keyframes + .iter() + .map(|(key, value)| { + ( + key.clone(), + value + .iter() + .map(|style| { + ( + style.property().to_string(), + style.value().to_string(), + ) + }) + .collect::>(), + ) + }) + .collect(), + ) { + collected = true; + } + } ExtractStyleValue::Css(cs) => { if sheet.add_css(&cs.file, &cs.css) { collected = true; diff --git a/libs/css/src/class_map.rs b/libs/css/src/class_map.rs index f2c7b992..693d68fa 100644 --- a/libs/css/src/class_map.rs +++ b/libs/css/src/class_map.rs @@ -19,3 +19,27 @@ pub fn set_class_map(map: HashMap) { pub fn get_class_map() -> HashMap { GLOBAL_CLASS_MAP.lock().unwrap().clone() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_set_and_get_class_map() { + let mut test_map = HashMap::new(); + test_map.insert("test-key".to_string(), 42); + set_class_map(test_map.clone()); + let got = get_class_map(); + assert_eq!(got.get("test-key"), Some(&42)); + } + + #[test] + fn test_reset_class_map() { + let mut test_map = HashMap::new(); + test_map.insert("reset-key".to_string(), 1); + set_class_map(test_map); + reset_class_map(); + let got = get_class_map(); + assert!(got.is_empty()); + } +} diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index 28d692aa..c9df267b 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -38,6 +38,20 @@ pub fn short_to_long(property: &str) -> String { .unwrap_or_else(|| property.to_string()) } +pub fn keyframes_to_keyframes_name(keyframes: &str) -> String { + if is_debug() { + format!("k-{keyframes}") + } else { + let key = format!("k-{keyframes}"); + let mut map = GLOBAL_CLASS_MAP.lock().unwrap(); + map.get(&key).map(|v| format!("k{v}")).unwrap_or_else(|| { + let len = map.len(); + map.insert(key, len as i32); + format!("k{}", map.len() - 1) + }) + } +} + pub fn sheet_to_classname( property: &str, level: u8, @@ -419,4 +433,18 @@ mod tests { set_class_map(map); assert_eq!(get_class_map().len(), 1); } + + #[test] + #[serial] + fn test_keyframes_to_keyframes_name() { + reset_class_map(); + set_debug(false); + assert_eq!(keyframes_to_keyframes_name("spin"), "k0"); + assert_eq!(keyframes_to_keyframes_name("spin"), "k0"); + assert_eq!(keyframes_to_keyframes_name("spin2"), "k1"); + reset_class_map(); + set_debug(true); + assert_eq!(keyframes_to_keyframes_name("spin"), "k-spin"); + assert_eq!(keyframes_to_keyframes_name("spin1"), "k-spin1"); + } } diff --git a/libs/extractor/src/css_utils.rs b/libs/extractor/src/css_utils.rs index 35d5e7a2..bdcca762 100644 --- a/libs/extractor/src/css_utils.rs +++ b/libs/extractor/src/css_utils.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use css::{style_selector::StyleSelector, utils::to_camel_case}; use crate::{ @@ -30,6 +32,37 @@ pub fn css_to_style<'a>( .collect() } +pub fn keyframes_to_keyframes_style<'a>( + keyframes: &str, +) -> BTreeMap>> { + let mut map = BTreeMap::new(); + let mut input = keyframes; + + while let Some(start) = input.find('{') { + let key = input[..start].trim().to_string(); + let rest = &input[start + 1..]; + if let Some(end) = rest.find('}') { + let block = &rest[..end]; + let mut styles = css_to_style(block, 0, &None) + .into_iter() + .collect::>(); + + styles.sort_by_key(|a| { + if let crate::ExtractStyleProp::Static(crate::ExtractStyleValue::Static(a)) = a { + a.property().to_string() + } else { + "".to_string() + } + }); + map.insert(key, styles); + input = &rest[end + 1..]; + } else { + break; + } + } + map +} + pub fn optimize_css_block(css: &str) -> String { css.split("{") .map(|s| s.trim().to_string()) @@ -143,4 +176,72 @@ mod tests { expected_sorted.sort(); assert_eq!(result, expected_sorted); } + + #[rstest] + #[case( + "to {\nbackground-color:red;\n}\nfrom {\nbackground-color:blue;\n}", + vec![ + ("to", vec![("backgroundColor", "red")]), + ("from", vec![("backgroundColor", "blue")]), + ], + )] + #[case( + "0% { opacity: 0; }\n100% { opacity: 1; }", + vec![ + ("0%", vec![("opacity", "0")]), + ("100%", vec![("opacity", "1")]), + ], + )] + #[case( + "from { left: 0; }\nto { left: 100px; }", + vec![ + ("from", vec![("left", "0")]), + ("to", vec![("left", "100px")]), + ], + )] + #[case( + "50% { color: red; background: blue; }", + vec![ + ("50%", vec![("color", "red"), ("background", "blue")]), + ], + )] + #[case( + "", + vec![], + )] + #[case( + "50% { color: red ; background: blue; }", + vec![ + ("50%", vec![("color", "red"), ("background", "blue")]), + ], + )] + fn test_keyframes_to_keyframes_style( + #[case] input: &str, + #[case] expected: Vec<(&str, Vec<(&str, &str)>)>, + ) { + let styles = keyframes_to_keyframes_style(input); + for (expected_key, expected_styles) in styles.iter() { + let styles = expected_styles; + let mut result: Vec<(&str, &str)> = styles + .iter() + .filter_map(|prop| { + if let crate::ExtractStyleProp::Static(crate::ExtractStyleValue::Static(st)) = + prop + { + Some((st.property(), st.value())) + } else { + None + } + }) + .collect(); + result.sort(); + let mut expected_sorted = expected + .iter() + .find(|(k, _)| k == expected_key) + .map(|(_, v)| v.clone()) + .unwrap(); + expected_sorted.sort(); + assert_eq!(result, expected_sorted); + } + } } diff --git a/libs/extractor/src/extract_style/extract_dynamic_style.rs b/libs/extractor/src/extract_style/extract_dynamic_style.rs index 44d66ed1..588469b2 100644 --- a/libs/extractor/src/extract_style/extract_dynamic_style.rs +++ b/libs/extractor/src/extract_style/extract_dynamic_style.rs @@ -3,7 +3,7 @@ use css::{ style_selector::StyleSelector, }; -use crate::{StyleProperty, extract_style::ExtractStyleProperty}; +use crate::extract_style::{ExtractStyleProperty, style_property::StyleProperty}; #[derive(Debug, PartialEq, Clone, Eq, Hash, Ord, PartialOrd)] pub struct ExtractDynamicStyle { diff --git a/libs/extractor/src/extract_style/extract_keyframes.rs b/libs/extractor/src/extract_style/extract_keyframes.rs new file mode 100644 index 00000000..66f8f39a --- /dev/null +++ b/libs/extractor/src/extract_style/extract_keyframes.rs @@ -0,0 +1,24 @@ +use std::{ + collections::BTreeMap, + hash::{DefaultHasher, Hash, Hasher}, +}; + +use css::keyframes_to_keyframes_name; + +use crate::extract_style::{ + ExtractStyleProperty, extract_static_style::ExtractStaticStyle, style_property::StyleProperty, +}; + +#[derive(Debug, Default, PartialEq, Clone, Eq, Hash, Ord, PartialOrd)] +pub struct ExtractKeyframes { + pub keyframes: BTreeMap>, +} + +impl ExtractStyleProperty for ExtractKeyframes { + fn extract(&self) -> StyleProperty { + let mut hasher = DefaultHasher::new(); + self.keyframes.hash(&mut hasher); + let hash_key = hasher.finish().to_string(); + StyleProperty::ClassName(keyframes_to_keyframes_name(&hash_key)) + } +} diff --git a/libs/extractor/src/extract_style/extract_static_style.rs b/libs/extractor/src/extract_style/extract_static_style.rs index 99c24da5..99b46cb3 100644 --- a/libs/extractor/src/extract_style/extract_static_style.rs +++ b/libs/extractor/src/extract_style/extract_static_style.rs @@ -4,8 +4,9 @@ use css::{ }; use crate::{ - StyleProperty, - extract_style::{ExtractStyleProperty, constant::MAINTAIN_VALUE_PROPERTIES}, + extract_style::{ + ExtractStyleProperty, constant::MAINTAIN_VALUE_PROPERTIES, style_property::StyleProperty, + }, utils::convert_value, }; diff --git a/libs/extractor/src/extract_style/extract_style_value.rs b/libs/extractor/src/extract_style/extract_style_value.rs index c944f02e..bfecf41c 100644 --- a/libs/extractor/src/extract_style/extract_style_value.rs +++ b/libs/extractor/src/extract_style/extract_style_value.rs @@ -1,9 +1,7 @@ -use crate::{ - StyleProperty, - extract_style::{ - ExtractStyleProperty, extract_css::ExtractCss, extract_dynamic_style::ExtractDynamicStyle, - extract_import::ExtractImport, extract_static_style::ExtractStaticStyle, - }, +use crate::extract_style::{ + ExtractStyleProperty, extract_css::ExtractCss, extract_dynamic_style::ExtractDynamicStyle, + extract_import::ExtractImport, extract_keyframes::ExtractKeyframes, + extract_static_style::ExtractStaticStyle, style_property::StyleProperty, }; #[derive(Debug, PartialEq, Clone, Eq, Hash, Ord, PartialOrd)] @@ -13,6 +11,7 @@ pub enum ExtractStyleValue { Dynamic(ExtractDynamicStyle), Css(ExtractCss), Import(ExtractImport), + Keyframes(ExtractKeyframes), } impl ExtractStyleValue { @@ -20,6 +19,7 @@ impl ExtractStyleValue { match self { ExtractStyleValue::Static(style) => Some(style.extract()), ExtractStyleValue::Dynamic(style) => Some(style.extract()), + ExtractStyleValue::Keyframes(keyframes) => Some(keyframes.extract()), ExtractStyleValue::Typography(typo) => { Some(StyleProperty::ClassName(format!("typo-{typo}"))) } @@ -60,4 +60,37 @@ mod tests { assert_eq!(style.style_order(), Some(1)); } } + #[test] + fn test_extract() { + let style = ExtractStaticStyle::new("margin", "10px", 0, None); + let value = ExtractStyleValue::Static(style); + let extracted = value.extract(); + assert!(matches!(extracted, Some(StyleProperty::ClassName(_)))); + + let style = ExtractDynamicStyle::new("margin", 0, "10px", None); + let value = ExtractStyleValue::Dynamic(style); + let extracted = value.extract(); + assert!(matches!(extracted, Some(StyleProperty::Variable { .. }))); + + let keyframes = ExtractKeyframes::default(); + let value = ExtractStyleValue::Keyframes(keyframes); + let extracted = value.extract(); + assert!(matches!(extracted, Some(StyleProperty::ClassName(_)))); + + let value = ExtractStyleValue::Typography("body1".to_string()); + let extracted = value.extract(); + assert!(matches!(extracted, Some(StyleProperty::ClassName(_)))); + + let value = ExtractStyleValue::Css(ExtractCss { + css: "".to_string(), + file: "".to_string(), + }); + assert!(matches!(value.extract(), None)); + + let value = ExtractStyleValue::Import(ExtractImport { + url: "".to_string(), + file: "".to_string(), + }); + assert!(matches!(value.extract(), None)); + } } diff --git a/libs/extractor/src/extract_style/mod.rs b/libs/extractor/src/extract_style/mod.rs index d5571caf..dd7dbd9c 100644 --- a/libs/extractor/src/extract_style/mod.rs +++ b/libs/extractor/src/extract_style/mod.rs @@ -2,10 +2,12 @@ pub(super) mod constant; pub(super) mod extract_css; pub(super) mod extract_dynamic_style; pub(super) mod extract_import; +pub(super) mod extract_keyframes; pub(super) mod extract_static_style; pub mod extract_style_value; +pub mod style_property; -use crate::StyleProperty; +use crate::extract_style::style_property::StyleProperty; pub trait ExtractStyleProperty { /// extract style properties diff --git a/libs/extractor/src/extract_style/style_property.rs b/libs/extractor/src/extract_style/style_property.rs new file mode 100644 index 00000000..4c04eed4 --- /dev/null +++ b/libs/extractor/src/extract_style/style_property.rs @@ -0,0 +1,36 @@ +pub enum StyleProperty { + ClassName(String), + Variable { + class_name: String, + variable_name: String, + identifier: String, + }, +} +impl StyleProperty { + pub fn to_string(&self) -> String { + match self { + StyleProperty::ClassName(name) => name.clone(), + StyleProperty::Variable { variable_name, .. } => format!("var({})", variable_name), + } + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_string_class_name() { + let prop = StyleProperty::ClassName("my-class".to_string()); + assert_eq!(prop.to_string(), "my-class".to_string()); + } + + #[test] + fn test_to_string_variable() { + let prop = StyleProperty::Variable { + class_name: "cls".to_string(), + variable_name: "--var-name".to_string(), + identifier: "id".to_string(), + }; + assert_eq!(prop.to_string(), "var(--var-name)".to_string()); + } +} diff --git a/libs/extractor/src/extractor/extract_keyframes_from_expression.rs b/libs/extractor/src/extractor/extract_keyframes_from_expression.rs new file mode 100644 index 00000000..6bc44157 --- /dev/null +++ b/libs/extractor/src/extractor/extract_keyframes_from_expression.rs @@ -0,0 +1,56 @@ +use crate::{ + ExtractStyleProp, + extract_style::{extract_keyframes::ExtractKeyframes, extract_style_value::ExtractStyleValue}, + extractor::{ + KeyframesExtractResult, extract_style_from_expression::extract_style_from_expression, + }, +}; +use oxc_ast::{ + AstBuilder, + ast::{Expression, ObjectPropertyKind, PropertyKey}, +}; + +pub fn extract_keyframes_from_expression<'a>( + ast_builder: &AstBuilder<'a>, + expression: &mut Expression<'a>, +) -> KeyframesExtractResult { + let mut keyframes = ExtractKeyframes::default(); + + if let Expression::ObjectExpression(obj) = expression { + for p in obj.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(o) = p { + let mut name = if let PropertyKey::StaticIdentifier(ident) = &o.key { + ident.name.to_string() + } else if let PropertyKey::StringLiteral(s) = &o.key { + s.value.to_string() + } else if let PropertyKey::TemplateLiteral(t) = &o.key { + t.quasis + .iter() + .map(|q| q.value.raw.as_str()) + .collect::>() + .join("") + } else if let PropertyKey::NumericLiteral(n) = &o.key { + n.value.to_string() + } else { + continue; + }; + // convert number + if let Ok(num) = name.parse::() { + name = format!("{num}%"); + } + let mut styles = + extract_style_from_expression(ast_builder, None, &mut o.value, 0, None) + .styles + .into_iter() + .filter_map(|s| match s { + ExtractStyleProp::Static(ExtractStyleValue::Static(s)) => Some(s), + _ => None, + }) + .collect::>(); + styles.sort_by_key(|a| a.property().to_string()); + keyframes.keyframes.insert(name, styles); + } + } + } + KeyframesExtractResult { keyframes } +} diff --git a/libs/extractor/src/extractor/extract_style_from_expression.rs b/libs/extractor/src/extractor/extract_style_from_expression.rs index 7df0a965..ae358630 100644 --- a/libs/extractor/src/extractor/extract_style_from_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_expression.rs @@ -136,6 +136,18 @@ pub fn extract_style_from_expression<'a>( level, None, ), + Expression::TemplateLiteral(tmp) => ExtractResult { + styles: css_to_style( + &tmp.quasis + .iter() + .map(|q| q.value.raw.as_str()) + .collect::>() + .join(""), + level, + &selector, + ), + ..ExtractResult::default() + }, _ => ExtractResult::default(), }; } diff --git a/libs/extractor/src/extractor/extract_style_from_jsx.rs b/libs/extractor/src/extractor/extract_style_from_jsx.rs index a44c03f5..6e385fbc 100644 --- a/libs/extractor/src/extractor/extract_style_from_jsx.rs +++ b/libs/extractor/src/extractor/extract_style_from_jsx.rs @@ -1,7 +1,6 @@ use crate::extractor::{ ExtractResult, extract_style_from_expression::extract_style_from_expression, }; -use css::style_selector::StyleSelector; use oxc_allocator::CloneIn; use oxc_ast::{ AstBuilder, @@ -12,7 +11,6 @@ pub fn extract_style_from_jsx<'a>( ast_builder: &AstBuilder<'a>, name: &str, value: &mut JSXAttributeValue<'a>, - selector: Option<&StyleSelector>, ) -> ExtractResult<'a> { match value { JSXAttributeValue::ExpressionContainer(expression) => { @@ -22,7 +20,7 @@ pub fn extract_style_from_jsx<'a>( Some(name), expression.expression.to_expression_mut(), 0, - selector, + None, ) } else { ExtractResult::default() @@ -33,7 +31,7 @@ pub fn extract_style_from_jsx<'a>( Some(name), &mut Expression::StringLiteral(literal.clone_in(ast_builder.allocator)), 0, - selector, + None, ), _ => ExtractResult::default(), } diff --git a/libs/extractor/src/extractor/mod.rs b/libs/extractor/src/extractor/mod.rs index a69c7234..61c14f84 100644 --- a/libs/extractor/src/extractor/mod.rs +++ b/libs/extractor/src/extractor/mod.rs @@ -1,8 +1,9 @@ use oxc_ast::ast::Expression; -use crate::ExtractStyleProp; +use crate::{ExtractStyleProp, extract_style::extract_keyframes::ExtractKeyframes}; pub(super) mod extract_global_style_from_expression; +pub(super) mod extract_keyframes_from_expression; pub(super) mod extract_style_from_expression; pub(super) mod extract_style_from_jsx; pub(super) mod extract_style_from_member_expression; @@ -28,3 +29,8 @@ pub struct GlobalExtractResult<'a> { pub styles: Vec>, pub style_order: Option, } + +#[derive(Debug)] +pub struct KeyframesExtractResult { + pub keyframes: ExtractKeyframes, +} diff --git a/libs/extractor/src/gen_class_name.rs b/libs/extractor/src/gen_class_name.rs index 3fa25566..33ebb489 100644 --- a/libs/extractor/src/gen_class_name.rs +++ b/libs/extractor/src/gen_class_name.rs @@ -1,6 +1,7 @@ +use crate::ExtractStyleProp; +use crate::extract_style::style_property::StyleProperty; use crate::prop_modify_utils::convert_class_name; use crate::utils::is_same_expression; -use crate::{ExtractStyleProp, StyleProperty}; use oxc_allocator::CloneIn; use oxc_ast::AstBuilder; use oxc_ast::ast::{Expression, PropertyKey, PropertyKind, TemplateElementValue}; diff --git a/libs/extractor/src/gen_style.rs b/libs/extractor/src/gen_style.rs index 2dc68c42..4a20b67e 100644 --- a/libs/extractor/src/gen_style.rs +++ b/libs/extractor/src/gen_style.rs @@ -1,4 +1,5 @@ -use crate::{ExtractStyleProp, StyleProperty}; +use crate::ExtractStyleProp; +use crate::extract_style::style_property::StyleProperty; use oxc_allocator::CloneIn; use oxc_ast::AstBuilder; use oxc_ast::ast::{Expression, ObjectPropertyKind, PropertyKey, PropertyKind}; diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 4dae94e5..0a1e1fab 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -67,15 +67,6 @@ impl ExtractStyleProp<'_> { } } /// Style property for props -pub enum StyleProperty { - ClassName(String), - Variable { - class_name: String, - variable_name: String, - identifier: String, - }, -} - #[derive(Debug)] pub struct ExtractOutput { // used styles @@ -4618,6 +4609,284 @@ globalCss({}) "test.tsx", r#"import { globalCss } from "@devup-ui/core"; globalCss() +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn extract_keyframs() { + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + from: { opacity: 0 }, + to: { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + "0%": { opacity: 0 }, + "50%": { opacity: 0.5 }, + "100%": { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + "0": { opacity: 0 }, + "50": { opacity: 0.5 }, + "100": { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + ["0"]: { opacity: 0 }, + ["50"]: { opacity: 0.5 }, + ["100"]: { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + [0]: { opacity: 0 }, + [50]: { opacity: 0.5 }, + [100]: { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + 0: { opacity: 0 }, + 50: { opacity: 0.5 }, + 100: { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + [`0`]: { opacity: 0 }, + [`50`]: { opacity: 0.5 }, + [`100`]: { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + [`0%`]: { opacity: 0 }, + [`50%`]: { opacity: 0.5 }, + [`100%`]: { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; + +keyframes({ + [`0`]: { opacity: 0 }, + [`50`]: { opacity: 0.5 }, + [`100`]: { opacity: 1 } +}); +keyframes({ + [`0%`]: { opacity: 0 }, + [`50%`]: { opacity: 0.5 }, + [`100%`]: { opacity: 1 } +}); +keyframes({ + [`1%`]: { opacity: 0 }, + [`50%`]: { opacity: 0.5 }, + [`100%`]: { opacity: 1 } +}); +keyframes({ + [`0%`]: { opacity: 1 }, + [`50%`]: { opacity: 0.5 }, + [`100%`]: { opacity: 1 } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn extract_keyframs_literal() { + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + from: ` + background-color: red; + `, + to: ` + background-color: blue; + ` +}) + +keyframes` + from { + background-color: red; + } + to { + background-color: blue; + } +` +keyframes({ + from: { + backgroundColor: "red" + }, + to: { + backgroundColor: "blue" + } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import { keyframes } from "@devup-ui/core"; +keyframes({ + "0%": ` + background-color: red; + color: blue; + `, + "100%": ` + background-color: blue; + color: red; + ` +}) + +keyframes` + 0% { + background-color: red; + color: blue; + } + 100% { + background-color: blue; + color: red; + } +` +keyframes({ + "0%": { + backgroundColor: "red", + color: "blue" + }, + "100%": { + backgroundColor: "blue", + color: "red" + } +}) "#, ExtractOption { package: "@devup-ui/core".to_string(), diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-2.snap new file mode 100644 index 00000000..e90ec901 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-2.snap @@ -0,0 +1,42 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n \"0%\": { opacity: 0 },\n \"50%\": { opacity: 0.5 },\n \"100%\": { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-3.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-3.snap new file mode 100644 index 00000000..cb339efc --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-3.snap @@ -0,0 +1,42 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n \"0\": { opacity: 0 },\n \"50\": { opacity: 0.5 },\n \"100\": { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-4.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-4.snap new file mode 100644 index 00000000..3fc937c6 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-4.snap @@ -0,0 +1,42 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n [\"0\"]: { opacity: 0 },\n [\"50\"]: { opacity: 0.5 },\n [\"100\"]: { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-5.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-5.snap new file mode 100644 index 00000000..84e3a4ab --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-5.snap @@ -0,0 +1,42 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n [0]: { opacity: 0 },\n [50]: { opacity: 0.5 },\n [100]: { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-6.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-6.snap new file mode 100644 index 00000000..950f909c --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-6.snap @@ -0,0 +1,42 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n 0: { opacity: 0 },\n 50: { opacity: 0.5 },\n 100: { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-7.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-7.snap new file mode 100644 index 00000000..182bdc6b --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-7.snap @@ -0,0 +1,42 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n [`0`]: { opacity: 0 },\n [`50`]: { opacity: 0.5 },\n [`100`]: { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-8.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-8.snap new file mode 100644 index 00000000..bfc4d10c --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-8.snap @@ -0,0 +1,42 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n [`0%`]: { opacity: 0 },\n [`50%`]: { opacity: 0.5 },\n [`100%`]: { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-9.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-9.snap new file mode 100644 index 00000000..1af56979 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs-9.snap @@ -0,0 +1,108 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\n\nkeyframes({\n [`0`]: { opacity: 0 },\n [`50`]: { opacity: 0.5 },\n [`100`]: { opacity: 1 }\n});\nkeyframes({\n [`0%`]: { opacity: 0 },\n [`50%`]: { opacity: 0.5 },\n [`100%`]: { opacity: 1 }\n});\nkeyframes({\n [`1%`]: { opacity: 0 },\n [`50%`]: { opacity: 0.5 },\n [`100%`]: { opacity: 1 }\n});\nkeyframes({\n [`0%`]: { opacity: 1 },\n [`50%`]: { opacity: 0.5 },\n [`100%`]: { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + Keyframes( + ExtractKeyframes { + keyframes: { + "1%": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + "50%": [ + ExtractStaticStyle { + property: "opacity", + value: "0.5", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n\"k0\";\n\"k1\";\n\"k2\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs.snap new file mode 100644 index 00000000..d19b80f8 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs.snap @@ -0,0 +1,33 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n from: { opacity: 0 },\n to: { opacity: 1 }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "from": [ + ExtractStaticStyle { + property: "opacity", + value: "0", + level: 0, + selector: None, + style_order: None, + }, + ], + "to": [ + ExtractStaticStyle { + property: "opacity", + value: "1", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal-2.snap new file mode 100644 index 00000000..5cc346a1 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal-2.snap @@ -0,0 +1,47 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n \"0%\": `\n background-color: red;\n color: blue;\n `,\n \"100%\": `\n background-color: blue;\n color: red;\n `\n})\n\nkeyframes`\n 0% {\n background-color: red;\n color: blue;\n }\n 100% {\n background-color: blue;\n color: red;\n }\n`\nkeyframes({\n \"0%\": {\n backgroundColor: \"red\",\n color: \"blue\"\n },\n \"100%\": {\n backgroundColor: \"blue\",\n color: \"red\"\n }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "0%": [ + ExtractStaticStyle { + property: "backgroundColor", + value: "red", + level: 0, + selector: None, + style_order: None, + }, + ExtractStaticStyle { + property: "color", + value: "blue", + level: 0, + selector: None, + style_order: None, + }, + ], + "100%": [ + ExtractStaticStyle { + property: "backgroundColor", + value: "blue", + level: 0, + selector: None, + style_order: None, + }, + ExtractStaticStyle { + property: "color", + value: "red", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n\"k0\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal.snap b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal.snap new file mode 100644 index 00000000..ef5556b1 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_keyframs_literal.snap @@ -0,0 +1,33 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { keyframes } from \"@devup-ui/core\";\nkeyframes({\n from: `\n background-color: red;\n `,\n to: `\n background-color: blue;\n `\n})\n\nkeyframes`\n from {\n background-color: red;\n }\n to {\n background-color: blue;\n }\n`\nkeyframes({\n from: {\n backgroundColor: \"red\"\n },\n to: {\n backgroundColor: \"blue\"\n }\n})\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +--- +ToBTreeSet { + styles: { + Keyframes( + ExtractKeyframes { + keyframes: { + "from": [ + ExtractStaticStyle { + property: "backgroundColor", + value: "red", + level: 0, + selector: None, + style_order: None, + }, + ], + "to": [ + ExtractStaticStyle { + property: "backgroundColor", + value: "blue", + level: 0, + selector: None, + style_order: None, + }, + ], + }, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui.css\";\n\"k0\";\n\"k0\";\n\"k0\";\n", +} diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs index 09231c67..34ac567e 100644 --- a/libs/extractor/src/utils.rs +++ b/libs/extractor/src/utils.rs @@ -230,6 +230,7 @@ pub(super) fn get_string_by_literal_expression(expr: &Expression) -> Option Some(str.value.into()), + Expression::BooleanLiteral(bool) => Some(bool.value.to_string()), Expression::TemplateLiteral(tmp) => { let mut collect = vec![]; for (idx, q) in tmp.quasis.iter().enumerate() { @@ -252,6 +253,7 @@ pub(super) fn get_string_by_literal_expression(expr: &Expression) -> Option VisitMut<'a> for DevupVisitor<'a> { } } UtilType::Keyframes => { - // TODO: implement keyframes + let KeyframesExtractResult { keyframes } = + extract_keyframes_from_expression( + &self.ast, + call.arguments[0].to_expression_mut(), + ); + + let name = keyframes.extract().to_string(); + self.styles.insert(ExtractStyleValue::Keyframes(keyframes)); self.ast - .expression_string_literal(SPAN, self.ast.atom(""), None) + .expression_string_literal(SPAN, self.ast.atom(&name), None) } UtilType::GlobalCss => { let GlobalExtractResult { @@ -237,7 +248,34 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { ); } UtilType::Keyframes => { - // TODO: implement keyframes + let keyframes = ExtractKeyframes { + keyframes: keyframes_to_keyframes_style(&css_str) + .into_iter() + .filter_map(|(k, v)| { + Some(( + k, + v.into_iter() + .filter_map(|ex| { + if let crate::ExtractStyleProp::Static( + crate::ExtractStyleValue::Static(st), + ) = ex + { + Some(st) + } else { + None + } + }) + .collect(), + )) + }) + .collect(), + }; + let name = keyframes.extract().to_string(); + + self.styles.insert(ExtractStyleValue::Keyframes(keyframes)); + *it = self + .ast + .expression_string_literal(SPAN, self.ast.atom(&name), None); } UtilType::GlobalCss => { let css = ExtractStyleValue::Css(ExtractCss { @@ -502,7 +540,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { if let Some(at) = &mut attr.value { let ExtractResult { styles, tag, .. } = - extract_style_from_jsx(&self.ast, &name, at, None); + extract_style_from_jsx(&self.ast, &name, at); props_styles.extend(styles.into_iter().rev()); tag_name = tag.unwrap_or(tag_name); continue; diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index 79a8257f..247d52b2 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -8,6 +8,7 @@ css = { path = "../css" } serde = { version = "1.0.219", features = ["derive"] } regex = "1.11.1" once_cell = "1.21.3" +extractor = { path = "../extractor" } [dev-dependencies] insta = "1.43.1" diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index 33e99c3b..ad35874f 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -22,6 +22,14 @@ pub struct StyleSheetProperty { pub value: String, pub selector: Option, } + +#[derive(Debug, Hash, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StyleSheetKeyframes { + pub name: String, + pub keyframes: BTreeMap>, +} + impl PartialOrd for StyleSheetProperty { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -124,7 +132,8 @@ pub struct StyleSheet { #[serde(deserialize_with = "deserialize_btree_map_u8")] pub properties: PropertyMap, pub css: BTreeMap>, - + #[serde(default)] + pub keyframes: BTreeMap>>, #[serde(default)] pub global_css_files: BTreeSet, #[serde(default)] @@ -176,6 +185,20 @@ impl StyleSheet { }) } + pub fn add_keyframes( + &mut self, + name: &str, + keyframes: BTreeMap>, + ) -> bool { + let map = self.keyframes.entry(name.to_string()).or_default(); + if map == &keyframes { + return false; + } + map.clear(); + map.extend(keyframes); + true + } + pub fn rm_global_css(&mut self, file: &str) { if !self.global_css_files.contains(file) { return; @@ -208,6 +231,23 @@ impl StyleSheet { .join(""); css.push_str(&self.theme.to_css()); + for (name, map) in self.keyframes.iter() { + css.push_str(&format!( + "@keyframes {name}{{{}}}", + map.iter() + .map(|(key, props)| format!( + "{key}{{{}}}", + props + .iter() + .map(|(key, value)| format!("{key}:{value}")) + .collect::>() + .join(";") + )) + .collect::>() + .join("") + )); + } + for (_, _css) in self.css.iter() { for _css in _css.iter() { css.push_str(&_css.extract()); diff --git a/libs/sheet/src/snapshots/sheet__tests__deserialize.snap b/libs/sheet/src/snapshots/sheet__tests__deserialize.snap index 91f75db5..31e307e1 100644 --- a/libs/sheet/src/snapshots/sheet__tests__deserialize.snap +++ b/libs/sheet/src/snapshots/sheet__tests__deserialize.snap @@ -16,6 +16,7 @@ StyleSheet { }, }, css: {}, + keyframes: {}, global_css_files: {}, imports: {}, theme: Theme { diff --git a/packages/react/src/utils/keyframes.ts b/packages/react/src/utils/keyframes.ts index 79d8b20e..77500196 100644 --- a/packages/react/src/utils/keyframes.ts +++ b/packages/react/src/utils/keyframes.ts @@ -1,16 +1,19 @@ import type { DevupCommonProps } from '../types/props' -export function keyframes( - props: Record<(string & {}) | 'from' | 'to', DevupCommonProps>, -): string +interface KeyframesProps { + from?: DevupCommonProps | string + to?: DevupCommonProps | string + [key: `${number}%`]: DevupCommonProps | string +} + +export function keyframes(props: KeyframesProps): string +export function keyframes(props: Record): string export function keyframes(strings: TemplateStringsArray): string export function keyframes(): string export function keyframes( // eslint-disable-next-line @typescript-eslint/no-unused-vars - strings?: - | TemplateStringsArray - | Record<(string & {}) | 'from' | 'to', DevupCommonProps>, + strings?: TemplateStringsArray | KeyframesProps, ): string { throw new Error('Cannot run on the runtime') } From afb7debe86b70643bd79f935c14545c4ebd04d33 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 19:25:33 +0900 Subject: [PATCH 08/11] Add nocheck --- packages/react/src/utils/reset-css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/utils/reset-css.ts b/packages/react/src/utils/reset-css.ts index b19ce675..ce60d21b 100644 --- a/packages/react/src/utils/reset-css.ts +++ b/packages/react/src/utils/reset-css.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { globalCss } from '@devup-ui/react' globalCss({ From 367d0dec40e156904bafbec6bc9a48d55e0f1fe9 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 19:29:48 +0900 Subject: [PATCH 09/11] Add rollup --- packages/reset-css/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/reset-css/package.json b/packages/reset-css/package.json index 4aecaa37..bc909d4c 100644 --- a/packages/reset-css/package.json +++ b/packages/reset-css/package.json @@ -44,7 +44,8 @@ "devDependencies": { "typescript": "^5.8.3", "vite": "^7.0.3", - "vite-plugin-dts": "^4.5.4" + "vite-plugin-dts": "^4.5.4", + "rollup-plugin-preserve-directives": "^0.4.0" }, "peerDependencies": { "@devup-ui/react": "workspace:*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55858fd5..63188ef7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -451,6 +451,9 @@ importers: specifier: workspace:* version: link:../react devDependencies: + rollup-plugin-preserve-directives: + specifier: ^0.4.0 + version: 0.4.0(rollup@4.44.0) typescript: specifier: ^5.8.3 version: 5.8.3 From 75de46a18de604e1f65ace84c5c3687dfbd72345 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 19:36:18 +0900 Subject: [PATCH 10/11] Add serial --- libs/css/src/class_map.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/css/src/class_map.rs b/libs/css/src/class_map.rs index 693d68fa..1d396173 100644 --- a/libs/css/src/class_map.rs +++ b/libs/css/src/class_map.rs @@ -22,9 +22,12 @@ pub fn get_class_map() -> HashMap { #[cfg(test)] mod tests { + use serial_test::serial; + use super::*; #[test] + #[serial] fn test_set_and_get_class_map() { let mut test_map = HashMap::new(); test_map.insert("test-key".to_string(), 42); @@ -34,6 +37,7 @@ mod tests { } #[test] + #[serial] fn test_reset_class_map() { let mut test_map = HashMap::new(); test_map.insert("reset-key".to_string(), 1); From 97a1679716b6a4e83fb7bcde35f920eab342673e Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 16 Jul 2025 19:44:41 +0900 Subject: [PATCH 11/11] Add vitest --- vitest.config.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 vitest.config.ts diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..33481a20 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,46 @@ +import { DevupUI } from '@devup-ui/vite-plugin' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + include: ['packages/*/src/**'], + exclude: [ + 'packages/*/src/types', + 'packages/*/src/**/__tests__', + '**/*.stories.{ts,tsx}', + ], + cleanOnRerun: true, + reporter: ['text', 'json', 'html'], + }, + projects: [ + { + test: { + name: 'node', + include: ['packages/*/src/**/__tests__/**/*.test.{ts,tsx}'], + exclude: ['packages/*/src/**/__tests__/**/*.browser.test.{ts,tsx}'], + globals: true, + environment: 'node', + }, + }, + + { + test: { + name: 'happy-dom', + include: ['packages/*/src/**/__tests__/**/*.browser.test.{ts,tsx}'], + environment: 'happy-dom', + globals: true, + css: true, + setupFiles: ['@testing-library/jest-dom/vitest'], + }, + plugins: [ + DevupUI({ + debug: true, + }), + ], + }, + ], + cache: false, + }, +})