@sprlab/wccompiler 0.11.12 → 0.12.1
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 +13 -2
- package/lib/compiler.js +38 -73
- package/lib/import-resolver.js +160 -0
- package/lib/template-normalizer.js +23 -3
- package/lib/types.js +3 -1
- 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
|
@@ -923,7 +923,14 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
923
923
|
|
|
924
924
|
// ── 1b. Child component imports ──
|
|
925
925
|
for (const ci of childImports) {
|
|
926
|
-
|
|
926
|
+
if (ci.sideEffect) {
|
|
927
|
+
// Side-effect import: no identifier, child self-registers
|
|
928
|
+
lines.push(`import '${ci.importPath}';`);
|
|
929
|
+
} else {
|
|
930
|
+
// Named import with guarded registration
|
|
931
|
+
lines.push(`import ${ci.identifier} from '${ci.importPath}';`);
|
|
932
|
+
lines.push(`if (!customElements.get(${ci.identifier}.__meta.tag)) customElements.define(${ci.identifier}.__meta.tag, ${ci.identifier});`);
|
|
933
|
+
}
|
|
927
934
|
}
|
|
928
935
|
if (childImports.length > 0) {
|
|
929
936
|
lines.push('');
|
|
@@ -2012,7 +2019,11 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
2012
2019
|
lines.push('');
|
|
2013
2020
|
|
|
2014
2021
|
// ── 5. Custom element registration ──
|
|
2015
|
-
lines.push(`customElements.define('${tagName}', ${className});`);
|
|
2022
|
+
lines.push(`if (!customElements.get('${tagName}')) customElements.define('${tagName}', ${className});`);
|
|
2023
|
+
lines.push('');
|
|
2024
|
+
|
|
2025
|
+
// ── 6. Default export (enables named imports from parent components) ──
|
|
2026
|
+
lines.push(`export default ${className};`);
|
|
2016
2027
|
|
|
2017
2028
|
return lines.join('\n');
|
|
2018
2029
|
}
|
package/lib/compiler.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { parseHTML } from 'linkedom';
|
|
10
|
-
import { readFileSync
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { basename } from 'node:path';
|
|
12
12
|
import { walkTree, processIfChains, processForBlocks, recomputeAnchorPath, detectRefs } from './tree-walker.js';
|
|
13
13
|
import { generateComponent } from './codegen.js';
|
|
14
14
|
import { parseSFC } from './sfc-parser.js';
|
|
@@ -43,30 +43,8 @@ import {
|
|
|
43
43
|
validateUndeclaredEmits,
|
|
44
44
|
} from './parser-extractors.js';
|
|
45
45
|
import { stripTypes } from './parser.js';
|
|
46
|
-
import { normalizeTemplate } from './template-normalizer.js';
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Resolve a child component's source file path by tag name.
|
|
50
|
-
*
|
|
51
|
-
* Searches for a file named after the tag in the source directory,
|
|
52
|
-
* trying extensions in priority order: .wcc, .js, .ts
|
|
53
|
-
*
|
|
54
|
-
* @param {string} tag — The custom element tag name (e.g., 'wcc-child')
|
|
55
|
-
* @param {string} sourceDir — Directory of the parent component
|
|
56
|
-
* @param {object} [config] — Optional config (reserved for future use)
|
|
57
|
-
* @returns {string | null} Relative import path (e.g., './wcc-child.js') or null if not found
|
|
58
|
-
*/
|
|
59
|
-
function resolveChildComponent(tag, sourceDir, config) {
|
|
60
|
-
const extensions = ['.wcc', '.js', '.ts'];
|
|
61
|
-
for (const ext of extensions) {
|
|
62
|
-
const candidate = resolve(sourceDir, `${tag}${ext}`);
|
|
63
|
-
if (existsSync(candidate)) {
|
|
64
|
-
// Return as a relative .js import path (compiled output)
|
|
65
|
-
return `./${tag}.js`;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
46
|
+
import { normalizeTemplate, pascalToKebab } from './template-normalizer.js';
|
|
47
|
+
import { extractWccImports } from './import-resolver.js';
|
|
70
48
|
|
|
71
49
|
/**
|
|
72
50
|
* Compile a single .wcc SFC file into a self-contained JS component.
|
|
@@ -88,14 +66,38 @@ async function compileSFC(filePath, config) {
|
|
|
88
66
|
// 2. Process script block — mirrors parser.js logic
|
|
89
67
|
let source = stripMacroImport(descriptor.script);
|
|
90
68
|
|
|
91
|
-
// 2b. Extract
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
69
|
+
// 2b. Extract .wcc imports using the import resolver
|
|
70
|
+
const wccImports = extractWccImports(source, fileName);
|
|
71
|
+
|
|
72
|
+
// Build importMap: PascalCase identifier → kebab-case tag
|
|
73
|
+
/** @type {Map<string, string>} */
|
|
74
|
+
const importMap = new Map();
|
|
75
|
+
for (const imp of wccImports.named) {
|
|
76
|
+
importMap.set(imp.identifier, pascalToKebab(imp.identifier));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build childImports from extracted imports
|
|
80
|
+
/** @type {import('./types.js').ChildComponentImport[]} */
|
|
81
|
+
const childImports = [];
|
|
82
|
+
for (const imp of wccImports.named) {
|
|
83
|
+
childImports.push({
|
|
84
|
+
tag: pascalToKebab(imp.identifier),
|
|
85
|
+
identifier: imp.identifier,
|
|
86
|
+
importPath: imp.compiledPath,
|
|
87
|
+
sideEffect: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const imp of wccImports.sideEffect) {
|
|
91
|
+
childImports.push({
|
|
92
|
+
tag: '',
|
|
93
|
+
identifier: '',
|
|
94
|
+
importPath: imp.compiledPath,
|
|
95
|
+
sideEffect: true,
|
|
96
|
+
});
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
// Use strippedSource (with .wcc imports removed) for subsequent extraction steps
|
|
100
|
+
source = wccImports.strippedSource;
|
|
99
101
|
|
|
100
102
|
// 3. Extract props/emits from generic forms BEFORE type stripping
|
|
101
103
|
const propsFromGeneric = extractPropsGeneric(source);
|
|
@@ -291,7 +293,7 @@ async function compileSFC(filePath, config) {
|
|
|
291
293
|
};
|
|
292
294
|
|
|
293
295
|
// 16. Process template through linkedom → tree-walker → codegen
|
|
294
|
-
const normalizedTemplate = normalizeTemplate(template);
|
|
296
|
+
const normalizedTemplate = normalizeTemplate(template, { importMap, fileName });
|
|
295
297
|
const { document } = parseHTML(`<div id="__root">${normalizedTemplate}</div>`);
|
|
296
298
|
const rootEl = document.getElementById('__root');
|
|
297
299
|
|
|
@@ -395,39 +397,8 @@ async function compileSFC(filePath, config) {
|
|
|
395
397
|
}
|
|
396
398
|
}
|
|
397
399
|
|
|
398
|
-
// 18.
|
|
399
|
-
|
|
400
|
-
const childImports = [];
|
|
401
|
-
const allChildTags = new Set(childComponents.map(c => c.tag));
|
|
402
|
-
|
|
403
|
-
// Collect child tags from if branches
|
|
404
|
-
for (const ifBlock of ifBlocks) {
|
|
405
|
-
for (const branch of ifBlock.branches) {
|
|
406
|
-
if (branch.childComponents) {
|
|
407
|
-
for (const cc of branch.childComponents) allChildTags.add(cc.tag);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Collect child tags from each blocks
|
|
413
|
-
for (const forBlock of forBlocks) {
|
|
414
|
-
if (forBlock.childComponents) {
|
|
415
|
-
for (const cc of forBlock.childComponents) allChildTags.add(cc.tag);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (allChildTags.size > 0) {
|
|
420
|
-
const sourceDir = dirname(filePath);
|
|
421
|
-
|
|
422
|
-
for (const tag of allChildTags) {
|
|
423
|
-
const resolved = resolveChildComponent(tag, sourceDir, config);
|
|
424
|
-
if (resolved) {
|
|
425
|
-
childImports.push({ tag, importPath: resolved });
|
|
426
|
-
} else {
|
|
427
|
-
console.warn(`Warning: child component <${tag}> used in template but source file not found`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
400
|
+
// 18. Child imports already built from extractWccImports (step 2b)
|
|
401
|
+
// No filesystem scanning needed — imports are explicit
|
|
431
402
|
|
|
432
403
|
// 19. Merge tree-walker results into ParseResult
|
|
433
404
|
parseResult.bindings = bindings;
|
|
@@ -442,12 +413,6 @@ async function compileSFC(filePath, config) {
|
|
|
442
413
|
parseResult.refBindings = refBindings;
|
|
443
414
|
parseResult.childComponents = childComponents;
|
|
444
415
|
|
|
445
|
-
// Add manual .wcc imports from script block
|
|
446
|
-
for (const imp of manualImports) {
|
|
447
|
-
if (!childImports.find(ci => ci.importPath === imp)) {
|
|
448
|
-
childImports.push({ tag: '', importPath: imp });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
416
|
parseResult.childImports = childImports;
|
|
452
417
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
453
418
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import resolver for .wcc component imports.
|
|
3
|
+
*
|
|
4
|
+
* Extracts and validates `.wcc` import statements from the script block,
|
|
5
|
+
* producing a structured representation of named default imports and
|
|
6
|
+
* side-effect imports. Rejects invalid import forms (namespace, named exports).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} WccNamedImport
|
|
13
|
+
* @property {string} identifier — The import name (e.g., 'WccBadge', 'MyButton')
|
|
14
|
+
* @property {string} sourcePath — Original .wcc path (e.g., './wcc-badge.wcc')
|
|
15
|
+
* @property {string} compiledPath — Rewritten .js path (e.g., './wcc-badge.js')
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} WccSideEffectImport
|
|
20
|
+
* @property {string} sourcePath — Original .wcc path (e.g., './child.wcc')
|
|
21
|
+
* @property {string} compiledPath — Rewritten .js path (e.g., './child.js')
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} WccImportResult
|
|
26
|
+
* @property {WccNamedImport[]} named — Named default imports
|
|
27
|
+
* @property {WccSideEffectImport[]} sideEffect — Side-effect imports
|
|
28
|
+
* @property {string} strippedSource — Script source with .wcc imports removed
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// ── Regex patterns ───────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Matches any import statement that references a .wcc file.
|
|
35
|
+
* Captures the full import line for removal and classification.
|
|
36
|
+
*
|
|
37
|
+
* Groups:
|
|
38
|
+
* - Full match: the entire import statement line
|
|
39
|
+
*
|
|
40
|
+
* We use individual patterns below for classification.
|
|
41
|
+
*/
|
|
42
|
+
const WCC_IMPORT_LINE_RE = /^[ \t]*import\s+.*?['"]([^'"]+\.wcc)['"]\s*;?[ \t]*$/gm;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Named default import: import Identifier from './path.wcc'
|
|
46
|
+
*/
|
|
47
|
+
const NAMED_DEFAULT_RE = /^[ \t]*import\s+([$\w]+)\s+from\s+['"]([^'"]+\.wcc)['"]\s*;?[ \t]*$/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Side-effect import: import './path.wcc'
|
|
51
|
+
*/
|
|
52
|
+
const SIDE_EFFECT_RE = /^[ \t]*import\s+['"]([^'"]+\.wcc)['"]\s*;?[ \t]*$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Namespace import: import * as Foo from './path.wcc'
|
|
56
|
+
*/
|
|
57
|
+
const NAMESPACE_RE = /^[ \t]*import\s+\*\s+as\s+\w+\s+from\s+['"][^'"]+\.wcc['"]\s*;?[ \t]*$/;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Named exports import: import { Foo } from './path.wcc'
|
|
61
|
+
* Also matches: import { Foo, Bar } from './path.wcc'
|
|
62
|
+
* Also matches: import { Foo as Bar } from './path.wcc'
|
|
63
|
+
*/
|
|
64
|
+
const NAMED_EXPORTS_RE = /^[ \t]*import\s+\{[^}]*\}\s+from\s+['"][^'"]+\.wcc['"]\s*;?[ \t]*$/;
|
|
65
|
+
|
|
66
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Rewrite a .wcc path to .js by replacing the extension.
|
|
70
|
+
* Preserves all relative path segments.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} wccPath — e.g., '../shared/wcc-button.wcc'
|
|
73
|
+
* @returns {string} — e.g., '../shared/wcc-button.js'
|
|
74
|
+
*/
|
|
75
|
+
function rewriteExtension(wccPath) {
|
|
76
|
+
return wccPath.replace(/\.wcc$/, '.js');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Main export ──────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract all .wcc imports from a script source string.
|
|
83
|
+
* Validates import forms and rejects invalid patterns.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} source — Script block content
|
|
86
|
+
* @param {string} fileName — Source file name for error messages
|
|
87
|
+
* @returns {WccImportResult}
|
|
88
|
+
* @throws {Error} with code 'INVALID_WCC_IMPORT' for namespace/named exports
|
|
89
|
+
*/
|
|
90
|
+
export function extractWccImports(source, fileName) {
|
|
91
|
+
/** @type {WccNamedImport[]} */
|
|
92
|
+
const named = [];
|
|
93
|
+
/** @type {WccSideEffectImport[]} */
|
|
94
|
+
const sideEffect = [];
|
|
95
|
+
|
|
96
|
+
// Collect all .wcc import lines for processing
|
|
97
|
+
const lines = source.split('\n');
|
|
98
|
+
/** @type {Set<number>} */
|
|
99
|
+
const linesToRemove = new Set();
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
|
|
104
|
+
// Check if this line contains a .wcc import
|
|
105
|
+
if (!line.match(/import\s.*\.wcc['"]/) && !line.match(/import\s+['"][^'"]+\.wcc['"]/)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check for invalid forms first
|
|
110
|
+
if (NAMESPACE_RE.test(line)) {
|
|
111
|
+
const error = new Error(
|
|
112
|
+
`Invalid import form in '${fileName}': .wcc files only support default imports (import Foo from './foo.wcc') or side-effect imports (import './foo.wcc')`
|
|
113
|
+
);
|
|
114
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
115
|
+
error.code = 'INVALID_WCC_IMPORT';
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (NAMED_EXPORTS_RE.test(line)) {
|
|
120
|
+
const error = new Error(
|
|
121
|
+
`Invalid import form in '${fileName}': .wcc files only support default imports (import Foo from './foo.wcc') or side-effect imports (import './foo.wcc')`
|
|
122
|
+
);
|
|
123
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
124
|
+
error.code = 'INVALID_WCC_IMPORT';
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check for named default import
|
|
129
|
+
const namedMatch = line.match(NAMED_DEFAULT_RE);
|
|
130
|
+
if (namedMatch) {
|
|
131
|
+
const identifier = namedMatch[1];
|
|
132
|
+
const sourcePath = namedMatch[2];
|
|
133
|
+
named.push({
|
|
134
|
+
identifier,
|
|
135
|
+
sourcePath,
|
|
136
|
+
compiledPath: rewriteExtension(sourcePath),
|
|
137
|
+
});
|
|
138
|
+
linesToRemove.add(i);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for side-effect import
|
|
143
|
+
const sideEffectMatch = line.match(SIDE_EFFECT_RE);
|
|
144
|
+
if (sideEffectMatch) {
|
|
145
|
+
const sourcePath = sideEffectMatch[1];
|
|
146
|
+
sideEffect.push({
|
|
147
|
+
sourcePath,
|
|
148
|
+
compiledPath: rewriteExtension(sourcePath),
|
|
149
|
+
});
|
|
150
|
+
linesToRemove.add(i);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Build stripped source by removing .wcc import lines
|
|
156
|
+
const strippedLines = lines.filter((_, i) => !linesToRemove.has(i));
|
|
157
|
+
const strippedSource = strippedLines.join('\n');
|
|
158
|
+
|
|
159
|
+
return { named, sideEffect, strippedSource };
|
|
160
|
+
}
|
|
@@ -45,13 +45,19 @@ export function isPascalCase(name) {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Normalize a WCC template string:
|
|
48
|
-
* 1. Convert PascalCase tags to kebab-case
|
|
48
|
+
* 1. Convert PascalCase tags to kebab-case (validated against importMap if provided)
|
|
49
49
|
* 2. Expand self-closing custom elements to open+close pairs
|
|
50
50
|
*
|
|
51
51
|
* @param {string} html — Raw template HTML
|
|
52
|
+
* @param {object} [options]
|
|
53
|
+
* @param {Map<string, string>} [options.importMap] — PascalCase identifier → kebab-case tag
|
|
54
|
+
* @param {string} [options.fileName] — Source file for error messages
|
|
52
55
|
* @returns {string} — Normalized HTML ready for DOM parsing
|
|
56
|
+
* @throws {Error} with code 'UNRESOLVED_COMPONENT' if PascalCase tag has no matching import
|
|
53
57
|
*/
|
|
54
|
-
export function normalizeTemplate(html) {
|
|
58
|
+
export function normalizeTemplate(html, options) {
|
|
59
|
+
const { importMap, fileName } = options || {};
|
|
60
|
+
|
|
55
61
|
// Match opening tags (including self-closing): <TagName ...> or <TagName ... />
|
|
56
62
|
// Also match closing tags: </TagName>
|
|
57
63
|
//
|
|
@@ -69,7 +75,21 @@ export function normalizeTemplate(html) {
|
|
|
69
75
|
|
|
70
76
|
// Step 1: Convert PascalCase to kebab-case
|
|
71
77
|
if (isPascalCase(tagName)) {
|
|
72
|
-
|
|
78
|
+
if (importMap) {
|
|
79
|
+
// Validate against the import map
|
|
80
|
+
if (importMap.has(tagName)) {
|
|
81
|
+
normalizedTag = importMap.get(tagName);
|
|
82
|
+
} else {
|
|
83
|
+
const error = new Error(
|
|
84
|
+
`Unresolved component '<${tagName}>' in '${fileName || 'unknown'}'. Did you forget to import it?`
|
|
85
|
+
);
|
|
86
|
+
error.code = 'UNRESOLVED_COMPONENT';
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// Backward compatible: convert all PascalCase to kebab-case
|
|
91
|
+
normalizedTag = pascalToKebab(tagName);
|
|
92
|
+
}
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
// Step 2: Handle self-closing tags
|
package/lib/types.js
CHANGED
|
@@ -221,8 +221,10 @@
|
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
223
|
* @typedef {Object} ChildComponentImport
|
|
224
|
-
* @property {string} tag — Child component tag name
|
|
224
|
+
* @property {string} tag — Child component tag name (kebab-case, used in template)
|
|
225
|
+
* @property {string} identifier — Import identifier (PascalCase, e.g., 'WccBadge')
|
|
225
226
|
* @property {string} importPath — Relative import path (e.g., './wcc-badge.js')
|
|
227
|
+
* @property {boolean} sideEffect — true if this is a side-effect import (no identifier)
|
|
226
228
|
*/
|
|
227
229
|
|
|
228
230
|
/**
|
package/package.json
CHANGED