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.
Files changed (80) hide show
  1. package/lib/cjs/core/parser/color.cjs +53 -24
  2. package/lib/cjs/core/parser/color.cjs.map +1 -1
  3. package/lib/cjs/core/parser/layout-dispatcher.cjs +20 -0
  4. package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -1
  5. package/lib/cjs/core/parser/length.cjs +20 -6
  6. package/lib/cjs/core/parser/length.cjs.map +1 -1
  7. package/lib/cjs/core/parser/length.d.ts +6 -3
  8. package/lib/cjs/core/parser/shorthand.cjs +37 -5
  9. package/lib/cjs/core/parser/shorthand.cjs.map +1 -1
  10. package/lib/cjs/core/parser/shorthand.d.ts +11 -5
  11. package/lib/cjs/core/parser/theme-vars.cjs +53 -0
  12. package/lib/cjs/core/parser/theme-vars.cjs.map +1 -1
  13. package/lib/cjs/core/parser/theme-vars.d.ts +21 -0
  14. package/lib/cjs/core/parser/tokens.cjs +183 -1
  15. package/lib/cjs/core/parser/tokens.cjs.map +1 -1
  16. package/lib/cjs/core/parser/tw-parser.cjs +140 -27
  17. package/lib/cjs/core/parser/tw-parser.cjs.map +1 -1
  18. package/lib/cjs/core/parser/tw-parser.d.ts +21 -5
  19. package/lib/cjs/core/parser/typography-dispatcher.cjs +16 -1
  20. package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -1
  21. package/lib/cjs/core/style-builder/build-style.cjs +73 -26
  22. package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
  23. package/lib/cjs/metro/state.cjs +52 -2
  24. package/lib/cjs/metro/state.cjs.map +1 -1
  25. package/lib/cjs/metro/state.d.ts +17 -1
  26. package/lib/cjs/metro/transform-ast.cjs +238 -21
  27. package/lib/cjs/metro/transform-ast.cjs.map +1 -1
  28. package/lib/cjs/metro/transform-ast.d.ts +15 -0
  29. package/lib/cjs/metro/transformer.cjs +29 -2
  30. package/lib/cjs/metro/transformer.cjs.map +1 -1
  31. package/lib/cjs/metro/with-config.cjs +1 -1
  32. package/lib/cjs/metro/with-config.cjs.map +1 -1
  33. package/lib/cjs/metro/with-config.d.ts +22 -0
  34. package/lib/esm/core/parser/color.mjs +53 -24
  35. package/lib/esm/core/parser/color.mjs.map +1 -1
  36. package/lib/esm/core/parser/layout-dispatcher.mjs +20 -0
  37. package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -1
  38. package/lib/esm/core/parser/length.d.ts +6 -3
  39. package/lib/esm/core/parser/length.mjs +20 -6
  40. package/lib/esm/core/parser/length.mjs.map +1 -1
  41. package/lib/esm/core/parser/shorthand.d.ts +11 -5
  42. package/lib/esm/core/parser/shorthand.mjs +37 -5
  43. package/lib/esm/core/parser/shorthand.mjs.map +1 -1
  44. package/lib/esm/core/parser/theme-vars.d.ts +21 -0
  45. package/lib/esm/core/parser/theme-vars.mjs +53 -1
  46. package/lib/esm/core/parser/theme-vars.mjs.map +1 -1
  47. package/lib/esm/core/parser/tokens.mjs +183 -1
  48. package/lib/esm/core/parser/tokens.mjs.map +1 -1
  49. package/lib/esm/core/parser/tw-parser.d.ts +21 -5
  50. package/lib/esm/core/parser/tw-parser.mjs +141 -28
  51. package/lib/esm/core/parser/tw-parser.mjs.map +1 -1
  52. package/lib/esm/core/parser/typography-dispatcher.mjs +16 -1
  53. package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -1
  54. package/lib/esm/core/style-builder/build-style.mjs +73 -26
  55. package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
  56. package/lib/esm/metro/state.d.ts +17 -1
  57. package/lib/esm/metro/state.mjs +51 -3
  58. package/lib/esm/metro/state.mjs.map +1 -1
  59. package/lib/esm/metro/transform-ast.d.ts +15 -0
  60. package/lib/esm/metro/transform-ast.mjs +238 -21
  61. package/lib/esm/metro/transform-ast.mjs.map +1 -1
  62. package/lib/esm/metro/transformer.mjs +30 -3
  63. package/lib/esm/metro/transformer.mjs.map +1 -1
  64. package/lib/esm/metro/with-config.d.ts +22 -0
  65. package/lib/esm/metro/with-config.mjs +1 -1
  66. package/lib/esm/metro/with-config.mjs.map +1 -1
  67. package/package.json +2 -1
  68. package/src/core/parser/color.ts +52 -18
  69. package/src/core/parser/layout-dispatcher.ts +19 -0
  70. package/src/core/parser/length.ts +20 -6
  71. package/src/core/parser/shorthand.ts +35 -5
  72. package/src/core/parser/theme-vars.ts +53 -0
  73. package/src/core/parser/tokens.ts +171 -1
  74. package/src/core/parser/tw-parser.ts +147 -28
  75. package/src/core/parser/typography-dispatcher.ts +15 -1
  76. package/src/core/style-builder/build-style.ts +84 -26
  77. package/src/metro/state.ts +49 -1
  78. package/src/metro/transform-ast.ts +249 -18
  79. package/src/metro/transformer.ts +28 -3
  80. 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 gradient JSX attributes (`colors={…}` / `start={…}` /
284
- * `end={…}`) into a JSXOpeningElement's attribute list, replacing
285
- * any already-present attribute with the same name. Users who manually
286
- * set `colors=` on the same element lose; rnwind's class-derived
287
- * values win — matching how `className`-resolved styles override
288
- * inline `style={}`.
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 gradientAttrs Freshly built JSX attributes.
291
- * @param gradientAttributes
484
+ * @param gradientAttributes Freshly built JSX attributes.
292
485
  */
293
486
  function appendGradientAttributes(opening, gradientAttributes) {
294
- const names = new Set();
295
- for (const attribute of gradientAttributes)
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
- return true;
490
+ continue;
301
491
  if (!t.isJSXIdentifier(attribute.name))
302
- return true;
303
- return !names.has(attribute.name.name);
304
- });
305
- opening.attributes.push(...gradientAttributes);
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: