rnwind 0.0.2 → 0.0.4

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 (87) 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/css-imports.cjs +81 -0
  24. package/lib/cjs/metro/css-imports.cjs.map +1 -0
  25. package/lib/cjs/metro/css-imports.d.ts +8 -0
  26. package/lib/cjs/metro/state.cjs +60 -10
  27. package/lib/cjs/metro/state.cjs.map +1 -1
  28. package/lib/cjs/metro/state.d.ts +17 -1
  29. package/lib/cjs/metro/transform-ast.cjs +238 -21
  30. package/lib/cjs/metro/transform-ast.cjs.map +1 -1
  31. package/lib/cjs/metro/transform-ast.d.ts +15 -0
  32. package/lib/cjs/metro/transformer.cjs +29 -2
  33. package/lib/cjs/metro/transformer.cjs.map +1 -1
  34. package/lib/cjs/metro/with-config.cjs +1 -1
  35. package/lib/cjs/metro/with-config.cjs.map +1 -1
  36. package/lib/cjs/metro/with-config.d.ts +22 -0
  37. package/lib/esm/core/parser/color.mjs +53 -24
  38. package/lib/esm/core/parser/color.mjs.map +1 -1
  39. package/lib/esm/core/parser/layout-dispatcher.mjs +20 -0
  40. package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
  41. package/lib/esm/core/parser/length.d.ts +6 -3
  42. package/lib/esm/core/parser/length.mjs +20 -6
  43. package/lib/esm/core/parser/length.mjs.map +1 -1
  44. package/lib/esm/core/parser/shorthand.d.ts +11 -5
  45. package/lib/esm/core/parser/shorthand.mjs +37 -5
  46. package/lib/esm/core/parser/shorthand.mjs.map +1 -1
  47. package/lib/esm/core/parser/theme-vars.d.ts +21 -0
  48. package/lib/esm/core/parser/theme-vars.mjs +53 -1
  49. package/lib/esm/core/parser/theme-vars.mjs.map +1 -1
  50. package/lib/esm/core/parser/tokens.mjs +183 -1
  51. package/lib/esm/core/parser/tokens.mjs.map +1 -1
  52. package/lib/esm/core/parser/tw-parser.d.ts +21 -5
  53. package/lib/esm/core/parser/tw-parser.mjs +141 -28
  54. package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
  55. package/lib/esm/core/parser/typography-dispatcher.mjs +16 -1
  56. package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
  57. package/lib/esm/core/style-builder/build-style.mjs +73 -26
  58. package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
  59. package/lib/esm/metro/css-imports.d.ts +8 -0
  60. package/lib/esm/metro/css-imports.mjs +79 -0
  61. package/lib/esm/metro/css-imports.mjs.map +1 -0
  62. package/lib/esm/metro/state.d.ts +17 -1
  63. package/lib/esm/metro/state.mjs +60 -12
  64. package/lib/esm/metro/state.mjs.map +1 -1
  65. package/lib/esm/metro/transform-ast.d.ts +15 -0
  66. package/lib/esm/metro/transform-ast.mjs +238 -21
  67. package/lib/esm/metro/transform-ast.mjs.map +1 -1
  68. package/lib/esm/metro/transformer.mjs +30 -3
  69. package/lib/esm/metro/transformer.mjs.map +1 -1
  70. package/lib/esm/metro/with-config.d.ts +22 -0
  71. package/lib/esm/metro/with-config.mjs +1 -1
  72. package/lib/esm/metro/with-config.mjs.map +1 -1
  73. package/package.json +2 -1
  74. package/src/core/parser/color.ts +52 -18
  75. package/src/core/parser/layout-dispatcher.ts +19 -0
  76. package/src/core/parser/length.ts +20 -6
  77. package/src/core/parser/shorthand.ts +35 -5
  78. package/src/core/parser/theme-vars.ts +53 -0
  79. package/src/core/parser/tokens.ts +171 -1
  80. package/src/core/parser/tw-parser.ts +147 -28
  81. package/src/core/parser/typography-dispatcher.ts +15 -1
  82. package/src/core/style-builder/build-style.ts +84 -26
  83. package/src/metro/css-imports.ts +75 -0
  84. package/src/metro/state.ts +58 -10
  85. package/src/metro/transform-ast.ts +249 -18
  86. package/src/metro/transformer.ts +28 -3
  87. package/src/metro/with-config.ts +23 -1
