rnwind 0.0.7 → 0.0.9
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/parser/color.cjs +33 -1
- package/lib/cjs/core/parser/color.cjs.map +1 -1
- package/lib/cjs/core/parser/color.d.ts +10 -0
- package/lib/cjs/core/parser/declaration.cjs +161 -10
- package/lib/cjs/core/parser/declaration.cjs.map +1 -1
- package/lib/cjs/core/parser/gradient.cjs +46 -12
- package/lib/cjs/core/parser/gradient.cjs.map +1 -1
- package/lib/cjs/core/parser/gradient.d.ts +2 -1
- package/lib/cjs/core/parser/keyframes.cjs +27 -12
- package/lib/cjs/core/parser/keyframes.cjs.map +1 -1
- package/lib/cjs/core/parser/keyframes.d.ts +11 -0
- package/lib/cjs/core/parser/layout-dispatcher.cjs +33 -10
- package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -1
- package/lib/cjs/core/parser/length.cjs +17 -1
- package/lib/cjs/core/parser/length.cjs.map +1 -1
- package/lib/cjs/core/parser/safe-area.cjs +24 -3
- package/lib/cjs/core/parser/safe-area.cjs.map +1 -1
- package/lib/cjs/core/parser/theme-vars.cjs +58 -8
- package/lib/cjs/core/parser/theme-vars.cjs.map +1 -1
- package/lib/cjs/core/parser/tokens.cjs +77 -9
- package/lib/cjs/core/parser/tokens.cjs.map +1 -1
- package/lib/cjs/core/parser/tokens.d.ts +9 -0
- package/lib/cjs/core/parser/transform.cjs +18 -9
- package/lib/cjs/core/parser/transform.cjs.map +1 -1
- package/lib/cjs/core/parser/tw-parser.cjs +93 -33
- package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
- package/lib/cjs/core/parser/typography-dispatcher.cjs +19 -1
- package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -1
- package/lib/cjs/core/parser/typography.cjs +15 -18
- package/lib/cjs/core/parser/typography.cjs.map +1 -1
- package/lib/cjs/core/parser/typography.d.ts +5 -5
- package/lib/cjs/core/style-builder/union-builder.cjs +0 -10
- package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
- package/lib/cjs/core/style-builder/union-builder.d.ts +0 -8
- package/lib/cjs/metro/dts.cjs +6 -1
- package/lib/cjs/metro/dts.cjs.map +1 -1
- package/lib/cjs/metro/transformer.cjs +43 -60
- package/lib/cjs/metro/transformer.cjs.map +1 -1
- package/lib/cjs/metro/with-config.cjs +9 -29
- package/lib/cjs/metro/with-config.cjs.map +1 -1
- package/lib/cjs/runtime/hooks/use-scheme.cjs +9 -6
- package/lib/cjs/runtime/hooks/use-scheme.cjs.map +1 -1
- package/lib/cjs/runtime/hooks/use-scheme.d.ts +7 -4
- package/lib/cjs/runtime/index.cjs +1 -1
- package/lib/cjs/runtime/index.cjs.map +1 -1
- package/lib/cjs/runtime/index.d.ts +1 -1
- package/lib/cjs/runtime/lookup-css.cjs +14 -0
- package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
- package/lib/cjs/runtime/lookup-css.d.ts +11 -0
- package/lib/cjs/runtime/resolve.cjs +8 -6
- package/lib/cjs/runtime/resolve.cjs.map +1 -1
- package/lib/cjs/runtime/wrap.cjs +50 -57
- package/lib/cjs/runtime/wrap.cjs.map +1 -1
- package/lib/cjs/runtime/wrap.d.ts +10 -4
- package/lib/esm/core/parser/color.d.ts +10 -0
- package/lib/esm/core/parser/color.mjs +33 -2
- package/lib/esm/core/parser/color.mjs.map +1 -1
- package/lib/esm/core/parser/declaration.mjs +162 -11
- package/lib/esm/core/parser/declaration.mjs.map +1 -1
- package/lib/esm/core/parser/gradient.d.ts +2 -1
- package/lib/esm/core/parser/gradient.mjs +45 -11
- package/lib/esm/core/parser/gradient.mjs.map +1 -1
- package/lib/esm/core/parser/keyframes.d.ts +11 -0
- package/lib/esm/core/parser/keyframes.mjs +27 -12
- package/lib/esm/core/parser/keyframes.mjs.map +1 -1
- package/lib/esm/core/parser/layout-dispatcher.mjs +33 -10
- package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
- package/lib/esm/core/parser/length.mjs +17 -1
- package/lib/esm/core/parser/length.mjs.map +1 -1
- package/lib/esm/core/parser/safe-area.mjs +24 -3
- package/lib/esm/core/parser/safe-area.mjs.map +1 -1
- package/lib/esm/core/parser/theme-vars.mjs +58 -8
- package/lib/esm/core/parser/theme-vars.mjs.map +1 -1
- package/lib/esm/core/parser/tokens.d.ts +9 -0
- package/lib/esm/core/parser/tokens.mjs +77 -10
- package/lib/esm/core/parser/tokens.mjs.map +1 -1
- package/lib/esm/core/parser/transform.mjs +18 -9
- package/lib/esm/core/parser/transform.mjs.map +1 -1
- package/lib/esm/core/parser/tw-parser.mjs +95 -35
- package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
- package/lib/esm/core/parser/typography-dispatcher.mjs +19 -1
- package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
- package/lib/esm/core/parser/typography.d.ts +5 -5
- package/lib/esm/core/parser/typography.mjs +15 -18
- package/lib/esm/core/parser/typography.mjs.map +1 -1
- package/lib/esm/core/style-builder/union-builder.d.ts +0 -8
- package/lib/esm/core/style-builder/union-builder.mjs +0 -10
- package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
- package/lib/esm/metro/dts.mjs +6 -1
- package/lib/esm/metro/dts.mjs.map +1 -1
- package/lib/esm/metro/transformer.mjs +43 -60
- package/lib/esm/metro/transformer.mjs.map +1 -1
- package/lib/esm/metro/with-config.mjs +10 -30
- package/lib/esm/metro/with-config.mjs.map +1 -1
- package/lib/esm/runtime/hooks/use-scheme.d.ts +7 -4
- package/lib/esm/runtime/hooks/use-scheme.mjs +9 -6
- package/lib/esm/runtime/hooks/use-scheme.mjs.map +1 -1
- package/lib/esm/runtime/index.d.ts +1 -1
- package/lib/esm/runtime/index.mjs +1 -1
- package/lib/esm/runtime/index.mjs.map +1 -1
- package/lib/esm/runtime/lookup-css.d.ts +11 -0
- package/lib/esm/runtime/lookup-css.mjs +14 -1
- package/lib/esm/runtime/lookup-css.mjs.map +1 -1
- package/lib/esm/runtime/resolve.mjs +9 -7
- package/lib/esm/runtime/resolve.mjs.map +1 -1
- package/lib/esm/runtime/wrap.d.ts +10 -4
- package/lib/esm/runtime/wrap.mjs +50 -57
- package/lib/esm/runtime/wrap.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/parser/color.ts +32 -1
- package/src/core/parser/declaration.ts +160 -10
- package/src/core/parser/gradient.ts +48 -11
- package/src/core/parser/keyframes.ts +31 -3
- package/src/core/parser/layout-dispatcher.ts +32 -9
- package/src/core/parser/length.ts +18 -1
- package/src/core/parser/safe-area.ts +23 -2
- package/src/core/parser/theme-vars.ts +75 -8
- package/src/core/parser/tokens.ts +76 -9
- package/src/core/parser/transform.ts +19 -8
- package/src/core/parser/tw-parser.ts +95 -30
- package/src/core/parser/typography-dispatcher.ts +20 -1
- package/src/core/parser/typography.ts +15 -15
- package/src/core/style-builder/union-builder.ts +0 -11
- package/src/metro/dts.ts +6 -1
- package/src/metro/transformer.ts +45 -61
- package/src/metro/with-config.ts +10 -29
- package/src/runtime/hooks/use-scheme.ts +9 -6
- package/src/runtime/index.ts +1 -1
- package/src/runtime/lookup-css.ts +14 -0
- package/src/runtime/resolve.ts +9 -7
- package/src/runtime/wrap.tsx +57 -61
|
@@ -3,25 +3,25 @@ import { dimensionPercentageToNumber } from './length'
|
|
|
3
3
|
import type { RNEntry } from './types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
6
|
+
* Display values React Native's `display` style prop actually accepts.
|
|
7
|
+
* Everything else (`block`, `inline`, `inline-block`, `grid`, `table`, …)
|
|
8
|
+
* has no RN analog — RN lays out as flex by default, and emitting an invalid
|
|
9
|
+
* value triggers a dev warning + silent drop. So we drop them outright.
|
|
10
|
+
*/
|
|
11
|
+
const RN_DISPLAY_VALUES: ReadonlySet<string> = new Set(['none', 'flex', 'contents'])
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Expand lightningcss's `Display` typed value to an RN `{display}` entry,
|
|
15
|
+
* keeping only the values RN supports (`none` / `flex` / `contents`).
|
|
16
|
+
* - `keyword` variant emits only when the keyword is RN-valid.
|
|
17
|
+
* - `pair` variant (the modern CSS model) collapses `flex` inside to
|
|
18
|
+
* `'flex'`; `flow` (`block`/`inline`) and `grid` have no RN analog → drop.
|
|
11
19
|
* @param value Typed display value.
|
|
12
20
|
* @returns RN entries (zero or one).
|
|
13
21
|
*/
|
|
14
22
|
export function displayToEntries(value: Display): readonly RNEntry[] {
|
|
15
|
-
if (value.type === 'keyword') return [['display', value.value]]
|
|
16
|
-
if (value.type === 'pair')
|
|
17
|
-
const inside = value.inside.type
|
|
18
|
-
// `flow` is the default inside mode — maps to `block` / `inline` /
|
|
19
|
-
// `inline-block` based on the outer; RN only distinguishes `block`-ish
|
|
20
|
-
// from `flex`, so collapse the `flow` family to the `outside` keyword.
|
|
21
|
-
if (inside === 'flow') return [['display', value.outside]]
|
|
22
|
-
if (inside === 'flex') return [['display', 'flex']]
|
|
23
|
-
if (inside === 'grid') return [['display', 'grid']]
|
|
24
|
-
}
|
|
23
|
+
if (value.type === 'keyword') return RN_DISPLAY_VALUES.has(value.value) ? [['display', value.value]] : []
|
|
24
|
+
if (value.type === 'pair' && value.inside.type === 'flex') return [['display', 'flex']]
|
|
25
25
|
return []
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -143,17 +143,6 @@ class UnionBuilder {
|
|
|
143
143
|
return path.join(this.cacheDir, MANIFEST_BASENAME)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/**
|
|
147
|
-
* Snapshot of every source file the builder has recorded atoms for
|
|
148
|
-
* this worker session. Used by `withRnwindConfig`'s CSS watcher to
|
|
149
|
-
* touch `mtime` on each and nudge Metro into re-transforming them
|
|
150
|
-
* with the new theme values.
|
|
151
|
-
* @returns Absolute source paths.
|
|
152
|
-
*/
|
|
153
|
-
public recordedFiles(): readonly string[] {
|
|
154
|
-
return [...this.fileAtomSets.keys()]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
146
|
/** Cumulative cache-miss count — exposed for tests to assert cache behaviour. */
|
|
158
147
|
public get serializedMisses(): number {
|
|
159
148
|
return this.serializedMissesCount
|
package/src/metro/dts.ts
CHANGED
|
@@ -100,7 +100,12 @@ export function writeDtsFile(targetPath: string, schemes: readonly string[]): vo
|
|
|
100
100
|
lines.push('}', '')
|
|
101
101
|
if (schemes.length > 0) {
|
|
102
102
|
lines.push(`declare module 'rnwind' {`, ` export interface RnwindConfig {`)
|
|
103
|
-
|
|
103
|
+
// Escape backslash / single-quote so a scheme name with a quote (only
|
|
104
|
+
// reachable via the public `writeDtsFile`, since CSS idents can't contain
|
|
105
|
+
// one) can't emit invalid TS that breaks the whole file and drops the
|
|
106
|
+
// `className` augmentation project-wide. Single-quoted to match the rest
|
|
107
|
+
// of the generated declaration.
|
|
108
|
+
const schemeLiterals = schemes.map((s) => `'${s.replaceAll('\\', '\\\\').replaceAll("'", String.raw`\'`)}'`).join(', ')
|
|
104
109
|
lines.push(` themes: readonly [${schemeLiterals}]`, ` }`, '}', '')
|
|
105
110
|
}
|
|
106
111
|
// The `export {}` is mandatory — without at least one top-level
|
package/src/metro/transformer.ts
CHANGED
|
@@ -137,35 +137,53 @@ function isThemeCssEntry(filename: string): boolean {
|
|
|
137
137
|
* @returns Rewritten source text.
|
|
138
138
|
*/
|
|
139
139
|
async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
const wrapped = rewriteWrapImports(ast, getWrapModules())
|
|
147
|
-
|
|
148
|
-
if (!/classname=/i.test(args.src)) {
|
|
149
|
-
// Import-only file: nothing to compile. Drop any stale atom
|
|
150
|
-
// contribution (className may have just been removed) and emit the
|
|
151
|
-
// wrapped imports — or the untouched source when nothing wrapped.
|
|
152
|
-
dropFileSafely(args.filename, projectRootOf(args))
|
|
153
|
-
return wrapped ? generateModule(ast).code : args.src
|
|
154
|
-
}
|
|
155
|
-
|
|
140
|
+
// SCAN FIRST — exactly like Tailwind: oxide extracts class candidates from
|
|
141
|
+
// the WHOLE file content (className, cva/clsx, plain strings, anywhere),
|
|
142
|
+
// and the compiler is the only filter. No babel parse needed to scan, so
|
|
143
|
+
// the common "file with no classes" case stays cheap and the source is
|
|
144
|
+
// returned untouched. This is what makes adding a class ANYWHERE register
|
|
145
|
+
// on hot-reload, not just inside a `className=` or a known helper call.
|
|
156
146
|
const state = getRnwindState(projectRootOf(args))
|
|
157
147
|
const extension = extensionOf(args.filename)
|
|
158
148
|
const parsed = await state.parser.parseAtoms({ content: args.src, extension })
|
|
149
|
+
const hasAtoms = parsed.atoms.size > 0
|
|
159
150
|
|
|
160
|
-
|
|
151
|
+
const hasClassName = /classname=/i.test(args.src)
|
|
152
|
+
const hasSpread = /\{\s*\.\.\./.test(args.src)
|
|
161
153
|
|
|
162
|
-
|
|
154
|
+
// Nothing for rnwind to do: no Tailwind class compiled AND no host
|
|
155
|
+
// wrapping needed. Drop any stale contribution and emit the file as-is.
|
|
156
|
+
if (!hasAtoms && !hasClassName && !hasSpread) {
|
|
163
157
|
state.builder.dropFile(args.filename)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
return args.src
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const ast = parseUserSource(args.src)
|
|
162
|
+
if (!ast) {
|
|
163
|
+
// Can't parse to wrap/inject, but we DID scan — still record the atoms so
|
|
164
|
+
// they register (a malformed-but-recoverable file shouldn't lose styles).
|
|
165
|
+
if (hasAtoms) {
|
|
166
|
+
await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, [])
|
|
167
|
+
await state.builder.writeSchemes()
|
|
168
|
+
} else {
|
|
169
|
+
state.builder.dropFile(args.filename)
|
|
170
|
+
}
|
|
171
|
+
return args.src
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Wrap host imports ONLY when className flows through a component — written
|
|
175
|
+
// (`hasClassName`) or forwarded (`{...rest}`). A `useCss`/`cva`-only file
|
|
176
|
+
// resolves manually, so its imports (e.g. skia drawing primitives) are
|
|
177
|
+
// left untouched.
|
|
178
|
+
const wrapped = hasClassName || hasSpread ? rewriteWrapImports(ast, getWrapModules()) : false
|
|
179
|
+
|
|
180
|
+
if (!hasAtoms) {
|
|
181
|
+
// Wrap-only forwarder — no classes to record/register.
|
|
182
|
+
state.builder.dropFile(args.filename)
|
|
183
|
+
return wrapped ? generateModule(ast).code : args.src
|
|
167
184
|
}
|
|
168
185
|
|
|
186
|
+
warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename, [parsed.gradientAtoms, parsed.hapticAtoms])
|
|
169
187
|
const literals = collectClassNameLiterals(ast)
|
|
170
188
|
const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, literals)
|
|
171
189
|
if (changed) await state.builder.writeSchemes()
|
|
@@ -175,21 +193,6 @@ async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
|
|
|
175
193
|
return generateModule(ast).code
|
|
176
194
|
}
|
|
177
195
|
|
|
178
|
-
/**
|
|
179
|
-
* Drop a file's union contribution, swallowing the "state not configured"
|
|
180
|
-
* error unit tests hit when they call the transformer without
|
|
181
|
-
* `configureRnwindState`.
|
|
182
|
-
* @param filename Absolute source path.
|
|
183
|
-
* @param projectRoot Project root for state lookup.
|
|
184
|
-
*/
|
|
185
|
-
function dropFileSafely(filename: string, projectRoot: string): void {
|
|
186
|
-
try {
|
|
187
|
-
getRnwindState(projectRoot).builder.dropFile(filename)
|
|
188
|
-
} catch {
|
|
189
|
-
// State not configured (standalone/unit test). Nothing to drop.
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
196
|
/**
|
|
194
197
|
* Whether a JSX attribute names a className-style prop (`className` or
|
|
195
198
|
* any `<prefix>ClassName`).
|
|
@@ -387,16 +390,12 @@ function loadUpstream(): UpstreamTransformer | null {
|
|
|
387
390
|
*/
|
|
388
391
|
function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
389
392
|
if (!/\.(?:tsx|ts|jsx|js)$/i.test(args.filename)) return false
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
// alone so it never pays for an unused wrapper.
|
|
397
|
-
const hasClassName = /classname=/i.test(args.src)
|
|
398
|
-
const isForwarder = /\{\s*\.\.\./.test(args.src) && mentionsWrapModule(args.src)
|
|
399
|
-
if (!hasClassName && !isForwarder) return false
|
|
393
|
+
// EVERY user source file is scanned — exactly like Tailwind walks its
|
|
394
|
+
// whole content set. The compiler is the only filter (see rewriteSource);
|
|
395
|
+
// no className/helper pre-filter, because that "filter magic" missed
|
|
396
|
+
// classes living in cva/clsx/plain-string files and broke their hot-reload.
|
|
397
|
+
// (Cheap when the file has no classes: oxide finds nothing, no babel parse,
|
|
398
|
+
// source returned untouched.)
|
|
400
399
|
if (!args.filename.includes('/node_modules/')) return true
|
|
401
400
|
// node_modules in path → could be a workspace symlink; resolve it.
|
|
402
401
|
try {
|
|
@@ -407,21 +406,6 @@ function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
|
407
406
|
}
|
|
408
407
|
}
|
|
409
408
|
|
|
410
|
-
/**
|
|
411
|
-
* Cheap pre-parse check: does the source import from any configured
|
|
412
|
-
* wrap-module? A quoted specifier match is enough — `rewriteWrapImports`
|
|
413
|
-
* re-verifies precisely on the AST, so a false positive only costs a
|
|
414
|
-
* no-op parse.
|
|
415
|
-
* @param source Source text.
|
|
416
|
-
* @returns True when a wrap-module specifier appears in the source.
|
|
417
|
-
*/
|
|
418
|
-
function mentionsWrapModule(source: string): boolean {
|
|
419
|
-
for (const moduleName of getWrapModules().keys()) {
|
|
420
|
-
if (source.includes(`'${moduleName}'`) || source.includes(`"${moduleName}"`)) return true
|
|
421
|
-
}
|
|
422
|
-
return false
|
|
423
|
-
}
|
|
424
|
-
|
|
425
409
|
/**
|
|
426
410
|
* Fallback parse when no upstream is configured AND Metro didn't hand
|
|
427
411
|
* us an AST. Used by unit tests and standalone setups.
|
package/src/metro/with-config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, watch as watchFile } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { writeDtsFile } from './dts'
|
|
4
4
|
import { createRnwindResolver, type ResolveRequestFn } from './resolver'
|
|
@@ -15,14 +15,15 @@ const DEFAULT_CACHE_DIR = '.rnwind'
|
|
|
15
15
|
let activeCssWatcher: { cssPath: string; close: () => void } | null = null
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Watch the theme CSS for edits. On change,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
18
|
+
* Watch the theme CSS for edits. On change, rebuild state against the fresh
|
|
19
|
+
* CSS and rewrite the per-scheme files (`onThemeChange` → full rescan →
|
|
20
|
+
* `writeSchemes`). That's the entire HMR signal: every transformed source
|
|
21
|
+
* imports `rnwind/__generated/schemes`, which eager-imports
|
|
22
|
+
* `common.style.js`; the rewrite changes its bytes, Metro's content-SHA1
|
|
23
|
+
* dedup notices, and every importer is invalidated + re-bundled with the new
|
|
24
|
+
* style values. The rewritten JSX references atoms by NAME (theme-independent),
|
|
25
|
+
* so no source-file re-transform / mtime nudge is needed — the dep graph
|
|
26
|
+
* carries the signal.
|
|
26
27
|
* @param cssPath Absolute path to the theme CSS to watch.
|
|
27
28
|
* @param projectRoot Metro's project root (for `getRnwindState`).
|
|
28
29
|
*/
|
|
@@ -40,7 +41,6 @@ function watchThemeCss(cssPath: string, projectRoot: string): void {
|
|
|
40
41
|
pending = false
|
|
41
42
|
try {
|
|
42
43
|
await onThemeChange(projectRoot)
|
|
43
|
-
touchRecordedFiles(projectRoot)
|
|
44
44
|
} catch {
|
|
45
45
|
// Invalidation is best-effort — never crash the dev server.
|
|
46
46
|
}
|
|
@@ -49,25 +49,6 @@ function watchThemeCss(cssPath: string, projectRoot: string): void {
|
|
|
49
49
|
activeCssWatcher = { cssPath, close: () => watcher.close() }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
* Bump the mtime on every file the builder has transformed. Metro's
|
|
54
|
-
* file watcher keys on `mtime`, so this is what makes it invalidate
|
|
55
|
-
* those modules and re-transform them.
|
|
56
|
-
* @param projectRoot Metro's project root.
|
|
57
|
-
*/
|
|
58
|
-
function touchRecordedFiles(projectRoot: string): void {
|
|
59
|
-
const state = getRnwindState(projectRoot)
|
|
60
|
-
const files = state.builder.recordedFiles()
|
|
61
|
-
const now = new Date()
|
|
62
|
-
for (const file of files) {
|
|
63
|
-
try {
|
|
64
|
-
if (existsSync(file)) utimesSync(file, now, now)
|
|
65
|
-
} catch {
|
|
66
|
-
// One file's stat failure shouldn't stop the others.
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
52
|
/**
|
|
72
53
|
* Where the rnwind babel transformer lives — resolved relative to this
|
|
73
54
|
* module so the path works from both the `src/` tree (tests) and the
|
|
@@ -42,22 +42,25 @@ export function useToken(cssVariable: string): string | number | undefined {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Read a color token by shorthand name — `useColor('primary')` resolves
|
|
45
|
-
* `--color-primary` for the active scheme.
|
|
46
|
-
*
|
|
45
|
+
* `--color-primary` for the active scheme. A fully-qualified name
|
|
46
|
+
* (`--color-primary`) is accepted as-is, so the call doesn't silently miss by
|
|
47
|
+
* double-prefixing into `--color---color-primary`.
|
|
48
|
+
* @param name Token suffix after `--color-`, or the full `--color-*` name.
|
|
47
49
|
* @returns Resolved color string, or undefined when the token is missing
|
|
48
50
|
* or its value isn't a string.
|
|
49
51
|
*/
|
|
50
52
|
export function useColor(name: string): string | undefined {
|
|
51
|
-
const value = useToken(`--color-${name}`)
|
|
53
|
+
const value = useToken(name.startsWith('--') ? name : `--color-${name}`)
|
|
52
54
|
return typeof value === 'string' ? value : undefined
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* Read a spacing token by shorthand name — `useSize('4')` resolves
|
|
57
|
-
* `--spacing-4` for the active scheme.
|
|
58
|
-
*
|
|
59
|
+
* `--spacing-4` for the active scheme. A fully-qualified `--spacing-*` name is
|
|
60
|
+
* accepted as-is (no double-prefix miss).
|
|
61
|
+
* @param name Token suffix after `--spacing-`, or the full `--spacing-*` name.
|
|
59
62
|
* @returns Resolved spacing value, or undefined when the token is missing.
|
|
60
63
|
*/
|
|
61
64
|
export function useSize(name: string): number | string | undefined {
|
|
62
|
-
return useToken(`--spacing-${name}`)
|
|
65
|
+
return useToken(name.startsWith('--') ? name : `--spacing-${name}`)
|
|
63
66
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -280,6 +280,20 @@ function tierFor(windowWidth: number): number {
|
|
|
280
280
|
return tier
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Public breakpoint-tier for a width — the count of registered breakpoints
|
|
285
|
+
* whose threshold is reached. Used by the runtime resolver as its width
|
|
286
|
+
* cache dimension: the numeric tier is bounded AND exact, unlike the
|
|
287
|
+
* clamped `activeBreakpoint` NAME (which collapses tier-0 into the smallest
|
|
288
|
+
* breakpoint, so widths straddling that threshold would share a cache key
|
|
289
|
+
* and serve a stale style).
|
|
290
|
+
* @param windowWidth Live window width in px.
|
|
291
|
+
* @returns Tier 0..N.
|
|
292
|
+
*/
|
|
293
|
+
export function breakpointTier(windowWidth: number): number {
|
|
294
|
+
return tierFor(windowWidth)
|
|
295
|
+
}
|
|
296
|
+
|
|
283
297
|
/**
|
|
284
298
|
* Build the style array for a (hoist, scheme, state, width) tuple.
|
|
285
299
|
* Walks the atom list, applies the interact-state and breakpoint
|
package/src/runtime/resolve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getStyleVersion, lookupCss, type InteractState } from './lookup-css'
|
|
1
|
+
import { breakpointTier, getStyleVersion, lookupCss, type InteractState } from './lookup-css'
|
|
2
2
|
import type { RnwindState } from './components/rnwind-provider'
|
|
3
3
|
import { normalizeClassName } from '../core/normalize-classname'
|
|
4
4
|
import type { GradientAtomInfo, GradientDirection } from '../core/parser/gradient'
|
|
@@ -148,17 +148,19 @@ const stateSignatureCache = new WeakMap<RnwindState, string>()
|
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Cache key dimension for the reactive context — everything that can
|
|
151
|
-
* change a resolved style. Uses the breakpoint TIER (
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
151
|
+
* change a resolved style. Uses the numeric breakpoint TIER (count of
|
|
152
|
+
* thresholds reached) from `breakpointTier(windowWidth)`, NOT the
|
|
153
|
+
* `activeBreakpoint` NAME: the name clamps tier-0 into the smallest
|
|
154
|
+
* breakpoint, so widths straddling that threshold (e.g. 320 vs 700 with
|
|
155
|
+
* `sm=640`) would collide on one cache key and serve a stale style. The
|
|
156
|
+
* tier is exact AND bounded — two widths in the same tier gate every
|
|
157
|
+
* `sm:`/`md:`/… atom identically, so they resolve the same.
|
|
156
158
|
* @param state Rnwind context.
|
|
157
159
|
* @returns Compact signature string.
|
|
158
160
|
*/
|
|
159
161
|
function stateSignature(state: RnwindState): string {
|
|
160
162
|
const { insets } = state
|
|
161
|
-
return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${state.
|
|
163
|
+
return `${state.scheme}|${insets.top},${insets.right},${insets.bottom},${insets.left}|${state.fontScale}|${breakpointTier(state.windowWidth)}`
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
/**
|
package/src/runtime/wrap.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { chainFocus, chainPress } from './chain-handlers'
|
|
|
3
3
|
import { useInteract } from './hooks/use-interact'
|
|
4
4
|
import { useRnwind } from './components/rnwind-provider'
|
|
5
5
|
import type { RnwindState } from './components/rnwind-provider'
|
|
6
|
+
import type { InteractState } from './lookup-css'
|
|
6
7
|
import { resolve, type ResolvedCss } from './resolve'
|
|
7
8
|
import type { OnHaptics } from '../core/parser/haptics'
|
|
8
9
|
|
|
@@ -79,14 +80,20 @@ const CLASSNAME_SUFFIX = 'ClassName'
|
|
|
79
80
|
* handled separately by the leaf and never reaches here.
|
|
80
81
|
* @param props Mutable prop object being assembled for the host.
|
|
81
82
|
* @param state Rnwind context for resolution.
|
|
83
|
+
* @param interactState Live press/focus state so `active:`/`focus:` variants
|
|
84
|
+
* on a secondary class prop resolve too (undefined for non-interactive).
|
|
82
85
|
*/
|
|
83
|
-
function applyContainerClassNames(
|
|
86
|
+
function applyContainerClassNames(
|
|
87
|
+
props: Record<string, unknown>,
|
|
88
|
+
state: RnwindState,
|
|
89
|
+
interactState?: InteractState,
|
|
90
|
+
): void {
|
|
84
91
|
for (const key of Object.keys(props)) {
|
|
85
92
|
if (!key.endsWith(CLASSNAME_SUFFIX)) continue
|
|
86
93
|
const value = props[key]
|
|
87
94
|
if (typeof value !== 'string') continue
|
|
88
95
|
const styleKey = `${key.slice(0, -CLASSNAME_SUFFIX.length)}Style`
|
|
89
|
-
props[styleKey] = resolve(value, state, props[styleKey]).style
|
|
96
|
+
props[styleKey] = resolve(value, state, props[styleKey], interactState).style
|
|
90
97
|
delete props[key]
|
|
91
98
|
}
|
|
92
99
|
}
|
|
@@ -102,6 +109,7 @@ function applyContainerClassNames(props: Record<string, unknown>, state: RnwindS
|
|
|
102
109
|
* @param state Rnwind context — used to resolve secondary class props.
|
|
103
110
|
* @param onHaptics Dispatcher from context.
|
|
104
111
|
* @param userOnPressIn Caller-supplied onPressIn to chain after the haptic.
|
|
112
|
+
* @param interactState Live press/focus state for secondary class props.
|
|
105
113
|
* @returns The merged prop object for `createElement`.
|
|
106
114
|
*/
|
|
107
115
|
function buildProps(
|
|
@@ -110,10 +118,11 @@ function buildProps(
|
|
|
110
118
|
state: RnwindState,
|
|
111
119
|
onHaptics: OnHaptics | undefined,
|
|
112
120
|
userOnPressIn?: unknown,
|
|
121
|
+
interactState?: InteractState,
|
|
113
122
|
): Record<string, unknown> {
|
|
114
123
|
warnIfHapticsUnwired(onHaptics, resolved.haptics)
|
|
115
124
|
const props: Record<string, unknown> = { ...rest, style: resolved.style }
|
|
116
|
-
applyContainerClassNames(props, state)
|
|
125
|
+
applyContainerClassNames(props, state, interactState)
|
|
117
126
|
if (resolved.colors) {
|
|
118
127
|
props.colors = resolved.colors
|
|
119
128
|
props.start = resolved.start
|
|
@@ -136,66 +145,31 @@ function buildProps(
|
|
|
136
145
|
return props
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
/** Props
|
|
140
|
-
interface
|
|
141
|
-
readonly as: ComponentType<Record<string, unknown>>
|
|
148
|
+
/** Props the wrapped renderer accepts — `className` plus forwarded props. */
|
|
149
|
+
interface WrappedProps {
|
|
142
150
|
readonly className?: string
|
|
143
151
|
readonly style?: unknown
|
|
152
|
+
readonly onPressIn?: unknown
|
|
153
|
+
readonly onPressOut?: unknown
|
|
154
|
+
readonly onFocus?: unknown
|
|
155
|
+
readonly onBlur?: unknown
|
|
144
156
|
readonly [key: string]: unknown
|
|
145
157
|
}
|
|
146
158
|
|
|
147
|
-
/**
|
|
148
|
-
* Non-interactive leaf: resolve className → style (+ features) and
|
|
149
|
-
* forward. One context read, one molecule/atom resolve.
|
|
150
|
-
* @param props Leaf props.
|
|
151
|
-
* @param props.as
|
|
152
|
-
* @param props.className
|
|
153
|
-
* @param props.style
|
|
154
|
-
* @param props.onPressIn
|
|
155
|
-
* @returns The rendered `as` element.
|
|
156
|
-
*/
|
|
157
|
-
function PlainLeaf({ as: As, className, style, onPressIn, ...rest }: LeafProps): ReactElement {
|
|
158
|
-
const state = useRnwind()
|
|
159
|
-
const resolved = resolve(className, state, style)
|
|
160
|
-
useMountHaptics(resolved, state.onHaptics)
|
|
161
|
-
return createElement(As, buildProps(rest, resolved, state, state.onHaptics, onPressIn))
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Interactive leaf: tracks press/focus via `useInteract()`, feeds it into
|
|
166
|
-
* `resolve` so `active:`/`focus:` atoms apply, and chains the
|
|
167
|
-
* press/focus handlers.
|
|
168
|
-
* @param props Leaf props.
|
|
169
|
-
* @param props.as
|
|
170
|
-
* @param props.className
|
|
171
|
-
* @param props.style
|
|
172
|
-
* @param props.onPressIn
|
|
173
|
-
* @param props.onPressOut
|
|
174
|
-
* @param props.onFocus
|
|
175
|
-
* @param props.onBlur
|
|
176
|
-
* @returns The rendered `as` element with interactive wiring.
|
|
177
|
-
*/
|
|
178
|
-
function InteractiveLeaf({ as: As, className, style, onPressIn, onPressOut, onFocus, onBlur, ...rest }: LeafProps): ReactElement {
|
|
179
|
-
const state = useRnwind()
|
|
180
|
-
const interact = useInteract()
|
|
181
|
-
const resolved = resolve(className, state, style, interact.state)
|
|
182
|
-
useMountHaptics(resolved, state.onHaptics)
|
|
183
|
-
const props = buildProps(rest, resolved, state, state.onHaptics, onPressIn)
|
|
184
|
-
props.onPressIn = chainPress(props.onPressIn as Parameters<typeof chainPress>[0], interact.onPressIn)
|
|
185
|
-
props.onPressOut = chainPress(onPressOut as Parameters<typeof chainPress>[0], interact.onPressOut)
|
|
186
|
-
props.onFocus = chainFocus(onFocus as Parameters<typeof chainFocus>[0], interact.onFocus)
|
|
187
|
-
props.onBlur = chainFocus(onBlur as Parameters<typeof chainFocus>[0], interact.onBlur)
|
|
188
|
-
return createElement(As, props)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
159
|
/**
|
|
192
160
|
* Wrap a component so its `className` prop resolves to RN `style` (plus
|
|
193
161
|
* gradient / truncate props and haptic dispatch) at render — no matter
|
|
194
162
|
* how className arrived: written directly, spread through `{...rest}`, or
|
|
195
|
-
* forwarded down custom wrappers.
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
163
|
+
* forwarded down custom wrappers. `ref` (a normal prop in React 19) and all
|
|
164
|
+
* other props forward untouched.
|
|
165
|
+
*
|
|
166
|
+
* A SINGLE stable component does the work and always calls `useInteract()`,
|
|
167
|
+
* so its identity never changes when `className` flips between interactive
|
|
168
|
+
* (`active:`/`focus:`) and not — swapping the rendered component type would
|
|
169
|
+
* unmount + remount the whole host subtree (lost state, effect re-runs,
|
|
170
|
+
* visual flash). `useInteract` is cheap and shares one idle-state ref, so the
|
|
171
|
+
* always-on cost is a couple of `useState` cells; the press/focus handlers
|
|
172
|
+
* and live state only wire in when the className actually needs them.
|
|
199
173
|
* @example
|
|
200
174
|
* ```tsx
|
|
201
175
|
* const Pressable = wrap(RNPressable)
|
|
@@ -207,16 +181,38 @@ function InteractiveLeaf({ as: As, className, style, onPressIn, onPressOut, onFo
|
|
|
207
181
|
export function wrap<P>(Component: ComponentType<P>): ComponentType<P & { className?: string }> {
|
|
208
182
|
const as = Component as unknown as ComponentType<Record<string, unknown>>
|
|
209
183
|
/**
|
|
210
|
-
* The wrapped component
|
|
184
|
+
* The wrapped component. Stable identity + unconditional hooks; branches
|
|
185
|
+
* internally on whether the className carries an interactive variant.
|
|
211
186
|
* @param props Forwarded props with `className` intercepted.
|
|
212
|
-
* @param props.className
|
|
213
|
-
* @
|
|
187
|
+
* @param props.className Raw className string.
|
|
188
|
+
* @param props.style Caller-supplied style, merged under the resolved style.
|
|
189
|
+
* @param props.onPressIn Caller onPressIn — chained after haptics / interact.
|
|
190
|
+
* @param props.onPressOut Caller onPressOut — chained with interact when active.
|
|
191
|
+
* @param props.onFocus Caller onFocus — chained with interact when active.
|
|
192
|
+
* @param props.onBlur Caller onBlur — chained with interact when active.
|
|
193
|
+
* @returns The rendered `as` element.
|
|
214
194
|
*/
|
|
215
|
-
function RnwindWrapped({ className,
|
|
216
|
-
|
|
217
|
-
|
|
195
|
+
function RnwindWrapped({ className, style, onPressIn, onPressOut, onFocus, onBlur, ...rest }: WrappedProps): ReactElement {
|
|
196
|
+
const state = useRnwind()
|
|
197
|
+
const interact = useInteract()
|
|
198
|
+
const isInteractive = className !== undefined && hasInteractiveVariant(className)
|
|
199
|
+
const interactState = isInteractive ? interact.state : undefined
|
|
200
|
+
const resolved = resolve(className, state, style, interactState)
|
|
201
|
+
useMountHaptics(resolved, state.onHaptics)
|
|
202
|
+
const props = buildProps(rest, resolved, state, state.onHaptics, onPressIn, interactState)
|
|
203
|
+
if (isInteractive) {
|
|
204
|
+
props.onPressIn = chainPress(props.onPressIn as Parameters<typeof chainPress>[0], interact.onPressIn)
|
|
205
|
+
props.onPressOut = chainPress(onPressOut as Parameters<typeof chainPress>[0], interact.onPressOut)
|
|
206
|
+
props.onFocus = chainFocus(onFocus as Parameters<typeof chainFocus>[0], interact.onFocus)
|
|
207
|
+
props.onBlur = chainFocus(onBlur as Parameters<typeof chainFocus>[0], interact.onBlur)
|
|
208
|
+
} else {
|
|
209
|
+
// Forward the caller's press/focus handlers untouched (onPressIn is
|
|
210
|
+
// already set by buildProps, possibly haptic-chained).
|
|
211
|
+
if (onPressOut !== undefined) props.onPressOut = onPressOut
|
|
212
|
+
if (onFocus !== undefined) props.onFocus = onFocus
|
|
213
|
+
if (onBlur !== undefined) props.onBlur = onBlur
|
|
218
214
|
}
|
|
219
|
-
return createElement(
|
|
215
|
+
return createElement(as, props)
|
|
220
216
|
}
|
|
221
217
|
RnwindWrapped.displayName = `wrap(${displayNameOf(Component)})`
|
|
222
218
|
return RnwindWrapped as unknown as ComponentType<P & { className?: string }>
|