rnwind 0.0.8 → 0.0.10

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 (145) hide show
  1. package/lib/cjs/core/parser/color.cjs +33 -1
  2. package/lib/cjs/core/parser/color.cjs.map +1 -1
  3. package/lib/cjs/core/parser/color.d.ts +10 -0
  4. package/lib/cjs/core/parser/declaration.cjs +121 -9
  5. package/lib/cjs/core/parser/declaration.cjs.map +1 -1
  6. package/lib/cjs/core/parser/gradient.cjs +46 -12
  7. package/lib/cjs/core/parser/gradient.cjs.map +1 -1
  8. package/lib/cjs/core/parser/gradient.d.ts +2 -1
  9. package/lib/cjs/core/parser/keyframes.cjs +27 -12
  10. package/lib/cjs/core/parser/keyframes.cjs.map +1 -1
  11. package/lib/cjs/core/parser/keyframes.d.ts +11 -0
  12. package/lib/cjs/core/parser/layout-dispatcher.cjs +33 -10
  13. package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -1
  14. package/lib/cjs/core/parser/length.cjs +17 -1
  15. package/lib/cjs/core/parser/length.cjs.map +1 -1
  16. package/lib/cjs/core/parser/safe-area.cjs +24 -3
  17. package/lib/cjs/core/parser/safe-area.cjs.map +1 -1
  18. package/lib/cjs/core/parser/theme-vars.cjs +58 -8
  19. package/lib/cjs/core/parser/theme-vars.cjs.map +1 -1
  20. package/lib/cjs/core/parser/tokens.cjs +77 -9
  21. package/lib/cjs/core/parser/tokens.cjs.map +1 -1
  22. package/lib/cjs/core/parser/tokens.d.ts +9 -0
  23. package/lib/cjs/core/parser/transform.cjs +18 -9
  24. package/lib/cjs/core/parser/transform.cjs.map +1 -1
  25. package/lib/cjs/core/parser/tw-parser.cjs +136 -34
  26. package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
  27. package/lib/cjs/core/parser/tw-parser.d.ts +20 -0
  28. package/lib/cjs/core/parser/typography-dispatcher.cjs +19 -1
  29. package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -1
  30. package/lib/cjs/core/parser/typography.cjs +15 -18
  31. package/lib/cjs/core/parser/typography.cjs.map +1 -1
  32. package/lib/cjs/core/parser/typography.d.ts +5 -5
  33. package/lib/cjs/core/style-builder/build-style.cjs +12 -3
  34. package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
  35. package/lib/cjs/core/style-builder/build-style.d.ts +3 -1
  36. package/lib/cjs/core/style-builder/union-builder.cjs +9 -11
  37. package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
  38. package/lib/cjs/core/style-builder/union-builder.d.ts +7 -8
  39. package/lib/cjs/metro/dts.cjs +6 -1
  40. package/lib/cjs/metro/dts.cjs.map +1 -1
  41. package/lib/cjs/metro/transformer.cjs +42 -77
  42. package/lib/cjs/metro/transformer.cjs.map +1 -1
  43. package/lib/cjs/metro/with-config.cjs +9 -29
  44. package/lib/cjs/metro/with-config.cjs.map +1 -1
  45. package/lib/cjs/runtime/hooks/use-scheme.cjs +17 -11
  46. package/lib/cjs/runtime/hooks/use-scheme.cjs.map +1 -1
  47. package/lib/cjs/runtime/hooks/use-scheme.d.ts +7 -4
  48. package/lib/cjs/runtime/index.cjs +2 -1
  49. package/lib/cjs/runtime/index.cjs.map +1 -1
  50. package/lib/cjs/runtime/index.d.ts +2 -2
  51. package/lib/cjs/runtime/lookup-css.cjs +41 -0
  52. package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
  53. package/lib/cjs/runtime/lookup-css.d.ts +29 -0
  54. package/lib/cjs/runtime/resolve.cjs +8 -6
  55. package/lib/cjs/runtime/resolve.cjs.map +1 -1
  56. package/lib/cjs/runtime/wrap.cjs +50 -57
  57. package/lib/cjs/runtime/wrap.cjs.map +1 -1
  58. package/lib/cjs/runtime/wrap.d.ts +10 -4
  59. package/lib/cjs/testing/index.cjs +1 -1
  60. package/lib/cjs/testing/index.cjs.map +1 -1
  61. package/lib/esm/core/parser/color.d.ts +10 -0
  62. package/lib/esm/core/parser/color.mjs +34 -3
  63. package/lib/esm/core/parser/color.mjs.map +1 -1
  64. package/lib/esm/core/parser/declaration.mjs +122 -10
  65. package/lib/esm/core/parser/declaration.mjs.map +1 -1
  66. package/lib/esm/core/parser/gradient.d.ts +2 -1
  67. package/lib/esm/core/parser/gradient.mjs +45 -11
  68. package/lib/esm/core/parser/gradient.mjs.map +1 -1
  69. package/lib/esm/core/parser/keyframes.d.ts +11 -0
  70. package/lib/esm/core/parser/keyframes.mjs +27 -12
  71. package/lib/esm/core/parser/keyframes.mjs.map +1 -1
  72. package/lib/esm/core/parser/layout-dispatcher.mjs +33 -10
  73. package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
  74. package/lib/esm/core/parser/length.mjs +17 -1
  75. package/lib/esm/core/parser/length.mjs.map +1 -1
  76. package/lib/esm/core/parser/safe-area.mjs +24 -3
  77. package/lib/esm/core/parser/safe-area.mjs.map +1 -1
  78. package/lib/esm/core/parser/theme-vars.mjs +58 -8
  79. package/lib/esm/core/parser/theme-vars.mjs.map +1 -1
  80. package/lib/esm/core/parser/tokens.d.ts +9 -0
  81. package/lib/esm/core/parser/tokens.mjs +77 -10
  82. package/lib/esm/core/parser/tokens.mjs.map +1 -1
  83. package/lib/esm/core/parser/transform.mjs +18 -9
  84. package/lib/esm/core/parser/transform.mjs.map +1 -1
  85. package/lib/esm/core/parser/tw-parser.d.ts +20 -0
  86. package/lib/esm/core/parser/tw-parser.mjs +138 -36
  87. package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
  88. package/lib/esm/core/parser/typography-dispatcher.mjs +19 -1
  89. package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
  90. package/lib/esm/core/parser/typography.d.ts +5 -5
  91. package/lib/esm/core/parser/typography.mjs +15 -18
  92. package/lib/esm/core/parser/typography.mjs.map +1 -1
  93. package/lib/esm/core/style-builder/build-style.d.ts +3 -1
  94. package/lib/esm/core/style-builder/build-style.mjs +12 -3
  95. package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
  96. package/lib/esm/core/style-builder/union-builder.d.ts +7 -8
  97. package/lib/esm/core/style-builder/union-builder.mjs +9 -11
  98. package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
  99. package/lib/esm/metro/dts.mjs +6 -1
  100. package/lib/esm/metro/dts.mjs.map +1 -1
  101. package/lib/esm/metro/transformer.mjs +42 -77
  102. package/lib/esm/metro/transformer.mjs.map +1 -1
  103. package/lib/esm/metro/with-config.mjs +10 -30
  104. package/lib/esm/metro/with-config.mjs.map +1 -1
  105. package/lib/esm/runtime/hooks/use-scheme.d.ts +7 -4
  106. package/lib/esm/runtime/hooks/use-scheme.mjs +17 -11
  107. package/lib/esm/runtime/hooks/use-scheme.mjs.map +1 -1
  108. package/lib/esm/runtime/index.d.ts +2 -2
  109. package/lib/esm/runtime/index.mjs +2 -2
  110. package/lib/esm/runtime/index.mjs.map +1 -1
  111. package/lib/esm/runtime/lookup-css.d.ts +29 -0
  112. package/lib/esm/runtime/lookup-css.mjs +39 -1
  113. package/lib/esm/runtime/lookup-css.mjs.map +1 -1
  114. package/lib/esm/runtime/resolve.mjs +9 -7
  115. package/lib/esm/runtime/resolve.mjs.map +1 -1
  116. package/lib/esm/runtime/wrap.d.ts +10 -4
  117. package/lib/esm/runtime/wrap.mjs +50 -57
  118. package/lib/esm/runtime/wrap.mjs.map +1 -1
  119. package/lib/esm/testing/index.mjs +2 -2
  120. package/lib/esm/testing/index.mjs.map +1 -1
  121. package/package.json +1 -1
  122. package/src/core/parser/color.ts +32 -1
  123. package/src/core/parser/declaration.ts +119 -10
  124. package/src/core/parser/gradient.ts +48 -11
  125. package/src/core/parser/keyframes.ts +31 -3
  126. package/src/core/parser/layout-dispatcher.ts +32 -9
  127. package/src/core/parser/length.ts +18 -1
  128. package/src/core/parser/safe-area.ts +23 -2
  129. package/src/core/parser/theme-vars.ts +75 -8
  130. package/src/core/parser/tokens.ts +76 -9
  131. package/src/core/parser/transform.ts +19 -8
  132. package/src/core/parser/tw-parser.ts +148 -31
  133. package/src/core/parser/typography-dispatcher.ts +20 -1
  134. package/src/core/parser/typography.ts +15 -15
  135. package/src/core/style-builder/build-style.ts +12 -1
  136. package/src/core/style-builder/union-builder.ts +10 -12
  137. package/src/metro/dts.ts +6 -1
  138. package/src/metro/transformer.ts +42 -78
  139. package/src/metro/with-config.ts +10 -29
  140. package/src/runtime/hooks/use-scheme.ts +17 -10
  141. package/src/runtime/index.ts +2 -1
  142. package/src/runtime/lookup-css.ts +42 -0
  143. package/src/runtime/resolve.ts +9 -7
  144. package/src/runtime/wrap.tsx +57 -61
  145. package/src/testing/index.ts +3 -0
