rnwind 0.0.2 → 0.0.3
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 +53 -24
- package/lib/cjs/core/parser/color.cjs.map +1 -1
- package/lib/cjs/core/parser/layout-dispatcher.cjs +20 -0
- package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -1
- package/lib/cjs/core/parser/length.cjs +20 -6
- package/lib/cjs/core/parser/length.cjs.map +1 -1
- package/lib/cjs/core/parser/length.d.ts +6 -3
- package/lib/cjs/core/parser/shorthand.cjs +37 -5
- package/lib/cjs/core/parser/shorthand.cjs.map +1 -1
- package/lib/cjs/core/parser/shorthand.d.ts +11 -5
- package/lib/cjs/core/parser/theme-vars.cjs +53 -0
- package/lib/cjs/core/parser/theme-vars.cjs.map +1 -1
- package/lib/cjs/core/parser/theme-vars.d.ts +21 -0
- package/lib/cjs/core/parser/tokens.cjs +183 -1
- package/lib/cjs/core/parser/tokens.cjs.map +1 -1
- package/lib/cjs/core/parser/tw-parser.cjs +140 -27
- package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
- package/lib/cjs/core/parser/tw-parser.d.ts +21 -5
- package/lib/cjs/core/parser/typography-dispatcher.cjs +16 -1
- package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -1
- package/lib/cjs/core/style-builder/build-style.cjs +73 -26
- package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
- package/lib/cjs/metro/state.cjs +52 -2
- package/lib/cjs/metro/state.cjs.map +1 -1
- package/lib/cjs/metro/state.d.ts +17 -1
- package/lib/cjs/metro/transform-ast.cjs +238 -21
- package/lib/cjs/metro/transform-ast.cjs.map +1 -1
- package/lib/cjs/metro/transform-ast.d.ts +15 -0
- package/lib/cjs/metro/transformer.cjs +29 -2
- package/lib/cjs/metro/transformer.cjs.map +1 -1
- package/lib/cjs/metro/with-config.cjs +1 -1
- package/lib/cjs/metro/with-config.cjs.map +1 -1
- package/lib/cjs/metro/with-config.d.ts +22 -0
- package/lib/esm/core/parser/color.mjs +53 -24
- package/lib/esm/core/parser/color.mjs.map +1 -1
- package/lib/esm/core/parser/layout-dispatcher.mjs +20 -0
- package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
- package/lib/esm/core/parser/length.d.ts +6 -3
- package/lib/esm/core/parser/length.mjs +20 -6
- package/lib/esm/core/parser/length.mjs.map +1 -1
- package/lib/esm/core/parser/shorthand.d.ts +11 -5
- package/lib/esm/core/parser/shorthand.mjs +37 -5
- package/lib/esm/core/parser/shorthand.mjs.map +1 -1
- package/lib/esm/core/parser/theme-vars.d.ts +21 -0
- package/lib/esm/core/parser/theme-vars.mjs +53 -1
- package/lib/esm/core/parser/theme-vars.mjs.map +1 -1
- package/lib/esm/core/parser/tokens.mjs +183 -1
- package/lib/esm/core/parser/tokens.mjs.map +1 -1
- package/lib/esm/core/parser/tw-parser.d.ts +21 -5
- package/lib/esm/core/parser/tw-parser.mjs +141 -28
- package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
- package/lib/esm/core/parser/typography-dispatcher.mjs +16 -1
- package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
- package/lib/esm/core/style-builder/build-style.mjs +73 -26
- package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
- package/lib/esm/metro/state.d.ts +17 -1
- package/lib/esm/metro/state.mjs +51 -3
- package/lib/esm/metro/state.mjs.map +1 -1
- package/lib/esm/metro/transform-ast.d.ts +15 -0
- package/lib/esm/metro/transform-ast.mjs +238 -21
- package/lib/esm/metro/transform-ast.mjs.map +1 -1
- package/lib/esm/metro/transformer.mjs +30 -3
- package/lib/esm/metro/transformer.mjs.map +1 -1
- package/lib/esm/metro/with-config.d.ts +22 -0
- package/lib/esm/metro/with-config.mjs +1 -1
- package/lib/esm/metro/with-config.mjs.map +1 -1
- package/package.json +2 -1
- package/src/core/parser/color.ts +52 -18
- package/src/core/parser/layout-dispatcher.ts +19 -0
- package/src/core/parser/length.ts +20 -6
- package/src/core/parser/shorthand.ts +35 -5
- package/src/core/parser/theme-vars.ts +53 -0
- package/src/core/parser/tokens.ts +171 -1
- package/src/core/parser/tw-parser.ts +147 -28
- package/src/core/parser/typography-dispatcher.ts +15 -1
- package/src/core/style-builder/build-style.ts +84 -26
- package/src/metro/state.ts +49 -1
- package/src/metro/transform-ast.ts +249 -18
- package/src/metro/transformer.ts +28 -3
- package/src/metro/with-config.ts +23 -1
|
@@ -83,6 +83,159 @@ const NON_INTERACTIVE_HOST_TAGS = new Set([
|
|
|
83
83
|
* on top, never replacing this.
|
|
84
84
|
*/
|
|
85
85
|
const DEFAULT_CLASSNAME_PREFIXES = ['contentContainer'];
|
|
86
|
+
/**
|
|
87
|
+
* Module specifiers whose JSX exports are "host-like" — they consume
|
|
88
|
+
* `style` directly (and own no opaque component logic that depends on
|
|
89
|
+
* receiving the raw `className` string). For tags imported from these
|
|
90
|
+
* sources the transformer rewrites `className="…"` → `style={lookupCss(…)}`
|
|
91
|
+
* at build time, so the runtime cost is zero.
|
|
92
|
+
*
|
|
93
|
+
* For tags from ANY other source the transformer leaves `className`
|
|
94
|
+
* alone — the importing component receives the raw string and decides
|
|
95
|
+
* what to do with it (forward to an inner host, reshape, route a slice
|
|
96
|
+
* to `contentContainerStyle`, …). This is what makes patterns like
|
|
97
|
+
* `<MyButton className="px-4 bg-primary" />` work without rnwind
|
|
98
|
+
* stealing the prop before the component sees it.
|
|
99
|
+
*
|
|
100
|
+
* Users extend the list via `withRnwindConfig`'s `hostSources` option.
|
|
101
|
+
*/
|
|
102
|
+
const DEFAULT_HOST_SOURCES = [
|
|
103
|
+
'react-native',
|
|
104
|
+
'react-native-reanimated',
|
|
105
|
+
'react-native-svg',
|
|
106
|
+
'react-native-gesture-handler',
|
|
107
|
+
'react-native-safe-area-context',
|
|
108
|
+
'expo-linear-gradient',
|
|
109
|
+
'expo-image',
|
|
110
|
+
'expo-blur',
|
|
111
|
+
'expo-symbols',
|
|
112
|
+
'@shopify/flash-list',
|
|
113
|
+
'@shopify/react-native-skia',
|
|
114
|
+
'lottie-react-native',
|
|
115
|
+
];
|
|
116
|
+
/**
|
|
117
|
+
* Whether a JSX tag name is lowercase. Lowercase tags don't appear in
|
|
118
|
+
* native React Native userland — but if one shows up (web target via
|
|
119
|
+
* `react-native-web`, mdx, etc.) treat it as a host so the rewrite
|
|
120
|
+
* engages instead of silently dropping the className.
|
|
121
|
+
* @param name JSX tag identifier text.
|
|
122
|
+
* @returns True for ASCII-lowercase first character.
|
|
123
|
+
*/
|
|
124
|
+
function isLowercaseTag(name) {
|
|
125
|
+
const code = name.codePointAt(0);
|
|
126
|
+
return code !== undefined && code >= 97 && code <= 122;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Walk a JSX opening element's tag name node into a dotted string
|
|
130
|
+
* (`Animated.View`, `Foo.Bar.Baz`). Returns `null` for namespaced names
|
|
131
|
+
* (`<svg:rect>` — invalid in RN; we skip them).
|
|
132
|
+
* @param name JSXOpeningElement name node.
|
|
133
|
+
* @returns Dotted tag text, or null.
|
|
134
|
+
*/
|
|
135
|
+
function jsxTagText(name) {
|
|
136
|
+
if (t.isJSXIdentifier(name))
|
|
137
|
+
return name.name;
|
|
138
|
+
if (t.isJSXMemberExpression(name)) {
|
|
139
|
+
const left = jsxTagText(name.object);
|
|
140
|
+
return left ? `${left}.${name.property.name}` : null;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Leftmost identifier of a (possibly dotted) tag — used to look up its import source.
|
|
146
|
+
* @param tagText
|
|
147
|
+
*/
|
|
148
|
+
function leftmostIdentifier(tagText) {
|
|
149
|
+
const dot = tagText.indexOf('.');
|
|
150
|
+
return dot === -1 ? tagText : tagText.slice(0, dot);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build the per-file host lookup. Walks every `import` declaration once
|
|
154
|
+
* to map every locally-bound name to its source module. A JSX tag is a
|
|
155
|
+
* host when:
|
|
156
|
+
* 1. its full text matches an entry in `extraHostComponents` (verbatim),
|
|
157
|
+
* 2. its leftmost identifier was imported from a `hostSources` module,
|
|
158
|
+
* 3. it's a lowercase tag (web targets, defensive).
|
|
159
|
+
*
|
|
160
|
+
* Anything else is custom and the transformer leaves its className alone.
|
|
161
|
+
* @param ast File AST.
|
|
162
|
+
* @param extraHostSources User-supplied additional host module specifiers.
|
|
163
|
+
* @param extraHostComponents User-supplied additional host component names.
|
|
164
|
+
* @returns Lookup callback.
|
|
165
|
+
*/
|
|
166
|
+
function buildHostLookup(ast, extraHostSources, extraHostComponents) {
|
|
167
|
+
const importSourceByLocal = new Map();
|
|
168
|
+
for (const node of ast.program.body) {
|
|
169
|
+
if (!t.isImportDeclaration(node))
|
|
170
|
+
continue;
|
|
171
|
+
const source = node.source.value;
|
|
172
|
+
for (const spec of node.specifiers) {
|
|
173
|
+
if (t.isImportDefaultSpecifier(spec) || t.isImportSpecifier(spec) || t.isImportNamespaceSpecifier(spec)) {
|
|
174
|
+
importSourceByLocal.set(spec.local.name, source);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Recognise module-local host aliases — common pattern in React Native:
|
|
179
|
+
// const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
|
|
180
|
+
// const Animated = createAnimatedComponent(View)
|
|
181
|
+
// The local binding wraps a host underneath so its className must still
|
|
182
|
+
// be rewritten. Without this every `<AnimatedTextInput className="…" />`
|
|
183
|
+
// site looked custom and the className silently dropped.
|
|
184
|
+
const localHostAliases = collectCreateAnimatedComponentAliases(ast);
|
|
185
|
+
const hostSources = new Set([...DEFAULT_HOST_SOURCES, ...(extraHostSources ?? [])]);
|
|
186
|
+
const hostComponents = new Set(extraHostComponents);
|
|
187
|
+
return (tagText) => {
|
|
188
|
+
if (isLowercaseTag(tagText))
|
|
189
|
+
return true;
|
|
190
|
+
if (hostComponents.has(tagText))
|
|
191
|
+
return true;
|
|
192
|
+
const left = leftmostIdentifier(tagText);
|
|
193
|
+
if (localHostAliases.has(left))
|
|
194
|
+
return true;
|
|
195
|
+
const source = importSourceByLocal.get(left);
|
|
196
|
+
return source !== undefined && hostSources.has(source);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Walk top-level `const X = createAnimatedComponent(Y)` /
|
|
201
|
+
* `Animated.createAnimatedComponent(Y)` declarations and return the set
|
|
202
|
+
* of local names so the host-lookup recognises them. Reanimated +
|
|
203
|
+
* RN-core `Animated.createAnimatedComponent` are the only creators in
|
|
204
|
+
* common use; matching by callee-name covers both shapes without
|
|
205
|
+
* needing import-source resolution.
|
|
206
|
+
* @param ast File AST.
|
|
207
|
+
* @returns Set of locally-bound names that wrap a host component.
|
|
208
|
+
*/
|
|
209
|
+
function collectCreateAnimatedComponentAliases(ast) {
|
|
210
|
+
const aliases = new Set();
|
|
211
|
+
for (const node of ast.program.body) {
|
|
212
|
+
const declaration = t.isExportNamedDeclaration(node) ? node.declaration : node;
|
|
213
|
+
if (!t.isVariableDeclaration(declaration))
|
|
214
|
+
continue;
|
|
215
|
+
for (const decl of declaration.declarations) {
|
|
216
|
+
if (!t.isIdentifier(decl.id) || !decl.init)
|
|
217
|
+
continue;
|
|
218
|
+
if (!isCreateAnimatedComponentCall(decl.init))
|
|
219
|
+
continue;
|
|
220
|
+
aliases.add(decl.id.name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return aliases;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* True for `createAnimatedComponent(...)` and `<x>.createAnimatedComponent(...)` calls.
|
|
227
|
+
* @param expr
|
|
228
|
+
*/
|
|
229
|
+
function isCreateAnimatedComponentCall(expr) {
|
|
230
|
+
if (!t.isCallExpression(expr))
|
|
231
|
+
return false;
|
|
232
|
+
const { callee } = expr;
|
|
233
|
+
if (t.isIdentifier(callee) && callee.name === 'createAnimatedComponent')
|
|
234
|
+
return true;
|
|
235
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property) && callee.property.name === 'createAnimatedComponent')
|
|
236
|
+
return true;
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
86
239
|
/**
|
|
87
240
|
* Mutate an already-parsed Babel AST in place:
|
|
88
241
|
* - Rewrite every JSX `className="…"` / `className={expr}` attribute to
|
|
@@ -106,6 +259,7 @@ function transformAst(ast, options) {
|
|
|
106
259
|
const literals = [];
|
|
107
260
|
const prefixSet = buildPrefixSet(options.classNamePrefixes);
|
|
108
261
|
const hapticHoister = createHapticHoister();
|
|
262
|
+
const isHostTag = buildHostLookup(ast, options.hostSources, options.hostComponents);
|
|
109
263
|
const rewriteCtx = {
|
|
110
264
|
needsInsets: false,
|
|
111
265
|
gradientAtoms: options.gradientAtoms ?? EMPTY_GRADIENT_ATOMS,
|
|
@@ -118,6 +272,15 @@ function transformAst(ast, options) {
|
|
|
118
272
|
let touched = false;
|
|
119
273
|
let usedLookupCss = false;
|
|
120
274
|
let usedInteractiveBox = false;
|
|
275
|
+
// Per-element host classification, captured the first time we see each
|
|
276
|
+
// JSXOpeningElement. Necessary because the InteractiveBox wrap mutates
|
|
277
|
+
// `parent.name` in-place from the original tag → `_ib`; sibling
|
|
278
|
+
// attributes processed AFTER the swap would otherwise re-classify off
|
|
279
|
+
// the now-meaningless `_ib` name and skip rewrites they should do
|
|
280
|
+
// (e.g. `contentContainerClassName` next to an `active:` className on
|
|
281
|
+
// the same `<ScrollView>`).
|
|
282
|
+
const customElements = new WeakSet();
|
|
283
|
+
const classifiedElements = new WeakSet();
|
|
121
284
|
traverse(ast, {
|
|
122
285
|
JSXAttribute(attributePath) {
|
|
123
286
|
const { node } = attributePath;
|
|
@@ -126,6 +289,24 @@ function transformAst(ast, options) {
|
|
|
126
289
|
const target = classifyAttributeName(node.name.name, prefixSet);
|
|
127
290
|
if (!target)
|
|
128
291
|
return;
|
|
292
|
+
// Skip className rewrite when the parent JSX tag is a custom
|
|
293
|
+
// component (not imported from a known host source). Custom
|
|
294
|
+
// components own their `className` prop — the transformer would
|
|
295
|
+
// steal the string from under them otherwise. The literal still
|
|
296
|
+
// appears in source text, so oxide still discovers its atoms via
|
|
297
|
+
// the project scan; the inner host that ultimately consumes the
|
|
298
|
+
// forwarded className gets rewritten by ITS file's transform.
|
|
299
|
+
const { parent } = attributePath;
|
|
300
|
+
if (t.isJSXOpeningElement(parent)) {
|
|
301
|
+
if (!classifiedElements.has(parent)) {
|
|
302
|
+
classifiedElements.add(parent);
|
|
303
|
+
const tagText = jsxTagText(parent.name);
|
|
304
|
+
if (tagText !== null && !isHostTag(tagText))
|
|
305
|
+
customElements.add(parent);
|
|
306
|
+
}
|
|
307
|
+
if (customElements.has(parent))
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
129
310
|
const rewritten = rewriteClassNameAttribute(attributePath, hoister, literals, rewriteCtx, target);
|
|
130
311
|
if (!rewritten)
|
|
131
312
|
return;
|
|
@@ -226,12 +407,20 @@ function rewriteClassNameAttribute(attributePath, hoister, literals, rewriteCtx,
|
|
|
226
407
|
const { value } = node;
|
|
227
408
|
if (!value)
|
|
228
409
|
return null;
|
|
229
|
-
const buildResult = buildFirstArgument(value, hoister, literals, rewriteCtx);
|
|
230
|
-
if (!buildResult)
|
|
231
|
-
return null;
|
|
232
410
|
const { parent } = attributePath;
|
|
233
411
|
if (!t.isJSXOpeningElement(parent))
|
|
234
412
|
return null;
|
|
413
|
+
// The rewrite emits references to `_t` (the `useR_()` binding). That
|
|
414
|
+
// binding can only live in a component body — so if this JSX site has
|
|
415
|
+
// no enclosing component (e.g. a top-level `const renderItem = (...) =>
|
|
416
|
+
// <View className=.../>` helper), bail and leave className untouched
|
|
417
|
+
// rather than emit a dangling `_t`. Checked BEFORE any mutation
|
|
418
|
+
// (hoist, sibling-style drop) so a bail leaves the AST pristine.
|
|
419
|
+
if (!hasComponentBody(attributePath))
|
|
420
|
+
return null;
|
|
421
|
+
const buildResult = buildFirstArgument(value, hoister, literals, rewriteCtx);
|
|
422
|
+
if (!buildResult)
|
|
423
|
+
return null;
|
|
235
424
|
const userStyleExpr = extractAndDropSiblingStyle(parent, target.styleProp);
|
|
236
425
|
// Single context binding `_t = _r()` — carries scheme, fontScale,
|
|
237
426
|
// insets together so React tracks all three as render deps via one
|
|
@@ -280,29 +469,36 @@ function applyDerivedJsxAttributes(attributePath, parent, result, target, rewrit
|
|
|
280
469
|
injectEventHapticHandlers(attributePath, parent, result.eventHaptics, rewriteCtx);
|
|
281
470
|
}
|
|
282
471
|
/**
|
|
283
|
-
* Splice
|
|
284
|
-
* `end={…}`
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
472
|
+
* Splice class-derived JSX attributes (`colors={…}` / `start={…}` /
|
|
473
|
+
* `end={…}` for gradients; `numberOfLines=` / `ellipsizeMode=` for
|
|
474
|
+
* truncate) into a JSXOpeningElement's attribute list — but only when
|
|
475
|
+
* the developer hasn't already written that attribute themselves.
|
|
476
|
+
*
|
|
477
|
+
* **User attrs always win.** If a hand-written `colors={USER}` is
|
|
478
|
+
* present, the class-derived hoist is dropped on the floor for that
|
|
479
|
+
* specific attribute. Same rule for every derived prop, applied
|
|
480
|
+
* per-attribute so the user can override one slot (e.g. `start={…}`)
|
|
481
|
+
* and let rnwind fill in the others. Documented in
|
|
482
|
+
* `docs/architecture.md`.
|
|
289
483
|
* @param opening JSXOpeningElement to mutate.
|
|
290
|
-
* @param
|
|
291
|
-
* @param gradientAttributes
|
|
484
|
+
* @param gradientAttributes Freshly built JSX attributes.
|
|
292
485
|
*/
|
|
293
486
|
function appendGradientAttributes(opening, gradientAttributes) {
|
|
294
|
-
const
|
|
295
|
-
for (const attribute of
|
|
296
|
-
if (t.isJSXIdentifier(attribute.name))
|
|
297
|
-
names.add(attribute.name.name);
|
|
298
|
-
opening.attributes = opening.attributes.filter((attribute) => {
|
|
487
|
+
const userAttributeNames = new Set();
|
|
488
|
+
for (const attribute of opening.attributes) {
|
|
299
489
|
if (!t.isJSXAttribute(attribute))
|
|
300
|
-
|
|
490
|
+
continue;
|
|
301
491
|
if (!t.isJSXIdentifier(attribute.name))
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
492
|
+
continue;
|
|
493
|
+
userAttributeNames.add(attribute.name.name);
|
|
494
|
+
}
|
|
495
|
+
for (const derived of gradientAttributes) {
|
|
496
|
+
if (!t.isJSXIdentifier(derived.name))
|
|
497
|
+
continue;
|
|
498
|
+
if (userAttributeNames.has(derived.name.name))
|
|
499
|
+
continue;
|
|
500
|
+
opening.attributes.push(derived);
|
|
501
|
+
}
|
|
306
502
|
}
|
|
307
503
|
/**
|
|
308
504
|
* Whether a JSX tag can fire press / focus events. Pure host-tag check
|
|
@@ -957,6 +1153,27 @@ function injectContextHook(path) {
|
|
|
957
1153
|
componentBody.unshiftContainer('body', declaration);
|
|
958
1154
|
return CONTEXT_BINDING;
|
|
959
1155
|
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Whether `path` sits inside a recognised function component — i.e.
|
|
1158
|
+
* {@link injectContextHook} would find a body to host `const _t =
|
|
1159
|
+
* useR_()`. Pure lookup that mirrors {@link findComponentBody}'s walk
|
|
1160
|
+
* but performs NO body promotion, so a caller can bail before mutating
|
|
1161
|
+
* when the answer is no.
|
|
1162
|
+
* @param path Rewrite-site path.
|
|
1163
|
+
* @returns True when an enclosing component function exists.
|
|
1164
|
+
*/
|
|
1165
|
+
function hasComponentBody(path) {
|
|
1166
|
+
let current = path;
|
|
1167
|
+
while (current) {
|
|
1168
|
+
const fn = current.findParent((parent) => parent.isFunction());
|
|
1169
|
+
if (!fn)
|
|
1170
|
+
return false;
|
|
1171
|
+
if (isComponentFunction(fn))
|
|
1172
|
+
return true;
|
|
1173
|
+
current = fn;
|
|
1174
|
+
}
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
960
1177
|
/**
|
|
961
1178
|
* Walk up from `path` to the nearest recognised function component.
|
|
962
1179
|
* Accepts:
|