From 0faa84ee73c4d9b72cf25f61369dc6e049e22284 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 3 Oct 2026 03:41:12 +0900 Subject: [PATCH 1/8] fix(plugin-utils): compile the css prop where tsconfig builds JSX with Emotion Refs #690 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...ngepack_log_emotion_jsx_import_source.json | 14 ++ libs/extractor/src/css_prop.rs | 31 ++++ libs/extractor/src/import_alias_visit.rs | 77 +++++++-- libs/extractor/src/lib.rs | 44 +++++ ...ts__project_building_jsx_with_emotion.snap | 75 +++++++++ ...e_the_project_builds_jsx_with_emotion.snap | 29 ++++ packages/bun-plugin/src/plugin.ts | 3 +- .../next-plugin/src/__tests__/plugin.test.ts | 6 + packages/next-plugin/src/plugin.ts | 6 +- .../src/__tests__/create-compat-types.test.ts | 14 ++ .../src/__tests__/jsx-import-source.test.ts | 119 +++++++++++++ .../__tests__/merge-import-aliases.test.ts | 22 +++ packages/plugin-utils/src/index.ts | 1 + .../plugin-utils/src/jsx-import-source.ts | 156 ++++++++++++++++++ packages/plugin-utils/src/types.ts | 19 ++- packages/react/package.json | 3 + .../react/src/compat/emotion-jsx-runtime.d.ts | 7 + packages/rsbuild-plugin/src/plugin.ts | 6 +- packages/vite-plugin/src/plugin.ts | 6 +- packages/webpack-plugin/src/plugin.ts | 6 +- 20 files changed, 628 insertions(+), 16 deletions(-) create mode 100644 .changepacks/changepack_log_emotion_jsx_import_source.json create mode 100644 libs/extractor/src/snapshots/extractor__import_alias_visit__tests__project_building_jsx_with_emotion.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__emotion_css_prop_where_the_project_builds_jsx_with_emotion.snap create mode 100644 packages/plugin-utils/src/__tests__/jsx-import-source.test.ts create mode 100644 packages/plugin-utils/src/jsx-import-source.ts create mode 100644 packages/react/src/compat/emotion-jsx-runtime.d.ts diff --git a/.changepacks/changepack_log_emotion_jsx_import_source.json b/.changepacks/changepack_log_emotion_jsx_import_source.json new file mode 100644 index 000000000..ceb232995 --- /dev/null +++ b/.changepacks/changepack_log_emotion_jsx_import_source.json @@ -0,0 +1,14 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch", + "packages/plugin-utils/package.json": "Patch", + "packages/react/package.json": "Patch", + "packages/vite-plugin/package.json": "Patch", + "packages/next-plugin/package.json": "Patch", + "packages/webpack-plugin/package.json": "Patch", + "packages/rsbuild-plugin/package.json": "Patch", + "packages/bun-plugin/package.json": "Patch" + }, + "note": "A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/libs/extractor/src/css_prop.rs b/libs/extractor/src/css_prop.rs index 0ea799d6d..408fdfeba 100644 --- a/libs/extractor/src/css_prop.rs +++ b/libs/extractor/src/css_prop.rs @@ -23,6 +23,26 @@ use crate::utils::{binding_root, get_string_by_literal_expression, unwrap_syntax /// The package whose alias makes elements take the `css` prop pub(crate) const EMOTION_REACT: &str = "@emotion/react"; +/// Emotion's JSX runtime, whose alias tells that the project builds JSX with +/// it, as a `jsxImportSource` of `@emotion/react` in tsconfig does +pub(crate) const EMOTION_JSX_RUNTIME: &str = "@emotion/react/jsx-runtime"; + +/// The pragma building a file's JSX with React, as its `css` props compile +pub(crate) const REACT_JSX_PRAGMA: &str = "/** @jsxImportSource react */\n"; + +/// Whether the project builds JSX with Emotion's runtime, which then builds it +/// with React's as every `css` prop compiles +pub(crate) fn builds_jsx_with_emotion(import_aliases: &HashMap) -> bool { + import_aliases.contains_key(EMOTION_REACT) && import_aliases.contains_key(EMOTION_JSX_RUNTIME) +} + +/// Whether `filename` is written in JSX, which the project's JSX settings build +pub(crate) fn is_jsx_file(filename: &str) -> bool { + filename + .rsplit_once('.') + .is_some_and(|(_, extension)| matches!(extension, "tsx" | "jsx")) +} + /// Which elements of a file take Emotion's `css` prop #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum CssProp { @@ -605,6 +625,17 @@ mod tests { ); assert_eq!(react_runtime("@emotion/react"), None); + assert!(is_jsx_file("src/App.tsx")); + assert!(is_jsx_file("App.jsx")); + assert!(!is_jsx_file("App.ts")); + assert!(!is_jsx_file("Makefile")); + + let mut aliases = + HashMap::from([(EMOTION_JSX_RUNTIME.to_string(), ImportAlias::NamedToNamed)]); + assert!(!builds_jsx_with_emotion(&aliases)); + aliases.insert(EMOTION_REACT.to_string(), ImportAlias::NamedToNamed); + assert!(builds_jsx_with_emotion(&aliases)); + let compat = "@devup-ui/react/compat"; assert!(is_jsx_function("react/jsx-runtime", "jsxs", compat)); assert!(is_jsx_function( diff --git a/libs/extractor/src/import_alias_visit.rs b/libs/extractor/src/import_alias_visit.rs index 666fe6b15..f5cf40351 100644 --- a/libs/extractor/src/import_alias_visit.rs +++ b/libs/extractor/src/import_alias_visit.rs @@ -9,7 +9,8 @@ use crate::ImportAlias; use crate::css_prop::{ - CssProp, EMOTION_REACT, emotion_pragma, is_emotion, is_jsx_function, react_runtime, returned, + CssProp, EMOTION_REACT, REACT_JSX_PRAGMA, builds_jsx_with_emotion, emotion_pragma, is_emotion, + is_jsx_file, is_jsx_function, react_runtime, returned, }; use crate::utils::{ get_str_by_property_key, is_vanilla_extract_file, js_number_literal, keeps_bare_number, @@ -349,9 +350,11 @@ pub fn transform_import_aliases_with_edits<'a>( css_prop, }; let emotion_aliased = import_aliases.contains_key(EMOTION_REACT); - // Quick check: if no aliases match and no element takes a `css` prop, - // return original code - if CssProp::of(import_aliases, code, false) == CssProp::Off + let emotion_jsx = builds_jsx_with_emotion(import_aliases); + // Quick check: if no aliases match, no element takes a `css` prop and the + // project builds no JSX with Emotion, return original code + if !(emotion_jsx && is_jsx_file(filename)) + && CssProp::of(import_aliases, code, false) == CssProp::Off && (import_aliases.is_empty() || !import_aliases.keys().any(|alias| code.contains(alias))) { return unchanged(CssProp::Off); @@ -369,14 +372,17 @@ pub fn transform_import_aliases_with_edits<'a>( // Collect import transformations let mut transformations: Vec<(usize, usize, String)> = Vec::new(); let mut numbers = LibraryNumbers::default(); - let mut uses_emotion = false; + let mut uses_emotion = emotion_jsx; + let mut jsx_pragma = false; let compat = format!("{package}/compat"); // A pragma building JSX with Emotion builds it with React once the `css` // props are compiled for comment in &program.comments { let span = comment.content_span(); - if let Some(at) = emotion_pragma(&code[span.start as usize..span.end as usize]) { + let text = &code[span.start as usize..span.end as usize]; + jsx_pragma |= text.contains("@jsx"); + if let Some(at) = emotion_pragma(text) { uses_emotion = true; if emotion_aliased { let start = span.start as usize + at; @@ -384,6 +390,11 @@ pub fn transform_import_aliases_with_edits<'a>( } } } + // So does a project building every file's JSX with Emotion, through a + // pragma, which a file's own one overrides + if emotion_jsx && is_jsx_file(filename) && !jsx_pragma { + transformations.push((0, 0, REACT_JSX_PRAGMA.to_string())); + } for stmt in &program.body { if let Statement::ImportDeclaration(import_decl) = stmt { @@ -413,9 +424,7 @@ pub fn transform_import_aliases_with_edits<'a>( span.end as usize, format!("\"{runtime}\""), )); - } - - if let Some(alias) = import_aliases.get(source_value) { + } else if let Some(alias) = import_aliases.get(source_value) { let span = import_decl.span; let new_import = generate_transformed_import(import_decl, alias, package, redirect_every_name); @@ -470,7 +479,8 @@ pub fn transform_import_aliases_with_edits<'a>( numbers.visit_program(&program); transformations.append(&mut numbers.replacements); } - transformations.sort_unstable_by_key(|(start, ..)| *start); + // An insertion comes before a replacement starting where it does + transformations.sort_unstable_by_key(|(start, end, _)| (*start, *end)); // Apply transformations in reverse order to preserve positions if transformations.is_empty() { @@ -1212,4 +1222,51 @@ const x = 1;", assert_eq!(without_css_prop.css_prop, CssProp::Off); assert!(matches!(without_css_prop.code, Cow::Borrowed(_))); } + + #[test] + fn test_project_building_jsx_with_emotion() { + let mut aliases = emotion_alias(); + aliases.insert(EMOTION_REACT.to_string(), ImportAlias::NamedToNamed); + aliases.insert( + crate::css_prop::EMOTION_JSX_RUNTIME.to_string(), + ImportAlias::NamedToNamed, + ); + let outputs: Vec<(String, CssProp, Vec)> = [ + ("test.tsx", "export const a = ;"), + ( + "test.jsx", + "import styled from '@emotion/styled';\nexport const a =
;", + ), + ( + "test.tsx", + "/** @jsxImportSource @emotion/react */\nexport const a =
;", + ), + ("test.tsx", "/** @jsx h */\nexport const a =
;"), + ("test.ts", "export const a = 1;"), + ( + "test.ts", + "import { jsx as _jsx } from '@emotion/react/jsx-runtime';\nexport const a = _jsx(Custom, { css: { top: 1 } });", + ), + ] + .iter() + .map(|(filename, code)| { + let aliased = + transform_import_aliases_with_edits(code, filename, "@devup-ui/react", &aliases); + (aliased.code.into_owned(), aliased.css_prop, aliased.edits) + }) + .collect(); + insta::assert_debug_snapshot!(outputs); + + let runtime_alone = transform_import_aliases_with_edits( + "export const a =
;", + "test.tsx", + "@devup-ui/react", + &HashMap::from([( + crate::css_prop::EMOTION_JSX_RUNTIME.to_string(), + ImportAlias::NamedToNamed, + )]), + ); + assert_eq!(runtime_alone.css_prop, CssProp::Off); + assert!(matches!(runtime_alone.code, Cow::Borrowed(_))); + } } diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index d101e4afa..9415e1c89 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -21376,6 +21376,50 @@ export const App = () =>
;\nexport const App = () => ;", + option.clone(), + ) + .unwrap(); + assert_debug_snapshot!(ToBTreeSet::from(output)); + + reset_class_map(); + reset_file_map(); + let plain = extract( + "test.tsx", + "export const App = () =>
;", + option.clone(), + ) + .unwrap(); + assert_eq!( + plain.code, + "/** @jsxImportSource react */\nexport const App = () =>
;\n" + ); + + reset_class_map(); + reset_file_map(); + let error = extract( + "test.tsx", + "import { css } from '@emotion/react';\nexport const App = () =>
;", + option, + ) + .err() + .map(|error| error.to_string()) + .unwrap_or_default(); + assert!(error.starts_with("test.tsx:2:36: "), "{error}"); + } + #[test] #[serial] fn test_emotion_css_prop_reports_what_it_cannot_compile() { diff --git a/libs/extractor/src/snapshots/extractor__import_alias_visit__tests__project_building_jsx_with_emotion.snap b/libs/extractor/src/snapshots/extractor__import_alias_visit__tests__project_building_jsx_with_emotion.snap new file mode 100644 index 000000000..f6128ba1a --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__import_alias_visit__tests__project_building_jsx_with_emotion.snap @@ -0,0 +1,75 @@ +--- +source: libs/extractor/src/import_alias_visit.rs +expression: outputs +--- +[ + ( + "/** @jsxImportSource react */\nexport const a = ;", + Everywhere, + [ + ( + 0, + 0, + 30, + ), + ( + 41, + 42, + 5, + ), + ], + ), + ( + "/** @jsxImportSource react */\nimport { styled } from '@devup-ui/react';\nexport const a =
;", + Off, + [ + ( + 0, + 0, + 30, + ), + ( + 0, + 37, + 41, + ), + ], + ), + ( + "/** @jsxImportSource react */\nexport const a =
;", + Off, + [ + ( + 21, + 35, + 5, + ), + ], + ), + ( + "/** @jsx h */\nexport const a =
;", + Off, + [], + ), + ( + "export const a = 1;", + Off, + [], + ), + ( + "import { jsx as _jsx } from \"react/jsx-runtime\";\nexport const a = _jsx(Custom, { css: { top: \"1px\" } });", + Everywhere, + [ + ( + 28, + 56, + 19, + ), + ( + 102, + 103, + 5, + ), + ], + ), +] diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_css_prop_where_the_project_builds_jsx_with_emotion.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_css_prop_where_the_project_builds_jsx_with_emotion.snap new file mode 100644 index 000000000..32e6388c3 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_css_prop_where_the_project_builds_jsx_with_emotion.snap @@ -0,0 +1,29 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(output)" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "color", + value: "red", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "2px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n/** @jsxImportSource react */\nconst Custom = ({ className }) =>
;\nexport const App = () => ;\n", +} diff --git a/packages/bun-plugin/src/plugin.ts b/packages/bun-plugin/src/plugin.ts index 52591c9ee..81edd30ae 100644 --- a/packages/bun-plugin/src/plugin.ts +++ b/packages/bun-plugin/src/plugin.ts @@ -9,6 +9,7 @@ import { type CustomShorthands, loadDevupConfig, mergeImportAliases, + readJsxImportSource, } from '@devup-ui/plugin-utils' import { codeExtract, @@ -29,7 +30,7 @@ const devupFile = 'devup.json' const distDir = 'df' const cssDir = resolve(distDir, cssDirName) const singleCss = true -const importAliases = mergeImportAliases() +const importAliases = mergeImportAliases(undefined, readJsxImportSource()) export interface DevupUIBunPluginOptions { shorthands?: CustomShorthands diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index 8bc599b7b..0f4ddce2d 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -89,6 +89,7 @@ let codeExtractSpy: ReturnType let codeExtractWithoutSourceMapSpy: ReturnType let devupUIWebpackPluginSpy: ReturnType let startCoordinatorSpy: ReturnType +let readJsxImportSourceSpy: ReturnType let originalEnv: NodeJS.ProcessEnv let originalFetch: typeof global.fetch @@ -142,6 +143,10 @@ beforeEach(() => { coordinatorModule, 'startCoordinator', ).mockReturnValue({ close: mock() as () => void }) + readJsxImportSourceSpy = spyOn( + importGraphModule, + 'readJsxImportSource', + ).mockReturnValue(undefined) setWasmForTesting(wasm) setWebpackPluginForTesting(webpackPluginModule) @@ -158,6 +163,7 @@ afterEach(() => { process.env = originalEnv global.fetch = originalFetch process.debugPort = originalDebugPort + readJsxImportSourceSpy.mockRestore() existsSyncSpy.mockRestore() mkdirSyncSpy.mockRestore() readFileSyncSpy.mockRestore() diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 7905cc7d0..e69c4d21f 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -20,6 +20,7 @@ import { loadDevupConfigSync, mergeImportAliases, planAtomHoist, + readJsxImportSource, type StaticImportGraph, } from '@devup-ui/plugin-utils' import { type NextConfig } from 'next' @@ -144,7 +145,10 @@ export function DevupUI( importAliases: userImportAliases, } = options - const importAliases = mergeImportAliases(userImportAliases) + const importAliases = mergeImportAliases( + userImportAliases, + readJsxImportSource(), + ) const watch = process.env.NODE_ENV === 'development' const sourceMap = watch || config.productionBrowserSourceMaps === true const sheetFile = join(distDir, 'sheet.json') diff --git a/packages/plugin-utils/src/__tests__/create-compat-types.test.ts b/packages/plugin-utils/src/__tests__/create-compat-types.test.ts index dd3fff83d..eb9c9f019 100644 --- a/packages/plugin-utils/src/__tests__/create-compat-types.test.ts +++ b/packages/plugin-utils/src/__tests__/create-compat-types.test.ts @@ -45,6 +45,20 @@ describe('createCompatTypes', () => { ) }) + it("types Emotion's JSX runtime where the project builds JSX with it", () => { + expect( + createCompatTypes(mergeImportAliases(undefined, '@emotion/react')), + ).toBe( + [ + '/// ', + '/// ', + '/// ', + '/// ', + '/// ', + ].join('\n') + '\n', + ) + }) + it('still emits stylex when nothing is aliased', () => { expect(createCompatTypes({})).toBe( '/// \n', diff --git a/packages/plugin-utils/src/__tests__/jsx-import-source.test.ts b/packages/plugin-utils/src/__tests__/jsx-import-source.test.ts new file mode 100644 index 000000000..8b9c9d6d8 --- /dev/null +++ b/packages/plugin-utils/src/__tests__/jsx-import-source.test.ts @@ -0,0 +1,119 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, beforeEach, describe, expect, it } from 'bun:test' + +import { readJsxImportSource } from '../jsx-import-source' + +describe('readJsxImportSource', () => { + let cwd: string + + function write(path: string, content: string) { + mkdirSync(dirname(join(cwd, path)), { recursive: true }) + writeFileSync(join(cwd, path), content) + } + + beforeEach(() => { + cwd = mkdtempSync(join(tmpdir(), 'devup-ui-jsx-import-source-')) + }) + + afterEach(() => { + rmSync(cwd, { recursive: true, force: true }) + }) + + it('reads nothing without a config', () => { + expect(readJsxImportSource(cwd)).toBeUndefined() + }) + + it('reads the default directory when none is given', () => { + expect(readJsxImportSource()).toBe(readJsxImportSource(process.cwd())) + }) + + it('reads tsconfig, with the comments and trailing commas it allows', () => { + write( + 'tsconfig.json', + `{ + // the JSX runtime + "compilerOptions": { + /* Emotion's */ "jsxImportSource": "@emotion/react", + "paths": { "a\\"b": ["./src/*"], }, + }, +}`, + ) + write( + 'jsconfig.json', + '{ "compilerOptions": { "jsxImportSource": "preact" } }', + ) + expect(readJsxImportSource(cwd)).toBe('@emotion/react') + }) + + it('reads jsconfig when there is no tsconfig', () => { + write( + 'jsconfig.json', + '{ "compilerOptions": { "jsxImportSource": "preact" } }', + ) + expect(readJsxImportSource(cwd)).toBe('preact') + }) + + it('follows extends, a later entry overriding an earlier one', () => { + write( + 'tsconfig.json', + '{ "extends": ["./base/first", "@acme/config", "./base/second.json"], "compilerOptions": { "jsxImportSource": 1 } }', + ) + write( + 'base/first.json', + '{ "compilerOptions": { "jsxImportSource": "first" } }', + ) + write('base/second.json', '{ "compilerOptions": {} }') + write( + 'node_modules/@acme/config/tsconfig.json', + '{ "compilerOptions": { "jsxImportSource": "@emotion/react" } }', + ) + expect(readJsxImportSource(cwd)).toBe('@emotion/react') + }) + + it("follows a package's main config", () => { + write('tsconfig.json', '{ "extends": "@acme/main" }') + write( + 'node_modules/@acme/main/package.json', + '{ "name": "@acme/main", "main": "base.json" }', + ) + write( + 'node_modules/@acme/main/base.json', + '{ "compilerOptions": { "jsxImportSource": "@emotion/react" } }', + ) + expect(readJsxImportSource(cwd)).toBe('@emotion/react') + }) + + it('follows project references to a file or a directory', () => { + write( + 'tsconfig.json', + '{ "files": [], "extends": ["@acme/missing", "./missing"], "references": [1, { "path": 2 }, { "path": "./gone" }, { "path": "./tsconfig.node.json" }, { "path": "./app" }] }', + ) + write('tsconfig.node.json', '{ "compilerOptions": {} }') + write( + 'app/tsconfig.json', + '{ "compilerOptions": { "jsxImportSource": "@emotion/react" } }', + ) + expect(readJsxImportSource(cwd)).toBe('@emotion/react') + }) + + it('stops at configs extending each other', () => { + write('tsconfig.json', '{ "extends": "./other.json" }') + write('other.json', '{ "extends": "./tsconfig.json" }') + expect(readJsxImportSource(cwd)).toBeUndefined() + }) + + it('reads nothing from a config it cannot parse', () => { + write('tsconfig.json', '{ "compilerOptions": /* unterminated') + expect(readJsxImportSource(cwd)).toBeUndefined() + write('tsconfig.json', '{ "extends": "./base" } // last line') + write('base', '{ "compilerOptions": { "jsxImportSource": "base" } }') + expect(readJsxImportSource(cwd)).toBe('base') + write('tsconfig.json', '{ "compilerOptions": [1, 2,] }') + expect(readJsxImportSource(cwd)).toBeUndefined() + write('tsconfig.json', '{ "compilerOptions": {} },') + expect(readJsxImportSource(cwd)).toBeUndefined() + }) +}) diff --git a/packages/plugin-utils/src/__tests__/merge-import-aliases.test.ts b/packages/plugin-utils/src/__tests__/merge-import-aliases.test.ts index 286292711..ea4cec89c 100644 --- a/packages/plugin-utils/src/__tests__/merge-import-aliases.test.ts +++ b/packages/plugin-utils/src/__tests__/merge-import-aliases.test.ts @@ -79,6 +79,28 @@ describe('mergeImportAliases', () => { // null = named exports 1:1 mapping expect(result['named-export-lib']).toBeNull() }) + + it("aliases Emotion's JSX runtime where the project builds JSX with it", () => { + expect(mergeImportAliases(undefined, '@emotion/react')).toEqual({ + '@emotion/react': null, + '@emotion/react/jsx-runtime': null, + '@emotion/styled': 'styled', + 'styled-components': 'styled', + '@vanilla-extract/css': null, + }) + expect( + mergeImportAliases({ '@emotion/react': false }, '@emotion/react'), + ).not.toHaveProperty('@emotion/react/jsx-runtime') + expect( + mergeImportAliases( + { '@emotion/react/jsx-runtime': false }, + '@emotion/react', + ), + ).not.toHaveProperty('@emotion/react/jsx-runtime') + expect(mergeImportAliases(undefined, 'preact')).not.toHaveProperty( + '@emotion/react/jsx-runtime', + ) + }) }) describe('DEFAULT_IMPORT_ALIASES', () => { diff --git a/packages/plugin-utils/src/index.ts b/packages/plugin-utils/src/index.ts index b66b07abb..bdca8063f 100644 --- a/packages/plugin-utils/src/index.ts +++ b/packages/plugin-utils/src/index.ts @@ -16,6 +16,7 @@ export { type ResolvedModule, type StaticImportGraph, } from './import-graph' +export { readJsxImportSource } from './jsx-import-source' export { deepMerge, loadDevupConfig, loadDevupConfigSync } from './load-config' export { createNodeModulesExcludeRegex, diff --git a/packages/plugin-utils/src/jsx-import-source.ts b/packages/plugin-utils/src/jsx-import-source.ts new file mode 100644 index 000000000..0531c083e --- /dev/null +++ b/packages/plugin-utils/src/jsx-import-source.ts @@ -0,0 +1,156 @@ +import { existsSync, readFileSync, statSync } from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, isAbsolute, join, resolve } from 'node:path' + +interface TsConfig { + extends?: unknown + compilerOptions?: { jsxImportSource?: unknown } + references?: unknown +} + +/** + * Where the comment starting at `index` of `text` ends, or `index` when none + * starts there + */ +function skipComment(text: string, index: number): number { + if (text.startsWith('//', index)) { + const end = text.indexOf('\n', index) + return end < 0 ? text.length : end + } + if (text.startsWith('/*', index)) { + const end = text.indexOf('*/', index + 2) + return end < 0 ? text.length : end + 2 + } + return index +} + +/** + * The character after `index` of `text` that is neither space nor comment + */ +function nextToken(text: string, index: number): string | undefined { + let at = index + while (at < text.length) { + const skipped = skipComment(text, at) + if (skipped !== at) at = skipped + else if (/\s/.test(text[at])) at++ + else return text[at] + } + return undefined +} + +/** + * JSON with the comments and trailing commas tsconfig allows, as JSON + */ +function stripJsonc(text: string): string { + let json = '' + let index = 0 + while (index < text.length) { + const char = text[index] + const skipped = skipComment(text, index) + if (skipped !== index) { + index = skipped + } else if (char === '"') { + const start = index + index++ + while (index < text.length && text[index] !== '"') { + index += text[index] === '\\' ? 2 : 1 + } + index++ + json += text.slice(start, index) + } else { + const trailing = + char === ',' && /[}\]]/.test(nextToken(text, index + 1) ?? '') + if (!trailing) json += char + index++ + } + } + return json +} + +function readConfig(path: string): TsConfig | undefined { + try { + return JSON.parse(stripJsonc(readFileSync(path, 'utf-8'))) as TsConfig + } catch { + return undefined + } +} + +/** + * The file an `extends` entry of the config at `from` names: a path, with + * `.json` optional, or a package's config + */ +function resolveExtended(entry: string, from: string): string | undefined { + if (entry.startsWith('.') || isAbsolute(entry)) { + const path = resolve(dirname(from), entry) + return [path, `${path}.json`].find((candidate) => existsSync(candidate)) + } + const require = createRequire(from) + for (const candidate of [entry, `${entry}/tsconfig.json`]) { + try { + return require.resolve(candidate) + } catch { + // Not a module path; the package may keep its config elsewhere + } + } + return undefined +} + +/** + * The config a project reference names: a config file, or a directory + * holding `tsconfig.json` + */ +function resolveReference( + reference: unknown, + from: string, +): string | undefined { + const path = + typeof reference === 'object' && + reference !== null && + 'path' in reference && + typeof reference.path === 'string' + ? resolve(dirname(from), reference.path) + : undefined + if (!path || !existsSync(path)) return undefined + return statSync(path).isDirectory() ? join(path, 'tsconfig.json') : path +} + +function jsxImportSourceOf( + path: string, + seen: Set, +): string | undefined { + if (seen.has(path)) return undefined + seen.add(path) + const config = readConfig(path) + const own = config?.compilerOptions?.jsxImportSource + if (typeof own === 'string') return own + const extended = [config?.extends] + .flat() + .filter((entry): entry is string => typeof entry === 'string') + // A later entry of `extends` overrides an earlier one + for (const entry of extended.reverse()) { + const parent = resolveExtended(entry, path) + const source = parent && jsxImportSourceOf(parent, seen) + if (source) return source + } + const references = Array.isArray(config?.references) ? config.references : [] + for (const reference of references) { + const referenced = resolveReference(reference, path) + const source = referenced && jsxImportSourceOf(referenced, seen) + if (source) return source + } + return undefined +} + +/** + * The module the project builds JSX with, as its `tsconfig.json` (or, without + * one, `jsconfig.json`) in `cwd` sets `compilerOptions.jsxImportSource`, + * following `extends` and project references + */ +export function readJsxImportSource( + cwd: string = process.cwd(), +): string | undefined { + const config = ['tsconfig.json', 'jsconfig.json'] + .map((name) => join(cwd, name)) + .find((path) => existsSync(path)) + return config && jsxImportSourceOf(config, new Set()) +} diff --git a/packages/plugin-utils/src/types.ts b/packages/plugin-utils/src/types.ts index 1a1365c2d..5b2ed69e1 100644 --- a/packages/plugin-utils/src/types.ts +++ b/packages/plugin-utils/src/types.ts @@ -117,10 +117,18 @@ export const DEFAULT_IMPORT_ALIASES: ImportAliases = { */ export type WasmImportAliases = Record +/** Emotion's JSX runtime, aliased when the project builds JSX with it */ +const EMOTION_JSX_RUNTIME = '@emotion/react/jsx-runtime' + /** * Merge user import aliases with defaults and convert to WASM format * + * A project building JSX with Emotion (`jsxImportSource` of `@emotion/react`) + * also aliases `@emotion/react/jsx-runtime`: its `css` props then compile in + * every file, and its JSX is built with React's runtime. + * * @param userAliases - User-provided aliases (optional) + * @param jsxImportSource - The module the project builds JSX with (optional) * @returns WASM-compatible import aliases * * @example @@ -131,8 +139,16 @@ export type WasmImportAliases = Record */ export function mergeImportAliases( userAliases?: ImportAliases, + jsxImportSource?: string, ): WasmImportAliases { - const merged = { ...DEFAULT_IMPORT_ALIASES, ...userAliases } + const merged: ImportAliases = { ...DEFAULT_IMPORT_ALIASES, ...userAliases } + if ( + jsxImportSource === '@emotion/react' && + merged['@emotion/react'] !== false && + !(EMOTION_JSX_RUNTIME in merged) + ) { + merged[EMOTION_JSX_RUNTIME] = true + } return Object.fromEntries( Object.entries(merged) .filter((entry): entry is [string, string | true] => entry[1] !== false) @@ -146,6 +162,7 @@ export function mergeImportAliases( */ const COMPAT_TYPE_ENTRIES: Record = { '@emotion/react': 'emotion', + [EMOTION_JSX_RUNTIME]: 'emotion-jsx-runtime', '@emotion/styled': 'emotion', '@vanilla-extract/css': 'vanilla-extract', 'styled-components': 'styled-components', diff --git a/packages/react/package.json b/packages/react/package.json index 4c6a47414..a1f587b92 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -52,6 +52,9 @@ "./compat/css-prop": { "types": "./dist/compat/css-prop.d.ts" }, + "./compat/emotion-jsx-runtime": { + "types": "./dist/compat/emotion-jsx-runtime.d.ts" + }, "./compat/stylex": { "types": "./dist/compat/stylex.d.ts" }, diff --git a/packages/react/src/compat/emotion-jsx-runtime.d.ts b/packages/react/src/compat/emotion-jsx-runtime.d.ts new file mode 100644 index 000000000..a06ee7c34 --- /dev/null +++ b/packages/react/src/compat/emotion-jsx-runtime.d.ts @@ -0,0 +1,7 @@ +declare module '@emotion/react/jsx-runtime' { + export * from 'react/jsx-runtime' +} + +declare module '@emotion/react/jsx-dev-runtime' { + export * from 'react/jsx-dev-runtime' +} diff --git a/packages/rsbuild-plugin/src/plugin.ts b/packages/rsbuild-plugin/src/plugin.ts index 990a4cd2a..1a796c057 100644 --- a/packages/rsbuild-plugin/src/plugin.ts +++ b/packages/rsbuild-plugin/src/plugin.ts @@ -15,6 +15,7 @@ import { loadDevupConfig, mergeImportAliases, planAtomHoist, + readJsxImportSource, } from '@devup-ui/plugin-utils' import { codeExtract, @@ -118,7 +119,10 @@ export const DevupUI = ({ importAliases: userImportAliases, }: Partial = {}): RsbuildPlugin => { registerShorthands(shorthands ?? {}) - const importAliases = mergeImportAliases(userImportAliases) + const importAliases = mergeImportAliases( + userImportAliases, + readJsxImportSource(), + ) return { name: 'devup-ui-rsbuild-plugin', diff --git a/packages/vite-plugin/src/plugin.ts b/packages/vite-plugin/src/plugin.ts index 9d0b77613..2ea19c45e 100644 --- a/packages/vite-plugin/src/plugin.ts +++ b/packages/vite-plugin/src/plugin.ts @@ -16,6 +16,7 @@ import { loadDevupConfig, mergeImportAliases, planAtomHoist, + readJsxImportSource, } from '@devup-ui/plugin-utils' import { codeExtract, @@ -274,7 +275,10 @@ export function DevupUI({ if (prefix) { setPrefix(prefix) } - const importAliases = mergeImportAliases(userImportAliases) + const importAliases = mergeImportAliases( + userImportAliases, + readJsxImportSource(), + ) const cssMap = new Map() let serverBundleToForward: Record | undefined let isServe = false diff --git a/packages/webpack-plugin/src/plugin.ts b/packages/webpack-plugin/src/plugin.ts index 54d0713a9..7b0cb1f45 100644 --- a/packages/webpack-plugin/src/plugin.ts +++ b/packages/webpack-plugin/src/plugin.ts @@ -16,6 +16,7 @@ import { loadDevupConfigSync, mergeImportAliases, planAtomHoist, + readJsxImportSource, type WasmImportAliases, } from '@devup-ui/plugin-utils' import { @@ -95,7 +96,10 @@ export class DevupUIWebpackPlugin { importAliases: userImportAliases, }: Partial = {}) { registerShorthands(shorthands ?? {}) - this.importAliases = mergeImportAliases(userImportAliases) + this.importAliases = mergeImportAliases( + userImportAliases, + readJsxImportSource(), + ) this.options = { package: libPackage, From 1a6a31eef64af031609420227f6db06dc5101c53 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 3 Oct 2026 05:25:30 +0900 Subject: [PATCH 2/8] fix(extractor): compile Emotion's ClassNames at build time Refs #690 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../changepack_log_emotion_class_names.json | 8 + libs/extractor/src/css_prop.rs | 173 +++++++- libs/extractor/src/import_alias_visit.rs | 42 +- libs/extractor/src/imported_constants.rs | 48 ++- libs/extractor/src/lib.rs | 71 ++++ libs/extractor/src/mutations.rs | 20 +- ...ts__emotion_class_names_in_every_form.snap | 13 + ..._names_reports_what_it_cannot_compile.snap | 32 ++ ...n_global_component_and_import_surface.snap | 2 +- libs/extractor/src/utils.rs | 10 + libs/extractor/src/visit.rs | 386 +++++++++++++++++- .../react/src/compat/__tests__/index.test.ts | 9 + packages/react/src/compat/emotion.d.ts | 2 + packages/react/src/compat/index.ts | 29 ++ 14 files changed, 811 insertions(+), 34 deletions(-) create mode 100644 .changepacks/changepack_log_emotion_class_names.json create mode 100644 libs/extractor/src/snapshots/extractor__tests__emotion_class_names_in_every_form.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap diff --git a/.changepacks/changepack_log_emotion_class_names.json b/.changepacks/changepack_log_emotion_class_names.json new file mode 100644 index 000000000..9317e2ecf --- /dev/null +++ b/.changepacks/changepack_log_emotion_class_names.json @@ -0,0 +1,8 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch", + "packages/react/package.json": "Patch" + }, + "note": "Emotion's now compiles at build time with no runtime: the element becomes what its child function ({ css, cx, theme }) => ... renders, each css and cx call becomes the classes it composes (later styles replace earlier ones; css reads strings as CSS text and its tagged templates split around mixins; cx keeps unknown classes, reads { name: condition } objects as conditional classes and composes nested css and cx calls), and theme.a.b in their values becomes var(--a-b). Numbers in css rules are px lengths, as Emotion reads them, and top-level constants are inlined. New build errors, each at file:line:col: a without exactly one child function taking { css, cx, theme } that gives what it renders at once (attributes, other children, rest or default parameters, async or generator functions, several statements); reading the theme other than as theme.a.b in a css value; using css or cx other than by calling them; a cx object with a spread, getter or method; parts css and cx cannot compose (runtime calls, spreads, a condition choosing between styles and classes). @devup-ui/react/compat exports ClassNames with the ClassNamesContent and ClassNamesArg types, and the @emotion/react declarations include them", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/libs/extractor/src/css_prop.rs b/libs/extractor/src/css_prop.rs index 408fdfeba..96cb8c59e 100644 --- a/libs/extractor/src/css_prop.rs +++ b/libs/extractor/src/css_prop.rs @@ -7,9 +7,9 @@ use std::collections::HashMap; use oxc_allocator::{CloneIn, FromIn, GetAllocator}; use oxc_ast::ast::{ Argument, ArrowFunctionBody, BinaryOperator, BindingPattern, CallExpression, Expression, - FormalParameters, FunctionBody, ImportDeclarationSpecifier, JSXAttributeItem, JSXElementName, - ObjectPropertyKind, Program, Statement, Str, TemplateElement, TemplateElementValue, - TemplateLiteral, + FormalParameters, FunctionBody, ImportDeclarationSpecifier, JSXAttributeItem, JSXChild, + JSXElement, JSXElementName, ObjectPropertyKind, Program, Statement, Str, TemplateElement, + TemplateElementValue, TemplateLiteral, }; use oxc_ast::builder::AstBuilder; use oxc_ast_visit::{VisitMut, walk_mut}; @@ -104,6 +104,8 @@ pub(crate) struct CssTakers<'s> { pub css_prop: CssProp, /// Local names of the functions building elements from a type and props jsx: FxHashSet<&'s str>, + /// Local names of Emotion's `ClassNames` + class_names: FxHashSet<&'s str>, } impl<'s> CssTakers<'s> { @@ -111,18 +113,43 @@ impl<'s> CssTakers<'s> { /// `compat` is the entry absorbing Emotion's own `jsx` pub(crate) fn new(program: &Program<'s>, css_prop: CssProp, compat: &str) -> Self { let mut jsx = FxHashSet::default(); + let mut class_names = FxHashSet::default(); for statement in &program.body { if let Statement::ImportDeclaration(import) = statement { + let source = import.source.value.as_str(); for specifier in import.specifiers.iter().flatten() { - if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier - && is_jsx_function(&import.source.value, &specifier.imported.name(), compat) - { + let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier else { + continue; + }; + let imported = specifier.imported.name(); + if is_jsx_function(source, &imported, compat) { jsx.insert(specifier.local.name.as_str()); + } else if imported == "ClassNames" + && (source == compat || source == EMOTION_REACT) + { + class_names.insert(specifier.local.name.as_str()); } } } } - Self { css_prop, jsx } + Self { + css_prop, + jsx, + class_names, + } + } + + /// The names a `` child function `element` gives takes `css` + /// and `cx` by; empty for any other element + pub(crate) fn class_names_calls(&self, element: &JSXElement<'_>) -> Vec { + match &element.opening_element.name { + JSXElementName::IdentifierReference(name) + if self.class_names.contains(name.name.as_str()) => + { + class_names_calls(element) + } + _ => Vec::new(), + } } /// Whether `attribute` of the element `name` is a `css` prop it takes; @@ -288,6 +315,138 @@ pub(crate) fn returned<'b, 'a>(body: &'b FunctionBody<'a>) -> Option<&'b Express } } +fn returned_mut<'b, 'a>(body: &'b mut FunctionBody<'a>) -> Option<&'b mut Expression<'a>> { + match body.statements.as_mut_slice() { + [Statement::ReturnStatement(statement)] => statement.argument.as_mut(), + _ => None, + } +} + +/// The parameters of `function` and what it gives at once, when it is a +/// plain function giving one value +pub(crate) fn render_function<'b, 'a>( + function: &'b mut Expression<'a>, +) -> Option<(&'b FormalParameters<'a>, &'b mut Expression<'a>)> { + match function { + Expression::ArrowFunctionExpression(arrow) if !arrow.r#async => { + let arrow = &mut **arrow; + let body = match &mut arrow.body { + ArrowFunctionBody::FunctionBody(body) => returned_mut(body)?, + body => body.as_expression_mut()?, + }; + Some((&arrow.params, body)) + } + Expression::FunctionExpression(function) if !function.r#async && !function.generator => { + let function = &mut **function; + Some(( + &function.params, + returned_mut(function.body.as_deref_mut()?)?, + )) + } + _ => None, + } +} + +/// The names Emotion's `` child function takes `css`, `cx` and +/// the theme by +#[derive(Default, Clone, Copy)] +pub(crate) struct ClassNamesParams<'a> { + pub css: Option<&'a str>, + pub cx: Option<&'a str>, + pub theme: Option<&'a str>, +} + +/// What `params` take of `{ css, cx, theme }`; `None` when they take it in a +/// way the build cannot follow +pub(crate) fn class_names_params<'a>( + params: &FormalParameters<'a>, +) -> Option> { + let mut names = ClassNamesParams::default(); + if params.rest.is_some() { + return None; + } + match params.items.as_slice() { + [] => {} + [param] if param.initializer.is_none() => { + let BindingPattern::ObjectPattern(object) = ¶m.pattern else { + return None; + }; + if object.rest.is_some() { + return None; + } + for property in &object.properties { + let (BindingPattern::BindingIdentifier(local), Some(key), false) = ( + &property.value, + property.key.static_name(), + property.computed, + ) else { + return None; + }; + let slot = match key.as_ref() { + "css" => &mut names.css, + "cx" => &mut names.cx, + "theme" => &mut names.theme, + _ => return None, + }; + *slot = Some(local.name.as_str()); + } + } + _ => return None, + } + Some(names) +} + +/// What the child function of the `` element `element` takes +pub(crate) fn class_names_child<'a>(element: &JSXElement<'a>) -> Option> { + let mut children = element + .children + .iter() + .filter(|child| !matches!(child, JSXChild::Text(text) if text.value.trim().is_empty())); + let (Some(JSXChild::ExpressionContainer(container)), None) = (children.next(), children.next()) + else { + return None; + }; + let params = match container.expression.as_expression()? { + Expression::ArrowFunctionExpression(arrow) => &arrow.params, + Expression::FunctionExpression(function) => &function.params, + _ => return None, + }; + class_names_params(params) +} + +/// The names the child function of the `` element `element` +/// takes `css` and `cx` by, which read styles +pub(crate) fn class_names_calls(element: &JSXElement<'_>) -> Vec { + class_names_child(element) + .map(|names| { + [names.css, names.cx] + .into_iter() + .flatten() + .map(str::to_string) + .collect() + }) + .unwrap_or_default() +} + +/// Write the reads of the theme bound to `theme` in `expression`, a CSS value +/// when `value`, as the CSS variables the `ThemeProvider` sets; `Err` holds +/// the first read the build cannot write so +pub(crate) fn read_theme<'a>( + ast: &AstBuilder<'a>, + expression: &mut Expression<'a>, + theme: &'a str, + value: bool, +) -> Result<(), Expression<'a>> { + let roots = [(theme, None)]; + let mut reads = ThemeReads { + ast, + roots: &roots, + unread: None, + }; + reads.visit(expression, value); + reads.unread.map_or(Ok(()), Err) +} + /// The names a function of the theme reads it by, each with the key of the /// theme it holds when destructured; `None` for parameters the build cannot /// follow diff --git a/libs/extractor/src/import_alias_visit.rs b/libs/extractor/src/import_alias_visit.rs index f5cf40351..db6e569c8 100644 --- a/libs/extractor/src/import_alias_visit.rs +++ b/libs/extractor/src/import_alias_visit.rs @@ -9,8 +9,8 @@ use crate::ImportAlias; use crate::css_prop::{ - CssProp, EMOTION_REACT, REACT_JSX_PRAGMA, builds_jsx_with_emotion, emotion_pragma, is_emotion, - is_jsx_file, is_jsx_function, react_runtime, returned, + CssProp, EMOTION_REACT, REACT_JSX_PRAGMA, builds_jsx_with_emotion, class_names_child, + emotion_pragma, is_emotion, is_jsx_file, is_jsx_function, react_runtime, returned, }; use crate::utils::{ get_str_by_property_key, is_vanilla_extract_file, js_number_literal, keeps_bare_number, @@ -18,12 +18,12 @@ use crate::utils::{ use oxc_allocator::Allocator; use oxc_ast::ast::{ Argument, ArrowFunctionBody, CallExpression, Expression, ImportDeclarationSpecifier, - JSXAttributeItem, JSXAttributeValue, JSXElementName, JSXOpeningElement, LogicalOperator, - ModuleExportName, ObjectPropertyKind, Statement, + JSXAttributeItem, JSXAttributeValue, JSXElement, JSXElementName, JSXOpeningElement, + LogicalOperator, ModuleExportName, ObjectPropertyKind, Statement, }; use oxc_ast_visit::{ Visit, - walk::{walk_call_expression, walk_jsx_opening_element}, + walk::{walk_call_expression, walk_jsx_element, walk_jsx_opening_element}, }; use oxc_parser::Parser; use oxc_span::{GetSpan, SourceType}; @@ -54,6 +54,9 @@ struct LibraryNumbers<'n> { devup: Vec<&'n str>, /// Local names of the functions building elements from a type and props jsx: Vec<&'n str>, + /// Local names of Emotion's `ClassNames`, whose child function takes a + /// `css` taking rules + class_names: Vec<&'n str>, replacements: Vec<(usize, usize, String)>, } @@ -196,7 +199,25 @@ impl LibraryNumbers<'_> { } } -impl<'a> Visit<'a> for LibraryNumbers<'_> { +impl<'a> Visit<'a> for LibraryNumbers<'a> { + fn visit_jsx_element(&mut self, element: &JSXElement<'a>) { + let css = match &element.opening_element.name { + JSXElementName::IdentifierReference(name) + if self.class_names.contains(&name.name.as_str()) => + { + class_names_child(element).and_then(|names| names.css) + } + _ => None, + }; + if let Some(css) = css { + self.calls.push((css, RulesAt::EveryArgument)); + } + walk_jsx_element(self, element); + if css.is_some() { + self.calls.pop(); + } + } + fn visit_call_expression(&mut self, call: &CallExpression<'a>) { let rules_at = match &call.callee { Expression::Identifier(callee) => self @@ -290,6 +311,7 @@ fn devup_equivalent(source: &str, imported: &str) -> Option (_, "styled") => Some(DevupTarget::Main("styled")), // The `css` props it builds compile away, leaving React's own element ("@emotion/react", "jsx" | "createElement") => Some(DevupTarget::Compat("jsx")), + ("@emotion/react", "ClassNames") => Some(DevupTarget::Compat("ClassNames")), (_, "createGlobalStyle") => Some(DevupTarget::Compat("createGlobalStyle")), (_, "Global") => Some(DevupTarget::Compat("Global")), (_, "ThemeProvider") => Some(DevupTarget::Compat("ThemeProvider")), @@ -446,6 +468,9 @@ pub fn transform_import_aliases_with_edits<'a>( "css" | "keyframes", ) => numbers.calls.push((local, RulesAt::EveryArgument)), ("@emotion/react", "Global") => numbers.components.push(local), + ("@emotion/react", "ClassNames") => { + numbers.class_names.push(local); + } _ => {} } } @@ -473,7 +498,10 @@ pub fn transform_import_aliases_with_edits<'a>( } } numbers.css_prop = CssProp::of(import_aliases, code, uses_emotion); - if !(numbers.calls.is_empty() && numbers.styled.is_empty() && numbers.components.is_empty()) + if !(numbers.calls.is_empty() + && numbers.styled.is_empty() + && numbers.components.is_empty() + && numbers.class_names.is_empty()) || numbers.css_prop != CssProp::Off { numbers.visit_program(&program); diff --git a/libs/extractor/src/imported_constants.rs b/libs/extractor/src/imported_constants.rs index 2094e2fea..03cb5c9b5 100644 --- a/libs/extractor/src/imported_constants.rs +++ b/libs/extractor/src/imported_constants.rs @@ -357,6 +357,7 @@ pub(crate) fn inline_constants<'a>( css_props: &css_props, names: FxHashSet::default(), depth: 0, + class_names: Vec::new(), }; read.visit_program(program); if read.names.is_empty() { @@ -509,6 +510,7 @@ pub(crate) fn inline_constants<'a>( objects: false, styles: false, px: false, + class_names: Vec::new(), } .visit_program(program); } @@ -643,6 +645,9 @@ struct StyleReads<'s> { css_props: &'s CssTakers<'s>, names: FxHashSet, depth: usize, + /// The names the `` child functions around take `css` and + /// `cx` by + class_names: Vec, } /// Whether `expression` is a style API: a root the package gives, or a member @@ -676,9 +681,18 @@ impl<'a> Visit<'a> for StyleReads<'_> { } } + fn visit_jsx_element(&mut self, element: &oxc_ast::ast::JSXElement<'a>) { + let calls = self.css_props.class_names_calls(element); + let taken = calls.len(); + self.class_names.extend(calls); + oxc_ast_visit::walk::walk_jsx_element(self, element); + self.class_names.truncate(self.class_names.len() - taken); + } + fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) { self.visit_expression(&call.callee); - let style = self.is_style_root(&call.callee); + let style = + self.is_style_root(&call.callee) || calls_one_of(&self.class_names, &call.callee); let css = self .css_props .property(call, |root| self.style_roots.contains(root)); @@ -703,7 +717,7 @@ impl<'a> Visit<'a> for StyleReads<'_> { tagged: &oxc_ast::ast::TaggedTemplateExpression<'a>, ) { self.visit_expression(&tagged.tag); - let style = self.is_style_root(&tagged.tag); + let style = self.is_style_root(&tagged.tag) || calls_one_of(&self.class_names, &tagged.tag); self.reading(style, |reads| reads.visit_template_literal(&tagged.quasi)); } @@ -728,6 +742,11 @@ impl<'a> Visit<'a> for StyleReads<'_> { } } +/// Whether `callee` is one of `names` +fn calls_one_of(names: &[String], callee: &Expression<'_>) -> bool { + matches!(callee, Expression::Identifier(callee) if names.iter().any(|name| name == callee.name.as_str())) +} + /// The name `` or `` starts with pub(crate) fn jsx_root<'n>(name: &'n JSXElementName<'_>) -> Option<&'n str> { match name { @@ -1793,6 +1812,9 @@ struct Inline<'s, 'a> { styles: bool, /// Inside a `css` prop, whose numbers Emotion reads as `px` lengths px: bool, + /// The names the `` child functions around take `css` and + /// `cx` by + class_names: Vec, } impl<'a> Inline<'_, 'a> { @@ -2070,6 +2092,12 @@ impl<'a> VisitMut<'a> for Inline<'_, 'a> { tagged: &mut oxc_ast::ast::TaggedTemplateExpression<'a>, ) { self.visit_expression(&mut tagged.tag); + if calls_one_of(&self.class_names, &tagged.tag) { + self.reading_css(true, |inline| { + inline.visit_template_literal(&mut tagged.quasi); + }); + return; + } let styles = is_style_root(self.style_roots, &tagged.tag); self.reading_styles(styles, |inline| { inline.visit_template_literal(&mut tagged.quasi); @@ -2082,8 +2110,24 @@ impl<'a> VisitMut<'a> for Inline<'_, 'a> { }); } + fn visit_jsx_element(&mut self, element: &mut oxc_ast::ast::JSXElement<'a>) { + let calls = self.css_props.class_names_calls(element); + let taken = calls.len(); + self.class_names.extend(calls); + walk_mut::walk_jsx_element(self, element); + self.class_names.truncate(self.class_names.len() - taken); + } + fn visit_call_expression(&mut self, call: &mut oxc_ast::ast::CallExpression<'a>) { self.visit_expression(&mut call.callee); + if calls_one_of(&self.class_names, &call.callee) { + self.reading_css(true, |inline| { + for argument in &mut call.arguments { + inline.visit_argument(argument); + } + }); + return; + } let objects = self.apis.reads(&call.callee); let styles = is_style_root(self.style_roots, &call.callee); let css = self diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 9415e1c89..e1a91c5e5 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -21453,4 +21453,75 @@ export const App = () => {({ css, cx, theme }) =>
};", + "export const App = ({ on }) => {({ css, cx }) =>
};", + "export const App = ({ on }) => {function ({ css: c }) { return ; }};", + "const mixin = { margin: 4 };\nexport const App = () =>
{({ css }) =>

}

;", + "export const App = () => {() =>
}
;", + "export const App = (p) => {({ cx }) => p.on()} title={String.raw`t`} id={String(1)} className={cx(p.className, cx('a'), p.on ? { x: p.on } : 'y', p.a || { z: p.b }, p.c ? cx('k') : null, p.d && cx())} />};", + "const tone = 'red';\nconst gap = 3;\nexport const App = () => {({ css }) => };", + ]; + let outputs: Vec = codes + .iter() + .map(|code| { + reset_class_map(); + reset_file_map(); + let output = + extract("test.tsx", &format!("{import}{code}"), emotion_option()).unwrap(); + format!("{}\n{:?}", output.code, output.styles) + }) + .collect(); + assert_debug_snapshot!(outputs); + } + + #[test] + #[serial] + fn test_emotion_class_names_reports_what_it_cannot_compile() { + let import = "import { ClassNames } from '@emotion/react';\n"; + let errors: Vec = [ + "export const App = () => {render};", + "export const App = () => {() =>
}{() => };", + "export const App = () => ;", + "export const App = () => {};", + "export const App = () => text;", + "export const App = () => {() => };", + "export const App = () => {(content) => };", + "export const App = () => {({ css, ...rest }) => };", + "export const App = () => {({ css = f }) => };", + "export const App = () => {({ other }) => };", + "export const App = () => {({ [k]: css }) => };", + "export const App = () => {({ css }, more) => };", + "export const App = () => {async ({ css }) => };", + "export const App = () => {function* ({ css }) { yield ; }};", + "export const App = () => {({ css }) => { const a = 1; return ; }};", + "export const App = () => {({ theme }) => };", + "export const App = () => {({ css, theme }) => };", + "export const App = () => {({ css }) => };", + "export const App = () => {({ cx }) => };", + "export const App = ({ s }) => {({ cx }) => };", + "export const App = () => {({ cx }) => };", + "export const App = ({ s }) => {({ css }) => };", + "export const App = ({ v }) => {({ css }) => };", + "export const App = ({ v }) => {({ css }) => };", + "export const App = ({ on }) => {({ css, cx }) => };", + "export const App = ({ on, off }) => {({ css, cx }) => };", + ] + .iter() + .map(|code| { + reset_class_map(); + reset_file_map(); + extract("test.tsx", &format!("{import}{code}"), emotion_option()) + .err() + .map(|error| error.to_string()) + .unwrap_or_default() + }) + .collect(); + assert_debug_snapshot!(errors); + } } diff --git a/libs/extractor/src/mutations.rs b/libs/extractor/src/mutations.rs index f1ea6c2ef..e140a52ff 100644 --- a/libs/extractor/src/mutations.rs +++ b/libs/extractor/src/mutations.rs @@ -390,8 +390,12 @@ impl Context<'_, '_> { std::iter::once(node) .chain(self.nodes.ancestor_ids(node)) .any(|id| match self.nodes.kind(id) { - AstKind::CallExpression(call) => self.is_style(&call.callee), - AstKind::TaggedTemplateExpression(tagged) => self.is_style(&tagged.tag), + AstKind::CallExpression(call) => { + self.is_style(&call.callee) || self.is_class_names_call(id, &call.callee) + } + AstKind::TaggedTemplateExpression(tagged) => { + self.is_style(&tagged.tag) || self.is_class_names_call(id, &tagged.tag) + } AstKind::JSXOpeningElement(element) => self.is_style_element(&element.name), AstKind::JSXAttribute(attribute) => self.is_css_attribute(id, attribute), AstKind::ObjectProperty(property) => self.is_css_property(id, property), @@ -399,6 +403,18 @@ impl Context<'_, '_> { }) } + /// Whether `callee`, called at `id`, is the `css` or `cx` a `` + /// child function around takes, whose calls the build compiles + fn is_class_names_call(&self, id: NodeId, callee: &Expression<'_>) -> bool { + let (Some(css), Expression::Identifier(callee)) = (self.css, callee) else { + return false; + }; + self.nodes.ancestor_ids(id).any(|ancestor| { + matches!(self.nodes.kind(ancestor), AstKind::JSXElement(element) + if css.class_names_calls(element).iter().any(|name| name == callee.name.as_str())) + }) + } + /// Whether the attribute `attribute` at `id` is a `css` prop the build /// compiles fn is_css_attribute(&self, id: NodeId, attribute: &oxc_ast::ast::JSXAttribute<'_>) -> bool { diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_in_every_form.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_in_every_form.snap new file mode 100644 index 000000000..fcb3f6c39 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_in_every_form.snap @@ -0,0 +1,13 @@ +--- +source: libs/extractor/src/lib.rs +expression: outputs +--- +[ + "import \"@devup-ui/react/devup-ui.css\";\nexport const App = () =>
;\n\n{Static(ExtractStaticStyle { property: \"padding\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"var(--color)\", level: 0, selector: None, style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nexport const App = ({ on }) =>
;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nexport const App = ({ on }) => ;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: None, style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nconst mixin = { margin: 4 };\nexport const App = () =>
{

}

;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"4px\", level: 0, selector: None, style_order: None, layer: None })}", + "export const App = () =>
;\n\n{}", + "export const App = (p) => p.on()} title={String.raw`t`} id={String(1)} className={`a ${p.className} ${p.on ? p.on ? \"x\" : \"\" : \"y\"} ${p.a ? typeof p.a === \"string\" ? p.a : \"\" : p.b ? \"z\" : \"\"} ${p.c ? \"k\" : \"\"}`} />;\n\n{}", + "import \"@devup-ui/react/devup-ui.css\";\nconst tone = \"red\";\nconst gap = 3;\nexport const App = () => ;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"3px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"2px\", level: 0, selector: None, style_order: None, layer: None })}", +] diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap new file mode 100644 index 000000000..785775e39 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap @@ -0,0 +1,32 @@ +--- +source: libs/extractor/src/lib.rs +expression: errors +--- +[ + "test.tsx:2:39: `` cannot use `render` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:26: `` cannot use `children` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:26: `` cannot use `children` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:38: `` cannot use `{}` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:26: `` cannot use `children` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:38: `` cannot use `an attribute` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `(content) =>
` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `({ css, ...rest }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `({ css = f }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `({ other }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `({ [k]: css }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `({ css }, more) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `async ({ css }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `(function* ({ css }) { yield ; })` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `({ css }) => { const a = 1; return ; }` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:64: `` cannot use `theme` at build time: it may read the theme only as `theme.a.b` in a value, which becomes the CSS variable `var(--a-b)` the `ThemeProvider` sets", + "test.tsx:2:73: `css()` cannot use `theme.space(2)` at build time: its values must be literals, theme tokens or constants, or be computed from them\ntest.tsx:2:87: `` cannot use `theme.space(2)` at build time: it may read the theme only as `theme.a.b` in a value, which becomes the CSS variable `var(--a-b)` the `ThemeProvider` sets", + "test.tsx:2:60: `` cannot use `css` at build time: the `css` and `cx` its child function takes can only be called, as the build compiles each call", + "test.tsx:2:65: `` cannot use `cx(getClass())` at build time: `css` and `cx` compose only style objects, CSS text, classes, calls of them, or arrays or conditions of these", + "test.tsx:2:78: `` cannot use `s` at build time: an object `cx` takes must give each class a condition, as `{ name: condition }`", + "test.tsx:2:75: `` cannot use `(function() { return true; })` at build time: an object `cx` takes must give each class a condition, as `{ name: condition }`", + "test.tsx:2:71: `` cannot use `css(...s)` at build time: `css` and `cx` compose only style objects, CSS text, classes, calls of them, or arrays or conditions of these", + "test.tsx:2:87: `` cannot use `v` at build time: a mixin must stand outside nested rules, where the parts it composes with can be split", + "test.tsx:2:71: `css()` cannot use `v` at build time: its values must be literals, theme tokens or constants, or be computed from them", + "test.tsx:2:76: `` cannot use `cx(on && cx(css({ color: \"red\" }), \"x\"))` at build time: `css` and `cx` compose only style objects, CSS text, classes, calls of them, or arrays or conditions of these", + "test.tsx:2:81: `` cannot use `cx(on && cx(off ? css({ color: \"red\" }) : null))` at build time: `css` and `cx` compose only style objects, CSS text, classes, calls of them, or arrays or conditions of these", +] diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap index 919298f4f..c64fe2255 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap @@ -32,5 +32,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Global, ThemeProvider, useTheme } from \"@devup-ui/react/compat\";\nimport { ClassNames } from \"@emotion/react\";\nconst S = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => <>;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Global, ThemeProvider, useTheme } from \"@devup-ui/react/compat\";\nconst S = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => <>;\n", } diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs index 10fff27b5..e5fb825c0 100644 --- a/libs/extractor/src/utils.rs +++ b/libs/extractor/src/utils.rs @@ -965,6 +965,16 @@ pub(super) const CSS_PROP_VALUE: &str = "it must be a style object, CSS text, a pub(super) const LOCAL_STYLES: &str = "a style object it composes must be written in it, or declared with `const` at the top level of the module, where the build reads it"; +pub(super) const CLASS_NAMES_CHILD: &str = + "it takes only a child function of `{ css, cx, theme }` giving what it renders at once"; + +pub(super) const CLASS_NAMES_CALL: &str = "the `css` and `cx` its child function takes can only be called, as the build compiles each call"; + +pub(super) const CLASS_NAMES_PART: &str = "`css` and `cx` compose only style objects, CSS text, classes, calls of them, or arrays or conditions of these"; + +pub(super) const CLASS_NAMES_CLASS_MAP: &str = + "an object `cx` takes must give each class a condition, as `{ name: condition }`"; + /// The `css` prop of the styled component `element` sets what its own styles /// set, which the build cannot order there pub(super) fn css_prop_override_error(element: &str) -> String { diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index a1f3eec31..a74a216aa 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -1,7 +1,10 @@ use crate::as_visit::As; use crate::component::ExportVariableKind; use crate::composition::{KnownPart, KnownSide, KnownStyles, overlaps, set_prop_order}; -use crate::css_prop::{CssProp, template_parts, theme_rules}; +use crate::css_prop::{ + ClassNamesParams, CssProp, THEME_READ, class_names_params, read_theme, render_function, + template_parts, theme_rules, +}; use crate::css_utils::{ TemplateStyles, css_to_style_template, keyframes_to_keyframes_style, optimize_css_block, template_css_text, @@ -51,10 +54,12 @@ use oxc_ast::ast::{ ComputedMemberExpression, Expression, ExpressionStatement, FormalParameter, FormalParameterKind, FormalParameters, IdentifierName, ImportDeclaration, ImportOrExportKind, JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement, JSXElementName, - JSXExpressionContainer, ObjectProperty, ObjectPropertyKind, Program, PropertyKey, PropertyKind, - Statement, StaticMemberExpression, Str, StringLiteral, UnaryOperator, VariableDeclarator, + JSXExpressionContainer, ObjectExpression, ObjectProperty, ObjectPropertyKind, Program, + PropertyKey, PropertyKind, Statement, StaticMemberExpression, Str, StringLiteral, + UnaryOperator, VariableDeclarator, }; use oxc_ast_visit::VisitMut; +use oxc_ast_visit::walk_mut; use oxc_ast_visit::walk_mut::{ walk_call_expression, walk_expression, walk_expression_statement, walk_import_declaration, walk_jsx_attribute_value, walk_jsx_child, walk_jsx_element, walk_program, @@ -65,9 +70,10 @@ use oxc_syntax::operator::LogicalOperator; use strum::IntoEnumIterator; use crate::utils::{ - CSS_PROP_VALUE, LOCAL_STYLES, ParsedStyleOrder, RUNTIME_VALUE, STYLE_OBJECT, StyleArguments, - Suspends, build_time_error, call_with_values, css_prop_error, css_prop_override_error, - element_error, expression_to_style_order, fixed_value, get_str_by_property_key, + CLASS_NAMES_CALL, CLASS_NAMES_CHILD, CLASS_NAMES_CLASS_MAP, CLASS_NAMES_PART, CSS_PROP_VALUE, + LOCAL_STYLES, ParsedStyleOrder, RUNTIME_VALUE, STYLE_OBJECT, StyleArguments, Suspends, + build_time_error, call_with_values, css_prop_error, css_prop_override_error, element_error, + expression_to_style_order, fixed_value, get_str_by_property_key, get_string_by_literal_expression, get_string_by_property_key, is_pure, jsx_expression_to_style_order, key_error, readable_argument, readable_code, reads_directly, reads_spreads_once, reads_unknown, runtime_classes, runtime_value, runtime_value_error, @@ -199,6 +205,11 @@ pub struct DevupVisitor<'a> { /// Local names bound to the `Global` component, whose `styles` prop declares /// global CSS instead of rendering markup. global_style_components: FxHashSet, + /// Local names bound to Emotion's `ClassNames`, whose element becomes + /// what its child function renders + class_names_components: FxHashSet, + /// What the `` child functions being compiled take + class_names_scope: Vec>, /// `defineVars` members flattened to `"vars.key"` -> `"var(--x)"`, so a /// `stylex.create()` value referencing one resolves to a static CSS value. @@ -394,13 +405,26 @@ impl<'a> DevupVisitor<'a> { } self.unknown_arguments("css", &call.arguments); self.changed_arguments("css", &call.arguments); + let (result, known) = self.composed_class(call.span.start, parts); + if let Some(known) = known { + self.css_styles = Some((call.span.start, known)); + } + Some(result) + } + /// The classes `parts`, composed at `offset`, give, with the styles they + /// always set when no class the build does not know joins them + fn composed_class( + &mut self, + offset: u32, + parts: Vec>, + ) -> (Expression<'a>, Option>) { let mut composition = crate::composition::Composition::default(); let mut classes = Vec::new(); for part in parts { match part { KnownPart::Styles(side) => { - let props = self.part_props(call.span.start, side, None); + let props = self.part_props(offset, side, None); composition.apply(&self.ast, props); } KnownPart::Conditional { @@ -408,8 +432,8 @@ impl<'a> DevupVisitor<'a> { consequent, alternate, } => { - let consequent = self.part_props(call.span.start, consequent, None); - let alternate = self.part_props(call.span.start, alternate, None); + let consequent = self.part_props(offset, consequent, None); + let alternate = self.part_props(offset, alternate, None); composition.apply_conditional(&self.ast, &test, consequent, alternate); } KnownPart::Class(mut class) => { @@ -418,7 +442,7 @@ impl<'a> DevupVisitor<'a> { } } } - let known = composition.unconditional(); + let known = composition.unconditional().filter(|_| classes.is_empty()); let mut props = composition.into_props(); // Class names come out in reverse, so they read in composing order props.reverse(); @@ -426,14 +450,10 @@ impl<'a> DevupVisitor<'a> { gen_class_names(&self.ast, &mut props, None, self.split_filename.as_deref()); self.styles .extend(props.into_iter().flat_map(ExtractStyleProp::into_extract)); - let composed_class = classes.is_empty(); let result = merge_expression_for_class_name(&self.ast, classes.into_iter().chain(class_name)) .unwrap_or_else(|| Expression::new_string_literal(SPAN, "", None, &self.ast)); - if composed_class && let Some(known) = known { - self.css_styles = Some((call.span.start, known)); - } - Some(result) + (result, known) } /// Whether `expression`, or a part of it, is a class whose styles the @@ -512,6 +532,13 @@ impl<'a> DevupVisitor<'a> { &conditional.consequent, Some(&conditional.alternate), ), + Expression::CallExpression(call) if self.class_names_text(&call.callee).is_some() => { + let text = self.class_names_text(&call.callee)?; + for argument in &call.arguments { + self.known_parts(argument.as_expression()?, parts, text)?; + } + return Some(()); + } expression => { match self.known_side(expression, text)? { KnownSide::Styles(side) => parts.push(KnownPart::Styles(side)), @@ -600,10 +627,40 @@ impl<'a> DevupVisitor<'a> { | Expression::ComputedMemberExpression(_) | Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => Some(KnownSide::Class(code())), + Expression::CallExpression(call) if self.class_names_text(&call.callee).is_some() => { + self.folded_side(expression, text) + } + Expression::ArrayExpression(_) if !self.class_names_scope.is_empty() => { + self.folded_side(expression, text) + } _ => None, } } + /// The parts `expression` composes as one side of a condition: only + /// styles, or only classes, which join + fn folded_side(&self, expression: &Expression<'a>, text: Text) -> Option> { + let mut parts = Vec::new(); + self.known_parts(expression, &mut parts, text)?; + let mut styles = Vec::new(); + let mut classes = Vec::new(); + for part in parts { + match part { + KnownPart::Styles(side) => styles.extend(side), + KnownPart::Class(class) => classes.push(class), + KnownPart::Conditional { .. } => return None, + } + } + match (styles.is_empty(), classes.is_empty()) { + (true, true) => Some(KnownSide::Empty), + (false, true) => Some(KnownSide::Styles(styles)), + (true, false) => { + merge_expression_for_class_name(&self.ast, classes).map(KnownSide::Class) + } + (false, false) => None, + } + } + /// The styles of a part, each at the order the part gives it; `element` /// names the element taking them as its `css` prop, which sets a value /// only the runtime gives as a CSS variable @@ -680,6 +737,8 @@ impl<'a> DevupVisitor<'a> { stylex_imports: FxHashSet::default(), stylex_named_imports: FxHashMap::default(), global_style_components: FxHashSet::default(), + class_names_components: FxHashSet::default(), + class_names_scope: Vec::new(), stylex_var_refs: FxHashMap::default(), stylex_var_names: FxHashMap::default(), stylex_theme_classes: FxHashMap::default(), @@ -1372,6 +1431,290 @@ impl<'a> DevupVisitor<'a> { add_class_and_style_to_object(&self.ast, &mut props.properties, class_name, style); } } + + /// What Emotion's `` element renders, its classes compiled; + /// `None` for any other element + fn class_names(&mut self, element: &mut JSXElement<'a>) -> Option> { + let JSXElementName::IdentifierReference(name) = &element.opening_element.name else { + return None; + }; + if !self.class_names_components.contains(name.name.as_str()) { + return None; + } + Some( + self.compile_class_names(element) + .unwrap_or_else(|(at, code)| { + self.errors + .push((at, element_error("ClassNames", &code, CLASS_NAMES_CHILD))); + Expression::new_null_literal(SPAN, &self.ast) + }), + ) + } + + /// `{({ css, cx, theme }) => rendered}` as + /// `rendered`: its `css` and `cx` calls compile as `css()`, and the theme + /// they read becomes the CSS variables the `ThemeProvider` sets. `Err` + /// holds where and what the build cannot compile. + fn compile_class_names( + &mut self, + element: &mut JSXElement<'a>, + ) -> Result, (u32, String)> { + if let Some(attribute) = element.opening_element.attributes.first() { + return Err((attribute.span().start, "an attribute".to_string())); + } + let span = element.span; + let mut children = element + .children + .iter_mut() + .filter(|child| !matches!(child, JSXChild::Text(text) if text.value.trim().is_empty())); + let (Some(JSXChild::ExpressionContainer(container)), None) = + (children.next(), children.next()) + else { + return Err((span.start, "children".to_string())); + }; + let Some(function) = container.expression.as_expression_mut() else { + return Err((container.span.start, "{}".to_string())); + }; + let function_at = function.span().start; + let function_code = readable_code(function); + let Some((names, body)) = render_function(function) + .and_then(|(params, body)| Some((class_names_params(params)?, body))) + else { + return Err((function_at, function_code)); + }; + let mut rendered = body.take_in(&self.ast); + let mut calls = ClassNamesCalls { + ast: &self.ast, + names, + unread: None, + }; + calls.visit_expression(&mut rendered); + if let Some((unread, requirement)) = calls.unread { + self.errors.push(( + unread.span().start, + element_error("ClassNames", &readable_code(&unread), requirement), + )); + } + self.class_names_scope.push(names); + self.visit_expression(&mut rendered); + self.class_names_scope.pop(); + Ok(rendered) + } + + /// What a call of the `css` or `cx` a `` child function + /// takes composes: `css` reads text as CSS, `cx` as classes + fn class_names_text(&self, callee: &Expression<'a>) -> Option { + let Expression::Identifier(callee) = callee else { + return None; + }; + let name = Some(callee.name.as_str()); + self.class_names_scope.iter().rev().find_map(|names| { + if names.css == name { + Some(Text::Rules) + } else if names.cx == name { + Some(Text::Classes) + } else { + None + } + }) + } + + /// Replace `it`, when a call of the `css` or `cx` a `` child + /// function takes, with the classes it composes + fn compile_class_names_call(&mut self, it: &mut Expression<'a>) -> bool { + let Expression::CallExpression(call) = it else { + return false; + }; + if self.class_names_text(&call.callee).is_none() { + return false; + } + let offset = call.span.start; + self.unknown_arguments("css", &call.arguments); + self.changed_arguments("css", &call.arguments); + let mut parts = Vec::new(); + *it = if self.known_parts(it, &mut parts, Text::Classes).is_some() { + self.composed_class(offset, parts).0 + } else { + self.errors.push(( + offset, + element_error("ClassNames", &readable_code(it), CLASS_NAMES_PART), + )); + Expression::new_string_literal(SPAN, "", None, &self.ast) + }; + true + } +} + +/// The calls of the `css` and `cx` a `` child function takes, +/// readied to compose: the theme their rules read becomes the CSS variables +/// the `ThemeProvider` sets, an object `cx` takes the conditions of its +/// classes, and CSS text `css` tags the parts around its mixins. The first +/// code the build cannot ready so is kept. +struct ClassNamesCalls<'r, 'a> { + ast: &'r AstBuilder<'a>, + names: ClassNamesParams<'a>, + unread: Option<(Expression<'a>, &'static str)>, +} + +impl<'a> ClassNamesCalls<'_, 'a> { + fn calls(callee: &Expression<'a>, name: Option<&'a str>) -> bool { + matches!(callee, Expression::Identifier(callee) if name == Some(callee.name.as_str())) + } + + fn read_theme(&mut self, expression: &mut Expression<'a>, value: bool) { + if let Some(theme) = self.names.theme + && let Err(unread) = read_theme(self.ast, expression, theme, value) + { + self.unread.get_or_insert((unread, THEME_READ)); + } + } + + /// `part` of what `cx` composes with each object in it written as the + /// conditions of the classes it names + fn class_maps(&mut self, part: &mut Expression<'a>) { + let classes = match unwrap_syntax_only_mut(part) { + Expression::ArrayExpression(array) => { + for part in &mut array.elements { + if let Some(part) = part.as_expression_mut() { + self.class_maps(part); + } + } + return; + } + Expression::ConditionalExpression(conditional) => { + self.class_maps(&mut conditional.consequent); + self.class_maps(&mut conditional.alternate); + return; + } + Expression::LogicalExpression(logical) => { + if logical.operator != LogicalOperator::And { + self.class_maps(&mut logical.left); + } + self.class_maps(&mut logical.right); + return; + } + Expression::ObjectExpression(object) => self.class_map(object), + _ => return, + }; + *part = classes; + } + + /// `{ name: condition }` as `[condition && 'name']` + fn class_map(&mut self, object: &mut ObjectExpression<'a>) -> Expression<'a> { + let mut classes = oxc_allocator::Vec::new_in(self.ast); + for property in object.properties.drain(..) { + let mut property = match property { + ObjectPropertyKind::ObjectProperty(property) + if property.kind == PropertyKind::Init && !property.method => + { + property + } + ObjectPropertyKind::ObjectProperty(mut property) => { + let value = property.value.take_in(self.ast); + self.unread.get_or_insert((value, CLASS_NAMES_CLASS_MAP)); + continue; + } + ObjectPropertyKind::SpreadProperty(mut spread) => { + let argument = spread.argument.take_in(self.ast); + self.unread.get_or_insert((argument, CLASS_NAMES_CLASS_MAP)); + continue; + } + }; + let class = if property.computed + && let Some(key) = property.key.as_expression_mut() + { + key.take_in(self.ast) + } else { + let name = property.key.static_name().unwrap_or_default(); + Expression::new_string_literal( + SPAN, + Str::from_in(name.as_ref(), self.ast.allocator()), + None, + self.ast, + ) + }; + let condition = property.value.take_in(self.ast); + classes.push( + Expression::new_logical_expression( + SPAN, + condition, + LogicalOperator::And, + class, + self.ast, + ) + .into(), + ); + } + Expression::new_array_expression(SPAN, classes, self.ast) + } +} + +impl<'a> VisitMut<'a> for ClassNamesCalls<'_, 'a> { + fn visit_expression(&mut self, it: &mut Expression<'a>) { + let Expression::TaggedTemplateExpression(tagged) = it else { + walk_mut::walk_expression(self, it); + return; + }; + if !Self::calls(&tagged.tag, self.names.css) { + walk_mut::walk_expression(self, it); + return; + } + for expression in &mut tagged.quasi.expressions { + self.read_theme(expression, true); + self.visit_expression(expression); + } + *it = match template_parts(self.ast, &tagged.quasi, true) { + Ok(parts) => Expression::new_call_expression( + SPAN, + tagged.tag.take_in(self.ast), + None::>>, + oxc_allocator::Vec::from_iter_in(parts.into_iter().map(Argument::from), self.ast), + false, + self.ast, + ), + Err(unplaced) => { + self.unread.get_or_insert(unplaced); + Expression::new_identifier(SPAN, "undefined", self.ast) + } + }; + } + + fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) { + let cx = Self::calls(&call.callee, self.names.cx); + if !cx && !Self::calls(&call.callee, self.names.css) { + walk_call_expression(self, call); + return; + } + for argument in &mut call.arguments { + let part = match argument { + Argument::SpreadElement(spread) => &mut spread.argument, + argument => argument.to_expression_mut(), + }; + if cx { + self.class_maps(part); + } + self.read_theme(part, false); + self.visit_expression(part); + } + } + + /// A read of the theme or of `css` and `cx` the calls do not take + fn visit_identifier_reference(&mut self, it: &mut oxc_ast::ast::IdentifierReference<'a>) { + let name = Some(it.name.as_str()); + let requirement = if self.names.theme == name { + THEME_READ + } else if self.names.css == name || self.names.cx == name { + CLASS_NAMES_CALL + } else { + return; + }; + self.unread.get_or_insert_with(|| { + ( + Expression::new_identifier(it.span, it.name, self.ast), + requirement, + ) + }); + } } impl<'a> DevupVisitor<'a> { @@ -1981,6 +2324,9 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } } fn visit_expression(&mut self, it: &mut Expression<'a>) { + if !self.class_names_scope.is_empty() && self.compile_class_names_call(it) { + return; + } if !self.styled_imports.is_empty() { match it { Expression::CallExpression(call) => self.plain_styled(&mut call.callee), @@ -3260,6 +3606,12 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.jsx_imports.insert(local, imported_str); continue; } + if compat && imported_str == "ClassNames" { + self.class_names_components.insert(local.clone()); + self.compiled_names.insert(local); + specifiers.remove(i); + continue; + } if let Ok(kind) = imported_str.parse::() { self.imports.insert(local.clone(), kind); } else if let Some(kind) = UtilType::from_str_opt(&imported_str) { @@ -3311,6 +3663,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } #[allow(clippy::set_contains_or_insert)] fn visit_jsx_element(&mut self, elem: &mut JSXElement<'a>) { + if let Some(rendered) = self.class_names(elem) { + self.pending_replacement = Some(rendered); + return; + } let css = self.take_css_prop(elem); walk_jsx_element(self, elem); diff --git a/packages/react/src/compat/__tests__/index.test.ts b/packages/react/src/compat/__tests__/index.test.ts index 65dd5eb70..f0dc55c58 100644 --- a/packages/react/src/compat/__tests__/index.test.ts +++ b/packages/react/src/compat/__tests__/index.test.ts @@ -6,6 +6,7 @@ describe('compat entry', () => { const compat = await import('../index') expect({ ...compat }).toEqual({ + ClassNames: expect.any(Function), Global: expect.any(Function), ThemeProvider: expect.any(Function), @@ -27,6 +28,14 @@ describe('compat entry', () => { expect(jsx).toBe(createElement) }) + it('leaves ClassNames for the build to compile', async () => { + const { ClassNames } = await import('../index') + + expect(() => ClassNames({ children: () => null })).toThrow( + 'Cannot run on the runtime', + ) + }) + it('keeps its useTheme distinct from the devup-ui one', async () => { const { useTheme: compatUseTheme } = await import('../index') const { useTheme: devupUseTheme } = await import('../../index') diff --git a/packages/react/src/compat/emotion.d.ts b/packages/react/src/compat/emotion.d.ts index 6f53d0cf3..095bb0938 100644 --- a/packages/react/src/compat/emotion.d.ts +++ b/packages/react/src/compat/emotion.d.ts @@ -9,7 +9,9 @@ declare module '@emotion/styled' { declare module '@emotion/react' { export { css, keyframes } from '@devup-ui/react' export type { StyledTheme as Theme } from '@devup-ui/react/compat' + export type { ClassNamesArg, ClassNamesContent } from '@devup-ui/react/compat' export { + ClassNames, jsx as createElement, Global, jsx, diff --git a/packages/react/src/compat/index.ts b/packages/react/src/compat/index.ts index ed84439f0..847c0dc26 100644 --- a/packages/react/src/compat/index.ts +++ b/packages/react/src/compat/index.ts @@ -7,6 +7,8 @@ * rewritten imports here automatically. */ +import type { ReactNode } from 'react' + import type { DevupPropsWithTheme } from '../types/props' import type { StyledTheme } from '../utils/theme-vars' @@ -39,3 +41,30 @@ export type CssInterpolation = /** Emotion's `css` prop: styles, or a function of the theme giving them. */ export type CssProp = CssInterpolation | ((theme: StyledTheme) => CssInterpolation) + +/** A class `cx` composes, or an object giving each class its condition. */ +export type ClassNamesArg = + | string + | false + | null + | undefined + | Record + | readonly ClassNamesArg[] + +/** What the child function of Emotion's `ClassNames` takes. */ +export interface ClassNamesContent { + css: (...styles: CssInterpolation[]) => string + cx: (...classes: ClassNamesArg[]) => string + theme: StyledTheme +} + +/** + * Emotion's `ClassNames`: the build renders what its child function gives in + * its place, each `css` and `cx` call compiled to classes. + */ +export function ClassNames( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + props: { children: (content: ClassNamesContent) => ReactNode }, +): ReactNode { + throw new Error('Cannot run on the runtime') +} From 4f88bd43a053540bebf62985c0164043548bec99 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 3 Oct 2026 05:33:25 +0900 Subject: [PATCH 3/8] test(extractor): cover ClassNames child functions taking rest or nested parameters Refs #690 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- libs/extractor/src/lib.rs | 2 ++ ...sts__emotion_class_names_reports_what_it_cannot_compile.snap | 2 ++ 2 files changed, 4 insertions(+) diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index e1a91c5e5..b26968d94 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -21497,6 +21497,8 @@ export const App = () => {({ other }) => };", "export const App = () => {({ [k]: css }) => };", "export const App = () => {({ css }, more) => };", + "export const App = () => {(...content) => };", + "export const App = () => {({ css: { a } }) => };", "export const App = () => {async ({ css }) => };", "export const App = () => {function* ({ css }) { yield ; }};", "export const App = () => {({ css }) => { const a = 1; return ; }};", diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap index 785775e39..796b3ae06 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_class_names_reports_what_it_cannot_compile.snap @@ -15,6 +15,8 @@ expression: errors "test.tsx:2:39: `` cannot use `({ other }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", "test.tsx:2:39: `` cannot use `({ [k]: css }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", "test.tsx:2:39: `` cannot use `({ css }, more) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `(...content) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", + "test.tsx:2:39: `` cannot use `({ css: { a } }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", "test.tsx:2:39: `` cannot use `async ({ css }) => ` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", "test.tsx:2:39: `` cannot use `(function* ({ css }) { yield ; })` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", "test.tsx:2:39: `` cannot use `({ css }) => { const a = 1; return ; }` at build time: it takes only a child function of `{ css, cx, theme }` giving what it renders at once", From d44950f769b5820b561b79b09a82b05b6cb78e9b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 3 Oct 2026 10:15:19 +0900 Subject: [PATCH 4/8] fix(extractor): compile Emotion component selectors at build time Refs #690 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...epack_log_emotion_component_selectors.json | 7 ++ libs/css/src/lib.rs | 38 +++++++++ libs/extractor/src/css_utils.rs | 67 +++++++++++---- .../extract_style_from_expression.rs | 5 +- .../extractor/extract_style_from_styled.rs | 80 +++++++++++++---- libs/extractor/src/lib.rs | 26 ++++++ ...r__tests__emotion_component_selectors.snap | 12 +++ libs/extractor/src/style_values.rs | 85 ++++++++++++++++++- libs/extractor/src/visit.rs | 61 +++++++++++-- 9 files changed, 334 insertions(+), 47 deletions(-) create mode 100644 .changepacks/changepack_log_emotion_component_selectors.json create mode 100644 libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap diff --git a/.changepacks/changepack_log_emotion_component_selectors.json b/.changepacks/changepack_log_emotion_component_selectors.json new file mode 100644 index 000000000..98c3f7259 --- /dev/null +++ b/.changepacks/changepack_log_emotion_component_selectors.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "Emotion and styled-components component selectors now compile at build time: a styled component the file defines that another style selects (${Child} { ... } in CSS text, [Child] or [`&:hover ${Child}`] as a rule key) gets a short marker class, which only such components get, and the selector reads it as .marker; a component extending it, or an element inlining it for its css prop, keeps the marker. Selectors written without & now nest under the component as stylis nests them (ul { ... } is & ul, a selector list nests each selector, :hover applies to the component itself) instead of becoming global rules, and rule-object keys starting with ., #, [, *, >, + or ~ are read as nested selectors instead of properties", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index 576ba53b9..427732ae7 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -429,6 +429,25 @@ pub fn keyframes_to_keyframes_name(keyframes: &str, filename: Option<&str>) -> S }) } +/// The class marking the component `name` defined in `filename`, which other +/// styles select it by +#[must_use] +pub fn component_marker(name: &str, filename: &str) -> String { + with_prefix(|prefix| { + if is_debug() { + format!("{prefix}c-{name}") + } else { + let class_num = class_num_for_key("", |key| { + key.push_str("c-"); + key.push_str(filename); + key.push('-'); + key.push_str(name); + }); + format!("{prefix}{class_num}") + } + }) +} + /// ASCII lookup table for selector encoding. `None` means pass through (alphanumeric, `-`, `_`) /// or fall through to the Unicode escape path. const SELECTOR_ENCODE: [Option<&str>; 128] = { @@ -753,6 +772,25 @@ mod tests { ); } + #[test] + #[serial] + fn test_component_marker() { + set_debug(false); + reset_class_map(); + let child = component_marker("Child", "a.tsx"); + assert_eq!(child, "a"); + assert_eq!(component_marker("Child", "a.tsx"), child); + assert_eq!(component_marker("Child", "b.tsx"), "b"); + assert_eq!(component_marker("Other", "a.tsx"), "c"); + assert_eq!( + sheet_to_classname("color", 0, Some("red"), None, None, None), + "d" + ); + set_debug(true); + assert_eq!(component_marker("Child", "a.tsx"), "c-Child"); + set_debug(false); + } + #[test] #[serial] fn test_sheet_to_classname() { diff --git a/libs/extractor/src/css_utils.rs b/libs/extractor/src/css_utils.rs index 6ea04a394..ab3239a64 100644 --- a/libs/extractor/src/css_utils.rs +++ b/libs/extractor/src/css_utils.rs @@ -587,19 +587,41 @@ fn nest_prelude(parent: Option<&StyleSelector>, prelude: &str) -> Option { - Some(selector) + Some(StyleSelector::nest_selector(parent, &descendants(prelude))) +} + +/// Each selector of the list `selectors` written without `&` as a descendant +/// of it, as stylis nests one +pub(crate) fn descendants(selectors: &str) -> String { + let mut depth = 0usize; + let mut from = 0; + let mut parts = Vec::new(); + for (index, c) in selectors.char_indices() { + match c { + '(' | '[' => depth += 1, + ')' | ']' => depth = depth.saturating_sub(1), + ',' if depth == 0 => { + parts.push(&selectors[from..index]); + from = index + 1; + } + _ => {} } - Some(StyleSelector::At { selector, .. }) => selector.as_ref(), - None => None, - }; - let template = if prelude.contains('&') || parent_selector.is_none() { - Cow::Borrowed(prelude) - } else { - Cow::Owned(format!("& {prelude}")) - }; - Some(StyleSelector::nest_selector(parent, &template)) + } + parts.push(&selectors[from..]); + parts + .iter() + .map(|part| { + let part = part.trim(); + if part.contains('&') { + part.to_string() + } else if part.starts_with(':') { + format!("&{part}") + } else { + format!("& {part}") + } + }) + .collect::>() + .join(",") } /// Optimize a declaration's value only when its property warrants multi-value @@ -1171,7 +1193,7 @@ mod tests { #[case( "`ul { font-family: 'Roboto Hello', sans-serif; }`", vec![ - ("font-family", "\"Roboto Hello\",sans-serif", Some(StyleSelector::Selector("ul".to_string()))), + ("font-family", "\"Roboto Hello\",sans-serif", Some(StyleSelector::Selector("& ul".to_string()))), ] )] #[case( @@ -1610,14 +1632,23 @@ mod tests { #[case( "ul { font-family: 'Roboto Hello', sans-serif; }", vec![ - ("font-family", "\"Roboto Hello\",sans-serif", Some(StyleSelector::Selector("ul".to_string()))), + ("font-family", "\"Roboto Hello\",sans-serif", Some(StyleSelector::Selector("& ul".to_string()))), ] )] #[case( "div { color: red; ; { background: blue; } }", vec![ - ("color", "red", Some(StyleSelector::Selector("div".to_string()))), - ("background", "blue", Some(StyleSelector::Selector("div".to_string()))), + ("color", "red", Some(StyleSelector::Selector("& div".to_string()))), + ("background", "blue", Some(StyleSelector::Selector("& div".to_string()))), + ] + )] + // A selector written without `&` selects within the element, each one of + // a list on its own, and a pseudo-class applies to the element itself + #[case( + "a, :is(b, c) d { color: red; } :hover { color: blue; }", + vec![ + ("color", "red", Some(StyleSelector::Selector("& a,&:is(b,c) d".to_string()))), + ("color", "blue", Some(StyleSelector::Selector("&:hover".to_string()))), ] )] // As in CSS nesting, only the text after the last `;` is the nested rule's @@ -1626,7 +1657,7 @@ mod tests { "color:red;background:blue { width: 1px; }", vec![ ("color", "red", None), - ("width", "1px", Some(StyleSelector::Selector("background:blue".to_string()))), + ("width", "1px", Some(StyleSelector::Selector("& background:blue".to_string()))), ] )] #[case( @@ -1634,7 +1665,7 @@ mod tests { vec![( "width", "1px", - Some(StyleSelector::Selector("color:red".to_string())) + Some(StyleSelector::Selector("& color:red".to_string())) )] )] #[case( diff --git a/libs/extractor/src/extractor/extract_style_from_expression.rs b/libs/extractor/src/extractor/extract_style_from_expression.rs index 4b8f7f27b..0b4b43d40 100644 --- a/libs/extractor/src/extractor/extract_style_from_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_expression.rs @@ -1257,9 +1257,10 @@ fn nest_underscore_name(parent: Option<&StyleSelector>, name: &str) -> Option li'`) rather +/// than CSS properties. fn is_nested_selector_key(key: &str) -> bool { - key.starts_with(':') || key.contains('&') + key.starts_with([':', '.', '#', '[', '*', '>', '+', '~']) || key.contains('&') } fn nest_selectors_key(parent: Option<&StyleSelector>, key: &str) -> Option { diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index e5955fb47..c1ceb13df 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -60,6 +60,27 @@ pub struct StyledDefinition<'a> { reads: Reads, /// Its `shouldForwardProp`, as the build evaluates it forward: Option, + /// The class other styles select it by, which a component extending it + /// gives too, as it renders this one + marker: Option, +} + +/// `marker` as a class among those a component gives +fn marker_class<'a>(ast_builder: &AstBuilder<'a>, marker: &str) -> Expression<'a> { + Expression::new_string_literal( + SPAN, + Str::from_in(marker, ast_builder.allocator()), + None, + ast_builder, + ) +} + +/// How the classes of a styled component are named: the file its atoms are +/// split into, and the class other styles select it by +#[derive(Clone, Copy, Default)] +pub struct Naming<'s> { + pub split_filename: Option<&'s str>, + pub marker: Option<&'s str>, } /// Whether a styled component renders a tag, which takes only valid @@ -119,6 +140,12 @@ impl<'a> StyledDefinition<'a> { .then_some((self.name.as_str(), self.styles.as_slice())) } + /// The class other styles select the component by + #[must_use] + pub fn marker(&self) -> Option<&str> { + self.marker.as_deref() + } + /// The styles the component gives what it renders #[must_use] pub fn styles(&self) -> &[ExtractStyleProp<'a>] { @@ -236,9 +263,11 @@ fn inherited_parts<'a>( |inherited| { ( inherited - .classes - .iter() - .map(|c| c.clone_in(allocator)) + .marker + .as_deref() + .map(|marker| marker_class(ast_builder, marker)) + .into_iter() + .chain(inherited.classes.iter().map(|c| c.clone_in(allocator))) .collect(), inherited .attrs @@ -294,6 +323,7 @@ impl<'a> Base<'a> { attrs: attrs.iter().map(|attr| attr.clone_in(allocator)).collect(), reads, forward, + marker: None, } } @@ -458,12 +488,16 @@ fn resolve_styled_call_target<'a>( pub fn extract_style_from_styled<'a>( ast_builder: &AstBuilder<'a>, expression: &mut Expression<'a>, - split_filename: Option<&str>, + naming: Naming<'_>, imports: &FxHashMap, attrs: &[Expression<'a>], inherited: Option<&StyledDefinition<'a>>, forward: Option, ) -> StyledExtraction<'a> { + let Naming { + split_filename, + marker, + } = naming; let forward = combine_forward(inherited.and_then(|i| i.forward.as_ref()), forward); let mut reads = inherited.map_or_else(Reads::default, |inherited| inherited.reads.clone()); for attr in attrs { @@ -547,12 +581,16 @@ pub fn extract_style_from_styled<'a>( })); let class_name = merge_expression_for_class_name( ast_builder, - clone_all(ast_builder, &classes).chain(gen_class_names( - ast_builder, - &mut props_styles, - None, - split_filename, - )), + marker + .map(|marker| marker_class(ast_builder, marker)) + .into_iter() + .chain(clone_all(ast_builder, &classes)) + .chain(gen_class_names( + ast_builder, + &mut props_styles, + None, + split_filename, + )), ); let tag = Some(Expression::new_string_literal( SPAN, @@ -567,8 +605,9 @@ pub fn extract_style_from_styled<'a>( &gen_styles(ast_builder, &props_styles, None), &base.withheld(&reads, forward.as_ref()), ); - let definition = + let mut definition = base.definition(ast_builder, classes, &props_styles, &attrs, reads, forward); + definition.marker = marker.map(str::to_string); let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, &attrs)); @@ -634,12 +673,16 @@ pub fn extract_style_from_styled<'a>( let class_name = merge_expression_for_class_name( ast_builder, - clone_all(ast_builder, &classes).chain(gen_class_names( - ast_builder, - &mut styles, - None, - split_filename, - )), + marker + .map(|marker| marker_class(ast_builder, marker)) + .into_iter() + .chain(clone_all(ast_builder, &classes)) + .chain(gen_class_names( + ast_builder, + &mut styles, + None, + split_filename, + )), ); let component = create_styled_component( ast_builder, @@ -648,7 +691,8 @@ pub fn extract_style_from_styled<'a>( &gen_styles(ast_builder, &styles, None), &base.withheld(&reads, forward.as_ref()), ); - let definition = base.definition(ast_builder, classes, &styles, &attrs, reads, forward); + let mut definition = base.definition(ast_builder, classes, &styles, &attrs, reads, forward); + definition.marker = marker.map(str::to_string); let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, &attrs)); diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index b26968d94..ac1dcbde6 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -21480,6 +21480,32 @@ export const App = () => ;", + "const Child = styled.div({ color: 'red' });\nexport const Parent = styled.div({ [Child]: { color: 'green' }, [`&:hover ${Child}`]: { color: 'blue' } });", + "const Child = styled.div({ color: 'red' });\nconst Wide = styled(Child)({ width: 1 });\nexport const Parent = styled.div`${Child} { color: green; }`;\nexport const App = () => <>;", + "const Child = styled.div`color: red;`;\nexport const Parent = styled.div`${Child} { color: green; }`;\nexport const C = () => ;", + "const notStyled = 'x';\nexport const Parent = styled.div`${notStyled} { color: green; }`;", + "export const Parent = styled.div`a, :is(b, c) d { color: green; }`;", + ]; + let outputs: Vec = codes + .iter() + .map(|code| { + reset_class_map(); + reset_file_map(); + match extract("test.tsx", &format!("{import}{code}"), emotion_option()) { + Ok(output) => format!("{}\n{:?}", output.code, output.styles), + Err(error) => error.to_string(), + } + }) + .collect(); + assert_debug_snapshot!(outputs); + } + #[test] #[serial] fn test_emotion_class_names_reports_what_it_cannot_compile() { diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap new file mode 100644 index 000000000..7193e2389 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap @@ -0,0 +1,12 @@ +--- +source: libs/extractor/src/lib.rs +expression: outputs +--- +[ + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c d\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => ;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: Some(Selector(\"&:hover .a,& .a + a\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c d\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: Some(Selector(\"&:hover .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nconst Wide = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a c b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => <>;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"width\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = () =>
;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst notStyled = \"x\";\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& x\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& a,&:is(b,c) d\")), style_order: None, layer: None })}", +] diff --git a/libs/extractor/src/style_values.rs b/libs/extractor/src/style_values.rs index 0f1618d8b..e908704c0 100644 --- a/libs/extractor/src/style_values.rs +++ b/libs/extractor/src/style_values.rs @@ -9,7 +9,7 @@ use oxc_ast_visit::{VisitMut, walk_mut}; use oxc_semantic::Scoping; use oxc_span::SPAN; use oxc_syntax::symbol::SymbolId; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use crate::ExtractStyleValue; @@ -22,6 +22,9 @@ pub enum StyleValue { Class(String, Option>), /// A name `keyframes()` gives Keyframes(String), + /// The selector of a styled component other styles select, read in CSS + /// text and rule keys alike + Component(String), } #[derive(Default)] @@ -67,6 +70,10 @@ impl StyleValues { self.values.insert(symbol, value); } + pub fn remove(&mut self, symbol: SymbolId) { + self.values.remove(&symbol); + } + /// The binding `expression` reads, when it reads one pub fn symbol(&self, expression: &Expression<'_>) -> Option { let Expression::Identifier(identifier) = expression else { @@ -134,19 +141,89 @@ impl StyleValues { values: &self.values, in_text: false, in_rules: false, + in_key: false, }) } } +/// The bindings styles read as selectors: interpolated in CSS text where a +/// value cannot stand, or as the computed key of a rule object +pub fn selected(program: &oxc_ast::ast::Program<'_>, values: &StyleValues) -> FxHashSet { + let mut selected = Selected { + values, + symbols: FxHashSet::default(), + }; + oxc_ast_visit::Visit::visit_program(&mut selected, program); + selected.symbols +} + +struct Selected<'s> { + values: &'s StyleValues, + symbols: FxHashSet, +} + +impl<'a> oxc_ast_visit::Visit<'a> for Selected<'_> { + fn visit_tagged_template_expression( + &mut self, + it: &oxc_ast::ast::TaggedTemplateExpression<'a>, + ) { + let quasi = &it.quasi; + let mut text = String::new(); + for (index, expression) in quasi.expressions.iter().enumerate() { + text.push_str(quasi.quasis[index].value.raw.as_str()); + if matches!( + crate::css_utils::interpolation_place(&text, &quasi.quasis[index + 1..]), + crate::css_utils::Place::Other + ) { + self.symbols.extend(self.values.symbol(expression)); + } + } + oxc_ast_visit::walk::walk_tagged_template_expression(self, it); + } + + fn visit_object_property(&mut self, it: &oxc_ast::ast::ObjectProperty<'a>) { + if it.computed + && let Some(key) = it.key.as_expression() + { + self.symbols.extend(self.values.symbol(key)); + } + oxc_ast_visit::walk::walk_object_property(self, it); + } +} + struct Reads<'s, 'a> { ast: &'s AstBuilder<'a>, scoping: &'s Scoping, values: &'s FxHashMap, in_text: bool, in_rules: bool, + /// Inside the key of a rule, which a component is read in as its selector + in_key: bool, } impl<'a> VisitMut<'a> for Reads<'_, 'a> { + fn visit_property_key(&mut self, it: &mut oxc_ast::ast::PropertyKey<'a>) { + // A component standing for the whole key selects it within the rule + if let Some(Expression::Identifier(identifier)) = it.as_expression() + && let Some(StyleValue::Component(selector)) = identifier + .reference_id + .get() + .and_then(|reference| self.scoping.get_reference(reference).symbol_id()) + .and_then(|symbol| self.values.get(&symbol)) + { + *it = oxc_ast::ast::PropertyKey::StringLiteral(oxc_ast::ast::StringLiteral::boxed( + SPAN, + Str::from_in(format!("& {selector}").as_str(), self.ast.allocator()), + None, + self.ast, + )); + return; + } + let outer = std::mem::replace(&mut self.in_key, true); + walk_mut::walk_property_key(self, it); + self.in_key = outer; + } + fn visit_expression(&mut self, it: &mut Expression<'a>) { if let Expression::Identifier(identifier) = it && let Some(value) = identifier @@ -157,7 +234,11 @@ impl<'a> VisitMut<'a> for Reads<'_, 'a> { { let value = match value { StyleValue::Class(..) if self.in_text => return, - StyleValue::Class(value, _) | StyleValue::Keyframes(value) => value, + // Elsewhere it is the component itself + StyleValue::Component(_) if !self.in_text && !self.in_key => return, + StyleValue::Class(value, _) + | StyleValue::Keyframes(value) + | StyleValue::Component(value) => value, }; *it = Expression::new_string_literal( SPAN, diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index a74a216aa..f78d276d3 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -26,8 +26,9 @@ use crate::extractor::{ }, extract_style_from_jsx::extract_style_from_jsx, extract_style_from_styled::{ - FORWARD_REF, StyledDefinition, StyledExtraction, extended, extract_style_from_styled, - forward_ref, read_forward, take_styled_modifiers, with_component, + FORWARD_REF, Naming, StyledDefinition, StyledExtraction, extended, + extract_style_from_styled, forward_ref, read_forward, take_styled_modifiers, + with_component, }, }; use crate::gen_class_name::{gen_class_names, merge_expression_for_class_name}; @@ -249,6 +250,11 @@ pub struct DevupVisitor<'a> { /// The styled component just built, by where it starts, for the `const` /// it initializes pending_styled: Option<(u32, StyledDefinition<'a>)>, + /// The styled bindings other styles select + selected_components: FxHashSet, + /// The marker class of the selected styled component the declaration + /// being visited defines, with where its definition starts + pending_marker: Option<(u32, String)>, /// Whether a generated styled component forwards refs through React's /// `forwardRef`, which the program then imports forwards_refs: bool, @@ -755,6 +761,8 @@ impl<'a> DevupVisitor<'a> { style_values: crate::style_values::StyleValues::default(), css_styles: None, pending_styled: None, + selected_components: FxHashSet::default(), + pending_marker: None, forwards_refs: false, styled_definitions: FxHashMap::default(), imported_css: FxHashMap::default(), @@ -1289,12 +1297,22 @@ impl<'a> DevupVisitor<'a> { .filter(|_| renders) .and_then(StyledDefinition::inline) .map(|(tag, styles)| (tag.to_string(), clone(styles))); + let marker = definition.and_then(StyledDefinition::marker).map(|marker| { + ExtractStyleProp::Expression { + expression: Expression::new_string_literal( + SPAN, + Str::from_in(marker, allocator), + None, + &self.ast, + ), + styles: vec![], + } + }); let own = definition.map(|definition| clone(definition.styles())); if let Some((tag, styles)) = inline { - return Some(( - Some(tag), - self.compose_css_prop(element, css, styles, class_name), - )); + let mut props = self.compose_css_prop(element, css, styles, class_name); + props.extend(marker); + return Some((Some(tag), props)); } let offset = css.offset; let props = self.compose_css_prop(element, css, vec![], class_name); @@ -2262,6 +2280,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { ); self.style_values .import(std::mem::take(&mut self.imported_css)); + self.selected_components = crate::style_values::selected(it, &self.style_values); } walk_program(self, it); if !self.compiled_names.is_empty() { @@ -2415,6 +2434,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .and_then(|symbol| self.styled_definitions.get(&symbol)) .filter(|definition| definition.extendable()); let start = it.span().start; + let marker = self + .pending_marker + .take_if(|(at, _)| *at == start) + .map(|(_, marker)| marker); let StyledExtraction { result, expression, @@ -2423,7 +2446,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } = extract_style_from_styled( &self.ast, it, - self.split_filename.as_deref(), + Naming { + split_filename: self.split_filename.as_deref(), + marker: marker.as_deref(), + }, &self.imports, &attrs, inherited, @@ -3494,8 +3520,29 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.local_styles.insert(symbol); } + let marked = styled_binding + .filter(|symbol| self.selected_components.contains(symbol)) + .zip(start) + .zip(it.id.get_binding_identifier().map(|id| id.name.to_string())); + if let Some(((symbol, start), name)) = &marked { + let (symbol, start) = (*symbol, *start); + let marker = css::component_marker(name, &self.filename); + self.style_values.insert( + symbol, + crate::style_values::StyleValue::Component(format!(".{marker}")), + ); + self.pending_marker = Some((start, marker)); + } + walk_variable_declarator(self, it); + // A binding selected but not to a styled component stays unread + if let Some(((symbol, _), _)) = marked + && self.pending_marker.take().is_some() + { + self.style_values.remove(symbol); + } + if let Some((at, definition)) = self.pending_styled.take() && Some(at) == start && let Some(symbol) = styled_binding From 70dac33c09d187d60400986e65b0d96e87a4b953 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 3 Oct 2026 17:01:53 +0900 Subject: [PATCH 5/8] feat(extractor): compose styled components other modules define Refs #688 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...epack_log_imported_styled_definitions.json | 7 + libs/css/src/lib.rs | 41 +- .../extractor/extract_style_from_styled.rs | 176 +++- libs/extractor/src/imported_styled.rs | 887 ++++++++++++++++++ libs/extractor/src/lib.rs | 19 +- ...ted_by_the_class_their_module_gives-2.snap | 5 + ...ected_by_the_class_their_module_gives.snap | 10 + ...ed_components_compose_like_local_ones.snap | 16 + ...s_give_css_props_what_local_ones_give.snap | 10 + ...d_cannot_read_stay_runtime_components.snap | 15 + ..._namespaces_reach_the_defining_module.snap | 14 + ...tests__compiled_bindings_never_dangle.snap | 2 +- ...ts__compose_rules_the_module_computes.snap | 2 +- ...what_elements_and_styled_objects_take.snap | 2 +- ...actor__tests__conditional_composition.snap | 2 +- ...ts__constant_objects_arrays_and_enums.snap | 2 +- ...and_styled_components_object_styles-2.snap | 2 +- ...n_and_styled_components_object_styles.snap | 2 +- ...r__tests__emotion_component_selectors.snap | 12 +- ...tor__tests__inline_imported_constants.snap | 2 +- ...ractor__tests__package_imported_whole.snap | 4 +- ...__tests__style_results_read_as_values.snap | 2 +- ...ponents_attrs_and_every_styled_import.snap | 2 +- ..._tests__styled_components_of_any_base.snap | 8 +- ...actor__tests__template_interpolations.snap | 2 +- libs/extractor/src/style_values.rs | 42 +- libs/extractor/src/visit.rs | 241 ++++- .../src/__tests__/imported-styled.test.ts | 137 +++ 28 files changed, 1546 insertions(+), 120 deletions(-) create mode 100644 .changepacks/changepack_log_imported_styled_definitions.json create mode 100644 libs/extractor/src/imported_styled.rs create mode 100644 libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives.snap create mode 100644 libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_compose_like_local_ones.snap create mode 100644 libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_give_css_props_what_local_ones_give.snap create mode 100644 libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap create mode 100644 libs/extractor/src/snapshots/extractor__imported_styled__tests__re_exports_and_namespaces_reach_the_defining_module.snap create mode 100644 packages/next-plugin/src/__tests__/imported-styled.test.ts diff --git a/.changepacks/changepack_log_imported_styled_definitions.json b/.changepacks/changepack_log_imported_styled_definitions.json new file mode 100644 index 000000000..184bcaf65 --- /dev/null +++ b/.changepacks/changepack_log_imported_styled_definitions.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "A styled component another module defines now composes at build time exactly as one the file defines itself: styled(ImportedBase)(...), styled(ImportedBase).attrs(...), ImportedBase.withComponent(...) and the Emotion css prop on ImportedBase render the base's tag with the base's styles under their own (the later declaration wins per property, selector, breakpoint and layer), merge attrs in order and filter props by what the styles read and shouldForwardProp. The build reads the other module through the same module resolution imported constants use (relative paths, tsconfig paths, package exports, include libraries) and reports it as a dependency, so a change to it rebuilds the importing file. A definition the build cannot read exactly (a base only the runtime gives, an attrs or style expression reading a binding of the defining module, a module with a build error, an unresolvable module, an import cycle) stays a runtime component that wraps, as before. Every exported styled component now carries a marker class computed from its file and its export name alone, so ${ImportedChild} { ... } in CSS text and [ImportedChild] as a rule key select it from another file; the marker replaces the registry-ordered one, which two files could not compute alike. No new build errors: the css prop on an imported styled component now reports the same override error a local one does when its styles overlap the component's", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index 427732ae7..d9bd4ae76 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -429,21 +429,25 @@ pub fn keyframes_to_keyframes_name(keyframes: &str, filename: Option<&str>) -> S }) } -/// The class marking the component `name` defined in `filename`, which other -/// styles select it by +/// The class marking the component `name` that `filename` defines. +/// +/// Other styles, in this file or another, select the component by it. It is +/// `index` among the components of the file, so it comes from the file and the +/// component alone: every file that selects the component computes the same +/// class, in any process. Two dashes keep it apart from every class styles are +/// named by. #[must_use] -pub fn component_marker(name: &str, filename: &str) -> String { +pub fn component_marker(name: &str, index: usize, filename: &str) -> String { with_prefix(|prefix| { + let file = get_file_num_by_filename(filename); if is_debug() { - format!("{prefix}c-{name}") + format!("{prefix}c-{file}-{name}") } else { - let class_num = class_num_for_key("", |key| { - key.push_str("c-"); - key.push_str(filename); - key.push('-'); - key.push_str(name); - }); - format!("{prefix}{class_num}") + format!( + "{prefix}{}--{}", + num_to_nm_base(file), + num_to_nm_base(index) + ) } }) } @@ -777,17 +781,18 @@ mod tests { fn test_component_marker() { set_debug(false); reset_class_map(); - let child = component_marker("Child", "a.tsx"); - assert_eq!(child, "a"); - assert_eq!(component_marker("Child", "a.tsx"), child); - assert_eq!(component_marker("Child", "b.tsx"), "b"); - assert_eq!(component_marker("Other", "a.tsx"), "c"); + crate::file_map::reset_file_map(); + let child = component_marker("Child", 0, "a.tsx"); + assert_eq!(child, "a--a"); + assert_eq!(component_marker("Child", 0, "a.tsx"), child); + assert_eq!(component_marker("Child", 0, "b.tsx"), "b--a"); + assert_eq!(component_marker("Other", 2, "a.tsx"), "a--c"); assert_eq!( sheet_to_classname("color", 0, Some("red"), None, None, None), - "d" + "a" ); set_debug(true); - assert_eq!(component_marker("Child", "a.tsx"), "c-Child"); + assert_eq!(component_marker("Child", 0, "a.tsx"), "c-0-Child"); set_debug(false); } diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index c1ceb13df..c5a484d76 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -23,12 +23,13 @@ use oxc_allocator::{CloneIn, FromIn, GetAllocator}; use oxc_ast::{ ast::{ Argument, BindingPattern, BindingProperty, BindingRestElement, CallExpression, Expression, - FormalParameter, FormalParameterKind, FormalParameters, JSXAttributeItem, JSXAttributeName, - JSXAttributeValue, JSXElementName, JSXOpeningElement, ObjectPropertyKind, PropertyKey, Str, + ExpressionStatement, FormalParameter, FormalParameterKind, FormalParameters, + JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXElementName, JSXOpeningElement, + ObjectPropertyKind, Program, PropertyKey, Statement, Str, }, builder::AstBuilder, }; -use oxc_span::{GetSpan, SPAN}; +use oxc_span::{GetSpan, SPAN, SourceType}; use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator}; /// The binding a styled component reads the component it renders through, when @@ -60,9 +61,9 @@ pub struct StyledDefinition<'a> { reads: Reads, /// Its `shouldForwardProp`, as the build evaluates it forward: Option, - /// The class other styles select it by, which a component extending it + /// The classes other styles select it by, which a component extending it /// gives too, as it renders this one - marker: Option, + markers: Vec, } /// `marker` as a class among those a component gives @@ -75,12 +76,22 @@ fn marker_class<'a>(ast_builder: &AstBuilder<'a>, marker: &str) -> Expression<'a ) } +/// `markers` as classes among those a component gives +fn marker_classes<'s, 'a>( + ast_builder: &'s AstBuilder<'a>, + markers: &'s [String], +) -> impl Iterator> + 's { + markers + .iter() + .map(|marker| marker_class(ast_builder, marker)) +} + /// How the classes of a styled component are named: the file its atoms are -/// split into, and the class other styles select it by +/// split into, and the classes other styles select it by #[derive(Clone, Copy, Default)] pub struct Naming<'s> { pub split_filename: Option<&'s str>, - pub marker: Option<&'s str>, + pub markers: &'s [String], } /// Whether a styled component renders a tag, which takes only valid @@ -140,10 +151,51 @@ impl<'a> StyledDefinition<'a> { .then_some((self.name.as_str(), self.styles.as_slice())) } - /// The class other styles select the component by + /// The classes other styles select the component by + #[must_use] + pub fn markers(&self) -> &[String] { + &self.markers + } + + /// This definition in the arena of another file + #[must_use] + pub fn clone_in<'b>(&self, allocator: &'b oxc_allocator::Allocator) -> StyledDefinition<'b> { + StyledDefinition { + name: self.name.clone(), + bound: self.bound.as_ref().map(|bound| bound.clone_in(allocator)), + classes: self + .classes + .iter() + .map(|class| class.clone_in(allocator)) + .collect(), + styles: self + .styles + .iter() + .map(|style| style.clone_in(allocator)) + .collect(), + attrs: self + .attrs + .iter() + .map(|attr| attr.clone_in(allocator)) + .collect(), + reads: self.reads.clone(), + forward: self.forward.clone(), + markers: self.markers.clone(), + } + } + + /// Whether another file can render this component in its place: it + /// renders a tag, and nothing it holds reads a binding of the file + /// defining it #[must_use] - pub fn marker(&self) -> Option<&str> { - self.marker.as_deref() + pub fn portable(&self) -> bool { + renders_tag(&self.name, self.bound.as_ref()) + && closed( + self.classes + .iter() + .chain(&self.attrs) + .chain(self.styles.iter().flat_map(style_expressions)), + ) } /// The styles the component gives what it renders @@ -262,11 +314,7 @@ fn inherited_parts<'a>( || (Vec::new(), Vec::new()), |inherited| { ( - inherited - .marker - .as_deref() - .map(|marker| marker_class(ast_builder, marker)) - .into_iter() + marker_classes(ast_builder, &inherited.markers) .chain(inherited.classes.iter().map(|c| c.clone_in(allocator))) .collect(), inherited @@ -323,7 +371,7 @@ impl<'a> Base<'a> { attrs: attrs.iter().map(|attr| attr.clone_in(allocator)).collect(), reads, forward, - marker: None, + markers: Vec::new(), } } @@ -496,7 +544,7 @@ pub fn extract_style_from_styled<'a>( ) -> StyledExtraction<'a> { let Naming { split_filename, - marker, + markers, } = naming; let forward = combine_forward(inherited.and_then(|i| i.forward.as_ref()), forward); let mut reads = inherited.map_or_else(Reads::default, |inherited| inherited.reads.clone()); @@ -581,9 +629,7 @@ pub fn extract_style_from_styled<'a>( })); let class_name = merge_expression_for_class_name( ast_builder, - marker - .map(|marker| marker_class(ast_builder, marker)) - .into_iter() + marker_classes(ast_builder, markers) .chain(clone_all(ast_builder, &classes)) .chain(gen_class_names( ast_builder, @@ -607,7 +653,7 @@ pub fn extract_style_from_styled<'a>( ); let mut definition = base.definition(ast_builder, classes, &props_styles, &attrs, reads, forward); - definition.marker = marker.map(str::to_string); + definition.markers = markers.to_vec(); let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, &attrs)); @@ -673,9 +719,7 @@ pub fn extract_style_from_styled<'a>( let class_name = merge_expression_for_class_name( ast_builder, - marker - .map(|marker| marker_class(ast_builder, marker)) - .into_iter() + marker_classes(ast_builder, markers) .chain(clone_all(ast_builder, &classes)) .chain(gen_class_names( ast_builder, @@ -692,7 +736,7 @@ pub fn extract_style_from_styled<'a>( &base.withheld(&reads, forward.as_ref()), ); let mut definition = base.definition(ast_builder, classes, &styles, &attrs, reads, forward); - definition.marker = marker.map(str::to_string); + definition.markers = markers.to_vec(); let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, &attrs)); @@ -725,6 +769,88 @@ pub fn extract_style_from_styled<'a>( } } +/// What every module reads the same, besides the props a component takes +const SHARED_NAMES: &[&str] = &[ + "rest", + "undefined", + "NaN", + "Infinity", + "Math", + "Number", + "String", + "Boolean", + "Object", + "Array", + "JSON", + "parseInt", + "parseFloat", + "isNaN", + "isFinite", +]; + +/// The expressions a component evaluates for `prop` +fn style_expressions<'p, 'a>(prop: &'p ExtractStyleProp<'a>) -> Vec<&'p Expression<'a>> { + match prop { + ExtractStyleProp::Static(_) | ExtractStyleProp::Unreadable { .. } => Vec::new(), + ExtractStyleProp::StaticArray(props) => props.iter().flat_map(style_expressions).collect(), + ExtractStyleProp::Conditional { + condition, + consequent, + alternate, + } => std::iter::once(condition) + .chain(consequent.iter().flat_map(|prop| style_expressions(prop))) + .chain(alternate.iter().flat_map(|prop| style_expressions(prop))) + .collect(), + ExtractStyleProp::Enum { condition, map } => std::iter::once(condition) + .chain(map.values().flatten().flat_map(style_expressions)) + .collect(), + ExtractStyleProp::Expression { expression, .. } => vec![expression], + ExtractStyleProp::MemberExpression { map, expression } => std::iter::once(expression) + .chain(map.values().flat_map(|prop| style_expressions(prop))) + .collect(), + } +} + +/// Whether `expressions` read nothing but the bindings they declare, the +/// props, and what every module reads the same +fn closed<'e, 'a: 'e>(expressions: impl Iterator>) -> bool { + let allocator = oxc_allocator::Allocator::default(); + let ast_builder = AstBuilder::new(&allocator); + let body = oxc_allocator::Vec::from_iter_in( + expressions.map(|expression| { + Statement::ExpressionStatement(ExpressionStatement::boxed( + SPAN, + expression.clone_in(&allocator), + &ast_builder, + )) + }), + &ast_builder, + ); + let program = Program::new( + SPAN, + SourceType::tsx(), + "", + oxc_allocator::Vec::new_in(&ast_builder), + None, + oxc_allocator::Vec::new_in(&ast_builder), + body, + &ast_builder, + ); + let scoping = oxc_semantic::SemanticBuilder::new() + .build(&program) + .semantic + .into_scoping(); + scoping + .root_unresolved_references() + .iter() + .all(|(name, references)| { + SHARED_NAMES.contains(&name.as_str()) + || references + .iter() + .all(|reference| !scoping.get_reference(*reference).is_value()) + }) +} + fn clone_all<'s, 'a>( ast_builder: &'s AstBuilder<'a>, expressions: &'s [Expression<'a>], diff --git a/libs/extractor/src/imported_styled.rs b/libs/extractor/src/imported_styled.rs new file mode 100644 index 000000000..26eb3c44f --- /dev/null +++ b/libs/extractor/src/imported_styled.rs @@ -0,0 +1,887 @@ +//! Styled components other modules define, read where a file extends one, +//! selects one in CSS text or gives one a `css` prop, so they compose exactly +//! as the ones the file defines itself. + +use std::collections::BTreeSet; +use std::rc::Rc; + +use oxc_allocator::{Allocator, GetAllocator}; +use oxc_ast::ast::{ + CallExpression, Declaration, Expression, ImportDeclarationSpecifier, JSXAttributeItem, + JSXAttributeName, JSXElementName, JSXOpeningElement, Program, Statement, +}; +use oxc_ast::builder::AstBuilder; +use oxc_ast_visit::{Visit, VisitMut, walk}; +use oxc_parser::{Parser, ParserReturn}; +use oxc_span::SourceType; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::css_prop::CssProp; +use crate::extractor::extract_style_from_styled::StyledDefinition; +use crate::import_alias_visit::{Aliased, transform_import_aliases_with_edits}; +use crate::utils::{is_vanilla_extract_file, unwrap_syntax_only}; +use crate::visit::DevupVisitor; +use crate::{ExtractOption, ModuleResolver}; + +/// What a module exports, which marks the components other files select +#[derive(Default)] +pub(crate) struct Exports { + /// Every name the module exports, in order + names: Vec, + /// The names each binding is exported as + bindings: FxHashMap>, + /// Whether `export default` exports a value no binding holds + anonymous_default: bool, +} + +impl Exports { + pub(crate) fn scan(program: &Program<'_>) -> Self { + let mut exports = Self::default(); + for statement in &program.body { + match statement { + Statement::ExportDeclaration(export) => { + if let Declaration::VariableDeclaration(declaration) = &export.declaration { + for declarator in &declaration.declarations { + if let Some(identifier) = declarator.id.get_binding_identifier() { + exports.add(identifier.name.as_str(), identifier.name.as_str()); + } + } + } + } + Statement::ExportNamedDeclaration(export) if !export.export_kind.is_type() => { + for specifier in export + .specifiers + .iter() + .filter(|specifier| !specifier.export_kind.is_type()) + { + exports.add( + specifier.local.name().as_str(), + specifier.exported.name().as_str(), + ); + } + } + Statement::ExportDefaultDeclaration(export) => { + match export.declaration.as_expression() { + Some(Expression::Identifier(identifier)) => { + exports.add(identifier.name.as_str(), "default"); + } + Some(_) => { + exports.anonymous_default = true; + exports.names.push("default".to_string()); + } + None => {} + } + } + _ => {} + } + } + exports.names.sort_unstable(); + exports.names.dedup(); + exports + } + + fn add(&mut self, binding: &str, exported: &str) { + self.names.push(exported.to_string()); + self.bindings + .entry(binding.to_string()) + .or_default() + .push(exported.to_string()); + } + + /// How many names the module exports + pub(crate) const fn count(&self) -> usize { + self.names.len() + } + + /// Where the exported `name` stands among the names the module exports + pub(crate) fn index(&self, name: &str) -> usize { + self.names + .binary_search_by(|exported| exported.as_str().cmp(name)) + .unwrap_or_default() + } + + /// The names the binding `binding` is exported as + pub(crate) fn of(&self, binding: &str) -> &[String] { + self.bindings.get(binding).map_or(&[], Vec::as_slice) + } + + pub(crate) fn bindings(&self) -> impl Iterator)> { + self.bindings.iter() + } + + pub(crate) const fn has_anonymous_default(&self) -> bool { + self.anonymous_default + } +} + +/// A styled component another module defines: the classes other styles +/// select it by, and what composing it takes when the build can read it +pub struct ImportedComponent<'a> { + pub markers: Vec, + pub definition: Option>, +} + +impl ImportedComponent<'_> { + /// This component in the arena of the file that reads it + pub fn clone_in<'b>(&self, allocator: &'b Allocator) -> ImportedComponent<'b> { + ImportedComponent { + markers: self.markers.clone(), + definition: self + .definition + .as_ref() + .map(|definition| definition.clone_in(allocator)), + } + } + + /// This component as another file can use it: composed only when its + /// definition holds nothing of the module defining it + fn shared(self, readable: bool) -> Self { + Self { + definition: self + .definition + .filter(|definition| readable && definition.portable()), + ..self + } + } +} + +type Components<'a> = FxHashMap>; + +/// The components a file imports, and the modules read to find them +#[derive(Default)] +pub(crate) struct Reading<'a> { + pub components: Components<'a>, + pub dependencies: BTreeSet, +} + +/// Read the styled components `program` extends, selects or gives a `css` +/// prop, from the modules it imports them from +pub(crate) fn read<'a>( + ast_builder: &AstBuilder<'a>, + program: &Program<'a>, + filename: &str, + option: &ExtractOption, + resolver: Option<&ModuleResolver>, + css_prop: CssProp, +) -> Reading<'a> { + let Some(resolver) = resolver else { + return Reading::default(); + }; + let allocator = ast_builder.allocator(); + let mut reader = Reader { + option, + resolver, + allocator, + modules: FxHashMap::default(), + reading: vec![filename.to_string()], + dependencies: BTreeSet::new(), + }; + let components = reader.imports(program, filename, css_prop, allocator); + Reading { + components, + dependencies: reader.dependencies, + } +} + +/// The components a module exports, by export name +type Module<'a> = Rc>; + +struct Reader<'r, 'a> { + option: &'r ExtractOption, + resolver: &'r ModuleResolver, + allocator: &'a Allocator, + modules: FxHashMap>>, + /// The modules being read, which one of them importing another in a + /// cycle cannot be read through + reading: Vec, + dependencies: BTreeSet, +} + +impl<'a> Reader<'_, 'a> { + /// The components of the module `specifier` names, as `importer` imports it + fn module(&mut self, specifier: &str, importer: &str) -> Option> { + let module = (self.resolver)(specifier, importer)?; + self.dependencies.insert(module.path.clone()); + if let Some(read) = self.modules.get(&module.path) { + return read.clone(); + } + if self.reading.contains(&module.path) { + return None; + } + self.reading.push(module.path.clone()); + let read = self.read(&module.path, &module.code).map(Rc::new); + self.reading.pop(); + self.modules.insert(module.path, read.clone()); + read + } + + /// The styled components the module at `path` exports, found by compiling + /// it as its own extraction does + fn read(&mut self, path: &str, code: &str) -> Option> { + if is_vanilla_extract_file(path) { + return None; + } + let Aliased { + code: transformed, + css_prop, + .. + } = transform_import_aliases_with_edits( + code, + path, + &self.option.package, + &self.option.import_aliases, + ); + let sub = Allocator::default(); + let ParserReturn { + mut program, + fatal_error, + .. + } = Parser::new(&sub, &transformed, SourceType::from_path(path).ok()?).parse(); + if fatal_error { + return None; + } + let ast_builder = AstBuilder::new(&sub); + let inlined = crate::imported_constants::inline_constants( + &ast_builder, + &mut program, + path, + self.option, + Some(self.resolver), + css_prop, + ); + self.dependencies.extend(inlined.dependencies); + let imports = self.imports(&program, path, css_prop, &sub); + let mut exports = self.reexports(&program, path); + let (bucket, global, _) = crate::resolve_css_target(path, self.option); + let mut visitor = DevupVisitor::new( + &sub, + path, + &self.option.package, + Vec::new(), + if global { None } else { Some(bucket) }, + ); + visitor.import_stylex(inlined.stylex_vars, inlined.stylex_themes); + visitor.import_css(inlined.css_styles); + visitor.unknown_bindings(&inlined.unknown); + visitor.changed_bindings(inlined.changed); + visitor.takes_css_prop(css_prop); + visitor.import_styled(imports); + visitor.visit_program(&mut program); + let readable = visitor.errors.is_empty() + && visitor.unknown_parts.is_empty() + && !visitor.composes_unknown; + exports.extend( + visitor + .components(self.allocator) + .into_iter() + .map(|(name, component)| (name, component.shared(readable))), + ); + Some(exports) + } + + /// What the module re-exports from the modules it exports from + fn reexports(&mut self, program: &Program<'_>, path: &str) -> Components<'a> { + let mut exports = FxHashMap::default(); + for statement in &program.body { + match statement { + Statement::ExportFromDeclaration(export) => { + if let Some(module) = self.module(&export.source.value, path) { + for specifier in &export.specifiers { + if let Some(component) = module.get(specifier.local.name().as_str()) { + exports.insert( + specifier.exported.name().to_string(), + component.clone_in(self.allocator), + ); + } + } + } + } + Statement::ExportAllDeclaration(export) if export.exported.is_none() => { + if let Some(module) = self.module(&export.source.value, path) { + for (name, component) in + module.iter().filter(|(name, _)| *name != "default") + { + exports + .entry(name.clone()) + .or_insert_with(|| component.clone_in(self.allocator)); + } + } + } + _ => {} + } + } + exports + } + + /// The components `program` imports that it extends, selects or gives a + /// `css` prop, in the arena of the file that reads them + fn imports<'b>( + &mut self, + program: &Program<'_>, + importer: &str, + css_prop: CssProp, + allocator: &'b Allocator, + ) -> Components<'b> { + let imported = imported_bindings(program, &self.option.package); + let mut used = Used { + styled: imported.styled, + css_prop: css_prop != CssProp::Off, + names: FxHashSet::default(), + }; + used.visit_program(program); + crate::style_values::selectors(program, &mut |expression| { + if let Expression::Identifier(identifier) = expression { + used.names.insert(identifier.name.to_string()); + } + }); + used.names.extend( + Exports::scan(program) + .bindings() + .map(|(binding, _)| binding.clone()), + ); + let mut names: Vec = used.names.into_iter().collect(); + names.sort_unstable(); + let mut components = FxHashMap::default(); + for name in names { + let found = match name.split_once('.') { + Some((namespace, member)) => imported + .bindings + .get(namespace) + .filter(|(_, export)| export == NAMESPACE) + .map(|(source, _)| (source, member)), + None => imported + .bindings + .get(&name) + .map(|(source, export)| (source, export.as_str())), + }; + if let Some((source, export)) = found + && let Some(module) = self.module(source, importer) + && let Some(component) = module.get(export) + { + components.insert(name, component.clone_in(allocator)); + } + } + components + } +} + +/// What a namespace import is imported as +const NAMESPACE: &str = "*"; + +/// What a program imports +struct Imported { + /// Each binding another module's export, with that module and the export + bindings: FxHashMap, + /// The bindings of the package's `styled` + styled: FxHashSet, +} + +fn imported_bindings(program: &Program<'_>, package: &str) -> Imported { + let mut imported = Imported { + bindings: FxHashMap::default(), + styled: FxHashSet::default(), + }; + for statement in &program.body { + let Statement::ImportDeclaration(import) = statement else { + continue; + }; + if import.import_kind.is_type() { + continue; + } + let source = import.source.value.as_str(); + let from_package = source.starts_with(package); + for specifier in import.specifiers.iter().flatten() { + let (local, export) = match specifier { + ImportDeclarationSpecifier::ImportSpecifier(specifier) + if !specifier.import_kind.is_type() => + { + ( + specifier.local.name.to_string(), + specifier.imported.name().to_string(), + ) + } + ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => { + (specifier.local.name.to_string(), "default".to_string()) + } + ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => { + (specifier.local.name.to_string(), NAMESPACE.to_string()) + } + ImportDeclarationSpecifier::ImportSpecifier(_) => continue, + }; + if !from_package { + imported + .bindings + .insert(local, (source.to_string(), export)); + } else if export == "styled" { + imported.styled.insert(local); + } + } + } + imported +} + +/// A binding or a member of one, as the program writes it +fn written_name(expression: &Expression<'_>) -> Option { + match unwrap_syntax_only(expression) { + Expression::Identifier(identifier) => Some(identifier.name.to_string()), + Expression::StaticMemberExpression(member) => match &member.object { + Expression::Identifier(object) => { + Some(format!("{}.{}", object.name, member.property.name)) + } + _ => None, + }, + _ => None, + } +} + +/// The bindings a program composes, takes the `css` prop of, or selects +struct Used { + styled: FxHashSet, + css_prop: bool, + names: FxHashSet, +} + +impl<'a> Visit<'a> for Used { + fn visit_call_expression(&mut self, call: &CallExpression<'a>) { + let name = match unwrap_syntax_only(&call.callee) { + Expression::Identifier(callee) if self.styled.contains(callee.name.as_str()) => call + .arguments + .first() + .and_then(|first| first.as_expression()) + .and_then(written_name), + Expression::StaticMemberExpression(member) + if member.property.name == "withComponent" => + { + written_name(&member.object) + } + _ => None, + }; + self.names.extend(name); + walk::walk_call_expression(self, call); + } + + fn visit_jsx_opening_element(&mut self, element: &JSXOpeningElement<'a>) { + if self.css_prop + && let JSXElementName::IdentifierReference(name) = &element.name + && element.attributes.iter().any(|attribute| { + matches!(attribute, JSXAttributeItem::Attribute(attribute) + if matches!(&attribute.name, JSXAttributeName::Identifier(attribute) if attribute.name == "css")) + }) + { + self.names.insert(name.name.to_string()); + } + walk::walk_jsx_opening_element(self, element); + } +} + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests { + use std::collections::{BTreeSet, HashMap}; + + use insta::assert_debug_snapshot; + use serial_test::serial; + + use super::*; + use crate::{ExtractOutput, ImportAlias, ResolvedModule, extract_with_modules}; + + fn option() -> ExtractOption { + ExtractOption { + single_css: true, + import_aliases: HashMap::from([ + ("@emotion/react".to_string(), ImportAlias::NamedToNamed), + ( + "@emotion/styled".to_string(), + ImportAlias::DefaultToNamed("styled".to_string()), + ), + ]), + ..ExtractOption::default() + } + } + + fn extract_in(files: &[(&str, &str)], entry: &str) -> Result { + let modules: Vec<(String, String)> = files + .iter() + .map(|(path, code)| ((*path).to_string(), (*code).to_string())) + .collect(); + let resolver = move |specifier: &str, importer: &str| { + let directory = importer + .rsplit_once('/') + .map_or("", |(directory, _)| directory); + let path = format!("{directory}/{}", specifier.trim_start_matches("./")); + modules + .iter() + .find(|(file, _)| { + ["", ".ts", ".tsx", ".css.ts", "/index.ts"] + .iter() + .any(|extension| format!("{path}{extension}") == *file) + }) + .map(|(file, code)| ResolvedModule { + path: file.clone(), + code: code.clone(), + }) + }; + let code = files.iter().find(|(path, _)| *path == entry).unwrap().1; + extract_with_modules(entry, code, option(), false, &resolver).map_err(|e| e.to_string()) + } + + fn reset() { + css::class_map::reset_class_map(); + css::file_map::reset_file_map(); + } + + fn render(files: &[(&str, &str)], entry: &str) -> String { + reset(); + match extract_in(files, entry) { + Ok(output) => { + let styles: BTreeSet = output + .styles + .iter() + .map(|style| format!("{style:?}")) + .collect(); + format!("{}\n{styles:#?}\n{:?}", output.code, output.dependencies) + } + Err(error) => error, + } + } + + const STYLED: &str = "import styled from '@emotion/styled';\n"; + const BASE_BODY: &str = r"export const Base = styled.div`color: red; padding: 4px;`; +export const Dyn = styled.button`font-size: ${(p) => (p.big ? '20px' : '10px')};`; +export const Pick = styled.p.withConfig({ shouldForwardProp: (prop) => prop !== 'tone' })`color: ${(p) => p.tone};`; +export default styled.span`margin: 1px;`; +"; + + fn base_module() -> String { + format!("{STYLED}{BASE_BODY}") + } + + fn consumers<'a>(base: &'a str, bodies: &[&'a str]) -> Vec { + bodies + .iter() + .map(|body| { + render( + &[ + ("/src/base.ts", base), + ( + "/src/app.tsx", + &format!( + "{STYLED}import Span, {{ Base, Dyn, Pick }} from './base';\n{body}" + ), + ), + ], + "/src/app.tsx", + ) + }) + .collect() + } + + const COMPOSITIONS: &[&str] = &[ + "export const A = styled(Base)`color: blue;`;", + "export const A = styled(Base)({ color: 'blue', padding: 8 });", + "export const A = styled(Base).attrs({ role: 'note' })`color: blue;`;", + "export const A = styled(Base).attrs((p) => ({ title: p.title }))`color: blue;`;", + "export const A = styled(Dyn)`color: blue;`;", + "export const A = styled(Pick)`margin: 0;`;", + "export const A = styled(Span)`color: blue;`;", + "export const A = styled(Base)`color: blue;`;\nexport const B = styled(A)`padding: 0;`;", + "export const W = Dyn.withComponent('section');\nexport const V = Base.withComponent(Span);", + "export const A = styled(Base)`color: blue;`;\nexport const C = () => ;", + ]; + + #[test] + #[serial] + fn imported_components_compose_like_local_ones() { + assert_debug_snapshot!(consumers(&base_module(), COMPOSITIONS)); + } + + const DEFINITIONS: [(&str, &str); 4] = [ + ( + "Base", + "export const Base = styled.div`color: red; padding: 4px;`;\n", + ), + ( + "Dyn", + "export const Dyn = styled.button`font-size: ${(p) => (p.big ? '20px' : '10px')};`;\n", + ), + ( + "Pick", + "export const Pick = styled.p.withConfig({ shouldForwardProp: (prop) => prop !== 'tone' })`color: ${(p) => p.tone};`;\n", + ), + ("Span", "export default styled.span`margin: 1px;`;\n"), + ]; + + #[test] + #[serial] + fn imported_components_give_no_style_local_ones_do_not() { + let pragma = "/** @jsxImportSource @emotion/react */\n"; + let css_props = [ + "export const C = () => ;", + "export const C = () => ;", + ]; + for body in COMPOSITIONS.iter().chain(&css_props) { + let header = if body.contains("css=") { pragma } else { "" }; + let used: Vec<&(&str, &str)> = DEFINITIONS + .iter() + .filter(|(name, _)| body.contains(name)) + .collect(); + let module: String = used.iter().map(|(_, definition)| *definition).collect(); + let named: Vec<&str> = used + .iter() + .map(|(name, _)| *name) + .filter(|name| *name != "Span") + .collect(); + let default = if used.iter().any(|(name, _)| *name == "Span") { + "Span, " + } else { + "" + }; + reset(); + let imported = extract_in( + &[ + ("/src/base.ts", &format!("{STYLED}{module}")), + ( + "/src/app.tsx", + &format!( + "{header}{STYLED}import {default}{{ {} }} from './base';\n{body}", + named.join(", ") + ), + ), + ], + "/src/app.tsx", + ) + .unwrap(); + reset(); + let local_source = format!( + "{header}{STYLED}{}\n{body}", + module.replace("export default styled.span", "const Span = styled.span") + ); + let local = extract_in(&[("/src/app.tsx", &local_source)], "/src/app.tsx").unwrap(); + assert!(imported.styles.is_subset(&local.styles), "{body}"); + assert_eq!(imported.dependencies, ["/src/base.ts"]); + } + } + #[test] + #[serial] + fn imported_components_give_css_props_what_local_ones_give() { + let pragma = "/** @jsxImportSource @emotion/react */\n"; + let bodies = [ + "export const C = () => ;", + "export const C = () => ;", + "export const C = () => ;", + "export const C = ({ rest }) => ;", + ]; + let outputs: Vec = bodies + .iter() + .map(|body| { + render( + &[ + ("/src/base.ts", &base_module()), + ( + "/src/app.tsx", + &format!("{pragma}import {{ Base, Dyn }} from './base';\n{body}"), + ), + ], + "/src/app.tsx", + ) + }) + .collect(); + assert_debug_snapshot!(outputs); + } + + #[test] + #[serial] + fn imported_components_are_selected_by_the_class_their_module_gives() { + let bodies = [ + "export const P = styled.div`${Base} { margin: 0; } ${Dyn}:hover & { color: red; }`;", + "export const P = styled.div({ [Base]: { margin: 0 }, [`&:focus ${Dyn}`]: { color: 'red' } });", + "export const P = styled.div`${Span} + ${Pick} { margin: 0; }`;", + "export const P = styled.div`${Base} { margin: 0; }`;\nexport const Q = styled(Base)`color: blue;`;", + ]; + assert_debug_snapshot!(consumers(&base_module(), &bodies)); + + let mid = format!( + "{STYLED}import {{ Base }} from './base';\nexport const Mid = styled(Base)`width: 1px;`;\nconst Local = styled(Mid)`height: 1px;`;\nexport {{ Local as Renamed }};\nexport default Mid;" + ); + let app = format!( + "{STYLED}import {{ Base }} from './base';\nimport Top, {{ Mid, Renamed }} from './mid';\nexport const P = styled.div`${{Base}} {{ margin: 0; }} ${{Mid}} {{ color: red; }} ${{Renamed}} {{ top: 0; }} ${{Top}} {{ left: 0; }}`;" + ); + assert_debug_snapshot!(render( + &[ + ("/src/base.ts", &base_module()), + ("/src/mid.ts", &mid), + ("/src/app.tsx", &app), + ], + "/src/app.tsx", + )); + } + + #[test] + #[serial] + fn every_file_computes_the_class_of_a_component_from_its_module_and_name() { + let files = [ + ("/src/base.ts", base_module()), + ( + "/src/app.tsx", + format!( + "{STYLED}import {{ Dyn }} from './base';\nexport const P = styled.div`${{Dyn}} {{ margin: 0; }}`;" + ), + ), + ]; + let files: Vec<(&str, &str)> = files.iter().map(|(p, c)| (*p, c.as_str())).collect(); + reset(); + let _ = css::file_map::get_file_num_by_filename("/src/app.tsx"); + let app = extract_in(&files, "/src/app.tsx").unwrap(); + let base = extract_in(&files, "/src/base.ts").unwrap(); + let marker = |text: &str| { + text.split(|c: char| !(c.is_alphanumeric() || c == '-' || c == '_')) + .find(|word| word.contains("--")) + .unwrap() + .to_string() + }; + let selector = app + .styles + .iter() + .find_map(|style| { + format!("{style:?}") + .split_once("& .") + .map(|(_, rest)| marker(rest)) + }) + .unwrap(); + assert!(base.code.contains(&selector), "{selector}: {}", base.code); + } + + #[test] + #[serial] + fn re_exports_and_namespaces_reach_the_defining_module() { + let reexports: &[(&str, &str)] = &[ + ("/src/base.ts", &base_module_static()), + ( + "/src/star.ts", + "export * from './base';\nexport * as everything from './base';\nexport const Base = 1;", + ), + ( + "/src/named.ts", + "export { Base as Renamed, Dyn, Missing } from './base';\nexport { default as Span } from './base';", + ), + ( + "/src/forward.ts", + "import { Dyn } from './base';\nexport { Dyn };\nexport type { Dyn as DynType };", + ), + ( + "/src/chain.ts", + "export * from './star';\nexport * from './named';", + ), + ]; + let bodies = [ + "import { Dyn } from './star';\nexport const A = styled(Dyn)`color: blue;`;", + "import { Base } from './star';\nexport const A = styled(Base)`color: blue;`;", + "import { Renamed, Span } from './named';\nexport const A = styled(Renamed)`color: blue;`;\nexport const B = styled(Span)`color: blue;`;", + "import { Dyn } from './forward';\nexport const A = styled(Dyn)`color: blue;`;", + "import { Renamed, Dyn } from './chain';\nexport const A = styled(Renamed)`color: blue;`;\nexport const B = styled(Dyn)`color: blue;`;", + "import * as UI from './named';\nexport const A = styled(UI.Renamed)`color: blue;`;\nexport const W = UI.Dyn.withComponent('a');\nexport const N = styled(UI.Nope)`color: red;`;", + "import { Base } from './named';\nexport const A = styled(Base)`color: blue;`;", + "import { Dyn, Span } from './star';\nexport const A = styled(Dyn)`color: blue;`;\nexport const B = styled(Span)`color: blue;`;", + ]; + let outputs: Vec = bodies + .iter() + .map(|body| { + let app = format!("{STYLED}{body}"); + let mut files = reexports.to_vec(); + files.push(("/src/app.tsx", &app)); + render(&files, "/src/app.tsx") + }) + .collect(); + assert_debug_snapshot!(outputs); + } + + fn base_module_static() -> String { + base_module() + } + + #[test] + #[serial] + fn imported_components_the_build_cannot_read_stay_runtime_components() { + let modules: &[(&str, &str)] = &[ + ( + "/src/link.ts", + "import styled from '@emotion/styled';\nimport Link from 'next/link';\nexport const L = styled(Link)`color: red;`;\nexport const M = styled(L)`margin: 0;`;", + ), + ( + "/src/free.ts", + "import styled from '@emotion/styled';\nconst handler = () => 1;\nexport const H = styled.div.attrs({ onClick: handler })`color: red;`;\nexport const C = styled.div({ color: window.dark ? 'red' : 'blue' });\nexport const E = styled.div({ display: window.mode });\nexport const T = styled.div({ color: ['red', 'blue'] });\nexport const U = styled.div({ color: (p: Props) => p.tone });\ntype Props = { tone: string };", + ), + ( + "/src/plain.ts", + "export const Base = 5;\nexport function fn() {}\nexport const { a, b } = { a: 1, b: 2 };\nexport default function () {}\nexport type { Props };\nexport { type Props as P };", + ), + ( + "/src/bad.ts", + "import styled from '@emotion/styled';\nexport const Good = styled.div`color: red;`;\nexport const Bad = styled.div;", + ), + ( + "/src/cycle-a.ts", + "import styled from '@emotion/styled';\nimport { B } from './cycle-b';\nexport const A = styled(B)`color: red;`;", + ), + ( + "/src/cycle-b.ts", + "import styled from '@emotion/styled';\nimport { A } from './cycle-a';\nexport const B = styled(A)`color: blue;`;", + ), + ( + "/src/theme.css.ts", + "import { style } from '@vanilla-extract/css';\nexport const base = style({ color: 'red' });", + ), + ("/src/data.json", "{}"), + ("/src/broken.ts", "export const = ;"), + ( + "/src/types.ts", + "import styled from '@emotion/styled';\nexport const Base = styled.div`color: red;`;", + ), + ]; + let bodies = [ + "import { L, M } from './link';\nexport const A = styled(L)`color: blue;`;\nexport const B = styled(M)`color: blue;`;", + "import { H, C, E, T, U } from './free';\nexport const A = styled(H)`color: blue;`;\nexport const B = styled(C)`color: blue;`;\nexport const D = styled(E)`color: blue;`;\nexport const F = styled(T)`color: blue;`;\nexport const G = styled(U)`color: blue;`;", + "import Anonymous, { Base, fn, a, P } from './plain';\nexport const A = styled(Base)`color: blue;`;\nexport const B = styled(Anonymous)`color: blue;`;\nexport const C = styled(fn)`color: blue;`;\nexport const D = styled(a)`color: blue;`;\nexport const E = styled(P)`color: blue;`;", + "import { Good, Bad } from './bad';\nexport const A = styled(Good)`color: blue;`;\nexport const B = styled.div`${Good} { color: red; }`;", + "import { A } from './cycle-a';\nexport const App = styled(A)`color: blue;`;", + "import { base } from './theme.css';\nimport { Data } from './data.json';\nimport { Broken } from './broken';\nimport { Missing } from './missing';\nimport { Nope } from './types';\nexport const A = styled(Data)`color: blue;`;\nexport const B = styled(Broken)`color: blue;`;\nexport const C = styled(Missing)`color: blue;`;\nexport const D = styled(Nope)`color: blue;`;\nexport const E = styled(base)`color: blue;`;", + "import type { Base } from './types';\nimport { type Base as Other } from './types';\nexport const A = styled(Base)`color: blue;`;\nexport const B = styled(Other)`color: blue;`;", + "import { Base } from './types';\nexport const A = styled(Base)`color: blue;`;", + "import * as types from './types';\nexport const A = styled(types.deep.Base)`color: blue;`;\nexport const B = styled(types['Base'])`color: blue;`;", + ]; + let outputs: Vec = bodies + .iter() + .map(|body| { + let app = format!("{STYLED}{body}"); + let mut files = modules.to_vec(); + files.push(("/src/app.tsx", &app)); + render(&files, "/src/app.tsx") + }) + .collect(); + assert_debug_snapshot!(outputs); + } + + #[test] + #[serial] + fn exports_name_what_a_module_gives_other_files() { + let code = "import styled from '@emotion/styled';\nexport const A = 1, { b } = {};\nconst C = 2;\nexport { C, C as D };\nexport default C;\nexport function f() {}\nexport type { C as T };"; + let allocator = Allocator::default(); + let program = Parser::new(&allocator, code, SourceType::ts()) + .parse() + .program; + let exports = Exports::scan(&program); + assert_eq!(exports.count(), 4); + assert_eq!(exports.of("C"), ["C", "D", "default"]); + assert_eq!(exports.of("A"), ["A"]); + assert_eq!(exports.of("missing"), [] as [String; 0]); + assert_eq!(exports.index("D"), 2); + assert!(!exports.has_anonymous_default()); + let code = "export default styled.div`color: red;`;"; + let program = Parser::new(&allocator, code, SourceType::ts()) + .parse() + .program; + let exports = Exports::scan(&program); + assert!(exports.has_anonymous_default()); + assert_eq!(exports.index("default"), 0); + } +} diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index ac1dcbde6..a302c4410 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -10,6 +10,7 @@ mod gen_class_name; mod gen_style; mod import_alias_visit; mod imported_constants; +mod imported_styled; mod module_loader; mod mutations; mod prop_modify_utils; @@ -81,8 +82,8 @@ pub enum ExtractStyleProp<'a> { }, } -impl<'a> ExtractStyleProp<'a> { - pub fn clone_in(&self, alloc: &'a Allocator) -> Self { +impl ExtractStyleProp<'_> { + pub fn clone_in<'b>(&self, alloc: &'b Allocator) -> ExtractStyleProp<'b> { match self { ExtractStyleProp::Static(v) => ExtractStyleProp::Static(v.clone()), ExtractStyleProp::StaticArray(arr) => { @@ -440,6 +441,19 @@ fn extract_source( imported_constants::Inlined::default() }; dependencies.extend(inlined.dependencies); + let imported = if processed_code.is_none() { + imported_styled::read( + &oxc_ast::builder::AstBuilder::new(&allocator), + &program, + filename, + &option, + resolver, + css_prop, + ) + } else { + imported_styled::Reading::default() + }; + dependencies.extend(imported.dependencies); let mut visitor = DevupVisitor::new( &allocator, filename, @@ -452,6 +466,7 @@ fn extract_source( visitor.unknown_bindings(&inlined.unknown); visitor.changed_bindings(inlined.changed.clone()); visitor.takes_css_prop(css_prop); + visitor.import_styled(imported.components); visitor.visit_program(&mut program); if !has_relevant_import && alias_edits.is_empty() && !visitor.compiled_css_prop { // No element took the `css` prop the text seemed to give diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives-2.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives-2.snap new file mode 100644 index 000000000..273862efa --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives-2.snap @@ -0,0 +1,5 @@ +--- +source: libs/extractor/src/imported_styled.rs +expression: "render(&[(\"/src/base.ts\", &base_module()), (\"/src/mid.ts\", &mid),\n(\"/src/app.tsx\", &app),], \"/src/app.tsx\",)" +--- +"import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Base } from \"./base\";\nimport Top, { Mid, Renamed } from \"./mid\";\nexport const P = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c--a j k l m\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"red\\\", level: 0, selector: Some(Selector(\\\"& .b--a\\\")), style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"left\\\", value: \\\"0\\\", level: 0, selector: Some(Selector(\\\"& .b--a\\\")), style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"0\\\", level: 0, selector: Some(Selector(\\\"& .a--a\\\")), style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"top\\\", value: \\\"0\\\", level: 0, selector: Some(Selector(\\\"& .b--b\\\")), style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/mid.ts\"]" diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives.snap new file mode 100644 index 000000000..835ee6a44 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_are_selected_by_the_class_their_module_gives.snap @@ -0,0 +1,10 @@ +--- +source: libs/extractor/src/imported_styled.rs +expression: "consumers(&base_module(), &bodies)" +--- +[ + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const P = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a h i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"red\\\", level: 0, selector: Some(Selector(\\\".a--b:hover &\\\")), style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"0\\\", level: 0, selector: Some(Selector(\\\"& .a--a\\\")), style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "/src/app.tsx:3:55: `styled()` cannot use `[`&:focus ${Dyn}`]` at build time: its styles must be an object literal or a constant object, or be computed from constants", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const P = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a h\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"0\\\", level: 0, selector: Some(Selector(\\\"& .a--d + .a--c\\\")), style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const P = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a h\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Q = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--b a--a i a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"0\\\", level: 0, selector: Some(Selector(\\\"& .a--a\\\")), style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", +] diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_compose_like_local_ones.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_compose_like_local_ones.snap new file mode 100644 index 000000000..acd88644e --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_compose_like_local_ones.snap @@ -0,0 +1,16 @@ +--- +source: libs/extractor/src/imported_styled.rs +expression: "consumers(&base_module(), COMPOSITIONS)" +--- +[ + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"8px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)(((__devupContext) => ({\n\t...__devupContext,\n\tclassName: [__devupContext.className, __devupProps.className].filter(Boolean).join(\" \") || undefined\n}))(((__devupContext) => ((__devupAttrs) => ({\n\t...__devupContext,\n\t...__devupAttrs,\n\tclassName: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined,\n\tstyle: {\n\t\t...__devupContext.style,\n\t\t...__devupAttrs.style\n\t}\n}))({\n\t...{},\n\t...((p) => ({ title: p.title }))(__devupContext)\n}))({\n\t...__devupProps,\n\tclassName: undefined\n}))))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--b h c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"font-size\\\", level: 0, identifier: \\\"(p=>p.big?`20px`:`10px`)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--c h e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (p=>p.tone)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"color\\\", level: 0, identifier: \\\"(p=>p.tone)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"0\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--d h g\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"1px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--b b--a a--a i h\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"0\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const W = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"section\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const V = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Span, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"font-size\\\", level: 0, identifier: \\\"(p=>p.big?`20px`:`10px`)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"red\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Span, { Base, Dyn, Pick } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = () => ;\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", +] diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_give_css_props_what_local_ones_give.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_give_css_props_what_local_ones_give.snap new file mode 100644 index 000000000..443d70f75 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_give_css_props_what_local_ones_give.snap @@ -0,0 +1,10 @@ +--- +source: libs/extractor/src/imported_styled.rs +expression: outputs +--- +[ + "import \"@devup-ui/react/devup-ui.css\";\n/** @jsxImportSource react */\nimport { Base, Dyn } from \"./base\";\nexport const C = () =>
;\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "/src/app.tsx:3:30: `css` on `` overrides styles `Base` sets, which the build orders only for a styled component rendering a tag with no attrs or props read, given no spread, `as` or `forwardedAs`: move these styles into `styled(Base)(...)`", + "import \"@devup-ui/react/devup-ui.css\";\n/** @jsxImportSource react */\nimport { Base, Dyn } from \"./base\";\nexport const C = () => ;\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\n/** @jsxImportSource react */\nimport { Base, Dyn } from \"./base\";\nexport const C = ({ rest }) => ;\n\n{\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"1px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\"]", +] diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap new file mode 100644 index 000000000..29a0e0cb7 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap @@ -0,0 +1,15 @@ +--- +source: libs/extractor/src/imported_styled.rs +expression: outputs +--- +[ + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { L, M } from \"./link\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = L, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = M, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/link.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { H, C, E, T, U } from \"./free\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = H, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = C, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const D = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = E, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const F = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = T, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const G = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = U, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/free.ts\"]", + "/src/app.tsx:3:18: `styled()` cannot use `styled(5)` at build time: it renders a tag, a component or a value naming one, with rule objects or CSS text", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Good, Bad } from \"./bad\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Good, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--b c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"red\\\", level: 0, selector: Some(Selector(\\\"& .a--b\\\")), style_order: None, layer: None })\",\n}\n[\"/src/bad.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { A } from \"./cycle-a\";\nexport const App = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = A, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/cycle-a.ts\", \"/src/cycle-b.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { base } from \"./theme.css\";\nimport { Data } from \"./data.json\";\nimport { Broken } from \"./broken\";\nimport { Missing } from \"./missing\";\nimport { Nope } from \"./types\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Data, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Broken, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Missing, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const D = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Nope, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const E = __devupForwardRef((__devupRefProps, __devupRef) => ((DevupStyled) => ({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => )(base)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/broken.ts\", \"/src/data.json\", \"/src/theme.css.ts\", \"/src/types.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport type { Base } from \"./types\";\nimport { type Base as Other } from \"./types\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Other, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/types.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Base } from \"./types\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/types.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as types from \"./types\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = types.deep.Base, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => ((DevupStyled) => ({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => )(types[\"Base\"])({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/types.ts\"]", +] diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__re_exports_and_namespaces_reach_the_defining_module.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__re_exports_and_namespaces_reach_the_defining_module.snap new file mode 100644 index 000000000..1cb3fd35d --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__re_exports_and_namespaces_reach_the_defining_module.snap @@ -0,0 +1,14 @@ +--- +source: libs/extractor/src/imported_styled.rs +expression: outputs +--- +[ + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./star\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--b h c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"font-size\\\", level: 0, identifier: \\\"(p=>p.big?`20px`:`10px`)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/star.ts\"]", + "/src/app.tsx:3:18: `styled()` cannot use `styled(1)` at build time: it renders a tag, a component or a value naming one, with rule objects or CSS text", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Renamed, Span } from \"./named\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--b a--d h g\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"margin\\\", value: \\\"1px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/named.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./forward\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--b h c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"font-size\\\", level: 0, identifier: \\\"(p=>p.big?`20px`:`10px`)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/forward.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Renamed, Dyn } from \"./chain\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--b a--b h c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"font-size\\\", level: 0, identifier: \\\"(p=>p.big?`20px`:`10px`)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/chain.ts\", \"/src/named.ts\", \"/src/star.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as UI from \"./named\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--a h a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const W = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"a\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const N = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = UI.Nope, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"font-size\\\", level: 0, identifier: \\\"(p=>p.big?`20px`:`10px`)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"red\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"padding\\\", value: \\\"4px\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/named.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Base } from \"./named\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/named.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn, Span } from \"./star\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--a a--b h c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Span, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"font-size\\\", level: 0, identifier: \\\"(p=>p.big?`20px`:`10px`)(rest)\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/base.ts\", \"/src/star.ts\"]", +] diff --git a/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap b/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap index 75f7d0d17..04056043f 100644 --- a/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap +++ b/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap @@ -5,7 +5,7 @@ expression: outputs [ ( "import { css, styled, Box } from '@devup-ui/react';\nconst myCss = css;\nconst again = myCss;\nconst S = styled;\nconst B = Box;\nexport const a = again({ color: 'red' });\nexport const s = S.div({ color: 'blue' });\nexport const b = ;", - "import { forwardRef as __devupForwardRef } from \"react\";\nexport const a = \"a-a\";\nexport const s = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const b =
;", + "import { forwardRef as __devupForwardRef } from \"react\";\nexport const a = \"a-a\";\nexport const s = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c a-b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const b =
;", ), ( "import { css } from '@devup-ui/react';\nexport function f(css) { return css; }\nexport const a = css({ color: 'red' });", diff --git a/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap b/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap index 816a032aa..cdd3c9e2c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap +++ b/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap @@ -75,5 +75,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as tokens from \"./styles\";\nimport { card, rules } from \"./styles\";\nimport { emotionClass } from \"./emotion\";\nconst make = (n: number) => ({ m: n });\nconst local = make(2);\nconst computed = make(3);\nconst name = String(\"named\");\nconst shared = {\n\trules: make(4),\n\tfixed: { p: 5 }\n};\nexport const a = \"a-e\";\nexport const b = \"a-a a-f\";\nexport const c = `named x ${`y`}`;\nexport const d = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-e\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const e = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const f = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-g\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const g = (on) => `a-e a-b ${on ? typeof on === \"string\" ? on : \"\" : \"\"}`;\nexport const h = \"a-c\";\nexport const i = \"a-e\";\nexport const k = `k ${tokens.card}`;\nexport const l = `a-d ${emotionClass}`;\nexport const n = ;\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as tokens from \"./styles\";\nimport { card, rules } from \"./styles\";\nimport { emotionClass } from \"./emotion\";\nconst make = (n: number) => ({ m: n });\nconst local = make(2);\nconst computed = make(3);\nconst name = String(\"named\");\nconst shared = {\n\trules: make(4),\n\tfixed: { p: 5 }\n};\nexport const a = \"a-e\";\nexport const b = \"a-a a-f\";\nexport const c = `named x ${`y`}`;\nexport const d = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--d a-e\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const e = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--e a-f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const f = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--f a-g\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const g = (on) => `a-e a-b ${on ? typeof on === \"string\" ? on : \"\" : \"\"}`;\nexport const h = \"a-c\";\nexport const i = \"a-e\";\nexport const k = `k ${tokens.card}`;\nexport const l = `a-d ${emotionClass}`;\nexport const n = ;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap b/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap index 66c9f653d..8b369a96f 100644 --- a/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap +++ b/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap @@ -99,5 +99,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY } from \"./tokens\";\nconst make = (n: number) => ({ m: n });\nconst KEY = [\"co\", \"lor\"].join(\"\");\nfunction darken(amount: number, color: string) {\n\treturn color === PRIMARY ? \"#112233\" : color;\n}\nconst card = make(2);\nconst hover = make(3);\nconst parts = [make(4), { p: 1 }];\nexport const a =
1} className=\"a-c\" />;\nexport const b =
;\nexport const c =
;\nexport const d =
;\nexport const e = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-f a-e a-g\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const f = \"a-h a-a\";\nexport const g = \"a-i\";\nexport const h =
;\nfunction local() {\n\treturn { p: 7 };\n}\nexport const i =
;\nexport const j = (register) => ((__devupSpread0) =>
)(register(\"email\"));\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY } from \"./tokens\";\nconst make = (n: number) => ({ m: n });\nconst KEY = [\"co\", \"lor\"].join(\"\");\nfunction darken(amount: number, color: string) {\n\treturn color === PRIMARY ? \"#112233\" : color;\n}\nconst card = make(2);\nconst hover = make(3);\nconst parts = [make(4), { p: 1 }];\nexport const a =
1} className=\"a-c\" />;\nexport const b =
;\nexport const c =
;\nexport const d =
;\nexport const e = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--e a-f a-e a-g\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const f = \"a-h a-a\";\nexport const g = \"a-i\";\nexport const h =
;\nfunction local() {\n\treturn { p: 7 };\n}\nexport const i =
;\nexport const j = (register) => ((__devupSpread0) =>
)(register(\"email\"));\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap b/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap index 98a5dd834..d4de79e93 100644 --- a/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap +++ b/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap @@ -117,5 +117,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const a = `a ${cond ? \"b\" : \"c\"} ${cond ? \"d\" : \"\"}`;\nexport const b = `${flag ? \"e\" : \"f\"} ${flag ? \"\" : \"g\"}`;\nexport const c = `c ${cond ? other : \"\"} ${flag ? \"x\" : \"\"}`;\nexport const d = `i ${cond ? \"h\" : \"\"}`;\nexport const e = `${flag ? other : \"\"} ${flag ? \"c\" : \"b\"}`;\nexport const h = `${cond ? typeof cond === \"string\" ? cond : \"\" : \"\"} ${cond ? \"c\" : \"b\"}`;\nexport const i = \"c\";\nexport const j = `${value != null ? typeof value === \"string\" ? value : \"\" : \"\"} ${value != null ? \"c\" : \"b\"}`;\nexport const K = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[`${base} ${`j ${cond ? \"b\" : \"\"}`}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const a = `a ${cond ? \"b\" : \"c\"} ${cond ? \"d\" : \"\"}`;\nexport const b = `${flag ? \"e\" : \"f\"} ${flag ? \"\" : \"g\"}`;\nexport const c = `c ${cond ? other : \"\"} ${flag ? \"x\" : \"\"}`;\nexport const d = `i ${cond ? \"h\" : \"\"}`;\nexport const e = `${flag ? other : \"\"} ${flag ? \"c\" : \"b\"}`;\nexport const h = `${cond ? typeof cond === \"string\" ? cond : \"\" : \"\"} ${cond ? \"c\" : \"b\"}`;\nexport const i = \"c\";\nexport const j = `${value != null ? typeof value === \"string\" ? value : \"\" : \"\"} ${value != null ? \"c\" : \"b\"}`;\nexport const K = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[`a--a ${base} ${`j ${cond ? \"b\" : \"\"}`}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap b/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap index a06c4cea3..39b76933b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap +++ b/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap @@ -305,6 +305,6 @@ Ok( }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { space, Size, Local, focus } from \"./tokens\";\nconst base = {\n\tp: 4,\n\tcolor: \"red\",\n\tcolor: \"blue\"\n};\nconst hover = { opacity: .5 };\nconst merged = {\n\t...base,\n\tm: 1\n};\nconst partial = {\n\t...unknown,\n\tm: 1\n};\nconst proto = {\n\t__proto__: null,\n\ta: 1\n};\nconst getter = { get a() {\n\treturn 1;\n} };\nexport const withGetter =
;\nconst NONE = null;\nconst MEDIA = \"@media (min-width: 1px)\";\nconst keyed = { [MEDIA]: { color: \"green\" } };\nconst widths = [\n\t10,\n\tnull,\n\t20\n];\nconst more = [...widths, 30];\nconst bad = [...unknown];\nenum Level {\n\tLow = 1,\n\tHigh,\n\tName = \"n\",\n\tComputed = Math.max(1, 2),\n\tLast = 9\n}\ndeclare enum Ambient {\n\tX = 1\n}\nconst sx = { color: \"red\" };\nexport const a = \"a-b a-c\";\nexport const b = \"a-b a-d a-e a-f\";\nexport const c = \"a-b a-c a-g\";\nexport const d = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const e = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-b a-c a-e\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const f = (Comp, handler) => ;\nexport const g =
;\nexport const h = \"a-b a-c\";\nexport const i = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const j = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"button\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const k = { \"x\": \"a-s\" };\n;\nexport const l = \"a-t\";\nexport const m = \"a-u a-v a-y a-z\";\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { space, Size, Local, focus } from \"./tokens\";\nconst base = {\n\tp: 4,\n\tcolor: \"red\",\n\tcolor: \"blue\"\n};\nconst hover = { opacity: .5 };\nconst merged = {\n\t...base,\n\tm: 1\n};\nconst partial = {\n\t...unknown,\n\tm: 1\n};\nconst proto = {\n\t__proto__: null,\n\ta: 1\n};\nconst getter = { get a() {\n\treturn 1;\n} };\nexport const withGetter =
;\nconst NONE = null;\nconst MEDIA = \"@media (min-width: 1px)\";\nconst keyed = { [MEDIA]: { color: \"green\" } };\nconst widths = [\n\t10,\n\tnull,\n\t20\n];\nconst more = [...widths, 30];\nconst bad = [...unknown];\nenum Level {\n\tLow = 1,\n\tHigh,\n\tName = \"n\",\n\tComputed = Math.max(1, 2),\n\tLast = 9\n}\ndeclare enum Ambient {\n\tX = 1\n}\nconst sx = { color: \"red\" };\nexport const a = \"a-b a-c\";\nexport const b = \"a-b a-d a-e a-f\";\nexport const c = \"a-b a-c a-g\";\nexport const d = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--d a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const e = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--e a-b a-c a-e\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const f = (Comp, handler) => ;\nexport const g =
;\nexport const h = \"a-b a-c\";\nexport const i = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--i a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const j = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--j a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"button\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const k = { \"x\": \"a-s\" };\n;\nexport const l = \"a-t\";\nexport const m = \"a-u a-v a-y a-z\";\n", }, ) diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap index 4ab5b5426..fe0d553e0 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap @@ -45,5 +45,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const G = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const H = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const I = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const j = \"d\";\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const G = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const H = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const I = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const j = \"d\";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap index b704f6dea..cd2f3c04b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap @@ -166,5 +166,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Global } from \"@devup-ui/react/compat\";\nexport const a = \"a b c d\";\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const D = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"f g h i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const E = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[`j ${cond ? \"k\" : \"\"}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const F = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"l\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const spin = \"m\";\nexport const App = () => <>;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Global } from \"@devup-ui/react/compat\";\nexport const a = \"a b c d\";\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b e\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const D = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--d f g h i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const E = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[`a--e ${`j ${cond ? \"k\" : \"\"}`}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const F = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--f l\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const spin = \"m\";\nexport const App = () => <>;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap index 7193e2389..27613d7d3 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_component_selectors.snap @@ -3,10 +3,10 @@ source: libs/extractor/src/lib.rs expression: outputs --- [ - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c d\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => ;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: Some(Selector(\"&:hover .a,& .a + a\")), style_order: None, layer: None })}", - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c d\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: Some(Selector(\"&:hover .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None })}", - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nconst Wide = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a c b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => <>;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"width\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}", - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = () =>
;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}", - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst notStyled = \"x\";\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& x\")), style_order: None, layer: None })}", - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& a,&:is(b,c) d\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b b c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => ;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: Some(Selector(\"&:hover .a--c,& .a--c + a\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a--c\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a b c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: Some(Selector(\"&:hover .a--b\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a--b\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nconst Wide = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c b a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const App = () => <>;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"width\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a--c\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst Child = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = () =>
;\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& .a--c\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst notStyled = \"x\";\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& x\")), style_order: None, layer: None })}", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const Parent = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: Some(Selector(\"& a,&:is(b,c) d\")), style_order: None, layer: None })}", ] diff --git a/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap b/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap index a590c2c1b..3326bad41 100644 --- a/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap +++ b/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap @@ -475,5 +475,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY, SIZE, NEG, TEMPLATE, colors, ALIAS, MUTABLE, DYNAMIC, renamed, alsoMutable, reBase, notThere, gone, value, ns, NEG_STRING, TEMPLATE_EXPRESSION, SPACED, NOT_OBJECT, helper, looped } from \"./tokens\";\nimport orange from \"./tokens\";\nimport * as tokens from \"./tokens\";\nimport named from \"./named\";\nimport { handler } from \"./handler\";\nimport cjs, { COMPILED, FROM_REQUIRE, DESTRUCTURED, MUTATED, DYNAMIC } from \"./cjs-tokens\";\nimport cjsObject, { OBJ, OVERRIDDEN, LATER } from \"./cjs-object\";\nimport twelve from \"./cjs-value\";\nimport twice from \"./cjs-twice\";\nimport marker, { named as markerNamed } from \"./cjs-marker\";\nimport { unused } from \"./unused\";\nimport missing from \"./missing\";\nexport const a =
;\nexport const b = \"a\";\nexport const c =
;\nexport const d = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const e = (PRIMARY) =>
;\nexport const f = {\n\tPRIMARY,\n\tunused,\n\tSIZE: tokens.SIZE\n};\nexport const g =
;\nexport const h = ;\nexport const i = Devup[\"css\"]({ color: SIZE });\nexport const l =
;\nexport const j =
;\nexport const k = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"ba\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY, SIZE, NEG, TEMPLATE, colors, ALIAS, MUTABLE, DYNAMIC, renamed, alsoMutable, reBase, notThere, gone, value, ns, NEG_STRING, TEMPLATE_EXPRESSION, SPACED, NOT_OBJECT, helper, looped } from \"./tokens\";\nimport orange from \"./tokens\";\nimport * as tokens from \"./tokens\";\nimport named from \"./named\";\nimport { handler } from \"./handler\";\nimport cjs, { COMPILED, FROM_REQUIRE, DESTRUCTURED, MUTATED, DYNAMIC } from \"./cjs-tokens\";\nimport cjsObject, { OBJ, OVERRIDDEN, LATER } from \"./cjs-object\";\nimport twelve from \"./cjs-value\";\nimport twice from \"./cjs-twice\";\nimport marker, { named as markerNamed } from \"./cjs-marker\";\nimport { unused } from \"./unused\";\nimport missing from \"./missing\";\nexport const a =
;\nexport const b = \"a\";\nexport const c =
;\nexport const d = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--d a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const e = (PRIMARY) =>
;\nexport const f = {\n\tPRIMARY,\n\tunused,\n\tSIZE: tokens.SIZE\n};\nexport const g =
;\nexport const h = ;\nexport const i = Devup[\"css\"]({ color: SIZE });\nexport const l =
;\nexport const j =
;\nexport const k = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--k ba\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap b/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap index 17f301dd0..a74044307 100644 --- a/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap +++ b/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap @@ -9,10 +9,10 @@ expression: outputs ), ( "import Devup from '@devup-ui/react';\nexport const S = Devup.styled.div({ color: 'red' });\nexport const T = Devup.styled('span', { color: 'blue' });\nexport const U = Devup.styled.p.attrs({ role: 'note' })({ m: 1 });\nexport const g = ;", - "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Devup from \"@devup-ui/react\";\nexport const S = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const T = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const U = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const g = ;\n", + "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport Devup from \"@devup-ui/react\";\nexport const S = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a a-a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const T = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b a-b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const U = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const g = ;\n", ), ( "import * as Devup from '@devup-ui/react';\nimport { styled } from '@devup-ui/react';\nexport const S = styled.div({ p: 1 });\nexport const o = other.styled.div({ p: 1 });\nexport const m = ;", - "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nexport const S = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const o = other.styled.div({ p: 1 });\nexport const m = ;\n", + "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as Devup from \"@devup-ui/react\";\nexport const S = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a a-a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const o = other.styled.div({ p: 1 });\nexport const m = ;\n", ), ] diff --git a/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap b/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap index 58328eee6..714fc0e47 100644 --- a/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap +++ b/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap @@ -141,5 +141,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Global } from \"@devup-ui/react/compat\";\nconst fadeIn = \"a-a\";\nconst slide = \"a-b\";\nconst card = \"a-c\";\nexport const a = \"a-d a-e\";\nexport const b = \"a-c a-f\";\nexport const c = \"a-g\";\nexport const d =
;\nexport const E = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n;\nexport const g = ;\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Global } from \"@devup-ui/react/compat\";\nconst fadeIn = \"a-a\";\nconst slide = \"a-b\";\nconst card = \"a-c\";\nexport const a = \"a-d a-e\";\nexport const b = \"a-c a-f\";\nexport const c = \"a-g\";\nexport const d =
;\nexport const E = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a a-i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n;\nexport const g = ;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap b/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap index 94b75818f..7b308a82c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap @@ -55,5 +55,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const D = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const E = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"input\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />)(((__devupContext) => ({\n\t...__devupContext,\n\tclassName: [__devupContext.className, __devupProps.className].filter(Boolean).join(\" \") || undefined\n}))(((__devupContext) => ((__devupAttrs) => ({\n\t...__devupContext,\n\t...__devupAttrs,\n\tclassName: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined,\n\tstyle: {\n\t\t...__devupContext.style,\n\t\t...__devupAttrs.style\n\t}\n}))({\n\t...{},\n\t...typeof extra === \"function\" ? extra(__devupContext) : extra\n}))(((__devupContext) => ((__devupAttrs) => ({\n\t...__devupContext,\n\t...__devupAttrs,\n\tclassName: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined,\n\tstyle: {\n\t\t...__devupContext.style,\n\t\t...__devupAttrs.style\n\t}\n}))({\n\t...{},\n\t...((props) => ({ size: props.small ? 5 : 10 }))(__devupContext)\n}))({\n\t...__devupProps,\n\tclassName: undefined\n})))))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const G = other.div.attrs({ role: \"note\" })({ margin: 1 });\nexport const I = factory.attrs({ id: \"i\" })({ margin: 3 });\nexport const J = other(Base).attrs({ id: \"j\" })({ margin: 4 });\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--a a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b b\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--c c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n}))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const D = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const E = __devupForwardRef((__devupRefProps, __devupRef) => ((__devupProps) => (({ style, className, as: DevupAs = \"input\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--e e\", className].filter(Boolean).join(\" \")} style={style} />)(((__devupContext) => ({\n\t...__devupContext,\n\tclassName: [__devupContext.className, __devupProps.className].filter(Boolean).join(\" \") || undefined\n}))(((__devupContext) => ((__devupAttrs) => ({\n\t...__devupContext,\n\t...__devupAttrs,\n\tclassName: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined,\n\tstyle: {\n\t\t...__devupContext.style,\n\t\t...__devupAttrs.style\n\t}\n}))({\n\t...{},\n\t...typeof extra === \"function\" ? extra(__devupContext) : extra\n}))(((__devupContext) => ((__devupAttrs) => ({\n\t...__devupContext,\n\t...__devupAttrs,\n\tclassName: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined,\n\tstyle: {\n\t\t...__devupContext.style,\n\t\t...__devupAttrs.style\n\t}\n}))({\n\t...{},\n\t...((props) => ({ size: props.small ? 5 : 10 }))(__devupContext)\n}))({\n\t...__devupProps,\n\tclassName: undefined\n})))))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const G = other.div.attrs({ role: \"note\" })({ margin: 1 });\nexport const I = factory.attrs({ id: \"i\" })({ margin: 3 });\nexport const J = other(Base).attrs({ id: \"j\" })({ margin: 4 });\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap b/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap index a22cb1722..4da4a88a3 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap @@ -5,15 +5,15 @@ expression: outputs [ ( "export const A = styled(motion.div)`color: red;`;", - "export const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = motion.div, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", + "export const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = motion.div, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", ), ( "export const B = styled((Motion as any).Div.Inner)({ color: 'red' });", - "export const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Motion.Div.Inner, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", + "export const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Motion.Div.Inner, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", ), ( "export const C = styled(forwardRef((p, r) =>
)).attrs({ role: 'x' })`color: red;`;", - "export const C = __devupForwardRef((__devupRefProps, __devupRef) => ((DevupStyled) => (__devupProps) => (({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => )({\n\t...__devupProps,\n\t...{ role: \"x\" }\n}))(forwardRef((p, r) =>
))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", + "export const C = __devupForwardRef((__devupRefProps, __devupRef) => ((DevupStyled) => (__devupProps) => (({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => )({\n\t...__devupProps,\n\t...{ role: \"x\" }\n}))(forwardRef((p, r) =>
))({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", ), ( "export const D = (tag) => styled(tag, { color: 'red' });", @@ -21,6 +21,6 @@ expression: outputs ), ( "export const E = styled(getThing().div)`color: red;`;", - "export const E = __devupForwardRef((__devupRefProps, __devupRef) => ((DevupStyled) => ({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => )(getThing().div)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", + "export const E = __devupForwardRef((__devupRefProps, __devupRef) => ((DevupStyled) => ({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => )(getThing().div)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));", ), ] diff --git a/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap b/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap index 688c87467..29b2e11de 100644 --- a/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap +++ b/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap @@ -97,5 +97,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst C = \"red\";\nconst O = .5;\nconst SEL = \".x\";\nconst SNIPPET = \"display: flex;\";\nconst mixin = \"a-a\";\n;\n;\nexport const k = \"a-b\";\nexport const c = \"a-c a-d\";\nexport const d = `a-e ${mixin}`;\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[`a-d ${mixin} ${((p) => p.active && mixin)(rest) || \"\"}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a-f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst C = \"red\";\nconst O = .5;\nconst SEL = \".x\";\nconst SNIPPET = \"display: flex;\";\nconst mixin = \"a-a\";\n;\n;\nexport const k = \"a-b\";\nexport const c = \"a-c a-d\";\nexport const d = `a-e ${mixin}`;\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[`a--a a-d ${mixin} ${((p) => p.active && mixin)(rest) || \"\"}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"a--b a-f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n", } diff --git a/libs/extractor/src/style_values.rs b/libs/extractor/src/style_values.rs index e908704c0..535e26a5c 100644 --- a/libs/extractor/src/style_values.rs +++ b/libs/extractor/src/style_values.rs @@ -58,6 +58,20 @@ impl StyleValues { .then_some(symbol) } + /// The name the import `symbol` binds + pub fn import_name(&self, symbol: SymbolId) -> Option<&str> { + let scoping = self.scoping.as_ref()?; + scoping + .symbol_flags(symbol) + .is_import() + .then(|| scoping.symbol_name(symbol)) + } + + /// The top-level binding `name` names + pub fn root_symbol(&self, name: &str) -> Option { + self.scoping.as_ref()?.get_root_binding(name.into()) + } + /// Whether `symbol` is declared below the top level of the module, where /// the constants styles read are not pub fn is_local(&self, symbol: SymbolId) -> bool { @@ -149,20 +163,24 @@ impl StyleValues { /// The bindings styles read as selectors: interpolated in CSS text where a /// value cannot stand, or as the computed key of a rule object pub fn selected(program: &oxc_ast::ast::Program<'_>, values: &StyleValues) -> FxHashSet { - let mut selected = Selected { - values, - symbols: FxHashSet::default(), - }; - oxc_ast_visit::Visit::visit_program(&mut selected, program); - selected.symbols + let mut symbols = FxHashSet::default(); + selectors(program, &mut |expression| { + symbols.extend(values.symbol(expression)); + }); + symbols +} + +/// Visit the expressions styles read as selectors: interpolated in CSS text +/// where a value cannot stand, or as the computed key of a rule object +pub fn selectors<'a>(program: &oxc_ast::ast::Program<'a>, found: &mut dyn FnMut(&Expression<'a>)) { + oxc_ast_visit::Visit::visit_program(&mut Selected { found }, program); } -struct Selected<'s> { - values: &'s StyleValues, - symbols: FxHashSet, +struct Selected<'s, 'a> { + found: &'s mut dyn FnMut(&Expression<'a>), } -impl<'a> oxc_ast_visit::Visit<'a> for Selected<'_> { +impl<'a> oxc_ast_visit::Visit<'a> for Selected<'_, 'a> { fn visit_tagged_template_expression( &mut self, it: &oxc_ast::ast::TaggedTemplateExpression<'a>, @@ -175,7 +193,7 @@ impl<'a> oxc_ast_visit::Visit<'a> for Selected<'_> { crate::css_utils::interpolation_place(&text, &quasi.quasis[index + 1..]), crate::css_utils::Place::Other ) { - self.symbols.extend(self.values.symbol(expression)); + (self.found)(expression); } } oxc_ast_visit::walk::walk_tagged_template_expression(self, it); @@ -185,7 +203,7 @@ impl<'a> oxc_ast_visit::Visit<'a> for Selected<'_> { if it.computed && let Some(key) = it.key.as_expression() { - self.symbols.extend(self.values.symbol(key)); + (self.found)(key); } oxc_ast_visit::walk::walk_object_property(self, it); } diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index f78d276d3..de061f843 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -33,6 +33,7 @@ use crate::extractor::{ }; use crate::gen_class_name::{gen_class_names, merge_expression_for_class_name}; use crate::gen_style::gen_styles; +use crate::imported_styled::{Exports, ImportedComponent}; use crate::prop_modify_utils::{ add_class_and_style, add_class_and_style_to_object, convert_class_name, modify_prop_object, modify_props, written_class_name, written_object_class_name, @@ -52,9 +53,9 @@ use oxc_ast::ast::JSXAttributeItem::Attribute; use oxc_ast::ast::JSXAttributeName::Identifier; use oxc_ast::ast::{ Argument, ArrayExpressionElement, BinaryOperator, BindingPattern, CallExpression, ChainElement, - ComputedMemberExpression, Expression, ExpressionStatement, FormalParameter, - FormalParameterKind, FormalParameters, IdentifierName, ImportDeclaration, ImportOrExportKind, - JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement, JSXElementName, + ComputedMemberExpression, ExportDefaultDeclaration, Expression, ExpressionStatement, + FormalParameter, FormalParameterKind, FormalParameters, IdentifierName, ImportDeclaration, + ImportOrExportKind, JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement, JSXElementName, JSXExpressionContainer, ObjectExpression, ObjectProperty, ObjectPropertyKind, Program, PropertyKey, PropertyKind, Statement, StaticMemberExpression, Str, StringLiteral, UnaryOperator, VariableDeclarator, @@ -62,9 +63,9 @@ use oxc_ast::ast::{ use oxc_ast_visit::VisitMut; use oxc_ast_visit::walk_mut; use oxc_ast_visit::walk_mut::{ - walk_call_expression, walk_expression, walk_expression_statement, walk_import_declaration, - walk_jsx_attribute_value, walk_jsx_child, walk_jsx_element, walk_program, - walk_variable_declarator, walk_variable_declarators, + walk_call_expression, walk_export_default_declaration, walk_expression, + walk_expression_statement, walk_import_declaration, walk_jsx_attribute_value, walk_jsx_child, + walk_jsx_element, walk_program, walk_variable_declarator, walk_variable_declarators, }; use oxc_syntax::number::NumberBase; use oxc_syntax::operator::LogicalOperator; @@ -252,9 +253,20 @@ pub struct DevupVisitor<'a> { pending_styled: Option<(u32, StyledDefinition<'a>)>, /// The styled bindings other styles select selected_components: FxHashSet, - /// The marker class of the selected styled component the declaration - /// being visited defines, with where its definition starts - pending_marker: Option<(u32, String)>, + /// The marker classes of the exported or selected styled component the + /// declaration being visited defines, with where its definition starts + pending_marker: Option<(u32, Vec)>, + /// What the file exports, whose components carry markers other files + /// select them by + exports: Exports, + /// The components only this file selects, which are marked after those it + /// exports + local_markers: usize, + /// The styled components other modules define that the file extends, + /// selects or gives a `css` prop, by the binding it imports them as + imported_styled: FxHashMap>, + /// The styled component an anonymous `export default` defines + default_definition: Option>, /// Whether a generated styled component forwards refs through React's /// `forwardRef`, which the program then imports forwards_refs: bool, @@ -763,6 +775,10 @@ impl<'a> DevupVisitor<'a> { pending_styled: None, selected_components: FxHashSet::default(), pending_marker: None, + exports: Exports::default(), + local_markers: 0, + imported_styled: FxHashMap::default(), + default_definition: None, forwards_refs: false, styled_definitions: FxHashMap::default(), imported_css: FxHashMap::default(), @@ -781,6 +797,104 @@ impl<'a> DevupVisitor<'a> { self.css_prop = css_prop; } + /// Hand the visitor the styled components other modules define + pub fn import_styled(&mut self, components: FxHashMap>) { + self.imported_styled = components; + } + + /// The styled component `symbol` binds, in this file or another module + fn definition(&self, symbol: oxc_syntax::symbol::SymbolId) -> Option<&StyledDefinition<'a>> { + self.styled_definitions.get(&symbol).or_else(|| { + self.style_values + .import_name(symbol) + .and_then(|name| self.imported_styled.get(name)) + .and_then(|component| component.definition.as_ref()) + }) + } + + /// The styled component `expression` reads: a binding, or a member of a + /// module imported whole + fn definition_of(&self, expression: &Expression<'_>) -> Option<&StyledDefinition<'a>> { + match expression { + Expression::StaticMemberExpression(member) => { + let namespace = self + .style_values + .symbol(&member.object) + .and_then(|symbol| self.style_values.import_name(symbol))?; + self.imported_styled + .get(&format!("{namespace}.{}", member.property.name)) + .and_then(|component| component.definition.as_ref()) + } + expression => self + .style_values + .symbol(expression) + .and_then(|symbol| self.definition(symbol)), + } + } + + /// The classes other styles select the component bound to `name` by: one + /// for each name the file exports it as, and a class of its own when it + /// is only selected here + fn markers_of(&mut self, name: &str, exported: bool, selected: bool) -> Vec { + let mut markers: Vec = if exported { + self.exports + .of(name) + .iter() + .map(|exported| self.marker(exported, self.exports.index(exported))) + .collect() + } else { + Vec::new() + }; + if markers.is_empty() && selected { + markers.push(self.marker(name, self.exports.count() + self.local_markers)); + self.local_markers += 1; + } + markers + } + + fn marker(&self, name: &str, index: usize) -> String { + css::component_marker(name, index, &self.filename) + } + + /// The styled components the file exports, by the names it exports them + /// as, in the arena of the file that reads them + pub fn components<'b>( + &self, + allocator: &'b Allocator, + ) -> FxHashMap> { + let mut components = FxHashMap::default(); + for (binding, names) in self.exports.bindings() { + let component = self + .style_values + .root_symbol(binding) + .and_then(|symbol| self.styled_definitions.get(&symbol)) + .map(|definition| ImportedComponent { + markers: definition.markers().to_vec(), + definition: Some(definition.clone_in(allocator)), + }) + .or_else(|| { + self.imported_styled + .get(binding) + .map(|component| component.clone_in(allocator)) + }); + if let Some(component) = component { + for name in names { + components.insert(name.clone(), component.clone_in(allocator)); + } + } + } + if let Some(definition) = &self.default_definition { + components.insert( + "default".to_string(), + ImportedComponent { + markers: definition.markers().to_vec(), + definition: Some(definition.clone_in(allocator)), + }, + ); + } + components + } + pub fn unknown_bindings(&mut self, unknown: &crate::imported_constants::Unknown) { self.unknown_bindings.clone_from(unknown); } @@ -1292,26 +1406,28 @@ impl<'a> DevupVisitor<'a> { .map(|style| style.clone_in(allocator)) .collect() }; - let definition = symbol.and_then(|symbol| self.styled_definitions.get(&symbol)); + let definition = symbol.and_then(|symbol| self.definition(symbol)); let inline = definition .filter(|_| renders) .and_then(StyledDefinition::inline) .map(|(tag, styles)| (tag.to_string(), clone(styles))); - let marker = definition.and_then(StyledDefinition::marker).map(|marker| { - ExtractStyleProp::Expression { + let markers: Vec> = definition + .into_iter() + .flat_map(StyledDefinition::markers) + .map(|marker| ExtractStyleProp::Expression { expression: Expression::new_string_literal( SPAN, - Str::from_in(marker, allocator), + Str::from_in(marker.as_str(), allocator), None, &self.ast, ), styles: vec![], - } - }); + }) + .collect(); let own = definition.map(|definition| clone(definition.styles())); if let Some((tag, styles)) = inline { let mut props = self.compose_css_prop(element, css, styles, class_name); - props.extend(marker); + props.extend(markers); return Some((Some(tag), props)); } let offset = css.offset; @@ -2270,7 +2386,29 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { walk_variable_declarators(self, it); } + fn visit_export_default_declaration(&mut self, it: &mut ExportDefaultDeclaration<'a>) { + let start = it + .declaration + .as_expression() + .filter(|_| self.exports.has_anonymous_default()) + .map(|expression| expression.span().start); + if let Some(start) = start { + let marker = self.marker("default", self.exports.index("default")); + self.pending_marker = Some((start, vec![marker])); + } + walk_export_default_declaration(self, it); + if start.is_some() { + self.pending_marker = None; + self.default_definition = self + .pending_styled + .take() + .filter(|(at, _)| Some(*at) == start) + .map(|(_, definition)| definition); + } + } + fn visit_program(&mut self, it: &mut Program<'a>) { + self.exports = Exports::scan(it); if self.binds_style_results(it) || self.css_prop != CssProp::Off { self.style_values = crate::style_values::StyleValues::new( oxc_semantic::SemanticBuilder::new() @@ -2281,6 +2419,21 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.style_values .import(std::mem::take(&mut self.imported_css)); self.selected_components = crate::style_values::selected(it, &self.style_values); + let imported: Vec<_> = self + .selected_components + .iter() + .filter_map(|symbol| { + let name = self.style_values.import_name(*symbol)?; + let marker = self.imported_styled.get(name)?.markers.first()?; + Some((*symbol, marker.clone())) + }) + .collect(); + for (symbol, marker) in imported { + self.style_values.insert( + symbol, + crate::style_values::StyleValue::Component(format!(".{marker}")), + ); + } } walk_program(self, it); if !self.compiled_names.is_empty() { @@ -2429,15 +2582,15 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.unknown_arguments("styled", &call.arguments); self.changed_arguments("styled", &call.arguments); } - let inherited = extended(it) - .and_then(|base| self.style_values.symbol(base)) - .and_then(|symbol| self.styled_definitions.get(&symbol)) - .filter(|definition| definition.extendable()); let start = it.span().start; - let marker = self + let markers = self .pending_marker .take_if(|(at, _)| *at == start) - .map(|(_, marker)| marker); + .map(|(_, markers)| markers) + .unwrap_or_default(); + let inherited = extended(it) + .and_then(|base| self.definition_of(base)) + .filter(|definition| definition.extendable()); let StyledExtraction { result, expression, @@ -2448,7 +2601,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { it, Naming { split_filename: self.split_filename.as_deref(), - marker: marker.as_deref(), + markers: &markers, }, &self.imports, &attrs, @@ -2482,10 +2635,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { ] = call.arguments.as_slice() && let Expression::StaticMemberExpression(member) = &call.callee && member.property.name == "withComponent" - && let Some(definition) = self - .style_values - .symbol(&member.object) - .and_then(|symbol| self.styled_definitions.get(&symbol)) + && let Some(definition) = self.definition_of(unwrap_syntax_only(&member.object)) && let Some((component, definition)) = with_component( &self.ast, definition, @@ -2495,6 +2645,12 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { { let start = call.span.start; self.forwards_refs = true; + self.styles.extend( + definition + .styles() + .iter() + .flat_map(ExtractStyleProp::extract), + ); *it = forward_ref(&self.ast, component); self.pending_styled = Some((start, definition)); } @@ -3521,23 +3677,28 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } let marked = styled_binding - .filter(|symbol| self.selected_components.contains(symbol)) .zip(start) - .zip(it.id.get_binding_identifier().map(|id| id.name.to_string())); - if let Some(((symbol, start), name)) = &marked { - let (symbol, start) = (*symbol, *start); - let marker = css::component_marker(name, &self.filename); - self.style_values.insert( - symbol, - crate::style_values::StyleValue::Component(format!(".{marker}")), - ); - self.pending_marker = Some((start, marker)); + .zip(it.id.get_binding_identifier().map(|id| id.name.to_string())) + .and_then(|((symbol, start), name)| { + let selected = self.selected_components.contains(&symbol); + let exported = !self.style_values.is_local(symbol); + let markers = self.markers_of(&name, exported, selected); + (!markers.is_empty()).then_some((symbol, start, markers, selected)) + }); + if let Some((symbol, start, markers, selected)) = &marked { + if *selected { + self.style_values.insert( + *symbol, + crate::style_values::StyleValue::Component(format!(".{}", markers[0])), + ); + } + self.pending_marker = Some((*start, markers.clone())); } walk_variable_declarator(self, it); - // A binding selected but not to a styled component stays unread - if let Some(((symbol, _), _)) = marked + // A binding marked but not to a styled component stays unread + if let Some((symbol, ..)) = marked && self.pending_marker.take().is_some() { self.style_values.remove(symbol); @@ -3723,7 +3884,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { && let Some(definition) = self .style_values .reference_symbol(name) - .and_then(|symbol| self.styled_definitions.get(&symbol)) + .and_then(|symbol| self.definition(symbol)) { elem.opening_element .attributes diff --git a/packages/next-plugin/src/__tests__/imported-styled.test.ts b/packages/next-plugin/src/__tests__/imported-styled.test.ts new file mode 100644 index 000000000..039e205d9 --- /dev/null +++ b/packages/next-plugin/src/__tests__/imported-styled.test.ts @@ -0,0 +1,137 @@ +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import * as wasm from '@devup-ui/wasm' +import { afterEach, beforeEach, describe, expect, it } from 'bun:test' + +import { withModuleResolver } from '../wasm' + +const originalCwd = process.cwd() + +describe('styled components another module defines', () => { + let root: string + + const write = (path: string, code: string) => { + mkdirSync(dirname(join(root, path)), { recursive: true }) + writeFileSync(join(root, path), code) + } + + const extract = (file: string) => + wasm.codeExtractWithoutSourceMap( + file, + readFileSync(join(root, file), 'utf8'), + '@devup-ui/react', + '@devup-ui/react', + true, + false, + false, + { '@emotion/react': null, '@emotion/styled': 'styled' }, + ) + + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'devup-ui-imported-styled-')) + write( + 'tsconfig.json', + JSON.stringify({ + compilerOptions: { baseUrl: '.', paths: { '@/*': ['src/*'] } }, + }), + ) + write( + 'src/Base.tsx', + "import styled from '@emotion/styled'\nexport const Base = styled.div`color: red; padding: 4px;`\nexport const Dyn = styled.button`font-size: ${(p) => (p.big ? '20px' : '10px')};`\n", + ) + write( + 'node_modules/@acme/ui/package.json', + JSON.stringify({ name: '@acme/ui', exports: { '.': './src/index.js' } }), + ) + write( + 'node_modules/@acme/ui/src/index.js', + "import styled from '@emotion/styled'\nexport const Card = styled.section`border: 13px solid;`\n", + ) + process.chdir(root) + withModuleResolver(wasm) + }) + + afterEach(() => { + process.chdir(originalCwd) + rmSync(root, { recursive: true, force: true }) + }) + + it('composes under the styles of the component using it', () => { + write( + 'src/App.tsx', + "import styled from '@emotion/styled'\nimport { Base, Dyn } from '@/Base'\nimport { Card } from '@acme/ui'\nexport const A = styled(Base).attrs({ role: 'note' })`color: blue;`\nexport const B = styled(Dyn)`margin: 0;`\nexport const C = styled(Card)`border: 17px dotted;`\nexport const W = Base.withComponent('section')\n", + ) + const app = extract('src/App.tsx') + expect(app.code).not.toContain('DevupAs = Base') + expect(app.code).not.toContain('DevupAs = Dyn') + expect(app.code).not.toContain('DevupAs = Card') + expect(app.code).toContain('DevupAs = "div"') + expect(app.code).toContain('DevupAs = "button"') + expect(app.code).toContain('DevupAs = "section"') + expect(app.code).toContain('role: "note"') + expect([...app.dependencies].sort()).toEqual([ + 'node_modules/@acme/ui/src/index.js', + 'src/Base.tsx', + ]) + const css = wasm.getCss(null, false) + expect(css).toContain('color:blue') + expect(css).toContain('padding:4px') + expect(css).toContain('border:17px dotted') + expect(css).not.toContain('border:13px solid') + }) + + it('follows the definition when its module changes', () => { + write( + 'src/App.tsx', + "import styled from '@emotion/styled'\nimport { Base } from './Base'\nexport const A = styled(Base)`margin: 0;`\n", + ) + const before = extract('src/App.tsx') + write( + 'src/Base.tsx', + "import styled from '@emotion/styled'\nexport const Base = styled.main`color: green;`\n", + ) + const after = extract('src/App.tsx') + expect(before.dependencies).toEqual(['src/Base.tsx']) + expect(after.dependencies).toEqual(['src/Base.tsx']) + expect(before.code).toContain('DevupAs = "div"') + expect(after.code).toContain('DevupAs = "main"') + expect(wasm.getCss(null, false)).toContain('color:green') + }) + + it('selects a component by the class its own module gives it', () => { + write( + 'src/App.tsx', + "import styled from '@emotion/styled'\nimport { Base } from './Base'\nexport const P = styled.div`${Base} { margin: 7123px; }`\n", + ) + extract('src/App.tsx') + const base = extract('src/Base.tsx') + const selector = /\.[\w-]+ \.([\w-]+)\{margin:7123px/.exec( + wasm.getCss(null, false), + ) + const marker = selector?.[1] + expect(marker).toBeDefined() + expect(base.code).toContain(marker as string) + }) + + it('keeps a component it cannot read a runtime component', () => { + write( + 'src/Link.tsx', + "import styled from '@emotion/styled'\nimport Link from 'next/link'\nexport const Nav = styled(Link)`color: red;`\n", + ) + write( + 'src/App.tsx', + "import styled from '@emotion/styled'\nimport { Nav } from './Link'\nexport const A = styled(Nav)`color: blue;`\n", + ) + const app = extract('src/App.tsx') + expect(app.code).toContain('DevupAs = Nav') + expect(app.dependencies).toEqual(['src/Link.tsx']) + }) +}) From 7df41c061ce1706c8cab1005330ca7ea1c3d5847 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 3 Oct 2026 17:34:36 +0900 Subject: [PATCH 6/8] fix(extractor): keep imported styled components that read their module runtime Refs #688 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../extractor/extract_style_from_styled.rs | 155 +++++++++++++----- libs/extractor/src/imported_styled.rs | 18 +- ...d_cannot_read_stay_runtime_components.snap | 2 +- 3 files changed, 126 insertions(+), 49 deletions(-) diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index c5a484d76..d2784b4f6 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -13,22 +13,22 @@ use crate::{ gen_style::gen_styles, styled_reads::{Forward, Reads, withheld}, utils::{ - STYLE_OBJECT, StyleArguments, build_time_error, call_with_values, merge_object_expressions, - readable_code, reads_directly, style_arguments, uncomposable_error, unplaced_error, - unreadable_styles, unwrap_syntax_only, unwrap_syntax_only_mut, wrap_array_filter, - wrap_direct_call, + STYLE_OBJECT, StyleArguments, build_time_error, call_with_values, expression_to_code, + merge_object_expressions, readable_code, reads_directly, style_arguments, + uncomposable_error, unplaced_error, unreadable_styles, unwrap_syntax_only, + unwrap_syntax_only_mut, wrap_array_filter, wrap_direct_call, }, }; use oxc_allocator::{CloneIn, FromIn, GetAllocator}; use oxc_ast::{ ast::{ Argument, BindingPattern, BindingProperty, BindingRestElement, CallExpression, Expression, - ExpressionStatement, FormalParameter, FormalParameterKind, FormalParameters, - JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXElementName, JSXOpeningElement, - ObjectPropertyKind, Program, PropertyKey, Statement, Str, + FormalParameter, FormalParameterKind, FormalParameters, JSXAttributeItem, JSXAttributeName, + JSXAttributeValue, JSXElementName, JSXOpeningElement, ObjectPropertyKind, PropertyKey, Str, }, builder::AstBuilder, }; +use oxc_parser::{Parser, ParserReturn}; use oxc_span::{GetSpan, SPAN, SourceType}; use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator}; @@ -189,12 +189,24 @@ impl<'a> StyledDefinition<'a> { /// defining it #[must_use] pub fn portable(&self) -> bool { - renders_tag(&self.name, self.bound.as_ref()) - && closed( - self.classes + renders_tag(&self.name, self.bound.as_ref()) && closed(self.code()) + } + + /// The code the component evaluates for its classes, attrs and styles + fn code(&self) -> impl Iterator + '_ { + self.classes + .iter() + .chain(&self.attrs) + .chain(self.styles.iter().flat_map(style_expressions)) + .map(expression_to_code) + .chain( + self.styles .iter() - .chain(&self.attrs) - .chain(self.styles.iter().flat_map(style_expressions)), + .flat_map(ExtractStyleProp::extract) + .filter_map(|style| match style { + ExtractStyleValue::Dynamic(style) => Some(style.identifier().to_string()), + _ => None, + }), ) } @@ -811,44 +823,32 @@ fn style_expressions<'p, 'a>(prop: &'p ExtractStyleProp<'a>) -> Vec<&'p Expressi } } -/// Whether `expressions` read nothing but the bindings they declare, the -/// props, and what every module reads the same -fn closed<'e, 'a: 'e>(expressions: impl Iterator>) -> bool { +/// Whether the expressions `code` writes read nothing but the bindings they +/// declare, the props, and what every module reads the same +fn closed(code: impl Iterator) -> bool { + let source = code + .map(|code| ["(", &code, ");\n"].concat()) + .collect::(); let allocator = oxc_allocator::Allocator::default(); - let ast_builder = AstBuilder::new(&allocator); - let body = oxc_allocator::Vec::from_iter_in( - expressions.map(|expression| { - Statement::ExpressionStatement(ExpressionStatement::boxed( - SPAN, - expression.clone_in(&allocator), - &ast_builder, - )) - }), - &ast_builder, - ); - let program = Program::new( - SPAN, - SourceType::tsx(), - "", - oxc_allocator::Vec::new_in(&ast_builder), - None, - oxc_allocator::Vec::new_in(&ast_builder), - body, - &ast_builder, - ); + let ParserReturn { + program, + fatal_error, + .. + } = Parser::new(&allocator, &source, SourceType::tsx()).parse(); let scoping = oxc_semantic::SemanticBuilder::new() .build(&program) .semantic .into_scoping(); - scoping - .root_unresolved_references() - .iter() - .all(|(name, references)| { - SHARED_NAMES.contains(&name.as_str()) - || references - .iter() - .all(|reference| !scoping.get_reference(*reference).is_value()) - }) + !fatal_error + && scoping + .root_unresolved_references() + .iter() + .all(|(name, references)| { + SHARED_NAMES.contains(&name.as_str()) + || references + .iter() + .all(|reference| !scoping.get_reference(*reference).is_value()) + }) } fn clone_all<'s, 'a>( @@ -1616,3 +1616,68 @@ fn create_styled_component<'a>( ast_builder, ) } + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests { + use std::collections::BTreeMap; + + use oxc_allocator::Allocator; + + use super::*; + + #[test] + fn style_expressions_find_what_every_kind_of_style_evaluates() { + let allocator = Allocator::default(); + let ast_builder = AstBuilder::new(&allocator); + let name = |name: &str| { + Expression::new_identifier(SPAN, Str::from_in(name, &allocator), &ast_builder) + }; + let evaluated = |expression| ExtractStyleProp::Expression { + styles: vec![], + expression, + }; + let props = [ + ExtractStyleProp::Static(ExtractStyleValue::Typography("t".to_string())), + ExtractStyleProp::Unreadable { + offset: 0, + code: String::new(), + prop: false, + }, + ExtractStyleProp::StaticArray(vec![evaluated(name("a"))]), + ExtractStyleProp::Conditional { + condition: name("b"), + consequent: Some(Box::new(evaluated(name("c")))), + alternate: Some(Box::new(evaluated(name("d")))), + }, + ExtractStyleProp::Enum { + condition: name("e"), + map: BTreeMap::from([("x".to_string(), vec![evaluated(name("f"))])]), + }, + ExtractStyleProp::MemberExpression { + map: BTreeMap::from([("k".to_string(), Box::new(evaluated(name("h"))))]), + expression: name("g"), + }, + ]; + let code: Vec = props + .iter() + .flat_map(style_expressions) + .map(expression_to_code) + .collect(); + assert_eq!(code, ["a;", "b;", "c;", "d;", "e;", "f;", "g;", "h;"]); + } + + #[test] + fn code_is_closed_when_it_reads_only_what_it_declares() { + let closed_code = |code: &[&str]| closed(code.iter().map(ToString::to_string)); + assert!(closed_code(&[])); + assert!(closed_code(&[ + "(p) => p.x * Math.PI", + "((p) => p.on)(rest)" + ])); + assert!(closed_code(&["(p: Props) => p.x"])); + assert!(!closed_code(&["(p) => scale(p.x)"])); + assert!(!closed_code(&["(p) => p.x", "window.mode"])); + assert!(!closed_code(&["@#$"])); + } +} diff --git a/libs/extractor/src/imported_styled.rs b/libs/extractor/src/imported_styled.rs index 26eb3c44f..ec855b88d 100644 --- a/libs/extractor/src/imported_styled.rs +++ b/libs/extractor/src/imported_styled.rs @@ -808,8 +808,20 @@ export default styled.span`margin: 1px;`; "import styled from '@emotion/styled';\nimport Link from 'next/link';\nexport const L = styled(Link)`color: red;`;\nexport const M = styled(L)`margin: 0;`;", ), ( - "/src/free.ts", - "import styled from '@emotion/styled';\nconst handler = () => 1;\nexport const H = styled.div.attrs({ onClick: handler })`color: red;`;\nexport const C = styled.div({ color: window.dark ? 'red' : 'blue' });\nexport const E = styled.div({ display: window.mode });\nexport const T = styled.div({ color: ['red', 'blue'] });\nexport const U = styled.div({ color: (p: Props) => p.tone });\ntype Props = { tone: string };", + "/src/attrs.ts", + "import styled from '@emotion/styled';\nconst handler = () => 1;\nexport const H = styled.div.attrs({ onClick: handler })`color: red;`;", + ), + ( + "/src/function.ts", + "import styled from '@emotion/styled';\nfunction scaled(n) { return n * 2; }\nexport const D = styled.div`margin: ${(p) => scaled(p.x)}px;`;", + ), + ( + "/src/global.ts", + "import styled from '@emotion/styled';\nexport const E = styled.div({ display: process.env.MODE });", + ), + ( + "/src/closed.ts", + "import styled from '@emotion/styled';\nexport const G = styled.div`margin: ${(p) => Number(p.x)}px;`;\nexport const T = styled.div({ color: ['red', 'blue'] });\nexport const U = styled.div`color: ${(p: Props) => p.tone};`;\ntype Props = { tone: string };", ), ( "/src/plain.ts", @@ -840,7 +852,7 @@ export default styled.span`margin: 1px;`; ]; let bodies = [ "import { L, M } from './link';\nexport const A = styled(L)`color: blue;`;\nexport const B = styled(M)`color: blue;`;", - "import { H, C, E, T, U } from './free';\nexport const A = styled(H)`color: blue;`;\nexport const B = styled(C)`color: blue;`;\nexport const D = styled(E)`color: blue;`;\nexport const F = styled(T)`color: blue;`;\nexport const G = styled(U)`color: blue;`;", + "import { H } from './attrs';\nimport { D } from './function';\nimport { E } from './global';\nimport { G, T, U } from './closed';\nexport const A = styled(H)`color: blue;`;\nexport const B = styled(D)`color: blue;`;\nexport const C = styled(G)`color: blue;`;\nexport const F = styled(E)`color: blue;`;\nexport const I = styled(T)`color: blue;`;\nexport const J = styled(U)`color: blue;`;", "import Anonymous, { Base, fn, a, P } from './plain';\nexport const A = styled(Base)`color: blue;`;\nexport const B = styled(Anonymous)`color: blue;`;\nexport const C = styled(fn)`color: blue;`;\nexport const D = styled(a)`color: blue;`;\nexport const E = styled(P)`color: blue;`;", "import { Good, Bad } from './bad';\nexport const A = styled(Good)`color: blue;`;\nexport const B = styled.div`${Good} { color: red; }`;", "import { A } from './cycle-a';\nexport const App = styled(A)`color: blue;`;", diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap index 29a0e0cb7..a51e528ff 100644 --- a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap @@ -4,7 +4,7 @@ expression: outputs --- [ "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { L, M } from \"./link\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = L, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = M, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/link.ts\"]", - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { H, C, E, T, U } from \"./free\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = H, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = C, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const D = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = E, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const F = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = T, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const G = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = U, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/free.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { H } from \"./attrs\";\nimport { D } from \"./function\";\nimport { E } from \"./global\";\nimport { G, T, U } from \"./closed\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = H, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = D, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--c c--a i a\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--b\": `${(p=>Number(p.x))(rest)}px` },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const F = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = E, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const I = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--e c--b i f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const J = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--f c--c i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"margin\\\", level: 0, identifier: \\\"`${(p=>Number(p.x))(rest)}px`\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 1, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/attrs.ts\", \"/src/closed.ts\", \"/src/function.ts\", \"/src/global.ts\"]", "/src/app.tsx:3:18: `styled()` cannot use `styled(5)` at build time: it renders a tag, a component or a value naming one, with rule objects or CSS text", "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Good, Bad } from \"./bad\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Good, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--b c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"red\\\", level: 0, selector: Some(Selector(\\\"& .a--b\\\")), style_order: None, layer: None })\",\n}\n[\"/src/bad.ts\"]", "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { A } from \"./cycle-a\";\nexport const App = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = A, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/cycle-a.ts\", \"/src/cycle-b.ts\"]", From b312482b45f8d08bdb262eb20325ad051f980d85 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 16 Oct 2026 20:13:29 +0900 Subject: [PATCH 7/8] test(extractor): cover unsupported numeric unary class guard Refs #682 #689 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- libs/extractor/src/class_names_numeric_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/extractor/src/class_names_numeric_tests.rs b/libs/extractor/src/class_names_numeric_tests.rs index 7af679077..83b5d9a02 100644 --- a/libs/extractor/src/class_names_numeric_tests.rs +++ b/libs/extractor/src/class_names_numeric_tests.rs @@ -157,6 +157,7 @@ fn class_names_numeric_arguments_keep_unknown_call_guards(#[case] expression: &s #[case("cx(-'2')")] #[case("cx(+true)")] #[case("cx(-false)")] +#[case("cx(~1)")] #[case("cx(-on)")] #[case("cx(+on)")] #[case("cx(-getClass())")] From f4aaa8144e8f7d243fea03b570ae2b10c13f50c2 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 17 Oct 2026 00:32:08 +0900 Subject: [PATCH 8/8] test(extractor): align imported Emotion fallback snapshot Refs #682 #695 #688 #689 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...omponents_the_build_cannot_read_stay_runtime_components.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap index 41d5d14b4..1f45e3241 100644 --- a/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap +++ b/libs/extractor/src/snapshots/extractor__imported_styled__tests__imported_components_the_build_cannot_read_stay_runtime_components.snap @@ -4,7 +4,7 @@ expression: outputs --- [ "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { L, M } from \"./link\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = L, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = M, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/link.ts\"]", - "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { H } from \"./attrs\";\nimport { D } from \"./function\";\nimport { E } from \"./global\";\nimport { G, T, U } from \"./closed\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = H, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = D, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--c c--a i a\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--b\": `${(p=>Number(p.x))(rest)}px` },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const F = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = E, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const I = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--e c--b i f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const J = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--f c--c i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"margin\\\", level: 0, identifier: \\\"`${(p=>Number(p.x))(rest)}px`\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 1, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/attrs.ts\", \"/src/closed.ts\", \"/src/function.ts\", \"/src/global.ts\"]", + "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { H } from \"./attrs\";\nimport { D } from \"./function\";\nimport { E } from \"./global\";\nimport { G, T, U } from \"./closed\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = H, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = D, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const C = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--c c--a i a\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--b\": `${(p=>Number(p.x))(rest)}px` },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const F = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = E, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const I = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--e c--b i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const J = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"e--f c--c i\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Dynamic(ExtractDynamicStyle { property: \\\"margin\\\", level: 0, identifier: \\\"`${(p=>Number(p.x))(rest)}px`\\\", selector: None, style_order: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/attrs.ts\", \"/src/closed.ts\", \"/src/function.ts\", \"/src/global.ts\"]", "/src/app.tsx:3:18: `styled()` cannot use `styled(5)` at build time: it renders a tag, a component or a value naming one, with rule objects or CSS text", "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Good, Bad } from \"./bad\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = Good, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)} as={forwardedAs} className={[\"b--b c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"red\\\", level: 0, selector: Some(Selector(\\\"& .a--b\\\")), style_order: None, layer: None })\",\n}\n[\"/src/bad.ts\"]", "import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { A } from \"./cycle-a\";\nexport const App = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = A, forwardedAs, ...rest }) => )({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n{\n \"Static(ExtractStaticStyle { property: \\\"color\\\", value: \\\"blue\\\", level: 0, selector: None, style_order: None, layer: None })\",\n}\n[\"/src/cycle-a.ts\", \"/src/cycle-b.ts\"]",