rnwind 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/core/normalize-classname.cjs +25 -0
- package/lib/cjs/core/normalize-classname.cjs.map +1 -0
- package/lib/cjs/core/normalize-classname.d.ts +10 -0
- package/lib/cjs/core/style-builder/build-style.cjs +258 -58
- package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
- package/lib/cjs/core/style-builder/build-style.d.ts +6 -1
- package/lib/cjs/core/style-builder/union-builder.cjs +37 -3
- package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
- package/lib/cjs/core/style-builder/union-builder.d.ts +21 -1
- package/lib/cjs/metro/dts.cjs +7 -16
- package/lib/cjs/metro/dts.cjs.map +1 -1
- package/lib/cjs/metro/dts.d.ts +2 -4
- package/lib/cjs/metro/state.cjs +30 -78
- package/lib/cjs/metro/state.cjs.map +1 -1
- package/lib/cjs/metro/state.d.ts +8 -25
- package/lib/cjs/metro/transformer.cjs +193 -34
- package/lib/cjs/metro/transformer.cjs.map +1 -1
- package/lib/cjs/metro/with-config.cjs +2 -2
- package/lib/cjs/metro/with-config.cjs.map +1 -1
- package/lib/cjs/metro/with-config.d.ts +11 -26
- package/lib/cjs/metro/wrap-imports.cjs +273 -0
- package/lib/cjs/metro/wrap-imports.cjs.map +1 -0
- package/lib/cjs/metro/wrap-imports.d.ts +26 -0
- package/lib/cjs/runtime/components/rnwind-provider.cjs +0 -17
- package/lib/cjs/runtime/components/rnwind-provider.cjs.map +1 -1
- package/lib/cjs/runtime/components/rnwind-provider.d.ts +0 -14
- package/lib/cjs/runtime/hooks/use-css.cjs +16 -10
- package/lib/cjs/runtime/hooks/use-css.cjs.map +1 -1
- package/lib/cjs/runtime/hooks/use-css.d.ts +15 -9
- package/lib/cjs/runtime/index.cjs +11 -13
- package/lib/cjs/runtime/index.cjs.map +1 -1
- package/lib/cjs/runtime/index.d.ts +4 -9
- package/lib/cjs/runtime/lookup-css.cjs +10 -0
- package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
- package/lib/cjs/runtime/lookup-css.d.ts +7 -0
- package/lib/cjs/runtime/resolve.cjs +348 -0
- package/lib/cjs/runtime/resolve.cjs.map +1 -0
- package/lib/cjs/runtime/resolve.d.ts +61 -0
- package/lib/cjs/runtime/wrap.cjs +254 -0
- package/lib/cjs/runtime/wrap.cjs.map +1 -0
- package/lib/cjs/runtime/wrap.d.ts +37 -0
- package/lib/cjs/testing/index.cjs +81 -50
- package/lib/cjs/testing/index.cjs.map +1 -1
- package/lib/esm/core/normalize-classname.d.ts +10 -0
- package/lib/esm/core/normalize-classname.mjs +23 -0
- package/lib/esm/core/normalize-classname.mjs.map +1 -0
- package/lib/esm/core/style-builder/build-style.d.ts +6 -1
- package/lib/esm/core/style-builder/build-style.mjs +258 -58
- package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
- package/lib/esm/core/style-builder/union-builder.d.ts +21 -1
- package/lib/esm/core/style-builder/union-builder.mjs +37 -3
- package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
- package/lib/esm/metro/dts.d.ts +2 -4
- package/lib/esm/metro/dts.mjs +7 -16
- package/lib/esm/metro/dts.mjs.map +1 -1
- package/lib/esm/metro/state.d.ts +8 -25
- package/lib/esm/metro/state.mjs +30 -76
- package/lib/esm/metro/state.mjs.map +1 -1
- package/lib/esm/metro/transformer.mjs +194 -35
- package/lib/esm/metro/transformer.mjs.map +1 -1
- package/lib/esm/metro/with-config.d.ts +11 -26
- package/lib/esm/metro/with-config.mjs +2 -2
- package/lib/esm/metro/with-config.mjs.map +1 -1
- package/lib/esm/metro/wrap-imports.d.ts +26 -0
- package/lib/esm/metro/wrap-imports.mjs +250 -0
- package/lib/esm/metro/wrap-imports.mjs.map +1 -0
- package/lib/esm/runtime/components/rnwind-provider.d.ts +0 -14
- package/lib/esm/runtime/components/rnwind-provider.mjs +1 -17
- package/lib/esm/runtime/components/rnwind-provider.mjs.map +1 -1
- package/lib/esm/runtime/hooks/use-css.d.ts +15 -9
- package/lib/esm/runtime/hooks/use-css.mjs +16 -10
- package/lib/esm/runtime/hooks/use-css.mjs.map +1 -1
- package/lib/esm/runtime/index.d.ts +4 -9
- package/lib/esm/runtime/index.mjs +4 -4
- package/lib/esm/runtime/index.mjs.map +1 -1
- package/lib/esm/runtime/lookup-css.d.ts +7 -0
- package/lib/esm/runtime/lookup-css.mjs +10 -1
- package/lib/esm/runtime/lookup-css.mjs.map +1 -1
- package/lib/esm/runtime/resolve.d.ts +61 -0
- package/lib/esm/runtime/resolve.mjs +341 -0
- package/lib/esm/runtime/resolve.mjs.map +1 -0
- package/lib/esm/runtime/wrap.d.ts +37 -0
- package/lib/esm/runtime/wrap.mjs +251 -0
- package/lib/esm/runtime/wrap.mjs.map +1 -0
- package/lib/esm/testing/index.mjs +84 -53
- package/lib/esm/testing/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/core/normalize-classname.ts +19 -0
- package/src/core/style-builder/build-style.ts +286 -55
- package/src/core/style-builder/union-builder.ts +36 -3
- package/src/metro/dts.ts +7 -19
- package/src/metro/state.ts +29 -74
- package/src/metro/transformer.ts +190 -34
- package/src/metro/with-config.ts +13 -28
- package/src/metro/wrap-imports.ts +260 -0
- package/src/runtime/components/rnwind-provider.tsx +0 -17
- package/src/runtime/hooks/use-css.ts +17 -11
- package/src/runtime/index.ts +3 -26
- package/src/runtime/lookup-css.ts +10 -0
- package/src/runtime/resolve.ts +381 -0
- package/src/runtime/wrap.tsx +267 -0
- package/src/testing/index.ts +106 -56
- package/lib/cjs/core/parser/text-truncate.cjs +0 -78
- package/lib/cjs/core/parser/text-truncate.cjs.map +0 -1
- package/lib/cjs/metro/transform-ast.cjs +0 -1472
- package/lib/cjs/metro/transform-ast.cjs.map +0 -1
- package/lib/cjs/metro/transform-ast.d.ts +0 -88
- package/lib/cjs/runtime/haptics.cjs +0 -113
- package/lib/cjs/runtime/haptics.cjs.map +0 -1
- package/lib/cjs/runtime/haptics.d.ts +0 -48
- package/lib/cjs/runtime/interactive-box.cjs +0 -35
- package/lib/cjs/runtime/interactive-box.cjs.map +0 -1
- package/lib/cjs/runtime/interactive-box.d.ts +0 -40
- package/lib/esm/core/parser/text-truncate.mjs +0 -75
- package/lib/esm/core/parser/text-truncate.mjs.map +0 -1
- package/lib/esm/metro/transform-ast.d.ts +0 -88
- package/lib/esm/metro/transform-ast.mjs +0 -1451
- package/lib/esm/metro/transform-ast.mjs.map +0 -1
- package/lib/esm/runtime/haptics.d.ts +0 -48
- package/lib/esm/runtime/haptics.mjs +0 -110
- package/lib/esm/runtime/haptics.mjs.map +0 -1
- package/lib/esm/runtime/interactive-box.d.ts +0 -40
- package/lib/esm/runtime/interactive-box.mjs +0 -33
- package/lib/esm/runtime/interactive-box.mjs.map +0 -1
- package/src/metro/transform-ast.ts +0 -1729
- package/src/runtime/haptics.ts +0 -120
- package/src/runtime/interactive-box.tsx +0 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rnwind",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Tailwind for React Native",
|
|
5
5
|
"author": "https://github.com/sagltd",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
],
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "rollup -c rollup.config.mjs",
|
|
59
|
+
"bench": "bun run bench/resolve.bench.ts",
|
|
59
60
|
"test": "bun test ./__tests__",
|
|
60
61
|
"test:coverage": "bun test ./__tests__ --coverage",
|
|
61
62
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a className for molecule keying: trim, collapse runs of
|
|
3
|
+
* whitespace, and drop exact-duplicate tokens — but PRESERVE ORDER.
|
|
4
|
+
* Tailwind is last-wins for conflicting utilities (`p-4 p-2` ≠ `p-2 p-4`),
|
|
5
|
+
* so sorting would corrupt the merge. Build-time (molecule keys) and
|
|
6
|
+
* runtime (lookup) call the identical function so their keys always match.
|
|
7
|
+
* @param className Raw className string.
|
|
8
|
+
* @returns Normalized, order-preserving className.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeClassName(className: string): string {
|
|
11
|
+
const seen = new Set<string>()
|
|
12
|
+
const out: string[] = []
|
|
13
|
+
for (const token of className.trim().split(/\s+/)) {
|
|
14
|
+
if (token.length === 0 || seen.has(token)) continue
|
|
15
|
+
seen.add(token)
|
|
16
|
+
out.push(token)
|
|
17
|
+
}
|
|
18
|
+
return out.join(' ')
|
|
19
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KeyframeBlock, RNStyle, SchemedStyle } from '../parser'
|
|
2
|
+
import { normalizeClassName } from '../normalize-classname'
|
|
2
3
|
|
|
3
4
|
/** Match atom names like `border-hairline`, `h-hairline`, `border-t-hairline`, etc. */
|
|
4
5
|
const HAIRLINE_ATOM = /-hairline$/
|
|
@@ -179,61 +180,96 @@ function prepareAtomValue(atomName: string, style: RNStyle, keyframes: ReadonlyM
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
/**
|
|
182
|
-
*
|
|
183
|
-
* value
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
183
|
+
* Decide which serialized atom values get hoisted to a shared `const`.
|
|
184
|
+
* A value is hoisted ONLY when ≥2 atoms share it — then one
|
|
185
|
+
* `const _s<N> = <value>` saves the repeated bytes AND gives those atoms
|
|
186
|
+
* one shared object (reference identity). A value used once is inlined
|
|
187
|
+
* directly at its atom (`"-m-2": {"margin":-8}`) — hoisting a singleton
|
|
188
|
+
* would only add bytes. First-seen order keeps the const indices stable
|
|
189
|
+
* across workers.
|
|
190
|
+
* @param entries `[atomName, serializedValue]` pairs (atom-sorted).
|
|
191
|
+
* @returns `{ constFor }` value→const-name map + `decls` source lines.
|
|
189
192
|
*/
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
193
|
+
function planValueConsts(entries: readonly (readonly [string, string])[]): {
|
|
194
|
+
constFor: ReadonlyMap<string, string>
|
|
195
|
+
decls: readonly string[]
|
|
196
|
+
} {
|
|
197
|
+
const counts = new Map<string, number>()
|
|
198
|
+
for (const [, value] of entries) counts.set(value, (counts.get(value) ?? 0) + 1)
|
|
199
|
+
const constFor = new Map<string, string>()
|
|
200
|
+
const decls: string[] = []
|
|
201
|
+
for (const [value, count] of counts) {
|
|
202
|
+
if (count < 2) continue
|
|
203
|
+
const name = `_s${decls.length}`
|
|
204
|
+
constFor.set(value, name)
|
|
205
|
+
decls.push(`const ${name} = ${value}`)
|
|
201
206
|
}
|
|
207
|
+
return { constFor, decls }
|
|
208
|
+
}
|
|
202
209
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Serialize a scheme's molecule map into a `registerMolecules(...)` object
|
|
212
|
+
* literal, sorted by className for byte-deterministic output.
|
|
213
|
+
* @param molecules normalized className → pre-merged style object.
|
|
214
|
+
* @returns Object-literal source (`null` when empty).
|
|
215
|
+
*/
|
|
216
|
+
function serializeMolecules(molecules: Record<string, RNStyle> | undefined): string | null {
|
|
217
|
+
if (!molecules) return null
|
|
218
|
+
const keys = Object.keys(molecules).toSorted((a, b) => a.localeCompare(b))
|
|
219
|
+
if (keys.length === 0) return null
|
|
220
|
+
const body = keys.map((cn) => ` ${JSON.stringify(cn)}: ${JSON.stringify(molecules[cn])},`)
|
|
221
|
+
return ['{', ...body, '}'].join('\n')
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
/**
|
|
209
225
|
* Render one scheme file's source. `entries` is the list of atoms this
|
|
210
226
|
* scheme contributes — for `common` every atom's canonical value; for
|
|
211
227
|
* a variant only atoms whose value differs from canonical. Hairline
|
|
212
|
-
* atoms in this file trigger the `StyleSheet` import.
|
|
228
|
+
* atoms in this file trigger the `StyleSheet` import. Pre-merged
|
|
229
|
+
* molecules (when present) are registered alongside the atoms so the
|
|
230
|
+
* runtime resolver's molecule-first path is populated.
|
|
213
231
|
* @param schemeName Registry key (`'common'` or the variant name).
|
|
214
232
|
* @param entries `[atomName, serializedValue]` pairs to emit.
|
|
233
|
+
* @param molecules Pre-merged className → style map for this scheme.
|
|
215
234
|
* @returns JS source text.
|
|
216
235
|
*/
|
|
217
|
-
function renderSchemeFile(
|
|
236
|
+
function renderSchemeFile(
|
|
237
|
+
schemeName: string,
|
|
238
|
+
entries: readonly (readonly [string, string])[],
|
|
239
|
+
molecules?: Record<string, RNStyle>,
|
|
240
|
+
): string {
|
|
218
241
|
const needsStyleSheet = entries.some(([atom]) => isHairlineAtom(atom))
|
|
219
|
-
const
|
|
220
|
-
const recordLines
|
|
221
|
-
|
|
222
|
-
const ref = deduper.intern(value)
|
|
223
|
-
recordLines.push(` ${JSON.stringify(atom)}: ${ref},`)
|
|
224
|
-
}
|
|
242
|
+
const { constFor, decls } = planValueConsts(entries)
|
|
243
|
+
const recordLines = entries.map(([atom, value]) => ` ${JSON.stringify(atom)}: ${constFor.get(value) ?? value},`)
|
|
244
|
+
const moleculeLiteral = serializeMolecules(molecules)
|
|
225
245
|
|
|
246
|
+
const imports = ['registerAtoms']
|
|
247
|
+
if (moleculeLiteral) imports.push('registerMolecules')
|
|
226
248
|
const lines: string[] = []
|
|
227
249
|
if (needsStyleSheet) lines.push(`import { StyleSheet } from 'react-native'`)
|
|
228
|
-
lines.push(`import {
|
|
229
|
-
if (
|
|
230
|
-
for (const decl of
|
|
250
|
+
lines.push(`import { ${imports.join(', ')} } from 'rnwind'`, ``)
|
|
251
|
+
if (decls.length > 0) {
|
|
252
|
+
for (const decl of decls) lines.push(decl)
|
|
231
253
|
lines.push(``)
|
|
232
254
|
}
|
|
233
255
|
lines.push(`registerAtoms(${JSON.stringify(schemeName)}, {`, ...recordLines, `})`, ``)
|
|
256
|
+
if (moleculeLiteral) lines.push(`registerMolecules(${JSON.stringify(schemeName)}, ${moleculeLiteral})`, ``)
|
|
234
257
|
return lines.join('\n')
|
|
235
258
|
}
|
|
236
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Serialize a feature map (atom name → JSON-able value: gradient info or
|
|
262
|
+
* haptic request) into a stable JS object literal for the manifest.
|
|
263
|
+
* Sorted by key so the output is byte-deterministic across workers.
|
|
264
|
+
* @param map Atom name → feature value.
|
|
265
|
+
* @returns Object-literal source.
|
|
266
|
+
*/
|
|
267
|
+
function serializeFeatureMap(map: ReadonlyMap<string, unknown>): string {
|
|
268
|
+
const entries = [...map.entries()].toSorted((a, b) => a[0].localeCompare(b[0]))
|
|
269
|
+
const body = entries.map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`).join(', ')
|
|
270
|
+
return `{ ${body} }`
|
|
271
|
+
}
|
|
272
|
+
|
|
237
273
|
/**
|
|
238
274
|
* Render the JS-object literal for the responsive-breakpoint table the
|
|
239
275
|
* runtime registers at manifest-load time. Sorted by ascending px
|
|
@@ -250,35 +286,215 @@ function serializeBreakpoints(breakpoints: ReadonlyMap<string, number>): string
|
|
|
250
286
|
}
|
|
251
287
|
|
|
252
288
|
/**
|
|
253
|
-
* Render the manifest module.
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
289
|
+
* Render the manifest module. EAGER-imports `common.style.js` AND every
|
|
290
|
+
* variant scheme file so every scheme's atoms register the moment the
|
|
291
|
+
* manifest evaluates — no lazy `require`. Lazy loading raced the cold
|
|
292
|
+
* start: `RnwindProvider` calls `loadScheme(scheme)` on its first render,
|
|
293
|
+
* but on a cold boot the manifest (hence `registerSchemeLoader`) may not
|
|
294
|
+
* have evaluated yet, so that call no-ops and the active variant's atoms
|
|
295
|
+
* never load — scheme-dependent styles fall back to `common` (the light
|
|
296
|
+
* default) until a reload. Eager imports remove the race entirely; the
|
|
297
|
+
* variant files are small diffs, so the upfront cost is negligible.
|
|
298
|
+
* `ensureSchemeLoaded` stays exported as a no-op for API compatibility.
|
|
260
299
|
* @param variants Variant scheme names (no `base`, no `common`).
|
|
261
300
|
* @param breakpoints Responsive breakpoint name → px-threshold map.
|
|
301
|
+
* @param gradients Atom → gradient info for `registerGradients`.
|
|
302
|
+
* @param haptics Atom → haptic request for `registerHaptics`.
|
|
262
303
|
* @returns JS source text.
|
|
263
304
|
*/
|
|
264
|
-
function renderManifest(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
305
|
+
function renderManifest(
|
|
306
|
+
variants: readonly string[],
|
|
307
|
+
breakpoints: ReadonlyMap<string, number>,
|
|
308
|
+
gradients: ReadonlyMap<string, unknown>,
|
|
309
|
+
haptics: ReadonlyMap<string, unknown>,
|
|
310
|
+
): string {
|
|
311
|
+
const imports = ['registerSchemeLoader', 'registerBreakpoints']
|
|
312
|
+
if (gradients.size > 0) imports.push('registerGradients')
|
|
313
|
+
if (haptics.size > 0) imports.push('registerHaptics')
|
|
314
|
+
const lines: string[] = [`import { ${imports.join(', ')} } from 'rnwind'`, `import './common.style'`]
|
|
315
|
+
for (const variant of variants) lines.push(`import ${JSON.stringify(`./${variant}.style`)}`)
|
|
316
|
+
lines.push(``, `registerBreakpoints(${serializeBreakpoints(breakpoints)})`)
|
|
317
|
+
if (gradients.size > 0) lines.push(`registerGradients(${serializeFeatureMap(gradients)})`)
|
|
318
|
+
if (haptics.size > 0) lines.push(`registerHaptics(${serializeFeatureMap(haptics)})`)
|
|
319
|
+
lines.push(
|
|
320
|
+
``,
|
|
321
|
+
`function ensureSchemeLoaded(_name) {}`,
|
|
268
322
|
``,
|
|
269
|
-
`
|
|
323
|
+
`registerSchemeLoader(ensureSchemeLoaded)`,
|
|
270
324
|
``,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
325
|
+
`export { ensureSchemeLoaded }`,
|
|
326
|
+
``,
|
|
327
|
+
)
|
|
328
|
+
return lines.join('\n')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Whether a resolved style carries a nested safe-area marker — molecules
|
|
333
|
+
* can't pre-bake these because the inset value is per-render.
|
|
334
|
+
* @param style Raw resolved RN style (pre-envelope).
|
|
335
|
+
* @returns True when any value is a `{__safe: ...}` marker.
|
|
336
|
+
*/
|
|
337
|
+
function hasSafeMarker(style: RNStyle): boolean {
|
|
338
|
+
for (const key of Object.keys(style)) {
|
|
339
|
+
const value = style[key]
|
|
340
|
+
if (typeof value !== 'object' || !value) continue
|
|
341
|
+
if ('__safe' in value) return true
|
|
275
342
|
}
|
|
276
|
-
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Whether a resolved style has font-scale-sensitive props. Molecules
|
|
348
|
+
* can't pre-bake these because `fontSize`/`lineHeight` scale per-render
|
|
349
|
+
* with `useWindowDimensions().fontScale`.
|
|
350
|
+
* @param style Resolved RN style.
|
|
351
|
+
* @returns True when `fontSize` or `lineHeight` is present.
|
|
352
|
+
*/
|
|
353
|
+
function hasFontScaleProperty(style: RNStyle): boolean {
|
|
354
|
+
return 'fontSize' in style || 'lineHeight' in style
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Whether a token is a feature-only utility (gradient stop/direction,
|
|
359
|
+
* haptic, or text-truncate) that contributes NO RN `style` — the runtime
|
|
360
|
+
* resolver folds these in via `attachFeatures`, so they don't disqualify
|
|
361
|
+
* a molecule, they just merge nothing.
|
|
362
|
+
* @param token Atom name.
|
|
363
|
+
* @param gradients Gradient feature map.
|
|
364
|
+
* @param haptics Haptic feature map.
|
|
365
|
+
* @returns True when the token is a non-style feature.
|
|
366
|
+
*/
|
|
367
|
+
function isFeatureToken(token: string, gradients: ReadonlyMap<string, unknown>, haptics: ReadonlyMap<string, unknown>): boolean {
|
|
368
|
+
if (gradients.has(token) || haptics.has(token)) return true
|
|
369
|
+
return token === 'truncate' || token === 'text-ellipsis' || token === 'text-clip' || token.startsWith('line-clamp-')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve one atom's value under a scheme: the scheme's own non-empty
|
|
374
|
+
* bucket, falling back to canonical. `common` always reads canonical.
|
|
375
|
+
* @param schemed Parser-produced per-scheme bucket.
|
|
376
|
+
* @param scheme Scheme key (`'common'` or a variant name).
|
|
377
|
+
* @returns The atom's RN style for that scheme, or undefined.
|
|
378
|
+
*/
|
|
379
|
+
function schemeValueOf(schemed: SchemedStyle, scheme: string): RNStyle | undefined {
|
|
380
|
+
if (scheme === COMMON_SCHEME) return canonicalValue(schemed)
|
|
381
|
+
const own = (schemed as Readonly<Record<string, RNStyle>>)[scheme]
|
|
382
|
+
return isNonEmptyStyle(own) ? own : canonicalValue(schemed)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Pre-merge a normalized className's atoms into ONE RN style object for a
|
|
387
|
+
* scheme, or null when the className is NOT molecule-eligible. A
|
|
388
|
+
* className is eligible only when every token is context-independent:
|
|
389
|
+
* - no variant prefix (`active:` / `focus:` / `md:` / `dark:` — anything
|
|
390
|
+
* with a `:`), so scheme/state/breakpoint gating never applies,
|
|
391
|
+
* - no `*-hairline`, `*-safe`, or font-scale (`fontSize`/`lineHeight`)
|
|
392
|
+
* atom, whose value is resolved per-render.
|
|
393
|
+
* Feature-only tokens (gradient / haptic / truncate) are skipped, not
|
|
394
|
+
* disqualifying — the runtime folds them in via `attachFeatures`. Unknown
|
|
395
|
+
* tokens disqualify so the atom path still surfaces the dev warning.
|
|
396
|
+
* @param tokens Normalized className tokens (order preserved).
|
|
397
|
+
* @param scheme Scheme key to resolve each atom under.
|
|
398
|
+
* @param resolved Per-atom schemed styles.
|
|
399
|
+
* @param keyframes Keyframes to inline into `animationName`.
|
|
400
|
+
* @param gradients Gradient feature map.
|
|
401
|
+
* @param haptics Haptic feature map.
|
|
402
|
+
* @returns Merged style object, or null when not eligible.
|
|
403
|
+
*/
|
|
404
|
+
function mergeMolecule(
|
|
405
|
+
tokens: readonly string[],
|
|
406
|
+
scheme: string,
|
|
407
|
+
resolved: ReadonlyMap<string, SchemedStyle>,
|
|
408
|
+
keyframes: ReadonlyMap<string, KeyframeBlock>,
|
|
409
|
+
gradients: ReadonlyMap<string, unknown>,
|
|
410
|
+
haptics: ReadonlyMap<string, unknown>,
|
|
411
|
+
): RNStyle | null {
|
|
412
|
+
const merged: RNStyle = {}
|
|
413
|
+
for (const token of tokens) {
|
|
414
|
+
if (token.includes(':')) return null
|
|
415
|
+
if (isFeatureToken(token, gradients, haptics)) continue
|
|
416
|
+
if (isHairlineAtom(token)) return null
|
|
417
|
+
const schemed = resolved.get(token)
|
|
418
|
+
if (!schemed) return null
|
|
419
|
+
const raw = schemeValueOf(schemed, scheme)
|
|
420
|
+
if (!raw) continue
|
|
421
|
+
if (hasSafeMarker(raw) || hasFontScaleProperty(raw)) return null
|
|
422
|
+
Object.assign(merged, inlineAnimationName(raw, keyframes))
|
|
423
|
+
}
|
|
424
|
+
return merged
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Emit each variant's molecule for one className — but only when the
|
|
429
|
+
* variant's merge DIFFERS from common (runtime falls back to common).
|
|
430
|
+
* @param normalized Normalized className key.
|
|
431
|
+
* @param tokens Normalized className tokens.
|
|
432
|
+
* @param commonText Serialized common-scheme merge for the diff check.
|
|
433
|
+
* @param variants Variant scheme names.
|
|
434
|
+
* @param variantMaps Mutable per-variant molecule collectors.
|
|
435
|
+
* @param resolved Per-atom schemed styles.
|
|
436
|
+
* @param keyframes Keyframes to inline.
|
|
437
|
+
* @param gradients Gradient feature map.
|
|
438
|
+
* @param haptics Haptic feature map.
|
|
439
|
+
*/
|
|
440
|
+
function addVariantMolecules(
|
|
441
|
+
normalized: string,
|
|
442
|
+
tokens: readonly string[],
|
|
443
|
+
commonText: string,
|
|
444
|
+
variants: readonly string[],
|
|
445
|
+
variantMaps: Record<string, Record<string, RNStyle>>,
|
|
446
|
+
resolved: ReadonlyMap<string, SchemedStyle>,
|
|
447
|
+
keyframes: ReadonlyMap<string, KeyframeBlock>,
|
|
448
|
+
gradients: ReadonlyMap<string, unknown>,
|
|
449
|
+
haptics: ReadonlyMap<string, unknown>,
|
|
450
|
+
): void {
|
|
277
451
|
for (const variant of variants) {
|
|
278
|
-
|
|
452
|
+
const variantMerged = mergeMolecule(tokens, variant, resolved, keyframes, gradients, haptics)
|
|
453
|
+
if (variantMerged === null) continue
|
|
454
|
+
if (JSON.stringify(variantMerged) !== commonText) variantMaps[variant][normalized] = variantMerged
|
|
279
455
|
}
|
|
280
|
-
|
|
281
|
-
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Build per-scheme molecules for every literal className the project
|
|
460
|
+
* uses. Each eligible className gets a pre-merged style object under
|
|
461
|
+
* `common`; a variant only carries an entry when its merge DIFFERS from
|
|
462
|
+
* common (runtime falls back `molecules[scheme] ?? molecules.common`).
|
|
463
|
+
* @param literals Distinct literal className strings (raw).
|
|
464
|
+
* @param resolved Per-atom schemed styles.
|
|
465
|
+
* @param keyframes Keyframes to inline.
|
|
466
|
+
* @param variants Variant scheme names.
|
|
467
|
+
* @param gradients Gradient feature map.
|
|
468
|
+
* @param haptics Haptic feature map.
|
|
469
|
+
* @returns scheme → (normalized className → merged style).
|
|
470
|
+
*/
|
|
471
|
+
function buildMolecules(
|
|
472
|
+
literals: readonly string[],
|
|
473
|
+
resolved: ReadonlyMap<string, SchemedStyle>,
|
|
474
|
+
keyframes: ReadonlyMap<string, KeyframeBlock>,
|
|
475
|
+
variants: readonly string[],
|
|
476
|
+
gradients: ReadonlyMap<string, unknown>,
|
|
477
|
+
haptics: ReadonlyMap<string, unknown>,
|
|
478
|
+
): Record<string, Record<string, RNStyle>> {
|
|
479
|
+
const common: Record<string, RNStyle> = {}
|
|
480
|
+
const variantMaps: Record<string, Record<string, RNStyle>> = {}
|
|
481
|
+
for (const variant of variants) variantMaps[variant] = {}
|
|
482
|
+
|
|
483
|
+
for (const literal of literals) {
|
|
484
|
+
const normalized = normalizeClassName(literal)
|
|
485
|
+
if (normalized.length === 0) continue
|
|
486
|
+
const tokens = normalized.split(' ')
|
|
487
|
+
const commonMerged = mergeMolecule(tokens, COMMON_SCHEME, resolved, keyframes, gradients, haptics)
|
|
488
|
+
if (commonMerged === null) continue
|
|
489
|
+
common[normalized] = commonMerged
|
|
490
|
+
addVariantMolecules(normalized, tokens, JSON.stringify(commonMerged), variants, variantMaps, resolved, keyframes, gradients, haptics)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const out: Record<string, Record<string, RNStyle>> = { [COMMON_SCHEME]: common }
|
|
494
|
+
for (const variant of variants) {
|
|
495
|
+
if (Object.keys(variantMaps[variant]).length > 0) out[variant] = variantMaps[variant]
|
|
496
|
+
}
|
|
497
|
+
return out
|
|
282
498
|
}
|
|
283
499
|
|
|
284
500
|
/** Output of one build pass — one source per scheme plus the manifest. */
|
|
@@ -458,6 +674,11 @@ const EMPTY_BREAKPOINTS: ReadonlyMap<string, number> = new Map()
|
|
|
458
674
|
* manifest emits `registerBreakpoints({...})` so the runtime can gate
|
|
459
675
|
* `md:*` / `lg:*` atoms on `windowWidth`. Optional — empty when the
|
|
460
676
|
* theme declares no breakpoints (legacy/test callers).
|
|
677
|
+
* @param gradients Gradient feature map (atom → role/colour) for the manifest + molecule eligibility.
|
|
678
|
+
* @param haptics Haptic feature map (atom → request) for the manifest + molecule eligibility.
|
|
679
|
+
* @param literals Distinct literal className strings — pre-merged into
|
|
680
|
+
* per-scheme molecules so the runtime resolver's O(1) molecule-first
|
|
681
|
+
* path is populated. Empty for legacy/test callers (atom path only).
|
|
461
682
|
* @returns Per-scheme sources, manifest source, variant list.
|
|
462
683
|
*/
|
|
463
684
|
export function buildSchemeSources(
|
|
@@ -466,6 +687,9 @@ export function buildSchemeSources(
|
|
|
466
687
|
keyframes: ReadonlyMap<string, KeyframeBlock>,
|
|
467
688
|
cache?: AtomSerializedCache,
|
|
468
689
|
breakpoints: ReadonlyMap<string, number> = EMPTY_BREAKPOINTS,
|
|
690
|
+
gradients: ReadonlyMap<string, unknown> = EMPTY_FEATURE_MAP,
|
|
691
|
+
haptics: ReadonlyMap<string, unknown> = EMPTY_FEATURE_MAP,
|
|
692
|
+
literals: readonly string[] = EMPTY_LITERALS,
|
|
469
693
|
): BuildSchemeSourcesOutput {
|
|
470
694
|
const variants = collectVariantSchemes(resolved)
|
|
471
695
|
const commonEntries: (readonly [string, string])[] = []
|
|
@@ -481,20 +705,27 @@ export function buildSchemeSources(
|
|
|
481
705
|
misses += collectAtomEntries(atom, schemed, canonical, variants, keyframes, commonEntries, variantEntries, cache)
|
|
482
706
|
}
|
|
483
707
|
|
|
708
|
+
const molecules = buildMolecules(literals, resolved, keyframes, variants, gradients, haptics)
|
|
484
709
|
const schemeSources: Record<string, string> = {
|
|
485
|
-
[COMMON_SCHEME]: renderSchemeFile(COMMON_SCHEME, commonEntries),
|
|
710
|
+
[COMMON_SCHEME]: renderSchemeFile(COMMON_SCHEME, commonEntries, molecules[COMMON_SCHEME]),
|
|
486
711
|
}
|
|
487
712
|
for (const variant of variants) {
|
|
488
|
-
schemeSources[variant] = renderSchemeFile(variant, variantEntries[variant])
|
|
713
|
+
schemeSources[variant] = renderSchemeFile(variant, variantEntries[variant], molecules[variant])
|
|
489
714
|
}
|
|
490
715
|
|
|
491
716
|
return {
|
|
492
717
|
schemeSources,
|
|
493
|
-
manifestSource: renderManifest(variants, breakpoints),
|
|
718
|
+
manifestSource: renderManifest(variants, breakpoints, gradients, haptics),
|
|
494
719
|
variants,
|
|
495
720
|
serializedMisses: misses,
|
|
496
721
|
}
|
|
497
722
|
}
|
|
498
723
|
|
|
724
|
+
/** Shared empty feature map default. */
|
|
725
|
+
const EMPTY_FEATURE_MAP: ReadonlyMap<string, unknown> = new Map()
|
|
726
|
+
|
|
727
|
+
/** Shared empty literal-list default (atom-only callers). */
|
|
728
|
+
const EMPTY_LITERALS: readonly string[] = []
|
|
729
|
+
|
|
499
730
|
/** Registry key the runtime uses for the always-loaded fallback. */
|
|
500
731
|
export const COMMON_SCHEME_NAME: string = COMMON_SCHEME
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto'
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import type { KeyframeBlock, SchemedStyle, TailwindParser } from '../parser'
|
|
4
|
+
import type { GradientAtomInfo, HapticRequest, KeyframeBlock, SchemedStyle, TailwindParser } from '../parser'
|
|
5
5
|
import { buildSchemeSources, type AtomSerializedCache } from './build-style'
|
|
6
6
|
|
|
7
7
|
/** Manifest module basename — the file SchemeProvider imports via the resolver. */
|
|
@@ -90,6 +90,17 @@ class UnionBuilder {
|
|
|
90
90
|
private readonly parser: TailwindParser
|
|
91
91
|
private readonly unionAtoms = new Map<string, SchemedStyle>()
|
|
92
92
|
private readonly unionKeyframes = new Map<string, KeyframeBlock>()
|
|
93
|
+
/** atom name → gradient role/colour, surfaced into the manifest's `registerGradients`. */
|
|
94
|
+
private readonly unionGradients = new Map<string, GradientAtomInfo>()
|
|
95
|
+
/** atom name → haptic request, surfaced into the manifest's `registerHaptics`. */
|
|
96
|
+
private readonly unionHaptics = new Map<string, HapticRequest>()
|
|
97
|
+
/**
|
|
98
|
+
* Distinct literal className strings seen across all files, pre-merged
|
|
99
|
+
* into per-scheme molecules at write time. Accumulate-only (like
|
|
100
|
+
* `unionAtoms`): orphaned literals just yield unused molecules and get
|
|
101
|
+
* reaped on the next cold start, so no refcount is needed.
|
|
102
|
+
*/
|
|
103
|
+
private readonly unionLiterals = new Set<string>()
|
|
93
104
|
/**
|
|
94
105
|
* Responsive breakpoints captured from the parser. Refreshed on every
|
|
95
106
|
* `recordFile` / `ensureProjectScanned` so user-defined
|
|
@@ -169,6 +180,8 @@ class UnionBuilder {
|
|
|
169
180
|
const parsed = await this.parser.parseProject()
|
|
170
181
|
for (const [name, style] of parsed.atoms) this.unionAtoms.set(name, style)
|
|
171
182
|
for (const [name, kf] of parsed.keyframes) this.unionKeyframes.set(name, kf)
|
|
183
|
+
for (const [name, gradient] of parsed.gradientAtoms) this.unionGradients.set(name, gradient)
|
|
184
|
+
for (const [name, haptic] of parsed.hapticAtoms) this.unionHaptics.set(name, haptic)
|
|
172
185
|
this.breakpoints = parsed.breakpoints
|
|
173
186
|
this.projectScanned = true
|
|
174
187
|
})()
|
|
@@ -187,6 +200,7 @@ class UnionBuilder {
|
|
|
187
200
|
* @param file Absolute source file path.
|
|
188
201
|
* @param atoms Per-atom resolved schemed styles from this transform.
|
|
189
202
|
* @param keyframes Keyframe blocks referenced by this file's atoms.
|
|
203
|
+
* @param literals
|
|
190
204
|
* @returns `{ changed: true }` when the union shifted (new atom name,
|
|
191
205
|
* removed atom name, or new keyframe) — the transformer uses this
|
|
192
206
|
* to skip the serializer + `writeSchemes` when nothing changed.
|
|
@@ -195,8 +209,10 @@ class UnionBuilder {
|
|
|
195
209
|
file: string,
|
|
196
210
|
atoms: ReadonlyMap<string, SchemedStyle>,
|
|
197
211
|
keyframes: ReadonlyMap<string, KeyframeBlock>,
|
|
212
|
+
literals: readonly string[] = [],
|
|
198
213
|
): Promise<{ changed: boolean }> {
|
|
199
214
|
await this.ensureProjectScanned()
|
|
215
|
+
const literalAdded = this.recordLiterals(literals)
|
|
200
216
|
const newAtomNames = new Set(atoms.keys())
|
|
201
217
|
const previous = this.fileAtomSets.get(file)
|
|
202
218
|
if (previous && setsEqual(previous, newAtomNames)) {
|
|
@@ -211,12 +227,29 @@ class UnionBuilder {
|
|
|
211
227
|
if (!this.unionKeyframes.has(name)) keyframeAdded = true
|
|
212
228
|
this.unionKeyframes.set(name, kf)
|
|
213
229
|
}
|
|
214
|
-
return { changed: keyframeAdded }
|
|
230
|
+
return { changed: keyframeAdded || literalAdded }
|
|
215
231
|
}
|
|
216
232
|
this.applyDiff(file, newAtomNames, atoms, keyframes)
|
|
217
233
|
return { changed: true }
|
|
218
234
|
}
|
|
219
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Merge a file's literal classNames into the union. A literal the
|
|
238
|
+
* union hasn't seen flips `changed` so `writeSchemes` re-emits the
|
|
239
|
+
* scheme files with the new molecule.
|
|
240
|
+
* @param literals Distinct literal className strings.
|
|
241
|
+
* @returns Whether any literal was new to the union.
|
|
242
|
+
*/
|
|
243
|
+
private recordLiterals(literals: readonly string[]): boolean {
|
|
244
|
+
let added = false
|
|
245
|
+
for (const literal of literals) {
|
|
246
|
+
if (this.unionLiterals.has(literal)) continue
|
|
247
|
+
this.unionLiterals.add(literal)
|
|
248
|
+
added = true
|
|
249
|
+
}
|
|
250
|
+
return added
|
|
251
|
+
}
|
|
252
|
+
|
|
220
253
|
/**
|
|
221
254
|
* Forget one source file's contribution. Idempotent — repeated calls
|
|
222
255
|
* for a file that's already dropped are no-ops. Does NOT remove the
|
|
@@ -246,7 +279,7 @@ class UnionBuilder {
|
|
|
246
279
|
public async writeSchemes(): Promise<{ changedSchemes: readonly string[] }> {
|
|
247
280
|
await this.ensureProjectScanned()
|
|
248
281
|
const sortedAtomNames = [...this.unionAtoms.keys()].toSorted((a, b) => a.localeCompare(b))
|
|
249
|
-
const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints)
|
|
282
|
+
const result = buildSchemeSources(sortedAtomNames, this.unionAtoms, this.unionKeyframes, this.serializedCache, this.breakpoints, this.unionGradients, this.unionHaptics, [...this.unionLiterals])
|
|
250
283
|
this.serializedMissesCount += result.serializedMisses
|
|
251
284
|
const { schemeSources, manifestSource } = result
|
|
252
285
|
|
package/src/metro/dts.ts
CHANGED
|
@@ -56,21 +56,15 @@ const CONTENT_CONTAINER_INTERFACES: ReadonlySet<string> = new Set([
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Build the body of one interface augmentation: `className?: string`
|
|
59
|
-
* plus
|
|
60
|
-
*
|
|
61
|
-
* scan and diff.
|
|
59
|
+
* plus `contentContainerClassName?: string` for the scroll interfaces
|
|
60
|
+
* that natively expose `contentContainerStyle`. Emits a single-line
|
|
61
|
+
* body so the file stays easy to scan and diff.
|
|
62
62
|
* @param interfaceName Bare interface name (generic parameters stripped).
|
|
63
|
-
* @param userPrefixes Extra prefixes from the Metro config — applied to
|
|
64
|
-
* every interface the same way `className` is.
|
|
65
63
|
* @returns Space-separated property declarations.
|
|
66
64
|
*/
|
|
67
|
-
function buildInterfaceBody(interfaceName: string
|
|
65
|
+
function buildInterfaceBody(interfaceName: string): string {
|
|
68
66
|
const props = ['className?: string']
|
|
69
67
|
if (CONTENT_CONTAINER_INTERFACES.has(interfaceName)) props.push(`${BUILTIN_PREFIX}ClassName?: string`)
|
|
70
|
-
for (const prefix of userPrefixes) {
|
|
71
|
-
if (prefix === BUILTIN_PREFIX) continue
|
|
72
|
-
props.push(`${prefix}ClassName?: string`)
|
|
73
|
-
}
|
|
74
68
|
return props.join('; ')
|
|
75
69
|
}
|
|
76
70
|
|
|
@@ -83,17 +77,11 @@ function buildInterfaceBody(interfaceName: string, userPrefixes: readonly string
|
|
|
83
77
|
* `Scheme` union so `useScheme()` returns the actual names.
|
|
84
78
|
*
|
|
85
79
|
* Called once at Metro-config time — overwrite-on-rewrite so the file
|
|
86
|
-
* stays in sync with the user's current theme CSS
|
|
80
|
+
* stays in sync with the user's current theme CSS.
|
|
87
81
|
* @param targetPath Absolute path to write (typically `rnwind-types.d.ts` at project root).
|
|
88
82
|
* @param schemes Scheme names from the user's `@variant` blocks (empty when none declared).
|
|
89
|
-
* @param classNamePrefixes Extra prefixes from the Metro config — merged
|
|
90
|
-
* on top of the built-in `'contentContainer'`. Defaults to empty.
|
|
91
83
|
*/
|
|
92
|
-
export function writeDtsFile(
|
|
93
|
-
targetPath: string,
|
|
94
|
-
schemes: readonly string[],
|
|
95
|
-
classNamePrefixes: readonly string[] = [],
|
|
96
|
-
): void {
|
|
84
|
+
export function writeDtsFile(targetPath: string, schemes: readonly string[]): void {
|
|
97
85
|
const lines: string[] = [
|
|
98
86
|
'// Auto-generated by rnwind — do not edit by hand.',
|
|
99
87
|
'// Overwritten on Metro start / theme CSS change.',
|
|
@@ -102,7 +90,7 @@ export function writeDtsFile(
|
|
|
102
90
|
]
|
|
103
91
|
for (const entry of INTERFACES) {
|
|
104
92
|
const name = typeof entry === 'string' ? entry : entry.name
|
|
105
|
-
const body = buildInterfaceBody(name
|
|
93
|
+
const body = buildInterfaceBody(name)
|
|
106
94
|
if (typeof entry === 'string') {
|
|
107
95
|
lines.push(` interface ${entry} { ${body} }`)
|
|
108
96
|
continue
|