@symbiote-native/vue 0.4.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 +24 -9
- package/babel-jsx.cjs +53 -0
- package/babel-lower-host-primitives.cjs +225 -0
- package/build/bootstrap.js +1 -1
- package/build/components/button.js +4 -2
- package/build/components/flat-list/index.js +14 -5
- package/build/components/image-background.js +4 -5
- package/build/components/image.js +3 -1
- package/build/components/keyboard-avoiding-view.js +39 -14
- package/build/components/modal/index.js +5 -2
- package/build/components/pressable.d.ts +1 -1
- package/build/components/pressable.js +39 -20
- package/build/components/safe-area-view.d.ts +1 -0
- package/build/components/scroll-view/index.android.d.ts +1 -1
- package/build/components/scroll-view/index.android.js +3 -1
- package/build/components/scroll-view/index.ios.d.ts +1 -1
- package/build/components/scroll-view/index.ios.js +3 -1
- package/build/components/scroll-view/shared.js +33 -13
- package/build/components/scroll-view/sticky-header.js +18 -5
- package/build/components/section-list/index.js +4 -2
- package/build/components/switch/shared.js +12 -3
- package/build/components/text-input/index.js +25 -2
- package/build/components/touchable-native-feedback.js +11 -4
- package/build/components/touchable.d.ts +7 -1
- package/build/components/touchable.js +184 -33
- package/build/components/virtualized-list/index.js +42 -20
- package/build/components/virtualized-section-list/index.d.ts +5 -0
- package/build/components/virtualized-section-list/index.js +33 -4
- package/build/components.d.ts +2 -0
- package/build/components.js +25 -4
- package/build/composables/use-raw-attrs.d.ts +1 -0
- package/build/composables/use-raw-attrs.js +26 -0
- package/build/create-portal/index.d.ts +28 -0
- package/build/create-portal/index.js +42 -0
- package/build/host-instance/index.js +7 -6
- package/build/index.d.ts +9 -6
- package/build/index.js +14 -3
- package/build/modules/animated/create-animated-component.d.ts +1 -1
- package/build/modules/animated/create-animated-component.js +27 -5
- package/build/modules/animated/index.d.ts +2 -0
- package/build/modules/animated/index.js +18 -5
- package/build/modules/app-registry/index.js +4 -2
- package/build/modules/status-bar.js +4 -2
- package/build/register.d.ts +1 -0
- package/build/register.js +35 -0
- package/build/render.d.ts +3 -1
- package/build/render.js +62 -1
- package/build/renderer/index.js +58 -9
- package/build/runtime-helpers/index.d.ts +48 -24
- package/build/runtime-helpers/index.js +314 -30
- 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 -13
- package/metro-css-parser.cjs +2 -1
- package/metro-vue-transformer.cjs +502 -101
- 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`
|
|
@@ -23,15 +36,13 @@ registerTS(() => require('typescript'));
|
|
|
23
36
|
// Required directly (not via the ./metro-css-parser public subpath, which exists for CONSUMERS)
|
|
24
37
|
// so it resolves from this package's own node_modules under pnpm.
|
|
25
38
|
const {
|
|
26
|
-
classTokensIn,
|
|
27
39
|
compile: compilePreprocessor,
|
|
28
40
|
compileCssFile,
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
compileCssModule,
|
|
42
|
+
compileCssToRules,
|
|
43
|
+
compileScopedCss,
|
|
31
44
|
hashFilePath,
|
|
32
45
|
isStyleFile,
|
|
33
|
-
kebabToCamel,
|
|
34
|
-
parseCSS,
|
|
35
46
|
resolveUpstreamTransformer,
|
|
36
47
|
} = require('@symbiote-native/css-parser');
|
|
37
48
|
|
|
@@ -53,36 +64,43 @@ const compileScriptFs = {
|
|
|
53
64
|
},
|
|
54
65
|
};
|
|
55
66
|
|
|
56
|
-
// Rewrites a Vue template AST so every `class`/`:class` binding
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
67
|
+
// Rewrites a Vue template AST so every `class`/`:class` binding names a class by the name
|
|
68
|
+
// lightningcss RENAMED it to. AST-level, not a raw-text regex: Vue merges a static class= and a
|
|
69
|
+
// dynamic :class= on the same element into ONE codegen entry, and text substitution can't
|
|
70
|
+
// reproduce that merge safely - letting Vue's own transformElement do the merge on our rewritten
|
|
71
|
+
// nodes reuses that logic.
|
|
72
|
+
//
|
|
73
|
+
// `renames` is compileScopedCss's own name map (authored name -> renamed name), NOT a set of
|
|
74
|
+
// names this file re-suffixes. That is the point of the migration: the rewriter and the style
|
|
75
|
+
// compiler used to derive the scoped name independently, so any disagreement between them was
|
|
76
|
+
// silent. Now there is one source and this half only reads it. One entry per class, keyed as
|
|
77
|
+
// AUTHORED — `class="section-label"` is the lookup, `sectionLabel` is not a second spelling of it.
|
|
61
78
|
//
|
|
62
79
|
// A static class="foo bar" (AttributeNode, prop.type === 6) resolves to its final string here
|
|
63
|
-
// at compile time - no runtime call needed.
|
|
64
|
-
// css
|
|
80
|
+
// at compile time - no runtime call needed. A token the map does not carry belongs to another
|
|
81
|
+
// file (App.css, a `:global()` escape hatch, a parent's class) and passes through untouched.
|
|
65
82
|
//
|
|
66
83
|
// A dynamic :class="expr" (bind DirectiveNode, prop.type === 7) targeting `class`: by the time
|
|
67
84
|
// our transform runs, Vue's own transformExpression has already turned prop.exp into either a
|
|
68
85
|
// COMPOUND_EXPRESSION (type 8) or, for a bare identifier binding, a SIMPLE_EXPRESSION (type 4).
|
|
69
86
|
// createCompoundExpression wraps the original exp node as-is inside
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
function createScopeClassNodeTransform(
|
|
87
|
+
// renameClassTokens(<original>, __scopedClassNames), so codegen reproduces the original
|
|
88
|
+
// expression unchanged - no per-shape branching needed, and a fully opaque runtime value
|
|
89
|
+
// (`:class="dynamicClass"`) still resolves against the same map at runtime.
|
|
90
|
+
function createScopeClassNodeTransform(renames) {
|
|
74
91
|
return function scopeClassNodeTransform(node) {
|
|
75
92
|
if (node.type !== 1 /* NodeTypes.ELEMENT */) return;
|
|
76
93
|
|
|
77
94
|
for (const prop of node.props) {
|
|
78
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
prop.type === 6 /* NodeTypes.ATTRIBUTE */ &&
|
|
97
|
+
prop.name === 'class' &&
|
|
98
|
+
prop.value
|
|
99
|
+
) {
|
|
79
100
|
prop.value.content = prop.value.content
|
|
80
101
|
.split(/\s+/)
|
|
81
102
|
.filter(Boolean)
|
|
82
|
-
.map(token =>
|
|
83
|
-
const camelToken = kebabToCamel(token);
|
|
84
|
-
return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
|
|
85
|
-
})
|
|
103
|
+
.map(token => renames.get(token) ?? token)
|
|
86
104
|
.join(' ');
|
|
87
105
|
continue;
|
|
88
106
|
}
|
|
@@ -96,7 +114,7 @@ function createScopeClassNodeTransform(localNames, scopeId) {
|
|
|
96
114
|
prop.exp
|
|
97
115
|
) {
|
|
98
116
|
prop.exp = createCompoundExpression(
|
|
99
|
-
['__scopeClass(', prop.exp, ',
|
|
117
|
+
['__scopeClass(', prop.exp, ', __scopedClassNames)'],
|
|
100
118
|
prop.exp.loc,
|
|
101
119
|
);
|
|
102
120
|
}
|
|
@@ -104,6 +122,300 @@ function createScopeClassNodeTransform(localNames, scopeId) {
|
|
|
104
122
|
};
|
|
105
123
|
}
|
|
106
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
|
+
|
|
107
419
|
// A short, stable id per file, used as the SFC scope id regardless of whether the file has
|
|
108
420
|
// scoped styles - built on css-parser's shared hashFilePath so the algorithm isn't duplicated
|
|
109
421
|
// against the standalone .module.css compiler's identical need.
|
|
@@ -111,6 +423,35 @@ function scopeIdFor(filename) {
|
|
|
111
423
|
return 'data-v-' + hashFilePath(filename);
|
|
112
424
|
}
|
|
113
425
|
|
|
426
|
+
// The local the compiled component lands in when a <style module> block needs __cssModules hung
|
|
427
|
+
// off it (compileScript's genDefaultAs). Same role as @vitejs/plugin-vue's own `_sfc_main`.
|
|
428
|
+
const SFC_COMPONENT_VAR = '__sfc__';
|
|
429
|
+
|
|
430
|
+
// Blocks CONCATENATE - each is compiled on its own, so every block's rules restart at order 0 and
|
|
431
|
+
// a later block would tie with an earlier one on the cascade's source-order tie-break, silently
|
|
432
|
+
// reordering equally-specific rules. Renumbering on append gives the file one monotonic sequence,
|
|
433
|
+
// which is what a single stylesheet holding the concatenated blocks would have produced.
|
|
434
|
+
function appendRules(target, rules) {
|
|
435
|
+
for (const rule of rules) {
|
|
436
|
+
target.push({ ...rule, order: target.length });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// `combinators` is compile-time-only - the registry matches a rule by token SUBSET and never reads
|
|
441
|
+
// it - so it is stripped rather than shipped in every app bundle. Twin of css-parser's own
|
|
442
|
+
// serializeRules (core/css-parser/src/metro-css-module/index.ts), which does this for a standalone
|
|
443
|
+
// style file.
|
|
444
|
+
function serializeRules(rules) {
|
|
445
|
+
return JSON.stringify(
|
|
446
|
+
rules.map(({ tokens, specificity, order, style }) => ({
|
|
447
|
+
tokens,
|
|
448
|
+
specificity,
|
|
449
|
+
order,
|
|
450
|
+
style,
|
|
451
|
+
})),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
114
455
|
// An SFC style block's `lang` attribute names a preprocessor language directly (`lang="scss"`),
|
|
115
456
|
// unlike a standalone file, which is identified by its extension — so this is its own small
|
|
116
457
|
// lookup rather than reusing detectLanguage(), which is extension-keyed.
|
|
@@ -128,7 +469,9 @@ async function compileStyleBlockContent(style, filename) {
|
|
|
128
469
|
|
|
129
470
|
const preprocessorLang = SFC_STYLE_LANG_TO_PREPROCESSOR.get(style.lang);
|
|
130
471
|
if (!preprocessorLang) {
|
|
131
|
-
throw new Error(
|
|
472
|
+
throw new Error(
|
|
473
|
+
`SFC style lang="${style.lang}" not supported yet — plain CSS only`,
|
|
474
|
+
);
|
|
132
475
|
}
|
|
133
476
|
|
|
134
477
|
// Sass' `.sass` indented syntax and `.scss` syntax share one compiler entry point that picks
|
|
@@ -141,12 +484,33 @@ async function compileStyleBlockContent(style, filename) {
|
|
|
141
484
|
}
|
|
142
485
|
|
|
143
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();
|
|
144
504
|
const { descriptor, errors } = parse(src, { filename });
|
|
145
505
|
if (errors && errors.length > 0) {
|
|
146
|
-
throw new Error(
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Vue SFC parse error in ${filename}:\n${errors.map(String).join('\n')}`,
|
|
508
|
+
);
|
|
147
509
|
}
|
|
148
510
|
if (descriptor.scriptSetup == null && descriptor.script == null) {
|
|
149
|
-
throw new Error(
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Vue SFC ${filename} has no <script> / <script setup> block`,
|
|
513
|
+
);
|
|
150
514
|
}
|
|
151
515
|
|
|
152
516
|
const scopeId = scopeIdFor(filename);
|
|
@@ -154,93 +518,118 @@ async function compileSfc(src, filename) {
|
|
|
154
518
|
// descriptor.styles is already parsed by @vue/compiler-sfc (one entry per <style> block,
|
|
155
519
|
// content pre-trimmed, scoped as a plain boolean) - no need to re-extract with a regex.
|
|
156
520
|
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
521
|
+
// Both scoped forms are ONE mechanism, lightningcss's CSS-Modules renaming, differing only in
|
|
522
|
+
// the pattern string (core/css-parser/src/scoped-classes.ts):
|
|
523
|
+
//
|
|
524
|
+
// <style scoped> [local]__data-v-<hash> compileScopedCss - `.card` -> `card__data-v-h`
|
|
525
|
+
// <style module> [local]__module__<hash> compileCssModule - the same call a standalone
|
|
526
|
+
// .module.css takes, so the two cannot diverge
|
|
527
|
+
// <style> not renamed at all compileCssToRules, classes register globally
|
|
162
528
|
//
|
|
163
|
-
//
|
|
529
|
+
// Renaming is our equivalent of Vue's `data-v-hash` ATTRIBUTE (we have no DOM and no
|
|
530
|
+
// attribute-selector matching, so a scope can only be expressed in the name), and `:global(...)`
|
|
531
|
+
// needs no handling here at all: a name lightningcss did not rename is global by definition.
|
|
164
532
|
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const styles = {};
|
|
174
|
-
const localScopedNames = new Set();
|
|
533
|
+
// Multiple blocks cascade last-block-wins, same as CSS, after each block is scoped
|
|
534
|
+
// independently — carried by the rules' `order`, which appendRules renumbers across blocks.
|
|
535
|
+
//
|
|
536
|
+
// A module block's classes are NEVER auto-applied to a literal class="..." (opt-in via $style.x
|
|
537
|
+
// only), so they stay out of the template rewriter's name map; its output is the name->scopedName
|
|
538
|
+
// map instead, emitted as a preamble const AND attached to the component as __cssModules.
|
|
539
|
+
const rules = [];
|
|
540
|
+
const scopedClassNames = new Map();
|
|
175
541
|
const cssModuleBindings = new Map();
|
|
176
542
|
|
|
177
543
|
for (const style of descriptor.styles) {
|
|
178
|
-
// Reduces a preprocessor block to plain CSS BEFORE the
|
|
179
|
-
//
|
|
544
|
+
// Reduces a preprocessor block to plain CSS BEFORE the renaming below runs - the renaming is
|
|
545
|
+
// language-agnostic, it only ever sees plain CSS.
|
|
180
546
|
const content = await compileStyleBlockContent(style, filename);
|
|
181
|
-
const parsed = parseCSS(content, { filename });
|
|
182
547
|
|
|
183
548
|
if (style.module) {
|
|
184
|
-
const bindingName =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
classMap[className] = registeredName;
|
|
193
|
-
styles[registeredName] = { ...styles[registeredName], ...props };
|
|
194
|
-
}
|
|
195
|
-
cssModuleBindings.set(bindingName, classMap);
|
|
549
|
+
const bindingName =
|
|
550
|
+
typeof style.module === 'string' ? style.module : '$style';
|
|
551
|
+
const compiled = compileCssModule(content, filename);
|
|
552
|
+
appendRules(rules, compiled.rules);
|
|
553
|
+
cssModuleBindings.set(bindingName, {
|
|
554
|
+
...cssModuleBindings.get(bindingName),
|
|
555
|
+
...compiled.classMap,
|
|
556
|
+
});
|
|
196
557
|
} else if (style.scoped) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
// `.card :global(.reset)` the KEY (`cardReset`) is this file's own, because `.card` is,
|
|
205
|
-
// but `reset` was written precisely to name markup this file does not own. Suffixing it
|
|
206
|
-
// along with the rest of its chain scope-mangles the escape hatch into matching nothing.
|
|
207
|
-
const globalTokens = globalClassTokensIn(content, { filename });
|
|
208
|
-
for (const [className, props] of Object.entries(parsed)) {
|
|
209
|
-
const isExempt = exemptFromScope.has(className);
|
|
210
|
-
const registeredName = isExempt ? className : `${className}__${scopeId}`;
|
|
211
|
-
if (!isExempt) {
|
|
212
|
-
localScopedNames.add(className);
|
|
213
|
-
for (const token of tokensByName.get(className) ?? []) {
|
|
214
|
-
if (!globalTokens.has(token)) localScopedNames.add(token);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
styles[registeredName] = { ...styles[registeredName], ...props };
|
|
218
|
-
}
|
|
558
|
+
const compiled = compileScopedCss(content, {
|
|
559
|
+
filename,
|
|
560
|
+
pattern: `[local]__${scopeId}`,
|
|
561
|
+
});
|
|
562
|
+
appendRules(rules, compiled.rules);
|
|
563
|
+
for (const [name, renamed] of compiled.names)
|
|
564
|
+
scopedClassNames.set(name, renamed);
|
|
219
565
|
} else {
|
|
220
|
-
|
|
221
|
-
styles[className] = { ...styles[className], ...props };
|
|
222
|
-
}
|
|
566
|
+
appendRules(rules, compileCssToRules(content, { filename }).rules);
|
|
223
567
|
}
|
|
224
568
|
}
|
|
225
569
|
|
|
226
|
-
//
|
|
227
|
-
// 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
|
+
);
|
|
228
606
|
const templateOptions =
|
|
229
|
-
|
|
607
|
+
nodeTransforms.length > 0
|
|
230
608
|
? {
|
|
231
609
|
compilerOptions: {
|
|
232
|
-
nodeTransforms
|
|
610
|
+
nodeTransforms,
|
|
611
|
+
isCustomElement: tag =>
|
|
612
|
+
lowerableTags.has(tag) || loweredTags.has(tag),
|
|
233
613
|
},
|
|
234
614
|
}
|
|
235
615
|
: undefined;
|
|
236
616
|
|
|
237
617
|
// inlineTemplate folds the <template> render fn into setup(): one module, one `export
|
|
238
618
|
// default`. Only valid with <script setup>, which the canary uses.
|
|
619
|
+
//
|
|
620
|
+
// genDefaultAs turns that `export default {...}` into `const __sfc__ = {...}`, so a <style
|
|
621
|
+
// module> block can hang __cssModules off the component OPTIONS object before the module
|
|
622
|
+
// exports it (the same thing @vitejs/plugin-vue does). Vue resolves a template's `$style` /
|
|
623
|
+
// `classes` off `instance.type.__cssModules`, NOT off module scope, so without this the
|
|
624
|
+
// emitted const is unreachable from the template and `$style.card` throws at render. Only
|
|
625
|
+
// requested when a module block exists, so every other .vue file keeps identical output.
|
|
626
|
+
const hasCssModules = cssModuleBindings.size > 0;
|
|
239
627
|
const compiled = compileScript(descriptor, {
|
|
240
628
|
id: scopeId,
|
|
241
629
|
inlineTemplate: true,
|
|
242
630
|
templateOptions,
|
|
243
631
|
fs: compileScriptFs,
|
|
632
|
+
...(hasCssModules ? { genDefaultAs: SFC_COMPONENT_VAR } : {}),
|
|
244
633
|
});
|
|
245
634
|
// Retargets every Vue import (compiler-injected helpers AND the user's own `from 'vue'`) at
|
|
246
635
|
// the runtime-helpers shim - no vue/runtime-dom in a native bundle.
|
|
@@ -249,35 +638,47 @@ async function compileSfc(src, filename) {
|
|
|
249
638
|
'from "@symbiote-native/vue/runtime-helpers"',
|
|
250
639
|
);
|
|
251
640
|
|
|
252
|
-
if (
|
|
641
|
+
if (rules.length === 0 && !hasCssModules) return code;
|
|
253
642
|
|
|
254
|
-
// Only a scoped file needs
|
|
643
|
+
// Only a scoped file needs the runtime rename helper and its per-file map, so these stay
|
|
255
644
|
// unimported for every non-scoped .vue file.
|
|
256
645
|
const engineImports =
|
|
257
|
-
|
|
646
|
+
scopedClassNames.size > 0
|
|
647
|
+
? 'registerRules, renameClassTokens as __scopeClass'
|
|
648
|
+
: 'registerRules';
|
|
258
649
|
|
|
259
|
-
const preamble = [
|
|
260
|
-
|
|
650
|
+
const preamble = [
|
|
651
|
+
`import { ${engineImports} } from '@symbiote-native/engine';`,
|
|
652
|
+
`registerRules(${serializeRules(rules)});`,
|
|
653
|
+
];
|
|
654
|
+
if (scopedClassNames.size > 0) {
|
|
261
655
|
preamble.push(
|
|
262
|
-
`const
|
|
263
|
-
`const __scopeId = ${JSON.stringify(scopeId)};`,
|
|
656
|
+
`const __scopedClassNames = ${JSON.stringify(Object.fromEntries(scopedClassNames))};`,
|
|
264
657
|
);
|
|
265
658
|
}
|
|
266
659
|
// Each <style module> binding becomes a top-level const holding its name->scopedName map,
|
|
267
|
-
// placed before the
|
|
268
|
-
//
|
|
660
|
+
// placed before the component so it's a closed-over module-scope variable usable from
|
|
661
|
+
// <script setup> code itself; the __cssModules tail below is what makes the TEMPLATE see it.
|
|
269
662
|
for (const [bindingName, classMap] of cssModuleBindings) {
|
|
270
663
|
preamble.push(`const ${bindingName} = ${JSON.stringify(classMap)};`);
|
|
271
664
|
}
|
|
272
665
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
666
|
+
const parts = [preamble.join('\n'), code];
|
|
667
|
+
if (hasCssModules) {
|
|
668
|
+
const bindings = [...cssModuleBindings.keys()]
|
|
669
|
+
.map(name => `${JSON.stringify(name)}: ${name}`)
|
|
670
|
+
.join(', ');
|
|
671
|
+
parts.push(
|
|
672
|
+
`${SFC_COMPONENT_VAR}.__cssModules = { ${bindings} };`,
|
|
673
|
+
`export default ${SFC_COMPONENT_VAR};`,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return parts.join('\n') + '\n';
|
|
277
678
|
}
|
|
278
679
|
|
|
279
680
|
// Exported separately from `transform` so tests can assert on the compiled SFC output
|
|
280
|
-
// (imports, injected `
|
|
681
|
+
// (imports, injected `registerRules` call) without driving the full upstream RN Babel preset.
|
|
281
682
|
module.exports.compileSfc = compileSfc;
|
|
282
683
|
|
|
283
684
|
// Async uniformly, including branches that never touch a preprocessor: compileSfc() itself is
|