@fluixi/compiler 1.0.0-alpha.68 → 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 +14 -0
- package/dist/frontend/babel/plugin.d.ts.map +1 -1
- package/dist/frontend/babel/plugin.js +291 -34
- package/dist/frontend/babel/plugin.mjs +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.mjs +1 -1
- package/dist/integrations.cjs +8 -8
- package/dist/integrations.d.ts +13 -0
- package/dist/integrations.d.ts.map +1 -1
- package/dist/integrations.js +9 -0
- package/dist/integrations.mjs +3 -3
- 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 +24 -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/component-resolver.cjs +1 -0
- package/dist/resolve/component-resolver.d.ts +69 -0
- package/dist/resolve/component-resolver.d.ts.map +1 -0
- package/dist/resolve/component-resolver.js +95 -0
- package/dist/resolve/component-resolver.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-6UTCV3CC.mjs +0 -2
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* - Control flow optimization
|
|
12
12
|
*/
|
|
13
13
|
import { declare } from '@babel/helper-plugin-utils';
|
|
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';
|
|
14
19
|
import { types as t, template, } from '@babel/core';
|
|
15
20
|
import { buildIR } from './build-ir.js';
|
|
16
21
|
import { buildIRFromLit } from './build-ir-lit.js';
|
|
@@ -18,27 +23,17 @@ import { imperativeBackend } from '../../codegen/backends/imperative.js';
|
|
|
18
23
|
// ============================================================================
|
|
19
24
|
// Constants
|
|
20
25
|
// ============================================================================
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'Match',
|
|
27
|
-
'Portal',
|
|
28
|
-
'Dynamic',
|
|
29
|
-
'ErrorBoundary',
|
|
30
|
-
'Suspense',
|
|
31
|
-
'SuspenseList',
|
|
32
|
-
'Await',
|
|
33
|
-
]);
|
|
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]);
|
|
34
31
|
// Control-flow components that live in @fluixi/core, NOT @fluixi/dom — they must
|
|
35
32
|
// be auto-imported from core (dom doesn't export them).
|
|
36
|
-
const CORE_ONLY_CONTROL_FLOW = new Set(
|
|
33
|
+
const CORE_ONLY_CONTROL_FLOW = new Set(CORE_CONTROL_FLOW);
|
|
37
34
|
// Router components auto-imported (from routerModule) the same way control flow is:
|
|
38
35
|
// use `<Router>`/`<Outlet>`/… in a template and the import is injected if absent.
|
|
39
|
-
|
|
40
|
-
// call, not a tag, so neither can be auto-imported this way.)
|
|
41
|
-
const ROUTER_COMPONENTS = new Set(['Router', 'Outlet', 'Redirect', 'Link']);
|
|
36
|
+
const ROUTER_COMPONENTS = new Set(ROUTER_COMPONENT_NAMES);
|
|
42
37
|
// Control-flow components read a single reactive *value* prop inside their own tracking
|
|
43
38
|
// scope. Emit that prop as a getter (like `children`) so reading it re-runs the caller's
|
|
44
39
|
// expression — otherwise `each={items()}` / `when={cond()}` snapshot once and never update.
|
|
@@ -84,17 +79,24 @@ const DELEGATABLE_EVENTS = [
|
|
|
84
79
|
// ============================================================================
|
|
85
80
|
export default declare((api, options = {}) => {
|
|
86
81
|
api.assertVersion(7);
|
|
87
|
-
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,
|
|
88
83
|
// `both` by default: JSX and html`` are compiled out of the box, so
|
|
89
84
|
// `fluixi()` needs no `format` option. Injected imports are deduped, so a
|
|
90
85
|
// file mixing the two styles won't redeclare a binding.
|
|
91
|
-
format = 'both', litTag = 'html', } = options;
|
|
86
|
+
format = 'both', litTag = 'html', resolve: resolveRules = [], } = options;
|
|
87
|
+
// Built once per plugin instance, not per file: the rules are static and the
|
|
88
|
+
// resolver memoises lookups, so every file in the build shares one cache.
|
|
89
|
+
const componentResolver = createComponentResolver(resolveRules);
|
|
92
90
|
const compileLit = format === 'lit' || format === 'both';
|
|
93
91
|
const pluginState = {
|
|
94
92
|
backend,
|
|
95
93
|
codegen,
|
|
96
94
|
irImports: new Set(),
|
|
97
95
|
libModule: '@fluixi/core',
|
|
96
|
+
controlFlowModule,
|
|
97
|
+
routerModule,
|
|
98
|
+
templateClone,
|
|
99
|
+
partialTemplates,
|
|
98
100
|
hasJSX: false,
|
|
99
101
|
hasReactivity: false,
|
|
100
102
|
hasLitHTML: false,
|
|
@@ -102,8 +104,12 @@ export default declare((api, options = {}) => {
|
|
|
102
104
|
needsControlFlowImport: false,
|
|
103
105
|
needsCreateMemo: false,
|
|
104
106
|
controlFlowComponents: new Set(),
|
|
107
|
+
resolvedComponents: new Map(),
|
|
108
|
+
deferredComponents: new Map(),
|
|
109
|
+
componentResolver,
|
|
105
110
|
delegatedEvents: new Set(),
|
|
106
111
|
staticElements: new Map(),
|
|
112
|
+
staticTemplates: new Map(),
|
|
107
113
|
staticCounter: 0,
|
|
108
114
|
};
|
|
109
115
|
return {
|
|
@@ -139,11 +145,19 @@ export default declare((api, options = {}) => {
|
|
|
139
145
|
needsControlFlowImport: false,
|
|
140
146
|
needsCreateMemo: false,
|
|
141
147
|
controlFlowComponents: new Set(),
|
|
148
|
+
resolvedComponents: new Map(),
|
|
149
|
+
deferredComponents: new Map(),
|
|
150
|
+
componentResolver,
|
|
142
151
|
delegatedEvents: new Set(),
|
|
143
152
|
staticElements: new Map(),
|
|
153
|
+
staticTemplates: new Map(),
|
|
144
154
|
autoShow: autoShowTransform,
|
|
145
155
|
staticCounter: 0,
|
|
146
156
|
libModule: '@fluixi/core',
|
|
157
|
+
controlFlowModule,
|
|
158
|
+
routerModule,
|
|
159
|
+
templateClone,
|
|
160
|
+
partialTemplates,
|
|
147
161
|
backend,
|
|
148
162
|
codegen,
|
|
149
163
|
irImports: new Set(),
|
|
@@ -211,7 +225,10 @@ export default declare((api, options = {}) => {
|
|
|
211
225
|
imports.push(t.importDeclaration(specifiers, t.stringLiteral(signalModule)));
|
|
212
226
|
}
|
|
213
227
|
}
|
|
214
|
-
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.
|
|
215
232
|
// Add JSX runtime imports
|
|
216
233
|
const specifiers = [
|
|
217
234
|
t.importSpecifier(t.identifier('jsx'), t.identifier('jsx')),
|
|
@@ -255,6 +272,50 @@ export default declare((api, options = {}) => {
|
|
|
255
272
|
if (fromRouter.length > 0)
|
|
256
273
|
imports.push(declFor(fromRouter, routerModule));
|
|
257
274
|
}
|
|
275
|
+
// Imports for tags matched by the `resolve` rules. Same binding guard as
|
|
276
|
+
// control flow: a hand-written import or a local of the same name wins, so
|
|
277
|
+
// enabling resolution can never change what an existing file means.
|
|
278
|
+
//
|
|
279
|
+
// Grouped by module so several components from one package share a single
|
|
280
|
+
// declaration, and emitted with the local name equal to the tag — the
|
|
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
|
+
}
|
|
304
|
+
if (state.resolvedComponents.size > 0) {
|
|
305
|
+
const byModule = new Map();
|
|
306
|
+
for (const [local, resolved] of state.resolvedComponents) {
|
|
307
|
+
if (path.scope.hasBinding(local))
|
|
308
|
+
continue;
|
|
309
|
+
const list = byModule.get(resolved.module) ?? [];
|
|
310
|
+
list.push({ local, exported: resolved.export });
|
|
311
|
+
byModule.set(resolved.module, list);
|
|
312
|
+
}
|
|
313
|
+
for (const [mod, entries] of byModule) {
|
|
314
|
+
imports.push(t.importDeclaration(entries.map(({ local, exported }) => exported === 'default'
|
|
315
|
+
? t.importDefaultSpecifier(t.identifier(local))
|
|
316
|
+
: t.importSpecifier(t.identifier(local), t.identifier(exported))), t.stringLiteral(mod)));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
258
319
|
// Add event delegation setup if needed
|
|
259
320
|
if (delegateEvents && state.delegatedEvents.size > 0) {
|
|
260
321
|
imports.push(t.importDeclaration([
|
|
@@ -275,6 +336,28 @@ export default declare((api, options = {}) => {
|
|
|
275
336
|
});
|
|
276
337
|
path.node.body.unshift(...hoisted);
|
|
277
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
|
+
}
|
|
278
361
|
}
|
|
279
362
|
// Add all imports at the top, deduped. When a file mixes inline JSX
|
|
280
363
|
// and html`` (format 'both'), the two injection paths can each emit an
|
|
@@ -296,9 +379,12 @@ export default declare((api, options = {}) => {
|
|
|
296
379
|
const element = path.node;
|
|
297
380
|
const openingElement = element.openingElement;
|
|
298
381
|
// Router components auto-import like control flow, but aren't optimized —
|
|
299
|
-
// 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.
|
|
300
385
|
if (t.isJSXIdentifier(openingElement.name) &&
|
|
301
|
-
ROUTER_COMPONENTS.has(openingElement.name.name)
|
|
386
|
+
ROUTER_COMPONENTS.has(openingElement.name.name) &&
|
|
387
|
+
!state.componentResolver?.resolve(openingElement.name.name)) {
|
|
302
388
|
state.controlFlowComponents.add(openingElement.name.name);
|
|
303
389
|
state.needsControlFlowImport = true;
|
|
304
390
|
}
|
|
@@ -494,11 +580,102 @@ function emitImperativeElement(tag, isNative, props, spreads, children, state) {
|
|
|
494
580
|
* `createMemo(() => createComponent(Comp, props))` — same shape jsx-runtime
|
|
495
581
|
* produces for components, so it stays reactive when used as a child.
|
|
496
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
|
+
}
|
|
497
625
|
function emitComponentCall(comp, propsObj, state) {
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
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)) {
|
|
641
|
+
state.controlFlowComponents.add(comp.name);
|
|
642
|
+
state.needsControlFlowImport = true;
|
|
643
|
+
}
|
|
644
|
+
let resolvedByRule = false;
|
|
645
|
+
if (t.isIdentifier(comp) && !CONTROL_FLOW_COMPONENTS.has(comp.name) && state.componentResolver) {
|
|
646
|
+
const resolved = state.componentResolver.resolve(comp.name);
|
|
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)) {
|
|
502
679
|
state.controlFlowComponents.add(comp.name);
|
|
503
680
|
state.needsControlFlowImport = true;
|
|
504
681
|
}
|
|
@@ -518,17 +695,54 @@ function emitComponentCall(comp, propsObj, state) {
|
|
|
518
695
|
* Produces the same runtime calls as the inline path — this is the route a future
|
|
519
696
|
* swc front-end reuses.
|
|
520
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
|
+
}
|
|
521
721
|
function emitViaIR(node, state) {
|
|
522
722
|
// `used` collects runtime symbols emitted for JSX nested inside expressions
|
|
523
723
|
// (render-props, ternaries, fallback={<p/>}, …) so we import them too.
|
|
524
724
|
const used = new Set();
|
|
525
725
|
const ir = buildIR(node, used);
|
|
526
|
-
|
|
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 });
|
|
527
739
|
for (const s of imports)
|
|
528
740
|
used.add(s);
|
|
529
741
|
for (const s of used)
|
|
530
742
|
state.irImports.add(s);
|
|
531
|
-
|
|
743
|
+
collectRuntimeNeeds(ir, state);
|
|
744
|
+
const adopted = adoptTemplates(code, templates, state);
|
|
745
|
+
return template.expression(adopted, { placeholderPattern: false, plugins: ['typescript'] })();
|
|
532
746
|
}
|
|
533
747
|
/**
|
|
534
748
|
* Merge injected import declarations so no module/name pair is imported twice.
|
|
@@ -572,7 +786,23 @@ function emitLitViaIR(node, state) {
|
|
|
572
786
|
const used = new Set();
|
|
573
787
|
const svg = t.isIdentifier(node.tag) && node.tag.name === 'svg';
|
|
574
788
|
const ir = buildIRFromLit(node, used, { svg });
|
|
575
|
-
|
|
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 });
|
|
576
806
|
for (const s of imports)
|
|
577
807
|
used.add(s);
|
|
578
808
|
for (const s of used)
|
|
@@ -583,7 +813,7 @@ function emitLitViaIR(node, state) {
|
|
|
583
813
|
// position — which is why compiled html`` had no source map at all. Give every
|
|
584
814
|
// generated node the template's own location so a breakpoint or stack frame
|
|
585
815
|
// lands on the template instead of nowhere.
|
|
586
|
-
const expression = template.expression(code, {
|
|
816
|
+
const expression = template.expression(adoptTemplates(code, templates, state), {
|
|
587
817
|
placeholderPattern: false,
|
|
588
818
|
plugins: ['typescript'],
|
|
589
819
|
})();
|
|
@@ -630,10 +860,29 @@ function inheritLocation(target, source) {
|
|
|
630
860
|
* (-> auto-import from the control-flow module, like the inline JSX path).
|
|
631
861
|
*/
|
|
632
862
|
function collectRuntimeNeeds(node, state) {
|
|
633
|
-
if (node.kind === 'component' &&
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
+
}
|
|
637
886
|
}
|
|
638
887
|
if (node.kind === 'element' || node.kind === 'component' || node.kind === 'control') {
|
|
639
888
|
if ('props' in node) {
|
|
@@ -836,6 +1085,9 @@ function transformAttributes(attributes, state, isNativeElement = true, tagName
|
|
|
836
1085
|
const props = [];
|
|
837
1086
|
const spreads = [];
|
|
838
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;
|
|
839
1091
|
for (const attr of attributes) {
|
|
840
1092
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
841
1093
|
// Collect spread expressions separately
|
|
@@ -843,6 +1095,11 @@ function transformAttributes(attributes, state, isNativeElement = true, tagName
|
|
|
843
1095
|
}
|
|
844
1096
|
else if (t.isJSXAttribute(attr)) {
|
|
845
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
|
+
}
|
|
846
1103
|
const value = transformAttributeValue(attr.value, state, name, isNativeElement);
|
|
847
1104
|
// Detect event handlers for delegation
|
|
848
1105
|
if (EVENT_PROPS.test(name)) {
|