rnwind 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/css-imports.cjs +81 -0
- package/lib/cjs/metro/css-imports.cjs.map +1 -0
- package/lib/cjs/metro/css-imports.d.ts +8 -0
- 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 +38 -86
- 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/css-imports.d.ts +8 -0
- package/lib/esm/metro/css-imports.mjs +79 -0
- package/lib/esm/metro/css-imports.mjs.map +1 -0
- 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 +39 -85
- 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/css-imports.ts +75 -0
- package/src/metro/dts.ts +7 -19
- package/src/metro/state.ts +38 -83
- 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/src/metro/transformer.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { parse } from '@babel/parser'
|
|
|
4
4
|
import generate from '@babel/generator'
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
6
|
import { realpathSync } from 'node:fs'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { getRnwindCacheKey, getRnwindState, getWrapModules, onThemeChange } from './state'
|
|
8
|
+
import { rewriteWrapImports } from './wrap-imports'
|
|
9
9
|
import { STYLE_SPECIFIERS, THEME_SIGNATURE_MODULE } from './resolver'
|
|
10
10
|
import { filterUnknownClassCandidates } from './warn-unknown-classes'
|
|
11
11
|
|
|
@@ -62,15 +62,23 @@ function parseUserSource(source: string): File | null {
|
|
|
62
62
|
* @param candidates Every candidate oxide surfaced from the source.
|
|
63
63
|
* @param atoms Successfully resolved atoms (keys are class names).
|
|
64
64
|
* @param filename Source path, prefixed onto the warning.
|
|
65
|
+
* @param features Feature-atom maps (gradient / haptic) — their names are
|
|
66
|
+
* known classes even though they carry no RN style, so they're excluded
|
|
67
|
+
* from the unknown-class warning.
|
|
65
68
|
*/
|
|
66
69
|
function warnUnknownClasses(
|
|
67
70
|
source: string,
|
|
68
71
|
candidates: readonly string[],
|
|
69
72
|
atoms: ReadonlyMap<string, unknown>,
|
|
70
73
|
filename: string,
|
|
74
|
+
features: ReadonlyArray<ReadonlyMap<string, unknown>> = [],
|
|
71
75
|
): void {
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
// Feature atoms (gradient / haptic) resolve to no RN style, so they're
|
|
77
|
+
// absent from `atoms` — but they're NOT unknown. Fold their names into
|
|
78
|
+
// the known set so `active:haptic-rigid` etc. don't warn at build time.
|
|
79
|
+
const known = new Set(atoms.keys())
|
|
80
|
+
for (const map of features) for (const name of map.keys()) known.add(name)
|
|
81
|
+
const unknown = filterUnknownClassCandidates(source, candidates, known)
|
|
74
82
|
if (unknown.length === 0) return
|
|
75
83
|
// eslint-disable-next-line no-console
|
|
76
84
|
console.warn(`rnwind: unknown class${unknown.length > 1 ? 'es' : ''} in ${filename}: ${unknown.join(', ')}`)
|
|
@@ -114,56 +122,183 @@ function isThemeCssEntry(filename: string): boolean {
|
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
125
|
+
* Wrap host imports + compile any className literals, then regenerate
|
|
126
|
+
* source. Two paths:
|
|
127
|
+
* - **className present**: oxide-scan the file, record its atoms into
|
|
128
|
+
* the union, and inject the generated-style + theme-signature
|
|
129
|
+
* side-effect imports so the runtime registries populate.
|
|
130
|
+
* - **import-only** (a `{...rest}` forwarder or a leaf with no literal
|
|
131
|
+
* `className=`): just wrap the host imports so a forwarded className
|
|
132
|
+
* still resolves at render — no oxide scan, no injected imports.
|
|
133
|
+
*
|
|
134
|
+
* On parse failure, fall back to the original source — a transient parse
|
|
135
|
+
* error shouldn't crash Metro for a file the upstream might handle fine.
|
|
121
136
|
* @param args Metro args; `src` is the original source text.
|
|
122
|
-
* @returns Rewritten source text
|
|
137
|
+
* @returns Rewritten source text.
|
|
123
138
|
*/
|
|
124
139
|
async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
|
|
125
140
|
const ast = parseUserSource(args.src)
|
|
126
141
|
if (!ast) return args.src
|
|
127
142
|
|
|
143
|
+
// Wrap host component imports so `<View className=…>` resolves at render
|
|
144
|
+
// through the runtime `wrap` (works for literal, spread, and forwarded
|
|
145
|
+
// classNames alike). No JSX is rewritten here.
|
|
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
|
+
|
|
128
156
|
const state = getRnwindState(projectRootOf(args))
|
|
129
157
|
const extension = extensionOf(args.filename)
|
|
130
158
|
const parsed = await state.parser.parseAtoms({ content: args.src, extension })
|
|
131
159
|
|
|
132
|
-
warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename)
|
|
160
|
+
warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename, [parsed.gradientAtoms, parsed.hapticAtoms])
|
|
133
161
|
|
|
134
|
-
const classNamePrefixes = getClassNamePrefixes()
|
|
135
|
-
const hostSources = getHostSources()
|
|
136
|
-
const hostComponents = getHostComponents()
|
|
137
162
|
if (parsed.atoms.size === 0) {
|
|
138
163
|
state.builder.dropFile(args.filename)
|
|
139
164
|
await state.builder.writeSchemes()
|
|
140
|
-
transformAst(ast, {
|
|
141
|
-
styleSpecifiers: [],
|
|
142
|
-
gradientAtoms: parsed.gradientAtoms,
|
|
143
|
-
hapticAtoms: parsed.hapticAtoms,
|
|
144
|
-
classNamePrefixes,
|
|
145
|
-
hostSources,
|
|
146
|
-
hostComponents,
|
|
147
|
-
})
|
|
148
165
|
injectThemeSignatureImport(ast)
|
|
149
166
|
return generateModule(ast).code
|
|
150
167
|
}
|
|
151
168
|
|
|
152
|
-
const
|
|
169
|
+
const literals = collectClassNameLiterals(ast)
|
|
170
|
+
const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, literals)
|
|
153
171
|
if (changed) await state.builder.writeSchemes()
|
|
154
172
|
|
|
155
|
-
|
|
156
|
-
styleSpecifiers: STYLE_SPECIFIERS as unknown as readonly string[],
|
|
157
|
-
gradientAtoms: parsed.gradientAtoms,
|
|
158
|
-
hapticAtoms: parsed.hapticAtoms,
|
|
159
|
-
classNamePrefixes,
|
|
160
|
-
hostSources,
|
|
161
|
-
hostComponents,
|
|
162
|
-
})
|
|
173
|
+
injectSideEffectImports(ast, STYLE_SPECIFIERS)
|
|
163
174
|
injectThemeSignatureImport(ast)
|
|
164
175
|
return generateModule(ast).code
|
|
165
176
|
}
|
|
166
177
|
|
|
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
|
+
/**
|
|
194
|
+
* Whether a JSX attribute names a className-style prop (`className` or
|
|
195
|
+
* any `<prefix>ClassName`).
|
|
196
|
+
* @param node JSX attribute node.
|
|
197
|
+
* @returns True when the attribute is a className prop.
|
|
198
|
+
*/
|
|
199
|
+
function isClassNameAttribute(node: t.JSXAttribute): boolean {
|
|
200
|
+
if (!t.isJSXIdentifier(node.name)) return false
|
|
201
|
+
const {name} = node.name
|
|
202
|
+
return name === 'className' || name.endsWith('ClassName')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pull static string literals out of a className expression. Handles a
|
|
207
|
+
* bare string, a no-substitution template, and the branches of a
|
|
208
|
+
* ternary / `&&` (so `cond ? 'a' : 'b'` and `flag && 'x'` both register
|
|
209
|
+
* their literals). Dynamic interpolations are skipped — they resolve via
|
|
210
|
+
* the runtime atom path.
|
|
211
|
+
* @param expr Expression inside a `className={...}` container.
|
|
212
|
+
* @param out Accumulator for discovered literals.
|
|
213
|
+
*/
|
|
214
|
+
function collectLiteralsFromExpression(expr: t.Expression | t.JSXEmptyExpression | null | undefined, out: string[]): void {
|
|
215
|
+
if (!expr) return
|
|
216
|
+
if (t.isStringLiteral(expr)) {
|
|
217
|
+
out.push(expr.value)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0 && expr.quasis.length === 1) {
|
|
221
|
+
const cooked = expr.quasis[0]?.value.cooked
|
|
222
|
+
if (typeof cooked === 'string') out.push(cooked)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (t.isConditionalExpression(expr)) {
|
|
226
|
+
collectLiteralsFromExpression(expr.consequent, out)
|
|
227
|
+
collectLiteralsFromExpression(expr.alternate, out)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
if (t.isLogicalExpression(expr)) {
|
|
231
|
+
collectLiteralsFromExpression(expr.right as t.Expression, out)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** AST node keys the literal walk skips — position / comment metadata. */
|
|
236
|
+
const SKIP_WALK_KEYS = new Set(['type', 'loc', 'start', 'end', 'range', 'leadingComments', 'trailingComments', 'innerComments'])
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Collect the static literals from one className JSX attribute into the
|
|
240
|
+
* dedup accumulator.
|
|
241
|
+
* @param attribute The (already className-matched) JSX attribute.
|
|
242
|
+
* @param seen Dedup set of literals already collected.
|
|
243
|
+
* @param out Ordered accumulator.
|
|
244
|
+
*/
|
|
245
|
+
function collectAttributeLiterals(attribute: t.JSXAttribute, seen: Set<string>, out: string[]): void {
|
|
246
|
+
const { value } = attribute
|
|
247
|
+
const found: string[] = []
|
|
248
|
+
if (t.isStringLiteral(value)) found.push(value.value)
|
|
249
|
+
else if (t.isJSXExpressionContainer(value)) collectLiteralsFromExpression(value.expression, found)
|
|
250
|
+
for (const literal of found) {
|
|
251
|
+
if (seen.has(literal)) continue
|
|
252
|
+
seen.add(literal)
|
|
253
|
+
out.push(literal)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Walk the AST for every `className=` / `<prefix>ClassName=` literal so
|
|
259
|
+
* the builder can pre-merge each into a per-scheme molecule. A generic
|
|
260
|
+
* node walk (no scope build) keeps it cheap; only JSX attribute nodes do
|
|
261
|
+
* any work.
|
|
262
|
+
* @param ast Parsed Babel file.
|
|
263
|
+
* @returns Distinct literal className strings, in first-seen order.
|
|
264
|
+
*/
|
|
265
|
+
function collectClassNameLiterals(ast: File): readonly string[] {
|
|
266
|
+
const out: string[] = []
|
|
267
|
+
const seen = new Set<string>()
|
|
268
|
+
const visit = (node: unknown): void => {
|
|
269
|
+
if (!node || typeof node !== 'object') return
|
|
270
|
+
if (Array.isArray(node)) {
|
|
271
|
+
for (const child of node) visit(child)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
const typed = node as { type?: string; [key: string]: unknown }
|
|
275
|
+
if (typeof typed.type !== 'string') return
|
|
276
|
+
if (typed.type === 'JSXAttribute' && isClassNameAttribute(node as t.JSXAttribute)) {
|
|
277
|
+
collectAttributeLiterals(node as t.JSXAttribute, seen, out)
|
|
278
|
+
}
|
|
279
|
+
for (const key in typed) {
|
|
280
|
+
if (SKIP_WALK_KEYS.has(key)) continue
|
|
281
|
+
visit(typed[key])
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
visit(ast.program)
|
|
285
|
+
return out
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Prepend side-effect imports (`import '<spec>'`) so the generated
|
|
290
|
+
* per-scheme style + manifest modules load — registering this file's
|
|
291
|
+
* atoms / molecules / features into the runtime registries the wrapper's
|
|
292
|
+
* `resolve` reads.
|
|
293
|
+
* @param ast Babel File AST to mutate in place.
|
|
294
|
+
* @param specifiers Module specifiers to side-effect-import.
|
|
295
|
+
*/
|
|
296
|
+
function injectSideEffectImports(ast: File, specifiers: readonly string[]): void {
|
|
297
|
+
for (const specifier of specifiers) {
|
|
298
|
+
ast.program.body.unshift(t.importDeclaration([], t.stringLiteral(specifier)))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
167
302
|
/**
|
|
168
303
|
* Prepend `import 'rnwind/__generated/theme-signature'` to every
|
|
169
304
|
* rnwind-transformed file. The resolver maps that specifier to the
|
|
@@ -252,10 +387,16 @@ function loadUpstream(): UpstreamTransformer | null {
|
|
|
252
387
|
*/
|
|
253
388
|
function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
254
389
|
if (!/\.(?:tsx|ts|jsx|js)$/i.test(args.filename)) return false
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
// `
|
|
258
|
-
|
|
390
|
+
// Process the file when it either:
|
|
391
|
+
// - carries a `className=` / `<prefix>ClassName=` literal (case-
|
|
392
|
+
// insensitive — `contentContainerClassName=` has a capital C), or
|
|
393
|
+
// - spreads props (`{...rest}`) onto a host from a wrap-module, where a
|
|
394
|
+
// forwarded className must still get its import wrapped (no literal
|
|
395
|
+
// appears in this file). A style-less `<View/>` with neither is left
|
|
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
|
|
259
400
|
if (!args.filename.includes('/node_modules/')) return true
|
|
260
401
|
// node_modules in path → could be a workspace symlink; resolve it.
|
|
261
402
|
try {
|
|
@@ -266,6 +407,21 @@ function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
|
266
407
|
}
|
|
267
408
|
}
|
|
268
409
|
|
|
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
|
+
|
|
269
425
|
/**
|
|
270
426
|
* Fallback parse when no upstream is configured AND Metro didn't hand
|
|
271
427
|
* us an AST. Used by unit tests and standalone setups.
|
package/src/metro/with-config.ts
CHANGED
|
@@ -127,35 +127,20 @@ export interface RnwindMetroOptions {
|
|
|
127
127
|
/** Cache directory. Absolute, or relative to `projectRoot`. Default: `.rnwind` at project root. */
|
|
128
128
|
cacheDir?: string
|
|
129
129
|
/**
|
|
130
|
-
* Extra
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* entries merge on top.
|
|
135
|
-
*/
|
|
136
|
-
classNamePrefixes?: readonly string[]
|
|
137
|
-
/**
|
|
138
|
-
* Extra module specifiers whose JSX exports rnwind should treat as
|
|
139
|
-
* "host components" — i.e. tags whose `className="…"` attribute is
|
|
140
|
-
* rewritten to `style={lookupCss(…)}` at build time (zero runtime
|
|
141
|
-
* cost). Merged with the built-in defaults: `react-native`,
|
|
130
|
+
* Extra module specifiers whose component exports rnwind should
|
|
131
|
+
* auto-wrap at import sites — `import { View } from 'react-native'`
|
|
132
|
+
* becomes `const View = wrap(_rnw0)` so `<View className="…">` resolves
|
|
133
|
+
* styles at runtime. Merged with the built-in defaults: `react-native`,
|
|
142
134
|
* `react-native-reanimated`, `react-native-svg`,
|
|
143
|
-
* `react-native-gesture-handler`, `
|
|
135
|
+
* `react-native-gesture-handler`, `react-native-safe-area-context`,
|
|
136
|
+
* `expo-linear-gradient`, `expo-image`, and more.
|
|
144
137
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* primitive packages into the
|
|
149
|
-
*/
|
|
150
|
-
hostSources?: readonly string[]
|
|
151
|
-
/**
|
|
152
|
-
* Extra JSX tag names (verbatim — may include `.` for member access
|
|
153
|
-
* like `'Animated.View'`) rnwind should treat as host components,
|
|
154
|
-
* regardless of where they're imported from. Useful for one-off
|
|
155
|
-
* escape-hatches: `import { View as MyBox } from 'react-native'`
|
|
156
|
-
* doesn't change the local name → `'MyBox'` here picks it up.
|
|
138
|
+
* A module NOT in this list keeps its raw imports — the importing
|
|
139
|
+
* component receives `className` as a plain prop and can resolve it
|
|
140
|
+
* via `useCss` / `wrap` itself. Use this to opt your design-system /
|
|
141
|
+
* UI primitive packages into the auto-wrap path.
|
|
157
142
|
*/
|
|
158
|
-
|
|
143
|
+
wrapModules?: readonly string[]
|
|
159
144
|
}
|
|
160
145
|
|
|
161
146
|
/** Shape we mutate on Metro's config. Loose so we don't pin Metro's internal types. */
|
|
@@ -202,7 +187,7 @@ export function withRnwindConfig<C extends MetroConfigLike>(metroConfig: C, opti
|
|
|
202
187
|
|
|
203
188
|
mkdirSync(cacheDir, { recursive: true })
|
|
204
189
|
const watchFolders = (metroConfig.watchFolders ?? []).filter((p) => typeof p === 'string' && p.length > 0)
|
|
205
|
-
configureRnwindState(cssEntry, cacheDir, watchFolders, options.
|
|
190
|
+
configureRnwindState(cssEntry, cacheDir, watchFolders, options.wrapModules)
|
|
206
191
|
|
|
207
192
|
// Warm the state eagerly (in the Metro master process) so oxide's
|
|
208
193
|
// Scanner walks every project source (and every monorepo
|
|
@@ -238,7 +223,7 @@ export function withRnwindConfig<C extends MetroConfigLike>(metroConfig: C, opti
|
|
|
238
223
|
if (options.dtsFile !== false) {
|
|
239
224
|
const dtsPath = options.dtsFile ?? path.resolve(projectRoot, 'rnwind-types.d.ts')
|
|
240
225
|
const schemes = discoverSchemes(cssEntry, projectRoot)
|
|
241
|
-
writeDtsFile(dtsPath, schemes
|
|
226
|
+
writeDtsFile(dtsPath, schemes)
|
|
242
227
|
}
|
|
243
228
|
|
|
244
229
|
// Watch the theme CSS. On edit, we rewrite scheme files AND touch
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as t from '@babel/types'
|
|
2
|
+
import type { File } from '@babel/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build-time import rewrite. For every `import { View } from
|
|
6
|
+
* 'react-native'` (and the other configured modules) it aliases the
|
|
7
|
+
* original export and binds a `wrap()`-ed component in its place:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* import { View, StyleSheet } from 'react-native'
|
|
11
|
+
* ⇩
|
|
12
|
+
* import { View as _rnw0, StyleSheet } from 'react-native'
|
|
13
|
+
* import { wrap as _rnwWrap } from 'rnwind'
|
|
14
|
+
* const View = _rnwWrap(_rnw0)
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Now `<View className="…">` resolves className → style at render via the
|
|
18
|
+
* wrapper — no matter how className arrived (literal, `{...rest}` spread,
|
|
19
|
+
* forwarded through custom layers). Non-component exports (`StyleSheet`)
|
|
20
|
+
* are left untouched.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Local binding the injected `wrap` import is aliased to. */
|
|
24
|
+
const WRAP_LOCAL = '_rnwWrap'
|
|
25
|
+
/** Local binding the injected `wrapNamespace` import is aliased to. */
|
|
26
|
+
const WRAP_NS_LOCAL = '_rnwWrapNs'
|
|
27
|
+
/** Module the wrapper is imported from. */
|
|
28
|
+
const RUNTIME_MODULE = 'rnwind'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Wrap-modules whose DEFAULT export is a component NAMESPACE accessed via
|
|
32
|
+
* member expressions (`Animated.View`), not a single component. Their
|
|
33
|
+
* default import is bound through `wrapNamespace` (a Proxy that wraps each
|
|
34
|
+
* accessed component member) instead of `wrap`. Every other default import
|
|
35
|
+
* is treated as a plain component.
|
|
36
|
+
*/
|
|
37
|
+
const NAMESPACE_DEFAULT_MODULES: ReadonlySet<string> = new Set(['react-native-reanimated'])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* react-native mixes styleable components with utilities (`StyleSheet`,
|
|
41
|
+
* `Platform`, …). Only these named exports are wrapped; everything else
|
|
42
|
+
* passes through. Other ecosystem modules export components only and use
|
|
43
|
+
* the `'all'` policy instead.
|
|
44
|
+
*/
|
|
45
|
+
const REACT_NATIVE_COMPONENTS: ReadonlySet<string> = new Set([
|
|
46
|
+
'View',
|
|
47
|
+
'Text',
|
|
48
|
+
'TextInput',
|
|
49
|
+
'Pressable',
|
|
50
|
+
'ScrollView',
|
|
51
|
+
'Image',
|
|
52
|
+
'ImageBackground',
|
|
53
|
+
'FlatList',
|
|
54
|
+
'SectionList',
|
|
55
|
+
'VirtualizedList',
|
|
56
|
+
'KeyboardAvoidingView',
|
|
57
|
+
'SafeAreaView',
|
|
58
|
+
'Modal',
|
|
59
|
+
'Switch',
|
|
60
|
+
'RefreshControl',
|
|
61
|
+
'ActivityIndicator',
|
|
62
|
+
'TouchableOpacity',
|
|
63
|
+
'TouchableHighlight',
|
|
64
|
+
'TouchableWithoutFeedback',
|
|
65
|
+
'TouchableNativeFeedback',
|
|
66
|
+
'Button',
|
|
67
|
+
'StatusBar',
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Named exports that LOOK like components (PascalCase) under an `'all'`
|
|
72
|
+
* policy but aren't — React contexts, gesture-handler enums/namespaces,
|
|
73
|
+
* etc. Wrapping these would turn `Gesture.Pan()` / `State.ACTIVE` /
|
|
74
|
+
* `<XContext.Provider>` into a `wrap()`-ed component and break them.
|
|
75
|
+
* Names ending in `Context` are excluded separately.
|
|
76
|
+
*/
|
|
77
|
+
const NON_COMPONENT_EXPORTS: ReadonlySet<string> = new Set([
|
|
78
|
+
'Gesture',
|
|
79
|
+
'GestureObjects',
|
|
80
|
+
'State',
|
|
81
|
+
'Directions',
|
|
82
|
+
'Extrapolation',
|
|
83
|
+
'Extrapolate',
|
|
84
|
+
'Easing',
|
|
85
|
+
'ReduceMotion',
|
|
86
|
+
'KeyframeRegistry',
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
/** Per-module policy: an explicit allow-list, or `'all'` named exports. */
|
|
90
|
+
export type WrapPolicy = 'all' | ReadonlySet<string>
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default module → wrap policy. react-native is allow-listed (mixed
|
|
94
|
+
* exports); the rest are component-only packages → `'all'`. Only modules
|
|
95
|
+
* the project has installed are ever hit (you can't import from a missing
|
|
96
|
+
* package), so listing optional peers is free.
|
|
97
|
+
*/
|
|
98
|
+
export const DEFAULT_WRAP_MODULES: ReadonlyMap<string, WrapPolicy> = new Map<string, WrapPolicy>([
|
|
99
|
+
['react-native', REACT_NATIVE_COMPONENTS],
|
|
100
|
+
['react-native-reanimated', 'all'],
|
|
101
|
+
['react-native-svg', 'all'],
|
|
102
|
+
['react-native-gesture-handler', 'all'],
|
|
103
|
+
['react-native-safe-area-context', 'all'],
|
|
104
|
+
['expo-linear-gradient', 'all'],
|
|
105
|
+
['expo-image', 'all'],
|
|
106
|
+
['expo-blur', 'all'],
|
|
107
|
+
['expo-symbols', 'all'],
|
|
108
|
+
['@shopify/flash-list', 'all'],
|
|
109
|
+
['@shopify/react-native-skia', 'all'],
|
|
110
|
+
['lottie-react-native', 'all'],
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether a named import from a wrap-module should be wrapped.
|
|
115
|
+
*
|
|
116
|
+
* Explicit allow-lists (react-native) match by exact name. The `'all'`
|
|
117
|
+
* policy wraps only component-style names — PascalCase, not a React
|
|
118
|
+
* context (`*Context`), and not a known non-component export. This is
|
|
119
|
+
* what stops `useSafeAreaInsets` (a hook) from being wrapped into a
|
|
120
|
+
* component and crashing when called.
|
|
121
|
+
* @param policy The module's wrap policy.
|
|
122
|
+
* @param importedName The exported name being imported.
|
|
123
|
+
* @returns True when the name is a component to wrap.
|
|
124
|
+
*/
|
|
125
|
+
function shouldWrap(policy: WrapPolicy, importedName: string): boolean {
|
|
126
|
+
if (policy !== 'all') return policy.has(importedName)
|
|
127
|
+
if (!/^[A-Z]/.test(importedName)) return false
|
|
128
|
+
if (importedName.endsWith('Context')) return false
|
|
129
|
+
return !NON_COMPONENT_EXPORTS.has(importedName)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Merge user-supplied wrap modules onto the defaults — a bare module name
|
|
134
|
+
* adopts the `'all'` policy.
|
|
135
|
+
* @param extra User module specifiers (or undefined).
|
|
136
|
+
* @returns Effective module → policy map.
|
|
137
|
+
*/
|
|
138
|
+
export function buildWrapModules(extra?: readonly string[]): ReadonlyMap<string, WrapPolicy> {
|
|
139
|
+
if (!extra || extra.length === 0) return DEFAULT_WRAP_MODULES
|
|
140
|
+
const merged = new Map<string, WrapPolicy>(DEFAULT_WRAP_MODULES)
|
|
141
|
+
for (const moduleName of extra) if (!merged.has(moduleName)) merged.set(moduleName, 'all')
|
|
142
|
+
return merged
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The `imported` name of an import specifier (`import { a as b }` → `'a'`).
|
|
147
|
+
* @param specifier Named import specifier.
|
|
148
|
+
* @returns The exported name.
|
|
149
|
+
*/
|
|
150
|
+
function importedNameOf(specifier: t.ImportSpecifier): string {
|
|
151
|
+
return t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build `const Local = <wrapper>(alias)` and rebind the specifier's local
|
|
156
|
+
* to `alias` in place.
|
|
157
|
+
* @param specifier The import specifier to rebind.
|
|
158
|
+
* @param alias The `_rnwN` alias to bind the original import to.
|
|
159
|
+
* @param wrapper The runtime helper local (`_rnwWrap` / `_rnwWrapNs`).
|
|
160
|
+
* @returns The wrap declaration.
|
|
161
|
+
*/
|
|
162
|
+
function makeWrapDecl(
|
|
163
|
+
specifier: t.ImportSpecifier | t.ImportDefaultSpecifier | t.ImportNamespaceSpecifier,
|
|
164
|
+
alias: string,
|
|
165
|
+
wrapper: string,
|
|
166
|
+
): t.VariableDeclaration {
|
|
167
|
+
const { name: localName } = specifier.local
|
|
168
|
+
specifier.local = t.identifier(alias)
|
|
169
|
+
return t.variableDeclaration('const', [
|
|
170
|
+
t.variableDeclarator(t.identifier(localName), t.callExpression(t.identifier(wrapper), [t.identifier(alias)])),
|
|
171
|
+
])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Rewrite one import declaration's wrappable specifiers, aliasing each to
|
|
176
|
+
* `_rnw<N>` in place:
|
|
177
|
+
* - named (`{ View }`) → `const View = wrap(_rnwN)` (per policy),
|
|
178
|
+
* - namespace (`* as Animated`) → `const Animated = wrapNamespace(_rnwN)`,
|
|
179
|
+
* - default → `wrapNamespace` for {@link NAMESPACE_DEFAULT_MODULES}
|
|
180
|
+
* (reanimated's `Animated`), else `wrap` (a plain default component).
|
|
181
|
+
* @param node Import declaration to inspect.
|
|
182
|
+
* @param policy The module's wrap policy.
|
|
183
|
+
* @param counter Next alias index (caller-threaded for uniqueness).
|
|
184
|
+
* @returns New wrap declarations, advanced counter, and whether any
|
|
185
|
+
* binding used `wrapNamespace`.
|
|
186
|
+
*/
|
|
187
|
+
function wrapSpecifiers(
|
|
188
|
+
node: t.ImportDeclaration,
|
|
189
|
+
policy: WrapPolicy,
|
|
190
|
+
counter: number,
|
|
191
|
+
): { decls: t.VariableDeclaration[]; counter: number; usesNamespace: boolean } {
|
|
192
|
+
const decls: t.VariableDeclaration[] = []
|
|
193
|
+
const moduleName = node.source.value
|
|
194
|
+
let next = counter
|
|
195
|
+
let usesNamespace = false
|
|
196
|
+
for (const specifier of node.specifiers) {
|
|
197
|
+
if (t.isImportSpecifier(specifier)) {
|
|
198
|
+
if (!shouldWrap(policy, importedNameOf(specifier))) continue
|
|
199
|
+
decls.push(makeWrapDecl(specifier, `_rnw${next}`, WRAP_LOCAL))
|
|
200
|
+
next += 1
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
const isNamespace = t.isImportNamespaceSpecifier(specifier) || NAMESPACE_DEFAULT_MODULES.has(moduleName)
|
|
204
|
+
const wrapper = isNamespace ? WRAP_NS_LOCAL : WRAP_LOCAL
|
|
205
|
+
decls.push(makeWrapDecl(specifier, `_rnw${next}`, wrapper))
|
|
206
|
+
next += 1
|
|
207
|
+
if (isNamespace) usesNamespace = true
|
|
208
|
+
}
|
|
209
|
+
return { decls, counter: next, usesNamespace }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Insert the `wrap` import at the top and the `const X = wrap(_rnwN)`
|
|
214
|
+
* declarations AFTER every import. The consts reference the aliased
|
|
215
|
+
* binding `_rnwN`, and in Metro's real transform (CommonJS interop + the
|
|
216
|
+
* reanimated worklets plugin) a const placed above its source import
|
|
217
|
+
* evaluates before the binding initialises → `ReferenceError: _rnw0
|
|
218
|
+
* doesn't exist`. ESM-only hoisting would mask this; the bundle does not.
|
|
219
|
+
* @param ast Parsed Babel file (mutated).
|
|
220
|
+
* @param wrapDecls The wrap declarations to place.
|
|
221
|
+
* @param usesNamespace Whether any binding used `wrapNamespace`.
|
|
222
|
+
*/
|
|
223
|
+
function placeWrapDecls(ast: File, wrapDecls: readonly t.VariableDeclaration[], usesNamespace: boolean): void {
|
|
224
|
+
const specifiers = [t.importSpecifier(t.identifier(WRAP_LOCAL), t.identifier('wrap'))]
|
|
225
|
+
if (usesNamespace) specifiers.push(t.importSpecifier(t.identifier(WRAP_NS_LOCAL), t.identifier('wrapNamespace')))
|
|
226
|
+
ast.program.body.unshift(t.importDeclaration(specifiers, t.stringLiteral(RUNTIME_MODULE)))
|
|
227
|
+
let afterImports = 0
|
|
228
|
+
for (let index = 0; index < ast.program.body.length; index += 1) {
|
|
229
|
+
if (t.isImportDeclaration(ast.program.body[index])) afterImports = index + 1
|
|
230
|
+
}
|
|
231
|
+
ast.program.body.splice(afterImports, 0, ...wrapDecls)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Rewrite component imports from the configured wrap-modules into
|
|
236
|
+
* `wrap()`-ed bindings, in place. Injects the `wrap` import once when any
|
|
237
|
+
* binding was rewritten.
|
|
238
|
+
* @param ast Parsed Babel file (mutated).
|
|
239
|
+
* @param modules Effective module → policy map.
|
|
240
|
+
* @returns True when at least one import was wrapped.
|
|
241
|
+
*/
|
|
242
|
+
export function rewriteWrapImports(ast: File, modules: ReadonlyMap<string, WrapPolicy>): boolean {
|
|
243
|
+
const wrapDecls: t.VariableDeclaration[] = []
|
|
244
|
+
let counter = 0
|
|
245
|
+
let usesNamespace = false
|
|
246
|
+
|
|
247
|
+
for (const node of ast.program.body) {
|
|
248
|
+
if (!t.isImportDeclaration(node)) continue
|
|
249
|
+
const policy = modules.get(node.source.value)
|
|
250
|
+
if (!policy) continue
|
|
251
|
+
const { decls, counter: nextCounter, usesNamespace: ns } = wrapSpecifiers(node, policy, counter)
|
|
252
|
+
counter = nextCounter
|
|
253
|
+
usesNamespace = usesNamespace || ns
|
|
254
|
+
wrapDecls.push(...decls)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (wrapDecls.length === 0) return false
|
|
258
|
+
placeWrapDecls(ast, wrapDecls, usesNamespace)
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
@@ -96,23 +96,6 @@ export function useRnwind(): RnwindState {
|
|
|
96
96
|
return useContext(RnwindContext)
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/**
|
|
100
|
-
* Internal context hook the babel transformer injects at the top of
|
|
101
|
-
* every rewritten component as `const _t = useR_()`. Same body as the
|
|
102
|
-
* public {@link useRnwind} — exposed under a `use*`-prefixed name so
|
|
103
|
-
* react-refresh's babel plugin (which only tracks call-sites whose
|
|
104
|
-
* identifier matches `^use[A-Z]`) folds it into each component's
|
|
105
|
-
* fast-refresh signature. Without that prefix the signature stayed
|
|
106
|
-
* stable across transformer changes; HMR then preserved fiber state
|
|
107
|
-
* while the rendered hook list shifted, surfacing as "change in the
|
|
108
|
-
* order of Hooks" runtime errors. Trailing underscore keeps it
|
|
109
|
-
* visually distinct from the user-facing `useRnwind`.
|
|
110
|
-
* @returns Active rnwind state.
|
|
111
|
-
*/
|
|
112
|
-
export function useR_(): RnwindState {
|
|
113
|
-
return useContext(RnwindContext)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
99
|
/**
|
|
117
100
|
* Provider for rnwind's full runtime state. fontScale + windowWidth
|
|
118
101
|
* come from `useWindowDimensions()` so they react to OS-level
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { useRnwind } from '../components/rnwind-provider'
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from '../resolve'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Resolve a className to a React Native `style` value against the active
|
|
6
|
+
* rnwind context (scheme, insets, fontScale, breakpoint). Molecule-fast:
|
|
7
|
+
* a literal className the scanner saw returns a pre-merged object by
|
|
8
|
+
* reference; anything else falls back to per-atom resolution. The escape
|
|
9
|
+
* hatch for custom components that hold a `className` prop:
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function Card({ className, style, ...rest }) {
|
|
13
|
+
* return <RNView style={useCss(className, style)} {...rest} />
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
* @param className Raw className string.
|
|
17
|
+
* @param userStyle Optional caller-supplied style appended last (wins).
|
|
18
|
+
* @returns RN `style` value (a single object or an array).
|
|
13
19
|
*/
|
|
14
|
-
export function useCss(className?: string |
|
|
15
|
-
return
|
|
20
|
+
export function useCss(className?: string | null, userStyle?: unknown): unknown {
|
|
21
|
+
return resolve(className, useRnwind(), userStyle).style
|
|
16
22
|
}
|