@fluixi/compiler 1.0.0-alpha.69 → 1.0.0-alpha.70
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/dist/babel-NY6FXXLW.mjs +2 -0
- package/dist/codegen/backend.cjs +1 -1
- package/dist/codegen/backend.d.ts +14 -0
- package/dist/codegen/backend.d.ts.map +1 -1
- package/dist/codegen/backends/imperative.cjs +1 -1
- package/dist/codegen/backends/imperative.d.ts +10 -1
- package/dist/codegen/backends/imperative.d.ts.map +1 -1
- package/dist/codegen/backends/imperative.js +127 -16
- package/dist/codegen/backends/imperative.mjs +1 -1
- package/dist/codegen/partial-template.cjs +1 -0
- package/dist/codegen/partial-template.d.ts +62 -0
- package/dist/codegen/partial-template.d.ts.map +1 -0
- package/dist/codegen/partial-template.js +0 -0
- package/dist/codegen/partial-template.mjs +1 -0
- package/dist/codegen/serialize-static.cjs +1 -0
- package/dist/codegen/serialize-static.d.ts +23 -0
- package/dist/codegen/serialize-static.d.ts.map +1 -0
- package/dist/codegen/serialize-static.js +140 -0
- package/dist/codegen/serialize-static.mjs +1 -0
- package/dist/frontend/babel/build-ir-lit.cjs +1 -1
- package/dist/frontend/babel/build-ir-lit.d.ts.map +1 -1
- package/dist/frontend/babel/build-ir-lit.js +5 -1
- package/dist/frontend/babel/build-ir-lit.mjs +1 -1
- package/dist/frontend/babel/build-ir.cjs +1 -1
- package/dist/frontend/babel/build-ir.d.ts.map +1 -1
- package/dist/frontend/babel/build-ir.js +36 -8
- package/dist/frontend/babel/build-ir.mjs +1 -1
- package/dist/frontend/babel/index.cjs +2 -2
- package/dist/frontend/babel/index.mjs +2 -2
- package/dist/frontend/babel/lower-template.cjs +1 -1
- package/dist/frontend/babel/lower-template.d.ts.map +1 -1
- package/dist/frontend/babel/lower-template.js +34 -5
- package/dist/frontend/babel/lower-template.mjs +1 -1
- package/dist/frontend/babel/plugin.cjs +2 -2
- package/dist/frontend/babel/plugin.d.ts +4 -0
- package/dist/frontend/babel/plugin.d.ts.map +1 -1
- package/dist/frontend/babel/plugin.js +258 -50
- package/dist/frontend/babel/plugin.mjs +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.mjs +1 -1
- package/dist/integrations.cjs +8 -8
- package/dist/integrations.d.ts +6 -0
- package/dist/integrations.d.ts.map +1 -1
- package/dist/integrations.js +6 -0
- package/dist/integrations.mjs +4 -4
- package/dist/ir/nodes.cjs +1 -1
- package/dist/ir/nodes.d.ts +43 -0
- package/dist/ir/nodes.d.ts.map +1 -1
- package/dist/optimize/analyze-static.cjs +1 -0
- package/dist/optimize/analyze-static.d.ts +40 -0
- package/dist/optimize/analyze-static.d.ts.map +1 -0
- package/dist/optimize/analyze-static.js +131 -0
- package/dist/optimize/analyze-static.mjs +1 -0
- package/dist/optimize/collapse-strategies.cjs +1 -0
- package/dist/optimize/collapse-strategies.d.ts +31 -0
- package/dist/optimize/collapse-strategies.d.ts.map +1 -0
- package/dist/optimize/collapse-strategies.js +33 -0
- package/dist/optimize/collapse-strategies.mjs +1 -0
- package/dist/options.cjs +1 -1
- package/dist/options.d.ts +18 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/resolve/builtins.cjs +1 -0
- package/dist/resolve/builtins.d.ts +69 -0
- package/dist/resolve/builtins.d.ts.map +1 -0
- package/dist/resolve/builtins.js +64 -0
- package/dist/resolve/builtins.mjs +1 -0
- package/dist/resolve/load-directive.cjs +1 -0
- package/dist/resolve/load-directive.d.ts +54 -0
- package/dist/resolve/load-directive.d.ts.map +1 -0
- package/dist/resolve/load-directive.js +93 -0
- package/dist/resolve/load-directive.mjs +1 -0
- package/dist/resolve/resolve-ir.cjs +1 -0
- package/dist/resolve/resolve-ir.d.ts +54 -0
- package/dist/resolve/resolve-ir.d.ts.map +1 -0
- package/dist/resolve/resolve-ir.js +58 -0
- package/dist/resolve/resolve-ir.mjs +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/dist/babel-UKVEBBZL.mjs +0 -2
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { declare } from '@babel/helper-plugin-utils';
|
|
14
14
|
import { createComponentResolver, } from '../../resolve/component-resolver.js';
|
|
15
|
+
import { resolveComponentSources } from '../../resolve/resolve-ir.js';
|
|
16
|
+
import { collapseStrategies } from '../../optimize/collapse-strategies.js';
|
|
17
|
+
import { EAGER, isDeferred, isLoadDirective, parseLoadDirective, LoadDirectiveError, } from '../../resolve/load-directive.js';
|
|
18
|
+
import { DOM_CONTROL_FLOW, CORE_CONTROL_FLOW, ROUTER_COMPONENTS as ROUTER_COMPONENT_NAMES, } from '../../resolve/builtins.js';
|
|
15
19
|
import { types as t, template, } from '@babel/core';
|
|
16
20
|
import { buildIR } from './build-ir.js';
|
|
17
21
|
import { buildIRFromLit } from './build-ir-lit.js';
|
|
@@ -19,27 +23,17 @@ import { imperativeBackend } from '../../codegen/backends/imperative.js';
|
|
|
19
23
|
// ============================================================================
|
|
20
24
|
// Constants
|
|
21
25
|
// ============================================================================
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'Match',
|
|
28
|
-
'Portal',
|
|
29
|
-
'Dynamic',
|
|
30
|
-
'ErrorBoundary',
|
|
31
|
-
'Suspense',
|
|
32
|
-
'SuspenseList',
|
|
33
|
-
'Await',
|
|
34
|
-
]);
|
|
26
|
+
// The names come from `resolve/builtins.ts`, which publishes them for the TS plugin,
|
|
27
|
+
// the language server and codemods — tools that need to know where `<Show>` comes from
|
|
28
|
+
// but must not re-derive it. What stays here is the part that is genuinely codegen:
|
|
29
|
+
// which names get special lowering, reactive-getter props, and memo exemption.
|
|
30
|
+
const CONTROL_FLOW_COMPONENTS = new Set([...DOM_CONTROL_FLOW, ...CORE_CONTROL_FLOW]);
|
|
35
31
|
// Control-flow components that live in @fluixi/core, NOT @fluixi/dom — they must
|
|
36
32
|
// be auto-imported from core (dom doesn't export them).
|
|
37
|
-
const CORE_ONLY_CONTROL_FLOW = new Set(
|
|
33
|
+
const CORE_ONLY_CONTROL_FLOW = new Set(CORE_CONTROL_FLOW);
|
|
38
34
|
// Router components auto-imported (from routerModule) the same way control flow is:
|
|
39
35
|
// use `<Router>`/`<Outlet>`/… in a template and the import is injected if absent.
|
|
40
|
-
|
|
41
|
-
// call, not a tag, so neither can be auto-imported this way.)
|
|
42
|
-
const ROUTER_COMPONENTS = new Set(['Router', 'Outlet', 'Redirect', 'Link']);
|
|
36
|
+
const ROUTER_COMPONENTS = new Set(ROUTER_COMPONENT_NAMES);
|
|
43
37
|
// Control-flow components read a single reactive *value* prop inside their own tracking
|
|
44
38
|
// scope. Emit that prop as a getter (like `children`) so reading it re-runs the caller's
|
|
45
39
|
// expression — otherwise `each={items()}` / `when={cond()}` snapshot once and never update.
|
|
@@ -85,7 +79,7 @@ const DELEGATABLE_EVENTS = [
|
|
|
85
79
|
// ============================================================================
|
|
86
80
|
export default declare((api, options = {}) => {
|
|
87
81
|
api.assertVersion(7);
|
|
88
|
-
const { runtime = 'automatic', importSource = '@fluixi/jsx', pragma = 'jsx', pragmaFrag = 'Fragment', development = false, detectReactivity = true, useLitHTML = true, hoistStatics = true, delegateEvents = true, delegatedEvents = DELEGATABLE_EVENTS, optimizeControlFlow = true, sourceMaps = true, signalModule = '@fluixi/dom', controlFlowModule = '@fluixi/dom', routerModule = '@fluixi/core/router-next', reactiveModule = '@fluixi/reactive/signal', autoShowTransform = false, backend = 'imperative', codegen = 'inline',
|
|
82
|
+
const { runtime = 'automatic', importSource = '@fluixi/jsx', pragma = 'jsx', pragmaFrag = 'Fragment', development = false, detectReactivity = true, useLitHTML = true, hoistStatics = true, delegateEvents = true, delegatedEvents = DELEGATABLE_EVENTS, optimizeControlFlow = true, sourceMaps = true, signalModule = '@fluixi/dom', controlFlowModule = '@fluixi/dom', routerModule = '@fluixi/core/router-next', reactiveModule = '@fluixi/reactive/signal', autoShowTransform = false, backend = 'imperative', codegen = 'inline', templateClone = true, partialTemplates = true,
|
|
89
83
|
// `both` by default: JSX and html`` are compiled out of the box, so
|
|
90
84
|
// `fluixi()` needs no `format` option. Injected imports are deduped, so a
|
|
91
85
|
// file mixing the two styles won't redeclare a binding.
|
|
@@ -99,6 +93,10 @@ export default declare((api, options = {}) => {
|
|
|
99
93
|
codegen,
|
|
100
94
|
irImports: new Set(),
|
|
101
95
|
libModule: '@fluixi/core',
|
|
96
|
+
controlFlowModule,
|
|
97
|
+
routerModule,
|
|
98
|
+
templateClone,
|
|
99
|
+
partialTemplates,
|
|
102
100
|
hasJSX: false,
|
|
103
101
|
hasReactivity: false,
|
|
104
102
|
hasLitHTML: false,
|
|
@@ -107,9 +105,11 @@ export default declare((api, options = {}) => {
|
|
|
107
105
|
needsCreateMemo: false,
|
|
108
106
|
controlFlowComponents: new Set(),
|
|
109
107
|
resolvedComponents: new Map(),
|
|
108
|
+
deferredComponents: new Map(),
|
|
110
109
|
componentResolver,
|
|
111
110
|
delegatedEvents: new Set(),
|
|
112
111
|
staticElements: new Map(),
|
|
112
|
+
staticTemplates: new Map(),
|
|
113
113
|
staticCounter: 0,
|
|
114
114
|
};
|
|
115
115
|
return {
|
|
@@ -146,12 +146,18 @@ export default declare((api, options = {}) => {
|
|
|
146
146
|
needsCreateMemo: false,
|
|
147
147
|
controlFlowComponents: new Set(),
|
|
148
148
|
resolvedComponents: new Map(),
|
|
149
|
+
deferredComponents: new Map(),
|
|
149
150
|
componentResolver,
|
|
150
151
|
delegatedEvents: new Set(),
|
|
151
152
|
staticElements: new Map(),
|
|
153
|
+
staticTemplates: new Map(),
|
|
152
154
|
autoShow: autoShowTransform,
|
|
153
155
|
staticCounter: 0,
|
|
154
156
|
libModule: '@fluixi/core',
|
|
157
|
+
controlFlowModule,
|
|
158
|
+
routerModule,
|
|
159
|
+
templateClone,
|
|
160
|
+
partialTemplates,
|
|
155
161
|
backend,
|
|
156
162
|
codegen,
|
|
157
163
|
irImports: new Set(),
|
|
@@ -219,7 +225,10 @@ export default declare((api, options = {}) => {
|
|
|
219
225
|
imports.push(t.importDeclaration(specifiers, t.stringLiteral(signalModule)));
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
|
-
else if (state.hasJSX && runtime === 'automatic') {
|
|
228
|
+
else if (state.hasJSX && runtime === 'automatic' && backend !== 'imperative') {
|
|
229
|
+
// Not under the imperative backend: `codegen: 'ir'` lands here because the
|
|
230
|
+
// branch above excludes it, and emitted an unused jsx/jsxs/Fragment import.
|
|
231
|
+
// The IR path produces runtime calls, never JSX factory calls.
|
|
223
232
|
// Add JSX runtime imports
|
|
224
233
|
const specifiers = [
|
|
225
234
|
t.importSpecifier(t.identifier('jsx'), t.identifier('jsx')),
|
|
@@ -270,6 +279,28 @@ export default declare((api, options = {}) => {
|
|
|
270
279
|
// Grouped by module so several components from one package share a single
|
|
271
280
|
// declaration, and emitted with the local name equal to the tag — the
|
|
272
281
|
// generated code refers to the tag, not to the export.
|
|
282
|
+
// Deferred components become value bindings, not imports: a literal
|
|
283
|
+
// `import()` is what lets the bundler split, hash and tree-shake the chunk
|
|
284
|
+
// as ordinary behaviour rather than as an integration.
|
|
285
|
+
const deferredDecls = [];
|
|
286
|
+
if (state.deferredComponents.size > 0) {
|
|
287
|
+
for (const [local, entry] of state.deferredComponents) {
|
|
288
|
+
if (path.scope.hasBinding(local))
|
|
289
|
+
continue;
|
|
290
|
+
deferredDecls.push(t.variableDeclaration('const', [
|
|
291
|
+
t.variableDeclarator(t.identifier(local), t.callExpression(t.identifier('deferred'), [
|
|
292
|
+
t.arrowFunctionExpression([], t.callExpression(t.import(), [t.stringLiteral(entry.module)])),
|
|
293
|
+
t.objectExpression([
|
|
294
|
+
t.objectProperty(t.identifier('strategy'), strategyLiteral(entry.strategy)),
|
|
295
|
+
t.objectProperty(t.identifier('export'), t.stringLiteral(entry.export)),
|
|
296
|
+
]),
|
|
297
|
+
])),
|
|
298
|
+
]));
|
|
299
|
+
}
|
|
300
|
+
if (deferredDecls.length > 0 && !path.scope.hasBinding('deferred')) {
|
|
301
|
+
imports.push(t.importDeclaration([t.importSpecifier(t.identifier('deferred'), t.identifier('deferred'))], t.stringLiteral(state.libModule ?? '@fluixi/core')));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
273
304
|
if (state.resolvedComponents.size > 0) {
|
|
274
305
|
const byModule = new Map();
|
|
275
306
|
for (const [local, resolved] of state.resolvedComponents) {
|
|
@@ -305,6 +336,28 @@ export default declare((api, options = {}) => {
|
|
|
305
336
|
});
|
|
306
337
|
path.node.body.unshift(...hoisted);
|
|
307
338
|
}
|
|
339
|
+
// After the hoisted statics but before imports are unshifted, so the
|
|
340
|
+
// final order is: imports, deferred bindings, everything else.
|
|
341
|
+
if (deferredDecls.length > 0) {
|
|
342
|
+
path.node.body.unshift(...deferredDecls);
|
|
343
|
+
}
|
|
344
|
+
// Static templates, parsed once per module rather than per instance.
|
|
345
|
+
if (state.staticTemplates.size > 0) {
|
|
346
|
+
const decls = [];
|
|
347
|
+
for (const [key, name] of state.staticTemplates) {
|
|
348
|
+
const svg = key.startsWith('svg:');
|
|
349
|
+
const html = svg ? key.slice(4) : key;
|
|
350
|
+
decls.push(t.variableDeclaration('const', [
|
|
351
|
+
t.variableDeclarator(t.identifier(name),
|
|
352
|
+
// Just the HTML. Parsing is deferred to `templateNode`, which
|
|
353
|
+
// knows whether this is a server render, a hydration or a fresh
|
|
354
|
+
// client render — a module-scope `createTemplate` would touch
|
|
355
|
+
// `document` at import time and break SSR outright.
|
|
356
|
+
t.stringLiteral(html)),
|
|
357
|
+
]));
|
|
358
|
+
}
|
|
359
|
+
path.node.body.unshift(...decls);
|
|
360
|
+
}
|
|
308
361
|
}
|
|
309
362
|
// Add all imports at the top, deduped. When a file mixes inline JSX
|
|
310
363
|
// and html`` (format 'both'), the two injection paths can each emit an
|
|
@@ -326,9 +379,12 @@ export default declare((api, options = {}) => {
|
|
|
326
379
|
const element = path.node;
|
|
327
380
|
const openingElement = element.openingElement;
|
|
328
381
|
// Router components auto-import like control flow, but aren't optimized —
|
|
329
|
-
// just register the name so the import gets injected if it isn't bound.
|
|
382
|
+
// just register the name so the import gets injected if it isn't bound. A
|
|
383
|
+
// configured rule takes precedence; registering anyway would inject the
|
|
384
|
+
// router's import alongside the resolved one and bind the name twice.
|
|
330
385
|
if (t.isJSXIdentifier(openingElement.name) &&
|
|
331
|
-
ROUTER_COMPONENTS.has(openingElement.name.name)
|
|
386
|
+
ROUTER_COMPONENTS.has(openingElement.name.name) &&
|
|
387
|
+
!state.componentResolver?.resolve(openingElement.name.name)) {
|
|
332
388
|
state.controlFlowComponents.add(openingElement.name.name);
|
|
333
389
|
state.needsControlFlowImport = true;
|
|
334
390
|
}
|
|
@@ -524,24 +580,104 @@ function emitImperativeElement(tag, isNative, props, spreads, children, state) {
|
|
|
524
580
|
* `createMemo(() => createComponent(Comp, props))` — same shape jsx-runtime
|
|
525
581
|
* produces for components, so it stays reactive when used as a child.
|
|
526
582
|
*/
|
|
583
|
+
/**
|
|
584
|
+
* Read a `load:` attribute's literal value. A `${…}` hole is rejected: the strategy
|
|
585
|
+
* decides how the module is emitted, so it has to be known at compile time.
|
|
586
|
+
*/
|
|
587
|
+
function readLoadDirective(name, attr) {
|
|
588
|
+
let value = null;
|
|
589
|
+
if (attr.value) {
|
|
590
|
+
if (t.isStringLiteral(attr.value))
|
|
591
|
+
value = attr.value.value;
|
|
592
|
+
else {
|
|
593
|
+
throw new LoadDirectiveError(`'${name}' needs a literal value, not an expression — the strategy is compile-time.`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return parseLoadDirective(name, value);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Two usages of one component in the same file share a binding, so their strategies
|
|
600
|
+
* have to agree. The earlier-loading one wins: the module is in whichever chunk loads
|
|
601
|
+
* first regardless, so deferring the other buys nothing — the same reasoning as the
|
|
602
|
+
* collapse pass, applied within a file.
|
|
603
|
+
*/
|
|
604
|
+
/** The strategy as an object literal the runtime can read. */
|
|
605
|
+
function strategyLiteral(strategy) {
|
|
606
|
+
const props = [
|
|
607
|
+
t.objectProperty(t.identifier('kind'), t.stringLiteral(strategy.kind)),
|
|
608
|
+
];
|
|
609
|
+
if (strategy.kind === 'visible' && strategy.rootMargin) {
|
|
610
|
+
props.push(t.objectProperty(t.identifier('rootMargin'), t.stringLiteral(strategy.rootMargin)));
|
|
611
|
+
}
|
|
612
|
+
if (strategy.kind === 'interaction') {
|
|
613
|
+
props.push(t.objectProperty(t.identifier('events'), t.arrayExpression(strategy.events.map((e) => t.stringLiteral(e)))));
|
|
614
|
+
}
|
|
615
|
+
if (strategy.kind === 'media') {
|
|
616
|
+
props.push(t.objectProperty(t.identifier('query'), t.stringLiteral(strategy.query)));
|
|
617
|
+
}
|
|
618
|
+
return t.objectExpression(props);
|
|
619
|
+
}
|
|
620
|
+
function mergeStrategy(existing, incoming) {
|
|
621
|
+
if (!isDeferred(existing) || !isDeferred(incoming))
|
|
622
|
+
return EAGER;
|
|
623
|
+
return existing;
|
|
624
|
+
}
|
|
527
625
|
function emitComponentCall(comp, propsObj, state) {
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
626
|
+
// Configured tags register here rather than in the JSX element visitor: this is the
|
|
627
|
+
// chokepoint every component passes through, and the visitor missed children because
|
|
628
|
+
// a lowered parent is replaced before its subtree is walked.
|
|
629
|
+
//
|
|
630
|
+
// A configured rule wins over the router built-ins. Those are only an import — no
|
|
631
|
+
// special lowering, no reactive-getter props — so letting a project's own `Link` lose
|
|
632
|
+
// to them silently was the same failure `Switch` has: the tag renders the router's
|
|
633
|
+
// component and nothing reports it. Control flow still wins, because there the name
|
|
634
|
+
// selects codegen and a rule cannot supply that.
|
|
635
|
+
// Control flow needs its import injected too, and registering it here rather than in
|
|
636
|
+
// the JSX visitor is what makes nested usage work: a lowered parent is replaced before
|
|
637
|
+
// its subtree is walked, so `<div><Show/></div>` never reached the visitor and `Show`
|
|
638
|
+
// compiled to a reference to nothing. Every fixture imported Show by hand, so nothing
|
|
639
|
+
// caught it.
|
|
640
|
+
if (t.isIdentifier(comp) && CONTROL_FLOW_COMPONENTS.has(comp.name)) {
|
|
532
641
|
state.controlFlowComponents.add(comp.name);
|
|
533
642
|
state.needsControlFlowImport = true;
|
|
534
643
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// missed children, because a lowered parent is replaced before its subtree is walked.
|
|
538
|
-
if (t.isIdentifier(comp) &&
|
|
539
|
-
!ROUTER_COMPONENTS.has(comp.name) &&
|
|
540
|
-
!CONTROL_FLOW_COMPONENTS.has(comp.name) &&
|
|
541
|
-
state.componentResolver) {
|
|
644
|
+
let resolvedByRule = false;
|
|
645
|
+
if (t.isIdentifier(comp) && !CONTROL_FLOW_COMPONENTS.has(comp.name) && state.componentResolver) {
|
|
542
646
|
const resolved = state.componentResolver.resolve(comp.name);
|
|
543
|
-
if (resolved)
|
|
544
|
-
state.
|
|
647
|
+
if (resolved) {
|
|
648
|
+
const strategy = state.pendingLoad ?? EAGER;
|
|
649
|
+
// An eager usage seen earlier in the file already put the module in the chunk,
|
|
650
|
+
// so a later deferred usage of the same name collapses to eager. Without this the
|
|
651
|
+
// file gets both a static import and a `const` for one name — a redeclaration.
|
|
652
|
+
const alreadyEager = state.resolvedComponents.has(comp.name);
|
|
653
|
+
if (isDeferred(strategy) && !alreadyEager) {
|
|
654
|
+
// A deferred component becomes a `deferred(() => import(...))` binding rather
|
|
655
|
+
// than a static import, so the bundler puts it in its own chunk.
|
|
656
|
+
const existing = state.deferredComponents.get(comp.name);
|
|
657
|
+
const merged = existing ? mergeStrategy(existing.strategy, strategy) : strategy;
|
|
658
|
+
if (isDeferred(merged)) {
|
|
659
|
+
state.deferredComponents.set(comp.name, { ...resolved, strategy: merged });
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
state.deferredComponents.delete(comp.name);
|
|
663
|
+
state.resolvedComponents.set(comp.name, resolved);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
// An eager usage anywhere wins: the module is in the chunk already.
|
|
668
|
+
state.deferredComponents.delete(comp.name);
|
|
669
|
+
state.resolvedComponents.set(comp.name, resolved);
|
|
670
|
+
}
|
|
671
|
+
resolvedByRule = true;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
state.pendingLoad = undefined;
|
|
675
|
+
// Router components auto-import like control flow — register the name here so
|
|
676
|
+
// `<Outlet/>` nested in markup is caught, not only a top-level router tag. Unbound
|
|
677
|
+
// names are injected at Program.exit.
|
|
678
|
+
if (!resolvedByRule && t.isIdentifier(comp) && ROUTER_COMPONENTS.has(comp.name)) {
|
|
679
|
+
state.controlFlowComponents.add(comp.name);
|
|
680
|
+
state.needsControlFlowImport = true;
|
|
545
681
|
}
|
|
546
682
|
if (state.backend === 'imperative') {
|
|
547
683
|
state.needsCreateComponent = true;
|
|
@@ -559,17 +695,54 @@ function emitComponentCall(comp, propsObj, state) {
|
|
|
559
695
|
* Produces the same runtime calls as the inline path — this is the route a future
|
|
560
696
|
* swc front-end reuses.
|
|
561
697
|
*/
|
|
698
|
+
/**
|
|
699
|
+
* Hoist the backend's static templates to module scope and rewrite the emitted code to
|
|
700
|
+
* reference them. Deduped by HTML, so a subtree repeated in a file is parsed once.
|
|
701
|
+
*/
|
|
702
|
+
function adoptTemplates(code, templates, state) {
|
|
703
|
+
if (!templates || templates.length === 0)
|
|
704
|
+
return code;
|
|
705
|
+
const rename = new Map();
|
|
706
|
+
for (const tpl of templates) {
|
|
707
|
+
const key = `${tpl.svg ? 'svg:' : ''}${tpl.html}`;
|
|
708
|
+
let name = state.staticTemplates.get(key);
|
|
709
|
+
if (!name) {
|
|
710
|
+
name = `_tmpl$${state.staticTemplates.size}`;
|
|
711
|
+
state.staticTemplates.set(key, name);
|
|
712
|
+
}
|
|
713
|
+
rename.set(tpl.id, name);
|
|
714
|
+
}
|
|
715
|
+
// One pass, not one replacement per id. The backend's local ids and the hoisted names
|
|
716
|
+
// share the `_tmpl$N` namespace, so replacing them in sequence can rewrite a name that
|
|
717
|
+
// an earlier replacement just produced — which silently points a template at another
|
|
718
|
+
// template's HTML.
|
|
719
|
+
return code.replace(/_tmpl\$\d+/g, (id) => rename.get(id) ?? id);
|
|
720
|
+
}
|
|
562
721
|
function emitViaIR(node, state) {
|
|
563
722
|
// `used` collects runtime symbols emitted for JSX nested inside expressions
|
|
564
723
|
// (render-props, ternaries, fallback={<p/>}, …) so we import them too.
|
|
565
724
|
const used = new Set();
|
|
566
725
|
const ir = buildIR(node, used);
|
|
567
|
-
|
|
726
|
+
// The same passes the lit path runs. Without them `codegen: 'ir'` silently skipped
|
|
727
|
+
// resolution entirely — a resolved tag compiled to a reference with no import.
|
|
728
|
+
resolveComponentSources(ir, {
|
|
729
|
+
resolver: state.componentResolver,
|
|
730
|
+
modules: {
|
|
731
|
+
controlFlowModule: state.controlFlowModule,
|
|
732
|
+
coreModule: state.libModule,
|
|
733
|
+
routerModule: state.routerModule,
|
|
734
|
+
},
|
|
735
|
+
strategyFor: (n) => n.load,
|
|
736
|
+
});
|
|
737
|
+
collapseStrategies(ir);
|
|
738
|
+
const { code, imports, templates } = imperativeBackend.emit(ir, { templateClone: state.templateClone, partialTemplates: state.partialTemplates });
|
|
568
739
|
for (const s of imports)
|
|
569
740
|
used.add(s);
|
|
570
741
|
for (const s of used)
|
|
571
742
|
state.irImports.add(s);
|
|
572
|
-
|
|
743
|
+
collectRuntimeNeeds(ir, state);
|
|
744
|
+
const adopted = adoptTemplates(code, templates, state);
|
|
745
|
+
return template.expression(adopted, { placeholderPattern: false, plugins: ['typescript'] })();
|
|
573
746
|
}
|
|
574
747
|
/**
|
|
575
748
|
* Merge injected import declarations so no module/name pair is imported twice.
|
|
@@ -613,7 +786,23 @@ function emitLitViaIR(node, state) {
|
|
|
613
786
|
const used = new Set();
|
|
614
787
|
const svg = t.isIdentifier(node.tag) && node.tag.name === 'svg';
|
|
615
788
|
const ir = buildIRFromLit(node, used, { svg });
|
|
616
|
-
|
|
789
|
+
// Resolve pass: annotate each component reference with where it comes from, before
|
|
790
|
+
// codegen. Bindings are not checked here — Program.exit knows the whole file's scope
|
|
791
|
+
// and drops any injected import whose name turns out to be bound.
|
|
792
|
+
resolveComponentSources(ir, {
|
|
793
|
+
resolver: state.componentResolver,
|
|
794
|
+
modules: {
|
|
795
|
+
controlFlowModule: state.controlFlowModule,
|
|
796
|
+
coreModule: state.libModule,
|
|
797
|
+
routerModule: state.routerModule,
|
|
798
|
+
},
|
|
799
|
+
strategyFor: (node) => node.load,
|
|
800
|
+
});
|
|
801
|
+
// Demote deferred references whose module is already in this chunk: a lazy boundary
|
|
802
|
+
// around something already present adds a wrapper, a promise and a re-render to
|
|
803
|
+
// fetch nothing.
|
|
804
|
+
collapseStrategies(ir);
|
|
805
|
+
const { code, imports, templates } = imperativeBackend.emit(ir, { templateClone: state.templateClone, partialTemplates: state.partialTemplates });
|
|
617
806
|
for (const s of imports)
|
|
618
807
|
used.add(s);
|
|
619
808
|
for (const s of used)
|
|
@@ -624,7 +813,7 @@ function emitLitViaIR(node, state) {
|
|
|
624
813
|
// position — which is why compiled html`` had no source map at all. Give every
|
|
625
814
|
// generated node the template's own location so a breakpoint or stack frame
|
|
626
815
|
// lands on the template instead of nowhere.
|
|
627
|
-
const expression = template.expression(code, {
|
|
816
|
+
const expression = template.expression(adoptTemplates(code, templates, state), {
|
|
628
817
|
placeholderPattern: false,
|
|
629
818
|
plugins: ['typescript'],
|
|
630
819
|
})();
|
|
@@ -671,18 +860,29 @@ function inheritLocation(target, source) {
|
|
|
671
860
|
* (-> auto-import from the control-flow module, like the inline JSX path).
|
|
672
861
|
*/
|
|
673
862
|
function collectRuntimeNeeds(node, state) {
|
|
674
|
-
if (node.kind === 'component' &&
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
863
|
+
if (node.kind === 'component' && node.source) {
|
|
864
|
+
// Provenance was decided by the resolve pass and recorded on the node; this only
|
|
865
|
+
// translates it into the two things Program.exit wires. Reading it here rather
|
|
866
|
+
// than re-resolving is the point — one decision, so import injection and every
|
|
867
|
+
// later pass cannot disagree.
|
|
868
|
+
if (node.source.origin === 'builtin') {
|
|
869
|
+
state.controlFlowComponents.add(node.name);
|
|
870
|
+
state.needsControlFlowImport = true;
|
|
871
|
+
}
|
|
872
|
+
else if (isDeferred(node.source.loading) && !state.resolvedComponents.has(node.name)) {
|
|
873
|
+
state.deferredComponents.set(node.name, {
|
|
874
|
+
module: node.source.module,
|
|
875
|
+
export: node.source.export,
|
|
876
|
+
strategy: node.source.loading,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
state.deferredComponents.delete(node.name);
|
|
881
|
+
state.resolvedComponents.set(node.name, {
|
|
882
|
+
module: node.source.module,
|
|
883
|
+
export: node.source.export,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
686
886
|
}
|
|
687
887
|
if (node.kind === 'element' || node.kind === 'component' || node.kind === 'control') {
|
|
688
888
|
if ('props' in node) {
|
|
@@ -885,6 +1085,9 @@ function transformAttributes(attributes, state, isNativeElement = true, tagName
|
|
|
885
1085
|
const props = [];
|
|
886
1086
|
const spreads = [];
|
|
887
1087
|
const cfValueProp = CONTROL_FLOW_VALUE_PROPS[tagName];
|
|
1088
|
+
// `load:` is a directive, not a prop — it says when this component's module is
|
|
1089
|
+
// fetched. Cleared per element so a strategy cannot leak onto the next tag.
|
|
1090
|
+
state.pendingLoad = undefined;
|
|
888
1091
|
for (const attr of attributes) {
|
|
889
1092
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
890
1093
|
// Collect spread expressions separately
|
|
@@ -892,6 +1095,11 @@ function transformAttributes(attributes, state, isNativeElement = true, tagName
|
|
|
892
1095
|
}
|
|
893
1096
|
else if (t.isJSXAttribute(attr)) {
|
|
894
1097
|
const name = getAttributeName(attr.name);
|
|
1098
|
+
if (isLoadDirective(name)) {
|
|
1099
|
+
// Same parser the html`` lowering uses, so the two syntaxes cannot drift.
|
|
1100
|
+
state.pendingLoad = readLoadDirective(name, attr);
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
895
1103
|
const value = transformAttributeValue(attr.value, state, name, isNativeElement);
|
|
896
1104
|
// Detect event handlers for delegation
|
|
897
1105
|
if (EVENT_PROPS.test(name)) {
|