@@ -1 +1 @@
1
- {"version":3,"file":"state.mjs","sources":["../../../../src/metro/state.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from 'node:fs'\nimport path from 'node:path'\nimport { createHash } from 'node:crypto'\nimport { UnionBuilder } from '../core/style-builder'\nimport { TailwindParser, type SourceEntry } from '../core/parser'\n\n/**\n * Default oxide Scanner globs — walk every JS/TS source under the\n * project root AND every monorepo watch folder, excluding\n * `node_modules` and rnwind's own cache dir so we don't rescan\n * generated scheme files.\n *\n * Monorepo layouts (Yarn workspaces, pnpm workspaces, Nx) surface\n * sibling package roots as `metroConfig.watchFolders`. Every folder\n * Metro watches must also be scanned so atoms declared in shared UI\n * packages make it into the union — without this, only the app's\n * own files would be scanned and every UI-package atom would resolve\n * to `undefined` at runtime.\n * @param projectRoot Absolute project root.\n * @param cacheDir Absolute rnwind cache dir (to exclude).\n * @param watchFolders Extra monorepo roots Metro is watching.\n * @returns Scanner sources suitable for `parser.parseProject()`.\n */\nfunction defaultSources(projectRoot: string, cacheDir: string, watchFolders: readonly string[]): readonly SourceEntry[] {\n const cacheBaseName = path.basename(cacheDir)\n const roots = new Set<string>([projectRoot, ...watchFolders])\n const sources: SourceEntry[] = []\n for (const root of roots) {\n sources.push({ base: root, pattern: '**/*.{ts,tsx,js,jsx}', negated: false }, { base: root, pattern: '**/node_modules/**', negated: true }, { base: root, pattern: `**/${cacheBaseName}/**`, negated: true })\n }\n return sources\n}\n\n/**\n * Read monorepo watch-folder paths out of the worker environment.\n * Empty array when the host isn't a monorepo.\n * @returns Absolute paths Metro also watches (sibling packages).\n */\nfunction readWatchFolders(): readonly string[] {\n const raw = process.env[WATCH_FOLDERS_ENV]\n if (!raw || raw.length === 0) return []\n return raw.split('\\0').filter((entry) => entry.length > 0)\n}\n\n/** Env var Metro workers read to locate the theme CSS on disk. */\nconst CSS_ENTRY_ENV = 'RNWIND_CSS_ENTRY_FILE'\n/** Env var Metro workers read to locate the cache directory (`.rnwind`). */\nconst CACHE_DIR_ENV = 'RNWIND_CACHE_DIR'\n/** Env var carrying `watchFolders` from Metro config (NUL-separated). */\nconst WATCH_FOLDERS_ENV = 'RNWIND_WATCH_FOLDERS'\n/** Env var carrying extra className prefixes the Metro config supplied. */\nconst CLASSNAME_PREFIXES_ENV = 'RNWIND_CLASSNAME_PREFIXES'\n\n/** Memoised library fingerprint — read once per worker process. */\nlet libraryFingerprint: string | undefined\n\n/** Live state shared across one Metro transform worker. */\nlet cached: RnwindState | null = null\n\n/**\n * Cheap content-hash readout. SHA-256 prefix of the CSS bytes plus the\n * file's mtime nanoseconds (so identical content with different mtime\n * — atomic rewrites — still picks up the change). Returns `'missing'`\n * when the file can't be read so the cache key is still deterministic.\n * @param cssPath Absolute CSS path.\n * @returns 16-char hex content hash.\n */\nfunction readThemeHashFor(cssPath: string): string {\n if (!existsSync(cssPath)) return 'missing'\n try {\n const bytes = readFileSync(cssPath)\n const mtime = statSync(cssPath).mtimeMs.toString()\n return createHash('sha256').update(bytes).update(mtime).digest('hex').slice(0, 16)\n } catch {\n return 'missing'\n }\n}\n\n/**\n * Hash a small set of rnwind library files whose changes affect the\n * generated transform output. When the library is rebuilt (workspace\n * dev OR npm install of a new version) the file bytes change, the\n * fingerprint rotates, and Metro's transform cache invalidates.\n *\n * Includes the JSX rewriter (`transform-ast`) alongside the parser /\n * style-builder so a change to the transformer — e.g. renaming the\n * injected context hook — invalidates every stale per-file cache entry\n * on the next dev run. Without this, a user upgrading rnwind in-place\n * would keep loading the old transformed bytes; React-refresh would\n * then preserve fiber state across the version bump and the rendered\n * hook list could shift, surfacing as \"change in the order of Hooks\"\n * runtime errors.\n * Memoised — read once per worker process.\n * @returns 16-char hex fingerprint.\n */\nfunction getLibraryFingerprint(): string {\n if (libraryFingerprint !== undefined) return libraryFingerprint\n const here = path.dirname(__filename)\n const candidates = [\n path.resolve(here, '..', 'core', 'style-builder', 'build-style.mjs'),\n path.resolve(here, '..', 'core', 'style-builder', 'build-style.cjs'),\n path.resolve(here, '..', 'core', 'parser', 'tw-parser.mjs'),\n path.resolve(here, '..', 'core', 'parser', 'tw-parser.cjs'),\n path.resolve(here, 'transform-ast.mjs'),\n path.resolve(here, 'transform-ast.cjs'),\n path.resolve(here, 'transformer.mjs'),\n path.resolve(here, 'transformer.cjs'),\n // Source-tree fallback for tests + workspace dev (no built lib yet).\n path.resolve(here, '..', '..', 'src', 'core', 'style-builder', 'build-style.ts'),\n path.resolve(here, '..', '..', 'src', 'core', 'parser', 'tw-parser.ts'),\n path.resolve(here, '..', '..', 'src', 'metro', 'transform-ast.ts'),\n path.resolve(here, '..', '..', 'src', 'metro', 'transformer.ts'),\n ]\n const hash = createHash('sha256')\n let included = 0\n for (const file of candidates) {\n if (!existsSync(file)) continue\n try {\n hash.update(readFileSync(file))\n included += 1\n } catch {\n // Unreadable file — skip; fingerprint still derives from whatever WE could read.\n }\n }\n libraryFingerprint = included > 0 ? hash.digest('hex').slice(0, 16) : '0'.repeat(16)\n return libraryFingerprint\n}\n\n/**\n * Worker-local state. Lazy-initialised on first access so files that\n * bypass the transform don't pay for construction.\n */\nexport interface RnwindState {\n parser: TailwindParser\n builder: UnionBuilder\n themeCss: string\n themeHash: string\n projectRoot: string\n}\n\n/**\n * Publish the theme CSS path + cache dir to the environment so worker\n * subprocesses (spawned by Metro once `babelTransformerPath` is set)\n * can rebuild the same state without re-reading the Metro config.\n * @param cssEntryFile Absolute path to the user's theme CSS.\n * @param cacheDir Absolute path to the cache dir (`.rnwind`).\n * @param watchFolders\n * @param classNamePrefixes Extra JSX prop-name prefixes to rewrite.\n */\nexport function configureRnwindState(\n cssEntryFile: string,\n cacheDir: string,\n watchFolders: readonly string[] = [],\n classNamePrefixes?: readonly string[],\n): void {\n process.env[CSS_ENTRY_ENV] = cssEntryFile\n process.env[CACHE_DIR_ENV] = cacheDir\n if (watchFolders.length === 0) {\n delete process.env[WATCH_FOLDERS_ENV]\n } else {\n process.env[WATCH_FOLDERS_ENV] = watchFolders.join('\\0')\n }\n if (!classNamePrefixes || classNamePrefixes.length === 0) {\n delete process.env[CLASSNAME_PREFIXES_ENV]\n } else {\n process.env[CLASSNAME_PREFIXES_ENV] = classNamePrefixes.join(',')\n }\n cached = null\n}\n\n/**\n * Read the caller-configured extra className prefixes out of the\n * worker environment. Returns an empty array when unset — the\n * transformer applies the built-in `contentContainer` default on top\n * either way.\n * @returns User-supplied extra prefixes.\n */\nexport function getClassNamePrefixes(): readonly string[] {\n const raw = process.env[CLASSNAME_PREFIXES_ENV]\n if (!raw || raw.length === 0) return []\n return raw.split(',').filter((entry) => entry.length > 0)\n}\n\n/**\n * Fetch (or build) the worker-local rnwind state. Re-reads the theme\n * CSS hash on every call: if the user edited `global.css` while Metro\n * is running, the cached state is dropped and a fresh parser + ledger\n * is built. Combined with the `getCacheKey()` export on the\n * transformer (which folds the same hash into Metro's per-file cache\n * key) every CSS edit produces a full, correct re-bundle.\n * @param projectRoot\n * @returns The live rnwind state.\n */\nexport function getRnwindState(projectRoot: string): RnwindState {\n const cssEntry = process.env[CSS_ENTRY_ENV]\n const cacheDir = process.env[CACHE_DIR_ENV]\n if (!cssEntry) throw new Error('rnwind: RNWIND_CSS_ENTRY_FILE is not set — did `withRnwindConfig` run?')\n if (!cacheDir) throw new Error('rnwind: RNWIND_CACHE_DIR is not set — did `withRnwindConfig` run?')\n const currentHash = readThemeHashFor(cssEntry)\n if (cached?.themeHash === currentHash && cached.projectRoot === projectRoot) return cached\n const themeCss = readFileSync(cssEntry, 'utf8')\n const parser = new TailwindParser({\n themeCss,\n sources: defaultSources(projectRoot, cacheDir, readWatchFolders()),\n })\n const builder = new UnionBuilder(cacheDir, parser)\n cached = { parser, builder, themeCss, themeHash: currentHash, projectRoot }\n return cached\n}\n\n/**\n * Compute the rnwind cache-key suffix Metro mixes into every per-file\n * transform cache entry via the transformer's `getCacheKey()` export.\n * Includes the CSS path + its current content hash + the rnwind\n * library fingerprint, so any edit to `global.css` OR a library\n * upgrade flips the cache key and forces Metro to re-run the\n * transformer.\n * @returns Deterministic string suitable for appending to Metro's cache key.\n */\nexport function getRnwindCacheKey(): string {\n const cssEntry = process.env[CSS_ENTRY_ENV] ?? ''\n const prefixes = process.env[CLASSNAME_PREFIXES_ENV] ?? ''\n return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|pfx:${prefixes}`\n}\n\n/** Drop the cached state — call after editing the theme CSS. */\nexport function resetRnwindState(): void {\n cached = null\n}\n\n/**\n * Drop cached state, rebuild parser/builder with the fresh CSS, rescan\n * the project, and rewrite every scheme file on disk. This is what\n * `withRnwindConfig`'s CSS file-watcher invokes so `global.css` edits\n * propagate to the app via Metro's HMR — without this, the CSS-as-JS\n * module would re-emit `export {}` whose bytes never change, so Metro\n * would never invalidate downstream modules.\n * @param projectRoot Absolute project root (from `metroConfig.projectRoot`).\n */\nexport async function onThemeChange(projectRoot: string): Promise<void> {\n resetRnwindState()\n const state = getRnwindState(projectRoot)\n await state.builder.writeSchemes()\n}\n\n/**\n * Resolve the on-disk path of the scheme manifest module for the\n * resolver. The manifest eager-imports `common.style.js` and\n * lazy-requires each variant scheme; SchemeProvider calls its\n * `ensureSchemeLoaded` export to trigger per-scheme requires.\n * @returns Absolute path to `<cacheDir>/schemes.js`.\n */\nexport function manifestPathFor(): string {\n const cacheDir = process.env[CACHE_DIR_ENV]\n if (!cacheDir) throw new Error('rnwind: RNWIND_CACHE_DIR is not set')\n return path.join(cacheDir, 'schemes.js')\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state.mjs","sources":["../../../../src/metro/state.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport path from 'node:path'\nimport { createHash } from 'node:crypto'\nimport { UnionBuilder } from '../core/style-builder'\nimport { TailwindParser, type SourceEntry } from '../core/parser'\nimport { resolveThemeCss } from './css-imports'\n\n/**\n * Default oxide Scanner globs — walk every JS/TS source under the\n * project root AND every monorepo watch folder, excluding\n * `node_modules` and rnwind's own cache dir so we don't rescan\n * generated scheme files.\n *\n * Monorepo layouts (Yarn workspaces, pnpm workspaces, Nx) surface\n * sibling package roots as `metroConfig.watchFolders`. Every folder\n * Metro watches must also be scanned so atoms declared in shared UI\n * packages make it into the union — without this, only the app's\n * own files would be scanned and every UI-package atom would resolve\n * to `undefined` at runtime.\n * @param projectRoot Absolute project root.\n * @param cacheDir Absolute rnwind cache dir (to exclude).\n * @param watchFolders Extra monorepo roots Metro is watching.\n * @returns Scanner sources suitable for `parser.parseProject()`.\n */\nfunction defaultSources(projectRoot: string, cacheDir: string, watchFolders: readonly string[]): readonly SourceEntry[] {\n const cacheBaseName = path.basename(cacheDir)\n const roots = new Set<string>([projectRoot, ...watchFolders])\n const sources: SourceEntry[] = []\n for (const root of roots) {\n sources.push({ base: root, pattern: '**/*.{ts,tsx,js,jsx}', negated: false }, { base: root, pattern: '**/node_modules/**', negated: true }, { base: root, pattern: `**/${cacheBaseName}/**`, negated: true })\n }\n return sources\n}\n\n/**\n * Read monorepo watch-folder paths out of the worker environment.\n * Empty array when the host isn't a monorepo.\n * @returns Absolute paths Metro also watches (sibling packages).\n */\nfunction readWatchFolders(): readonly string[] {\n const raw = process.env[WATCH_FOLDERS_ENV]\n if (!raw || raw.length === 0) return []\n return raw.split('\\0').filter((entry) => entry.length > 0)\n}\n\n/** Env var Metro workers read to locate the theme CSS on disk. */\nconst CSS_ENTRY_ENV = 'RNWIND_CSS_ENTRY_FILE'\n/** Env var Metro workers read to locate the cache directory (`.rnwind`). */\nconst CACHE_DIR_ENV = 'RNWIND_CACHE_DIR'\n/** Env var carrying `watchFolders` from Metro config (NUL-separated). */\nconst WATCH_FOLDERS_ENV = 'RNWIND_WATCH_FOLDERS'\n/** Env var carrying extra className prefixes the Metro config supplied. */\nconst CLASSNAME_PREFIXES_ENV = 'RNWIND_CLASSNAME_PREFIXES'\n/** Env var carrying extra import sources whose JSX exports get className→style rewrites. Comma-separated. */\nconst HOST_SOURCES_ENV = 'RNWIND_HOST_SOURCES'\n/** Env var carrying extra JSX tag names (verbatim, may contain `.`) treated as hosts. Comma-separated. */\nconst HOST_COMPONENTS_ENV = 'RNWIND_HOST_COMPONENTS'\n\n/** Memoised library fingerprint — read once per worker process. */\nlet libraryFingerprint: string | undefined\n\n/** Live state shared across one Metro transform worker. */\nlet cached: RnwindState | null = null\n\n/**\n * Cheap content-hash readout. SHA-256 prefix of the FULLY-RESOLVED theme\n * CSS — `@import`s flattened — so an edit to a theme file the entry only\n * re-exports (`@import \"@acme/ui/theme.css\"`) still rotates the hash and\n * invalidates Metro's cache. Returns `'missing'` when the entry can't be\n * read so the cache key stays deterministic.\n * @param cssPath Absolute CSS path.\n * @returns 16-char hex content hash.\n */\nfunction readThemeHashFor(cssPath: string): string {\n if (!existsSync(cssPath)) return 'missing'\n try {\n return createHash('sha256').update(resolveThemeCss(cssPath)).digest('hex').slice(0, 16)\n } catch {\n return 'missing'\n }\n}\n\n/**\n * Hash a small set of rnwind library files whose changes affect the\n * generated transform output. When the library is rebuilt (workspace\n * dev OR npm install of a new version) the file bytes change, the\n * fingerprint rotates, and Metro's transform cache invalidates.\n *\n * Includes the JSX rewriter (`transform-ast`) alongside the parser /\n * style-builder so a change to the transformer — e.g. renaming the\n * injected context hook — invalidates every stale per-file cache entry\n * on the next dev run. Without this, a user upgrading rnwind in-place\n * would keep loading the old transformed bytes; React-refresh would\n * then preserve fiber state across the version bump and the rendered\n * hook list could shift, surfacing as \"change in the order of Hooks\"\n * runtime errors.\n * Memoised — read once per worker process.\n * @returns 16-char hex fingerprint.\n */\nfunction getLibraryFingerprint(): string {\n if (libraryFingerprint !== undefined) return libraryFingerprint\n const here = path.dirname(__filename)\n const candidates = [\n path.resolve(here, '..', 'core', 'style-builder', 'build-style.mjs'),\n path.resolve(here, '..', 'core', 'style-builder', 'build-style.cjs'),\n path.resolve(here, '..', 'core', 'parser', 'tw-parser.mjs'),\n path.resolve(here, '..', 'core', 'parser', 'tw-parser.cjs'),\n path.resolve(here, 'transform-ast.mjs'),\n path.resolve(here, 'transform-ast.cjs'),\n path.resolve(here, 'transformer.mjs'),\n path.resolve(here, 'transformer.cjs'),\n // Source-tree fallback for tests + workspace dev (no built lib yet).\n path.resolve(here, '..', '..', 'src', 'core', 'style-builder', 'build-style.ts'),\n path.resolve(here, '..', '..', 'src', 'core', 'parser', 'tw-parser.ts'),\n path.resolve(here, '..', '..', 'src', 'metro', 'transform-ast.ts'),\n path.resolve(here, '..', '..', 'src', 'metro', 'transformer.ts'),\n ]\n const hash = createHash('sha256')\n let included = 0\n for (const file of candidates) {\n if (!existsSync(file)) continue\n try {\n hash.update(readFileSync(file))\n included += 1\n } catch {\n // Unreadable file — skip; fingerprint still derives from whatever WE could read.\n }\n }\n libraryFingerprint = included > 0 ? hash.digest('hex').slice(0, 16) : '0'.repeat(16)\n return libraryFingerprint\n}\n\n/**\n * Worker-local state. Lazy-initialised on first access so files that\n * bypass the transform don't pay for construction.\n */\nexport interface RnwindState {\n parser: TailwindParser\n builder: UnionBuilder\n themeCss: string\n themeHash: string\n projectRoot: string\n}\n\n/**\n * Publish the theme CSS path + cache dir to the environment so worker\n * subprocesses (spawned by Metro once `babelTransformerPath` is set)\n * can rebuild the same state without re-reading the Metro config.\n * @param cssEntryFile Absolute path to the user's theme CSS.\n * @param cacheDir Absolute path to the cache dir (`.rnwind`).\n * @param watchFolders\n * @param classNamePrefixes Extra JSX prop-name prefixes to rewrite.\n * @param hostSources\n * @param hostComponents\n */\nexport function configureRnwindState(\n cssEntryFile: string,\n cacheDir: string,\n watchFolders: readonly string[] = [],\n classNamePrefixes?: readonly string[],\n hostSources?: readonly string[],\n hostComponents?: readonly string[],\n): void {\n process.env[CSS_ENTRY_ENV] = cssEntryFile\n process.env[CACHE_DIR_ENV] = cacheDir\n if (watchFolders.length === 0) {\n delete process.env[WATCH_FOLDERS_ENV]\n } else {\n process.env[WATCH_FOLDERS_ENV] = watchFolders.join('\\0')\n }\n if (!classNamePrefixes || classNamePrefixes.length === 0) {\n delete process.env[CLASSNAME_PREFIXES_ENV]\n } else {\n process.env[CLASSNAME_PREFIXES_ENV] = classNamePrefixes.join(',')\n }\n if (!hostSources || hostSources.length === 0) {\n delete process.env[HOST_SOURCES_ENV]\n } else {\n process.env[HOST_SOURCES_ENV] = hostSources.join(',')\n }\n if (!hostComponents || hostComponents.length === 0) {\n delete process.env[HOST_COMPONENTS_ENV]\n } else {\n process.env[HOST_COMPONENTS_ENV] = hostComponents.join(',')\n }\n cached = null\n}\n\n/**\n * Read the caller-configured extra className prefixes out of the\n * worker environment. Returns an empty array when unset — the\n * transformer applies the built-in `contentContainer` default on top\n * either way.\n * @returns User-supplied extra prefixes.\n */\nexport function getClassNamePrefixes(): readonly string[] {\n const raw = process.env[CLASSNAME_PREFIXES_ENV]\n if (!raw || raw.length === 0) return []\n return raw.split(',').filter((entry) => entry.length > 0)\n}\n\n/**\n * Read the caller-configured extra host module sources out of the\n * worker environment. Empty array when unset — the transformer applies\n * its built-in default list on top either way.\n * @returns User-supplied extra host sources.\n */\nexport function getHostSources(): readonly string[] {\n const raw = process.env[HOST_SOURCES_ENV]\n if (!raw || raw.length === 0) return []\n return raw.split(',').filter((entry) => entry.length > 0)\n}\n\n/**\n * Read the caller-configured extra host JSX tag names out of the worker\n * environment. Verbatim names — may include `.` for member expressions\n * like `'Animated.View'`.\n * @returns User-supplied extra host component names.\n */\nexport function getHostComponents(): readonly string[] {\n const raw = process.env[HOST_COMPONENTS_ENV]\n if (!raw || raw.length === 0) return []\n return raw.split(',').filter((entry) => entry.length > 0)\n}\n\n/**\n * Fetch (or build) the worker-local rnwind state. Re-reads the theme\n * CSS hash on every call: if the user edited `global.css` while Metro\n * is running, the cached state is dropped and a fresh parser + ledger\n * is built. Combined with the `getCacheKey()` export on the\n * transformer (which folds the same hash into Metro's per-file cache\n * key) every CSS edit produces a full, correct re-bundle.\n * @param projectRoot\n * @returns The live rnwind state.\n */\nexport function getRnwindState(projectRoot: string): RnwindState {\n const cssEntry = process.env[CSS_ENTRY_ENV]\n const cacheDir = process.env[CACHE_DIR_ENV]\n if (!cssEntry) throw new Error('rnwind: RNWIND_CSS_ENTRY_FILE is not set — did `withRnwindConfig` run?')\n if (!cacheDir) throw new Error('rnwind: RNWIND_CACHE_DIR is not set — did `withRnwindConfig` run?')\n const currentHash = readThemeHashFor(cssEntry)\n if (cached?.themeHash === currentHash && cached.projectRoot === projectRoot) return cached\n const themeCss = resolveThemeCss(cssEntry)\n const parser = new TailwindParser({\n themeCss,\n sources: defaultSources(projectRoot, cacheDir, readWatchFolders()),\n })\n const builder = new UnionBuilder(cacheDir, parser)\n cached = { parser, builder, themeCss, themeHash: currentHash, projectRoot }\n return cached\n}\n\n/**\n * Compute the rnwind cache-key suffix Metro mixes into every per-file\n * transform cache entry via the transformer's `getCacheKey()` export.\n * Includes the CSS path + its current content hash + the rnwind\n * library fingerprint, so any edit to `global.css` OR a library\n * upgrade flips the cache key and forces Metro to re-run the\n * transformer.\n * @returns Deterministic string suitable for appending to Metro's cache key.\n */\nexport function getRnwindCacheKey(): string {\n const cssEntry = process.env[CSS_ENTRY_ENV] ?? ''\n const prefixes = process.env[CLASSNAME_PREFIXES_ENV] ?? ''\n // Host source / component config changes which JSX tags get rewritten,\n // so it MUST flip the cache key — otherwise Metro replays stale\n // transforms (a newly-opted-in host keeps its raw className, a removed\n // one keeps the rewrite).\n const hostSources = process.env[HOST_SOURCES_ENV] ?? ''\n const hostComponents = process.env[HOST_COMPONENTS_ENV] ?? ''\n return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|pfx:${prefixes}|hs:${hostSources}|hc:${hostComponents}`\n}\n\n/** Drop the cached state — call after editing the theme CSS. */\nexport function resetRnwindState(): void {\n cached = null\n}\n\n/**\n * Drop cached state, rebuild parser/builder with the fresh CSS, rescan\n * the project, and rewrite every scheme file on disk. This is what\n * `withRnwindConfig`'s CSS file-watcher invokes so `global.css` edits\n * propagate to the app via Metro's HMR — without this, the CSS-as-JS\n * module would re-emit `export {}` whose bytes never change, so Metro\n * would never invalidate downstream modules.\n * @param projectRoot Absolute project root (from `metroConfig.projectRoot`).\n */\nexport async function onThemeChange(projectRoot: string): Promise<void> {\n resetRnwindState()\n const state = getRnwindState(projectRoot)\n await state.builder.writeSchemes()\n}\n\n/**\n * Resolve the on-disk path of the scheme manifest module for the\n * resolver. The manifest eager-imports `common.style.js` and\n * lazy-requires each variant scheme; SchemeProvider calls its\n * `ensureSchemeLoaded` export to trigger per-scheme requires.\n * @returns Absolute path to `<cacheDir>/schemes.js`.\n */\nexport function manifestPathFor(): string {\n const cacheDir = process.env[CACHE_DIR_ENV]\n if (!cacheDir) throw new Error('rnwind: RNWIND_CACHE_DIR is not set')\n return path.join(cacheDir, 'schemes.js')\n}\n"],"names":[],"mappings":"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"}
@@ -52,6 +52,21 @@ export interface TransformAstOptions {
52
52
  * or dynamic.
53
53
  */
54
54
  classNamePrefixes?: readonly string[];
55
+ /**
56
+ * Extra module specifiers whose JSX exports the transformer should
57
+ * treat as hosts (rewrite `className` → `style` at compile time).
58
+ * Merged with the built-in {@link DEFAULT_HOST_SOURCES} list. Use
59
+ * this for design-system packages whose primitives wrap RN hosts and
60
+ * accept `style` directly.
61
+ */
62
+ hostSources?: readonly string[];
63
+ /**
64
+ * Extra component names (verbatim, including dotted member access
65
+ * like `'Animated.View'`) the transformer should treat as hosts. Use
66
+ * this for one-off escape-hatches that aren't matchable by source —
67
+ * e.g. you alias `View as MyBox` and want the compile-time path.
68
+ */
69
+ hostComponents?: readonly string[];
55
70
  }
56
71
  /**
57
72
  * Mutate an already-parsed Babel AST in place:
@@ -83,6 +83,159 @@ const NON_INTERACTIVE_HOST_TAGS = new Set([
83
83
  * on top, never replacing this.
84
84
  */
85
85
  const DEFAULT_CLASSNAME_PREFIXES = ['contentContainer'];
86
+ /**
87
+ * Module specifiers whose JSX exports are "host-like" — they consume
88
+ * `style` directly (and own no opaque component logic that depends on
89
+ * receiving the raw `className` string). For tags imported from these
90
+ * sources the transformer rewrites `className="…"` → `style={lookupCss(…)}`
91
+ * at build time, so the runtime cost is zero.
92
+ *
93
+ * For tags from ANY other source the transformer leaves `className`
94
+ * alone — the importing component receives the raw string and decides
95
+ * what to do with it (forward to an inner host, reshape, route a slice
96
+ * to `contentContainerStyle`, …). This is what makes patterns like
97
+ * `<MyButton className="px-4 bg-primary" />` work without rnwind
98
+ * stealing the prop before the component sees it.
99
+ *
100
+ * Users extend the list via `withRnwindConfig`'s `hostSources` option.
101
+ */
102
+ const DEFAULT_HOST_SOURCES = [
103
+ 'react-native',
104
+ 'react-native-reanimated',
105
+ 'react-native-svg',
106
+ 'react-native-gesture-handler',
107
+ 'react-native-safe-area-context',
108
+ 'expo-linear-gradient',
109
+ 'expo-image',
110
+ 'expo-blur',
111
+ 'expo-symbols',
112
+ '@shopify/flash-list',
113
+ '@shopify/react-native-skia',
114
+ 'lottie-react-native',
115
+ ];
116
+ /**
117
+ * Whether a JSX tag name is lowercase. Lowercase tags don't appear in
118
+ * native React Native userland — but if one shows up (web target via
119
+ * `react-native-web`, mdx, etc.) treat it as a host so the rewrite
120
+ * engages instead of silently dropping the className.
121
+ * @param name JSX tag identifier text.
122
+ * @returns True for ASCII-lowercase first character.
123
+ */
124
+ function isLowercaseTag(name) {
125
+ const code = name.codePointAt(0);
126
+ return code !== undefined && code >= 97 && code <= 122;
127
+ }
128
+ /**
129
+ * Walk a JSX opening element's tag name node into a dotted string
130
+ * (`Animated.View`, `Foo.Bar.Baz`). Returns `null` for namespaced names
131
+ * (`<svg:rect>` — invalid in RN; we skip them).
132
+ * @param name JSXOpeningElement name node.
133
+ * @returns Dotted tag text, or null.
134
+ */
135
+ function jsxTagText(name) {
136
+ if (t.isJSXIdentifier(name))
137
+ return name.name;
138
+ if (t.isJSXMemberExpression(name)) {
139
+ const left = jsxTagText(name.object);
140
+ return left ? `${left}.${name.property.name}` : null;
141
+ }
142
+ return null;
143
+ }
144
+ /**
145
+ * Leftmost identifier of a (possibly dotted) tag — used to look up its import source.
146
+ * @param tagText
147
+ */
148
+ function leftmostIdentifier(tagText) {
149
+ const dot = tagText.indexOf('.');
150
+ return dot === -1 ? tagText : tagText.slice(0, dot);
151
+ }
152
+ /**
153
+ * Build the per-file host lookup. Walks every `import` declaration once
154
+ * to map every locally-bound name to its source module. A JSX tag is a
155
+ * host when:
156
+ * 1. its full text matches an entry in `extraHostComponents` (verbatim),
157
+ * 2. its leftmost identifier was imported from a `hostSources` module,
158
+ * 3. it's a lowercase tag (web targets, defensive).
159
+ *
160
+ * Anything else is custom and the transformer leaves its className alone.
161
+ * @param ast File AST.
162
+ * @param extraHostSources User-supplied additional host module specifiers.
163
+ * @param extraHostComponents User-supplied additional host component names.
164
+ * @returns Lookup callback.
165
+ */
166
+ function buildHostLookup(ast, extraHostSources, extraHostComponents) {
167
+ const importSourceByLocal = new Map();
168
+ for (const node of ast.program.body) {
169
+ if (!t.isImportDeclaration(node))
170
+ continue;
171
+ const source = node.source.value;
172
+ for (const spec of node.specifiers) {
173
+ if (t.isImportDefaultSpecifier(spec) || t.isImportSpecifier(spec) || t.isImportNamespaceSpecifier(spec)) {
174
+ importSourceByLocal.set(spec.local.name, source);
175
+ }
176
+ }
177
+ }
178
+ // Recognise module-local host aliases — common pattern in React Native:
179
+ // const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
180
+ // const Animated = createAnimatedComponent(View)
181
+ // The local binding wraps a host underneath so its className must still
182
+ // be rewritten. Without this every `<AnimatedTextInput className="…" />`
183
+ // site looked custom and the className silently dropped.
184
+ const localHostAliases = collectCreateAnimatedComponentAliases(ast);
185
+ const hostSources = new Set([...DEFAULT_HOST_SOURCES, ...(extraHostSources ?? [])]);
186
+ const hostComponents = new Set(extraHostComponents);
187
+ return (tagText) => {
188
+ if (isLowercaseTag(tagText))
189
+ return true;
190
+ if (hostComponents.has(tagText))
191
+ return true;
192
+ const left = leftmostIdentifier(tagText);
193
+ if (localHostAliases.has(left))
194
+ return true;
195
+ const source = importSourceByLocal.get(left);
196
+ return source !== undefined && hostSources.has(source);
197
+ };
198
+ }
199
+ /**
200
+ * Walk top-level `const X = createAnimatedComponent(Y)` /
201
+ * `Animated.createAnimatedComponent(Y)` declarations and return the set
202
+ * of local names so the host-lookup recognises them. Reanimated +
203
+ * RN-core `Animated.createAnimatedComponent` are the only creators in
204
+ * common use; matching by callee-name covers both shapes without
205
+ * needing import-source resolution.
206
+ * @param ast File AST.
207
+ * @returns Set of locally-bound names that wrap a host component.
208
+ */
209
+ function collectCreateAnimatedComponentAliases(ast) {
210
+ const aliases = new Set();
211
+ for (const node of ast.program.body) {
212
+ const declaration = t.isExportNamedDeclaration(node) ? node.declaration : node;
213
+ if (!t.isVariableDeclaration(declaration))
214
+ continue;
215
+ for (const decl of declaration.declarations) {
216
+ if (!t.isIdentifier(decl.id) || !decl.init)
217
+ continue;
218
+ if (!isCreateAnimatedComponentCall(decl.init))
219
+ continue;
220
+ aliases.add(decl.id.name);
221
+ }
222
+ }
223
+ return aliases;
224
+ }
225
+ /**
226
+ * True for `createAnimatedComponent(...)` and `<x>.createAnimatedComponent(...)` calls.
227
+ * @param expr
228
+ */
229
+ function isCreateAnimatedComponentCall(expr) {
230
+ if (!t.isCallExpression(expr))
231
+ return false;
232
+ const { callee } = expr;
233
+ if (t.isIdentifier(callee) && callee.name === 'createAnimatedComponent')
234
+ return true;
235
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property) && callee.property.name === 'createAnimatedComponent')
236
+ return true;
237
+ return false;
238
+ }
86
239
  /**
87
240
  * Mutate an already-parsed Babel AST in place:
88
241
  * - Rewrite every JSX `className="…"` / `className={expr}` attribute to
@@ -106,6 +259,7 @@ function transformAst(ast, options) {
106
259
  const literals = [];
107
260
  const prefixSet = buildPrefixSet(options.classNamePrefixes);
108
261
  const hapticHoister = createHapticHoister();
262
+ const isHostTag = buildHostLookup(ast, options.hostSources, options.hostComponents);
109
263
  const rewriteCtx = {
110
264
  needsInsets: false,
111
265
  gradientAtoms: options.gradientAtoms ?? EMPTY_GRADIENT_ATOMS,
@@ -118,6 +272,15 @@ function transformAst(ast, options) {
118
272
  let touched = false;
119
273
  let usedLookupCss = false;
120
274
  let usedInteractiveBox = false;
275
+ // Per-element host classification, captured the first time we see each
276
+ // JSXOpeningElement. Necessary because the InteractiveBox wrap mutates
277
+ // `parent.name` in-place from the original tag → `_ib`; sibling
278
+ // attributes processed AFTER the swap would otherwise re-classify off
279
+ // the now-meaningless `_ib` name and skip rewrites they should do
280
+ // (e.g. `contentContainerClassName` next to an `active:` className on
281
+ // the same `<ScrollView>`).
282
+ const customElements = new WeakSet();
283
+ const classifiedElements = new WeakSet();
121
284
  traverse(ast, {
122
285
  JSXAttribute(attributePath) {
123
286
  const { node } = attributePath;
@@ -126,6 +289,24 @@ function transformAst(ast, options) {
126
289
  const target = classifyAttributeName(node.name.name, prefixSet);
127
290
  if (!target)
128
291
  return;
292
+ // Skip className rewrite when the parent JSX tag is a custom
293
+ // component (not imported from a known host source). Custom
294
+ // components own their `className` prop — the transformer would
295
+ // steal the string from under them otherwise. The literal still
296
+ // appears in source text, so oxide still discovers its atoms via
297
+ // the project scan; the inner host that ultimately consumes the
298
+ // forwarded className gets rewritten by ITS file's transform.
299
+ const { parent } = attributePath;
300
+ if (t.isJSXOpeningElement(parent)) {
301
+ if (!classifiedElements.has(parent)) {
302
+ classifiedElements.add(parent);
303
+ const tagText = jsxTagText(parent.name);
304
+ if (tagText !== null && !isHostTag(tagText))
305
+ customElements.add(parent);
306
+ }
307
+ if (customElements.has(parent))
308
+ return;
309
+ }
129
310
  const rewritten = rewriteClassNameAttribute(attributePath, hoister, literals, rewriteCtx, target);
130
311
  if (!rewritten)
131
312
  return;
@@ -226,12 +407,20 @@ function rewriteClassNameAttribute(attributePath, hoister, literals, rewriteCtx,
226
407
  const { value } = node;
227
408
  if (!value)
228
409
  return null;
229
- const buildResult = buildFirstArgument(value, hoister, literals, rewriteCtx);
230
- if (!buildResult)
231
- return null;
232
410
  const { parent } = attributePath;
233
411
  if (!t.isJSXOpeningElement(parent))
234
412
  return null;
413
+ // The rewrite emits references to `_t` (the `useR_()` binding). That
414
+ // binding can only live in a component body — so if this JSX site has
415
+ // no enclosing component (e.g. a top-level `const renderItem = (...) =>
416
+ // <View className=.../>` helper), bail and leave className untouched
417
+ // rather than emit a dangling `_t`. Checked BEFORE any mutation
418
+ // (hoist, sibling-style drop) so a bail leaves the AST pristine.
419
+ if (!hasComponentBody(attributePath))
420
+ return null;
421
+ const buildResult = buildFirstArgument(value, hoister, literals, rewriteCtx);
422
+ if (!buildResult)
423
+ return null;
235
424
  const userStyleExpr = extractAndDropSiblingStyle(parent, target.styleProp);
236
425
  // Single context binding `_t = _r()` — carries scheme, fontScale,
237
426
  // insets together so React tracks all three as render deps via one
@@ -280,29 +469,36 @@ function applyDerivedJsxAttributes(attributePath, parent, result, target, rewrit
280
469
  injectEventHapticHandlers(attributePath, parent, result.eventHaptics, rewriteCtx);
281
470
  }
282
471
  /**
283
- * Splice gradient JSX attributes (`colors={…}` / `start={…}` /
284
- * `end={…}`) into a JSXOpeningElement's attribute list, replacing
285
- * any already-present attribute with the same name. Users who manually
286
- * set `colors=` on the same element lose; rnwind's class-derived
287
- * values win — matching how `className`-resolved styles override
288
- * inline `style={}`.
472
+ * Splice class-derived JSX attributes (`colors={…}` / `start={…}` /
473
+ * `end={…}` for gradients; `numberOfLines=` / `ellipsizeMode=` for
474
+ * truncate) into a JSXOpeningElement's attribute list but only when
475
+ * the developer hasn't already written that attribute themselves.
476
+ *
477
+ * **User attrs always win.** If a hand-written `colors={USER}` is
478
+ * present, the class-derived hoist is dropped on the floor for that
479
+ * specific attribute. Same rule for every derived prop, applied
480
+ * per-attribute so the user can override one slot (e.g. `start={…}`)
481
+ * and let rnwind fill in the others. Documented in
482
+ * `docs/architecture.md`.
289
483
  * @param opening JSXOpeningElement to mutate.
290
- * @param gradientAttrs Freshly built JSX attributes.
291
- * @param gradientAttributes
484
+ * @param gradientAttributes Freshly built JSX attributes.
292
485
  */
