rnwind 0.0.10 → 0.0.12

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 (98) hide show
  1. package/lib/cjs/core/normalize-classname.cjs +3 -1
  2. package/lib/cjs/core/normalize-classname.cjs.map +1 -1
  3. package/lib/cjs/core/parser/border-dispatcher.cjs +20 -10
  4. package/lib/cjs/core/parser/border-dispatcher.cjs.map +1 -1
  5. package/lib/cjs/core/parser/color-properties-dispatcher.cjs +7 -5
  6. package/lib/cjs/core/parser/color-properties-dispatcher.cjs.map +1 -1
  7. package/lib/cjs/core/parser/color.cjs +194 -10
  8. package/lib/cjs/core/parser/color.cjs.map +1 -1
  9. package/lib/cjs/core/parser/color.d.ts +18 -3
  10. package/lib/cjs/core/parser/declaration.cjs +62 -4
  11. package/lib/cjs/core/parser/declaration.cjs.map +1 -1
  12. package/lib/cjs/core/parser/layout-dispatcher.cjs +32 -2
  13. package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -1
  14. package/lib/cjs/core/parser/shorthand.cjs +10 -3
  15. package/lib/cjs/core/parser/shorthand.cjs.map +1 -1
  16. package/lib/cjs/core/parser/tokens.cjs +9 -0
  17. package/lib/cjs/core/parser/tokens.cjs.map +1 -1
  18. package/lib/cjs/core/parser/tw-parser.cjs +89 -2
  19. package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
  20. package/lib/cjs/core/parser/tw-parser.d.ts +2 -0
  21. package/lib/cjs/core/parser/typography-dispatcher.cjs +15 -8
  22. package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -1
  23. package/lib/cjs/core/style-builder/union-builder.cjs +81 -2
  24. package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
  25. package/lib/cjs/core/style-builder/union-builder.d.ts +28 -0
  26. package/lib/cjs/metro/state.cjs +74 -13
  27. package/lib/cjs/metro/state.cjs.map +1 -1
  28. package/lib/cjs/metro/state.d.ts +18 -0
  29. package/lib/cjs/metro/transformer.cjs +10 -4
  30. package/lib/cjs/metro/transformer.cjs.map +1 -1
  31. package/lib/cjs/metro/with-config.cjs +57 -0
  32. package/lib/cjs/metro/with-config.cjs.map +1 -1
  33. package/lib/cjs/metro/with-config.d.ts +12 -0
  34. package/lib/cjs/metro/wrap-imports.cjs +36 -1
  35. package/lib/cjs/metro/wrap-imports.cjs.map +1 -1
  36. package/lib/cjs/runtime/hooks/use-scheme.cjs +14 -7
  37. package/lib/cjs/runtime/hooks/use-scheme.cjs.map +1 -1
  38. package/lib/cjs/runtime/resolve.cjs +6 -2
  39. package/lib/cjs/runtime/resolve.cjs.map +1 -1
  40. package/lib/cjs/runtime/resolve.d.ts +5 -1
  41. package/lib/esm/core/normalize-classname.mjs +3 -1
  42. package/lib/esm/core/normalize-classname.mjs.map +1 -1
  43. package/lib/esm/core/parser/border-dispatcher.mjs +21 -11
  44. package/lib/esm/core/parser/border-dispatcher.mjs.map +1 -1
  45. package/lib/esm/core/parser/color-properties-dispatcher.mjs +8 -6
  46. package/lib/esm/core/parser/color-properties-dispatcher.mjs.map +1 -1
  47. package/lib/esm/core/parser/color.d.ts +18 -3
  48. package/lib/esm/core/parser/color.mjs +195 -12
  49. package/lib/esm/core/parser/color.mjs.map +1 -1
  50. package/lib/esm/core/parser/declaration.mjs +63 -5
  51. package/lib/esm/core/parser/declaration.mjs.map +1 -1
  52. package/lib/esm/core/parser/layout-dispatcher.mjs +32 -2
  53. package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
  54. package/lib/esm/core/parser/shorthand.mjs +11 -4
  55. package/lib/esm/core/parser/shorthand.mjs.map +1 -1
  56. package/lib/esm/core/parser/tokens.mjs +10 -1
  57. package/lib/esm/core/parser/tokens.mjs.map +1 -1
  58. package/lib/esm/core/parser/tw-parser.d.ts +2 -0
  59. package/lib/esm/core/parser/tw-parser.mjs +69 -0
  60. package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
  61. package/lib/esm/core/parser/typography-dispatcher.mjs +15 -8
  62. package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
  63. package/lib/esm/core/style-builder/union-builder.d.ts +28 -0
  64. package/lib/esm/core/style-builder/union-builder.mjs +82 -3
  65. package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
  66. package/lib/esm/metro/state.d.ts +18 -0
  67. package/lib/esm/metro/state.mjs +75 -14
  68. package/lib/esm/metro/state.mjs.map +1 -1
  69. package/lib/esm/metro/transformer.mjs +10 -4
  70. package/lib/esm/metro/transformer.mjs.map +1 -1
  71. package/lib/esm/metro/with-config.d.ts +12 -0
  72. package/lib/esm/metro/with-config.mjs +58 -2
  73. package/lib/esm/metro/with-config.mjs.map +1 -1
  74. package/lib/esm/metro/wrap-imports.mjs +36 -1
  75. package/lib/esm/metro/wrap-imports.mjs.map +1 -1
  76. package/lib/esm/runtime/hooks/use-scheme.mjs +14 -7
  77. package/lib/esm/runtime/hooks/use-scheme.mjs.map +1 -1
  78. package/lib/esm/runtime/resolve.d.ts +5 -1
  79. package/lib/esm/runtime/resolve.mjs +6 -2
  80. package/lib/esm/runtime/resolve.mjs.map +1 -1
  81. package/package.json +1 -1
  82. package/src/core/normalize-classname.ts +4 -1
  83. package/src/core/parser/border-dispatcher.ts +22 -11
  84. package/src/core/parser/color-properties-dispatcher.ts +7 -5
  85. package/src/core/parser/color.ts +182 -11
  86. package/src/core/parser/declaration.ts +61 -5
  87. package/src/core/parser/layout-dispatcher.ts +34 -2
  88. package/src/core/parser/shorthand.ts +9 -3
  89. package/src/core/parser/tokens.ts +10 -1
  90. package/src/core/parser/tw-parser.ts +71 -1
  91. package/src/core/parser/typography-dispatcher.ts +15 -6
  92. package/src/core/style-builder/union-builder.ts +83 -3
  93. package/src/metro/state.ts +117 -12
  94. package/src/metro/transformer.ts +9 -4
  95. package/src/metro/with-config.ts +59 -1
  96. package/src/metro/wrap-imports.ts +36 -1
  97. package/src/runtime/hooks/use-scheme.ts +13 -6
  98. package/src/runtime/resolve.ts +6 -2
@@ -3,20 +3,27 @@ import { firstConcreteFontFamily } from './tokens.mjs';
3
3
 
4
4
  /** RN-supported `textDecorationStyle` values (`wavy` has no RN equivalent). */
5
5
  const RN_DECORATION_STYLES = new Set(['solid', 'double', 'dotted', 'dashed']);
6
+ /**
7
+ * The only `textDecorationLine` keywords React Native renders. CSS `overline`
8
+ * has no RN analog, so any line string containing it (or any other unknown
9
+ * keyword) is dropped rather than leaked as a value RN warns on + ignores.
10
+ */
11
+ const RN_DECORATION_LINES = new Set(['none', 'underline', 'line-through', 'underline line-through']);
6
12
  /**
7
13
  * Build the RN `textDecorationLine` entry — string identity for the
8
- * single-line cases, joined-string for the array shape.
14
+ * single-line cases, joined-string for the array shape. Drops any value
15
+ * outside RN's enum (`overline`, `overline underline`, …) so no invalid
16
+ * keyword reaches the StyleSheet.
9
17
  * @param value Typed text-decoration-line.
10
- * @returns Single-entry list with `textDecorationLine`.
18
+ * @returns Single-entry list with a valid `textDecorationLine`, or empty.
11
19
  */
12
20
  function textDecorationLineToEntries(value) {
13
- if (value === 'none')
14
- return [['textDecorationLine', 'none']];
15
21
  if (typeof value === 'string')
16
- return [['textDecorationLine', value]];
17
- if (Array.isArray(value))
18
- return [['textDecorationLine', value.join(' ')]];
19
- return [];
22
+ return RN_DECORATION_LINES.has(value) ? [['textDecorationLine', value]] : [];
23
+ if (!Array.isArray(value))
24
+ return [];
25
+ const line = value.join(' ');
26
+ return RN_DECORATION_LINES.has(line) ? [['textDecorationLine', line]] : [];
20
27
  }
21
28
  /**
22
29
  * Build the RN `aspectRatio` entry from lightningcss's typed value.
@@ -1 +1 @@
1
- {"version":3,"file":"typography-dispatcher.mjs","sources":["../../../../../src/core/parser/typography-dispatcher.ts"],"sourcesContent":["import type { Declaration as LcDeclaration } from 'lightningcss'\nimport { lineHeightToEntries } from './typography'\nimport { firstConcreteFontFamily } from './tokens'\nimport type { RNEntry } from './types'\n\n/** RN-supported `textDecorationStyle` values (`wavy` has no RN equivalent). */\nconst RN_DECORATION_STYLES: ReadonlySet<string> = new Set(['solid', 'double', 'dotted', 'dashed'])\n\n/**\n * Build the RN `textDecorationLine` entry — string identity for the\n * single-line cases, joined-string for the array shape.\n * @param value Typed text-decoration-line.\n * @returns Single-entry list with `textDecorationLine`.\n */\nfunction textDecorationLineToEntries(value: LcDeclaration['value']): readonly RNEntry[] {\n if (value === 'none') return [['textDecorationLine', 'none']]\n if (typeof value === 'string') return [['textDecorationLine', value]]\n if (Array.isArray(value)) return [['textDecorationLine', value.join(' ')]]\n return []\n}\n\n/**\n * Build the RN `aspectRatio` entry from lightningcss's typed value.\n * Drops `auto` (no RN equivalent).\n * @param value Typed aspect-ratio value.\n * @param value.auto Whether the value resolved to `auto`.\n * @param value.ratio Numeric `[width, height]` ratio (or null/undefined).\n * @returns Single-entry list or empty.\n */\nfunction aspectRatioToEntries(value: { auto?: boolean; ratio?: readonly [number, number] | null }): readonly RNEntry[] {\n if (value.auto) return []\n if (!value.ratio) return []\n const [w, h] = value.ratio\n if (h === 0) return []\n return [['aspectRatio', w / h]]\n}\n\n/**\n * Build the RN `letterSpacing` entry. RN expects pixel numbers; rem\n * lengths are scaled to px (16-px base).\n * @param value Typed letter-spacing value.\n * @returns Single-entry list or empty.\n */\nfunction letterSpacingToEntries(value: LcDeclaration['value']): readonly RNEntry[] {\n if (typeof value !== 'object') return []\n const tagged = value as { type?: string; value?: { type?: string; value?: { unit?: string; value?: number } } }\n if (tagged.type === 'normal') return [['letterSpacing', 0]]\n const inner = tagged.value\n if (inner?.type !== 'value' || !inner.value) return []\n const { unit, value: px } = inner.value\n if (typeof px !== 'number') return []\n const resolved = unit === 'px' ? px : px * 16\n // Round off lightningcss f32 noise (`0.1em` → `1.600000023841858`).\n return [['letterSpacing', Math.round(resolved * 10_000) / 10_000]]\n}\n\n/**\n * Lower a CSS `text-align` keyword to one RN's `textAlign` accepts. RN\n * has no logical `start`/`end`, so map them to physical sides (LTR\n * default); every other keyword (left/right/center/justify/auto) is\n * already valid and passes through.\n * @param align CSS text-align keyword.\n * @returns RN-valid textAlign keyword.\n */\nfunction physicalTextAlign(align: string): string {\n if (align === 'start') return 'left'\n if (align === 'end') return 'right'\n return align\n}\n\n/**\n * Dispatch typography declarations rnwind cares about (text-align,\n * text-transform, text-decoration-line, line-height, letter-spacing,\n * aspect-ratio). Returns null when the property isn't one of these so\n * the caller can fall through to its main switch.\n * @param decl One lightningcss declaration.\n * @returns RN entries when the property matched, else `null`.\n */\nexport function dispatchTypographyDeclaration(decl: LcDeclaration): readonly RNEntry[] | null {\n switch (decl.property) {\n case 'text-align': {\n return [['textAlign', physicalTextAlign(String(decl.value))]]\n }\n case 'text-transform': {\n return [['textTransform', decl.value.case ?? 'none']]\n }\n case 'text-decoration-line': {\n return textDecorationLineToEntries(decl.value)\n }\n case 'text-decoration-style': {\n // RN <Text> supports textDecorationStyle (solid/double/dotted/dashed).\n const style = String(decl.value)\n return RN_DECORATION_STYLES.has(style) ? [['textDecorationStyle', style]] : []\n }\n case 'font-family': {\n // Typed `font-family` is a fallback LIST (`font-sans`, `font-mono`,\n // `font-[Inter]`). RN takes one concrete typeface; an all-generic\n // stack (default `font-sans`) emits nothing → system font. The themed\n // `var(--font-*)` path goes through `coerceFontFamily` in declaration.ts.\n const family = firstConcreteFontFamily(decl.value as readonly unknown[])\n return family === undefined ? [] : [['fontFamily', family]]\n }\n case 'aspect-ratio': {\n return aspectRatioToEntries(decl.value)\n }\n case 'line-height': {\n return lineHeightToEntries(decl.value)\n }\n case 'letter-spacing': {\n return letterSpacingToEntries(decl.value)\n }\n default: {\n return null\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"typography-dispatcher.mjs","sources":["../../../../../src/core/parser/typography-dispatcher.ts"],"sourcesContent":["import type { Declaration as LcDeclaration } from 'lightningcss'\nimport { lineHeightToEntries } from './typography'\nimport { firstConcreteFontFamily } from './tokens'\nimport type { RNEntry } from './types'\n\n/** RN-supported `textDecorationStyle` values (`wavy` has no RN equivalent). */\nconst RN_DECORATION_STYLES: ReadonlySet<string> = new Set(['solid', 'double', 'dotted', 'dashed'])\n\n/**\n * The only `textDecorationLine` keywords React Native renders. CSS `overline`\n * has no RN analog, so any line string containing it (or any other unknown\n * keyword) is dropped rather than leaked as a value RN warns on + ignores.\n */\nconst RN_DECORATION_LINES: ReadonlySet<string> = new Set(['none', 'underline', 'line-through', 'underline line-through'])\n\n/**\n * Build the RN `textDecorationLine` entry — string identity for the\n * single-line cases, joined-string for the array shape. Drops any value\n * outside RN's enum (`overline`, `overline underline`, …) so no invalid\n * keyword reaches the StyleSheet.\n * @param value Typed text-decoration-line.\n * @returns Single-entry list with a valid `textDecorationLine`, or empty.\n */\nfunction textDecorationLineToEntries(value: LcDeclaration['value']): readonly RNEntry[] {\n if (typeof value === 'string') return RN_DECORATION_LINES.has(value) ? [['textDecorationLine', value]] : []\n if (!Array.isArray(value)) return []\n const line = value.join(' ')\n return RN_DECORATION_LINES.has(line) ? [['textDecorationLine', line]] : []\n}\n\n/**\n * Build the RN `aspectRatio` entry from lightningcss's typed value.\n * Drops `auto` (no RN equivalent).\n * @param value Typed aspect-ratio value.\n * @param value.auto Whether the value resolved to `auto`.\n * @param value.ratio Numeric `[width, height]` ratio (or null/undefined).\n * @returns Single-entry list or empty.\n */\nfunction aspectRatioToEntries(value: { auto?: boolean; ratio?: readonly [number, number] | null }): readonly RNEntry[] {\n if (value.auto) return []\n if (!value.ratio) return []\n const [w, h] = value.ratio\n if (h === 0) return []\n return [['aspectRatio', w / h]]\n}\n\n/**\n * Build the RN `letterSpacing` entry. RN expects pixel numbers; rem\n * lengths are scaled to px (16-px base).\n * @param value Typed letter-spacing value.\n * @returns Single-entry list or empty.\n */\nfunction letterSpacingToEntries(value: LcDeclaration['value']): readonly RNEntry[] {\n if (typeof value !== 'object') return []\n const tagged = value as { type?: string; value?: { type?: string; value?: { unit?: string; value?: number } } }\n if (tagged.type === 'normal') return [['letterSpacing', 0]]\n const inner = tagged.value\n if (inner?.type !== 'value' || !inner.value) return []\n const { unit, value: px } = inner.value\n if (typeof px !== 'number') return []\n const resolved = unit === 'px' ? px : px * 16\n // Round off lightningcss f32 noise (`0.1em` → `1.600000023841858`).\n return [['letterSpacing', Math.round(resolved * 10_000) / 10_000]]\n}\n\n/**\n * Lower a CSS `text-align` keyword to one RN's `textAlign` accepts. RN\n * has no logical `start`/`end`, so map them to physical sides (LTR\n * default); every other keyword (left/right/center/justify/auto) is\n * already valid and passes through.\n * @param align CSS text-align keyword.\n * @returns RN-valid textAlign keyword.\n */\nfunction physicalTextAlign(align: string): string {\n if (align === 'start') return 'left'\n if (align === 'end') return 'right'\n return align\n}\n\n/**\n * Dispatch typography declarations rnwind cares about (text-align,\n * text-transform, text-decoration-line, line-height, letter-spacing,\n * aspect-ratio). Returns null when the property isn't one of these so\n * the caller can fall through to its main switch.\n * @param decl One lightningcss declaration.\n * @returns RN entries when the property matched, else `null`.\n */\nexport function dispatchTypographyDeclaration(decl: LcDeclaration): readonly RNEntry[] | null {\n switch (decl.property) {\n case 'text-align': {\n return [['textAlign', physicalTextAlign(String(decl.value))]]\n }\n case 'text-transform': {\n return [['textTransform', decl.value.case ?? 'none']]\n }\n case 'text-decoration-line': {\n return textDecorationLineToEntries(decl.value)\n }\n case 'text-decoration-style': {\n // RN <Text> supports textDecorationStyle (solid/double/dotted/dashed).\n const style = String(decl.value)\n return RN_DECORATION_STYLES.has(style) ? [['textDecorationStyle', style]] : []\n }\n case 'font-family': {\n // Typed `font-family` is a fallback LIST (`font-sans`, `font-mono`,\n // `font-[Inter]`). RN takes one concrete typeface; an all-generic\n // stack (default `font-sans`) emits nothing → system font. The themed\n // `var(--font-*)` path goes through `coerceFontFamily` in declaration.ts.\n const family = firstConcreteFontFamily(decl.value as readonly unknown[])\n return family === undefined ? [] : [['fontFamily', family]]\n }\n case 'aspect-ratio': {\n return aspectRatioToEntries(decl.value)\n }\n case 'line-height': {\n return lineHeightToEntries(decl.value)\n }\n case 'letter-spacing': {\n return letterSpacingToEntries(decl.value)\n }\n default: {\n return null\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -74,6 +74,12 @@ declare class UnionBuilder {
74
74
  get manifestPath(): string;
75
75
  /** Cumulative cache-miss count — exposed for tests to assert cache behaviour. */
76
76
  get serializedMisses(): number;
77
+ /**
78
+ * Snapshot of the scheme keys currently tracked in `schemeSignatures` —
79
+ * exposed for tests to assert orphan-signature cleanup.
80
+ * @returns Scheme signature keys (includes the `__manifest` sentinel).
81
+ */
82
+ schemeSignatureKeys(): readonly string[];
77
83
  /**
78
84
  * Absolute path of one scheme's style file.
79
85
  * @param scheme Registry key.
@@ -129,6 +135,28 @@ declare class UnionBuilder {
129
135
  writeSchemes(): Promise<{
130
136
  changedSchemes: readonly string[];
131
137
  }>;
138
+ /**
139
+ * Whether the current write for one scheme can be skipped. A skip is
140
+ * safe only when the cached signature matches AND the bytes on disk
141
+ * still equal the expected source — an `existsSync` pass alone would
142
+ * keep a truncated or externally-modified file (corrupt content with a
143
+ * stale-but-matching signature). The byte read happens only on a
144
+ * signature match, so the common no-change path stays cheap.
145
+ * @param scheme Scheme registry key.
146
+ * @param signature Signature of the source about to be written.
147
+ * @param target Absolute path of the scheme file.
148
+ * @param source Expected file content.
149
+ * @returns Whether writing this scheme can be skipped.
150
+ */
151
+ private canSkipWrite;
152
+ /**
153
+ * Delete `<scheme>.style.js` files left behind by a scheme that's no
154
+ * longer part of the build (removed `@variant`, theme swap), and drop
155
+ * their cached signatures so a later re-introduction rewrites cleanly.
156
+ * The `common` file and the manifest are never touched.
157
+ * @param liveSchemes Scheme names the current build wrote.
158
+ */
159
+ private reapOrphanedSchemes;
132
160
  /**
133
161
  * Ensure the manifest + common scheme files exist on disk so Metro's
134
162
  * resolver can SHA1 them at boot before the first transform runs.
@@ -1,10 +1,14 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { mkdirSync, existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
2
+ import { mkdirSync, existsSync, readFileSync, rmSync, writeFileSync, renameSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { buildSchemeSources } from './build-style.mjs';
5
5
 
6
6
  /** Manifest module basename — the file SchemeProvider imports via the resolver. */
7
7
  const MANIFEST_BASENAME = 'schemes.js';
8
+ /** Suffix every per-scheme style file carries on disk. */
9
+ const SCHEME_FILE_SUFFIX = '.style.js';
10
+ /** Registry key for the always-loaded fallback scheme — never reaped. */
11
+ const COMMON_SCHEME = 'common';
8
12
  /**
9
13
  * Atomic file write — stage to a `.tmp.<pid>.<nonce>` sibling, then
10
14
  * `rename()` into place. Skips the write entirely when the existing
@@ -66,7 +70,36 @@ function setsEqual(a, b) {
66
70
  * @returns Absolute path, e.g. `<cacheDir>/dark.style.js`.
67
71
  */
68
72
  function schemeFilePath(cacheDir, scheme) {
69
- return path.join(cacheDir, `${scheme}.style.js`);
73
+ return path.join(cacheDir, `${scheme}${SCHEME_FILE_SUFFIX}`);
74
+ }
75
+ /**
76
+ * List scheme names whose `<scheme>.style.js` exists on disk but is NOT in
77
+ * the set the current build emits — orphans left by a removed variant
78
+ * (e.g. user drops `@variant dark`, or a theme swap via git pull). The
79
+ * always-loaded `common` scheme is never an orphan. The manifest
80
+ * (`schemes.js`) doesn't carry the `.style.js` suffix, so it's skipped.
81
+ * @param cacheDir Absolute cache directory.
82
+ * @param liveSchemes Scheme names the current build writes.
83
+ * @returns Orphaned scheme names safe to delete.
84
+ */
85
+ function findOrphanedSchemes(cacheDir, liveSchemes) {
86
+ let names;
87
+ try {
88
+ names = readdirSync(cacheDir);
89
+ }
90
+ catch {
91
+ return [];
92
+ }
93
+ const orphans = [];
94
+ for (const name of names) {
95
+ if (!name.endsWith(SCHEME_FILE_SUFFIX))
96
+ continue;
97
+ const scheme = name.slice(0, -SCHEME_FILE_SUFFIX.length);
98
+ if (scheme === COMMON_SCHEME || liveSchemes.has(scheme))
99
+ continue;
100
+ orphans.push(scheme);
101
+ }
102
+ return orphans;
70
103
  }
71
104
  /**
72
105
  * In-memory atom union + per-scheme style-file emitter.
@@ -151,6 +184,14 @@ class UnionBuilder {
151
184
  get serializedMisses() {
152
185
  return this.serializedMissesCount;
153
186
  }
187
+ /**
188
+ * Snapshot of the scheme keys currently tracked in `schemeSignatures` —
189
+ * exposed for tests to assert orphan-signature cleanup.
190
+ * @returns Scheme signature keys (includes the `__manifest` sentinel).
191
+ */
192
+ schemeSignatureKeys() {
193
+ return [...this.schemeSignatures.keys()];
194
+ }
154
195
  /**
155
196
  * Absolute path of one scheme's style file.
156
197
  * @param scheme Registry key.
@@ -281,7 +322,7 @@ class UnionBuilder {
281
322
  for (const [scheme, source] of Object.entries(schemeSources)) {
282
323
  const signature = signatureOf(source);
283
324
  const target = schemeFilePath(this.cacheDir, scheme);
284
- if (this.schemeSignatures.get(scheme) === signature && existsSync(target))
325
+ if (this.canSkipWrite(scheme, signature, target, source))
285
326
  continue;
286
327
  if (writeIfChanged(target, source))
287
328
  changed.push(scheme);
@@ -294,8 +335,46 @@ class UnionBuilder {
294
335
  changed.push('__manifest');
295
336
  this.schemeSignatures.set('__manifest', manifestSignature);
296
337
  }
338
+ this.reapOrphanedSchemes(new Set(Object.keys(schemeSources)));
297
339
  return { changedSchemes: changed };
298
340
  }
341
+ /**
342
+ * Whether the current write for one scheme can be skipped. A skip is
343
+ * safe only when the cached signature matches AND the bytes on disk
344
+ * still equal the expected source — an `existsSync` pass alone would
345
+ * keep a truncated or externally-modified file (corrupt content with a
346
+ * stale-but-matching signature). The byte read happens only on a
347
+ * signature match, so the common no-change path stays cheap.
348
+ * @param scheme Scheme registry key.
349
+ * @param signature Signature of the source about to be written.
350
+ * @param target Absolute path of the scheme file.
351
+ * @param source Expected file content.
352
+ * @returns Whether writing this scheme can be skipped.
353
+ */
354
+ canSkipWrite(scheme, signature, target, source) {
355
+ if (this.schemeSignatures.get(scheme) !== signature)
356
+ return false;
357
+ try {
358
+ return readFileSync(target, 'utf8') === source;
359
+ }
360
+ catch {
361
+ // Missing or unreadable on disk — must rewrite.
362
+ return false;
363
+ }
364
+ }
365
+ /**
366
+ * Delete `<scheme>.style.js` files left behind by a scheme that's no
367
+ * longer part of the build (removed `@variant`, theme swap), and drop
368
+ * their cached signatures so a later re-introduction rewrites cleanly.
369
+ * The `common` file and the manifest are never touched.
370
+ * @param liveSchemes Scheme names the current build wrote.
371
+ */
372
+ reapOrphanedSchemes(liveSchemes) {
373
+ for (const scheme of findOrphanedSchemes(this.cacheDir, liveSchemes)) {
374
+ rmSync(schemeFilePath(this.cacheDir, scheme), { force: true });
375
+ this.schemeSignatures.delete(scheme);
376
+ }
377
+ }
299
378
  /**
300
379
  * Ensure the manifest + common scheme files exist on disk so Metro's
301
380
  * resolver can SHA1 them at boot before the first transform runs.
@@ -1 +1 @@
1
- {"version":3,"file":"union-builder.mjs","sources":["../../../../../src/core/style-builder/union-builder.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'\nimport path from 'node:path'\nimport type { GradientAtomInfo, HapticRequest, KeyframeBlock, SchemedStyle, TailwindParser } from '../parser'\nimport type { ThemeTables } from '../types'\nimport { buildSchemeSources, type AtomSerializedCache } from './build-style'\n\n/** Manifest module basename — the file SchemeProvider imports via the resolver. */\nconst MANIFEST_BASENAME = 'schemes.js'\n\n/**\n * Atomic file write — stage to a `.tmp.<pid>.<nonce>` sibling, then\n * `rename()` into place. Skips the write entirely when the existing\n * content matches.\n * @param target Final destination path.\n * @param content Bytes to write.\n * @returns Whether the file was actually rewritten.\n */\nfunction writeIfChanged(target: string, content: string): boolean {\n if (existsSync(target)) {\n try {\n if (readFileSync(target, 'utf8') === content) return false\n } catch {\n // Unreadable — fall through to rewrite.\n }\n }\n mkdirSync(path.dirname(target), { recursive: true })\n const temporary = `${target}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`\n try {\n writeFileSync(temporary, content, 'utf8')\n renameSync(temporary, target)\n return true\n } catch (error) {\n rmSync(temporary, { force: true })\n throw error\n }\n}\n\n/**\n * SHA-256 prefix of a string — cheap signature used to detect whether a\n * per-scheme file's source has changed since the last write.\n * @param text Input text.\n * @returns 16-char hex digest.\n */\nfunction signatureOf(text: string): string {\n return createHash('sha256').update(text).digest('hex').slice(0, 16)\n}\n\n/**\n * Compare two `Set<string>`s for equality — same size + every element\n * of `a` present in `b`.\n * @param a First set.\n * @param b Second set.\n * @returns Whether the two sets contain identical values.\n */\nfunction setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {\n if (a.size !== b.size) return false\n for (const v of a) if (!b.has(v)) return false\n return true\n}\n\n/**\n * Compute the absolute path of a per-scheme style file under the cache dir.\n * @param cacheDir Absolute cache directory.\n * @param scheme Registry key (`'common'` or a variant name).\n * @returns Absolute path, e.g. `<cacheDir>/dark.style.js`.\n */\nfunction schemeFilePath(cacheDir: string, scheme: string): string {\n return path.join(cacheDir, `${scheme}.style.js`)\n}\n\n/**\n * In-memory atom union + per-scheme style-file emitter.\n *\n * Correctness under multi-worker Metro relies on `ensureProjectScanned`:\n * the FIRST `recordFile` / `writeSchemes` call in every worker drives\n * the oxide Scanner across ALL project sources and hydrates the union\n * with the complete set of candidates. Subsequent per-file\n * `recordFile` calls only layer in atoms the scan already knew about,\n * so writes are idempotent — different workers can't clobber each\n * other's scheme files with partial views.\n *\n * Per-file deltas (atom set unchanged → early return) skip\n * serialization entirely. On a theme-CSS change, `getRnwindState`\n * builds a fresh parser + builder; the next call re-runs\n * `ensureProjectScanned` against the new parser, producing scheme\n * files with the new theme values.\n */\nclass UnionBuilder {\n private readonly cacheDir: string\n private readonly parser: TailwindParser\n private readonly unionAtoms = new Map<string, SchemedStyle>()\n private readonly unionKeyframes = new Map<string, KeyframeBlock>()\n /** atom name → gradient role/colour, surfaced into the manifest's `registerGradients`. */\n private readonly unionGradients = new Map<string, GradientAtomInfo>()\n /** atom name → haptic request, surfaced into the manifest's `registerHaptics`. */\n private readonly unionHaptics = new Map<string, HapticRequest>()\n /**\n * Distinct literal className strings seen across all files, pre-merged\n * into per-scheme molecules at write time. Accumulate-only (like\n * `unionAtoms`): orphaned literals just yield unused molecules and get\n * reaped on the next cold start, so no refcount is needed.\n */\n private readonly unionLiterals = new Set<string>()\n /**\n * Responsive breakpoints captured from the parser. Refreshed on every\n * `recordFile` / `ensureProjectScanned` so user-defined\n * `--breakpoint-*` overrides land in the manifest the next time it's\n * written. Identical for every parser call within one parser instance\n * (theme is fixed for the parser's lifetime), so storing the latest\n * snapshot is sufficient.\n */\n private breakpoints: ReadonlyMap<string, number> = new Map()\n /**\n * Per-scheme theme token tables captured from the parser. Refreshed on\n * every `recordFile` / `ensureProjectScanned` so theme-token edits land in\n * the manifest's `registerThemeTokens({...})` — the data source for\n * `useColor` / `useToken` / `useSize`.\n */\n private themeTokens: ThemeTables = {}\n /** file → set of atom names this file currently contributes. */\n private readonly fileAtomSets = new Map<string, Set<string>>()\n /** atom name → how many files currently contribute it (refcount). */\n private readonly atomRefCount = new Map<string, number>()\n /** scheme → last-written source SHA. Skips re-writing unchanged schemes. */\n private readonly schemeSignatures = new Map<string, string>()\n /**\n * Per-atom serialized-value cache — identity-keyed on each atom's\n * SchemedStyle reference. Carried across `writeSchemes` calls so the\n * typical \"user added one className\" FR case re-stringifies ONE atom\n * instead of all 175+. Cleared on `ensureProjectScanned` (full\n * rescan replaces every reference) and individually invalidated for\n * any atom `applyDiff` mutates.\n */\n private readonly serializedCache: AtomSerializedCache = new Map()\n /** Running count of stringify passes (cache misses). Test telemetry. */\n private serializedMissesCount = 0\n /** Set after `ensureProjectScanned` completes. */\n private projectScanned = false\n /** Promise guard so concurrent first-calls await ONE scan. */\n private pendingScan: Promise<void> | null = null\n\n constructor(cacheDir: string, parser: TailwindParser) {\n this.cacheDir = cacheDir\n this.parser = parser\n mkdirSync(this.cacheDir, { recursive: true })\n }\n\n /** Absolute path of the manifest module (`rnwind/__generated/schemes`). */\n public get manifestPath(): string {\n return path.join(this.cacheDir, MANIFEST_BASENAME)\n }\n\n /** Cumulative cache-miss count — exposed for tests to assert cache behaviour. */\n public get serializedMisses(): number {\n return this.serializedMissesCount\n }\n\n /**\n * Absolute path of one scheme's style file.\n * @param scheme Registry key.\n * @returns Absolute path.\n */\n public schemePath(scheme: string): string {\n return schemeFilePath(this.cacheDir, scheme)\n }\n\n /**\n * One-shot oxide scan + compile across every source the parser was\n * configured with. Idempotent — safe to call from any entry point.\n * Concurrent callers share the same in-flight promise.\n */\n public async ensureProjectScanned(): Promise<void> {\n if (this.projectScanned) return\n if (this.pendingScan) return this.pendingScan\n this.pendingScan = (async () => {\n const parsed = await this.parser.parseProject()\n for (const [name, style] of parsed.atoms) this.unionAtoms.set(name, style)\n for (const [name, kf] of parsed.keyframes) this.unionKeyframes.set(name, kf)\n for (const [name, gradient] of parsed.gradientAtoms) this.unionGradients.set(name, gradient)\n for (const [name, haptic] of parsed.hapticAtoms) this.unionHaptics.set(name, haptic)\n this.breakpoints = parsed.breakpoints\n this.themeTokens = parsed.themeTokens\n this.projectScanned = true\n })()\n try {\n await this.pendingScan\n } finally {\n this.pendingScan = null\n }\n }\n\n /**\n * Record one source file's resolved atoms + keyframes. Short-circuits\n * when the file's atom name set hasn't changed — the common case on\n * every Fast Refresh save of a file whose className literals are\n * unchanged.\n * @param file Absolute source file path.\n * @param atoms Per-atom resolved schemed styles from this transform.\n * @param keyframes Keyframe blocks referenced by this file's atoms.\n * @param literals\n * @returns `{ changed: true }` when the union shifted (new atom name,\n * removed atom name, or new keyframe) — the transformer uses this\n * to skip the serializer + `writeSchemes` when nothing changed.\n */\n public async recordFile(\n file: string,\n atoms: ReadonlyMap<string, SchemedStyle>,\n keyframes: ReadonlyMap<string, KeyframeBlock>,\n literals: readonly string[] = [],\n ): Promise<{ changed: boolean }> {\n await this.ensureProjectScanned()\n const literalAdded = this.recordLiterals(literals)\n const newAtomNames = new Set(atoms.keys())\n const previous = this.fileAtomSets.get(file)\n if (previous && setsEqual(previous, newAtomNames)) {\n // Atom set unchanged — skip the unionAtoms update entirely. The\n // project scan already populated them, and re-setting a fresh\n // object ref here would invalidate the per-atom serialization\n // cache on every FR save for no gain (values are identical).\n // Theme edits go through `getRnwindState` → new builder → fresh\n // scan, so stale cache is impossible.\n let keyframeAdded = false\n for (const [name, kf] of keyframes) {\n if (!this.unionKeyframes.has(name)) keyframeAdded = true\n this.unionKeyframes.set(name, kf)\n }\n return { changed: keyframeAdded || literalAdded }\n }\n this.applyDiff(file, newAtomNames, atoms, keyframes)\n return { changed: true }\n }\n\n /**\n * Merge a file's literal classNames into the union. A literal the\n * union hasn't seen flips `changed` so `writeSchemes` re-emits the\n * scheme files with the new molecule.\n * @param literals Distinct literal className strings.\n * @returns Whether any literal was new to the union.\n */\n private recordLiterals(literals: readonly string[]): boolean {\n let added = false\n for (const literal of literals) {\n if (this.unionLiterals.has(literal)) continue\n this.unionLiterals.add(literal)\n added = true\n }\n return added\n }\n\n /**\n * Forget one source file's contribution. Idempotent — repeated calls\n * for a file that's already dropped are no-ops. Does NOT remove the\n * atom from the union when another file (or the project scan) still\n * references it.\n * @param file Absolute source file path.\n */\n public dropFile(file: string): void {\n const previous = this.fileAtomSets.get(file)\n if (!previous) return\n for (const name of previous) {\n const count = (this.atomRefCount.get(name) ?? 0) - 1\n if (count <= 0) this.atomRefCount.delete(name)\n else this.atomRefCount.set(name, count)\n }\n this.fileAtomSets.delete(file)\n }\n\n /**\n * Serialize the union into per-scheme files + manifest, writing only\n * files whose source bytes changed. Called after every `recordFile`\n * from the transformer — and once at Metro startup via\n * `ensureFilesExist` to seed disk from the project scan alone.\n * @returns List of scheme keys whose files were rewritten (empty\n * when the union is byte-identical to the last flush).\n */\n public async writeSchemes(): Promise<{ changedSchemes: readonly string[] }> {\n await this.ensureProjectScanned()\n const sortedAtomNames = [...this.unionAtoms.keys()].toSorted((a, b) => a.localeCompare(b))\n const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints, this.unionGradients, this.unionHaptics, [...this.unionLiterals], this.themeTokens)\n this.serializedMissesCount += result.serializedMisses\n const { schemeSources, manifestSource } = result\n\n const changed: string[] = []\n for (const [scheme, source] of Object.entries(schemeSources)) {\n const signature = signatureOf(source)\n const target = schemeFilePath(this.cacheDir, scheme)\n if (this.schemeSignatures.get(scheme) === signature && existsSync(target)) continue\n if (writeIfChanged(target, source)) changed.push(scheme)\n this.schemeSignatures.set(scheme, signature)\n }\n\n const manifestSignature = signatureOf(manifestSource)\n const manifestTarget = path.join(this.cacheDir, MANIFEST_BASENAME)\n if (this.schemeSignatures.get('__manifest') !== manifestSignature || !existsSync(manifestTarget)) {\n if (writeIfChanged(manifestTarget, manifestSource)) changed.push('__manifest')\n this.schemeSignatures.set('__manifest', manifestSignature)\n }\n\n return { changedSchemes: changed }\n }\n\n /**\n * Ensure the manifest + common scheme files exist on disk so Metro's\n * resolver can SHA1 them at boot before the first transform runs.\n */\n public async ensureFilesExist(): Promise<void> {\n if (existsSync(this.manifestPath) && existsSync(schemeFilePath(this.cacheDir, 'common'))) {\n // Still trigger the scan so the in-memory union is complete; file\n // bytes may already be authoritative from a prior Metro run.\n await this.ensureProjectScanned()\n return\n }\n await this.writeSchemes()\n }\n\n /**\n * Apply one file's atom-name diff to the in-memory refcount + union.\n * @param file Source file path.\n * @param newAtoms New atom-name set for the file.\n * @param resolvedAtoms Fresh parser output — carries the resolved\n * styles for every entry in `newAtoms`.\n * @param newKeyframes Keyframes this file's atoms reference.\n */\n private applyDiff(\n file: string,\n newAtoms: ReadonlySet<string>,\n resolvedAtoms: ReadonlyMap<string, SchemedStyle>,\n newKeyframes: ReadonlyMap<string, KeyframeBlock>,\n ): void {\n const previous = this.fileAtomSets.get(file) ?? new Set<string>()\n for (const name of previous) {\n if (newAtoms.has(name)) continue\n const count = (this.atomRefCount.get(name) ?? 0) - 1\n if (count <= 0) this.atomRefCount.delete(name)\n else this.atomRefCount.set(name, count)\n // Do NOT remove `name` from `unionAtoms` — the project scan still\n // references it (orphaned atoms get reaped on the next Metro\n // cold start when the scanner re-walks disk).\n }\n for (const name of newAtoms) {\n if (!previous.has(name)) this.atomRefCount.set(name, (this.atomRefCount.get(name) ?? 0) + 1)\n // Only install the resolved style when the atom is new to the\n // union. Replacing an existing entry with a fresh parser-\n // produced object would swap the identity guard the per-atom\n // serialization cache uses and force a re-stringify for every\n // atom on every FR save. CSS edits rebuild the whole builder\n // (via `getRnwindState`) so stale values aren't possible.\n if (!this.unionAtoms.has(name)) {\n const style = resolvedAtoms.get(name)\n if (style) this.unionAtoms.set(name, style)\n }\n }\n this.fileAtomSets.set(file, new Set(newAtoms))\n for (const [name, kf] of newKeyframes) this.unionKeyframes.set(name, kf)\n }\n}\n\nexport { UnionBuilder }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"union-builder.mjs","sources":["../../../../../src/core/style-builder/union-builder.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'\nimport path from 'node:path'\nimport type { GradientAtomInfo, HapticRequest, KeyframeBlock, SchemedStyle, TailwindParser } from '../parser'\nimport type { ThemeTables } from '../types'\nimport { buildSchemeSources, type AtomSerializedCache } from './build-style'\n\n/** Manifest module basename — the file SchemeProvider imports via the resolver. */\nconst MANIFEST_BASENAME = 'schemes.js'\n\n/** Suffix every per-scheme style file carries on disk. */\nconst SCHEME_FILE_SUFFIX = '.style.js'\n\n/** Registry key for the always-loaded fallback scheme — never reaped. */\nconst COMMON_SCHEME = 'common'\n\n/**\n * Atomic file write — stage to a `.tmp.<pid>.<nonce>` sibling, then\n * `rename()` into place. Skips the write entirely when the existing\n * content matches.\n * @param target Final destination path.\n * @param content Bytes to write.\n * @returns Whether the file was actually rewritten.\n */\nfunction writeIfChanged(target: string, content: string): boolean {\n if (existsSync(target)) {\n try {\n if (readFileSync(target, 'utf8') === content) return false\n } catch {\n // Unreadable — fall through to rewrite.\n }\n }\n mkdirSync(path.dirname(target), { recursive: true })\n const temporary = `${target}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`\n try {\n writeFileSync(temporary, content, 'utf8')\n renameSync(temporary, target)\n return true\n } catch (error) {\n rmSync(temporary, { force: true })\n throw error\n }\n}\n\n/**\n * SHA-256 prefix of a string — cheap signature used to detect whether a\n * per-scheme file's source has changed since the last write.\n * @param text Input text.\n * @returns 16-char hex digest.\n */\nfunction signatureOf(text: string): string {\n return createHash('sha256').update(text).digest('hex').slice(0, 16)\n}\n\n/**\n * Compare two `Set<string>`s for equality — same size + every element\n * of `a` present in `b`.\n * @param a First set.\n * @param b Second set.\n * @returns Whether the two sets contain identical values.\n */\nfunction setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {\n if (a.size !== b.size) return false\n for (const v of a) if (!b.has(v)) return false\n return true\n}\n\n/**\n * Compute the absolute path of a per-scheme style file under the cache dir.\n * @param cacheDir Absolute cache directory.\n * @param scheme Registry key (`'common'` or a variant name).\n * @returns Absolute path, e.g. `<cacheDir>/dark.style.js`.\n */\nfunction schemeFilePath(cacheDir: string, scheme: string): string {\n return path.join(cacheDir, `${scheme}${SCHEME_FILE_SUFFIX}`)\n}\n\n/**\n * List scheme names whose `<scheme>.style.js` exists on disk but is NOT in\n * the set the current build emits — orphans left by a removed variant\n * (e.g. user drops `@variant dark`, or a theme swap via git pull). The\n * always-loaded `common` scheme is never an orphan. The manifest\n * (`schemes.js`) doesn't carry the `.style.js` suffix, so it's skipped.\n * @param cacheDir Absolute cache directory.\n * @param liveSchemes Scheme names the current build writes.\n * @returns Orphaned scheme names safe to delete.\n */\nfunction findOrphanedSchemes(cacheDir: string, liveSchemes: ReadonlySet<string>): readonly string[] {\n let names: readonly string[]\n try {\n names = readdirSync(cacheDir)\n } catch {\n return []\n }\n const orphans: string[] = []\n for (const name of names) {\n if (!name.endsWith(SCHEME_FILE_SUFFIX)) continue\n const scheme = name.slice(0, -SCHEME_FILE_SUFFIX.length)\n if (scheme === COMMON_SCHEME || liveSchemes.has(scheme)) continue\n orphans.push(scheme)\n }\n return orphans\n}\n\n/**\n * In-memory atom union + per-scheme style-file emitter.\n *\n * Correctness under multi-worker Metro relies on `ensureProjectScanned`:\n * the FIRST `recordFile` / `writeSchemes` call in every worker drives\n * the oxide Scanner across ALL project sources and hydrates the union\n * with the complete set of candidates. Subsequent per-file\n * `recordFile` calls only layer in atoms the scan already knew about,\n * so writes are idempotent — different workers can't clobber each\n * other's scheme files with partial views.\n *\n * Per-file deltas (atom set unchanged → early return) skip\n * serialization entirely. On a theme-CSS change, `getRnwindState`\n * builds a fresh parser + builder; the next call re-runs\n * `ensureProjectScanned` against the new parser, producing scheme\n * files with the new theme values.\n */\nclass UnionBuilder {\n private readonly cacheDir: string\n private readonly parser: TailwindParser\n private readonly unionAtoms = new Map<string, SchemedStyle>()\n private readonly unionKeyframes = new Map<string, KeyframeBlock>()\n /** atom name → gradient role/colour, surfaced into the manifest's `registerGradients`. */\n private readonly unionGradients = new Map<string, GradientAtomInfo>()\n /** atom name → haptic request, surfaced into the manifest's `registerHaptics`. */\n private readonly unionHaptics = new Map<string, HapticRequest>()\n /**\n * Distinct literal className strings seen across all files, pre-merged\n * into per-scheme molecules at write time. Accumulate-only (like\n * `unionAtoms`): orphaned literals just yield unused molecules and get\n * reaped on the next cold start, so no refcount is needed.\n */\n private readonly unionLiterals = new Set<string>()\n /**\n * Responsive breakpoints captured from the parser. Refreshed on every\n * `recordFile` / `ensureProjectScanned` so user-defined\n * `--breakpoint-*` overrides land in the manifest the next time it's\n * written. Identical for every parser call within one parser instance\n * (theme is fixed for the parser's lifetime), so storing the latest\n * snapshot is sufficient.\n */\n private breakpoints: ReadonlyMap<string, number> = new Map()\n /**\n * Per-scheme theme token tables captured from the parser. Refreshed on\n * every `recordFile` / `ensureProjectScanned` so theme-token edits land in\n * the manifest's `registerThemeTokens({...})` — the data source for\n * `useColor` / `useToken` / `useSize`.\n */\n private themeTokens: ThemeTables = {}\n /** file → set of atom names this file currently contributes. */\n private readonly fileAtomSets = new Map<string, Set<string>>()\n /** atom name → how many files currently contribute it (refcount). */\n private readonly atomRefCount = new Map<string, number>()\n /** scheme → last-written source SHA. Skips re-writing unchanged schemes. */\n private readonly schemeSignatures = new Map<string, string>()\n /**\n * Per-atom serialized-value cache — identity-keyed on each atom's\n * SchemedStyle reference. Carried across `writeSchemes` calls so the\n * typical \"user added one className\" FR case re-stringifies ONE atom\n * instead of all 175+. Cleared on `ensureProjectScanned` (full\n * rescan replaces every reference) and individually invalidated for\n * any atom `applyDiff` mutates.\n */\n private readonly serializedCache: AtomSerializedCache = new Map()\n /** Running count of stringify passes (cache misses). Test telemetry. */\n private serializedMissesCount = 0\n /** Set after `ensureProjectScanned` completes. */\n private projectScanned = false\n /** Promise guard so concurrent first-calls await ONE scan. */\n private pendingScan: Promise<void> | null = null\n\n constructor(cacheDir: string, parser: TailwindParser) {\n this.cacheDir = cacheDir\n this.parser = parser\n mkdirSync(this.cacheDir, { recursive: true })\n }\n\n /** Absolute path of the manifest module (`rnwind/__generated/schemes`). */\n public get manifestPath(): string {\n return path.join(this.cacheDir, MANIFEST_BASENAME)\n }\n\n /** Cumulative cache-miss count — exposed for tests to assert cache behaviour. */\n public get serializedMisses(): number {\n return this.serializedMissesCount\n }\n\n /**\n * Snapshot of the scheme keys currently tracked in `schemeSignatures` —\n * exposed for tests to assert orphan-signature cleanup.\n * @returns Scheme signature keys (includes the `__manifest` sentinel).\n */\n public schemeSignatureKeys(): readonly string[] {\n return [...this.schemeSignatures.keys()]\n }\n\n /**\n * Absolute path of one scheme's style file.\n * @param scheme Registry key.\n * @returns Absolute path.\n */\n public schemePath(scheme: string): string {\n return schemeFilePath(this.cacheDir, scheme)\n }\n\n /**\n * One-shot oxide scan + compile across every source the parser was\n * configured with. Idempotent — safe to call from any entry point.\n * Concurrent callers share the same in-flight promise.\n */\n public async ensureProjectScanned(): Promise<void> {\n if (this.projectScanned) return\n if (this.pendingScan) return this.pendingScan\n this.pendingScan = (async () => {\n const parsed = await this.parser.parseProject()\n for (const [name, style] of parsed.atoms) this.unionAtoms.set(name, style)\n for (const [name, kf] of parsed.keyframes) this.unionKeyframes.set(name, kf)\n for (const [name, gradient] of parsed.gradientAtoms) this.unionGradients.set(name, gradient)\n for (const [name, haptic] of parsed.hapticAtoms) this.unionHaptics.set(name, haptic)\n this.breakpoints = parsed.breakpoints\n this.themeTokens = parsed.themeTokens\n this.projectScanned = true\n })()\n try {\n await this.pendingScan\n } finally {\n this.pendingScan = null\n }\n }\n\n /**\n * Record one source file's resolved atoms + keyframes. Short-circuits\n * when the file's atom name set hasn't changed — the common case on\n * every Fast Refresh save of a file whose className literals are\n * unchanged.\n * @param file Absolute source file path.\n * @param atoms Per-atom resolved schemed styles from this transform.\n * @param keyframes Keyframe blocks referenced by this file's atoms.\n * @param literals\n * @returns `{ changed: true }` when the union shifted (new atom name,\n * removed atom name, or new keyframe) — the transformer uses this\n * to skip the serializer + `writeSchemes` when nothing changed.\n */\n public async recordFile(\n file: string,\n atoms: ReadonlyMap<string, SchemedStyle>,\n keyframes: ReadonlyMap<string, KeyframeBlock>,\n literals: readonly string[] = [],\n ): Promise<{ changed: boolean }> {\n await this.ensureProjectScanned()\n const literalAdded = this.recordLiterals(literals)\n const newAtomNames = new Set(atoms.keys())\n const previous = this.fileAtomSets.get(file)\n if (previous && setsEqual(previous, newAtomNames)) {\n // Atom set unchanged — skip the unionAtoms update entirely. The\n // project scan already populated them, and re-setting a fresh\n // object ref here would invalidate the per-atom serialization\n // cache on every FR save for no gain (values are identical).\n // Theme edits go through `getRnwindState` → new builder → fresh\n // scan, so stale cache is impossible.\n let keyframeAdded = false\n for (const [name, kf] of keyframes) {\n if (!this.unionKeyframes.has(name)) keyframeAdded = true\n this.unionKeyframes.set(name, kf)\n }\n return { changed: keyframeAdded || literalAdded }\n }\n this.applyDiff(file, newAtomNames, atoms, keyframes)\n return { changed: true }\n }\n\n /**\n * Merge a file's literal classNames into the union. A literal the\n * union hasn't seen flips `changed` so `writeSchemes` re-emits the\n * scheme files with the new molecule.\n * @param literals Distinct literal className strings.\n * @returns Whether any literal was new to the union.\n */\n private recordLiterals(literals: readonly string[]): boolean {\n let added = false\n for (const literal of literals) {\n if (this.unionLiterals.has(literal)) continue\n this.unionLiterals.add(literal)\n added = true\n }\n return added\n }\n\n /**\n * Forget one source file's contribution. Idempotent — repeated calls\n * for a file that's already dropped are no-ops. Does NOT remove the\n * atom from the union when another file (or the project scan) still\n * references it.\n * @param file Absolute source file path.\n */\n public dropFile(file: string): void {\n const previous = this.fileAtomSets.get(file)\n if (!previous) return\n for (const name of previous) {\n const count = (this.atomRefCount.get(name) ?? 0) - 1\n if (count <= 0) this.atomRefCount.delete(name)\n else this.atomRefCount.set(name, count)\n }\n this.fileAtomSets.delete(file)\n }\n\n /**\n * Serialize the union into per-scheme files + manifest, writing only\n * files whose source bytes changed. Called after every `recordFile`\n * from the transformer — and once at Metro startup via\n * `ensureFilesExist` to seed disk from the project scan alone.\n * @returns List of scheme keys whose files were rewritten (empty\n * when the union is byte-identical to the last flush).\n */\n public async writeSchemes(): Promise<{ changedSchemes: readonly string[] }> {\n await this.ensureProjectScanned()\n const sortedAtomNames = [...this.unionAtoms.keys()].toSorted((a, b) => a.localeCompare(b))\n const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints, this.unionGradients, this.unionHaptics, [...this.unionLiterals], this.themeTokens)\n this.serializedMissesCount += result.serializedMisses\n const { schemeSources, manifestSource } = result\n\n const changed: string[] = []\n for (const [scheme, source] of Object.entries(schemeSources)) {\n const signature = signatureOf(source)\n const target = schemeFilePath(this.cacheDir, scheme)\n if (this.canSkipWrite(scheme, signature, target, source)) continue\n if (writeIfChanged(target, source)) changed.push(scheme)\n this.schemeSignatures.set(scheme, signature)\n }\n\n const manifestSignature = signatureOf(manifestSource)\n const manifestTarget = path.join(this.cacheDir, MANIFEST_BASENAME)\n if (this.schemeSignatures.get('__manifest') !== manifestSignature || !existsSync(manifestTarget)) {\n if (writeIfChanged(manifestTarget, manifestSource)) changed.push('__manifest')\n this.schemeSignatures.set('__manifest', manifestSignature)\n }\n\n this.reapOrphanedSchemes(new Set(Object.keys(schemeSources)))\n return { changedSchemes: changed }\n }\n\n /**\n * Whether the current write for one scheme can be skipped. A skip is\n * safe only when the cached signature matches AND the bytes on disk\n * still equal the expected source — an `existsSync` pass alone would\n * keep a truncated or externally-modified file (corrupt content with a\n * stale-but-matching signature). The byte read happens only on a\n * signature match, so the common no-change path stays cheap.\n * @param scheme Scheme registry key.\n * @param signature Signature of the source about to be written.\n * @param target Absolute path of the scheme file.\n * @param source Expected file content.\n * @returns Whether writing this scheme can be skipped.\n */\n private canSkipWrite(scheme: string, signature: string, target: string, source: string): boolean {\n if (this.schemeSignatures.get(scheme) !== signature) return false\n try {\n return readFileSync(target, 'utf8') === source\n } catch {\n // Missing or unreadable on disk — must rewrite.\n return false\n }\n }\n\n /**\n * Delete `<scheme>.style.js` files left behind by a scheme that's no\n * longer part of the build (removed `@variant`, theme swap), and drop\n * their cached signatures so a later re-introduction rewrites cleanly.\n * The `common` file and the manifest are never touched.\n * @param liveSchemes Scheme names the current build wrote.\n */\n private reapOrphanedSchemes(liveSchemes: ReadonlySet<string>): void {\n for (const scheme of findOrphanedSchemes(this.cacheDir, liveSchemes)) {\n rmSync(schemeFilePath(this.cacheDir, scheme), { force: true })\n this.schemeSignatures.delete(scheme)\n }\n }\n\n /**\n * Ensure the manifest + common scheme files exist on disk so Metro's\n * resolver can SHA1 them at boot before the first transform runs.\n */\n public async ensureFilesExist(): Promise<void> {\n if (existsSync(this.manifestPath) && existsSync(schemeFilePath(this.cacheDir, 'common'))) {\n // Still trigger the scan so the in-memory union is complete; file\n // bytes may already be authoritative from a prior Metro run.\n await this.ensureProjectScanned()\n return\n }\n await this.writeSchemes()\n }\n\n /**\n * Apply one file's atom-name diff to the in-memory refcount + union.\n * @param file Source file path.\n * @param newAtoms New atom-name set for the file.\n * @param resolvedAtoms Fresh parser output — carries the resolved\n * styles for every entry in `newAtoms`.\n * @param newKeyframes Keyframes this file's atoms reference.\n */\n private applyDiff(\n file: string,\n newAtoms: ReadonlySet<string>,\n resolvedAtoms: ReadonlyMap<string, SchemedStyle>,\n newKeyframes: ReadonlyMap<string, KeyframeBlock>,\n ): void {\n const previous = this.fileAtomSets.get(file) ?? new Set<string>()\n for (const name of previous) {\n if (newAtoms.has(name)) continue\n const count = (this.atomRefCount.get(name) ?? 0) - 1\n if (count <= 0) this.atomRefCount.delete(name)\n else this.atomRefCount.set(name, count)\n // Do NOT remove `name` from `unionAtoms` — the project scan still\n // references it (orphaned atoms get reaped on the next Metro\n // cold start when the scanner re-walks disk).\n }\n for (const name of newAtoms) {\n if (!previous.has(name)) this.atomRefCount.set(name, (this.atomRefCount.get(name) ?? 0) + 1)\n // Only install the resolved style when the atom is new to the\n // union. Replacing an existing entry with a fresh parser-\n // produced object would swap the identity guard the per-atom\n // serialization cache uses and force a re-stringify for every\n // atom on every FR save. CSS edits rebuild the whole builder\n // (via `getRnwindState`) so stale values aren't possible.\n if (!this.unionAtoms.has(name)) {\n const style = resolvedAtoms.get(name)\n if (style) this.unionAtoms.set(name, style)\n }\n }\n this.fileAtomSets.set(file, new Set(newAtoms))\n for (const [name, kf] of newKeyframes) this.unionKeyframes.set(name, kf)\n }\n}\n\nexport { UnionBuilder }\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,18 @@
1
1
  import { UnionBuilder } from '../core/style-builder';
2
2
  import { TailwindParser } from '../core/parser';
3
3
  import { buildWrapModules } from './wrap-imports';
4
+ /** Test-only — count of actual disk reads of the theme CSS (memo misses). */
5
+ export declare function __getThemeReadCount(): number;
6
+ /** Test-only — clear the theme memo so the next read hits disk. */
7
+ export declare function __resetThemeMemo(): void;
8
+ /**
9
+ * Test-only override for the memoised library fingerprint. Production reads
10
+ * the value once from disk; tests use this to simulate a library upgrade
11
+ * (fingerprint rotation) without rebuilding the lib. Passing `undefined`
12
+ * clears the override so the next call re-derives from disk.
13
+ * @param value Forced fingerprint, or omit/`undefined` to clear the override.
14
+ */
15
+ export declare function __setLibraryFingerprintForTest(value?: string): void;
4
16
  /**
5
17
  * Worker-local state. Lazy-initialised on first access so files that
6
18
  * bypass the transform don't pay for construction.
@@ -10,6 +22,12 @@ export interface RnwindState {
10
22
  builder: UnionBuilder;
11
23
  themeCss: string;
12
24
  themeHash: string;
25
+ /**
26
+ * Library fingerprint captured when this state was built. Folded into the
27
+ * rebuild guard so an in-place rnwind upgrade (unchanged CSS, rotated
28
+ * fingerprint) drops the stale builder + on-disk scheme format.
29
+ */
30
+ libraryFingerprint: string;
13
31
  projectRoot: string;
14
32
  }
15
33
  /**
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, statSync } 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/union-builder.mjs';
@@ -55,24 +55,68 @@ const WRAP_MODULES_ENV = 'RNWIND_WRAP_MODULES';
55
55
  let libraryFingerprint;
56
56
  /** Live state shared across one Metro transform worker. */
57
57
  let cached = null;
58
+ /**
59
+ * Cached wrap-module map. The env var feeding it only changes on a Metro
60
+ * (re)configure, so rebuilding the 11-entry Map per file transform is pure
61
+ * waste — `configureRnwindState` / `resetRnwindState` clear this so the next
62
+ * call rebuilds from the current env.
63
+ */
64
+ let cachedWrapModules = null;
65
+ /**
66
+ * Process-scoped memo of resolved theme CSS keyed by entry path. Every file
67
+ * transform reads the theme hash (via `getRnwindState` + `getRnwindCacheKey`)
68
+ * and the rebuild path reads the full CSS again — without this memo each is a
69
+ * full disk read + `@import` inline + SHA-256. Keyed on `statSync` mtime so a
70
+ * real edit busts it while unchanged files reuse the cached result. This is
71
+ * NOT a competing persistent cache: it's in-memory, process-scoped, and only
72
+ * defers the redundant re-reads Metro's own caching can't see.
73
+ */
74
+ const themeMemo = new Map();
75
+ /**
76
+ * Load (or reuse) the resolved theme CSS + hash for `cssPath`. Re-reads from
77
+ * disk only when the file's mtime changed since the last read; otherwise the
78
+ * cached entry is returned untouched.
79
+ * @param cssPath Absolute CSS entry path.
80
+ * @returns The memo entry, or `null` when the entry can't be read.
81
+ */
82
+ function loadThemeMemo(cssPath) {
83
+ let stat;
84
+ try {
85
+ stat = statSync(cssPath);
86
+ }
87
+ catch {
88
+ themeMemo.delete(cssPath);
89
+ return null;
90
+ }
91
+ const { mtimeMs } = stat;
92
+ const cachedEntry = themeMemo.get(cssPath);
93
+ if (cachedEntry?.mtimeMs === mtimeMs)
94
+ return cachedEntry;
95
+ const css = resolveThemeCss(cssPath);
96
+ const entry = { mtimeMs, hash: createHash('sha256').update(css).digest('hex').slice(0, 16), css };
97
+ themeMemo.set(cssPath, entry);
98
+ return entry;
99
+ }
58
100
  /**
59
101
  * Cheap content-hash readout. SHA-256 prefix of the FULLY-RESOLVED theme
60
102
  * CSS — `@import`s flattened — so an edit to a theme file the entry only
61
103
  * re-exports (`@import "@acme/ui/theme.css"`) still rotates the hash and
62
104
  * invalidates Metro's cache. Returns `'missing'` when the entry can't be
63
- * read so the cache key stays deterministic.
105
+ * read so the cache key stays deterministic. Memoised by mtime.
64
106
  * @param cssPath Absolute CSS path.
65
107
  * @returns 16-char hex content hash.
66
108
  */
67
109
  function readThemeHashFor(cssPath) {
68
- if (!existsSync(cssPath))
69
- return 'missing';
70
- try {
71
- return createHash('sha256').update(resolveThemeCss(cssPath)).digest('hex').slice(0, 16);
72
- }
73
- catch {
74
- return 'missing';
75
- }
110
+ return loadThemeMemo(cssPath)?.hash ?? 'missing';
111
+ }
112
+ /**
113
+ * Resolved theme CSS for the entry, served from the same mtime memo so the
114
+ * rebuild path doesn't re-read the file the hash readout already loaded.
115
+ * @param cssPath Absolute CSS path.
116
+ * @returns Fully-inlined theme CSS, or `''` when unreadable.
117
+ */
118
+ function readThemeCssFor(cssPath) {
119
+ return loadThemeMemo(cssPath)?.css ?? '';
76
120
  }
77
121
  /**
78
122
  * Hash a small set of rnwind library files whose changes affect the
@@ -151,6 +195,7 @@ function configureRnwindState(cssEntryFile, cacheDir, watchFolders = [], wrapMod
151
195
  process.env[WRAP_MODULES_ENV] = wrapModules.join(',');
152
196
  }
153
197
  cached = null;
198
+ cachedWrapModules = null;
154
199
  }
155
200
  /**
156
201
  * Effective module → wrap-policy map: the built-in defaults merged with
@@ -158,9 +203,12 @@ function configureRnwindState(cssEntryFile, cacheDir, watchFolders = [], wrapMod
158
203
  * @returns Module → policy map the import-rewrite consults.
159
204
  */
160
205
  function getWrapModules() {
206
+ if (cachedWrapModules)
207
+ return cachedWrapModules;
161
208
  const raw = process.env[WRAP_MODULES_ENV];
162
209
  const extra = raw && raw.length > 0 ? raw.split(',').filter((entry) => entry.length > 0) : undefined;
163
- return buildWrapModules(extra);
210
+ cachedWrapModules = buildWrapModules(extra);
211
+ return cachedWrapModules;
164
212
  }
165
213
  /**
166
214
  * Fetch (or build) the worker-local rnwind state. Re-reads the theme
@@ -180,15 +228,24 @@ function getRnwindState(projectRoot) {
180
228
  if (!cacheDir)
181
229
  throw new Error('rnwind: RNWIND_CACHE_DIR is not set — did `withRnwindConfig` run?');
182
230
  const currentHash = readThemeHashFor(cssEntry);
183
- if (cached?.themeHash === currentHash && cached.projectRoot === projectRoot)
231
+ const currentFingerprint = getLibraryFingerprint();
232
+ // Reuse only when the CSS hash, the project root, AND the library
233
+ // fingerprint all match — an in-place rnwind upgrade rotates the
234
+ // fingerprint while the CSS hash is unchanged, and that must still rebuild.
235
+ if (cached?.themeHash === currentHash &&
236
+ cached.libraryFingerprint === currentFingerprint &&
237
+ cached.projectRoot === projectRoot) {
184
238
  return cached;
185
- const themeCss = resolveThemeCss(cssEntry);
239
+ }
240
+ // Served from the same mtime memo `readThemeHashFor` just populated — no
241
+ // second disk read on the rebuild path.
242
+ const themeCss = readThemeCssFor(cssEntry);
186
243
  const parser = new TailwindParser({
187
244
  themeCss,
188
245
  sources: defaultSources(projectRoot, cacheDir, readWatchFolders()),
189
246
  });
190
247
  const builder = new UnionBuilder(cacheDir, parser);
191
- cached = { parser, builder, themeCss, themeHash: currentHash, projectRoot };
248
+ cached = { parser, builder, themeCss, themeHash: currentHash, libraryFingerprint: currentFingerprint, projectRoot };
192
249
  return cached;
193
250
  }
194
251
  /**
@@ -212,6 +269,7 @@ function getRnwindCacheKey() {
212
269
  /** Drop the cached state — call after editing the theme CSS. */
213
270
  function resetRnwindState() {
214
271
  cached = null;
272
+ cachedWrapModules = null;
215
273
  }
216
274
  /**
217
275
  * Drop cached state, rebuild parser/builder with the fresh CSS, rescan
@@ -223,6 +281,9 @@ function resetRnwindState() {
223
281
  * @param projectRoot Absolute project root (from `metroConfig.projectRoot`).
224
282
  */
225
283
  async function onThemeChange(projectRoot) {
284
+ // The watcher already told us the CSS changed; an atomic save can reuse the
285
+ // same mtime, so bust the memo unconditionally rather than trusting stat.
286
+ themeMemo.clear();
226
287
  resetRnwindState();
227
288
  const state = getRnwindState(projectRoot);
228
289
  await state.builder.writeSchemes();