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
|
@@ -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
|
|
305
|
-
* `end={…}`
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
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
|
|
312
|
-
* @param gradientAttributes
|
|
505
|
+
* @param gradientAttributes Freshly built JSX attributes.
|
|
313
506
|
*/
|
|
314
507
|
function appendGradientAttributes(opening, gradientAttributes) {
|
|
315
|
-
const
|
|
316
|
-
for (const attribute of
|
|
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
|
-
|
|
511
|
+
continue;
|
|
322
512
|
if (!t__namespace.isJSXIdentifier(attribute.name))
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
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:
|