rnwind 0.0.4 → 0.0.6

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 (127) hide show
  1. package/lib/cjs/core/normalize-classname.cjs +25 -0
  2. package/lib/cjs/core/normalize-classname.cjs.map +1 -0
  3. package/lib/cjs/core/normalize-classname.d.ts +10 -0
  4. package/lib/cjs/core/style-builder/build-style.cjs +258 -58
  5. package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
  6. package/lib/cjs/core/style-builder/build-style.d.ts +6 -1
  7. package/lib/cjs/core/style-builder/union-builder.cjs +37 -3
  8. package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
  9. package/lib/cjs/core/style-builder/union-builder.d.ts +21 -1
  10. package/lib/cjs/metro/dts.cjs +7 -16
  11. package/lib/cjs/metro/dts.cjs.map +1 -1
  12. package/lib/cjs/metro/dts.d.ts +2 -4
  13. package/lib/cjs/metro/state.cjs +30 -78
  14. package/lib/cjs/metro/state.cjs.map +1 -1
  15. package/lib/cjs/metro/state.d.ts +8 -25
  16. package/lib/cjs/metro/transformer.cjs +193 -34
  17. package/lib/cjs/metro/transformer.cjs.map +1 -1
  18. package/lib/cjs/metro/with-config.cjs +2 -2
  19. package/lib/cjs/metro/with-config.cjs.map +1 -1
  20. package/lib/cjs/metro/with-config.d.ts +11 -26
  21. package/lib/cjs/metro/wrap-imports.cjs +273 -0
  22. package/lib/cjs/metro/wrap-imports.cjs.map +1 -0
  23. package/lib/cjs/metro/wrap-imports.d.ts +26 -0
  24. package/lib/cjs/runtime/components/rnwind-provider.cjs +0 -17
  25. package/lib/cjs/runtime/components/rnwind-provider.cjs.map +1 -1
  26. package/lib/cjs/runtime/components/rnwind-provider.d.ts +0 -14
  27. package/lib/cjs/runtime/hooks/use-css.cjs +16 -10
  28. package/lib/cjs/runtime/hooks/use-css.cjs.map +1 -1
  29. package/lib/cjs/runtime/hooks/use-css.d.ts +15 -9
  30. package/lib/cjs/runtime/index.cjs +11 -13
  31. package/lib/cjs/runtime/index.cjs.map +1 -1
  32. package/lib/cjs/runtime/index.d.ts +4 -9
  33. package/lib/cjs/runtime/lookup-css.cjs +10 -0
  34. package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
  35. package/lib/cjs/runtime/lookup-css.d.ts +7 -0
  36. package/lib/cjs/runtime/resolve.cjs +400 -0
  37. package/lib/cjs/runtime/resolve.cjs.map +1 -0
  38. package/lib/cjs/runtime/resolve.d.ts +66 -0
  39. package/lib/cjs/runtime/wrap.cjs +254 -0
  40. package/lib/cjs/runtime/wrap.cjs.map +1 -0
  41. package/lib/cjs/runtime/wrap.d.ts +37 -0
  42. package/lib/cjs/testing/index.cjs +81 -50
  43. package/lib/cjs/testing/index.cjs.map +1 -1
  44. package/lib/esm/core/normalize-classname.d.ts +10 -0
  45. package/lib/esm/core/normalize-classname.mjs +23 -0
  46. package/lib/esm/core/normalize-classname.mjs.map +1 -0
  47. package/lib/esm/core/style-builder/build-style.d.ts +6 -1
  48. package/lib/esm/core/style-builder/build-style.mjs +258 -58
  49. package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
  50. package/lib/esm/core/style-builder/union-builder.d.ts +21 -1
  51. package/lib/esm/core/style-builder/union-builder.mjs +37 -3
  52. package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
  53. package/lib/esm/metro/dts.d.ts +2 -4
  54. package/lib/esm/metro/dts.mjs +7 -16
  55. package/lib/esm/metro/dts.mjs.map +1 -1
  56. package/lib/esm/metro/state.d.ts +8 -25
  57. package/lib/esm/metro/state.mjs +30 -76
  58. package/lib/esm/metro/state.mjs.map +1 -1
  59. package/lib/esm/metro/transformer.mjs +194 -35
  60. package/lib/esm/metro/transformer.mjs.map +1 -1
  61. package/lib/esm/metro/with-config.d.ts +11 -26
  62. package/lib/esm/metro/with-config.mjs +2 -2
  63. package/lib/esm/metro/with-config.mjs.map +1 -1
  64. package/lib/esm/metro/wrap-imports.d.ts +26 -0
  65. package/lib/esm/metro/wrap-imports.mjs +250 -0
  66. package/lib/esm/metro/wrap-imports.mjs.map +1 -0
  67. package/lib/esm/runtime/components/rnwind-provider.d.ts +0 -14
  68. package/lib/esm/runtime/components/rnwind-provider.mjs +1 -17
  69. package/lib/esm/runtime/components/rnwind-provider.mjs.map +1 -1
  70. package/lib/esm/runtime/hooks/use-css.d.ts +15 -9
  71. package/lib/esm/runtime/hooks/use-css.mjs +16 -10
  72. package/lib/esm/runtime/hooks/use-css.mjs.map +1 -1
  73. package/lib/esm/runtime/index.d.ts +4 -9
  74. package/lib/esm/runtime/index.mjs +4 -4
  75. package/lib/esm/runtime/index.mjs.map +1 -1
  76. package/lib/esm/runtime/lookup-css.d.ts +7 -0
  77. package/lib/esm/runtime/lookup-css.mjs +10 -1
  78. package/lib/esm/runtime/lookup-css.mjs.map +1 -1
  79. package/lib/esm/runtime/resolve.d.ts +66 -0
  80. package/lib/esm/runtime/resolve.mjs +393 -0
  81. package/lib/esm/runtime/resolve.mjs.map +1 -0
  82. package/lib/esm/runtime/wrap.d.ts +37 -0
  83. package/lib/esm/runtime/wrap.mjs +251 -0
  84. package/lib/esm/runtime/wrap.mjs.map +1 -0
  85. package/lib/esm/testing/index.mjs +84 -53
  86. package/lib/esm/testing/index.mjs.map +1 -1
  87. package/package.json +2 -1
  88. package/src/core/normalize-classname.ts +19 -0
  89. package/src/core/style-builder/build-style.ts +286 -55
  90. package/src/core/style-builder/union-builder.ts +36 -3
  91. package/src/metro/dts.ts +7 -19
  92. package/src/metro/state.ts +29 -74
  93. package/src/metro/transformer.ts +190 -34
  94. package/src/metro/with-config.ts +13 -28
  95. package/src/metro/wrap-imports.ts +260 -0
  96. package/src/runtime/components/rnwind-provider.tsx +0 -17
  97. package/src/runtime/hooks/use-css.ts +17 -11
  98. package/src/runtime/index.ts +3 -26
  99. package/src/runtime/lookup-css.ts +10 -0
  100. package/src/runtime/resolve.ts +438 -0
  101. package/src/runtime/wrap.tsx +267 -0
  102. package/src/testing/index.ts +106 -56
  103. package/lib/cjs/core/parser/text-truncate.cjs +0 -78
  104. package/lib/cjs/core/parser/text-truncate.cjs.map +0 -1
  105. package/lib/cjs/metro/transform-ast.cjs +0 -1472
  106. package/lib/cjs/metro/transform-ast.cjs.map +0 -1
  107. package/lib/cjs/metro/transform-ast.d.ts +0 -88
  108. package/lib/cjs/runtime/haptics.cjs +0 -113
  109. package/lib/cjs/runtime/haptics.cjs.map +0 -1
  110. package/lib/cjs/runtime/haptics.d.ts +0 -48
  111. package/lib/cjs/runtime/interactive-box.cjs +0 -35
  112. package/lib/cjs/runtime/interactive-box.cjs.map +0 -1
  113. package/lib/cjs/runtime/interactive-box.d.ts +0 -40
  114. package/lib/esm/core/parser/text-truncate.mjs +0 -75
  115. package/lib/esm/core/parser/text-truncate.mjs.map +0 -1
  116. package/lib/esm/metro/transform-ast.d.ts +0 -88
  117. package/lib/esm/metro/transform-ast.mjs +0 -1451
  118. package/lib/esm/metro/transform-ast.mjs.map +0 -1
  119. package/lib/esm/runtime/haptics.d.ts +0 -48
  120. package/lib/esm/runtime/haptics.mjs +0 -110
  121. package/lib/esm/runtime/haptics.mjs.map +0 -1
  122. package/lib/esm/runtime/interactive-box.d.ts +0 -40
  123. package/lib/esm/runtime/interactive-box.mjs +0 -33
  124. package/lib/esm/runtime/interactive-box.mjs.map +0 -1
  125. package/src/metro/transform-ast.ts +0 -1729
  126. package/src/runtime/haptics.ts +0 -120
  127. package/src/runtime/interactive-box.tsx +0 -57
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
4
4
  import { UnionBuilder } from '../core/style-builder/union-builder.mjs';
5
5
  import { TailwindParser } from '../core/parser/tw-parser.mjs';
6
6
  import { resolveThemeCss } from './css-imports.mjs';
7
+ import { buildWrapModules } from './wrap-imports.mjs';
7
8
 
8
9
  /**
9
10
  * Default oxide Scanner globs — walk every JS/TS source under the
@@ -48,12 +49,8 @@ const CSS_ENTRY_ENV = 'RNWIND_CSS_ENTRY_FILE';
48
49
  const CACHE_DIR_ENV = 'RNWIND_CACHE_DIR';
49
50
  /** Env var carrying `watchFolders` from Metro config (NUL-separated). */
50
51
  const WATCH_FOLDERS_ENV = 'RNWIND_WATCH_FOLDERS';
51
- /** Env var carrying extra className prefixes the Metro config supplied. */
52
- const CLASSNAME_PREFIXES_ENV = 'RNWIND_CLASSNAME_PREFIXES';
53
- /** Env var carrying extra import sources whose JSX exports get className→style rewrites. Comma-separated. */
54
- const HOST_SOURCES_ENV = 'RNWIND_HOST_SOURCES';
55
- /** Env var carrying extra JSX tag names (verbatim, may contain `.`) treated as hosts. Comma-separated. */
56
- const HOST_COMPONENTS_ENV = 'RNWIND_HOST_COMPONENTS';
52
+ /** Env var carrying extra modules whose component exports get `wrap()`-ed. Comma-separated. */
53
+ const WRAP_MODULES_ENV = 'RNWIND_WRAP_MODULES';
57
54
  /** Memoised library fingerprint — read once per worker process. */
58
55
  let libraryFingerprint;
59
56
  /** Live state shared across one Metro transform worker. */
@@ -83,10 +80,10 @@ function readThemeHashFor(cssPath) {
83
80
  * dev OR npm install of a new version) the file bytes change, the
84
81
  * fingerprint rotates, and Metro's transform cache invalidates.
85
82
  *
86
- * Includes the JSX rewriter (`transform-ast`) alongside the parser /
87
- * style-builder so a change to the transformer — e.g. renaming the
88
- * injected context hook — invalidates every stale per-file cache entry
89
- * on the next dev run. Without this, a user upgrading rnwind in-place
83
+ * Includes the import-rewriter (`wrap-imports`) and runtime resolver
84
+ * alongside the parser / style-builder so a change to the transformer —
85
+ * e.g. renaming the injected wrap helper — invalidates every stale
86
+ * per-file cache entry on the next dev run. Without this, a user upgrading rnwind in-place
90
87
  * would keep loading the old transformed bytes; React-refresh would
91
88
  * then preserve fiber state across the version bump and the rendered
92
89
  * hook list could shift, surfacing as "change in the order of Hooks"
@@ -103,14 +100,14 @@ function getLibraryFingerprint() {
103
100
  path.resolve(here, '..', 'core', 'style-builder', 'build-style.cjs'),
104
101
  path.resolve(here, '..', 'core', 'parser', 'tw-parser.mjs'),
105
102
  path.resolve(here, '..', 'core', 'parser', 'tw-parser.cjs'),
106
- path.resolve(here, 'transform-ast.mjs'),
107
- path.resolve(here, 'transform-ast.cjs'),
103
+ path.resolve(here, 'wrap-imports.mjs'),
104
+ path.resolve(here, 'wrap-imports.cjs'),
108
105
  path.resolve(here, 'transformer.mjs'),
109
106
  path.resolve(here, 'transformer.cjs'),
110
107
  // Source-tree fallback for tests + workspace dev (no built lib yet).
111
108
  path.resolve(here, '..', '..', 'src', 'core', 'style-builder', 'build-style.ts'),
112
109
  path.resolve(here, '..', '..', 'src', 'core', 'parser', 'tw-parser.ts'),
113
- path.resolve(here, '..', '..', 'src', 'metro', 'transform-ast.ts'),
110
+ path.resolve(here, '..', '..', 'src', 'metro', 'wrap-imports.ts'),
114
111
  path.resolve(here, '..', '..', 'src', 'metro', 'transformer.ts'),
115
112
  ];
116
113
  const hash = createHash('sha256');
@@ -135,12 +132,10 @@ function getLibraryFingerprint() {
135
132
  * can rebuild the same state without re-reading the Metro config.
136
133
  * @param cssEntryFile Absolute path to the user's theme CSS.
137
134
  * @param cacheDir Absolute path to the cache dir (`.rnwind`).
138
- * @param watchFolders
139
- * @param classNamePrefixes Extra JSX prop-name prefixes to rewrite.
140
- * @param hostSources
141
- * @param hostComponents
135
+ * @param watchFolders Monorepo watch folders to scan for atoms.
136
+ * @param wrapModules Extra modules whose component exports get `wrap()`-ed.
142
137
  */
143
- function configureRnwindState(cssEntryFile, cacheDir, watchFolders = [], classNamePrefixes, hostSources, hostComponents) {
138
+ function configureRnwindState(cssEntryFile, cacheDir, watchFolders = [], wrapModules) {
144
139
  process.env[CSS_ENTRY_ENV] = cssEntryFile;
145
140
  process.env[CACHE_DIR_ENV] = cacheDir;
146
141
  if (watchFolders.length === 0) {
@@ -149,62 +144,23 @@ function configureRnwindState(cssEntryFile, cacheDir, watchFolders = [], classNa
149
144
  else {
150
145
  process.env[WATCH_FOLDERS_ENV] = watchFolders.join('\0');
151
146
  }
152
- if (!classNamePrefixes || classNamePrefixes.length === 0) {
153
- delete process.env[CLASSNAME_PREFIXES_ENV];
147
+ if (!wrapModules || wrapModules.length === 0) {
148
+ delete process.env[WRAP_MODULES_ENV];
154
149
  }
155
150
  else {
156
- process.env[CLASSNAME_PREFIXES_ENV] = classNamePrefixes.join(',');
157
- }
158
- if (!hostSources || hostSources.length === 0) {
159
- delete process.env[HOST_SOURCES_ENV];
160
- }
161
- else {
162
- process.env[HOST_SOURCES_ENV] = hostSources.join(',');
163
- }
164
- if (!hostComponents || hostComponents.length === 0) {
165
- delete process.env[HOST_COMPONENTS_ENV];
166
- }
167
- else {
168
- process.env[HOST_COMPONENTS_ENV] = hostComponents.join(',');
151
+ process.env[WRAP_MODULES_ENV] = wrapModules.join(',');
169
152
  }
170
153
  cached = null;
171
154
  }
172
155
  /**
173
- * Read the caller-configured extra className prefixes out of the
174
- * worker environment. Returns an empty array when unset — the
175
- * transformer applies the built-in `contentContainer` default on top
176
- * either way.
177
- * @returns User-supplied extra prefixes.
178
- */
179
- function getClassNamePrefixes() {
180
- const raw = process.env[CLASSNAME_PREFIXES_ENV];
181
- if (!raw || raw.length === 0)
182
- return [];
183
- return raw.split(',').filter((entry) => entry.length > 0);
184
- }
185
- /**
186
- * Read the caller-configured extra host module sources out of the
187
- * worker environment. Empty array when unset — the transformer applies
188
- * its built-in default list on top either way.
189
- * @returns User-supplied extra host sources.
156
+ * Effective module → wrap-policy map: the built-in defaults merged with
157
+ * any extra modules the Metro config supplied.
158
+ * @returns Module policy map the import-rewrite consults.
190
159
  */
191
- function getHostSources() {
192
- const raw = process.env[HOST_SOURCES_ENV];
193
- if (!raw || raw.length === 0)
194
- return [];
195
- return raw.split(',').filter((entry) => entry.length > 0);
196
- }
197
- /**
198
- * Read the caller-configured extra host JSX tag names out of the worker
199
- * environment. Verbatim names — may include `.` for member expressions
200
- * like `'Animated.View'`.
201
- * @returns User-supplied extra host component names.
202
- */
203
- function getHostComponents() {
204
- const raw = process.env[HOST_COMPONENTS_ENV];
205
- if (!raw || raw.length === 0)
206
- return [];
207
- return raw.split(',').filter((entry) => entry.length > 0);
160
+ function getWrapModules() {
161
+ const raw = process.env[WRAP_MODULES_ENV];
162
+ const extra = raw && raw.length > 0 ? raw.split(',').filter((entry) => entry.length > 0) : undefined;
163
+ return buildWrapModules(extra);
208
164
  }
209
165
  /**
210
166
  * Fetch (or build) the worker-local rnwind state. Re-reads the theme
@@ -246,14 +202,12 @@ function getRnwindState(projectRoot) {
246
202
  */
247
203
  function getRnwindCacheKey() {
248
204
  const cssEntry = process.env[CSS_ENTRY_ENV] ?? '';
249
- const prefixes = process.env[CLASSNAME_PREFIXES_ENV] ?? '';
250
- // Host source / component config changes which JSX tags get rewritten,
251
- // so it MUST flip the cache key otherwise Metro replays stale
252
- // transforms (a newly-opted-in host keeps its raw className, a removed
253
- // one keeps the rewrite).
254
- const hostSources = process.env[HOST_SOURCES_ENV] ?? '';
255
- const hostComponents = process.env[HOST_COMPONENTS_ENV] ?? '';
256
- return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|pfx:${prefixes}|hs:${hostSources}|hc:${hostComponents}`;
205
+ // Wrap-module config changes which import sites get `wrap()`-ed, so it
206
+ // MUST flip the cache key otherwise Metro replays stale transforms
207
+ // (a newly-opted-in module keeps its raw import, a removed one keeps
208
+ // the wrap).
209
+ const wrapModules = process.env[WRAP_MODULES_ENV] ?? '';
210
+ return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|wm:${wrapModules}`;
257
211
  }
258
212
  /** Drop the cached state — call after editing the theme CSS. */
259
213
  function resetRnwindState() {
@@ -287,5 +241,5 @@ function manifestPathFor() {
287
241
  return path.join(cacheDir, 'schemes.js');
288
242
  }
289
243
 
290
- export { configureRnwindState, getClassNamePrefixes, getHostComponents, getHostSources, getRnwindCacheKey, getRnwindState, manifestPathFor, onThemeChange, resetRnwindState };
244
+ export { configureRnwindState, getRnwindCacheKey, getRnwindState, getWrapModules, manifestPathFor, onThemeChange, resetRnwindState };
291
245
  //# sourceMappingURL=state.mjs.map
@@ -1 +1 @@
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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"}
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'\nimport { buildWrapModules } from './wrap-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 modules whose component exports get `wrap()`-ed. Comma-separated. */\nconst WRAP_MODULES_ENV = 'RNWIND_WRAP_MODULES'\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 import-rewriter (`wrap-imports`) and runtime resolver\n * alongside the parser / style-builder so a change to the transformer —\n * e.g. renaming the injected wrap helper — invalidates every stale\n * per-file cache entry 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, 'wrap-imports.mjs'),\n path.resolve(here, 'wrap-imports.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', 'wrap-imports.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 Monorepo watch folders to scan for atoms.\n * @param wrapModules Extra modules whose component exports get `wrap()`-ed.\n */\nexport function configureRnwindState(\n cssEntryFile: string,\n cacheDir: string,\n watchFolders: readonly string[] = [],\n wrapModules?: 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 (!wrapModules || wrapModules.length === 0) {\n delete process.env[WRAP_MODULES_ENV]\n } else {\n process.env[WRAP_MODULES_ENV] = wrapModules.join(',')\n }\n cached = null\n}\n\n/**\n * Effective module → wrap-policy map: the built-in defaults merged with\n * any extra modules the Metro config supplied.\n * @returns Module → policy map the import-rewrite consults.\n */\nexport function getWrapModules(): ReturnType<typeof buildWrapModules> {\n const raw = process.env[WRAP_MODULES_ENV]\n const extra = raw && raw.length > 0 ? raw.split(',').filter((entry) => entry.length > 0) : undefined\n return buildWrapModules(extra)\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 // Wrap-module config changes which import sites get `wrap()`-ed, so it\n // MUST flip the cache key — otherwise Metro replays stale transforms\n // (a newly-opted-in module keeps its raw import, a removed one keeps\n // the wrap).\n const wrapModules = process.env[WRAP_MODULES_ENV] ?? ''\n return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|wm:${wrapModules}`\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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"}
@@ -3,8 +3,8 @@ import { parse } from '@babel/parser';
3
3
  import generate from '@babel/generator';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { realpathSync } from 'node:fs';
6
- import { transformAst } from './transform-ast.mjs';
7
- import { onThemeChange, getRnwindState, getRnwindCacheKey, getClassNamePrefixes, getHostSources, getHostComponents } from './state.mjs';
6
+ import { onThemeChange, getRnwindState, getRnwindCacheKey, getWrapModules } from './state.mjs';
7
+ import { rewriteWrapImports } from './wrap-imports.mjs';
8
8
  import { THEME_SIGNATURE_MODULE, STYLE_SPECIFIERS } from './resolver.mjs';
9
9
  import { filterUnknownClassCandidates } from './warn-unknown-classes.mjs';
10
10
 
@@ -54,10 +54,19 @@ function parseUserSource(source) {
54
54
  * @param candidates Every candidate oxide surfaced from the source.
55
55
  * @param atoms Successfully resolved atoms (keys are class names).
56
56
  * @param filename Source path, prefixed onto the warning.
57
+ * @param features Feature-atom maps (gradient / haptic) — their names are
58
+ * known classes even though they carry no RN style, so they're excluded
59
+ * from the unknown-class warning.
57
60
  */
58
- function warnUnknownClasses(source, candidates, atoms, filename) {
59
- const atomNames = new Set(atoms.keys());
60
- const unknown = filterUnknownClassCandidates(source, candidates, atomNames);
61
+ function warnUnknownClasses(source, candidates, atoms, filename, features = []) {
62
+ // Feature atoms (gradient / haptic) resolve to no RN style, so they're
63
+ // absent from `atoms` — but they're NOT unknown. Fold their names into
64
+ // the known set so `active:haptic-rigid` etc. don't warn at build time.
65
+ const known = new Set(atoms.keys());
66
+ for (const map of features)
67
+ for (const name of map.keys())
68
+ known.add(name);
69
+ const unknown = filterUnknownClassCandidates(source, candidates, known);
61
70
  if (unknown.length === 0)
62
71
  return;
63
72
  // eslint-disable-next-line no-console
@@ -100,52 +109,181 @@ function isThemeCssEntry(filename) {
100
109
  return typeof cssEntry === 'string' && cssEntry.length > 0 && cssEntry === filename;
101
110
  }
102
111
  /**
103
- * Parse + run rnwind's JSX rewrite + regenerate source code. When
104
- * parsing or transformation fails, fall back to the original source —
105
- * we don't want a transient parse error to crash Metro for a file the
106
- * upstream might handle fine.
112
+ * Wrap host imports + compile any className literals, then regenerate
113
+ * source. Two paths:
114
+ * - **className present**: oxide-scan the file, record its atoms into
115
+ * the union, and inject the generated-style + theme-signature
116
+ * side-effect imports so the runtime registries populate.
117
+ * - **import-only** (a `{...rest}` forwarder or a leaf with no literal
118
+ * `className=`): just wrap the host imports so a forwarded className
119
+ * still resolves at render — no oxide scan, no injected imports.
120
+ *
121
+ * On parse failure, fall back to the original source — a transient parse
122
+ * error shouldn't crash Metro for a file the upstream might handle fine.
107
123
  * @param args Metro args; `src` is the original source text.
108
- * @returns Rewritten source text (with `className=` rewrites applied).
124
+ * @returns Rewritten source text.
109
125
  */
110
126
  async function rewriteSource(args) {
111
127
  const ast = parseUserSource(args.src);
112
128
  if (!ast)
113
129
  return args.src;
130
+ // Wrap host component imports so `<View className=…>` resolves at render
131
+ // through the runtime `wrap` (works for literal, spread, and forwarded
132
+ // classNames alike). No JSX is rewritten here.
133
+ const wrapped = rewriteWrapImports(ast, getWrapModules());
134
+ if (!/classname=/i.test(args.src)) {
135
+ // Import-only file: nothing to compile. Drop any stale atom
136
+ // contribution (className may have just been removed) and emit the
137
+ // wrapped imports — or the untouched source when nothing wrapped.
138
+ dropFileSafely(args.filename, projectRootOf(args));
139
+ return wrapped ? generateModule(ast).code : args.src;
140
+ }
114
141
  const state = getRnwindState(projectRootOf(args));
115
142
  const extension = extensionOf(args.filename);
116
143
  const parsed = await state.parser.parseAtoms({ content: args.src, extension });
117
- warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename);
118
- const classNamePrefixes = getClassNamePrefixes();
119
- const hostSources = getHostSources();
120
- const hostComponents = getHostComponents();
144
+ warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename, [parsed.gradientAtoms, parsed.hapticAtoms]);
121
145
  if (parsed.atoms.size === 0) {
122
146
  state.builder.dropFile(args.filename);
123
147
  await state.builder.writeSchemes();
124
- transformAst(ast, {
125
- styleSpecifiers: [],
126
- gradientAtoms: parsed.gradientAtoms,
127
- hapticAtoms: parsed.hapticAtoms,
128
- classNamePrefixes,
129
- hostSources,
130
- hostComponents,
131
- });
132
148
  injectThemeSignatureImport(ast);
133
149
  return generateModule(ast).code;
134
150
  }
135
- const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes);
151
+ const literals = collectClassNameLiterals(ast);
152
+ const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, literals);
136
153
  if (changed)
137
154
  await state.builder.writeSchemes();
138
- transformAst(ast, {
139
- styleSpecifiers: STYLE_SPECIFIERS,
140
- gradientAtoms: parsed.gradientAtoms,
141
- hapticAtoms: parsed.hapticAtoms,
142
- classNamePrefixes,
143
- hostSources,
144
- hostComponents,
145
- });
155
+ injectSideEffectImports(ast, STYLE_SPECIFIERS);
146
156
  injectThemeSignatureImport(ast);
147
157
  return generateModule(ast).code;
148
158
  }
159
+ /**
160
+ * Drop a file's union contribution, swallowing the "state not configured"
161
+ * error unit tests hit when they call the transformer without
162
+ * `configureRnwindState`.
163
+ * @param filename Absolute source path.
164
+ * @param projectRoot Project root for state lookup.
165
+ */
166
+ function dropFileSafely(filename, projectRoot) {
167
+ try {
168
+ getRnwindState(projectRoot).builder.dropFile(filename);
169
+ }
170
+ catch {
171
+ // State not configured (standalone/unit test). Nothing to drop.
172
+ }
173
+ }
174
+ /**
175
+ * Whether a JSX attribute names a className-style prop (`className` or
176
+ * any `<prefix>ClassName`).
177
+ * @param node JSX attribute node.
178
+ * @returns True when the attribute is a className prop.
179
+ */
180
+ function isClassNameAttribute(node) {
181
+ if (!t.isJSXIdentifier(node.name))
182
+ return false;
183
+ const { name } = node.name;
184
+ return name === 'className' || name.endsWith('ClassName');
185
+ }
186
+ /**
187
+ * Pull static string literals out of a className expression. Handles a
188
+ * bare string, a no-substitution template, and the branches of a
189
+ * ternary / `&&` (so `cond ? 'a' : 'b'` and `flag && 'x'` both register
190
+ * their literals). Dynamic interpolations are skipped — they resolve via
191
+ * the runtime atom path.
192
+ * @param expr Expression inside a `className={...}` container.
193
+ * @param out Accumulator for discovered literals.
194
+ */
195
+ function collectLiteralsFromExpression(expr, out) {
196
+ if (!expr)
197
+ return;
198
+ if (t.isStringLiteral(expr)) {
199
+ out.push(expr.value);
200
+ return;
201
+ }
202
+ if (t.isTemplateLiteral(expr) && expr.expressions.length === 0 && expr.quasis.length === 1) {
203
+ const cooked = expr.quasis[0]?.value.cooked;
204
+ if (typeof cooked === 'string')
205
+ out.push(cooked);
206
+ return;
207
+ }
208
+ if (t.isConditionalExpression(expr)) {
209
+ collectLiteralsFromExpression(expr.consequent, out);
210
+ collectLiteralsFromExpression(expr.alternate, out);
211
+ return;
212
+ }
213
+ if (t.isLogicalExpression(expr)) {
214
+ collectLiteralsFromExpression(expr.right, out);
215
+ }
216
+ }
217
+ /** AST node keys the literal walk skips — position / comment metadata. */
218
+ const SKIP_WALK_KEYS = new Set(['type', 'loc', 'start', 'end', 'range', 'leadingComments', 'trailingComments', 'innerComments']);
219
+ /**
220
+ * Collect the static literals from one className JSX attribute into the
221
+ * dedup accumulator.
222
+ * @param attribute The (already className-matched) JSX attribute.
223
+ * @param seen Dedup set of literals already collected.
224
+ * @param out Ordered accumulator.
225
+ */
226
+ function collectAttributeLiterals(attribute, seen, out) {
227
+ const { value } = attribute;
228
+ const found = [];
229
+ if (t.isStringLiteral(value))
230
+ found.push(value.value);
231
+ else if (t.isJSXExpressionContainer(value))
232
+ collectLiteralsFromExpression(value.expression, found);
233
+ for (const literal of found) {
234
+ if (seen.has(literal))
235
+ continue;
236
+ seen.add(literal);
237
+ out.push(literal);
238
+ }
239
+ }
240
+ /**
241
+ * Walk the AST for every `className=` / `<prefix>ClassName=` literal so
242
+ * the builder can pre-merge each into a per-scheme molecule. A generic
243
+ * node walk (no scope build) keeps it cheap; only JSX attribute nodes do
244
+ * any work.
245
+ * @param ast Parsed Babel file.
246
+ * @returns Distinct literal className strings, in first-seen order.
247
+ */
248
+ function collectClassNameLiterals(ast) {
249
+ const out = [];
250
+ const seen = new Set();
251
+ const visit = (node) => {
252
+ if (!node || typeof node !== 'object')
253
+ return;
254
+ if (Array.isArray(node)) {
255
+ for (const child of node)
256
+ visit(child);
257
+ return;
258
+ }
259
+ const typed = node;
260
+ if (typeof typed.type !== 'string')
261
+ return;
262
+ if (typed.type === 'JSXAttribute' && isClassNameAttribute(node)) {
263
+ collectAttributeLiterals(node, seen, out);
264
+ }
265
+ for (const key in typed) {
266
+ if (SKIP_WALK_KEYS.has(key))
267
+ continue;
268
+ visit(typed[key]);
269
+ }
270
+ };
271
+ visit(ast.program);
272
+ return out;
273
+ }
274
+ /**
275
+ * Prepend side-effect imports (`import '<spec>'`) so the generated
276
+ * per-scheme style + manifest modules load — registering this file's
277
+ * atoms / molecules / features into the runtime registries the wrapper's
278
+ * `resolve` reads.
279
+ * @param ast Babel File AST to mutate in place.
280
+ * @param specifiers Module specifiers to side-effect-import.
281
+ */
282
+ function injectSideEffectImports(ast, specifiers) {
283
+ for (const specifier of specifiers) {
284
+ ast.program.body.unshift(t.importDeclaration([], t.stringLiteral(specifier)));
285
+ }
286
+ }
149
287
  /**
150
288
  * Prepend `import 'rnwind/__generated/theme-signature'` to every
151
289
  * rnwind-transformed file. The resolver maps that specifier to the
@@ -241,10 +379,16 @@ function loadUpstream() {
241
379
  function isRewriteCandidate(args) {
242
380
  if (!/\.(?:tsx|ts|jsx|js)$/i.test(args.filename))
243
381
  return false;
244
- // Case-insensitive so `<prefix>ClassName=` (e.g. `contentContainerClassName=`)
245
- // which has a capital `C` and so doesn't contain the lowercase
246
- // `className=` still routes the file through the rewrite pass.
247
- if (!/classname=/i.test(args.src))
382
+ // Process the file when it either:
383
+ // - carries a `className=` / `<prefix>ClassName=` literal (case-
384
+ // insensitive `contentContainerClassName=` has a capital C), or
385
+ // - spreads props (`{...rest}`) onto a host from a wrap-module, where a
386
+ // forwarded className must still get its import wrapped (no literal
387
+ // appears in this file). A style-less `<View/>` with neither is left
388
+ // alone so it never pays for an unused wrapper.
389
+ const hasClassName = /classname=/i.test(args.src);
390
+ const isForwarder = /\{\s*\.\.\./.test(args.src) && mentionsWrapModule(args.src);
391
+ if (!hasClassName && !isForwarder)
248
392
  return false;
249
393
  if (!args.filename.includes('/node_modules/'))
250
394
  return true;
@@ -257,6 +401,21 @@ function isRewriteCandidate(args) {
257
401
  return false;
258
402
  }
259
403
  }
404
+ /**
405
+ * Cheap pre-parse check: does the source import from any configured
406
+ * wrap-module? A quoted specifier match is enough — `rewriteWrapImports`
407
+ * re-verifies precisely on the AST, so a false positive only costs a
408
+ * no-op parse.
409
+ * @param source Source text.
410
+ * @returns True when a wrap-module specifier appears in the source.
411
+ */
412
+ function mentionsWrapModule(source) {
413
+ for (const moduleName of getWrapModules().keys()) {
414
+ if (source.includes(`'${moduleName}'`) || source.includes(`"${moduleName}"`))
415
+ return true;
416
+ }
417
+ return false;
418
+ }
260
419
  /**
261
420
  * Fallback parse when no upstream is configured AND Metro didn't hand
262
421
  * us an AST. Used by unit tests and standalone setups.