@sprlab/wccompiler 0.12.0 → 0.13.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 +46 -7
- package/bin/wcc.js +53 -0
- package/lib/codegen.js +46 -1
- package/lib/compiler.js +7 -1
- package/lib/template-normalizer.js +5 -0
- package/lib/tree-walker.js +91 -1
- package/lib/types.js +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -458,12 +458,13 @@ Consumer (receives data via template props):
|
|
|
458
458
|
|
|
459
459
|
## Nested Components
|
|
460
460
|
|
|
461
|
-
Components can use other components in their templates
|
|
461
|
+
Components can import and use other components in their templates using PascalCase tags:
|
|
462
462
|
|
|
463
463
|
```html
|
|
464
|
+
<!-- src/nested/wcc-profile.wcc -->
|
|
464
465
|
<script>
|
|
465
466
|
import { defineComponent, signal } from 'wcc'
|
|
466
|
-
import './wcc-badge.wcc'
|
|
467
|
+
import WccBadge from './wcc-badge.wcc'
|
|
467
468
|
|
|
468
469
|
export default defineComponent({ tag: 'wcc-profile' })
|
|
469
470
|
|
|
@@ -476,15 +477,17 @@ function increment() {
|
|
|
476
477
|
|
|
477
478
|
<template>
|
|
478
479
|
<div class="profile">
|
|
479
|
-
<
|
|
480
|
+
<WccBadge :count="count()" @click="increment"></WccBadge>
|
|
480
481
|
</div>
|
|
481
482
|
</template>
|
|
482
483
|
```
|
|
483
484
|
|
|
484
|
-
- **
|
|
485
|
-
- **
|
|
485
|
+
- **Named import**: `import WccBadge from './wcc-badge.wcc'` — the PascalCase identifier becomes the tag alias in the template
|
|
486
|
+
- **Side-effect import**: `import './wcc-child.wcc'` — registers the child without using it in the template (for programmatic creation)
|
|
486
487
|
- **Reactive props**: Use `:prop="expr"` to pass reactive data down — updates automatically when the expression changes
|
|
487
488
|
- **Event listening**: Use `@event="handler"` to listen to custom events emitted by the child
|
|
489
|
+
- **Compile-time validation**: Using a PascalCase tag without a matching import throws an error at build time
|
|
490
|
+
- **Hyphenated tags**: Tags like `<my-element>` without a corresponding import are treated as plain custom elements (no import generated)
|
|
488
491
|
|
|
489
492
|
## Lifecycle Hooks
|
|
490
493
|
|
|
@@ -613,12 +616,48 @@ timer.value!.start() // ✅ typed
|
|
|
613
616
|
## CLI
|
|
614
617
|
|
|
615
618
|
```bash
|
|
616
|
-
wcc build
|
|
617
|
-
wcc
|
|
619
|
+
wcc build # Compile all .wcc files from input/ to output/
|
|
620
|
+
wcc build --bundle # Compile + produce a single bundle.js (works from file://)
|
|
621
|
+
wcc build --minify # Compile with minification
|
|
622
|
+
wcc build --bundle --minify # Production bundle (smallest output)
|
|
623
|
+
wcc dev # Build + watch + live-reload dev server
|
|
618
624
|
```
|
|
619
625
|
|
|
620
626
|
The CLI discovers all `.wcc` files in your source directory and compiles each into a standalone `.js` file.
|
|
621
627
|
|
|
628
|
+
### Bundle Mode
|
|
629
|
+
|
|
630
|
+
The `--bundle` flag produces a single `bundle.js` file that includes all components and their dependencies in one IIFE (Immediately Invoked Function Expression). This file:
|
|
631
|
+
|
|
632
|
+
- Works with `<script src="bundle.js">` (no `type="module"` needed)
|
|
633
|
+
- Works from `file://` protocol (no server required)
|
|
634
|
+
- Includes all child component imports resolved and inlined
|
|
635
|
+
- Includes the reactive runtime
|
|
636
|
+
- Supports `--minify` for production
|
|
637
|
+
|
|
638
|
+
```html
|
|
639
|
+
<!-- Works by double-clicking the HTML file — no server needed -->
|
|
640
|
+
<!DOCTYPE html>
|
|
641
|
+
<html>
|
|
642
|
+
<body>
|
|
643
|
+
<wcc-my-app></wcc-my-app>
|
|
644
|
+
<script src="dist/bundle.js"></script>
|
|
645
|
+
</body>
|
|
646
|
+
</html>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**When to use `--bundle`:**
|
|
650
|
+
- Static HTML files opened from disk
|
|
651
|
+
- Electron apps loading local files
|
|
652
|
+
- Offline-first applications
|
|
653
|
+
- Quick prototyping without a dev server
|
|
654
|
+
- Distributing a complete app as HTML + JS
|
|
655
|
+
|
|
656
|
+
**When NOT to use `--bundle`:**
|
|
657
|
+
- Apps served via HTTP (use ES modules for better caching)
|
|
658
|
+
- When you need per-component lazy loading
|
|
659
|
+
- When using a bundler like Vite/Webpack (they handle bundling themselves)
|
|
660
|
+
|
|
622
661
|
### Configuration
|
|
623
662
|
|
|
624
663
|
Create `wcc.config.js` in your project root:
|
package/bin/wcc.js
CHANGED
|
@@ -291,6 +291,26 @@ function discoverFiles(dir) {
|
|
|
291
291
|
return results;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Discovers compiled .js entry points in the output directory.
|
|
296
|
+
* Excludes runtime files, stubs, and metadata.
|
|
297
|
+
*/
|
|
298
|
+
function discoverCompiledEntries(outputDir) {
|
|
299
|
+
const skip = new Set(['__wcc-signals.js', 'wcc-runtime.js', 'wcc-react.js', 'wcc-vue.js', 'bundle.js']);
|
|
300
|
+
const results = [];
|
|
301
|
+
function walk(dir) {
|
|
302
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
303
|
+
if (entry.isDirectory()) {
|
|
304
|
+
walk(join(dir, entry.name));
|
|
305
|
+
} else if (entry.isFile() && entry.name.endsWith('.js') && !skip.has(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
306
|
+
results.push(join(dir, entry.name));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
walk(outputDir);
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
async function main() {
|
|
295
315
|
const cwd = process.cwd();
|
|
296
316
|
const config = await loadConfig(cwd);
|
|
@@ -298,10 +318,43 @@ async function main() {
|
|
|
298
318
|
// CLI flags override config
|
|
299
319
|
if (process.argv.includes('--minify')) config.minify = true;
|
|
300
320
|
if (process.argv.includes('--comments')) config.comments = true;
|
|
321
|
+
const shouldBundle = process.argv.includes('--bundle');
|
|
301
322
|
|
|
302
323
|
if (command === 'build') {
|
|
303
324
|
const errors = await build(config, cwd);
|
|
304
325
|
if (errors > 0) process.exit(1);
|
|
326
|
+
|
|
327
|
+
// Bundle step: produce a single IIFE file from all compiled entry points
|
|
328
|
+
if (shouldBundle) {
|
|
329
|
+
const { build: esbuild } = await import('esbuild');
|
|
330
|
+
const outputDir = resolve(cwd, config.output);
|
|
331
|
+
const entryPoints = discoverCompiledEntries(outputDir);
|
|
332
|
+
|
|
333
|
+
if (entryPoints.length > 0) {
|
|
334
|
+
// Generate a virtual entry that imports all components
|
|
335
|
+
const bundleEntry = join(outputDir, '__bundle-entry.js');
|
|
336
|
+
const imports = entryPoints.map(f => {
|
|
337
|
+
let rel = relative(outputDir, f).replace(/\\/g, '/');
|
|
338
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
339
|
+
return `import '${rel}';`;
|
|
340
|
+
}).join('\n');
|
|
341
|
+
writeFileSync(bundleEntry, imports);
|
|
342
|
+
|
|
343
|
+
await esbuild({
|
|
344
|
+
entryPoints: [bundleEntry],
|
|
345
|
+
bundle: true,
|
|
346
|
+
format: 'iife',
|
|
347
|
+
outfile: join(outputDir, 'bundle.js'),
|
|
348
|
+
minify: !!config.minify,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Clean up temp entry
|
|
352
|
+
const { unlinkSync } = await import('node:fs');
|
|
353
|
+
unlinkSync(bundleEntry);
|
|
354
|
+
|
|
355
|
+
console.log(`Bundled ${entryPoints.length} components → ${config.output}/bundle.js`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
305
358
|
} else if (command === 'dev') {
|
|
306
359
|
await build(config, cwd);
|
|
307
360
|
const outputDir = resolve(cwd, config.output);
|
package/lib/codegen.js
CHANGED
|
@@ -877,6 +877,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
877
877
|
childImports = [],
|
|
878
878
|
exposeNames = [],
|
|
879
879
|
modelDefs = [],
|
|
880
|
+
dynamicComponents = [],
|
|
880
881
|
} = parseResult;
|
|
881
882
|
|
|
882
883
|
const signalNames = signals.map(s => s.name);
|
|
@@ -903,7 +904,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
903
904
|
// ── 1. Reactive runtime (shared import or inline) ──
|
|
904
905
|
if (options.comments) lines.push('// ── Runtime ──────────────────────────────────────────');
|
|
905
906
|
// Determine which runtime functions this component needs
|
|
906
|
-
const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
|
|
907
|
+
const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || dynamicComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
|
|
907
908
|
const needsComputed = computeds.length > 0;
|
|
908
909
|
const needsUntrack = watchers.length > 0;
|
|
909
910
|
|
|
@@ -1186,6 +1187,15 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1186
1187
|
lines.push(` this.${vn}_nodes = [];`);
|
|
1187
1188
|
}
|
|
1188
1189
|
|
|
1190
|
+
// ── dynamic component: anchor reference, state init ──
|
|
1191
|
+
for (const dyn of dynamicComponents) {
|
|
1192
|
+
const vn = dyn.varName;
|
|
1193
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(dyn.anchorPath, '__root')};`);
|
|
1194
|
+
lines.push(` this.${vn}_current = null;`);
|
|
1195
|
+
lines.push(` this.${vn}_tag = null;`);
|
|
1196
|
+
lines.push(` this.${vn}_propDisposers = [];`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1189
1199
|
// ── Ref DOM reference assignments (before appendChild moves nodes) ──
|
|
1190
1200
|
for (const rb of refBindings) {
|
|
1191
1201
|
lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
|
|
@@ -1671,6 +1681,41 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1671
1681
|
}
|
|
1672
1682
|
}
|
|
1673
1683
|
|
|
1684
|
+
// ── dynamic component effects ──
|
|
1685
|
+
for (const dyn of dynamicComponents) {
|
|
1686
|
+
const vn = dyn.varName;
|
|
1687
|
+
const isExpr = transformExpr(dyn.isExpression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1688
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1689
|
+
lines.push(` const __tag = ${isExpr};`);
|
|
1690
|
+
lines.push(` if (__tag === this.${vn}_tag) return;`);
|
|
1691
|
+
lines.push(` if (this.${vn}_current) {`);
|
|
1692
|
+
lines.push(` this.${vn}_propDisposers.forEach(d => d());`);
|
|
1693
|
+
lines.push(` this.${vn}_propDisposers = [];`);
|
|
1694
|
+
lines.push(` this.${vn}_current.remove();`);
|
|
1695
|
+
lines.push(` this.${vn}_current = null;`);
|
|
1696
|
+
lines.push(' }');
|
|
1697
|
+
lines.push(' if (__tag) {');
|
|
1698
|
+
lines.push(' const el = document.createElement(__tag);');
|
|
1699
|
+
// Emit nested prop effects
|
|
1700
|
+
for (const prop of dyn.props) {
|
|
1701
|
+
const propExprTransformed = transformExpr(prop.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1702
|
+
lines.push(` this.${vn}_propDisposers.push(__effect(() => {`);
|
|
1703
|
+
lines.push(` el.setAttribute('${prop.attr}', ${propExprTransformed});`);
|
|
1704
|
+
lines.push(' }));');
|
|
1705
|
+
}
|
|
1706
|
+
// Emit event listeners
|
|
1707
|
+
for (const evt of dyn.events) {
|
|
1708
|
+
const handlerExpr = generateEventHandler(evt.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
|
|
1709
|
+
lines.push(` el.addEventListener('${evt.event}', ${handlerExpr});`);
|
|
1710
|
+
}
|
|
1711
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(el, this.${vn}_anchor);`);
|
|
1712
|
+
lines.push(' customElements.upgrade(el);');
|
|
1713
|
+
lines.push(` this.${vn}_current = el;`);
|
|
1714
|
+
lines.push(' }');
|
|
1715
|
+
lines.push(` this.${vn}_tag = __tag;`);
|
|
1716
|
+
lines.push(' }));');
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1674
1719
|
// Lifecycle: onMount hooks (at the very end of connectedCallback)
|
|
1675
1720
|
for (const hook of onMountHooks) {
|
|
1676
1721
|
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
package/lib/compiler.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { parseHTML } from 'linkedom';
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
|
-
import { walkTree, processIfChains, processForBlocks, recomputeAnchorPath, detectRefs } from './tree-walker.js';
|
|
12
|
+
import { walkTree, processIfChains, processForBlocks, processDynamicComponents, recomputeAnchorPath, detectRefs } from './tree-walker.js';
|
|
13
13
|
import { generateComponent } from './codegen.js';
|
|
14
14
|
import { parseSFC } from './sfc-parser.js';
|
|
15
15
|
import {
|
|
@@ -290,6 +290,7 @@ async function compileSFC(filePath, config) {
|
|
|
290
290
|
childImports: [],
|
|
291
291
|
exposeNames,
|
|
292
292
|
modelDefs,
|
|
293
|
+
dynamicComponents: [],
|
|
293
294
|
};
|
|
294
295
|
|
|
295
296
|
// 16. Process template through linkedom → tree-walker → codegen
|
|
@@ -307,6 +308,7 @@ async function compileSFC(filePath, config) {
|
|
|
307
308
|
|
|
308
309
|
const forBlocks = processForBlocks(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
309
310
|
const ifBlocks = processIfChains(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
311
|
+
const dynamicComponents = processDynamicComponents(rootEl, []);
|
|
310
312
|
|
|
311
313
|
rootEl.normalize();
|
|
312
314
|
|
|
@@ -316,6 +318,9 @@ async function compileSFC(filePath, config) {
|
|
|
316
318
|
for (const ib of ifBlocks) {
|
|
317
319
|
ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
318
320
|
}
|
|
321
|
+
for (const dc of dynamicComponents) {
|
|
322
|
+
dc.anchorPath = recomputeAnchorPath(rootEl, dc._anchorNode);
|
|
323
|
+
}
|
|
319
324
|
|
|
320
325
|
const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
|
|
321
326
|
|
|
@@ -412,6 +417,7 @@ async function compileSFC(filePath, config) {
|
|
|
412
417
|
parseResult.slots = slots;
|
|
413
418
|
parseResult.refBindings = refBindings;
|
|
414
419
|
parseResult.childComponents = childComponents;
|
|
420
|
+
parseResult.dynamicComponents = dynamicComponents;
|
|
415
421
|
|
|
416
422
|
parseResult.childImports = childImports;
|
|
417
423
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
@@ -71,6 +71,11 @@ export function normalizeTemplate(html, options) {
|
|
|
71
71
|
const TAG_RE = /<(\/?)([A-Za-z][\w-]*)((?:\s[^>]*?)?)(\/?)>/g;
|
|
72
72
|
|
|
73
73
|
return html.replace(TAG_RE, (match, closingSlash, tagName, attrs, selfClosing) => {
|
|
74
|
+
// Guard: preserve <component> tags as-is — this is a compiler directive, not a custom element
|
|
75
|
+
if (tagName.toLowerCase() === 'component') {
|
|
76
|
+
return match;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
let normalizedTag = tagName;
|
|
75
80
|
|
|
76
81
|
// Step 1: Convert PascalCase to kebab-case
|
package/lib/tree-walker.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { parseHTML } from 'linkedom';
|
|
16
16
|
import { BOOLEAN_ATTRIBUTES } from './types.js';
|
|
17
17
|
|
|
18
|
-
/** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, ModelPropBinding, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding } from './types.js' */
|
|
18
|
+
/** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, ModelPropBinding, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding, DynamicComponentBinding, DynPropBinding, DynEventBinding } from './types.js' */
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Walk a DOM tree rooted at rootEl, discovering bindings and events.
|
|
@@ -871,6 +871,96 @@ export function processForBlocks(parent, parentPath, signalNames, computedNames,
|
|
|
871
871
|
}
|
|
872
872
|
|
|
873
873
|
|
|
874
|
+
// ── Dynamic component processing ────────────────────────────────────
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Process dynamic component elements (`<component :is="expr">`) in descendants of a parent element.
|
|
878
|
+
* Recursively detects `<component>` elements, validates the `:is` attribute,
|
|
879
|
+
* extracts prop/event bindings, and replaces them with comment anchors.
|
|
880
|
+
*
|
|
881
|
+
* @param {Element} parent - Root element to search
|
|
882
|
+
* @param {string[]} parentPath - DOM path to parent from __root
|
|
883
|
+
* @returns {DynamicComponentBinding[]}
|
|
884
|
+
*/
|
|
885
|
+
export function processDynamicComponents(parent, parentPath) {
|
|
886
|
+
/** @type {DynamicComponentBinding[]} */
|
|
887
|
+
const dynamicComponents = [];
|
|
888
|
+
let dynIdx = 0;
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Recursively search for <component> elements in the subtree.
|
|
892
|
+
* @param {Element} node
|
|
893
|
+
* @param {string[]} currentPath
|
|
894
|
+
*/
|
|
895
|
+
function findDynamicComponents(node, currentPath) {
|
|
896
|
+
const children = Array.from(node.childNodes);
|
|
897
|
+
for (let i = 0; i < children.length; i++) {
|
|
898
|
+
const child = children[i];
|
|
899
|
+
if (child.nodeType !== 1) continue;
|
|
900
|
+
const el = /** @type {Element} */ (child);
|
|
901
|
+
|
|
902
|
+
if (el.tagName === 'COMPONENT') {
|
|
903
|
+
// Validate :is attribute is present
|
|
904
|
+
const isExpr = el.getAttribute(':is');
|
|
905
|
+
if (!isExpr) {
|
|
906
|
+
const error = new Error(':is attribute is required on <component> elements');
|
|
907
|
+
/** @ts-expect-error — custom error code */
|
|
908
|
+
error.code = 'MISSING_IS_ATTRIBUTE';
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Collect prop bindings (:attr="expr", excluding :is)
|
|
913
|
+
/** @type {DynPropBinding[]} */
|
|
914
|
+
const props = [];
|
|
915
|
+
// Collect event bindings (@event="handler")
|
|
916
|
+
/** @type {DynEventBinding[]} */
|
|
917
|
+
const events = [];
|
|
918
|
+
|
|
919
|
+
for (const attr of Array.from(el.attributes)) {
|
|
920
|
+
if (attr.name.startsWith(':') && attr.name !== ':is') {
|
|
921
|
+
props.push({
|
|
922
|
+
attr: attr.name.slice(1),
|
|
923
|
+
expression: attr.value,
|
|
924
|
+
});
|
|
925
|
+
} else if (attr.name.startsWith('@')) {
|
|
926
|
+
events.push({
|
|
927
|
+
event: attr.name.slice(1),
|
|
928
|
+
handler: attr.value,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Replace <component> with a comment node <!-- dynamic -->
|
|
934
|
+
const doc = node.ownerDocument;
|
|
935
|
+
const comment = doc.createComment(' dynamic ');
|
|
936
|
+
node.replaceChild(comment, el);
|
|
937
|
+
|
|
938
|
+
// Calculate anchorPath
|
|
939
|
+
const updatedChildren = Array.from(node.childNodes);
|
|
940
|
+
const commentIndex = updatedChildren.indexOf(comment);
|
|
941
|
+
const anchorPath = [...currentPath, `childNodes[${commentIndex}]`];
|
|
942
|
+
|
|
943
|
+
// Create DynamicComponentBinding
|
|
944
|
+
dynamicComponents.push({
|
|
945
|
+
varName: `__dyn${dynIdx++}`,
|
|
946
|
+
isExpression: isExpr,
|
|
947
|
+
props,
|
|
948
|
+
events,
|
|
949
|
+
anchorPath,
|
|
950
|
+
_anchorNode: comment,
|
|
951
|
+
});
|
|
952
|
+
} else {
|
|
953
|
+
// Recurse into non-component elements to find nested dynamic components
|
|
954
|
+
const childPath = [...currentPath, `childNodes[${i}]`];
|
|
955
|
+
findDynamicComponents(el, childPath);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
findDynamicComponents(parent, parentPath);
|
|
961
|
+
return dynamicComponents;
|
|
962
|
+
}
|
|
963
|
+
|
|
874
964
|
// ── Ref detection ───────────────────────────────────────────────────
|
|
875
965
|
|
|
876
966
|
/**
|
package/lib/types.js
CHANGED
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
* @property {RefBinding[]} refBindings — ref attribute bindings from template (empty array if none)
|
|
101
101
|
* @property {ChildComponentBinding[]} childComponents — Child component bindings (empty array if none)
|
|
102
102
|
* @property {ChildComponentImport[]} childImports — Resolved child component imports (empty array if none)
|
|
103
|
+
* @property {DynamicComponentBinding[]} dynamicComponents — Dynamic component bindings (empty array if none)
|
|
103
104
|
* @property {string[]} exposeNames — Property names from defineExpose (empty array if none)
|
|
104
105
|
*/
|
|
105
106
|
|
|
@@ -227,6 +228,27 @@
|
|
|
227
228
|
* @property {boolean} sideEffect — true if this is a side-effect import (no identifier)
|
|
228
229
|
*/
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* @typedef {Object} DynPropBinding
|
|
233
|
+
* @property {string} attr — Attribute name (e.g., 'label', 'count')
|
|
234
|
+
* @property {string} expression — Expression string (e.g., "name()", "props.title")
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @typedef {Object} DynEventBinding
|
|
239
|
+
* @property {string} event — Event name (e.g., 'click', 'change')
|
|
240
|
+
* @property {string} handler — Handler expression (e.g., "handleClick", "handleClick($event)")
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @typedef {Object} DynamicComponentBinding
|
|
245
|
+
* @property {string} varName — Unique name: '__dyn0', '__dyn1', ...
|
|
246
|
+
* @property {string} isExpression — The :is attribute expression (e.g., "currentTag()")
|
|
247
|
+
* @property {DynPropBinding[]} props — Prop bindings from :attr="expr" attributes
|
|
248
|
+
* @property {DynEventBinding[]} events — Event bindings from @event="handler" attributes
|
|
249
|
+
* @property {string[]} anchorPath — DOM path to comment anchor from __root
|
|
250
|
+
*/
|
|
251
|
+
|
|
230
252
|
/**
|
|
231
253
|
* Set of HTML attributes that use property assignment instead of setAttribute.
|
|
232
254
|
* @type {Set<string>}
|
package/package.json
CHANGED