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
@@ -104,6 +104,159 @@ const NON_INTERACTIVE_HOST_TAGS = new Set([
104
104
  * on top, never replacing this.
105
105
  */
106
106
  const DEFAULT_CLASSNAME_PREFIXES = ['contentContainer'];
107
+ /**
108
+ * Module specifiers whose JSX exports are "host-like" — they consume
109
+ * `style` directly (and own no opaque component logic that depends on
110
+ * receiving the raw `className` string). For tags imported from these
111
+ * sources the transformer rewrites `className="…"` → `style={lookupCss(…)}`
112
+ * at build time, so the runtime cost is zero.
113
+ *
114
+ * For tags from ANY other source the transformer leaves `className`
115
+ * alone — the importing component receives the raw string and decides
116
+ * what to do with it (forward to an inner host, reshape, route a slice
117
+ * to `contentContainerStyle`, …). This is what makes patterns like
118
+ * `<MyButton className="px-4 bg-primary" />` work without rnwind
119
+ * stealing the prop before the component sees it.
120
+ *
121
+ * Users extend the list via `withRnwindConfig`'s `hostSources` option.
122
+ */
123
+ const DEFAULT_HOST_SOURCES = [
124
+ 'react-native',
125
+ 'react-native-reanimated',
126
+ 'react-native-svg',
127
+ 'react-native-gesture-handler',
128
+ 'react-native-safe-area-context',
129
+ 'expo-linear-gradient',
130
+ 'expo-image',
131
+ 'expo-blur',
132
+ 'expo-symbols',
133
+ '@shopify/flash-list',
134
+ '@shopify/react-native-skia',
135
+ 'lottie-react-native',
136
+ ];
137
+ /**
138
+ * Whether a JSX tag name is lowercase. Lowercase tags don't appear in
139
+ * native React Native userland — but if one shows up (web target via
140
+ * `react-native-web`, mdx, etc.) treat it as a host so the rewrite
141
+ * engages instead of silently dropping the className.
142
+ * @param name JSX tag identifier text.
143
+ * @returns True for ASCII-lowercase first character.
144
+ */
145
+ function isLowercaseTag(name) {
146
+ const code = name.codePointAt(0);
147
+ return code !== undefined && code >= 97 && code <= 122;
148
+ }
149
+ /**
150
+ * Walk a JSX opening element's tag name node into a dotted string
151
+ * (`Animated.View`, `Foo.Bar.Baz`). Returns `null` for namespaced names
152
+ * (`<svg:rect>` — invalid in RN; we skip them).
153
+ * @param name JSXOpeningElement name node.
154
+ * @returns Dotted tag text, or null.
155
+ */
156
+ function jsxTagText(name) {
157
+ if (t__namespace.isJSXIdentifier(name))
158
+ return name.name;
159
+ if (t__namespace.isJSXMemberExpression(name)) {
160
+ const left = jsxTagText(name.object);
161
+ return left ? `${left}.${name.property.name}` : null;
162
+ }
163
+ return null;
164
+ }
165
+ /**
166
+ * Leftmost identifier of a (possibly dotted) tag — used to look up its import source.
167
+ * @param tagText
168
+ */
169
+ function leftmostIdentifier(tagText) {
170
+ const dot = tagText.indexOf('.');
171
+ return dot === -1 ? tagText : tagText.slice(0, dot);
172
+ }
173
+ /**
174
+ * Build the per-file host lookup. Walks every `import` declaration once
175
+ * to map every locally-bound name to its source module. A JSX tag is a
176
+ * host when:
177
+ * 1. its full text matches an entry in `extraHostComponents` (verbatim),
178
+ * 2. its leftmost identifier was imported from a `hostSources` module,
179
+ * 3. it's a lowercase tag (web targets, defensive).
180
+ *
181
+ * Anything else is custom and the transformer leaves its className alone.
182
+ * @param ast File AST.
183
+ * @param extraHostSources User-supplied additional host module specifiers.
184
+ * @param extraHostComponents User-supplied additional host component names.
185
+ * @returns Lookup callback.
186
+ */
187
+ function buildHostLookup(ast, extraHostSources, extraHostComponents) {
188
+ const importSourceByLocal = new Map();
189
+ for (const node of ast.program.body) {
190
+ if (!t__namespace.isImportDeclaration(node))
191
+ continue;
192
+ const source = node.source.value;
193
+ for (const spec of node.specifiers) {
194
+ if (t__namespace.isImportDefaultSpecifier(spec) || t__namespace.isImportSpecifier(spec) || t__namespace.isImportNamespaceSpecifier(spec)) {
195
+ importSourceByLocal.set(spec.local.name, source);
196
+ }
197
+ }
198
+ }
199
+ // Recognise module-local host aliases — common pattern in React Native:
200
+ // const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
201
+ // const Animated = createAnimatedComponent(View)
202
+ // The local binding wraps a host underneath so its className must still
203
+ // be rewritten. Without this every `<AnimatedTextInput className="…" />`
204
+ // site looked custom and the className silently dropped.
205
+ const localHostAliases = collectCreateAnimatedComponentAliases(ast);
206
+ const hostSources = new Set([...DEFAULT_HOST_SOURCES, ...(extraHostSources ?? [])]);
207
+ const hostComponents = new Set(extraHostComponents);
208
+ return (tagText) => {
209
+ if (isLowercaseTag(tagText))
210
+ return true;
211
+ if (hostComponents.has(tagText))
212
+ return true;
213
+ const left = leftmostIdentifier(tagText);
214
+ if (localHostAliases.has(left))
215
+ return true;
216
+ const source = importSourceByLocal.get(left);
217
+ return source !== undefined && hostSources.has(source);
218
+ };
219
+ }
220
+ /**
221
+ * Walk top-level `const X = createAnimatedComponent(Y)` /
222
+ * `Animated.createAnimatedComponent(Y)` declarations and return the set
223
+ * of local names so the host-lookup recognises them. Reanimated +
224
+ * RN-core `Animated.createAnimatedComponent` are the only creators in
225
+ * common use; matching by callee-name covers both shapes without
226
+ * needing import-source resolution.
227
+ * @param ast File AST.
228
+ * @returns Set of locally-bound names that wrap a host component.
229
+ */
230
+ function collectCreateAnimatedComponentAliases(ast) {
231
+ const aliases = new Set();
232
+ for (const node of ast.program.body) {
233
+ const declaration = t__namespace.isExportNamedDeclaration(node) ? node.declaration : node;
234
+ if (!t__namespace.isVariableDeclaration(declaration))
235
+ continue;
236
+ for (const decl of declaration.declarations) {
237
+ if (!t__namespace.isIdentifier(decl.id) || !decl.init)
238
+ continue;
239
+ if (!isCreateAnimatedComponentCall(decl.init))
240
+ continue;
241
+ aliases.add(decl.id.name);
242
+ }
243
+ }
244
+ return aliases;
245
+ }
246
+ /**
247
+ * True for `createAnimatedComponent(...)` and `<x>.createAnimatedComponent(...)` calls.
248
+ * @param expr
249
+ */
250
+ function isCreateAnimatedComponentCall(expr) {
251
+ if (!t__namespace.isCallExpression(expr))
252
+ return false;
253
+ const { callee } = expr;
254
+ if (t__namespace.isIdentifier(callee) && callee.name === 'createAnimatedComponent')
255
+ return true;
256
+ if (t__namespace.isMemberExpression(callee) && t__namespace.isIdentifier(callee.property) && callee.property.name === 'createAnimatedComponent')
257
+ return true;
258
+ return false;
259
+ }
107
260
  /**
108
261
  * Mutate an already-parsed Babel AST in place:
109
262
  * - Rewrite every JSX `className="…"` / `className={expr}` attribute to
@@ -127,6 +280,7 @@ function transformAst(ast, options) {
127
280
  const literals = [];
128
281
  const prefixSet = buildPrefixSet(options.classNamePrefixes);
129
282
  const hapticHoister = createHapticHoister();
283
+ const isHostTag = buildHostLookup(ast, options.hostSources, options.hostComponents);
130
284
  const rewriteCtx = {
131
285
  needsInsets: false,
132
286
  gradientAtoms: options.gradientAtoms ?? EMPTY_GRADIENT_ATOMS,
@@ -139,6 +293,15 @@ function transformAst(ast, options) {
139
293
  let touched = false;
140
294
  let usedLookupCss = false;
141
295
  let usedInteractiveBox = false;
296
+ // Per-element host classification, captured the first time we see each
297
+ // JSXOpeningElement. Necessary because the InteractiveBox wrap mutates
298
+ // `parent.name` in-place from the original tag → `_ib`; sibling
299
+ // attributes processed AFTER the swap would otherwise re-classify off
300
+ // the now-meaningless `_ib` name and skip rewrites they should do
301
+ // (e.g. `contentContainerClassName` next to an `active:` className on
302
+ // the same `<ScrollView>`).
303
+ const customElements = new WeakSet();
304
+ const classifiedElements = new WeakSet();
142
305
  traverse(ast, {
143
306
  JSXAttribute(attributePath) {
144
307
  const { node } = attributePath;
@@ -147,6 +310,24 @@ function transformAst(ast, options) {
147
310
  const target = classifyAttributeName(node.name.name, prefixSet);
148
311
  if (!target)
149
312
  return;
313
+ // Skip className rewrite when the parent JSX tag is a custom
314
+ // component (not imported from a known host source). Custom
315
+ // components own their `className` prop — the transformer would
316
+ // steal the string from under them otherwise. The literal still
317
+ // appears in source text, so oxide still discovers its atoms via
318
+ // the project scan; the inner host that ultimately consumes the
319
+ // forwarded className gets rewritten by ITS file's transform.
320
+ const { parent } = attributePath;
321
+ if (t__namespace.isJSXOpeningElement(parent)) {
322
+ if (!classifiedElements.has(parent)) {
323
+ classifiedElements.add(parent);
324
+ const tagText = jsxTagText(parent.name);
325
+ if (tagText !== null && !isHostTag(tagText))
326
+ customElements.add(parent);
327
+ }
328
+ if (customElements.has(parent))
329
+ return;
330
+ }
150
331
  const rewritten = rewriteClassNameAttribute(attributePath, hoister, literals, rewriteCtx, target);
151
332
  if (!rewritten)
152
333
  return;
@@ -247,12 +428,20 @@ function rewriteClassNameAttribute(attributePath, hoister, literals, rewriteCtx,
247
428
  const { value } = node;
248
429
  if (!value)
249
430
  return null;
250
- const buildResult = buildFirstArgument(value, hoister, literals, rewriteCtx);
251
- if (!buildResult)
252
- return null;
253
431
  const { parent } = attributePath;
254
432
  if (!t__namespace.isJSXOpeningElement(parent))
255
433
  return null;
434
+ // The rewrite emits references to `_t` (the `useR_()` binding). That
435
+ // binding can only live in a component body — so if this JSX site has
436
+ // no enclosing component (e.g. a top-level `const renderItem = (...) =>
437
+ // <View className=.../>` helper), bail and leave className untouched
438
+ // rather than emit a dangling `_t`. Checked BEFORE any mutation
439
+ // (hoist, sibling-style drop) so a bail leaves the AST pristine.
440
+ if (!hasComponentBody(attributePath))
441
+ return null;
442
+ const buildResult = buildFirstArgument(value, hoister, literals, rewriteCtx);
443
+ if (!buildResult)
444
+ return null;
256
445
  const userStyleExpr = extractAndDropSiblingStyle(parent, target.styleProp);
257
446
  // Single context binding `_t = _r()` — carries scheme, fontScale,
258
447
  // insets together so React tracks all three as render deps via one
@@ -301,29 +490,36 @@ function applyDerivedJsxAttributes(attributePath, parent, result, target, rewrit
301
490
  injectEventHapticHandlers(attributePath, parent, result.eventHaptics, rewriteCtx);
302
491
  }
303
492
  /**
304
- * Splice gradient JSX attributes (`colors={…}` / `start={…}` /
305
- * `end={…}`) into a JSXOpeningElement's attribute list, replacing
306
- * any already-present attribute with the same name. Users who manually
307
- * set `colors=` on the same element lose; rnwind's class-derived
308
- * values win — matching how `className`-resolved styles override
309
- * inline `style={}`.
493
+ * Splice class-derived JSX attributes (`colors={…}` / `start={…}` /
494
+ * `end={…}` for gradients; `numberOfLines=` / `ellipsizeMode=` for
495
+ * truncate) into a JSXOpeningElement's attribute list but only when
496
+ * the developer hasn't already written that attribute themselves.
497
+ *
498
+ * **User attrs always win.** If a hand-written `colors={USER}` is
499
+ * present, the class-derived hoist is dropped on the floor for that
500
+ * specific attribute. Same rule for every derived prop, applied
501
+ * per-attribute so the user can override one slot (e.g. `start={…}`)
502
+ * and let rnwind fill in the others. Documented in
503
+ * `docs/architecture.md`.
310
504
  * @param opening JSXOpeningElement to mutate.
311
- * @param gradientAttrs Freshly built JSX attributes.
312
- * @param gradientAttributes
505
+ * @param gradientAttributes Freshly built JSX attributes.
313
506
  */
314
507
  function appendGradientAttributes(opening, gradientAttributes) {
315
- const names = new Set();
316
- for (const attribute of gradientAttributes)
317
- if (t__namespace.isJSXIdentifier(attribute.name))
318
- names.add(attribute.name.name);
319
- opening.attributes = opening.attributes.filter((attribute) => {
508
+ const userAttributeNames = new Set();
509
+ for (const attribute of opening.attributes) {
320
510
  if (!t__namespace.isJSXAttribute(attribute))
321
- return true;
511
+ continue;
322
512
  if (!t__namespace.isJSXIdentifier(attribute.name))
323
- return true;
324
- return !names.has(attribute.name.name);
325
- });
326
- opening.attributes.push(...gradientAttributes);
513
+ continue;
514
+ userAttributeNames.add(attribute.name.name);
515
+ }
516
+ for (const derived of gradientAttributes) {
517
+ if (!t__namespace.isJSXIdentifier(derived.name))
518
+ continue;
519
+ if (userAttributeNames.has(derived.name.name))
520
+ continue;
521
+ opening.attributes.push(derived);
522
+ }
327
523
  }
328
524
  /**
329
525
  * Whether a JSX tag can fire press / focus events. Pure host-tag check
@@ -978,6 +1174,27 @@ function injectContextHook(path) {
978
1174
  componentBody.unshiftContainer('body', declaration);
979
1175
  return CONTEXT_BINDING;
980
1176
  }
1177
+ /**
1178
+ * Whether `path` sits inside a recognised function component — i.e.
1179
+ * {@link injectContextHook} would find a body to host `const _t =
1180
+ * useR_()`. Pure lookup that mirrors {@link findComponentBody}'s walk
1181
+ * but performs NO body promotion, so a caller can bail before mutating
1182
+ * when the answer is no.
1183
+ * @param path Rewrite-site path.
1184
+ * @returns True when an enclosing component function exists.
1185
+ */
1186
+ function hasComponentBody(path) {
1187
+ let current = path;
1188
+ while (current) {
1189
+ const fn = current.findParent((parent) => parent.isFunction());
1190
+ if (!fn)
1191
+ return false;
1192
+ if (isComponentFunction(fn))
1193
+ return true;
1194
+ current = fn;
1195
+ }
1196
+ return false;
1197
+ }
981
1198
  /**
982
1199
  * Walk up from `path` to the nearest recognised function component.
983
1200
  * Accepts: