@symbiote-native/vue 1.0.0 → 3.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 +13 -12
- package/babel-jsx.cjs +26 -18
- package/build/components/activity-indicator-props.d.ts +5 -0
- package/build/components/activity-indicator-props.js +1 -0
- package/build/components/button-props.d.ts +5 -0
- package/build/components/button-props.js +1 -0
- package/build/components/flat-list/index.d.ts +3 -3
- package/build/components/flat-list/index.js +3 -3
- package/build/components/image-background-props.d.ts +7 -0
- package/build/components/image-background-props.js +1 -0
- package/build/components/image-props.d.ts +5 -0
- package/build/components/image-props.js +5 -0
- package/build/components/{input-accessory-view.d.ts → input-accessory-view-props.d.ts} +1 -3
- package/build/components/input-accessory-view-props.js +5 -0
- package/build/components/keyboard-avoiding-view.js +9 -5
- package/build/components/modal/index.js +1 -1
- package/build/components/pressable-props.d.ts +31 -0
- package/build/components/pressable-props.js +12 -0
- package/build/components/refresh-control-props.d.ts +16 -0
- package/build/components/refresh-control-props.js +9 -0
- package/build/components/safe-area-view-props.d.ts +6 -0
- package/build/components/safe-area-view-props.js +5 -0
- package/build/components/scroll-view/{shared.d.ts → scroll-view-props.d.ts} +4 -27
- package/build/components/scroll-view/scroll-view-props.js +1 -0
- package/build/components/section-list/index.d.ts +3 -3
- package/build/components/switch/switch-props.d.ts +6 -0
- package/build/components/switch/switch-props.js +1 -0
- package/build/components/text-input/text-input-props.d.ts +5 -0
- package/build/components/text-input/text-input-props.js +1 -0
- package/build/components/text-props.d.ts +24 -0
- package/build/components/text-props.js +6 -0
- package/build/components/touchable-native-feedback-props.d.ts +7 -0
- package/build/components/touchable-native-feedback-props.js +1 -0
- package/build/components/touchable-props.d.ts +17 -0
- package/build/components/touchable-props.js +1 -0
- package/build/components/touchable-without-feedback-props.d.ts +5 -0
- package/build/components/touchable-without-feedback-props.js +1 -0
- package/build/components/view-props.d.ts +29 -0
- package/build/components/view-props.js +5 -0
- package/build/components/virtualized-list/index.d.ts +3 -3
- package/build/components/virtualized-list/index.js +64 -46
- package/build/components/virtualized-section-list/index.d.ts +3 -3
- package/build/descriptor-to-vue.js +2 -2
- package/build/host-instance/index.js +2 -2
- package/build/index.d.ts +24 -27
- package/build/index.js +31 -16
- package/build/intrinsic-elements.d.ts +19 -0
- package/build/intrinsic-elements.js +1 -0
- package/build/jsx-runtime.d.ts +67 -0
- package/build/jsx-runtime.js +1 -0
- package/build/modules/animated/index.d.ts +41 -20
- package/build/modules/animated/index.js +20 -19
- package/build/modules/image/index.d.ts +3 -0
- package/build/modules/image/index.js +13 -0
- package/build/register.d.ts +1 -1
- package/build/register.js +13 -33
- package/build/renderer/index.js +108 -54
- package/build/runtime-helpers/index.d.ts +10 -1
- package/build/runtime-helpers/index.js +56 -22
- package/build/utils/normalize-attrs.js +3 -3
- package/intrinsic-tags.cjs +52 -0
- package/metro-vue-transformer.cjs +55 -345
- package/package.json +13 -9
- package/babel-lower-host-primitives.cjs +0 -225
- package/build/components/activity-indicator/index.android.d.ts +0 -4
- package/build/components/activity-indicator/index.android.js +0 -10
- package/build/components/activity-indicator/index.d.ts +0 -1
- package/build/components/activity-indicator/index.ios.d.ts +0 -4
- package/build/components/activity-indicator/index.ios.js +0 -10
- package/build/components/activity-indicator/index.js +0 -4
- package/build/components/activity-indicator/shared.d.ts +0 -8
- package/build/components/activity-indicator/shared.js +0 -48
- package/build/components/button.d.ts +0 -14
- package/build/components/button.js +0 -61
- package/build/components/image-background.d.ts +0 -9
- package/build/components/image-background.js +0 -103
- package/build/components/image.d.ts +0 -9
- package/build/components/image.js +0 -90
- package/build/components/input-accessory-view.js +0 -41
- package/build/components/pressable.d.ts +0 -58
- package/build/components/pressable.js +0 -233
- package/build/components/refresh-control.d.ts +0 -20
- package/build/components/refresh-control.js +0 -47
- package/build/components/safe-area-view.d.ts +0 -9
- package/build/components/safe-area-view.js +0 -33
- package/build/components/scroll-view/index.android.d.ts +0 -4
- package/build/components/scroll-view/index.android.js +0 -64
- package/build/components/scroll-view/index.d.ts +0 -1
- package/build/components/scroll-view/index.ios.d.ts +0 -4
- package/build/components/scroll-view/index.ios.js +0 -17
- package/build/components/scroll-view/index.js +0 -4
- package/build/components/scroll-view/shared.js +0 -271
- package/build/components/scroll-view/sticky-header.d.ts +0 -7
- package/build/components/scroll-view/sticky-header.js +0 -208
- package/build/components/switch/index.android.d.ts +0 -12
- package/build/components/switch/index.android.js +0 -13
- package/build/components/switch/index.d.ts +0 -2
- package/build/components/switch/index.ios.d.ts +0 -12
- package/build/components/switch/index.ios.js +0 -11
- package/build/components/switch/index.js +0 -5
- package/build/components/switch/shared.d.ts +0 -23
- package/build/components/switch/shared.js +0 -126
- package/build/components/text-input/index.d.ts +0 -24
- package/build/components/text-input/index.js +0 -260
- package/build/components/touchable-native-feedback.d.ts +0 -24
- package/build/components/touchable-native-feedback.js +0 -82
- package/build/components/touchable.d.ts +0 -50
- package/build/components/touchable.js +0 -281
- package/build/components.d.ts +0 -53
- package/build/components.js +0 -45
- package/build/composables/use-raw-attrs.d.ts +0 -1
- package/build/composables/use-raw-attrs.js +0 -26
- package/build/state-style.d.ts +0 -1
- package/build/state-style.js +0 -13
|
@@ -16,16 +16,8 @@ const {
|
|
|
16
16
|
parseCache,
|
|
17
17
|
compileScript,
|
|
18
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
19
|
} = require('@vue/compiler-sfc');
|
|
25
|
-
const {
|
|
26
|
-
createCompoundExpression,
|
|
27
|
-
createSimpleExpression,
|
|
28
|
-
} = require('@vue/compiler-core');
|
|
20
|
+
const { createCompoundExpression } = require('@vue/compiler-core');
|
|
29
21
|
|
|
30
22
|
// A bare-specifier type import (`import type { X } from '@symbiote-native/navigation/vue'`) needs
|
|
31
23
|
// real node_modules resolution to turn the specifier into a file path - compileScript's own `fs`
|
|
@@ -122,299 +114,38 @@ function createScopeClassNodeTransform(renames) {
|
|
|
122
114
|
};
|
|
123
115
|
}
|
|
124
116
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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;
|
|
117
|
+
// The parser's own condense() collapses a text node's multi-line whitespace to a single boundary
|
|
118
|
+
// space but never trims it: `<text>\n plain\n</text>` compiles to " plain " instead of "plain" -
|
|
119
|
+
// a real leading/trailing space on the rendered RN Text. Babel's JSX whitespace cleaner
|
|
120
|
+
// (cleanJSXElementLiteralChild, used by every other adapter) DOES trim this; Vue's does not, so we
|
|
121
|
+
// do it here, once, for every template. Matches Babel's own rule: only a text node whose SOURCE
|
|
122
|
+
// crosses a line break is touched, so a genuinely single-line `<text>Score: {{ x }}</text>` keeps
|
|
123
|
+
// its intentional trailing space untouched (see examples/vue-sfc's CompoundClassDemo.vue for the
|
|
124
|
+
// device-measured bug this closes).
|
|
125
|
+
function trimReflowedTextNodeTransform(node) {
|
|
126
|
+
if (node.type !== 1 /* NodeTypes.ELEMENT */ || node.children.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
const first = node.children[0];
|
|
129
|
+
if (
|
|
130
|
+
first.type === 2 /* NodeTypes.TEXT */ &&
|
|
131
|
+
first.loc.source.includes('\n') &&
|
|
132
|
+
first.content.startsWith(' ')
|
|
133
|
+
) {
|
|
134
|
+
first.content = first.content.slice(1);
|
|
319
135
|
}
|
|
320
|
-
|
|
136
|
+
const last = node.children[node.children.length - 1];
|
|
321
137
|
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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';
|
|
138
|
+
last.type === 2 /* NodeTypes.TEXT */ &&
|
|
139
|
+
last.loc.source.includes('\n') &&
|
|
140
|
+
last.content.endsWith(' ')
|
|
141
|
+
) {
|
|
142
|
+
last.content = last.content.slice(0, -1);
|
|
387
143
|
}
|
|
388
|
-
|
|
389
|
-
return resolved ? choice.intrinsic : entry.intrinsic;
|
|
390
144
|
}
|
|
391
145
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
}
|
|
146
|
+
// Shared with ./babel-jsx.cjs: Vue's two compilers must give one answer to element-vs-component.
|
|
147
|
+
const INTRINSIC_TAGS = require('./intrinsic-tags.cjs');
|
|
148
|
+
|
|
418
149
|
|
|
419
150
|
// A short, stable id per file, used as the SFC scope id regardless of whether the file has
|
|
420
151
|
// scoped styles - built on css-parser's shared hashFilePath so the algorithm isn't duplicated
|
|
@@ -485,10 +216,9 @@ async function compileStyleBlockContent(style, filename) {
|
|
|
485
216
|
|
|
486
217
|
async function compileSfc(src, filename) {
|
|
487
218
|
// `parse` memoizes its descriptor on (source, filename) in a module-global LRU, and the node
|
|
488
|
-
//
|
|
489
|
-
// `
|
|
490
|
-
//
|
|
491
|
-
// node is missing for element/if/for node`.
|
|
219
|
+
// scope-class transform below MUTATES that descriptor's template AST in place, replacing
|
|
220
|
+
// `prop.exp`. Handing the same object to a second compile therefore walks a half-transformed
|
|
221
|
+
// tree and generation dies with `Codegen node is missing for element/if/for node`.
|
|
492
222
|
//
|
|
493
223
|
// Metro compiles each file once per build, so the cache buys this caller nothing; the only place
|
|
494
224
|
// it was ever exercised is css-parser's golden-corpus determinism check, which compiles one
|
|
@@ -501,7 +231,15 @@ async function compileSfc(src, filename) {
|
|
|
501
231
|
// re-parse, which is what we need anyway.
|
|
502
232
|
|
|
503
233
|
parseCache.clear();
|
|
504
|
-
|
|
234
|
+
// element-vs-component is decided by the PARSER, not by the later `isCustomElement` in
|
|
235
|
+
// templateOptions — `compileScript` reuses this descriptor's already-parsed AST, where `tagType`
|
|
236
|
+
// is already fixed. So a hand-written `<pressable>` needs the answer here or it stays a component
|
|
237
|
+
// whatever templateOptions says, which is how it read until 2026-09-07:
|
|
238
|
+
// `resolveComponent("pressable")`, a blank subtree with no error.
|
|
239
|
+
const { descriptor, errors } = parse(src, {
|
|
240
|
+
filename,
|
|
241
|
+
templateParseOptions: { isCustomElement: tag => INTRINSIC_TAGS.has(tag) },
|
|
242
|
+
});
|
|
505
243
|
if (errors && errors.length > 0) {
|
|
506
244
|
throw new Error(
|
|
507
245
|
`Vue SFC parse error in ${filename}:\n${errors.map(String).join('\n')}`,
|
|
@@ -568,51 +306,23 @@ async function compileSfc(src, filename) {
|
|
|
568
306
|
}
|
|
569
307
|
|
|
570
308
|
// 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.
|
|
572
|
-
|
|
573
|
-
const lowerableTags = lowerableTagsIn(descriptor);
|
|
574
|
-
const nodeTransforms = [];
|
|
575
|
-
if (lowerableTags.size > 0)
|
|
576
|
-
nodeTransforms.push(
|
|
577
|
-
createHostPrimitiveLowering(lowerableTags),
|
|
578
|
-
);
|
|
309
|
+
// this file is scoped, so a .vue with only unscoped/no styles adds no runtime cost.
|
|
310
|
+
const nodeTransforms = [trimReflowedTextNodeTransform];
|
|
579
311
|
if (scopedClassNames.size > 0)
|
|
580
312
|
nodeTransforms.push(createScopeClassNodeTransform(scopedClassNames));
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
);
|
|
606
|
-
const templateOptions =
|
|
607
|
-
nodeTransforms.length > 0
|
|
608
|
-
? {
|
|
609
|
-
compilerOptions: {
|
|
610
|
-
nodeTransforms,
|
|
611
|
-
isCustomElement: tag =>
|
|
612
|
-
lowerableTags.has(tag) || loweredTags.has(tag),
|
|
613
|
-
},
|
|
614
|
-
}
|
|
615
|
-
: undefined;
|
|
313
|
+
// UNCONDITIONAL, and covering EVERY intrinsic rather than the ones this file happens to import.
|
|
314
|
+
// Both narrowings were live until 2026-09-07 and both broke a hand-written tag: a file importing
|
|
315
|
+
// no wrapper got no `isCustomElement` at all, and a file importing one got an answer scoped to
|
|
316
|
+
// that import, so `<pressable>` written by hand compiled to `resolveComponent("pressable")` —
|
|
317
|
+
// a blank subtree with no error — whichever way the file was written. `<switch v-model>` failed
|
|
318
|
+
// louder: `@vue/compiler-dom`'s transformModel rejects v-model on an element that is neither
|
|
319
|
+
// input/textarea/select nor a custom element, so it was a hard compile error.
|
|
320
|
+
const templateOptions = {
|
|
321
|
+
compilerOptions: {
|
|
322
|
+
...(nodeTransforms.length > 0 ? { nodeTransforms } : {}),
|
|
323
|
+
isCustomElement: tag => INTRINSIC_TAGS.has(tag),
|
|
324
|
+
},
|
|
325
|
+
};
|
|
616
326
|
|
|
617
327
|
// inlineTemplate folds the <template> render fn into setup(): one module, one `export
|
|
618
328
|
// default`. Only valid with <script setup>, which the canary uses.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/vue",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.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": {
|
|
@@ -45,15 +45,19 @@
|
|
|
45
45
|
"./metro-css-parser": "./metro-css-parser.cjs",
|
|
46
46
|
"./metro-vue-transformer": "./metro-vue-transformer.cjs",
|
|
47
47
|
"./babel-jsx": "./babel-jsx.cjs",
|
|
48
|
-
"./
|
|
49
|
-
"types": "./build/
|
|
50
|
-
"default": "./build/
|
|
48
|
+
"./jsx-runtime": {
|
|
49
|
+
"types": "./build/jsx-runtime.d.ts",
|
|
50
|
+
"default": "./build/jsx-runtime.js"
|
|
51
|
+
},
|
|
52
|
+
"./jsx-dev-runtime": {
|
|
53
|
+
"types": "./build/jsx-runtime.d.ts",
|
|
54
|
+
"default": "./build/jsx-runtime.js"
|
|
51
55
|
}
|
|
52
56
|
},
|
|
53
57
|
"files": [
|
|
54
58
|
"babel-jsx.cjs",
|
|
55
|
-
"babel-lower-host-primitives.cjs",
|
|
56
59
|
"build",
|
|
60
|
+
"intrinsic-tags.cjs",
|
|
57
61
|
"metro-css-parser.cjs",
|
|
58
62
|
"metro-vue-transformer.cjs"
|
|
59
63
|
],
|
|
@@ -66,18 +70,18 @@
|
|
|
66
70
|
"@vue/compiler-sfc": "^3.5.13",
|
|
67
71
|
"@vue/runtime-core": "^3.5.13",
|
|
68
72
|
"typescript": "~6.0.0",
|
|
69
|
-
"@symbiote-native/components": "
|
|
73
|
+
"@symbiote-native/components": "3.0.0",
|
|
70
74
|
"@symbiote-native/css-parser": "0.5.0"
|
|
71
75
|
},
|
|
72
76
|
"peerDependencies": {
|
|
73
77
|
"vue": ">=3.5.0",
|
|
74
78
|
"react-native": ">=0.86",
|
|
75
|
-
"@symbiote-native/engine": "^0.
|
|
79
|
+
"@symbiote-native/engine": "^1.0.0"
|
|
76
80
|
},
|
|
77
81
|
"devDependencies": {
|
|
78
82
|
"@babel/core": "^7.25.2",
|
|
79
|
-
"@symbiote-native/engine": "0.
|
|
80
|
-
"@symbiote-native/test-utils": "0.3.
|
|
83
|
+
"@symbiote-native/engine": "1.0.0",
|
|
84
|
+
"@symbiote-native/test-utils": "0.3.1"
|
|
81
85
|
},
|
|
82
86
|
"scripts": {
|
|
83
87
|
"typecheck": "tsc --build",
|