rnwind 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/lib/cjs/core/parser/color.cjs +53 -24
  2. package/lib/cjs/core/parser/color.cjs.map +1 -1
  3. package/lib/cjs/core/parser/layout-dispatcher.cjs +20 -0
  4. package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -1
  5. package/lib/cjs/core/parser/length.cjs +20 -6
  6. package/lib/cjs/core/parser/length.cjs.map +1 -1
  7. package/lib/cjs/core/parser/length.d.ts +6 -3
  8. package/lib/cjs/core/parser/shorthand.cjs +37 -5
  9. package/lib/cjs/core/parser/shorthand.cjs.map +1 -1
  10. package/lib/cjs/core/parser/shorthand.d.ts +11 -5
  11. package/lib/cjs/core/parser/theme-vars.cjs +53 -0
  12. package/lib/cjs/core/parser/theme-vars.cjs.map +1 -1
  13. package/lib/cjs/core/parser/theme-vars.d.ts +21 -0
  14. package/lib/cjs/core/parser/tokens.cjs +183 -1
  15. package/lib/cjs/core/parser/tokens.cjs.map +1 -1
  16. package/lib/cjs/core/parser/tw-parser.cjs +140 -27
  17. package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
  18. package/lib/cjs/core/parser/tw-parser.d.ts +21 -5
  19. package/lib/cjs/core/parser/typography-dispatcher.cjs +16 -1
  20. package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -1
  21. package/lib/cjs/core/style-builder/build-style.cjs +73 -26
  22. package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
  23. package/lib/cjs/metro/state.cjs +52 -2
  24. package/lib/cjs/metro/state.cjs.map +1 -1
  25. package/lib/cjs/metro/state.d.ts +17 -1
  26. package/lib/cjs/metro/transform-ast.cjs +238 -21
  27. package/lib/cjs/metro/transform-ast.cjs.map +1 -1
  28. package/lib/cjs/metro/transform-ast.d.ts +15 -0
  29. package/lib/cjs/metro/transformer.cjs +29 -2
  30. package/lib/cjs/metro/transformer.cjs.map +1 -1
  31. package/lib/cjs/metro/with-config.cjs +1 -1
  32. package/lib/cjs/metro/with-config.cjs.map +1 -1
  33. package/lib/cjs/metro/with-config.d.ts +22 -0
  34. package/lib/esm/core/parser/color.mjs +53 -24
  35. package/lib/esm/core/parser/color.mjs.map +1 -1
  36. package/lib/esm/core/parser/layout-dispatcher.mjs +20 -0
  37. package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
  38. package/lib/esm/core/parser/length.d.ts +6 -3
  39. package/lib/esm/core/parser/length.mjs +20 -6
  40. package/lib/esm/core/parser/length.mjs.map +1 -1
  41. package/lib/esm/core/parser/shorthand.d.ts +11 -5
  42. package/lib/esm/core/parser/shorthand.mjs +37 -5
  43. package/lib/esm/core/parser/shorthand.mjs.map +1 -1
  44. package/lib/esm/core/parser/theme-vars.d.ts +21 -0
  45. package/lib/esm/core/parser/theme-vars.mjs +53 -1
  46. package/lib/esm/core/parser/theme-vars.mjs.map +1 -1
  47. package/lib/esm/core/parser/tokens.mjs +183 -1
  48. package/lib/esm/core/parser/tokens.mjs.map +1 -1
  49. package/lib/esm/core/parser/tw-parser.d.ts +21 -5
  50. package/lib/esm/core/parser/tw-parser.mjs +141 -28
  51. package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
  52. package/lib/esm/core/parser/typography-dispatcher.mjs +16 -1
  53. package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
  54. package/lib/esm/core/style-builder/build-style.mjs +73 -26
  55. package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
  56. package/lib/esm/metro/state.d.ts +17 -1
  57. package/lib/esm/metro/state.mjs +51 -3
  58. package/lib/esm/metro/state.mjs.map +1 -1
  59. package/lib/esm/metro/transform-ast.d.ts +15 -0
  60. package/lib/esm/metro/transform-ast.mjs +238 -21
  61. package/lib/esm/metro/transform-ast.mjs.map +1 -1
  62. package/lib/esm/metro/transformer.mjs +30 -3
  63. package/lib/esm/metro/transformer.mjs.map +1 -1
  64. package/lib/esm/metro/with-config.d.ts +22 -0
  65. package/lib/esm/metro/with-config.mjs +1 -1
  66. package/lib/esm/metro/with-config.mjs.map +1 -1
  67. package/package.json +2 -1
  68. package/src/core/parser/color.ts +52 -18
  69. package/src/core/parser/layout-dispatcher.ts +19 -0
  70. package/src/core/parser/length.ts +20 -6
  71. package/src/core/parser/shorthand.ts +35 -5
  72. package/src/core/parser/theme-vars.ts +53 -0
  73. package/src/core/parser/tokens.ts +171 -1
  74. package/src/core/parser/tw-parser.ts +147 -28
  75. package/src/core/parser/typography-dispatcher.ts +15 -1
  76. package/src/core/style-builder/build-style.ts +84 -26
  77. package/src/metro/state.ts +49 -1
  78. package/src/metro/transform-ast.ts +249 -18
  79. package/src/metro/transformer.ts +28 -3
  80. package/src/metro/with-config.ts +23 -1