@@ -1,4 +1,5 @@
1
1
  import type { KeyframeBlock, RNStyle, SchemedStyle } from '../parser'
2
+ import type { ThemeTables } from '../types'
2
3
  import { normalizeClassName } from '../normalize-classname'
3
4
 
4
5
  /** Match atom names like `border-hairline`, `h-hairline`, `border-t-hairline`, etc. */
@@ -300,6 +301,7 @@ function serializeBreakpoints(breakpoints: ReadonlyMap<string, number>): string
300
301
  * @param breakpoints Responsive breakpoint name → px-threshold map.
301
302
  * @param gradients Atom → gradient info for `registerGradients`.
302
303
  * @param haptics Atom → haptic request for `registerHaptics`.
304
+ * @param themeTokens
303
305
  * @returns JS source text.
304
306
  */
305
307
  function renderManifest(
@@ -307,15 +309,19 @@ function renderManifest(
307
309
  breakpoints: ReadonlyMap<string, number>,
308
310
  gradients: ReadonlyMap<string, unknown>,
309
311
  haptics: ReadonlyMap<string, unknown>,
312
+ themeTokens: ThemeTables,
310
313
  ): string {
314
+ const hasTokens = Object.keys(themeTokens).length > 0
311
315
  const imports = ['registerSchemeLoader', 'registerBreakpoints']
312
316
  if (gradients.size > 0) imports.push('registerGradients')
313
317
  if (haptics.size > 0) imports.push('registerHaptics')
318
+ if (hasTokens) imports.push('registerThemeTokens')
314
319
  const lines: string[] = [`import { ${imports.join(', ')} } from 'rnwind'`, `import './common.style'`]
315
320
  for (const variant of variants) lines.push(`import ${JSON.stringify(`./${variant}.style`)}`)
316
321
  lines.push(``, `registerBreakpoints(${serializeBreakpoints(breakpoints)})`)
317
322
  if (gradients.size > 0) lines.push(`registerGradients(${serializeFeatureMap(gradients)})`)
318
323
  if (haptics.size > 0) lines.push(`registerHaptics(${serializeFeatureMap(haptics)})`)
324
+ if (hasTokens) lines.push(`registerThemeTokens(${JSON.stringify(themeTokens)})`)
319
325
  lines.push(
320
326
  ``,
321
327
  `function ensureSchemeLoaded(_name) {}`,
@@ -679,6 +685,7 @@ const EMPTY_BREAKPOINTS: ReadonlyMap<string, number> = new Map()
679
685
  * @param literals Distinct literal className strings — pre-merged into
680
686
  * per-scheme molecules so the runtime resolver's O(1) molecule-first
681
687
  * path is populated. Empty for legacy/test callers (atom path only).
688
+ * @param themeTokens
682
689
  * @returns Per-scheme sources, manifest source, variant list.
683
690
  */
684
691
  export function buildSchemeSources(
@@ -690,6 +697,7 @@ export function buildSchemeSources(
690
697
  gradients: ReadonlyMap<string, unknown> = EMPTY_FEATURE_MAP,
691
698
  haptics: ReadonlyMap<string, unknown> = EMPTY_FEATURE_MAP,
692
699
  literals: readonly string[] = EMPTY_LITERALS,
700
+ themeTokens: ThemeTables = EMPTY_THEME_TOKENS,
693
701
  ): BuildSchemeSourcesOutput {
694
702
  const variants = collectVariantSchemes(resolved)
695
703
  const commonEntries: (readonly [string, string])[] = []
@@ -715,7 +723,7 @@ export function buildSchemeSources(
715
723
 
716
724
  return {
717
725
  schemeSources,
718
- manifestSource: renderManifest(variants, breakpoints, gradients, haptics),
726
+ manifestSource: renderManifest(variants, breakpoints, gradients, haptics, themeTokens),
719
727
  variants,
720
728
  serializedMisses: misses,
721
729
  }
@@ -727,5 +735,8 @@ const EMPTY_FEATURE_MAP: ReadonlyMap<string, unknown> = new Map()
727
735
  /** Shared empty literal-list default (atom-only callers). */
728
736
  const EMPTY_LITERALS: readonly string[] = []
729
737
 
738
+ /** Shared empty theme-token default (callers without theme tokens). */
739
+ const EMPTY_THEME_TOKENS: ThemeTables = {}
740
+
730
741
  /** Registry key the runtime uses for the always-loaded fallback. */
731
742
  export const COMMON_SCHEME_NAME: string = COMMON_SCHEME
@@ -2,6 +2,7 @@ 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
4
  import type { GradientAtomInfo, HapticRequest, KeyframeBlock, SchemedStyle, TailwindParser } from '../parser'
5
+ import type { ThemeTables } from '../types'
5
6
  import { buildSchemeSources, type AtomSerializedCache } from './build-style'
6
7
 
7
8
  /** Manifest module basename — the file SchemeProvider imports via the resolver. */
@@ -110,6 +111,13 @@ class UnionBuilder {
110
111
  * snapshot is sufficient.
111
112
  */
112
113
  private breakpoints: ReadonlyMap<string, number> = new Map()
114
+ /**
115
+ * Per-scheme theme token tables captured from the parser. Refreshed on
116
+ * every `recordFile` / `ensureProjectScanned` so theme-token edits land in
117
+ * the manifest's `registerThemeTokens({...})` — the data source for
118
+ * `useColor` / `useToken` / `useSize`.
119
+ */
120
+ private themeTokens: ThemeTables = {}
113
121
  /** file → set of atom names this file currently contributes. */
114
122
  private readonly fileAtomSets = new Map<string, Set<string>>()
115
123
  /** atom name → how many files currently contribute it (refcount). */
@@ -143,17 +151,6 @@ class UnionBuilder {
143
151
  return path.join(this.cacheDir, MANIFEST_BASENAME)
144
152
  }
145
153
 
146
- /**
147
- * Snapshot of every source file the builder has recorded atoms for
148
- * this worker session. Used by `withRnwindConfig`'s CSS watcher to
149
- * touch `mtime` on each and nudge Metro into re-transforming them
150
- * with the new theme values.
151
- * @returns Absolute source paths.
152
- */
153
- public recordedFiles(): readonly string[] {
154
- return [...this.fileAtomSets.keys()]
155
- }
156
-
157
154
  /** Cumulative cache-miss count — exposed for tests to assert cache behaviour. */
158
155
  public get serializedMisses(): number {
159
156
  return this.serializedMissesCount
@@ -183,6 +180,7 @@ class UnionBuilder {
183
180
  for (const [name, gradient] of parsed.gradientAtoms) this.unionGradients.set(name, gradient)
184
181
  for (const [name, haptic] of parsed.hapticAtoms) this.unionHaptics.set(name, haptic)
185
182
  this.breakpoints = parsed.breakpoints
183
+ this.themeTokens = parsed.themeTokens
186
184
  this.projectScanned = true
187
185
  })()
188
186
  try {
@@ -279,7 +277,7 @@ class UnionBuilder {
279
277
  public async writeSchemes(): Promise<{ changedSchemes: readonly string[] }> {
280
278
  await this.ensureProjectScanned()
281
279
  const sortedAtomNames = [...this.unionAtoms.keys()].toSorted((a, b) => a.localeCompare(b))
282
- const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints, this.unionGradients, this.unionHaptics, [...this.unionLiterals])
280
+ const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints, this.unionGradients, this.unionHaptics, [...this.unionLiterals], this.themeTokens)
283
281
  this.serializedMissesCount += result.serializedMisses
284
282
  const { schemeSources, manifestSource } = result
285
283
 
package/src/metro/dts.ts CHANGED
@@ -100,7 +100,12 @@ export function writeDtsFile(targetPath: string, schemes: readonly string[]): vo
100
100
  lines.push('}', '')
101
101
  if (schemes.length > 0) {
102
102
  lines.push(`declare module 'rnwind' {`, ` export interface RnwindConfig {`)
103
- const schemeLiterals = schemes.map((s) => `'${s}'`).join(', ')
103
+ // Escape backslash / single-quote so a scheme name with a quote (only
104
+ // reachable via the public `writeDtsFile`, since CSS idents can't contain
105
+ // one) can't emit invalid TS that breaks the whole file and drops the
106
+ // `className` augmentation project-wide. Single-quoted to match the rest
107
+ // of the generated declaration.
108
+ const schemeLiterals = schemes.map((s) => `'${s.replaceAll('\\', '\\\\').replaceAll("'", String.raw`\'`)}'`).join(', ')
104
109
  lines.push(` themes: readonly [${schemeLiterals}]`, ` }`, '}', '')
105
110
  }
106
111
  // The `export {}` is mandatory — without at least one top-level
@@ -17,14 +17,6 @@ interface UpstreamTransformer {
17
17
  /** Env var that points at the upstream `babelTransformerPath` we override. */
18
18
  const UPSTREAM_ENV = 'RNWIND_UPSTREAM_TRANSFORMER'
19
19
 
20
- /**
21
- * Matches a `useCss(` call. Files that resolve classes via the `useCss`
22
- * hook (no JSX `className=`) must still be scanned so the classes inside
23
- * `useCss("…")` get registered — common for theme/accent hooks living in a
24
- * shared package the project scan may not reach.
25
- */
26
- const USE_CSS_CALL = /\buseCss\s*\(/
27
-
28
20
  /** Cached upstream module — required once, reused across every transform call. */
29
21
  let cachedUpstream: UpstreamTransformer | null = null
30
22
 
@@ -145,44 +137,53 @@ function isThemeCssEntry(filename: string): boolean {
145
137
  * @returns Rewritten source text.
146
138
  */
147
139
  async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
148
- const ast = parseUserSource(args.src)
149
- if (!ast) return args.src
140
+ // SCAN FIRST — exactly like Tailwind: oxide extracts class candidates from
141
+ // the WHOLE file content (className, cva/clsx, plain strings, anywhere),
142
+ // and the compiler is the only filter. No babel parse needed to scan, so
143
+ // the common "file with no classes" case stays cheap and the source is
144
+ // returned untouched. This is what makes adding a class ANYWHERE register
145
+ // on hot-reload, not just inside a `className=` or a known helper call.
146
+ const state = getRnwindState(projectRootOf(args))
147
+ const extension = extensionOf(args.filename)
148
+ const parsed = await state.parser.parseAtoms({ content: args.src, extension })
149
+ const hasAtoms = parsed.atoms.size > 0
150
150
 
151
151
  const hasClassName = /classname=/i.test(args.src)
152
- const hasUseCss = USE_CSS_CALL.test(args.src)
153
152
  const hasSpread = /\{\s*\.\.\./.test(args.src)
154
153
 
155
- // Wrap host component imports ONLY when className arrives through a
156
- // component written (`hasClassName`) or forwarded (`hasSpread`). A
157
- // `useCss("…")`-only file resolves manually, so we must NOT wrap its
158
- // imports: a non-RN drawing lib (e.g. skia's `LinearGradient` nested in
159
- // `SkiaText`) breaks if its primitives are replaced by a wrapper.
160
- const wrapped = hasClassName || hasSpread ? rewriteWrapImports(ast, getWrapModules()) : false
161
-
162
- if (!hasClassName && !hasUseCss) {
163
- // Import-only file: nothing to compile. Drop any stale atom
164
- // contribution (className may have just been removed) and emit the
165
- // wrapped imports — or the untouched source when nothing wrapped.
166
- dropFileSafely(args.filename, projectRootOf(args))
167
- return wrapped ? generateModule(ast).code : args.src
154
+ // Nothing for rnwind to do: no Tailwind class compiled AND no host
155
+ // wrapping needed. Drop any stale contribution and emit the file as-is.
156
+ if (!hasAtoms && !hasClassName && !hasSpread) {
157
+ state.builder.dropFile(args.filename)
158
+ return args.src
168
159
  }
169
160
 
170
- // Scan path — runs for `className=` literals AND/OR `useCss("…")` strings.
171
- // oxide reads class candidates from the whole file content, so classes
172
- // that only ever appear inside a `useCss("…")` call still register.
173
- const state = getRnwindState(projectRootOf(args))
174
- const extension = extensionOf(args.filename)
175
- const parsed = await state.parser.parseAtoms({ content: args.src, extension })
161
+ const ast = parseUserSource(args.src)
162
+ if (!ast) {
163
+ // Can't parse to wrap/inject, but we DID scan still record the atoms so
164
+ // they register (a malformed-but-recoverable file shouldn't lose styles).
165
+ if (hasAtoms) {
166
+ await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, [])
167
+ await state.builder.writeSchemes()
168
+ } else {
169
+ state.builder.dropFile(args.filename)
170
+ }
171
+ return args.src
172
+ }
176
173
 
177
- warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename, [parsed.gradientAtoms, parsed.hapticAtoms])
174
+ // Wrap host imports ONLY when className flows through a component — written
175
+ // (`hasClassName`) or forwarded (`{...rest}`). A `useCss`/`cva`-only file
176
+ // resolves manually, so its imports (e.g. skia drawing primitives) are
177
+ // left untouched.
178
+ const wrapped = hasClassName || hasSpread ? rewriteWrapImports(ast, getWrapModules()) : false
178
179
 
179
- if (parsed.atoms.size === 0) {
180
+ if (!hasAtoms) {
181
+ // Wrap-only forwarder — no classes to record/register.
180
182
  state.builder.dropFile(args.filename)
181
- await state.builder.writeSchemes()
182
- injectThemeSignatureImport(ast)
183
- return generateModule(ast).code
183
+ return wrapped ? generateModule(ast).code : args.src
184
184
  }
185
185
 
186
+ warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename, [parsed.gradientAtoms, parsed.hapticAtoms])
186
187
  const literals = collectClassNameLiterals(ast)
187
188
  const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, literals)
188
189
  if (changed) await state.builder.writeSchemes()
@@ -192,21 +193,6 @@ async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
192
193
  return generateModule(ast).code
193
194
  }
194
195
 
195
- /**
196
- * Drop a file's union contribution, swallowing the "state not configured"
197
- * error unit tests hit when they call the transformer without
198
- * `configureRnwindState`.
199
- * @param filename Absolute source path.
200
- * @param projectRoot Project root for state lookup.
201
- */
202
- function dropFileSafely(filename: string, projectRoot: string): void {
203
- try {
204
- getRnwindState(projectRoot).builder.dropFile(filename)
205
- } catch {
206
- // State not configured (standalone/unit test). Nothing to drop.
207
- }
208
- }
209
-
210
196
  /**
211
197
  * Whether a JSX attribute names a className-style prop (`className` or
212
198
  * any `<prefix>ClassName`).
@@ -404,19 +390,12 @@ function loadUpstream(): UpstreamTransformer | null {
404
390
  */
405
391
  function isRewriteCandidate(args: BabelTransformerArgs): boolean {
406
392
  if (!/\.(?:tsx|ts|jsx|js)$/i.test(args.filename)) return false
407
- // Process the file when it either:
408
- // - carries a `className=` / `<prefix>ClassName=` literal (case-
409
- // insensitive `contentContainerClassName=` has a capital C), or
410
- // - calls `useCss("…")` (classes that only ever appear in the hook must
411
- // still be scanned + registered common for theme/accent hooks), or
412
- // - spreads props (`{...rest}`) onto a host from a wrap-module, where a
413
- // forwarded className must still get its import wrapped (no literal
414
- // appears in this file). A style-less `<View/>` with none is left
415
- // alone so it never pays for an unused wrapper.
416
- const hasClassName = /classname=/i.test(args.src)
417
- const hasUseCss = USE_CSS_CALL.test(args.src)
418
- const isForwarder = /\{\s*\.\.\./.test(args.src) && mentionsWrapModule(args.src)
419
- if (!hasClassName && !hasUseCss && !isForwarder) return false
393
+ // EVERY user source file is scanned — exactly like Tailwind walks its
394
+ // whole content set. The compiler is the only filter (see rewriteSource);
395
+ // no className/helper pre-filter, because that "filter magic" missed
396
+ // classes living in cva/clsx/plain-string files and broke their hot-reload.
397
+ // (Cheap when the file has no classes: oxide finds nothing, no babel parse,
398
+ // source returned untouched.)
420
399
  if (!args.filename.includes('/node_modules/')) return true
421
400
  // node_modules in path → could be a workspace symlink; resolve it.
422
401
  try {
@@ -427,21 +406,6 @@ function isRewriteCandidate(args: BabelTransformerArgs): boolean {
427
406
  }
428
407
  }
429
408
 
430
- /**
431
- * Cheap pre-parse check: does the source import from any configured
432
- * wrap-module? A quoted specifier match is enough — `rewriteWrapImports`
433
- * re-verifies precisely on the AST, so a false positive only costs a
434
- * no-op parse.
435
- * @param source Source text.
436
- * @returns True when a wrap-module specifier appears in the source.
437
- */
438
- function mentionsWrapModule(source: string): boolean {
439
- for (const moduleName of getWrapModules().keys()) {
440
- if (source.includes(`'${moduleName}'`) || source.includes(`"${moduleName}"`)) return true
441
- }
442
- return false
443
- }
444
-
445
409
  /**
446
410
  * Fallback parse when no upstream is configured AND Metro didn't hand
447
411
  * us an AST. Used by unit tests and standalone setups.
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, utimesSync, watch as watchFile } from 'node:fs'
1
+ import { existsSync, mkdirSync, watch as watchFile } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { writeDtsFile } from './dts'
4
4
  import { createRnwindResolver, type ResolveRequestFn } from './resolver'
@@ -15,14 +15,15 @@ const DEFAULT_CACHE_DIR = '.rnwind'
15
15
  let activeCssWatcher: { cssPath: string; close: () => void } | null = null
16
16
 
17
17
  /**
18
- * Watch the theme CSS for edits. On change, rewrite the per-scheme
19
- * files with the fresh theme AND bump `mtime` on every source file
20
- * rnwind has transformed so far Metro's own watcher sees those
21
- * mtime changes, invalidates the modules, and re-transforms them
22
- * against the new CSS on the next request. `getCacheKey()` alone is
23
- * NOT enough: Metro samples the cache key once per worker lifetime,
24
- * so edits during an already-running dev server don't propagate
25
- * without this explicit nudge.
18
+ * Watch the theme CSS for edits. On change, rebuild state against the fresh
19
+ * CSS and rewrite the per-scheme files (`onThemeChange` full rescan
20
+ * `writeSchemes`). That's the entire HMR signal: every transformed source
21
+ * imports `rnwind/__generated/schemes`, which eager-imports
22
+ * `common.style.js`; the rewrite changes its bytes, Metro's content-SHA1
23
+ * dedup notices, and every importer is invalidated + re-bundled with the new
24
+ * style values. The rewritten JSX references atoms by NAME (theme-independent),
25
+ * so no source-file re-transform / mtime nudge is needed — the dep graph
26
+ * carries the signal.
26
27
  * @param cssPath Absolute path to the theme CSS to watch.
27
28
  * @param projectRoot Metro's project root (for `getRnwindState`).
28
29
  */
@@ -40,7 +41,6 @@ function watchThemeCss(cssPath: string, projectRoot: string): void {
40
41
  pending = false
41
42
  try {
42
43
  await onThemeChange(projectRoot)
43
- touchRecordedFiles(projectRoot)
44
44
  } catch {
45
45
  // Invalidation is best-effort — never crash the dev server.
46
46
  }
@@ -49,25 +49,6 @@ function watchThemeCss(cssPath: string, projectRoot: string): void {
49
49
  activeCssWatcher = { cssPath, close: () => watcher.close() }
50
50
  }
51
51
 
52
- /**
53
- * Bump the mtime on every file the builder has transformed. Metro's
54
- * file watcher keys on `mtime`, so this is what makes it invalidate
55
- * those modules and re-transform them.
56
- * @param projectRoot Metro's project root.
57
- */
58
- function touchRecordedFiles(projectRoot: string): void {
59
- const state = getRnwindState(projectRoot)
60
- const files = state.builder.recordedFiles()
61
- const now = new Date()
62
- for (const file of files) {
63
- try {
64
- if (existsSync(file)) utimesSync(file, now, now)
65
- } catch {
66
- // One file's stat failure shouldn't stop the others.
67
- }
68
- }
69
- }
70
-
71
52
  /**
72
53
  * Where the rnwind babel transformer lives — resolved relative to this
73
54
  * module so the path works from both the `src/` tree (tests) and the
@@ -1,5 +1,6 @@
1
1
  import type { ThemeTable } from '../../core/types'
2
2
  import { useRnwind } from '../components/rnwind-provider'
3
+ import { getThemeTokens } from '../lookup-css'
3
4
 
4
5
  /**
5
6
  * Synthetic scheme name applied when tokens aren't declared under any
@@ -20,10 +21,13 @@ const BASE_SCHEME = 'base'
20
21
  */
21
22
  export function useTheme(): ThemeTable {
22
23
  const { scheme, tables } = useRnwind()
23
- const base = tables[BASE_SCHEME] ?? {}
24
- const schemeTable = tables[scheme]
25
- if (!schemeTable) return base
26
- // Fast path: nothing to merge when the scheme table is empty.
24
+ // The build registers token tables on the manifest so `useColor` works out
25
+ // of the box; an explicit `tables` prop layers on top (the prop wins).
26
+ const registered = getThemeTokens()
27
+ const base = { ...registered[BASE_SCHEME], ...tables[BASE_SCHEME] }
28
+ const schemeTable = { ...registered[scheme], ...tables[scheme] }
29
+ // Base tokens apply everywhere (CSS `:root` cascade); the active scheme's
30
+ // own entries override on overlap.
27
31
  if (Object.keys(schemeTable).length === 0) return base
28
32
  return { ...base, ...schemeTable }
29
33
  }
@@ -42,22 +46,25 @@ export function useToken(cssVariable: string): string | number | undefined {
42
46
 
43
47
  /**
44
48
  * Read a color token by shorthand name — `useColor('primary')` resolves
45
- * `--color-primary` for the active scheme.
46
- * @param name Token suffix after `--color-`.
49
+ * `--color-primary` for the active scheme. A fully-qualified name
50
+ * (`--color-primary`) is accepted as-is, so the call doesn't silently miss by
51
+ * double-prefixing into `--color---color-primary`.
52
+ * @param name Token suffix after `--color-`, or the full `--color-*` name.
47
53
  * @returns Resolved color string, or undefined when the token is missing
48
54
  * or its value isn't a string.
49
55
  */
50
56
  export function useColor(name: string): string | undefined {
51
- const value = useToken(`--color-${name}`)
57
+ const value = useToken(name.startsWith('--') ? name : `--color-${name}`)
52
58
  return typeof value === 'string' ? value : undefined
53
59
  }
54
60
 
55
61
  /**
56
62
  * Read a spacing token by shorthand name — `useSize('4')` resolves
57
- * `--spacing-4` for the active scheme.
58
- * @param name Token suffix after `--spacing-`.
63
+ * `--spacing-4` for the active scheme. A fully-qualified `--spacing-*` name is
64
+ * accepted as-is (no double-prefix miss).
65
+ * @param name Token suffix after `--spacing-`, or the full `--spacing-*` name.
59
66
  * @returns Resolved spacing value, or undefined when the token is missing.
60
67
  */
61
68
  export function useSize(name: string): number | string | undefined {
62
- return useToken(`--spacing-${name}`)
69
+ return useToken(name.startsWith('--') ? name : `--spacing-${name}`)
63
70
  }
@@ -2,6 +2,7 @@ export {
2
2
  lookupCss,
3
3
  registerAtoms,
4
4
  registerBreakpoints,
5
+ registerThemeTokens,
5
6
  registerSchemeLoader,
6
7
  setWindowHeightProvider,
7
8
  getBreakpoints,
@@ -28,4 +29,4 @@ export type { Scheme, RnwindConfig } from './types'
28
29
  * string when integrating with build tooling that may see multiple rnwind
29
30
  * copies (e.g. workspace overrides).
30
31
  */
31
- export const VERSION = '0.0.1' as const
32
+ export const VERSION = '0.0.8' as const
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { RnwindState } from './components/rnwind-provider'
19
+ import type { ThemeTables } from '../core/types'
19
20
 
20
21
  /** Empty sentinel returned when the input is null / undefined / empty. */
21
22
  const EMPTY_STYLES: readonly unknown[] = []
@@ -62,6 +63,8 @@ const cache = {
62
63
  atoms: Object.create(null) as Partial<Record<string, SchemeAtomsRecord>>,
63
64
  breakpoints: Object.create(null) as Partial<Record<string, number>>,
64
65
  breakpointList: [] as readonly BreakpointEntry[],
66
+ /** Per-scheme theme token tables (`--color-*`, `--spacing-*`, …) the manifest registers for `useColor` / `useToken` / `useSize`. */
67
+ themeTokens: {} as ThemeTables,
65
68
  }
66
69
 
67
70
  /**
@@ -280,6 +283,20 @@ function tierFor(windowWidth: number): number {
280
283
  return tier
281
284
  }
282
285
 
286
+ /**
287
+ * Public breakpoint-tier for a width — the count of registered breakpoints
288
+ * whose threshold is reached. Used by the runtime resolver as its width
289
+ * cache dimension: the numeric tier is bounded AND exact, unlike the
290
+ * clamped `activeBreakpoint` NAME (which collapses tier-0 into the smallest
291
+ * breakpoint, so widths straddling that threshold would share a cache key
292
+ * and serve a stale style).
293
+ * @param windowWidth Live window width in px.
294
+ * @returns Tier 0..N.
295
+ */
296
+ export function breakpointTier(windowWidth: number): number {
297
+ return tierFor(windowWidth)
298
+ }
299
+
283
300
  /**
284
301
  * Build the style array for a (hoist, scheme, state, width) tuple.
285
302
  * Walks the atom list, applies the interact-state and breakpoint
@@ -539,6 +556,30 @@ export function getBreakpoints(): readonly BreakpointEntry[] {
539
556
  return cache.breakpointList
540
557
  }
541
558
 
559
+ /**
560
+ * Register the per-scheme theme token tables the manifest module emits at
561
+ * load time — the data source for `useColor` / `useToken` / `useSize`. The
562
+ * build lowers `--color-*` tokens to sRGB before registering them, so these
563
+ * are RN-safe. Replaces the prior tables; bumps `atomVersion` so a theme HMR
564
+ * cycle re-resolves. The build registers tokens here so the hooks work out of
565
+ * the box, without the user manually threading a `tables` prop on the provider.
566
+ * @param tables Scheme name → (token name → value) map.
567
+ */
568
+ export function registerThemeTokens(tables: ThemeTables): void {
569
+ cache.themeTokens = tables
570
+ atomVersion += 1
571
+ }
572
+
573
+ /**
574
+ * The manifest-registered theme token tables. The provider merges these under
575
+ * any explicit `tables` prop (the prop wins), so `useColor` resolves from the
576
+ * build by default.
577
+ * @returns Registered per-scheme token tables.
578
+ */
579
+ export function getThemeTokens(): ThemeTables {
580
+ return cache.themeTokens
581
+ }
582
+
542
583
  /**
543
584
  * Sentinel name returned by {@link activeBreakpointFor} ONLY when no
544
585
  * breakpoints are registered at all (bundle without rnwind-transformed
@@ -621,6 +662,7 @@ export function __resetLookupCssState(): void {
621
662
  for (const key of Object.keys(cache.atoms)) delete cache.atoms[key]
622
663
  for (const key of Object.keys(cache.breakpoints)) delete cache.breakpoints[key]
623
664
  cache.breakpointList = []
665
+ cache.themeTokens = {}
624
666
  windowHeightProvider = null
625
667
  schemeLoader = null
626
668
  WARNED_MISSING_INSETS = false
@@ -1,4 +1,4 @@
1
- import { getStyleVersion, lookupCss, type InteractState } from './lookup-css'
1
+ import { breakpointTier, getStyleVersion, lookupCss, type InteractState } from './lookup-css'
2
2
  import type { RnwindState } from './components/rnwind-provider'
3
3
  import { normalizeClassName } from '../core/normalize-classname'
4
4
  import type { GradientAtomInfo, GradientDirection } from '../core/parser/gradient'
@@ -148,17 +148,19 @@ const stateSignatureCache = new WeakMap<RnwindState, string>()
148
148
 
149
149
  /**
150
150
  * Cache key dimension for the reactive context — everything that can
151
- * change a resolved style. Uses the breakpoint TIER (`activeBreakpoint`),
152
- * NOT the raw `windowWidth`: two widths in the same tier gate `md:*` atoms
153
- * identically, so they resolve the same. This collapses the window axis
154
- * from "every pixel" to ~6 values, bounding the cache on resizable
155
- * surfaces (web / desktop) without changing any result.
151
+ * change a resolved style. Uses the numeric breakpoint TIER (count of
152
+ * thresholds reached) from `breakpointTier(windowWidth)`, NOT the
153
+ * `activeBreakpoint` NAME: the name clamps tier-0 into the smallest
154
+ * breakpoint, so widths straddling that threshold (e.g. 320 vs 700 with
155
+ * `sm=640`) would collide on one cache key and serve a stale style. The
156
+ * tier is exact AND bounded — two widths in the same tier gate every
157
+ * `sm:`/`md:`/… atom identically, so they resolve the same.
156
158
  * @param state Rnwind context.
157
159
  * @returns Compact signature string.
158
160
  */
159
161
  function stateSignature(state: RnwindState): string {
160
162
  const { insets } = state
161
- return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${state.activeBreakpoint}`
163
+ return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${breakpointTier(state.windowWidth)}`
162
164
  }
163
165
 
164
166
  /**