@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.
Files changed (82) hide show
  1. package/dist/babel-NY6FXXLW.mjs +2 -0
  2. package/dist/codegen/backend.cjs +1 -1
  3. package/dist/codegen/backend.d.ts +14 -0
  4. package/dist/codegen/backend.d.ts.map +1 -1
  5. package/dist/codegen/backends/imperative.cjs +1 -1
  6. package/dist/codegen/backends/imperative.d.ts +10 -1
  7. package/dist/codegen/backends/imperative.d.ts.map +1 -1
  8. package/dist/codegen/backends/imperative.js +127 -16
  9. package/dist/codegen/backends/imperative.mjs +1 -1
  10. package/dist/codegen/partial-template.cjs +1 -0
  11. package/dist/codegen/partial-template.d.ts +62 -0
  12. package/dist/codegen/partial-template.d.ts.map +1 -0
  13. package/dist/codegen/partial-template.js +0 -0
  14. package/dist/codegen/partial-template.mjs +1 -0
  15. package/dist/codegen/serialize-static.cjs +1 -0
  16. package/dist/codegen/serialize-static.d.ts +23 -0
  17. package/dist/codegen/serialize-static.d.ts.map +1 -0
  18. package/dist/codegen/serialize-static.js +140 -0
  19. package/dist/codegen/serialize-static.mjs +1 -0
  20. package/dist/frontend/babel/build-ir-lit.cjs +1 -1
  21. package/dist/frontend/babel/build-ir-lit.d.ts.map +1 -1
  22. package/dist/frontend/babel/build-ir-lit.js +5 -1
  23. package/dist/frontend/babel/build-ir-lit.mjs +1 -1
  24. package/dist/frontend/babel/build-ir.cjs +1 -1
  25. package/dist/frontend/babel/build-ir.d.ts.map +1 -1
  26. package/dist/frontend/babel/build-ir.js +36 -8
  27. package/dist/frontend/babel/build-ir.mjs +1 -1
  28. package/dist/frontend/babel/index.cjs +2 -2
  29. package/dist/frontend/babel/index.mjs +2 -2
  30. package/dist/frontend/babel/lower-template.cjs +1 -1
  31. package/dist/frontend/babel/lower-template.d.ts.map +1 -1
  32. package/dist/frontend/babel/lower-template.js +34 -5
  33. package/dist/frontend/babel/lower-template.mjs +1 -1
  34. package/dist/frontend/babel/plugin.cjs +2 -2
  35. package/dist/frontend/babel/plugin.d.ts +4 -0
  36. package/dist/frontend/babel/plugin.d.ts.map +1 -1
  37. package/dist/frontend/babel/plugin.js +258 -50
  38. package/dist/frontend/babel/plugin.mjs +2 -2
  39. package/dist/index.cjs +1 -1
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -0
  43. package/dist/index.mjs +1 -1
  44. package/dist/integrations.cjs +8 -8
  45. package/dist/integrations.d.ts +6 -0
  46. package/dist/integrations.d.ts.map +1 -1
  47. package/dist/integrations.js +6 -0
  48. package/dist/integrations.mjs +4 -4
  49. package/dist/ir/nodes.cjs +1 -1
  50. package/dist/ir/nodes.d.ts +43 -0
  51. package/dist/ir/nodes.d.ts.map +1 -1
  52. package/dist/optimize/analyze-static.cjs +1 -0
  53. package/dist/optimize/analyze-static.d.ts +40 -0
  54. package/dist/optimize/analyze-static.d.ts.map +1 -0
  55. package/dist/optimize/analyze-static.js +131 -0
  56. package/dist/optimize/analyze-static.mjs +1 -0
  57. package/dist/optimize/collapse-strategies.cjs +1 -0
  58. package/dist/optimize/collapse-strategies.d.ts +31 -0
  59. package/dist/optimize/collapse-strategies.d.ts.map +1 -0
  60. package/dist/optimize/collapse-strategies.js +33 -0
  61. package/dist/optimize/collapse-strategies.mjs +1 -0
  62. package/dist/options.cjs +1 -1
  63. package/dist/options.d.ts +18 -0
  64. package/dist/options.d.ts.map +1 -1
  65. package/dist/resolve/builtins.cjs +1 -0
  66. package/dist/resolve/builtins.d.ts +69 -0
  67. package/dist/resolve/builtins.d.ts.map +1 -0
  68. package/dist/resolve/builtins.js +64 -0
  69. package/dist/resolve/builtins.mjs +1 -0
  70. package/dist/resolve/load-directive.cjs +1 -0
  71. package/dist/resolve/load-directive.d.ts +54 -0
  72. package/dist/resolve/load-directive.d.ts.map +1 -0
  73. package/dist/resolve/load-directive.js +93 -0
  74. package/dist/resolve/load-directive.mjs +1 -0
  75. package/dist/resolve/resolve-ir.cjs +1 -0
  76. package/dist/resolve/resolve-ir.d.ts +54 -0
  77. package/dist/resolve/resolve-ir.d.ts.map +1 -0
  78. package/dist/resolve/resolve-ir.js +58 -0
  79. package/dist/resolve/resolve-ir.mjs +1 -0
  80. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  81. package/package.json +4 -4
  82. 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
- const CONTROL_FLOW_COMPONENTS = new Set([
23
- 'Show',
24
- 'For',
25
- 'Index',
26
- 'Switch',
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(['Suspense', 'SuspenseList', 'Await']);
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
- // (`Routes` isn't a component — routing is `<Router routes={…}>`; `lazy` is a function
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
- // Router components (Router/Outlet/…) auto-import like control flow register the
529
- // name here (the nested lowering chokepoint) so `<Outlet/>` inside markup is caught,
530
- // not only a top-level router tag. Unbound names are injected at Program.exit.
531
- if (t.isIdentifier(comp) && ROUTER_COMPONENTS.has(comp.name)) {
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
- // Configured tags register here for the same reason: this is the chokepoint every
536
- // component passes through, nested or not. Hooking the JSX element visitor instead
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.resolvedComponents.set(comp.name, resolved);
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
- const { code, imports } = imperativeBackend.emit(ir, {});
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
- return template.expression(code, { placeholderPattern: false, plugins: ['typescript'] })();
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
- const { code, imports } = imperativeBackend.emit(ir, {});
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
- (CONTROL_FLOW_COMPONENTS.has(node.name) || ROUTER_COMPONENTS.has(node.name))) {
676
- state.controlFlowComponents.add(node.name);
677
- state.needsControlFlowImport = true;
678
- }
679
- else if (node.kind === 'component' && state.componentResolver) {
680
- // A configured tag inside html``. Recorded here rather than at the tag site so
681
- // nested usage is caught too; the binding check happens at Program.exit, where
682
- // the whole file's scope is known.
683
- const resolved = state.componentResolver.resolve(node.name);
684
- if (resolved)
685
- state.resolvedComponents.set(node.name, resolved);
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)) {