@sprlab/wccompiler 0.2.0 → 0.2.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/lib/codegen.js +36 -0
- package/lib/compiler.js +79 -2
- package/lib/tree-walker.js +42 -3
- package/lib/types.js +23 -0
- package/package.json +1 -1
package/lib/codegen.js
CHANGED
|
@@ -322,6 +322,8 @@ export function generateComponent(parseResult) {
|
|
|
322
322
|
constantVars = [],
|
|
323
323
|
refs = [],
|
|
324
324
|
refBindings = [],
|
|
325
|
+
childComponents = [],
|
|
326
|
+
childImports = [],
|
|
325
327
|
} = parseResult;
|
|
326
328
|
|
|
327
329
|
const signalNames = signals.map(s => s.name);
|
|
@@ -336,6 +338,14 @@ export function generateComponent(parseResult) {
|
|
|
336
338
|
lines.push(reactiveRuntime.trim());
|
|
337
339
|
lines.push('');
|
|
338
340
|
|
|
341
|
+
// ── 1b. Child component imports ──
|
|
342
|
+
for (const ci of childImports) {
|
|
343
|
+
lines.push(`import '${ci.importPath}';`);
|
|
344
|
+
}
|
|
345
|
+
if (childImports.length > 0) {
|
|
346
|
+
lines.push('');
|
|
347
|
+
}
|
|
348
|
+
|
|
339
349
|
// ── 2. CSS injection (scoped CSS into document.head, always) ──
|
|
340
350
|
if (style) {
|
|
341
351
|
const scoped = scopeCSS(style, tagName);
|
|
@@ -410,6 +420,11 @@ export function generateComponent(parseResult) {
|
|
|
410
420
|
lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
|
|
411
421
|
}
|
|
412
422
|
|
|
423
|
+
// Assign DOM refs for child component instances
|
|
424
|
+
for (const cc of childComponents) {
|
|
425
|
+
lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
|
|
426
|
+
}
|
|
427
|
+
|
|
413
428
|
// Assign DOM refs for attr bindings (reuse ref when same path)
|
|
414
429
|
const attrPathMap = new Map();
|
|
415
430
|
for (const ab of attrBindings) {
|
|
@@ -542,6 +557,27 @@ export function generateComponent(parseResult) {
|
|
|
542
557
|
}
|
|
543
558
|
}
|
|
544
559
|
|
|
560
|
+
// Child component reactive prop bindings
|
|
561
|
+
for (const cc of childComponents) {
|
|
562
|
+
for (const pb of cc.propBindings) {
|
|
563
|
+
let ref;
|
|
564
|
+
if (pb.type === 'prop') {
|
|
565
|
+
ref = `this._s_${pb.expr}()`;
|
|
566
|
+
} else if (pb.type === 'computed') {
|
|
567
|
+
ref = `this._c_${pb.expr}()`;
|
|
568
|
+
} else if (pb.type === 'signal') {
|
|
569
|
+
ref = `this._${pb.expr}()`;
|
|
570
|
+
} else if (pb.type === 'constant') {
|
|
571
|
+
ref = `this._const_${pb.expr}`;
|
|
572
|
+
} else {
|
|
573
|
+
ref = `this._${pb.expr}()`;
|
|
574
|
+
}
|
|
575
|
+
lines.push(' __effect(() => {');
|
|
576
|
+
lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
|
|
577
|
+
lines.push(' });');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
545
581
|
// User effects
|
|
546
582
|
for (const eff of effects) {
|
|
547
583
|
const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
package/lib/compiler.js
CHANGED
|
@@ -8,9 +8,67 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { JSDOM } from 'jsdom';
|
|
11
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { resolve, relative, dirname, extname } from 'node:path';
|
|
11
13
|
import { parse } from './parser.js';
|
|
12
14
|
import { walkTree, processIfChains, processForBlocks, recomputeAnchorPath, detectRefs } from './tree-walker.js';
|
|
13
15
|
import { generateComponent } from './codegen.js';
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a child component's import path by searching for a source file
|
|
18
|
+
* whose defineComponent({ tag }) matches the given tag name.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} tag — Child component tag name (e.g., 'wcc-badge')
|
|
21
|
+
* @param {string} sourceDir — Directory of the parent component source file
|
|
22
|
+
* @param {object} [config] — Optional config with input/output dirs
|
|
23
|
+
* @returns {string | null} Relative import path (e.g., './wcc-badge.js') or null if not found
|
|
24
|
+
*/
|
|
25
|
+
function resolveChildComponent(tag, sourceDir, config) {
|
|
26
|
+
// Search in the same directory and subdirectories for a matching source file
|
|
27
|
+
const searchDirs = [sourceDir];
|
|
28
|
+
|
|
29
|
+
// Also search parent directory (common case: components in sibling folders)
|
|
30
|
+
const parentDir = dirname(sourceDir);
|
|
31
|
+
if (parentDir !== sourceDir) {
|
|
32
|
+
searchDirs.push(parentDir);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const dir of searchDirs) {
|
|
36
|
+
if (!existsSync(dir)) continue;
|
|
37
|
+
try {
|
|
38
|
+
const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isFile()) continue;
|
|
41
|
+
const ext = extname(entry.name);
|
|
42
|
+
if (ext !== '.js' && ext !== '.ts') continue;
|
|
43
|
+
if (entry.name.includes('.test.')) continue;
|
|
44
|
+
if (entry.name.endsWith('.d.ts')) continue;
|
|
45
|
+
|
|
46
|
+
const fullPath = resolve(dir, entry.parentPath ? relative(dir, entry.parentPath) : '', entry.name);
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
49
|
+
// Quick check: does this file define the component with the matching tag?
|
|
50
|
+
const tagMatch = content.match(/defineComponent\(\s*\{[^}]*tag\s*:\s*['"]([^'"]+)['"]/);
|
|
51
|
+
if (tagMatch && tagMatch[1] === tag) {
|
|
52
|
+
// Compute relative path from sourceDir to this file, with .js extension
|
|
53
|
+
let relPath = relative(sourceDir, fullPath);
|
|
54
|
+
// Ensure .js extension (replace .ts)
|
|
55
|
+
relPath = relPath.replace(/\.ts$/, '.js');
|
|
56
|
+
// Ensure starts with ./
|
|
57
|
+
if (!relPath.startsWith('.')) relPath = './' + relPath;
|
|
58
|
+
return relPath;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Skip files that can't be read
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Skip dirs that can't be listed
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
14
72
|
/**
|
|
15
73
|
* Compile a single .ts/.js source file into a self-contained JS component.
|
|
16
74
|
*
|
|
@@ -52,7 +110,7 @@ export async function compile(filePath, config) {
|
|
|
52
110
|
}
|
|
53
111
|
|
|
54
112
|
// 8. Walk the tree (discovers bindings/events/showBindings/slots in non-conditional content)
|
|
55
|
-
const { bindings, events, showBindings, modelBindings, attrBindings, slots } = walkTree(rootEl, signalNames, computedNames, propNames);
|
|
113
|
+
const { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNames);
|
|
56
114
|
|
|
57
115
|
// 9. Detect refs (after walkTree — ref attributes are compile-time directives)
|
|
58
116
|
const refBindings = detectRefs(rootEl);
|
|
@@ -106,7 +164,24 @@ export async function compile(filePath, config) {
|
|
|
106
164
|
}
|
|
107
165
|
}
|
|
108
166
|
|
|
109
|
-
// 11.
|
|
167
|
+
// 11. Resolve child component imports
|
|
168
|
+
/** @type {import('./types.js').ChildComponentImport[]} */
|
|
169
|
+
const childImports = [];
|
|
170
|
+
if (childComponents.length > 0) {
|
|
171
|
+
const uniqueTags = [...new Set(childComponents.map(c => c.tag))];
|
|
172
|
+
const sourceDir = dirname(filePath);
|
|
173
|
+
|
|
174
|
+
for (const tag of uniqueTags) {
|
|
175
|
+
const resolved = resolveChildComponent(tag, sourceDir, config);
|
|
176
|
+
if (resolved) {
|
|
177
|
+
childImports.push({ tag, importPath: resolved });
|
|
178
|
+
} else {
|
|
179
|
+
console.warn(`Warning: child component <${tag}> used in template but source file not found`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 12. Merge results into ParseResult
|
|
110
185
|
parseResult.bindings = bindings;
|
|
111
186
|
parseResult.events = events;
|
|
112
187
|
parseResult.showBindings = showBindings;
|
|
@@ -116,6 +191,8 @@ export async function compile(filePath, config) {
|
|
|
116
191
|
parseResult.forBlocks = forBlocks;
|
|
117
192
|
parseResult.slots = slots;
|
|
118
193
|
parseResult.refBindings = refBindings;
|
|
194
|
+
parseResult.childComponents = childComponents;
|
|
195
|
+
parseResult.childImports = childImports;
|
|
119
196
|
// Recompute processedTemplate after all directive replacements (including ref removal)
|
|
120
197
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
121
198
|
|
package/lib/tree-walker.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { JSDOM } from 'jsdom';
|
|
16
16
|
import { BOOLEAN_ATTRIBUTES } from './types.js';
|
|
17
17
|
|
|
18
|
-
/** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, SlotBinding, SlotProp, RefBinding } from './types.js' */
|
|
18
|
+
/** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding } from './types.js' */
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Walk a DOM tree rooted at rootEl, discovering bindings and events.
|
|
@@ -24,7 +24,7 @@ import { BOOLEAN_ATTRIBUTES } from './types.js';
|
|
|
24
24
|
* @param {Set<string>} signalNames — Set of signal variable names
|
|
25
25
|
* @param {Set<string>} computedNames — Set of computed variable names
|
|
26
26
|
* @param {Set<string>} [propNames] — Set of prop names from defineProps
|
|
27
|
-
* @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], modelBindings: ModelBinding[], attrBindings: AttrBinding[], slots: SlotBinding[] }}
|
|
27
|
+
* @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], modelBindings: ModelBinding[], attrBindings: AttrBinding[], slots: SlotBinding[], childComponents: ChildComponentBinding[] }}
|
|
28
28
|
*/
|
|
29
29
|
export function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
30
30
|
/** @type {Binding[]} */
|
|
@@ -39,12 +39,15 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
39
39
|
const attrBindings = [];
|
|
40
40
|
/** @type {SlotBinding[]} */
|
|
41
41
|
const slots = [];
|
|
42
|
+
/** @type {ChildComponentBinding[]} */
|
|
43
|
+
const childComponents = [];
|
|
42
44
|
let bindIdx = 0;
|
|
43
45
|
let eventIdx = 0;
|
|
44
46
|
let showIdx = 0;
|
|
45
47
|
let modelIdx = 0;
|
|
46
48
|
let attrIdx = 0;
|
|
47
49
|
let slotIdx = 0;
|
|
50
|
+
let childIdx = 0;
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* Determine the binding type for a variable name.
|
|
@@ -103,6 +106,40 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
103
106
|
return; // Don't recurse into the replaced element
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
// Detect child custom elements (tag name contains a hyphen)
|
|
110
|
+
const tagLower = el.tagName.toLowerCase();
|
|
111
|
+
if (tagLower.includes('-') && tagLower !== rootEl.tagName?.toLowerCase()) {
|
|
112
|
+
/** @type {ChildPropBinding[]} */
|
|
113
|
+
const propBindings = [];
|
|
114
|
+
for (const attr of Array.from(el.attributes)) {
|
|
115
|
+
// Skip directive attributes (@event, :bind, show, model, etc.)
|
|
116
|
+
if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:')) continue;
|
|
117
|
+
if (['show', 'model', 'if', 'else-if', 'else', 'each', 'ref'].includes(attr.name)) continue;
|
|
118
|
+
|
|
119
|
+
// Check for {{interpolation}} in attribute value
|
|
120
|
+
const interpMatch = attr.value.match(/^\{\{([\w.]+)\}\}$/);
|
|
121
|
+
if (interpMatch) {
|
|
122
|
+
const expr = interpMatch[1];
|
|
123
|
+
propBindings.push({
|
|
124
|
+
attr: attr.name,
|
|
125
|
+
expr,
|
|
126
|
+
type: propNames.has(expr) ? 'prop' : signalNames.has(expr) ? 'signal' : computedNames.has(expr) ? 'computed' : 'method',
|
|
127
|
+
});
|
|
128
|
+
// Clear the interpolation from the attribute — the effect sets it at runtime
|
|
129
|
+
el.setAttribute(attr.name, '');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (propBindings.length > 0) {
|
|
134
|
+
childComponents.push({
|
|
135
|
+
tag: tagLower,
|
|
136
|
+
varName: `__child${childIdx++}`,
|
|
137
|
+
path: [...pathParts],
|
|
138
|
+
propBindings,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
// Check for @event attributes
|
|
107
144
|
const attrsToRemove = [];
|
|
108
145
|
for (const attr of Array.from(el.attributes)) {
|
|
@@ -265,7 +302,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
265
302
|
}
|
|
266
303
|
|
|
267
304
|
walk(rootEl, []);
|
|
268
|
-
return { bindings, events, showBindings, modelBindings, attrBindings, slots };
|
|
305
|
+
return { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents };
|
|
269
306
|
}
|
|
270
307
|
|
|
271
308
|
// ── Conditional chain processing (if / else-if / else) ──────────────
|
|
@@ -338,6 +375,7 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
|
|
|
338
375
|
stripFirstSegment(result.attrBindings);
|
|
339
376
|
stripFirstSegment(result.modelBindings);
|
|
340
377
|
stripFirstSegment(result.slots);
|
|
378
|
+
stripFirstSegment(result.childComponents);
|
|
341
379
|
|
|
342
380
|
return {
|
|
343
381
|
bindings: result.bindings,
|
|
@@ -346,6 +384,7 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
|
|
|
346
384
|
attrBindings: result.attrBindings,
|
|
347
385
|
modelBindings: result.modelBindings,
|
|
348
386
|
slots: result.slots,
|
|
387
|
+
childComponents: result.childComponents,
|
|
349
388
|
processedHtml,
|
|
350
389
|
};
|
|
351
390
|
}
|
package/lib/types.js
CHANGED
|
@@ -86,6 +86,8 @@
|
|
|
86
86
|
* @property {SlotBinding[]} slots — Slot bindings (empty array if no slots)
|
|
87
87
|
* @property {RefDeclaration[]} refs — templateRef declarations from script (empty array if none)
|
|
88
88
|
* @property {RefBinding[]} refBindings — ref attribute bindings from template (empty array if none)
|
|
89
|
+
* @property {ChildComponentBinding[]} childComponents — Child component bindings (empty array if none)
|
|
90
|
+
* @property {ChildComponentImport[]} childImports — Resolved child component imports (empty array if none)
|
|
89
91
|
*/
|
|
90
92
|
|
|
91
93
|
/**
|
|
@@ -179,6 +181,27 @@
|
|
|
179
181
|
* @property {SlotProp[]} slotProps — Array of :prop="expr" bindings on the slot
|
|
180
182
|
*/
|
|
181
183
|
|
|
184
|
+
/**
|
|
185
|
+
* @typedef {Object} ChildPropBinding
|
|
186
|
+
* @property {string} attr — Attribute name on the child element (e.g., 'label')
|
|
187
|
+
* @property {string} expr — Expression from {{expr}} (e.g., 'role')
|
|
188
|
+
* @property {string} type — Binding source type: 'signal' | 'computed' | 'prop' | 'constant' | 'method'
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @typedef {Object} ChildComponentBinding
|
|
193
|
+
* @property {string} tag — Child component tag name (e.g., 'wcc-badge')
|
|
194
|
+
* @property {string} varName — Internal ref name (e.g., '__child0')
|
|
195
|
+
* @property {string[]} path — DOM path from __root
|
|
196
|
+
* @property {ChildPropBinding[]} propBindings — Reactive attribute bindings
|
|
197
|
+
*/
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @typedef {Object} ChildComponentImport
|
|
201
|
+
* @property {string} tag — Child component tag name
|
|
202
|
+
* @property {string} importPath — Relative import path (e.g., './wcc-badge.js')
|
|
203
|
+
*/
|
|
204
|
+
|
|
182
205
|
/**
|
|
183
206
|
* Set of HTML attributes that use property assignment instead of setAttribute.
|
|
184
207
|
* @type {Set<string>}
|
package/package.json
CHANGED