293
486
  function appendGradientAttributes(opening, gradientAttributes) {
294
- const names = new Set();
295
- for (const attribute of gradientAttributes)
296
- if (t.isJSXIdentifier(attribute.name))
297
- names.add(attribute.name.name);
298
- opening.attributes = opening.attributes.filter((attribute) => {
487
+ const userAttributeNames = new Set();
488
+ for (const attribute of opening.attributes) {
299
489
  if (!t.isJSXAttribute(attribute))
300
- return true;
490
+ continue;
301
491
  if (!t.isJSXIdentifier(attribute.name))
302
- return true;
303
- return !names.has(attribute.name.name);
304
- });
305
- opening.attributes.push(...gradientAttributes);
492
+ continue;
493
+ userAttributeNames.add(attribute.name.name);
494
+ }
495
+ for (const derived of gradientAttributes) {
496
+ if (!t.isJSXIdentifier(derived.name))
497
+ continue;
498
+ if (userAttributeNames.has(derived.name.name))
499
+ continue;
500
+ opening.attributes.push(derived);
501
+ }
306
502
  }
307
503
  /**
308
504
  * Whether a JSX tag can fire press / focus events. Pure host-tag check
@@ -957,6 +1153,27 @@ function injectContextHook(path) {
957
1153
  componentBody.unshiftContainer('body', declaration);
958
1154
  return CONTEXT_BINDING;
959
1155
  }
1156
+ /**
1157
+ * Whether `path` sits inside a recognised function component — i.e.
1158
+ * {@link injectContextHook} would find a body to host `const _t =
1159
+ * useR_()`. Pure lookup that mirrors {@link findComponentBody}'s walk
1160
+ * but performs NO body promotion, so a caller can bail before mutating
1161
+ * when the answer is no.
1162
+ * @param path Rewrite-site path.
1163
+ * @returns True when an enclosing component function exists.
1164
+ */
1165
+ function hasComponentBody(path) {
1166
+ let current = path;
1167
+ while (current) {
1168
+ const fn = current.findParent((parent) => parent.isFunction());
1169
+ if (!fn)
1170
+ return false;
1171
+ if (isComponentFunction(fn))
1172
+ return true;
1173
+ current = fn;
1174
+ }
1175
+ return false;
1176
+ }
960
1177
  /**
961
1178
  * Walk up from `path` to the nearest recognised function component.
962
1179
  * Accepts: