rnwind 0.0.3 → 0.0.5

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 (134) 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/css-imports.cjs +81 -0
  11. package/lib/cjs/metro/css-imports.cjs.map +1 -0
  12. package/lib/cjs/metro/css-imports.d.ts +8 -0
  13. package/lib/cjs/metro/dts.cjs +7 -16
  14. package/lib/cjs/metro/dts.cjs.map +1 -1
  15. package/lib/cjs/metro/dts.d.ts +2 -4
  16. package/lib/cjs/metro/state.cjs +38 -86
  17. package/lib/cjs/metro/state.cjs.map +1 -1
  18. package/lib/cjs/metro/state.d.ts +8 -25
  19. package/lib/cjs/metro/transformer.cjs +193 -34
  20. package/lib/cjs/metro/transformer.cjs.map +1 -1
  21. package/lib/cjs/metro/with-config.cjs +2 -2
  22. package/lib/cjs/metro/with-config.cjs.map +1 -1
  23. package/lib/cjs/metro/with-config.d.ts +11 -26
  24. package/lib/cjs/metro/wrap-imports.cjs +273 -0
  25. package/lib/cjs/metro/wrap-imports.cjs.map +1 -0
  26. package/lib/cjs/metro/wrap-imports.d.ts +26 -0
  27. package/lib/cjs/runtime/components/rnwind-provider.cjs +0 -17
  28. package/lib/cjs/runtime/components/rnwind-provider.cjs.map +1 -1
  29. package/lib/cjs/runtime/components/rnwind-provider.d.ts +0 -14
  30. package/lib/cjs/runtime/hooks/use-css.cjs +16 -10
  31. package/lib/cjs/runtime/hooks/use-css.cjs.map +1 -1
  32. package/lib/cjs/runtime/hooks/use-css.d.ts +15 -9
  33. package/lib/cjs/runtime/index.cjs +11 -13
  34. package/lib/cjs/runtime/index.cjs.map +1 -1
  35. package/lib/cjs/runtime/index.d.ts +4 -9
  36. package/lib/cjs/runtime/lookup-css.cjs +10 -0
  37. package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
  38. package/lib/cjs/runtime/lookup-css.d.ts +7 -0
  39. package/lib/cjs/runtime/resolve.cjs +348 -0
  40. package/lib/cjs/runtime/resolve.cjs.map +1 -0
  41. package/lib/cjs/runtime/resolve.d.ts +61 -0
  42. package/lib/cjs/runtime/wrap.cjs +254 -0
  43. package/lib/cjs/runtime/wrap.cjs.map +1 -0
  44. package/lib/cjs/runtime/wrap.d.ts +37 -0
  45. package/lib/cjs/testing/index.cjs +81 -50
  46. package/lib/cjs/testing/index.cjs.map +1 -1
  47. package/lib/esm/core/normalize-classname.d.ts +10 -0
  48. package/lib/esm/core/normalize-classname.mjs +23 -0
  49. package/lib/esm/core/normalize-classname.mjs.map +1 -0
  50. package/lib/esm/core/style-builder/build-style.d.ts +6 -1
  51. package/lib/esm/core/style-builder/build-style.mjs +258 -58
  52. package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
  53. package/lib/esm/core/style-builder/union-builder.d.ts +21 -1
  54. package/lib/esm/core/style-builder/union-builder.mjs +37 -3
  55. package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
  56. package/lib/esm/metro/css-imports.d.ts +8 -0
  57. package/lib/esm/metro/css-imports.mjs +79 -0
  58. package/lib/esm/metro/css-imports.mjs.map +1 -0
  59. package/lib/esm/metro/dts.d.ts +2 -4
  60. package/lib/esm/metro/dts.mjs +7 -16
  61. package/lib/esm/metro/dts.mjs.map +1 -1
  62. package/lib/esm/metro/state.d.ts +8 -25
  63. package/lib/esm/metro/state.mjs +39 -85
  64. package/lib/esm/metro/state.mjs.map +1 -1
  65. package/lib/esm/metro/transformer.mjs +194 -35
  66. package/lib/esm/metro/transformer.mjs.map +1 -1
  67. package/lib/esm/metro/with-config.d.ts +11 -26
  68. package/lib/esm/metro/with-config.mjs +2 -2
  69. package/lib/esm/metro/with-config.mjs.map +1 -1
  70. package/lib/esm/metro/wrap-imports.d.ts +26 -0
  71. package/lib/esm/metro/wrap-imports.mjs +250 -0
  72. package/lib/esm/metro/wrap-imports.mjs.map +1 -0
  73. package/lib/esm/runtime/components/rnwind-provider.d.ts +0 -14
  74. package/lib/esm/runtime/components/rnwind-provider.mjs +1 -17
  75. package/lib/esm/runtime/components/rnwind-provider.mjs.map +1 -1
  76. package/lib/esm/runtime/hooks/use-css.d.ts +15 -9
  77. package/lib/esm/runtime/hooks/use-css.mjs +16 -10
  78. package/lib/esm/runtime/hooks/use-css.mjs.map +1 -1
  79. package/lib/esm/runtime/index.d.ts +4 -9
  80. package/lib/esm/runtime/index.mjs +4 -4
  81. package/lib/esm/runtime/index.mjs.map +1 -1
  82. package/lib/esm/runtime/lookup-css.d.ts +7 -0
  83. package/lib/esm/runtime/lookup-css.mjs +10 -1
  84. package/lib/esm/runtime/lookup-css.mjs.map +1 -1
  85. package/lib/esm/runtime/resolve.d.ts +61 -0
  86. package/lib/esm/runtime/resolve.mjs +341 -0
  87. package/lib/esm/runtime/resolve.mjs.map +1 -0
  88. package/lib/esm/runtime/wrap.d.ts +37 -0
  89. package/lib/esm/runtime/wrap.mjs +251 -0
  90. package/lib/esm/runtime/wrap.mjs.map +1 -0
  91. package/lib/esm/testing/index.mjs +84 -53
  92. package/lib/esm/testing/index.mjs.map +1 -1
  93. package/package.json +2 -1
  94. package/src/core/normalize-classname.ts +19 -0
  95. package/src/core/style-builder/build-style.ts +286 -55
  96. package/src/core/style-builder/union-builder.ts +36 -3
  97. package/src/metro/css-imports.ts +75 -0
  98. package/src/metro/dts.ts +7 -19
  99. package/src/metro/state.ts +38 -83
  100. package/src/metro/transformer.ts +190 -34
  101. package/src/metro/with-config.ts +13 -28
  102. package/src/metro/wrap-imports.ts +260 -0
  103. package/src/runtime/components/rnwind-provider.tsx +0 -17
  104. package/src/runtime/hooks/use-css.ts +17 -11
  105. package/src/runtime/index.ts +3 -26
  106. package/src/runtime/lookup-css.ts +10 -0
  107. package/src/runtime/resolve.ts +381 -0
  108. package/src/runtime/wrap.tsx +267 -0
  109. package/src/testing/index.ts +106 -56
  110. package/lib/cjs/core/parser/text-truncate.cjs +0 -78
  111. package/lib/cjs/core/parser/text-truncate.cjs.map +0 -1
  112. package/lib/cjs/metro/transform-ast.cjs +0 -1472
  113. package/lib/cjs/metro/transform-ast.cjs.map +0 -1
  114. package/lib/cjs/metro/transform-ast.d.ts +0 -88
  115. package/lib/cjs/runtime/haptics.cjs +0 -113
  116. package/lib/cjs/runtime/haptics.cjs.map +0 -1
  117. package/lib/cjs/runtime/haptics.d.ts +0 -48
  118. package/lib/cjs/runtime/interactive-box.cjs +0 -35
  119. package/lib/cjs/runtime/interactive-box.cjs.map +0 -1
  120. package/lib/cjs/runtime/interactive-box.d.ts +0 -40
  121. package/lib/esm/core/parser/text-truncate.mjs +0 -75
  122. package/lib/esm/core/parser/text-truncate.mjs.map +0 -1
  123. package/lib/esm/metro/transform-ast.d.ts +0 -88
  124. package/lib/esm/metro/transform-ast.mjs +0 -1451
  125. package/lib/esm/metro/transform-ast.mjs.map +0 -1
  126. package/lib/esm/runtime/haptics.d.ts +0 -48
  127. package/lib/esm/runtime/haptics.mjs +0 -110
  128. package/lib/esm/runtime/haptics.mjs.map +0 -1
  129. package/lib/esm/runtime/interactive-box.d.ts +0 -40
  130. package/lib/esm/runtime/interactive-box.mjs +0 -33
  131. package/lib/esm/runtime/interactive-box.mjs.map +0 -1
  132. package/src/metro/transform-ast.ts +0 -1729
  133. package/src/runtime/haptics.ts +0 -120
  134. package/src/runtime/interactive-box.tsx +0 -57
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto'
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
- import type { KeyframeBlock, SchemedStyle, TailwindParser } from '../parser'
4
+ import type { GradientAtomInfo, HapticRequest, KeyframeBlock, SchemedStyle, TailwindParser } from '../parser'
5
5
  import { buildSchemeSources, type AtomSerializedCache } from './build-style'
6
6
 
7
7
  /** Manifest module basename — the file SchemeProvider imports via the resolver. */
@@ -90,6 +90,17 @@ class UnionBuilder {
90
90
  private readonly parser: TailwindParser
91
91
  private readonly unionAtoms = new Map<string, SchemedStyle>()
92
92
  private readonly unionKeyframes = new Map<string, KeyframeBlock>()
93
+ /** atom name → gradient role/colour, surfaced into the manifest's `registerGradients`. */
94
+ private readonly unionGradients = new Map<string, GradientAtomInfo>()
95
+ /** atom name → haptic request, surfaced into the manifest's `registerHaptics`. */
96
+ private readonly unionHaptics = new Map<string, HapticRequest>()
97
+ /**
98
+ * Distinct literal className strings seen across all files, pre-merged
99
+ * into per-scheme molecules at write time. Accumulate-only (like
100
+ * `unionAtoms`): orphaned literals just yield unused molecules and get
101
+ * reaped on the next cold start, so no refcount is needed.
102
+ */
103
+ private readonly unionLiterals = new Set<string>()
93
104
  /**
94
105
  * Responsive breakpoints captured from the parser. Refreshed on every
95
106
  * `recordFile` / `ensureProjectScanned` so user-defined
@@ -169,6 +180,8 @@ class UnionBuilder {
169
180
  const parsed = await this.parser.parseProject()
170
181
  for (const [name, style] of parsed.atoms) this.unionAtoms.set(name, style)
171
182
  for (const [name, kf] of parsed.keyframes) this.unionKeyframes.set(name, kf)
183
+ for (const [name, gradient] of parsed.gradientAtoms) this.unionGradients.set(name, gradient)
184
+ for (const [name, haptic] of parsed.hapticAtoms) this.unionHaptics.set(name, haptic)
172
185
  this.breakpoints = parsed.breakpoints
173
186
  this.projectScanned = true
174
187
  })()
@@ -187,6 +200,7 @@ class UnionBuilder {
187
200
  * @param file Absolute source file path.
188
201
  * @param atoms Per-atom resolved schemed styles from this transform.
189
202
  * @param keyframes Keyframe blocks referenced by this file's atoms.
203
+ * @param literals
190
204
  * @returns `{ changed: true }` when the union shifted (new atom name,
191
205
  * removed atom name, or new keyframe) — the transformer uses this
192
206
  * to skip the serializer + `writeSchemes` when nothing changed.
@@ -195,8 +209,10 @@ class UnionBuilder {
195
209
  file: string,
196
210
  atoms: ReadonlyMap<string, SchemedStyle>,
197
211
  keyframes: ReadonlyMap<string, KeyframeBlock>,
212
+ literals: readonly string[] = [],
198
213
  ): Promise<{ changed: boolean }> {
199
214
  await this.ensureProjectScanned()
215
+ const literalAdded = this.recordLiterals(literals)
200
216
  const newAtomNames = new Set(atoms.keys())
201
217
  const previous = this.fileAtomSets.get(file)
202
218
  if (previous && setsEqual(previous, newAtomNames)) {
@@ -211,12 +227,29 @@ class UnionBuilder {
211
227
  if (!this.unionKeyframes.has(name)) keyframeAdded = true
212
228
  this.unionKeyframes.set(name, kf)
213
229
  }
214
- return { changed: keyframeAdded }
230
+ return { changed: keyframeAdded || literalAdded }
215
231
  }
216
232
  this.applyDiff(file, newAtomNames, atoms, keyframes)
217
233
  return { changed: true }
218
234
  }
219
235
 
236
+ /**
237
+ * Merge a file's literal classNames into the union. A literal the
238
+ * union hasn't seen flips `changed` so `writeSchemes` re-emits the
239
+ * scheme files with the new molecule.
240
+ * @param literals Distinct literal className strings.
241
+ * @returns Whether any literal was new to the union.
242
+ */
243
+ private recordLiterals(literals: readonly string[]): boolean {
244
+ let added = false
245
+ for (const literal of literals) {
246
+ if (this.unionLiterals.has(literal)) continue
247
+ this.unionLiterals.add(literal)
248
+ added = true
249
+ }
250
+ return added
251
+ }
252
+
220
253
  /**
221
254
  * Forget one source file's contribution. Idempotent — repeated calls
222
255
  * for a file that's already dropped are no-ops. Does NOT remove the
@@ -246,7 +279,7 @@ class UnionBuilder {
246
279
  public async writeSchemes(): Promise<{ changedSchemes: readonly string[] }> {
247
280
  await this.ensureProjectScanned()
248
281
  const sortedAtomNames = [...this.unionAtoms.keys()].toSorted((a, b) => a.localeCompare(b))
249
- const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints)
282
+ const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints, this.unionGradients, this.unionHaptics, [...this.unionLiterals])
250
283
  this.serializedMissesCount += result.serializedMisses
251
284
  const { schemeSources, manifestSource } = result
252
285
 
@@ -0,0 +1,75 @@
1
+ import { createRequire } from 'node:module'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+
4
+ /**
5
+ * Inline `@import` resolution for the theme entry CSS.
6
+ *
7
+ * rnwind's scheme extractors (`extractThemeVars`, `extractSchemeAliases`,
8
+ * `extractCustomVariantSchemes`) are plain text passes over the entry
9
+ * CSS — they don't follow `@import`. Tailwind's own compiler DOES follow
10
+ * imports, so utilities still compile, but a user who keeps their theme
11
+ * in a separate file and points the entry at it:
12
+ *
13
+ * ```css
14
+ * @import "@acme/ui/theme.css"; // global.css — the cssEntryFile
15
+ * ```
16
+ *
17
+ * would hand the extractors a file with no `@theme` / `@variant` /
18
+ * `@custom-variant` in sight → every scheme collapses to base and theme
19
+ * switching dies. This module flattens those user imports first so the
20
+ * extractors (and the compiler) see the real declarations.
21
+ */
22
+
23
+ /** Matches a bare `@import "spec";` / `@import 'spec';` (no media/layer suffix). */
24
+ const CSS_IMPORT = /@import\s+(["'])([^"']+)\1\s*;/g
25
+
26
+ /**
27
+ * Specs left untouched for the Tailwind compiler to resolve itself.
28
+ * `tailwindcss` resolves to JS (not inlinable) and `rnwind/css` is the
29
+ * framework preset — both are the compiler's job, not user theme files.
30
+ */
31
+ const FRAMEWORK_SPECS = new Set(['tailwindcss', 'rnwind/css', 'rnwind'])
32
+
33
+ /**
34
+ * Read `filePath` and replace each user `@import "<spec>";` whose spec
35
+ * resolves (Node resolution, honouring `exports` maps and workspace
36
+ * symlinks) to a readable `.css` file with that file's inlined +
37
+ * recursively-resolved contents. Unresolvable specs, non-CSS targets,
38
+ * and {@link FRAMEWORK_SPECS} are left as-is for the compiler.
39
+ * @param filePath Absolute path of the CSS file being inlined.
40
+ * @param seen Visited set guarding against import cycles.
41
+ * @returns CSS text with user imports flattened in place.
42
+ */
43
+ function inlineImports(filePath: string, seen: Set<string>): string {
44
+ if (seen.has(filePath)) return ''
45
+ seen.add(filePath)
46
+ let css: string
47
+ try {
48
+ css = readFileSync(filePath, 'utf8')
49
+ } catch {
50
+ return ''
51
+ }
52
+ const request = createRequire(filePath)
53
+ return css.replaceAll(CSS_IMPORT, (full: string, _quote: string, spec: string): string => {
54
+ if (FRAMEWORK_SPECS.has(spec)) return full
55
+ let resolved: string
56
+ try {
57
+ resolved = request.resolve(spec)
58
+ } catch {
59
+ return full
60
+ }
61
+ if (!resolved.endsWith('.css') || !existsSync(resolved)) return full
62
+ return inlineImports(resolved, seen)
63
+ })
64
+ }
65
+
66
+ /**
67
+ * Flatten user `@import`s in a theme entry CSS file so rnwind's text-based
68
+ * scheme extractors see the real `@theme` / `@variant` / `@custom-variant`
69
+ * declarations even when the entry only re-exports them via `@import`.
70
+ * @param entryFilePath Absolute path to the theme entry CSS (the cssEntryFile).
71
+ * @returns Inlined CSS, or `''` when the entry can't be read.
72
+ */
73
+ export function resolveThemeCss(entryFilePath: string): string {
74
+ return inlineImports(entryFilePath, new Set<string>())
75
+ }
package/src/metro/dts.ts CHANGED
@@ -56,21 +56,15 @@ const CONTENT_CONTAINER_INTERFACES: ReadonlySet<string> = new Set([
56
56
 
57
57
  /**
58
58
  * Build the body of one interface augmentation: `className?: string`
59
- * plus `<prefix>ClassName?: string` for every prefix that applies to
60
- * THIS interface. Emits a single-line body so the file stays easy to
61
- * scan and diff.
59
+ * plus `contentContainerClassName?: string` for the scroll interfaces
60
+ * that natively expose `contentContainerStyle`. Emits a single-line
61
+ * body so the file stays easy to scan and diff.
62
62
  * @param interfaceName Bare interface name (generic parameters stripped).
63
- * @param userPrefixes Extra prefixes from the Metro config — applied to
64
- * every interface the same way `className` is.
65
63
  * @returns Space-separated property declarations.
66
64
  */
67
- function buildInterfaceBody(interfaceName: string, userPrefixes: readonly string[]): string {
65
+ function buildInterfaceBody(interfaceName: string): string {
68
66
  const props = ['className?: string']
69
67
  if (CONTENT_CONTAINER_INTERFACES.has(interfaceName)) props.push(`${BUILTIN_PREFIX}ClassName?: string`)
70
- for (const prefix of userPrefixes) {
71
- if (prefix === BUILTIN_PREFIX) continue
72
- props.push(`${prefix}ClassName?: string`)
73
- }
74
68
  return props.join('; ')
75
69
  }
76
70
 
@@ -83,17 +77,11 @@ function buildInterfaceBody(interfaceName: string, userPrefixes: readonly string
83
77
  * `Scheme` union so `useScheme()` returns the actual names.
84
78
  *
85
79
  * Called once at Metro-config time — overwrite-on-rewrite so the file
86
- * stays in sync with the user's current theme CSS + prefix config.
80
+ * stays in sync with the user's current theme CSS.
87
81
  * @param targetPath Absolute path to write (typically `rnwind-types.d.ts` at project root).
88
82
  * @param schemes Scheme names from the user's `@variant` blocks (empty when none declared).
89
- * @param classNamePrefixes Extra prefixes from the Metro config — merged
90
- * on top of the built-in `'contentContainer'`. Defaults to empty.
91
83
  */
92
- export function writeDtsFile(
93
- targetPath: string,
94
- schemes: readonly string[],
95
- classNamePrefixes: readonly string[] = [],
96
- ): void {
84
+ export function writeDtsFile(targetPath: string, schemes: readonly string[]): void {
97
85
  const lines: string[] = [
98
86
  '// Auto-generated by rnwind — do not edit by hand.',
99
87
  '// Overwritten on Metro start / theme CSS change.',
@@ -102,7 +90,7 @@ export function writeDtsFile(
102
90
  ]
103
91
  for (const entry of INTERFACES) {
104
92
  const name = typeof entry === 'string' ? entry : entry.name
105
- const body = buildInterfaceBody(name, classNamePrefixes)
93
+ const body = buildInterfaceBody(name)
106
94
  if (typeof entry === 'string') {
107
95
  lines.push(` interface ${entry} { ${body} }`)
108
96
  continue
@@ -1,8 +1,10 @@
1
- import { existsSync, readFileSync, statSync } from 'node:fs'
1
+ import { existsSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { createHash } from 'node:crypto'
4
4
  import { UnionBuilder } from '../core/style-builder'
5
5
  import { TailwindParser, type SourceEntry } from '../core/parser'
6
+ import { resolveThemeCss } from './css-imports'
7
+ import { buildWrapModules } from './wrap-imports'
6
8
 
7
9
  /**
8
10
  * Default oxide Scanner globs — walk every JS/TS source under the
@@ -48,12 +50,8 @@ const CSS_ENTRY_ENV = 'RNWIND_CSS_ENTRY_FILE'
48
50
  const CACHE_DIR_ENV = 'RNWIND_CACHE_DIR'
49
51
  /** Env var carrying `watchFolders` from Metro config (NUL-separated). */
50
52
  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'
53
+ /** Env var carrying extra modules whose component exports get `wrap()`-ed. Comma-separated. */
54
+ const WRAP_MODULES_ENV = 'RNWIND_WRAP_MODULES'
57
55
 
58
56
  /** Memoised library fingerprint — read once per worker process. */
59
57
  let libraryFingerprint: string | undefined
@@ -62,19 +60,18 @@ let libraryFingerprint: string | undefined
62
60
  let cached: RnwindState | null = null
63
61
 
64
62
  /**
65
- * Cheap content-hash readout. SHA-256 prefix of the CSS bytes plus the
66
- * file's mtime nanoseconds (so identical content with different mtime
67
- * atomic rewrites still picks up the change). Returns `'missing'`
68
- * when the file can't be read so the cache key is still deterministic.
63
+ * Cheap content-hash readout. SHA-256 prefix of the FULLY-RESOLVED theme
64
+ * CSS — `@import`s flattened so an edit to a theme file the entry only
65
+ * re-exports (`@import "@acme/ui/theme.css"`) still rotates the hash and
66
+ * invalidates Metro's cache. Returns `'missing'` when the entry can't be
67
+ * read so the cache key stays deterministic.
69
68
  * @param cssPath Absolute CSS path.
70
69
  * @returns 16-char hex content hash.
71
70
  */
72
71
  function readThemeHashFor(cssPath: string): string {
73
72
  if (!existsSync(cssPath)) return 'missing'
74
73
  try {
75
- const bytes = readFileSync(cssPath)
76
- const mtime = statSync(cssPath).mtimeMs.toString()
77
- return createHash('sha256').update(bytes).update(mtime).digest('hex').slice(0, 16)
74
+ return createHash('sha256').update(resolveThemeCss(cssPath)).digest('hex').slice(0, 16)
78
75
  } catch {
79
76
  return 'missing'
80
77
  }
@@ -86,10 +83,10 @@ function readThemeHashFor(cssPath: string): string {
86
83
  * dev OR npm install of a new version) the file bytes change, the
87
84
  * fingerprint rotates, and Metro's transform cache invalidates.
88
85
  *
89
- * Includes the JSX rewriter (`transform-ast`) alongside the parser /
90
- * style-builder so a change to the transformer — e.g. renaming the
91
- * injected context hook — invalidates every stale per-file cache entry
92
- * on the next dev run. Without this, a user upgrading rnwind in-place
86
+ * Includes the import-rewriter (`wrap-imports`) and runtime resolver
87
+ * alongside the parser / style-builder so a change to the transformer —
88
+ * e.g. renaming the injected wrap helper — invalidates every stale
89
+ * per-file cache entry on the next dev run. Without this, a user upgrading rnwind in-place
93
90
  * would keep loading the old transformed bytes; React-refresh would
94
91
  * then preserve fiber state across the version bump and the rendered
95
92
  * hook list could shift, surfacing as "change in the order of Hooks"
@@ -105,14 +102,14 @@ function getLibraryFingerprint(): string {
105
102
  path.resolve(here, '..', 'core', 'style-builder', 'build-style.cjs'),
106
103
  path.resolve(here, '..', 'core', 'parser', 'tw-parser.mjs'),
107
104
  path.resolve(here, '..', 'core', 'parser', 'tw-parser.cjs'),
108
- path.resolve(here, 'transform-ast.mjs'),
109
- path.resolve(here, 'transform-ast.cjs'),
105
+ path.resolve(here, 'wrap-imports.mjs'),
106
+ path.resolve(here, 'wrap-imports.cjs'),
110
107
  path.resolve(here, 'transformer.mjs'),
111
108
  path.resolve(here, 'transformer.cjs'),
112
109
  // Source-tree fallback for tests + workspace dev (no built lib yet).
113
110
  path.resolve(here, '..', '..', 'src', 'core', 'style-builder', 'build-style.ts'),
114
111
  path.resolve(here, '..', '..', 'src', 'core', 'parser', 'tw-parser.ts'),
115
- path.resolve(here, '..', '..', 'src', 'metro', 'transform-ast.ts'),
112
+ path.resolve(here, '..', '..', 'src', 'metro', 'wrap-imports.ts'),
116
113
  path.resolve(here, '..', '..', 'src', 'metro', 'transformer.ts'),
117
114
  ]
118
115
  const hash = createHash('sha256')
@@ -148,18 +145,14 @@ export interface RnwindState {
148
145
  * can rebuild the same state without re-reading the Metro config.
149
146
  * @param cssEntryFile Absolute path to the user's theme CSS.
150
147
  * @param cacheDir Absolute path to the cache dir (`.rnwind`).
151
- * @param watchFolders
152
- * @param classNamePrefixes Extra JSX prop-name prefixes to rewrite.
153
- * @param hostSources
154
- * @param hostComponents
148
+ * @param watchFolders Monorepo watch folders to scan for atoms.
149
+ * @param wrapModules Extra modules whose component exports get `wrap()`-ed.
155
150
  */
156
151
  export function configureRnwindState(
157
152
  cssEntryFile: string,
158
153
  cacheDir: string,
159
154
  watchFolders: readonly string[] = [],
160
- classNamePrefixes?: readonly string[],
161
- hostSources?: readonly string[],
162
- hostComponents?: readonly string[],
155
+ wrapModules?: readonly string[],
163
156
  ): void {
164
157
  process.env[CSS_ENTRY_ENV] = cssEntryFile
165
158
  process.env[CACHE_DIR_ENV] = cacheDir
@@ -168,59 +161,23 @@ export function configureRnwindState(
168
161
  } else {
169
162
  process.env[WATCH_FOLDERS_ENV] = watchFolders.join('\0')
170
163
  }
171
- if (!classNamePrefixes || classNamePrefixes.length === 0) {
172
- delete process.env[CLASSNAME_PREFIXES_ENV]
164
+ if (!wrapModules || wrapModules.length === 0) {
165
+ delete process.env[WRAP_MODULES_ENV]
173
166
  } else {
174
- process.env[CLASSNAME_PREFIXES_ENV] = classNamePrefixes.join(',')
175
- }
176
- if (!hostSources || hostSources.length === 0) {
177
- delete process.env[HOST_SOURCES_ENV]
178
- } else {
179
- process.env[HOST_SOURCES_ENV] = hostSources.join(',')
180
- }
181
- if (!hostComponents || hostComponents.length === 0) {
182
- delete process.env[HOST_COMPONENTS_ENV]
183
- } else {
184
- process.env[HOST_COMPONENTS_ENV] = hostComponents.join(',')
167
+ process.env[WRAP_MODULES_ENV] = wrapModules.join(',')
185
168
  }
186
169
  cached = null
187
170
  }
188
171
 
189
172
  /**
190
- * Read the caller-configured extra className prefixes out of the
191
- * worker environment. Returns an empty array when unset — the
192
- * transformer applies the built-in `contentContainer` default on top
193
- * either way.
194
- * @returns User-supplied extra prefixes.
173
+ * Effective module → wrap-policy map: the built-in defaults merged with
174
+ * any extra modules the Metro config supplied.
175
+ * @returns Module policy map the import-rewrite consults.
195
176
  */
196
- export function getClassNamePrefixes(): readonly string[] {
197
- const raw = process.env[CLASSNAME_PREFIXES_ENV]
198
- if (!raw || raw.length === 0) return []
199
- return raw.split(',').filter((entry) => entry.length > 0)
200
- }
201
-
202
- /**
203
- * Read the caller-configured extra host module sources out of the
204
- * worker environment. Empty array when unset — the transformer applies
205
- * its built-in default list on top either way.
206
- * @returns User-supplied extra host sources.
207
- */
208
- export function getHostSources(): readonly string[] {
209
- const raw = process.env[HOST_SOURCES_ENV]
210
- if (!raw || raw.length === 0) return []
211
- return raw.split(',').filter((entry) => entry.length > 0)
212
- }
213
-
214
- /**
215
- * Read the caller-configured extra host JSX tag names out of the worker
216
- * environment. Verbatim names — may include `.` for member expressions
217
- * like `'Animated.View'`.
218
- * @returns User-supplied extra host component names.
219
- */
220
- export function getHostComponents(): readonly string[] {
221
- const raw = process.env[HOST_COMPONENTS_ENV]
222
- if (!raw || raw.length === 0) return []
223
- return raw.split(',').filter((entry) => entry.length > 0)
177
+ export function getWrapModules(): ReturnType<typeof buildWrapModules> {
178
+ const raw = process.env[WRAP_MODULES_ENV]
179
+ const extra = raw && raw.length > 0 ? raw.split(',').filter((entry) => entry.length > 0) : undefined
180
+ return buildWrapModules(extra)
224
181
  }
225
182
 
226
183
  /**
@@ -240,7 +197,7 @@ export function getRnwindState(projectRoot: string): RnwindState {
240
197
  if (!cacheDir) throw new Error('rnwind: RNWIND_CACHE_DIR is not set — did `withRnwindConfig` run?')
241
198
  const currentHash = readThemeHashFor(cssEntry)
242
199
  if (cached?.themeHash === currentHash && cached.projectRoot === projectRoot) return cached
243
- const themeCss = readFileSync(cssEntry, 'utf8')
200
+ const themeCss = resolveThemeCss(cssEntry)
244
201
  const parser = new TailwindParser({
245
202
  themeCss,
246
203
  sources: defaultSources(projectRoot, cacheDir, readWatchFolders()),
@@ -261,14 +218,12 @@ export function getRnwindState(projectRoot: string): RnwindState {
261
218
  */
262
219
  export function getRnwindCacheKey(): string {
263
220
  const cssEntry = process.env[CSS_ENTRY_ENV] ?? ''
264
- const prefixes = process.env[CLASSNAME_PREFIXES_ENV] ?? ''
265
- // Host source / component config changes which JSX tags get rewritten,
266
- // so it MUST flip the cache key otherwise Metro replays stale
267
- // transforms (a newly-opted-in host keeps its raw className, a removed
268
- // one keeps the rewrite).
269
- const hostSources = process.env[HOST_SOURCES_ENV] ?? ''
270
- const hostComponents = process.env[HOST_COMPONENTS_ENV] ?? ''
271
- return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|pfx:${prefixes}|hs:${hostSources}|hc:${hostComponents}`
221
+ // Wrap-module config changes which import sites get `wrap()`-ed, so it
222
+ // MUST flip the cache key otherwise Metro replays stale transforms
223
+ // (a newly-opted-in module keeps its raw import, a removed one keeps
224
+ // the wrap).
225
+ const wrapModules = process.env[WRAP_MODULES_ENV] ?? ''
226
+ return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|wm:${wrapModules}`
272
227
  }
273
228
 
274
229
  /** Drop the cached state — call after editing the theme CSS. */