@symbiote-native/vue 0.5.0 → 1.0.0
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/README.md +19 -4
- package/babel-jsx.cjs +53 -0
- package/babel-lower-host-primitives.cjs +225 -0
- package/build/components/button.js +4 -2
- package/build/components/pressable.js +39 -20
- package/build/components/safe-area-view.d.ts +1 -0
- package/build/components/text-input/index.js +18 -0
- package/build/components/touchable-native-feedback.js +8 -3
- package/build/components/touchable.js +3 -0
- package/build/components.d.ts +2 -0
- package/build/components.js +18 -2
- package/build/composables/use-raw-attrs.d.ts +1 -0
- package/build/composables/use-raw-attrs.js +26 -0
- package/build/host-instance/index.js +4 -5
- package/build/index.d.ts +1 -0
- package/build/index.js +5 -0
- package/build/modules/animated/create-animated-component.d.ts +1 -1
- package/build/modules/animated/create-animated-component.js +18 -4
- package/build/register.d.ts +1 -0
- package/build/register.js +35 -0
- package/build/renderer/index.js +58 -9
- package/build/runtime-helpers/index.d.ts +4 -0
- package/build/runtime-helpers/index.js +209 -4
- package/build/state-style.d.ts +1 -0
- package/build/state-style.js +13 -0
- package/build/utils/normalize-attrs.d.ts +1 -0
- package/build/utils/normalize-attrs.js +21 -15
- package/metro-css-parser.cjs +2 -1
- package/metro-vue-transformer.cjs +367 -7
- package/package.json +29 -8
|
@@ -11,8 +11,21 @@
|
|
|
11
11
|
// babelTransformerPath at it.
|
|
12
12
|
|
|
13
13
|
const nodeFs = require('fs');
|
|
14
|
-
const {
|
|
15
|
-
|
|
14
|
+
const {
|
|
15
|
+
parse,
|
|
16
|
+
parseCache,
|
|
17
|
+
compileScript,
|
|
18
|
+
registerTS,
|
|
19
|
+
// Babel's own parser, re-exported. Using it costs no new dependency, which matters: @babel/parser
|
|
20
|
+
// does not resolve from this package under pnpm's isolated layout, and the alternative — adding
|
|
21
|
+
// @babel/{parser,types,generator} to a PUBLISHED adapter — would also force every example through
|
|
22
|
+
// a full reinstall, because the overlay's folder swap cannot install a new dependency.
|
|
23
|
+
babelParse,
|
|
24
|
+
} = require('@vue/compiler-sfc');
|
|
25
|
+
const {
|
|
26
|
+
createCompoundExpression,
|
|
27
|
+
createSimpleExpression,
|
|
28
|
+
} = require('@vue/compiler-core');
|
|
16
29
|
|
|
17
30
|
// A bare-specifier type import (`import type { X } from '@symbiote-native/navigation/vue'`) needs
|
|
18
31
|
// real node_modules resolution to turn the specifier into a file path - compileScript's own `fs`
|
|
@@ -109,6 +122,300 @@ function createScopeClassNodeTransform(renames) {
|
|
|
109
122
|
};
|
|
110
123
|
}
|
|
111
124
|
|
|
125
|
+
// Which primitives lower, and to which tag, comes from the SHARED SPEC — this file used to keep
|
|
126
|
+
// its own copy of the map, one of four, and the copies had already drifted apart (Vue folded
|
|
127
|
+
// `id` -> `nativeID` on neither tag while Solid did both and Svelte did one).
|
|
128
|
+
//
|
|
129
|
+
// Only the `intrinsic` field is read here. `aliases` deliberately is NOT: Vue applies that fold at
|
|
130
|
+
// RUNTIME in `src/renderer/index.ts`'s patchProp, because Vue reaches a node by four paths and
|
|
131
|
+
// compile time covers only two of them. The spec says so at its `aliases` declaration; a transform
|
|
132
|
+
// does not own a fold just because the spec describes it.
|
|
133
|
+
//
|
|
134
|
+
// WHY lower at all, measured 2026-08-23: Vue charges a full component instance
|
|
135
|
+
// (createComponentInstance + initProps + initSlots + setupRenderEffect) for a functional component
|
|
136
|
+
// too, and our benchmark row is 7 instances (Row + View + 3 Text + 2 Pressable) where React pays 7
|
|
137
|
+
// far cheaper fibers. That is the whole reason Vue is level with React on the web (a <div> is an
|
|
138
|
+
// element) and 1.8x behind here. Same 36 001-node tree with View/Text lowered: 138.0 -> 118.5 ms,
|
|
139
|
+
// 14.1%. It also lets Vue hoist static props out of the render fn and emit patch flags, neither of
|
|
140
|
+
// which a component gets.
|
|
141
|
+
const {
|
|
142
|
+
HOST_PRIMITIVES,
|
|
143
|
+
} = require('@symbiote-native/components/host-primitives');
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
const LOWERABLE_HOST_PRIMITIVES = new Map(
|
|
147
|
+
Object.entries(HOST_PRIMITIVES).map(([name, spec]) => [
|
|
148
|
+
name,
|
|
149
|
+
{
|
|
150
|
+
intrinsic: spec.intrinsic,
|
|
151
|
+
observesState: spec.observesState === true,
|
|
152
|
+
intrinsicWhen: spec.intrinsicWhen,
|
|
153
|
+
},
|
|
154
|
+
]),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Lower ONLY a name this file actually imported from us. Matching on the bare tag would silently
|
|
158
|
+
// rewrite an app's own <View>, and the failure would be invisible: the app's component never
|
|
159
|
+
// renders and the intrinsic paints an empty box. Alias-aware, so `import { View as RNView }`
|
|
160
|
+
// lowers RNView and leaves View alone.
|
|
161
|
+
const SYMBIOTE_VUE_IMPORT =
|
|
162
|
+
/import\s*\{([^}]*)\}\s*from\s*['"]@symbiote-native\/vue['"]/g;
|
|
163
|
+
|
|
164
|
+
function lowerableTagsIn(descriptor) {
|
|
165
|
+
const source =
|
|
166
|
+
(descriptor.scriptSetup?.content ?? '') +
|
|
167
|
+
(descriptor.script?.content ?? '');
|
|
168
|
+
const tags = new Map();
|
|
169
|
+
for (const match of source.matchAll(SYMBIOTE_VUE_IMPORT)) {
|
|
170
|
+
for (const clause of match[1].split(',')) {
|
|
171
|
+
const [imported, local] = clause.trim().split(/\s+as\s+/);
|
|
172
|
+
const entry = LOWERABLE_HOST_PRIMITIVES.get(imported);
|
|
173
|
+
if (entry !== undefined) tags.set(local ?? imported, entry);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return tags;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// A primitive that OWNS STATE (Pressable's pressed) can only lower when the template does not read
|
|
180
|
+
// that state — an element has no instance to read it from, so the machine moves to the engine node
|
|
181
|
+
// and there is nothing left to hand a template. Two authoring shapes ask for it, and both are
|
|
182
|
+
// visible here:
|
|
183
|
+
//
|
|
184
|
+
// :style="fnStyle" the style is a function of press state
|
|
185
|
+
// v-slot="{ pressed }" / #default=… the children are a function of press state
|
|
186
|
+
//
|
|
187
|
+
// REFUSING IS ALWAYS SAFE and under-refusing never is: a refused element keeps today's behaviour
|
|
188
|
+
// and merely misses the win, while a wrongly-lowered one is a button that stops responding
|
|
189
|
+
// visually, on device, with every test green. Every judgement below therefore defaults to refusing
|
|
190
|
+
// whenever it cannot PROVE the safe case.
|
|
191
|
+
//
|
|
192
|
+
// The one thing that can be proven is an object literal. `:style="{ borderColor: c }"` provably is
|
|
193
|
+
// not a function; `:style="anything else"` cannot be told apart from one at compile time, because
|
|
194
|
+
// the compiler sees an expression, never its value. That asymmetry is also what makes the
|
|
195
|
+
// ActionButton migration work: move `opacity` into `.action-button:active` and what remains is an
|
|
196
|
+
// object literal, so the same button lowers without the transform learning anything new.
|
|
197
|
+
function refusesLowering(node) {
|
|
198
|
+
for (const prop of node.props) {
|
|
199
|
+
// REFUSAL_CATEGORIES.instanceBoundDirective, and it is checked before the directive filter
|
|
200
|
+
// because `ref="x"` is a plain ATTRIBUTE while `:ref="x"` is a directive. `ref` on a component
|
|
201
|
+
// yields the component instance and on an element the host node, so lowering silently changes
|
|
202
|
+
// what the app receives. Found by the shared verdict table, not by a Vue test.
|
|
203
|
+
if (prop.type === 6 /* ATTRIBUTE */ && prop.name === 'ref') return true;
|
|
204
|
+
if (prop.type !== 7 /* NodeTypes.DIRECTIVE */) continue;
|
|
205
|
+
if (prop.name === 'bind' && prop.arg?.content === 'ref') return true;
|
|
206
|
+
|
|
207
|
+
// `v-bind="obj"` — a whole attribute set this pass cannot enumerate, so it may be hiding a
|
|
208
|
+
// functional `style`. REFUSAL_CATEGORIES.unreadableAttributeSet.
|
|
209
|
+
if (prop.name === 'bind' && !prop.arg) return true;
|
|
210
|
+
|
|
211
|
+
// REFUSAL_CATEGORIES.stateInTemplate no longer fires on a `style`: every shape is covered, by
|
|
212
|
+
// an inert passthrough, a direct call, or the runtime helper. The one thing still unreachable
|
|
213
|
+
// is an expression whose source text cannot be reconstructed at all, which cannot occur in a
|
|
214
|
+
// file that compiles — `styleEmissionKind` returning undefined keeps the component if it does.
|
|
215
|
+
if (
|
|
216
|
+
prop.name === 'bind' &&
|
|
217
|
+
prop.arg?.content === 'style' &&
|
|
218
|
+
!isInertValueExpression(prop.exp) &&
|
|
219
|
+
styleEmissionKind(prop.exp) === undefined
|
|
220
|
+
)
|
|
221
|
+
return true;
|
|
222
|
+
|
|
223
|
+
// `v-slot="{ pressed }"` on the element itself. REFUSAL_CATEGORIES.renderPropChild.
|
|
224
|
+
if (prop.name === 'slot' && prop.exp) return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ANY `<template>` child, with or without a slot argument — not only the `#default="{ pressed }"`
|
|
228
|
+
// form this rule was written for. Measured through the real compileSfc: lowering an element that
|
|
229
|
+
// has one makes codegen throw `Codegen node is missing for element/if/for node`, because the
|
|
230
|
+
// element path never builds a codegen node for a child the slot path owns. Loud rather than
|
|
231
|
+
// silent, but still a build failure in an app that writes a perfectly ordinary template, so it
|
|
232
|
+
// refuses here. This is the one place the spec's "zero-arity is not a refusal" carve-out does
|
|
233
|
+
// NOT apply: it is about a JSX function child, and an SFC `<template>` child cannot be lowered
|
|
234
|
+
// at all.
|
|
235
|
+
return node.children.some(
|
|
236
|
+
child =>
|
|
237
|
+
child.type === 1 /* ELEMENT */ && child.tagType === 3 /* TEMPLATE */,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// An ALLOW-LIST, never a hunt for a function literal, and the shared spec pins all five transforms
|
|
242
|
+
// to this side of it (`REFUSAL_CATEGORIES.stateInTemplate`). `:style="styleFn"` is an identifier at
|
|
243
|
+
// compile time and NO transform can tell whether it holds an object or a function, so only
|
|
244
|
+
// provably inert value shapes lower — object, array, string/number literal, template literal — and
|
|
245
|
+
// everything else refuses. A narrow "refuse a function literal" reading passes every obvious test
|
|
246
|
+
// and fails on the one call site that hoists its style into a variable, which is exactly what
|
|
247
|
+
// ActionButton does.
|
|
248
|
+
//
|
|
249
|
+
// Which AST node carries the source is NOT what intuition says, and the probe that established it
|
|
250
|
+
// is why this reads both shapes. `transformExpression` runs before this pass and rewrites every
|
|
251
|
+
// setup binding, so `:style="{ borderColor: c }"` arrives as a COMPOUND (type 8) with children
|
|
252
|
+
// `['{ borderColor: ', '$setup.c', ' }']`, while the dangerous `:style="fnStyle"` arrives SIMPLE
|
|
253
|
+
// (type 4) as `'$setup.fnStyle'`. Testing `type === 4` alone therefore refuses exactly the shape it
|
|
254
|
+
// should allow — the first version of this did that, and the probe caught it before any test ran.
|
|
255
|
+
// An object literal with nothing to rewrite (`{ borderColor: 1 }`) stays SIMPLE, so both matter.
|
|
256
|
+
const INERT_VALUE_HEAD = /^[{['"`\d]/;
|
|
257
|
+
|
|
258
|
+
function isInertValueExpression(exp) {
|
|
259
|
+
const head = leadingSource(exp);
|
|
260
|
+
return head !== undefined && INERT_VALUE_HEAD.test(head.trimStart());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The literal source text the expression opens with. An arrow never opens with one of the
|
|
264
|
+
// characters above — it opens with `(` or its parameter name — so reading only the head is sound.
|
|
265
|
+
function leadingSource(exp) {
|
|
266
|
+
if (exp?.type === 4 /* SIMPLE_EXPRESSION */) return exp.content;
|
|
267
|
+
if (exp?.type !== 8 /* COMPOUND_EXPRESSION */) return undefined;
|
|
268
|
+
const head = exp.children?.[0];
|
|
269
|
+
return typeof head === 'string' ? head : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The FINAL source text of a template expression — what codegen would have emitted. Not
|
|
273
|
+
// `exp.loc.source`, which is the text the AUTHOR wrote: `transformExpression` runs before this pass
|
|
274
|
+
// and rewrites every setup binding, so `loc.source` still says `color` where the compiled render fn
|
|
275
|
+
// must say what the compound's children say. Reading the children is therefore the only correct
|
|
276
|
+
// reconstruction, and re-emitting it verbatim is what keeps the rewrite intact.
|
|
277
|
+
function expressionSource(exp) {
|
|
278
|
+
if (exp === undefined || exp === null) return undefined;
|
|
279
|
+
if (exp.type === 4 /* SIMPLE_EXPRESSION */) return exp.content;
|
|
280
|
+
if (exp.type !== 8 /* COMPOUND_EXPRESSION */) return undefined;
|
|
281
|
+
let source = '';
|
|
282
|
+
for (const child of exp.children) {
|
|
283
|
+
if (typeof child === 'string') {
|
|
284
|
+
source += child;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const nested = expressionSource(child);
|
|
288
|
+
if (nested === undefined) return undefined;
|
|
289
|
+
source += nested;
|
|
290
|
+
}
|
|
291
|
+
return source;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// HOW the pair is emitted, never WHETHER the element lowers — every shape lowers. Classified off a
|
|
295
|
+
// real Babel AST so this path and the JSX path cannot drift by reading the same text differently.
|
|
296
|
+
//
|
|
297
|
+
// literal (EXPR)({ pressed: false }) two closures, no read hazard
|
|
298
|
+
// reference typeof (E) === 'function' ? (E)({…}) : (E) E printed twice, but it is a read
|
|
299
|
+
// opaque v-bind="__helper(EXPR)" EXPR printed ONCE — required
|
|
300
|
+
//
|
|
301
|
+
// The third bucket is `REFUSAL_CATEGORIES.emitStyleExpressionOnce`: `getStyle()`, `bag[i]` and
|
|
302
|
+
// `flag ? a : b` change meaning when printed twice. It is not the default because a spread is the
|
|
303
|
+
// only Vue form yielding two props from one evaluation, and a spread costs the element its patch
|
|
304
|
+
// flag — measured on the real compileSfc, `12 /* STYLE, PROPS */` becomes `16 /* FULL_PROPS */`
|
|
305
|
+
// plus a mergeProps on every render. Paid only where a repeated read would actually be wrong.
|
|
306
|
+
function styleEmissionKind(exp) {
|
|
307
|
+
const source = expressionSource(exp);
|
|
308
|
+
if (source === undefined) return undefined;
|
|
309
|
+
let expression;
|
|
310
|
+
try {
|
|
311
|
+
// Wrapped in parens so a bare object or an arrow parses as an EXPRESSION rather than a block.
|
|
312
|
+
// `typescript` because a template in a lang="ts" SFC may carry an annotation on the callback's
|
|
313
|
+
// parameter — which is exactly how the real call sites are written.
|
|
314
|
+
const file = babelParse(`(${source})`, { plugins: ['typescript'] });
|
|
315
|
+
expression = file.program.body[0]?.expression;
|
|
316
|
+
} catch {
|
|
317
|
+
// An expression this parser cannot read is one we cannot classify, so it keeps the component.
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
if (expression === undefined) return undefined;
|
|
321
|
+
if (
|
|
322
|
+
expression.type === 'ArrowFunctionExpression' ||
|
|
323
|
+
expression.type === 'FunctionExpression'
|
|
324
|
+
)
|
|
325
|
+
return 'literal';
|
|
326
|
+
return isCheapReferenceNode(expression) ? 'reference' : 'opaque';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isCheapReferenceNode(node) {
|
|
330
|
+
if (node.type === 'Identifier') return true;
|
|
331
|
+
return (
|
|
332
|
+
node.type === 'MemberExpression' &&
|
|
333
|
+
!node.computed &&
|
|
334
|
+
node.property.type === 'Identifier' &&
|
|
335
|
+
isCheapReferenceNode(node.object)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
// The parser has already decided `<View>` is a COMPONENT (capitalized, and a <script setup>
|
|
341
|
+
// binding), so renaming the tag is not enough — isCustomElement is consulted for the ORIGINAL tag
|
|
342
|
+
// and tagType must be flipped in the same pass, before transformElement's exit hook turns the
|
|
343
|
+
// children into withCtx slots. Get either half wrong and codegen emits a component whose children
|
|
344
|
+
// are slots the element path never mounts: a silently empty subtree, which is how the first
|
|
345
|
+
// attempt at this read.
|
|
346
|
+
// REFUSAL_CATEGORIES.dynamicIntrinsicChoice — the SFC twin of babel-lower-host-primitives.cjs's
|
|
347
|
+
// `intrinsicWhenFor`, same rule over a different AST. A primitive whose spec entry carries
|
|
348
|
+
// `intrinsicWhen` selects between TWO Fabric views by one prop's value, and this pass prints a
|
|
349
|
+
// static tag, so it can choose only from a compile-time literal.
|
|
350
|
+
//
|
|
351
|
+
// Why it refuses harder than the value categories do: an unreadable VALUE lands a prop wrong and a
|
|
352
|
+
// later write can still correct it; the wrong choice here commits the wrong native view, which no
|
|
353
|
+
// prop write can move a node between.
|
|
354
|
+
//
|
|
355
|
+
// `multiline="true"` — a static STRING attribute — deliberately does not resolve, even though the
|
|
356
|
+
// component treats it as truthy. `multiline="false"` is truthy too, which is exactly where author
|
|
357
|
+
// intent and runtime behaviour part company; refusing costs the optimisation, guessing costs the
|
|
358
|
+
// view.
|
|
359
|
+
//
|
|
360
|
+
// Returns the tag to emit, or undefined to refuse.
|
|
361
|
+
function intrinsicWhenFor(node, entry) {
|
|
362
|
+
const choice = entry.intrinsicWhen;
|
|
363
|
+
if (choice === undefined) return entry.intrinsic;
|
|
364
|
+
|
|
365
|
+
let resolved = false;
|
|
366
|
+
for (const prop of node.props) {
|
|
367
|
+
// `v-bind="obj"` may carry the selector prop and cannot be enumerated, so it is indistinguish-
|
|
368
|
+
// able from the prop being absent — refuse rather than default to the single-line view.
|
|
369
|
+
if (prop.type === 7 /* DIRECTIVE */ && prop.name === 'bind' && !prop.arg)
|
|
370
|
+
return undefined;
|
|
371
|
+
|
|
372
|
+
if (prop.type === 6 /* ATTRIBUTE */ && prop.name === choice.prop) {
|
|
373
|
+
// A bare `multiline` carries no value, which is the template spelling of `true`. Anything
|
|
374
|
+
// else on a plain attribute is a string.
|
|
375
|
+
if (prop.value === undefined) {
|
|
376
|
+
resolved = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (prop.type !== 7 /* DIRECTIVE */) continue;
|
|
383
|
+
if (prop.name !== 'bind' || prop.arg?.content !== choice.prop) continue;
|
|
384
|
+
const source = expressionSource(prop.exp)?.trim();
|
|
385
|
+
if (source !== 'true' && source !== 'false') return undefined;
|
|
386
|
+
resolved = source === 'true';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return resolved ? choice.intrinsic : entry.intrinsic;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function createHostPrimitiveLowering(tags) {
|
|
393
|
+
return function lowerHostPrimitive(node) {
|
|
394
|
+
if (node.type !== 1 /* NodeTypes.ELEMENT */) return;
|
|
395
|
+
const entry = tags.get(node.tag);
|
|
396
|
+
if (entry === undefined) return;
|
|
397
|
+
// Resolved BEFORE the state refusal and OUTSIDE its `observesState` gate — the two guard
|
|
398
|
+
// different things, and this one applies to a primitive that owns no state. Folding it into
|
|
399
|
+
// refusesLowering would put it behind that flag, where it would never run for TextInput.
|
|
400
|
+
const intrinsic = intrinsicWhenFor(node, entry);
|
|
401
|
+
if (intrinsic === undefined) return;
|
|
402
|
+
if (entry.observesState && refusesLowering(node)) return;
|
|
403
|
+
// NO state-style expansion, deliberately. A functional `style` reaches `routeProp` untouched
|
|
404
|
+
// and the engine resolves it at both values of `pressed` (`isStyleCallback`), so rewriting the
|
|
405
|
+
// attribute into a resting/active pair here would be this transform carrying BEHAVIOUR — what
|
|
406
|
+
// `tests/lowering-transform-carries-no-behaviour.test.ts` exists to keep out.
|
|
407
|
+
//
|
|
408
|
+
// Removing it is also FASTER, which was not the expectation going in. The split emitted TWO
|
|
409
|
+
// props per element and Vue hoists neither — the compiled output builds and invokes the
|
|
410
|
+
// callback twice and allocates two style objects, per element per render. Measured headless
|
|
411
|
+
// through the real compileSfc on 1 000 styled nodes: 5.2/5.1 ms with the split against
|
|
412
|
+
// 4.6/4.5 ms without, with createNode 3002, appendChild 3000, VISITED 3003 and WRITES 4000
|
|
413
|
+
// byte-identical in every arm.
|
|
414
|
+
node.tag = intrinsic;
|
|
415
|
+
node.tagType = 0; /* ElementTypes.ELEMENT */
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
112
419
|
// A short, stable id per file, used as the SFC scope id regardless of whether the file has
|
|
113
420
|
// scoped styles - built on css-parser's shared hashFilePath so the algorithm isn't duplicated
|
|
114
421
|
// against the standalone .module.css compiler's identical need.
|
|
@@ -177,6 +484,23 @@ async function compileStyleBlockContent(style, filename) {
|
|
|
177
484
|
}
|
|
178
485
|
|
|
179
486
|
async function compileSfc(src, filename) {
|
|
487
|
+
// `parse` memoizes its descriptor on (source, filename) in a module-global LRU, and the node
|
|
488
|
+
// transforms below MUTATE that descriptor's template AST in place — the lowering one rewrites
|
|
489
|
+
// `node.tag`/`node.tagType`, the scope-class one replaces `prop.exp`. Handing the same object to
|
|
490
|
+
// a second compile therefore walks a half-transformed tree and generation dies with `Codegen
|
|
491
|
+
// node is missing for element/if/for node`.
|
|
492
|
+
//
|
|
493
|
+
// Metro compiles each file once per build, so the cache buys this caller nothing; the only place
|
|
494
|
+
// it was ever exercised is css-parser's golden-corpus determinism check, which compiles one
|
|
495
|
+
// source twice on purpose. That is the ONLY test anywhere that can see this, which is why a
|
|
496
|
+
// transform bug surfaced as a failure in another package's corpus.
|
|
497
|
+
//
|
|
498
|
+
// `clear()` rather than deleting our one key: the key is `source + JSON.stringify(options)`
|
|
499
|
+
// (`@vue/shared`'s genCacheKey), so reproducing it here would couple this file to an internal
|
|
500
|
+
// format across every Vue bump. Clearing a pure memo is always safe — the only cost is a
|
|
501
|
+
// re-parse, which is what we need anyway.
|
|
502
|
+
|
|
503
|
+
parseCache.clear();
|
|
180
504
|
const { descriptor, errors } = parse(src, { filename });
|
|
181
505
|
if (errors && errors.length > 0) {
|
|
182
506
|
throw new Error(
|
|
@@ -243,13 +567,49 @@ async function compileSfc(src, filename) {
|
|
|
243
567
|
}
|
|
244
568
|
}
|
|
245
569
|
|
|
246
|
-
//
|
|
247
|
-
// a .vue with only unscoped/no styles
|
|
570
|
+
// Scoped-class rewriting is skipped entirely (not even passed to the compiler) when nothing in
|
|
571
|
+
// this file is scoped, so a .vue with only unscoped/no styles adds no runtime cost. Host-
|
|
572
|
+
// primitive lowering is independent of it and applies whenever the file imports one.
|
|
573
|
+
const lowerableTags = lowerableTagsIn(descriptor);
|
|
574
|
+
const nodeTransforms = [];
|
|
575
|
+
if (lowerableTags.size > 0)
|
|
576
|
+
nodeTransforms.push(
|
|
577
|
+
createHostPrimitiveLowering(lowerableTags),
|
|
578
|
+
);
|
|
579
|
+
if (scopedClassNames.size > 0)
|
|
580
|
+
nodeTransforms.push(createScopeClassNodeTransform(scopedClassNames));
|
|
581
|
+
// `.values()` yields the SPEC ENTRIES, so this must map to `.intrinsic` — a Set of entry objects
|
|
582
|
+
// makes `loweredTags.has('symbiote-pressable')` permanently false, and the failure is not the
|
|
583
|
+
// obvious one. Pass 1 rewrites the tag and forces tagType, so the file compiles; `@vue/compiler-
|
|
584
|
+
// sfc` then caches the descriptor by (source, filename) and pass 2 walks the ALREADY-REWRITTEN
|
|
585
|
+
// AST, where neither `lowerableTags` nor a broken `loweredTags` recognises `symbiote-pressable`.
|
|
586
|
+
// The node is then an unknown component with no codegen node and generation throws
|
|
587
|
+
// `Codegen node is missing for element/if/for node` — in the only test anywhere that compiles one
|
|
588
|
+
// source twice, which is css-parser's golden-corpus determinism check, i.e. nowhere near here.
|
|
589
|
+
// BOTH tags a spec entry can emit, not just its default — a primitive with an `intrinsicWhen`
|
|
590
|
+
// has a second intrinsic, and the paragraph above is about exactly what happens to a tag missing
|
|
591
|
+
// from this set.
|
|
592
|
+
//
|
|
593
|
+
// HONEST STATUS: extending the guard to the second tag is UNWITNESSED. Removing it leaves every
|
|
594
|
+
// test in the repo green, including a case built specifically to catch it by compiling one source
|
|
595
|
+
// twice. The failure needs pass 2 to walk a rewritten AST, and the only place that happens is
|
|
596
|
+
// css-parser's golden-corpus determinism check — which will not reach this until a corpus file
|
|
597
|
+
// renders a multiline TextInput. Kept because the hazard it extends is documented and cost a real
|
|
598
|
+
// failure once; not kept because a test proves it.
|
|
599
|
+
const loweredTags = new Set(
|
|
600
|
+
[...lowerableTags.values()].flatMap(entry =>
|
|
601
|
+
entry.intrinsicWhen === undefined
|
|
602
|
+
? [entry.intrinsic]
|
|
603
|
+
: [entry.intrinsic, entry.intrinsicWhen.intrinsic],
|
|
604
|
+
),
|
|
605
|
+
);
|
|
248
606
|
const templateOptions =
|
|
249
|
-
|
|
607
|
+
nodeTransforms.length > 0
|
|
250
608
|
? {
|
|
251
609
|
compilerOptions: {
|
|
252
|
-
nodeTransforms
|
|
610
|
+
nodeTransforms,
|
|
611
|
+
isCustomElement: tag =>
|
|
612
|
+
lowerableTags.has(tag) || loweredTags.has(tag),
|
|
253
613
|
},
|
|
254
614
|
}
|
|
255
615
|
: undefined;
|
|
@@ -303,7 +663,7 @@ async function compileSfc(src, filename) {
|
|
|
303
663
|
preamble.push(`const ${bindingName} = ${JSON.stringify(classMap)};`);
|
|
304
664
|
}
|
|
305
665
|
|
|
306
|
-
const parts = [
|
|
666
|
+
const parts = [preamble.join('\n'), code];
|
|
307
667
|
if (hasCssModules) {
|
|
308
668
|
const bindings = [...cssModuleBindings.keys()]
|
|
309
669
|
.map(name => `${JSON.stringify(name)}: ${name}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "SymbioteNative's Vue adapter — a @vue/runtime-core createRenderer driving real native iOS/Android views through the same engine as the React adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
"url": "https://github.com/OneEyed1366/symbiote-native/issues"
|
|
14
14
|
},
|
|
15
15
|
"author": "Andrey Prokopenko <psevdoproger@gmail.com>",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"symbiote-native",
|
|
19
|
+
"vue",
|
|
20
|
+
"vue3",
|
|
21
|
+
"fabric",
|
|
22
|
+
"renderer",
|
|
23
|
+
"cross-platform",
|
|
24
|
+
"mobile",
|
|
25
|
+
"ios",
|
|
26
|
+
"android"
|
|
27
|
+
],
|
|
16
28
|
"type": "module",
|
|
17
29
|
"main": "./build/index.js",
|
|
18
30
|
"module": "./build/index.js",
|
|
@@ -31,9 +43,16 @@
|
|
|
31
43
|
"default": "./build/runtime-helpers/index.js"
|
|
32
44
|
},
|
|
33
45
|
"./metro-css-parser": "./metro-css-parser.cjs",
|
|
34
|
-
"./metro-vue-transformer": "./metro-vue-transformer.cjs"
|
|
46
|
+
"./metro-vue-transformer": "./metro-vue-transformer.cjs",
|
|
47
|
+
"./babel-jsx": "./babel-jsx.cjs",
|
|
48
|
+
"./state-style": {
|
|
49
|
+
"types": "./build/state-style.d.ts",
|
|
50
|
+
"default": "./build/state-style.js"
|
|
51
|
+
}
|
|
35
52
|
},
|
|
36
53
|
"files": [
|
|
54
|
+
"babel-jsx.cjs",
|
|
55
|
+
"babel-lower-host-primitives.cjs",
|
|
37
56
|
"build",
|
|
38
57
|
"metro-css-parser.cjs",
|
|
39
58
|
"metro-vue-transformer.cjs"
|
|
@@ -42,21 +61,23 @@
|
|
|
42
61
|
"access": "public"
|
|
43
62
|
},
|
|
44
63
|
"dependencies": {
|
|
64
|
+
"@vue/babel-plugin-jsx": "^1.5.0",
|
|
45
65
|
"@vue/compiler-core": "^3.5.13",
|
|
46
66
|
"@vue/compiler-sfc": "^3.5.13",
|
|
47
67
|
"@vue/runtime-core": "^3.5.13",
|
|
48
68
|
"typescript": "~6.0.0",
|
|
49
|
-
"@symbiote-native/components": "0.
|
|
50
|
-
"@symbiote-native/css-parser": "0.
|
|
69
|
+
"@symbiote-native/components": "1.0.0",
|
|
70
|
+
"@symbiote-native/css-parser": "0.5.0"
|
|
51
71
|
},
|
|
52
72
|
"peerDependencies": {
|
|
53
|
-
"@symbiote-native/engine": ">=0.1.7",
|
|
54
73
|
"vue": ">=3.5.0",
|
|
55
|
-
"react-native": ">=0.86"
|
|
74
|
+
"react-native": ">=0.86",
|
|
75
|
+
"@symbiote-native/engine": "^0.4.0"
|
|
56
76
|
},
|
|
57
77
|
"devDependencies": {
|
|
58
|
-
"@
|
|
59
|
-
"@symbiote-native/
|
|
78
|
+
"@babel/core": "^7.25.2",
|
|
79
|
+
"@symbiote-native/engine": "0.4.0",
|
|
80
|
+
"@symbiote-native/test-utils": "0.3.0"
|
|
60
81
|
},
|
|
61
82
|
"scripts": {
|
|
62
83
|
"typecheck": "tsc --build",
|