@@ -1 +1 @@
1
- {"version":3,"file":"transformer.mjs","sources":["../../../../src/metro/transformer.ts"],"sourcesContent":["import type { File } from '@babel/types'\nimport * as t from '@babel/types'\nimport { parse } from '@babel/parser'\nimport generate from '@babel/generator'\nimport { createHash } from 'node:crypto'\nimport { transformAst } from './transform-ast'\nimport { getClassNamePrefixes, getRnwindCacheKey, getRnwindState, onThemeChange } from './state'\nimport { STYLE_SPECIFIERS, THEME_SIGNATURE_MODULE } from './resolver'\nimport { filterUnknownClassCandidates } from './warn-unknown-classes'\n\n/** The shape of the upstream module we delegate parsing/babel work to. */\ninterface UpstreamTransformer {\n transform: (args: BabelTransformerArgs) => Promise<BabelTransformerResult> | BabelTransformerResult\n}\n\n/** Env var that points at the upstream `babelTransformerPath` we override. */\nconst UPSTREAM_ENV = 'RNWIND_UPSTREAM_TRANSFORMER'\n\n/** Cached upstream module — required once, reused across every transform call. */\nlet cachedUpstream: UpstreamTransformer | null = null\n\nconst generateModule = (generate as unknown as { default?: typeof generate }).default ?? generate\n\n/**\n * Parse user source with the broad plugin set (Flow + JSX + TypeScript\n * + class properties). Permissive on purpose so we don't reject any\n * file the upstream could have handled. Returns `null` when parse\n * fails — caller falls back to the raw source string.\n * @param source Source text.\n * @returns Parsed AST, or null on parse failure.\n */\nfunction parseUserSource(source: string): File | null {\n try {\n return parse(source, {\n sourceType: 'unambiguous',\n allowReturnOutsideFunction: true,\n allowImportExportEverywhere: true,\n plugins: ['typescript', 'jsx'],\n }) as unknown as File\n } catch {\n try {\n return parse(source, {\n sourceType: 'unambiguous',\n allowReturnOutsideFunction: true,\n allowImportExportEverywhere: true,\n plugins: ['flow', 'jsx'],\n }) as unknown as File\n } catch {\n return null\n }\n }\n}\n\n/**\n * Print Tailwind-shaped candidates oxide picked up but the parser\n * could NOT compile — typo, missing custom utility, or class not in\n * the user's theme. Filtering by candidates that ALSO appear inside a\n * `className=\"…\"` literal eliminates false positives from imports,\n * comments, and JSX prop values.\n * @param source Original source text — searched for className literals.\n * @param candidates Every candidate oxide surfaced from the source.\n * @param atoms Successfully resolved atoms (keys are class names).\n * @param filename Source path, prefixed onto the warning.\n */\nfunction warnUnknownClasses(\n source: string,\n candidates: readonly string[],\n atoms: ReadonlyMap<string, unknown>,\n filename: string,\n): void {\n const atomNames = new Set(atoms.keys())\n const unknown = filterUnknownClassCandidates(source, candidates, atomNames)\n if (unknown.length === 0) return\n // eslint-disable-next-line no-console\n console.warn(`rnwind: unknown class${unknown.length > 1 ? 'es' : ''} in ${filename}: ${unknown.join(', ')}`)\n}\n\n/**\n * Extract the bare extension for oxide / internal switches.\n * @param filename Absolute path.\n * @returns Extension without the leading dot (`tsx` / `ts` / `js` / `jsx`).\n */\nfunction extensionOf(filename: string): string {\n const index = filename.lastIndexOf('.')\n if (index === -1) return 'tsx'\n return filename.slice(index + 1)\n}\n\n/**\n * Read the project root Metro hands us per-transform. Falls back to\n * `process.cwd()` only when the upstream harness doesn't set it (unit\n * tests, standalone). Metro's production pipeline always sets it.\n * @param args Metro transformer args.\n * @returns Absolute project root.\n */\nfunction projectRootOf(args: BabelTransformerArgs): string {\n const fromOptions = args.options?.projectRoot\n if (typeof fromOptions === 'string' && fromOptions.length > 0) return fromOptions\n return process.cwd()\n}\n\n/**\n * Whether a `.css` filename is the user's theme entry (the file\n * `withRnwindConfig` pointed us at via `RNWIND_CSS_ENTRY_FILE`).\n * Only the theme CSS should trigger a scheme rebuild — unrelated CSS\n * files in the project stay invisible to rnwind.\n * @param filename Absolute CSS path.\n * @returns Whether the file is the configured theme entry.\n */\nfunction isThemeCssEntry(filename: string): boolean {\n const cssEntry = process.env.RNWIND_CSS_ENTRY_FILE\n return typeof cssEntry === 'string' && cssEntry.length > 0 && cssEntry === filename\n}\n\n/**\n * Parse + run rnwind's JSX rewrite + regenerate source code. When\n * parsing or transformation fails, fall back to the original source —\n * we don't want a transient parse error to crash Metro for a file the\n * upstream might handle fine.\n * @param args Metro args; `src` is the original source text.\n * @returns Rewritten source text (with `className=` rewrites applied).\n */\nasync function rewriteSource(args: BabelTransformerArgs): Promise<string> {\n const ast = parseUserSource(args.src)\n if (!ast) return args.src\n\n const state = getRnwindState(projectRootOf(args))\n const extension = extensionOf(args.filename)\n const parsed = await state.parser.parseAtoms({ content: args.src, extension })\n\n warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename)\n\n const classNamePrefixes = getClassNamePrefixes()\n if (parsed.atoms.size === 0) {\n state.builder.dropFile(args.filename)\n await state.builder.writeSchemes()\n transformAst(ast, {\n styleSpecifiers: [],\n gradientAtoms: parsed.gradientAtoms,\n hapticAtoms: parsed.hapticAtoms,\n classNamePrefixes,\n })\n injectThemeSignatureImport(ast)\n return generateModule(ast).code\n }\n\n const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes)\n if (changed) await state.builder.writeSchemes()\n\n transformAst(ast, {\n styleSpecifiers: STYLE_SPECIFIERS as unknown as readonly string[],\n gradientAtoms: parsed.gradientAtoms,\n hapticAtoms: parsed.hapticAtoms,\n classNamePrefixes,\n })\n injectThemeSignatureImport(ast)\n return generateModule(ast).code\n}\n\n/**\n * Prepend `import 'rnwind/__generated/theme-signature'` to every\n * rnwind-transformed file. The resolver maps that specifier to the\n * user's theme CSS so Metro's dependency graph carries a real edge\n * from this JS file to the CSS. When the user edits `global.css`,\n * the CSS module's SHA1 changes, and Metro invalidates every JS file\n * holding this import — forcing them to re-transform with the new\n * theme. The `.css` branch in {@link transform} returns an empty\n * `export {}` module so the runtime cost is one extra `require()`.\n * @param ast Babel File AST to mutate in place.\n */\nfunction injectThemeSignatureImport(ast: File): void {\n const declaration = t.importDeclaration([], t.stringLiteral(THEME_SIGNATURE_MODULE))\n ast.program.body.unshift(declaration)\n}\n\n/**\n * Read the upstream transformer's `getCacheKey()` so our cache-key\n * contribution composes with — rather than replaces — whatever the\n * host framework wants to mix in.\n * @returns Upstream cache key, or `null` when no upstream exposes one.\n */\nfunction loadUpstreamCacheKey(): string | null {\n const upstream = loadUpstream() as (UpstreamTransformer & { getCacheKey?: () => string }) | null\n if (!upstream) return null\n try {\n return typeof upstream.getCacheKey === 'function' ? upstream.getCacheKey() : null\n } catch {\n return null\n }\n}\n\n/**\n * Invoke the upstream `babelTransformerPath` Metro originally had\n * configured. The path is read from `RNWIND_UPSTREAM_TRANSFORMER`,\n * which `withRnwindConfig` sets at Metro startup. When the env var is\n * unset (unit tests, standalone use), fall back to a typescript+jsx\n * parse.\n * @param args Metro's per-file args.\n * @returns Upstream transform result containing the post-babel AST.\n */\nasync function runUpstream(args: BabelTransformerArgs): Promise<BabelTransformerResult> {\n if (args.ast && !process.env[UPSTREAM_ENV]) return { ast: args.ast }\n const upstream = loadUpstream()\n if (upstream) return await Promise.resolve(upstream.transform(args))\n if (args.ast) return { ast: args.ast }\n return { ast: parseSource(args.src) }\n}\n\n/**\n * Lazily require the upstream transformer module. Cached after first\n * load so per-file overhead is one cache lookup.\n * @returns Upstream module, or null when env is unset.\n */\nfunction loadUpstream(): UpstreamTransformer | null {\n if (cachedUpstream) return cachedUpstream\n const upstreamPath = process.env[UPSTREAM_ENV]\n if (!upstreamPath || upstreamPath.length === 0) return null\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires\n const required = require(upstreamPath) as UpstreamTransformer | { default?: UpstreamTransformer }\n const upstream = (required as { default?: UpstreamTransformer }).default ?? (required as UpstreamTransformer)\n if (typeof upstream.transform !== 'function') return null\n cachedUpstream = upstream\n return upstream\n } catch (error) {\n // eslint-disable-next-line no-console\n if (process.env.RNWIND_DEBUG) console.error('rnwind: failed to load upstream transformer:', error)\n return null\n }\n}\n\n/**\n * Cheap guard — the file has to look JS/TS, live outside `node_modules`,\n * and mention `className=` before we spend AST cycles on it.\n * @param args Metro args.\n * @returns Whether the file might need the rnwind pass.\n */\nfunction isRewriteCandidate(args: BabelTransformerArgs): boolean {\n if (!/\\.(?:tsx|ts|jsx|js)$/i.test(args.filename)) return false\n if (args.filename.includes('/node_modules/')) return false\n return args.src.includes('className=')\n}\n\n/**\n * Fallback parse when no upstream is configured AND Metro didn't hand\n * us an AST. Used by unit tests and standalone setups.\n * @param source Source text.\n * @returns Parsed Babel File.\n */\nfunction parseSource(source: string): File {\n return parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] }) as unknown as File\n}\n\n/** Metro's babel transformer signature. */\nexport interface BabelTransformerArgs {\n filename: string\n src: string\n options: { projectRoot?: string; [key: string]: unknown }\n ast?: File\n plugins?: readonly unknown[]\n}\n\n/** Return shape Metro expects from a babel transformer. */\nexport interface BabelTransformerResult {\n ast: File\n metadata?: unknown\n}\n\n/**\n * rnwind's Metro babel transformer. Two phases per source file:\n *\n * 1. **Pre-process the source string before handing it to the upstream\n * babel pipeline.** babel-preset-expo / React's JSX transform run\n * inside the upstream and convert `<View className=\"...\"/>` into\n * `React.createElement(View, {className})`. If we walked the AST\n * AFTER the upstream, there'd be no JSX attributes left to\n * rewrite. So we parse, run our pass, regenerate code, and feed\n * THAT to the upstream as `src`.\n * 2. **Delegate to the upstream `babelTransformerPath`** (Expo's\n * default handles Flow stripping, expo-router macros, etc.).\n *\n * Skip both phases when the file isn't a JS/TS source under user\n * code, or doesn't mention `className=` — hand straight to upstream.\n * @param args Metro's per-file args.\n * @returns Mutated AST + metadata.\n */\nexport async function transform(args: BabelTransformerArgs): Promise<BabelTransformerResult> {\n // Short-circuit `.css` inputs: the theme CSS is pulled into the dep\n // graph as a sentinel (see `THEME_SIGNATURE_MODULE` in resolver.ts)\n // so Metro watches it and invalidates importers on edit, but the\n // file's CSS syntax can't go through a JS babel transformer.\n //\n // When the CSS being transformed IS the user's theme entry, we\n // piggyback on Metro's own file-watcher: Metro calls us here on\n // every CSS save; we trigger `onThemeChange` to rebuild parser +\n // rewrite scheme files with the new values. Metro's dep graph then\n // HMRs the regenerated `common.style.js` to the running app.\n //\n // Emitting the CSS content hash in the fake JS output is what makes\n // Metro propagate invalidation to downstream importers — constant\n // `export {}` bytes would never look changed and Metro would skip\n // the chain.\n if (args.filename.endsWith('.css')) {\n if (isThemeCssEntry(args.filename)) {\n try {\n await onThemeChange(projectRootOf(args))\n } catch {\n // CSS edit happened outside a configured project (e.g. tests).\n }\n }\n const themeHash = createHash('sha256').update(args.src).digest('hex').slice(0, 16)\n const stub = `export const __rnwindThemeHash = ${JSON.stringify(themeHash)};\\n`\n return { ast: parse(stub, { sourceType: 'module' }) as unknown as File }\n }\n if (!isRewriteCandidate(args)) {\n if (/\\.(?:tsx|ts|jsx|js)$/i.test(args.filename) && !args.filename.includes('/node_modules/')) {\n try {\n getRnwindState(projectRootOf(args)).builder.dropFile(args.filename)\n } catch {\n // State not configured (e.g. test). Nothing to drop.\n }\n }\n return runUpstream(args)\n }\n\n const rewrittenSource = await rewriteSource(args)\n return runUpstream({ ...args, src: rewrittenSource, ast: undefined })\n}\n\n/**\n * Metro's babel-transformer contract: a `getCacheKey()` export is\n * sampled per-file and mixed into the transform cache key. Returning\n * a string that includes the theme CSS content hash invalidates every\n * cached transform on every CSS edit — so the bundle rebuilds with\n * the new theme automatically on the next request.\n * @returns Cache-key segment that includes rnwind's current theme hash.\n */\nexport function getCacheKey(): string {\n const upstreamKey = loadUpstreamCacheKey()\n const ownKey = getRnwindCacheKey()\n return upstreamKey ? `${upstreamKey}|${ownKey}` : ownKey\n}\n\n/** Test-only — drop the cached upstream so a new env var picks up next call. */\nexport function __resetUpstreamCache(): void {\n cachedUpstream = null\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transformer.mjs","sources":["../../../../src/metro/transformer.ts"],"sourcesContent":["import type { File } from '@babel/types'\nimport * as t from '@babel/types'\nimport { parse } from '@babel/parser'\nimport generate from '@babel/generator'\nimport { createHash } from 'node:crypto'\nimport { realpathSync } from 'node:fs'\nimport { transformAst } from './transform-ast'\nimport { getClassNamePrefixes, getHostComponents, getHostSources, getRnwindCacheKey, getRnwindState, onThemeChange } from './state'\nimport { STYLE_SPECIFIERS, THEME_SIGNATURE_MODULE } from './resolver'\nimport { filterUnknownClassCandidates } from './warn-unknown-classes'\n\n/** The shape of the upstream module we delegate parsing/babel work to. */\ninterface UpstreamTransformer {\n transform: (args: BabelTransformerArgs) => Promise<BabelTransformerResult> | BabelTransformerResult\n}\n\n/** Env var that points at the upstream `babelTransformerPath` we override. */\nconst UPSTREAM_ENV = 'RNWIND_UPSTREAM_TRANSFORMER'\n\n/** Cached upstream module — required once, reused across every transform call. */\nlet cachedUpstream: UpstreamTransformer | null = null\n\nconst generateModule = (generate as unknown as { default?: typeof generate }).default ?? generate\n\n/**\n * Parse user source with the broad plugin set (Flow + JSX + TypeScript\n * + class properties). Permissive on purpose so we don't reject any\n * file the upstream could have handled. Returns `null` when parse\n * fails — caller falls back to the raw source string.\n * @param source Source text.\n * @returns Parsed AST, or null on parse failure.\n */\nfunction parseUserSource(source: string): File | null {\n try {\n return parse(source, {\n sourceType: 'unambiguous',\n allowReturnOutsideFunction: true,\n allowImportExportEverywhere: true,\n plugins: ['typescript', 'jsx'],\n }) as unknown as File\n } catch {\n try {\n return parse(source, {\n sourceType: 'unambiguous',\n allowReturnOutsideFunction: true,\n allowImportExportEverywhere: true,\n plugins: ['flow', 'jsx'],\n }) as unknown as File\n } catch {\n return null\n }\n }\n}\n\n/**\n * Print Tailwind-shaped candidates oxide picked up but the parser\n * could NOT compile — typo, missing custom utility, or class not in\n * the user's theme. Filtering by candidates that ALSO appear inside a\n * `className=\"…\"` literal eliminates false positives from imports,\n * comments, and JSX prop values.\n * @param source Original source text — searched for className literals.\n * @param candidates Every candidate oxide surfaced from the source.\n * @param atoms Successfully resolved atoms (keys are class names).\n * @param filename Source path, prefixed onto the warning.\n */\nfunction warnUnknownClasses(\n source: string,\n candidates: readonly string[],\n atoms: ReadonlyMap<string, unknown>,\n filename: string,\n): void {\n const atomNames = new Set(atoms.keys())\n const unknown = filterUnknownClassCandidates(source, candidates, atomNames)\n if (unknown.length === 0) return\n // eslint-disable-next-line no-console\n console.warn(`rnwind: unknown class${unknown.length > 1 ? 'es' : ''} in ${filename}: ${unknown.join(', ')}`)\n}\n\n/**\n * Extract the bare extension for oxide / internal switches.\n * @param filename Absolute path.\n * @returns Extension without the leading dot (`tsx` / `ts` / `js` / `jsx`).\n */\nfunction extensionOf(filename: string): string {\n const index = filename.lastIndexOf('.')\n if (index === -1) return 'tsx'\n return filename.slice(index + 1)\n}\n\n/**\n * Read the project root Metro hands us per-transform. Falls back to\n * `process.cwd()` only when the upstream harness doesn't set it (unit\n * tests, standalone). Metro's production pipeline always sets it.\n * @param args Metro transformer args.\n * @returns Absolute project root.\n */\nfunction projectRootOf(args: BabelTransformerArgs): string {\n const fromOptions = args.options?.projectRoot\n if (typeof fromOptions === 'string' && fromOptions.length > 0) return fromOptions\n return process.cwd()\n}\n\n/**\n * Whether a `.css` filename is the user's theme entry (the file\n * `withRnwindConfig` pointed us at via `RNWIND_CSS_ENTRY_FILE`).\n * Only the theme CSS should trigger a scheme rebuild — unrelated CSS\n * files in the project stay invisible to rnwind.\n * @param filename Absolute CSS path.\n * @returns Whether the file is the configured theme entry.\n */\nfunction isThemeCssEntry(filename: string): boolean {\n const cssEntry = process.env.RNWIND_CSS_ENTRY_FILE\n return typeof cssEntry === 'string' && cssEntry.length > 0 && cssEntry === filename\n}\n\n/**\n * Parse + run rnwind's JSX rewrite + regenerate source code. When\n * parsing or transformation fails, fall back to the original source —\n * we don't want a transient parse error to crash Metro for a file the\n * upstream might handle fine.\n * @param args Metro args; `src` is the original source text.\n * @returns Rewritten source text (with `className=` rewrites applied).\n */\nasync function rewriteSource(args: BabelTransformerArgs): Promise<string> {\n const ast = parseUserSource(args.src)\n if (!ast) return args.src\n\n const state = getRnwindState(projectRootOf(args))\n const extension = extensionOf(args.filename)\n const parsed = await state.parser.parseAtoms({ content: args.src, extension })\n\n warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename)\n\n const classNamePrefixes = getClassNamePrefixes()\n const hostSources = getHostSources()\n const hostComponents = getHostComponents()\n if (parsed.atoms.size === 0) {\n state.builder.dropFile(args.filename)\n await state.builder.writeSchemes()\n transformAst(ast, {\n styleSpecifiers: [],\n gradientAtoms: parsed.gradientAtoms,\n hapticAtoms: parsed.hapticAtoms,\n classNamePrefixes,\n hostSources,\n hostComponents,\n })\n injectThemeSignatureImport(ast)\n return generateModule(ast).code\n }\n\n const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes)\n if (changed) await state.builder.writeSchemes()\n\n transformAst(ast, {\n styleSpecifiers: STYLE_SPECIFIERS as unknown as readonly string[],\n gradientAtoms: parsed.gradientAtoms,\n hapticAtoms: parsed.hapticAtoms,\n classNamePrefixes,\n hostSources,\n hostComponents,\n })\n injectThemeSignatureImport(ast)\n return generateModule(ast).code\n}\n\n/**\n * Prepend `import 'rnwind/__generated/theme-signature'` to every\n * rnwind-transformed file. The resolver maps that specifier to the\n * user's theme CSS so Metro's dependency graph carries a real edge\n * from this JS file to the CSS. When the user edits `global.css`,\n * the CSS module's SHA1 changes, and Metro invalidates every JS file\n * holding this import — forcing them to re-transform with the new\n * theme. The `.css` branch in {@link transform} returns an empty\n * `export {}` module so the runtime cost is one extra `require()`.\n * @param ast Babel File AST to mutate in place.\n */\nfunction injectThemeSignatureImport(ast: File): void {\n const declaration = t.importDeclaration([], t.stringLiteral(THEME_SIGNATURE_MODULE))\n ast.program.body.unshift(declaration)\n}\n\n/**\n * Read the upstream transformer's `getCacheKey()` so our cache-key\n * contribution composes with — rather than replaces — whatever the\n * host framework wants to mix in.\n * @returns Upstream cache key, or `null` when no upstream exposes one.\n */\nfunction loadUpstreamCacheKey(): string | null {\n const upstream = loadUpstream() as (UpstreamTransformer & { getCacheKey?: () => string }) | null\n if (!upstream) return null\n try {\n return typeof upstream.getCacheKey === 'function' ? upstream.getCacheKey() : null\n } catch {\n return null\n }\n}\n\n/**\n * Invoke the upstream `babelTransformerPath` Metro originally had\n * configured. The path is read from `RNWIND_UPSTREAM_TRANSFORMER`,\n * which `withRnwindConfig` sets at Metro startup. When the env var is\n * unset (unit tests, standalone use), fall back to a typescript+jsx\n * parse.\n * @param args Metro's per-file args.\n * @returns Upstream transform result containing the post-babel AST.\n */\nasync function runUpstream(args: BabelTransformerArgs): Promise<BabelTransformerResult> {\n if (args.ast && !process.env[UPSTREAM_ENV]) return { ast: args.ast }\n const upstream = loadUpstream()\n if (upstream) return await Promise.resolve(upstream.transform(args))\n if (args.ast) return { ast: args.ast }\n return { ast: parseSource(args.src) }\n}\n\n/**\n * Lazily require the upstream transformer module. Cached after first\n * load so per-file overhead is one cache lookup.\n * @returns Upstream module, or null when env is unset.\n */\nfunction loadUpstream(): UpstreamTransformer | null {\n if (cachedUpstream) return cachedUpstream\n const upstreamPath = process.env[UPSTREAM_ENV]\n if (!upstreamPath || upstreamPath.length === 0) return null\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires\n const required = require(upstreamPath) as UpstreamTransformer | { default?: UpstreamTransformer }\n const upstream = (required as { default?: UpstreamTransformer }).default ?? (required as UpstreamTransformer)\n if (typeof upstream.transform !== 'function') return null\n cachedUpstream = upstream\n return upstream\n } catch (error) {\n // eslint-disable-next-line no-console\n if (process.env.RNWIND_DEBUG) console.error('rnwind: failed to load upstream transformer:', error)\n return null\n }\n}\n\n/**\n * Cheap guard — the file has to look JS/TS, live outside `node_modules`,\n * and mention `className=` before we spend AST cycles on it.\n *\n * Symlink awareness: monorepo workspaces (yarn / pnpm / bun workspaces)\n * symlink each package into the consumer's `node_modules/<name>`, so a\n * file from `packages/ui/src/Foo.tsx` ends up reaching the transformer\n * as `<root>/node_modules/ui/src/Foo.tsx`. The naïve `/node_modules/`\n * check would skip every workspace UI file. We `realpath` the filename\n * once and only bail when the resolved real path is ALSO under\n * node_modules — true third-party installs.\n * @param args Metro args.\n * @returns Whether the file might need the rnwind pass.\n */\nfunction isRewriteCandidate(args: BabelTransformerArgs): boolean {\n if (!/\\.(?:tsx|ts|jsx|js)$/i.test(args.filename)) return false\n // Case-insensitive so `<prefix>ClassName=` (e.g. `contentContainerClassName=`)\n // — which has a capital `C` and so doesn't contain the lowercase\n // `className=` — still routes the file through the rewrite pass.\n if (!/classname=/i.test(args.src)) return false\n if (!args.filename.includes('/node_modules/')) return true\n // node_modules in path → could be a workspace symlink; resolve it.\n try {\n return !realpathSync(args.filename).includes('/node_modules/')\n } catch {\n // realpath failed (broken symlink, missing file). Fall back to skipping.\n return false\n }\n}\n\n/**\n * Fallback parse when no upstream is configured AND Metro didn't hand\n * us an AST. Used by unit tests and standalone setups.\n * @param source Source text.\n * @returns Parsed Babel File.\n */\nfunction parseSource(source: string): File {\n return parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] }) as unknown as File\n}\n\n/** Metro's babel transformer signature. */\nexport interface BabelTransformerArgs {\n filename: string\n src: string\n options: { projectRoot?: string; [key: string]: unknown }\n ast?: File\n plugins?: readonly unknown[]\n}\n\n/** Return shape Metro expects from a babel transformer. */\nexport interface BabelTransformerResult {\n ast: File\n metadata?: unknown\n}\n\n/**\n * rnwind's Metro babel transformer. Two phases per source file:\n *\n * 1. **Pre-process the source string before handing it to the upstream\n * babel pipeline.** babel-preset-expo / React's JSX transform run\n * inside the upstream and convert `<View className=\"...\"/>` into\n * `React.createElement(View, {className})`. If we walked the AST\n * AFTER the upstream, there'd be no JSX attributes left to\n * rewrite. So we parse, run our pass, regenerate code, and feed\n * THAT to the upstream as `src`.\n * 2. **Delegate to the upstream `babelTransformerPath`** (Expo's\n * default handles Flow stripping, expo-router macros, etc.).\n *\n * Skip both phases when the file isn't a JS/TS source under user\n * code, or doesn't mention `className=` — hand straight to upstream.\n * @param args Metro's per-file args.\n * @returns Mutated AST + metadata.\n */\nexport async function transform(args: BabelTransformerArgs): Promise<BabelTransformerResult> {\n // Short-circuit `.css` inputs: the theme CSS is pulled into the dep\n // graph as a sentinel (see `THEME_SIGNATURE_MODULE` in resolver.ts)\n // so Metro watches it and invalidates importers on edit, but the\n // file's CSS syntax can't go through a JS babel transformer.\n //\n // When the CSS being transformed IS the user's theme entry, we\n // piggyback on Metro's own file-watcher: Metro calls us here on\n // every CSS save; we trigger `onThemeChange` to rebuild parser +\n // rewrite scheme files with the new values. Metro's dep graph then\n // HMRs the regenerated `common.style.js` to the running app.\n //\n // Emitting the CSS content hash in the fake JS output is what makes\n // Metro propagate invalidation to downstream importers — constant\n // `export {}` bytes would never look changed and Metro would skip\n // the chain.\n if (args.filename.endsWith('.css')) {\n if (isThemeCssEntry(args.filename)) {\n try {\n await onThemeChange(projectRootOf(args))\n } catch {\n // CSS edit happened outside a configured project (e.g. tests).\n }\n }\n const themeHash = createHash('sha256').update(args.src).digest('hex').slice(0, 16)\n const stub = `export const __rnwindThemeHash = ${JSON.stringify(themeHash)};\\n`\n return { ast: parse(stub, { sourceType: 'module' }) as unknown as File }\n }\n if (!isRewriteCandidate(args)) {\n if (/\\.(?:tsx|ts|jsx|js)$/i.test(args.filename) && !args.filename.includes('/node_modules/')) {\n try {\n getRnwindState(projectRootOf(args)).builder.dropFile(args.filename)\n } catch {\n // State not configured (e.g. test). Nothing to drop.\n }\n }\n return runUpstream(args)\n }\n\n const rewrittenSource = await rewriteSource(args)\n return runUpstream({ ...args, src: rewrittenSource, ast: undefined })\n}\n\n/**\n * Metro's babel-transformer contract: a `getCacheKey()` export is\n * sampled per-file and mixed into the transform cache key. Returning\n * a string that includes the theme CSS content hash invalidates every\n * cached transform on every CSS edit — so the bundle rebuilds with\n * the new theme automatically on the next request.\n * @returns Cache-key segment that includes rnwind's current theme hash.\n */\nexport function getCacheKey(): string {\n const upstreamKey = loadUpstreamCacheKey()\n const ownKey = getRnwindCacheKey()\n return upstreamKey ? `${upstreamKey}|${ownKey}` : ownKey\n}\n\n/** Test-only — drop the cached upstream so a new env var picks up next call. */\nexport function __resetUpstreamCache(): void {\n cachedUpstream = null\n}\n"],"names":[],"mappings":"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"}
@@ -17,6 +17,28 @@ export interface RnwindMetroOptions {
17
17
  * entries merge on top.
18
18
  */
19
19
  classNamePrefixes?: readonly string[];
20
+ /**
21
+ * Extra module specifiers whose JSX exports rnwind should treat as
22
+ * "host components" — i.e. tags whose `className="…"` attribute is
23
+ * rewritten to `style={lookupCss(…)}` at build time (zero runtime
24
+ * cost). Merged with the built-in defaults: `react-native`,
25
+ * `react-native-reanimated`, `react-native-svg`,
26
+ * `react-native-gesture-handler`, `expo-linear-gradient`, `expo-image`.
27
+ *
28
+ * Anything NOT marked as a host has its `className` left untouched —
29
+ * the importing component receives the raw string and decides what
30
+ * to do with it. Use this option to opt your design-system / UI
31
+ * primitive packages into the zero-runtime path.
32
+ */
33
+ hostSources?: readonly string[];
34
+ /**
35
+ * Extra JSX tag names (verbatim — may include `.` for member access
36
+ * like `'Animated.View'`) rnwind should treat as host components,
37
+ * regardless of where they're imported from. Useful for one-off
38
+ * escape-hatches: `import { View as MyBox } from 'react-native'`
39
+ * doesn't change the local name → `'MyBox'` here picks it up.
40
+ */
41
+ hostComponents?: readonly string[];
20
42
  }
21
43
  /** Shape we mutate on Metro's config. Loose so we don't pin Metro's internal types. */
22
44
  export interface MetroConfigLike {
@@ -147,7 +147,7 @@ function withRnwindConfig(metroConfig, options) {
147
147
  const cssEntry = path.isAbsolute(options.cssEntryFile) ? options.cssEntryFile : path.resolve(projectRoot, options.cssEntryFile);
148
148
  mkdirSync(cacheDir, { recursive: true });
149
149
  const watchFolders = (metroConfig.watchFolders ?? []).filter((p) => typeof p === 'string' && p.length > 0);
150
- configureRnwindState(cssEntry, cacheDir, watchFolders, options.classNamePrefixes);
150
+ configureRnwindState(cssEntry, cacheDir, watchFolders, options.classNamePrefixes, options.hostSources, options.hostComponents);
151
151
  // Warm the state eagerly (in the Metro master process) so oxide's
152
152
  // Scanner walks every project source (and every monorepo
153
153
  // watch-folder) ONCE and the manifest + scheme files hold the
@@ -1 +1 @@
1
- {"version":3,"file":"with-config.mjs","sources":["../../../../src/metro/with-config.ts"],"sourcesContent":["import { existsSync, mkdirSync, utimesSync, watch as watchFile } from 'node:fs'\nimport path from 'node:path'\nimport { writeDtsFile } from './dts'\nimport { createRnwindResolver, type ResolveRequestFn } from './resolver'\nimport { configureRnwindState, getRnwindState, onThemeChange } from './state'\n\n/** Default cache directory at the project root. Visible for debugging. */\nconst DEFAULT_CACHE_DIR = '.rnwind'\n\n/**\n * Active CSS watcher — replaced (and the prior one closed) when\n * `withRnwindConfig` is called again (Metro restart, repeated init).\n * Only one watcher per process; no stacking.\n */\nlet activeCssWatcher: { cssPath: string; close: () => void } | null = null\n\n/**\n * Watch the theme CSS for edits. On change, rewrite the per-scheme\n * files with the fresh theme AND bump `mtime` on every source file\n * rnwind has transformed so far — Metro's own watcher sees those\n * mtime changes, invalidates the modules, and re-transforms them\n * against the new CSS on the next request. `getCacheKey()` alone is\n * NOT enough: Metro samples the cache key once per worker lifetime,\n * so edits during an already-running dev server don't propagate\n * without this explicit nudge.\n * @param cssPath Absolute path to the theme CSS to watch.\n * @param projectRoot Metro's project root (for `getRnwindState`).\n */\nfunction watchThemeCss(cssPath: string, projectRoot: string): void {\n if (activeCssWatcher?.cssPath === cssPath) return\n activeCssWatcher?.close()\n if (!existsSync(cssPath)) return\n let pending = false\n const watcher = watchFile(cssPath, { persistent: false }, () => {\n // Debounce: editors often emit 2-3 change events per save (atomic\n // rename dance, tmp files). Coalesce to ONE rebuild per microtask.\n if (pending) return\n pending = true\n queueMicrotask(async () => {\n pending = false\n try {\n await onThemeChange(projectRoot)\n touchRecordedFiles(projectRoot)\n } catch {\n // Invalidation is best-effort — never crash the dev server.\n }\n })\n })\n activeCssWatcher = { cssPath, close: () => watcher.close() }\n}\n\n/**\n * Bump the mtime on every file the builder has transformed. Metro's\n * file watcher keys on `mtime`, so this is what makes it invalidate\n * those modules and re-transform them.\n * @param projectRoot Metro's project root.\n */\nfunction touchRecordedFiles(projectRoot: string): void {\n const state = getRnwindState(projectRoot)\n const files = state.builder.recordedFiles()\n const now = new Date()\n for (const file of files) {\n try {\n if (existsSync(file)) utimesSync(file, now, now)\n } catch {\n // One file's stat failure shouldn't stop the others.\n }\n }\n}\n\n/**\n * Where the rnwind babel transformer lives — resolved relative to this\n * module so the path works from both the `src/` tree (tests) and the\n * built `lib/` output. Tries a few extensions because `require.resolve`\n * doesn't auto-find `.cjs` / `.mjs` from a bare specifier.\n * @returns Absolute path to the rnwind transformer module.\n */\nfunction transformerPath(): string {\n for (const candidate of ['./transformer.cjs', './transformer.mjs', './transformer.js', './transformer.ts', './transformer']) {\n try {\n return require.resolve(candidate)\n } catch {\n // try the next extension\n }\n }\n throw new Error('rnwind: could not resolve the metro transformer path')\n}\n\n/**\n * Resolve the effective cache directory, honoring a user override and\n * falling back to `<projectRoot>/.rnwind`.\n * @param projectRoot Anchor for relative paths.\n * @param override User-supplied option.\n * @returns Absolute cache directory.\n */\nfunction resolveCacheDir(projectRoot: string, override: string | undefined): string {\n if (!override || override.length === 0) return path.resolve(projectRoot, DEFAULT_CACHE_DIR)\n return path.isAbsolute(override) ? override : path.resolve(projectRoot, override)\n}\n\n/**\n * Read the theme CSS and extract `@variant <name>` blocks for the .d.ts\n * generator. Forces construction of `getRnwindState`, then reads\n * `parser.declaredSchemes` (populated synchronously at construction).\n * @param cssEntry Absolute path to theme CSS.\n * @param projectRoot\n * @returns Scheme names (empty when the theme has no variants; `'base'` is filtered).\n */\nfunction discoverSchemes(cssEntry: string, projectRoot: string): readonly string[] {\n if (!existsSync(cssEntry)) return []\n try {\n const { parser } = getRnwindState(projectRoot)\n return parser.declaredSchemes.filter((name) => name !== 'base')\n } catch {\n return []\n }\n}\n\n/** User-facing options for `withRnwindConfig`. */\nexport interface RnwindMetroOptions {\n /** Path to the theme CSS (absolute or relative to `projectRoot`). Required. */\n cssEntryFile: string\n /** Where rnwind writes the `.d.ts` file. Set `false` to disable. Defaults to `<projectRoot>/rnwind-types.d.ts`. */\n dtsFile?: string | false\n /** Optional project-root override — defaults to `metroConfig.projectRoot` then `process.cwd()`. */\n projectRoot?: string\n /** Cache directory. Absolute, or relative to `projectRoot`. Default: `.rnwind` at project root. */\n cacheDir?: string\n /**\n * Extra JSX prop-name prefixes that rnwind should rewrite. Each\n * prefix `P` turns `<Tag PClassName=\"…\">` into `<Tag\n * PStyle={lookupCss(…)}>`. The built-in `'contentContainer'` prefix\n * is always on (covers ScrollView / FlatList / SectionList); user\n * entries merge on top.\n */\n classNamePrefixes?: readonly string[]\n}\n\n/** Shape we mutate on Metro's config. Loose so we don't pin Metro's internal types. */\nexport interface MetroConfigLike {\n projectRoot?: string\n watchFolders?: string[]\n transformer?: {\n babelTransformerPath?: string\n [key: string]: unknown\n }\n resolver?: {\n resolveRequest?: ResolveRequestFn | null\n [key: string]: unknown\n }\n [key: string]: unknown\n}\n\n/**\n * Wrap a Metro config with rnwind's pipeline:\n * - Install the rnwind babel transformer.\n * - Chain a `resolveRequest` hook that serves\n * `rnwind/__generated/schemes` from `<cacheDir>/schemes.js`.\n * - Write the `.d.ts` so TypeScript accepts `className=` on RN components.\n * - Publish the theme CSS path + cache dir via env so Metro workers\n * can rebuild their local state.\n * - Ensure the cache dir is a watched folder Metro's haste-map indexes.\n *\n * Theme-edit hot reload happens implicitly: every transformed file\n * imports `rnwind/__generated/schemes`, and that module eager-imports\n * `common.style.js`. When the theme changes, the per-scheme files\n * regenerate with new bytes; Metro's content SHA1 dedup detects the\n * change and invalidates every importer automatically.\n * `getCacheKey()` on the transformer covers the per-file transform\n * cache. No file watcher / source-padding hack needed — the dep graph\n * carries the signal.\n * @param metroConfig Config from `getDefaultConfig(__dirname)` or equivalent.\n * @param options rnwind options.\n * @returns The same config, mutated.\n */\nexport function withRnwindConfig<C extends MetroConfigLike>(metroConfig: C, options: RnwindMetroOptions): C {\n const projectRoot = options.projectRoot ?? metroConfig.projectRoot ?? process.cwd()\n const cacheDir = resolveCacheDir(projectRoot, options.cacheDir)\n const cssEntry = path.isAbsolute(options.cssEntryFile) ? options.cssEntryFile : path.resolve(projectRoot, options.cssEntryFile)\n\n mkdirSync(cacheDir, { recursive: true })\n const watchFolders = (metroConfig.watchFolders ?? []).filter((p) => typeof p === 'string' && p.length > 0)\n configureRnwindState(cssEntry, cacheDir, watchFolders, options.classNamePrefixes)\n\n // Warm the state eagerly (in the Metro master process) so oxide's\n // Scanner walks every project source (and every monorepo\n // watch-folder) ONCE and the manifest + scheme files hold the\n // complete union before Metro's resolver tries to SHA1 them on the\n // first transform. Each worker lazy-repeats this scan on its first\n // transform to converge on identical state.\n try {\n void getRnwindState(projectRoot).builder.ensureFilesExist()\n } catch {\n // Any init error surfaces again at the first transform; don't crash Metro boot.\n }\n\n // Install transformer + resolver. Capture the existing\n // babelTransformerPath BEFORE we override it — our worker chains to\n // it (env-passed) so Flow / expo-router / babel-preset-expo etc. all\n // continue to run.\n const existingTransformerPath = metroConfig.transformer?.babelTransformerPath\n if (typeof existingTransformerPath === 'string' && existingTransformerPath.length > 0) {\n process.env.RNWIND_UPSTREAM_TRANSFORMER = existingTransformerPath\n }\n const upstream = metroConfig.resolver?.resolveRequest ?? null\n metroConfig.transformer = { ...metroConfig.transformer, babelTransformerPath: transformerPath() }\n metroConfig.resolver = { ...metroConfig.resolver, resolveRequest: createRnwindResolver(upstream) }\n\n // Metro's haste-map indexes `watchFolders` at startup. Adding the\n // cache dir guarantees scheme style files + manifest get SHA1'd\n // without a \"Failed to get the SHA-1\" race when the first transform\n // writes them.\n const existingWatch = metroConfig.watchFolders ?? []\n metroConfig.watchFolders = existingWatch.includes(cacheDir) ? existingWatch : [...existingWatch, cacheDir]\n\n if (options.dtsFile !== false) {\n const dtsPath = options.dtsFile ?? path.resolve(projectRoot, 'rnwind-types.d.ts')\n const schemes = discoverSchemes(cssEntry, projectRoot)\n writeDtsFile(dtsPath, schemes, options.classNamePrefixes)\n }\n\n // Watch the theme CSS. On edit, we rewrite scheme files AND touch\n // mtime on every transformed source file so Metro invalidates them\n // and re-transforms — the only reliable way to propagate theme\n // changes to an already-running dev server.\n watchThemeCss(cssEntry, projectRoot)\n\n return metroConfig\n}\n"],"names":["watchFile"],"mappings":"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"}
1
+ {"version":3,"file":"with-config.mjs","sources":["../../../../src/metro/with-config.ts"],"sourcesContent":["import { existsSync, mkdirSync, utimesSync, watch as watchFile } from 'node:fs'\nimport path from 'node:path'\nimport { writeDtsFile } from './dts'\nimport { createRnwindResolver, type ResolveRequestFn } from './resolver'\nimport { configureRnwindState, getRnwindState, onThemeChange } from './state'\n\n/** Default cache directory at the project root. Visible for debugging. */\nconst DEFAULT_CACHE_DIR = '.rnwind'\n\n/**\n * Active CSS watcher — replaced (and the prior one closed) when\n * `withRnwindConfig` is called again (Metro restart, repeated init).\n * Only one watcher per process; no stacking.\n */\nlet activeCssWatcher: { cssPath: string; close: () => void } | null = null\n\n/**\n * Watch the theme CSS for edits. On change, rewrite the per-scheme\n * files with the fresh theme AND bump `mtime` on every source file\n * rnwind has transformed so far — Metro's own watcher sees those\n * mtime changes, invalidates the modules, and re-transforms them\n * against the new CSS on the next request. `getCacheKey()` alone is\n * NOT enough: Metro samples the cache key once per worker lifetime,\n * so edits during an already-running dev server don't propagate\n * without this explicit nudge.\n * @param cssPath Absolute path to the theme CSS to watch.\n * @param projectRoot Metro's project root (for `getRnwindState`).\n */\nfunction watchThemeCss(cssPath: string, projectRoot: string): void {\n if (activeCssWatcher?.cssPath === cssPath) return\n activeCssWatcher?.close()\n if (!existsSync(cssPath)) return\n let pending = false\n const watcher = watchFile(cssPath, { persistent: false }, () => {\n // Debounce: editors often emit 2-3 change events per save (atomic\n // rename dance, tmp files). Coalesce to ONE rebuild per microtask.\n if (pending) return\n pending = true\n queueMicrotask(async () => {\n pending = false\n try {\n await onThemeChange(projectRoot)\n touchRecordedFiles(projectRoot)\n } catch {\n // Invalidation is best-effort — never crash the dev server.\n }\n })\n })\n activeCssWatcher = { cssPath, close: () => watcher.close() }\n}\n\n/**\n * Bump the mtime on every file the builder has transformed. Metro's\n * file watcher keys on `mtime`, so this is what makes it invalidate\n * those modules and re-transform them.\n * @param projectRoot Metro's project root.\n */\nfunction touchRecordedFiles(projectRoot: string): void {\n const state = getRnwindState(projectRoot)\n const files = state.builder.recordedFiles()\n const now = new Date()\n for (const file of files) {\n try {\n if (existsSync(file)) utimesSync(file, now, now)\n } catch {\n // One file's stat failure shouldn't stop the others.\n }\n }\n}\n\n/**\n * Where the rnwind babel transformer lives — resolved relative to this\n * module so the path works from both the `src/` tree (tests) and the\n * built `lib/` output. Tries a few extensions because `require.resolve`\n * doesn't auto-find `.cjs` / `.mjs` from a bare specifier.\n * @returns Absolute path to the rnwind transformer module.\n */\nfunction transformerPath(): string {\n for (const candidate of ['./transformer.cjs', './transformer.mjs', './transformer.js', './transformer.ts', './transformer']) {\n try {\n return require.resolve(candidate)\n } catch {\n // try the next extension\n }\n }\n throw new Error('rnwind: could not resolve the metro transformer path')\n}\n\n/**\n * Resolve the effective cache directory, honoring a user override and\n * falling back to `<projectRoot>/.rnwind`.\n * @param projectRoot Anchor for relative paths.\n * @param override User-supplied option.\n * @returns Absolute cache directory.\n */\nfunction resolveCacheDir(projectRoot: string, override: string | undefined): string {\n if (!override || override.length === 0) return path.resolve(projectRoot, DEFAULT_CACHE_DIR)\n return path.isAbsolute(override) ? override : path.resolve(projectRoot, override)\n}\n\n/**\n * Read the theme CSS and extract `@variant <name>` blocks for the .d.ts\n * generator. Forces construction of `getRnwindState`, then reads\n * `parser.declaredSchemes` (populated synchronously at construction).\n * @param cssEntry Absolute path to theme CSS.\n * @param projectRoot\n * @returns Scheme names (empty when the theme has no variants; `'base'` is filtered).\n */\nfunction discoverSchemes(cssEntry: string, projectRoot: string): readonly string[] {\n if (!existsSync(cssEntry)) return []\n try {\n const { parser } = getRnwindState(projectRoot)\n return parser.declaredSchemes.filter((name) => name !== 'base')\n } catch {\n return []\n }\n}\n\n/** User-facing options for `withRnwindConfig`. */\nexport interface RnwindMetroOptions {\n /** Path to the theme CSS (absolute or relative to `projectRoot`). Required. */\n cssEntryFile: string\n /** Where rnwind writes the `.d.ts` file. Set `false` to disable. Defaults to `<projectRoot>/rnwind-types.d.ts`. */\n dtsFile?: string | false\n /** Optional project-root override — defaults to `metroConfig.projectRoot` then `process.cwd()`. */\n projectRoot?: string\n /** Cache directory. Absolute, or relative to `projectRoot`. Default: `.rnwind` at project root. */\n cacheDir?: string\n /**\n * Extra JSX prop-name prefixes that rnwind should rewrite. Each\n * prefix `P` turns `<Tag PClassName=\"…\">` into `<Tag\n * PStyle={lookupCss(…)}>`. The built-in `'contentContainer'` prefix\n * is always on (covers ScrollView / FlatList / SectionList); user\n * entries merge on top.\n */\n classNamePrefixes?: readonly string[]\n /**\n * Extra module specifiers whose JSX exports rnwind should treat as\n * \"host components\" — i.e. tags whose `className=\"…\"` attribute is\n * rewritten to `style={lookupCss(…)}` at build time (zero runtime\n * cost). Merged with the built-in defaults: `react-native`,\n * `react-native-reanimated`, `react-native-svg`,\n * `react-native-gesture-handler`, `expo-linear-gradient`, `expo-image`.\n *\n * Anything NOT marked as a host has its `className` left untouched —\n * the importing component receives the raw string and decides what\n * to do with it. Use this option to opt your design-system / UI\n * primitive packages into the zero-runtime path.\n */\n hostSources?: readonly string[]\n /**\n * Extra JSX tag names (verbatim — may include `.` for member access\n * like `'Animated.View'`) rnwind should treat as host components,\n * regardless of where they're imported from. Useful for one-off\n * escape-hatches: `import { View as MyBox } from 'react-native'`\n * doesn't change the local name → `'MyBox'` here picks it up.\n */\n hostComponents?: readonly string[]\n}\n\n/** Shape we mutate on Metro's config. Loose so we don't pin Metro's internal types. */\nexport interface MetroConfigLike {\n projectRoot?: string\n watchFolders?: string[]\n transformer?: {\n babelTransformerPath?: string\n [key: string]: unknown\n }\n resolver?: {\n resolveRequest?: ResolveRequestFn | null\n [key: string]: unknown\n }\n [key: string]: unknown\n}\n\n/**\n * Wrap a Metro config with rnwind's pipeline:\n * - Install the rnwind babel transformer.\n * - Chain a `resolveRequest` hook that serves\n * `rnwind/__generated/schemes` from `<cacheDir>/schemes.js`.\n * - Write the `.d.ts` so TypeScript accepts `className=` on RN components.\n * - Publish the theme CSS path + cache dir via env so Metro workers\n * can rebuild their local state.\n * - Ensure the cache dir is a watched folder Metro's haste-map indexes.\n *\n * Theme-edit hot reload happens implicitly: every transformed file\n * imports `rnwind/__generated/schemes`, and that module eager-imports\n * `common.style.js`. When the theme changes, the per-scheme files\n * regenerate with new bytes; Metro's content SHA1 dedup detects the\n * change and invalidates every importer automatically.\n * `getCacheKey()` on the transformer covers the per-file transform\n * cache. No file watcher / source-padding hack needed — the dep graph\n * carries the signal.\n * @param metroConfig Config from `getDefaultConfig(__dirname)` or equivalent.\n * @param options rnwind options.\n * @returns The same config, mutated.\n */\nexport function withRnwindConfig<C extends MetroConfigLike>(metroConfig: C, options: RnwindMetroOptions): C {\n const projectRoot = options.projectRoot ?? metroConfig.projectRoot ?? process.cwd()\n const cacheDir = resolveCacheDir(projectRoot, options.cacheDir)\n const cssEntry = path.isAbsolute(options.cssEntryFile) ? options.cssEntryFile : path.resolve(projectRoot, options.cssEntryFile)\n\n mkdirSync(cacheDir, { recursive: true })\n const watchFolders = (metroConfig.watchFolders ?? []).filter((p) => typeof p === 'string' && p.length > 0)\n configureRnwindState(cssEntry, cacheDir, watchFolders, options.classNamePrefixes, options.hostSources, options.hostComponents)\n\n // Warm the state eagerly (in the Metro master process) so oxide's\n // Scanner walks every project source (and every monorepo\n // watch-folder) ONCE and the manifest + scheme files hold the\n // complete union before Metro's resolver tries to SHA1 them on the\n // first transform. Each worker lazy-repeats this scan on its first\n // transform to converge on identical state.\n try {\n void getRnwindState(projectRoot).builder.ensureFilesExist()\n } catch {\n // Any init error surfaces again at the first transform; don't crash Metro boot.\n }\n\n // Install transformer + resolver. Capture the existing\n // babelTransformerPath BEFORE we override it — our worker chains to\n // it (env-passed) so Flow / expo-router / babel-preset-expo etc. all\n // continue to run.\n const existingTransformerPath = metroConfig.transformer?.babelTransformerPath\n if (typeof existingTransformerPath === 'string' && existingTransformerPath.length > 0) {\n process.env.RNWIND_UPSTREAM_TRANSFORMER = existingTransformerPath\n }\n const upstream = metroConfig.resolver?.resolveRequest ?? null\n metroConfig.transformer = { ...metroConfig.transformer, babelTransformerPath: transformerPath() }\n metroConfig.resolver = { ...metroConfig.resolver, resolveRequest: createRnwindResolver(upstream) }\n\n // Metro's haste-map indexes `watchFolders` at startup. Adding the\n // cache dir guarantees scheme style files + manifest get SHA1'd\n // without a \"Failed to get the SHA-1\" race when the first transform\n // writes them.\n const existingWatch = metroConfig.watchFolders ?? []\n metroConfig.watchFolders = existingWatch.includes(cacheDir) ? existingWatch : [...existingWatch, cacheDir]\n\n if (options.dtsFile !== false) {\n const dtsPath = options.dtsFile ?? path.resolve(projectRoot, 'rnwind-types.d.ts')\n const schemes = discoverSchemes(cssEntry, projectRoot)\n writeDtsFile(dtsPath, schemes, options.classNamePrefixes)\n }\n\n // Watch the theme CSS. On edit, we rewrite scheme files AND touch\n // mtime on every transformed source file so Metro invalidates them\n // and re-transforms — the only reliable way to propagate theme\n // changes to an already-running dev server.\n watchThemeCss(cssEntry, projectRoot)\n\n return metroConfig\n}\n"],"names":["watchFile"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnwind",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Tailwind for React Native",
5
5
  "author": "https://github.com/sagltd",
6
6
  "license": "MIT",
@@ -96,6 +96,7 @@
96
96
  "@types/culori": "^4.0.1",
97
97
  "@types/node": "^25.6.0",
98
98
  "metro-babel-transformer": "^0.84.3",
99
+ "tailwind-variants": "^3.2.2",
99
100
  "tinybench": "^6.0.0"
100
101
  },
101
102
  "dependencies": {
@@ -73,6 +73,23 @@ function culoriHexFor(color: LABColor): string | null {
73
73
  }
74
74
  }
75
75
 
76
+ /**
77
+ * Composite a culori-produced sRGB hex with the source alpha into the RN
78
+ * color string. Shared tail for every culori-backed conversion (lab
79
+ * family, XYZ, wide-gamut RGB): opaque → the hex as-is; translucent →
80
+ * `rgba(...)` rebuilt from the hex channels.
81
+ * @param hex sRGB hex from culori, or `null` when culori rejected the color.
82
+ * @param alpha Source alpha (0–1).
83
+ * @returns RN color string.
84
+ */
85
+ function withAlpha(hex: string | null, alpha: number): string {
86
+ if (!hex) return alpha < 1 ? 'rgba(0, 0, 0, 0)' : 'transparent'
87
+ if (alpha >= 1) return hex
88
+ const back = culoriRgb(hex)
89
+ if (!back) return hex
90
+ return rgbIntsToString(clampByte(back.r * 255), clampByte(back.g * 255), clampByte(back.b * 255), alpha)
91
+ }
92
+
76
93
  /**
77
94
  * Convert a LAB / LCH / OKLAB / OKLCH color to sRGB hex via culori. RN
78
95
  * can't evaluate these modern color spaces at paint time; compile-time
@@ -81,12 +98,24 @@ function culoriHexFor(color: LABColor): string | null {
81
98
  * @returns Hex or rgba string in sRGB.
82
99
  */
83
100
  function labFamilyToHex(color: LABColor): string {
84
- const hex = culoriHexFor(color)
85
- if (!hex) return color.alpha < 1 ? 'rgba(0, 0, 0, 0)' : 'transparent'
86
- if (color.alpha >= 1) return hex
87
- const back = culoriRgb(hex)
88
- if (!back) return hex
89
- return rgbIntsToString(clampByte(back.r * 255), clampByte(back.g * 255), clampByte(back.b * 255), color.alpha)
101
+ return withAlpha(culoriHexFor(color), color.alpha)
102
+ }
103
+
104
+ /**
105
+ * Convert a wide-gamut `color(<space> r g b)` triple to sRGB hex via
106
+ * culori. The channels are NOT sRGB each space (display-p3, rec2020,
107
+ * a98-rgb, prophoto-rgb, srgb-linear) carries its own primaries / transfer
108
+ * function, so a bare `channel * 255` would mis-paint. culori does the
109
+ * gamut + gamma conversion to sRGB.
110
+ * @param mode culori mode key for the source space.
111
+ * @param r Source red (0–1).
112
+ * @param g Source green (0–1).
113
+ * @param b Source blue (0–1).
114
+ * @param alpha Alpha channel (0–1).
115
+ * @returns sRGB color string RN accepts.
116
+ */
117
+ function wideGamutToHex(mode: 'lrgb' | 'p3' | 'a98' | 'prophoto' | 'rec2020', r: number, g: number, b: number, alpha: number): string {
118
+ return withAlpha(formatHex({ mode, r, g, b }) ?? null, alpha)
90
119
  }
91
120
 
92
121
  /**
@@ -102,12 +131,7 @@ function labFamilyToHex(color: LABColor): string {
102
131
  */
103
132
  function xyzToHex(color: { type: 'xyz-d50' | 'xyz-d65'; x: number; y: number; z: number; alpha: number }): string {
104
133
  const mode = color.type === 'xyz-d50' ? 'xyz50' : 'xyz65'
105
- const hex = formatHex({ mode, x: color.x, y: color.y, z: color.z }) ?? null
106
- if (!hex) return color.alpha < 1 ? 'rgba(0, 0, 0, 0)' : 'transparent'
107
- if (color.alpha >= 1) return hex
108
- const back = culoriRgb(hex)
109
- if (!back) return hex
110
- return rgbIntsToString(clampByte(back.r * 255), clampByte(back.g * 255), clampByte(back.b * 255), color.alpha)
134
+ return withAlpha(formatHex({ mode, x: color.x, y: color.y, z: color.z }) ?? null, color.alpha)
111
135
  }
112
136
 
113
137
  /**
@@ -132,14 +156,24 @@ export function cssColorToString(color: CssColor): string {
132
156
  case 'oklch': {
133
157
  return labFamilyToHex(color)
134
158
  }
135
- case 'srgb':
136
- case 'srgb-linear':
137
- case 'display-p3':
138
- case 'a98-rgb':
139
- case 'prophoto-rgb':
140
- case 'rec2020': {
159
+ case 'srgb': {
141
160
  return floatRgbToString(color.r, color.g, color.b, color.alpha)
142
161
  }
162
+ case 'srgb-linear': {
163
+ return wideGamutToHex('lrgb', color.r, color.g, color.b, color.alpha)
164
+ }
165
+ case 'display-p3': {
166
+ return wideGamutToHex('p3', color.r, color.g, color.b, color.alpha)
167
+ }
168
+ case 'a98-rgb': {
169
+ return wideGamutToHex('a98', color.r, color.g, color.b, color.alpha)
170
+ }
171
+ case 'prophoto-rgb': {
172
+ return wideGamutToHex('prophoto', color.r, color.g, color.b, color.alpha)
173
+ }
174
+ case 'rec2020': {
175
+ return wideGamutToHex('rec2020', color.r, color.g, color.b, color.alpha)
176
+ }
143
177
  case 'xyz-d50':
144
178
  case 'xyz-d65': {
145
179
  return xyzToHex(color)
@@ -85,6 +85,25 @@ export function dispatchLayoutDeclaration(decl: LcDeclaration): readonly RNEntry
85
85
  const v = mapJustifyKeyword(decl.value)
86
86
  return v === null ? [] : [['justifyContent', v]]
87
87
  }
88
+ case 'overflow': {
89
+ // Lightningcss splits CSS `overflow` into `{x, y}` axes; RN only
90
+ // supports a single `overflow` keyword (and only `'hidden' |
91
+ // 'visible' | 'scroll'` on iOS, `'hidden' | 'visible'` on
92
+ // Android — RN ignores unsupported keywords at runtime). Take
93
+ // the `x` axis when the user wrote shorthand; per-axis Tailwind
94
+ // utilities both emit shorthand here so axis splitting is rare.
95
+ const value = decl.value as { x?: unknown; y?: unknown }
96
+ if (typeof value.x !== 'string') return []
97
+ return [['overflow', value.x]]
98
+ }
99
+ case 'overflow-x':
100
+ case 'overflow-y': {
101
+ // Tailwind's `overflow-x-*` / `overflow-y-*` emit these longhands,
102
+ // not the `overflow` shorthand. RN has only a single `overflow`,
103
+ // so collapse both axes onto it (last one declared wins via the
104
+ // normal entry-merge order).
105
+ return typeof decl.value === 'string' ? [['overflow', decl.value]] : []
106
+ }
88
107
  default: {
89
108
  return null
90
109
  }
@@ -25,24 +25,38 @@ function roundFloat(n: number): number {
25
25
  return Math.round(n * 10_000) / 10_000
26
26
  }
27
27
 
28
+ /**
29
+ * "Fully rounded" sentinel — emitted for Tailwind's `rounded-full` (and
30
+ * any other utility expanding to `calc(infinity * 1px)`). RN can't
31
+ * render `Infinity` as a style value (the StyleSheet validator silently
32
+ * drops it), but it accepts a finite large pixel count and renders the
33
+ * same pill / circle shape. 9999 covers every realistic phone screen.
34
+ */
35
+ const FULLY_ROUNDED_PX = 9999
36
+
28
37
  /**
29
38
  * Convert a lightningcss `LengthValue` to a pixel number. Handles the
30
- * units Tailwind emits: px, rem, em. Unknown units pass through as the
31
- * raw numeric value so a later pass can warn.
39
+ * units Tailwind emits: px, rem, em. Tailwind v4's "fully rounded"
40
+ * expansion (`calc(infinity * 1px)`) lands here as `value === Infinity`
41
+ * — we clamp to a finite sentinel so RN can render it. Other non-finite
42
+ * values (NaN from a malformed expression) are clamped to 0 rather
43
+ * than leaking through as `null` in the serialized RN style.
32
44
  * @param length Typed length value.
33
- * @returns Pixel number.
45
+ * @returns Finite pixel number.
34
46
  */
35
47
  export function lengthToPx(length: LengthValue): number {
48
+ const raw = length.value
49
+ if (!Number.isFinite(raw)) return raw === Number.POSITIVE_INFINITY ? FULLY_ROUNDED_PX : 0
36
50
  switch (length.unit) {
37
51
  case 'px': {
38
- return length.value
52
+ return raw
39
53
  }
40
54
  case 'rem':
41
55
  case 'em': {
42
- return length.value * REM_TO_PX
56
+ return raw * REM_TO_PX
43
57
  }
44
58
  default: {
45
- return length.value
59
+ return raw
46
60
  }
47
61
  }
48
62
  }
@@ -16,8 +16,11 @@ import type { RNEntry } from './types'
16
16
 
17
17
  /**
18
18
  * Expand `margin` / `padding` shorthand (`{top, right, bottom, left}`) to
19
- * RN entries. When all four sides share the same converted value, collapse
20
- * to the single-key shorthand RN accepts; otherwise emit four longhands.
19
+ * RN entries. Collapses progressively for a smaller emitted style:
20
+ * - all four equal single `padding` / `margin`
21
+ * - matching axes → `paddingHorizontal` + `paddingVertical`
22
+ * - one matching axis → that axis collapsed, opposite axis as longhands
23
+ * - otherwise → four longhands
21
24
  * @param property `'padding'` or `'margin'`.
22
25
  * @param value Typed shorthand record.
23
26
  * @returns RN entries.
@@ -29,6 +32,28 @@ export function expandFourSided(property: 'padding' | 'margin', value: Padding |
29
32
  const left = lengthPercentageOrAutoToValue(value.left)
30
33
  if (top === null || right === null || bottom === null || left === null) return []
31
34
  if (top === right && right === bottom && bottom === left) return [[property, top]]
35
+ const horizontalEqual = left === right
36
+ const verticalEqual = top === bottom
37
+ if (horizontalEqual && verticalEqual) {
38
+ return [
39
+ [`${property}Vertical`, top],
40
+ [`${property}Horizontal`, left],
41
+ ]
42
+ }
43
+ if (horizontalEqual) {
44
+ return [
45
+ [`${property}Top`, top],
46
+ [`${property}Bottom`, bottom],
47
+ [`${property}Horizontal`, left],
48
+ ]
49
+ }
50
+ if (verticalEqual) {
51
+ return [
52
+ [`${property}Vertical`, top],
53
+ [`${property}Right`, right],
54
+ [`${property}Left`, left],
55
+ ]
56
+ }
32
57
  return [
33
58
  [`${property}Top`, top],
34
59
  [`${property}Right`, right],
@@ -39,8 +64,10 @@ export function expandFourSided(property: 'padding' | 'margin', value: Padding |
39
64
 
40
65
  /**
41
66
  * Expand `padding-inline` / `margin-inline` (logical property) into RN's
42
- * physical left / right pair. RN has no RTL-aware logical props at the
43
- * style-object level, so we lower at compile time.
67
+ * physical pair. RN has no RTL-aware logical props at the style-object
68
+ * level, so we lower at compile time. When both sides match, emit the
69
+ * single `paddingHorizontal` / `marginHorizontal` shorthand for a more
70
+ * compact style.
44
71
  * @param property `'padding'` or `'margin'`.
45
72
  * @param value Typed inline shorthand.
46
73
  * @returns RN entries.
@@ -49,6 +76,7 @@ export function expandLogicalInline(property: 'padding' | 'margin', value: Paddi
49
76
  const start = lengthPercentageOrAutoToValue(value.inlineStart)
50
77
  const end = lengthPercentageOrAutoToValue(value.inlineEnd)
51
78
  if (start === null || end === null) return []
79
+ if (start === end) return [[`${property}Horizontal`, start]]
52
80
  return [
53
81
  [`${property}Left`, start],
54
82
  [`${property}Right`, end],
@@ -57,7 +85,8 @@ export function expandLogicalInline(property: 'padding' | 'margin', value: Paddi
57
85
 
58
86
  /**
59
87
  * Expand `padding-block` / `margin-block` (logical property) into RN's
60
- * physical top / bottom pair.
88
+ * physical pair. When both sides match, emit `paddingVertical` /
89
+ * `marginVertical` for a more compact style.
61
90
  * @param property `'padding'` or `'margin'`.
62
91
  * @param value Typed block shorthand.
63
92
  * @returns RN entries.
@@ -66,6 +95,7 @@ export function expandLogicalBlock(property: 'padding' | 'margin', value: Paddin
66
95
  const start = lengthPercentageOrAutoToValue(value.blockStart)
67
96
  const end = lengthPercentageOrAutoToValue(value.blockEnd)
68
97
  if (start === null || end === null) return []
98
+ if (start === end) return [[`${property}Vertical`, start]]
69
99
  return [
70
100
  [`${property}Top`, start],
71
101
  [`${property}Bottom`, end],
@@ -385,6 +385,59 @@ export function extractSchemeAliases(css: string): Map<string, string> {
385
385
  return aliases
386
386
  }
387
387
 
388
+ /** Single class token (`.scheme-dark`) — non-global so `.test` is stateless. */
389
+ const CLASS_TOKEN = /\.[A-Za-z_][\w-]*/
390
+
391
+ /**
392
+ * Whether a `@custom-variant` selector body targets an ancestor *class*
393
+ * container (`&:where(.scheme-dark, .scheme-dark *)`, `.dark`). That's
394
+ * the only shape rnwind resolves as a runtime scheme — the parser's
395
+ * nested-rule matcher keys schemes off `.class` selectors. Pseudo-class
396
+ * / `@media` / `@supports` custom-variants (`&:hover`, `@supports
397
+ * (display: grid)`) carry no class and are ordinary Tailwind variants,
398
+ * not schemes.
399
+ * @param selector Selector body captured after the variant name.
400
+ * @returns True when the selector contains at least one class token.
401
+ */
402
+ function isSchemeSelector(selector: string): boolean {
403
+ return CLASS_TOKEN.test(selector)
404
+ }
405
+
406
+ /**
407
+ * Collect every scheme name declared via `@custom-variant <name>
408
+ * (<class-selector>);` in first-appearance order.
409
+ *
410
+ * A scheme can be declared this way WITHOUT a matching `@variant <name>
411
+ * { … }` override block — its values then come entirely from the base
412
+ * `@theme`. That's Tailwind v4's standard shape: light defaults sit in
413
+ * `@theme` and only `@variant dark { … }` overrides them. Such a scheme
414
+ * still has to register so the runtime can switch to it, but
415
+ * {@link extractThemeVars} (which only sees `@variant` blocks) never
416
+ * surfaces it. The parser unions these names into its declared-scheme
417
+ * list so the base-only scheme isn't dropped.
418
+ *
419
+ * Only class-container selectors count — `@custom-variant` is Tailwind's
420
+ * general variant mechanism, so hover / focus / media / supports
421
+ * variants must NOT inflate the scheme list (or the generated `Scheme`
422
+ * union). See {@link isSchemeSelector}.
423
+ * @param css Theme CSS source.
424
+ * @returns Ordered, de-duplicated `@custom-variant` scheme names.
425
+ */
426
+ export function extractCustomVariantSchemes(css: string): string[] {
427
+ const stripped = stripComments(css)
428
+ const seen = new Set<string>()
429
+ const out: string[] = []
430
+ for (const match of stripped.matchAll(CUSTOM_VARIANT_WITH_SELECTOR)) {
431
+ const name = match[1]!
432
+ const selector = match[2]!
433
+ if (name === BASE_SCHEME || seen.has(name)) continue
434
+ if (!isSchemeSelector(selector)) continue
435
+ seen.add(name)
436
+ out.push(name)
437
+ }
438
+ return out
439
+ }
440
+
388
441
  /**
389
442
  * Rewrite the theme CSS so Tailwind's compiler accepts it:
390
443
  * 1. Strip every `@variant <name> { ... }` block — Tailwind rejects