@sprlab/wccompiler 0.7.3 → 0.8.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/adapters/angular.js +104 -0
- package/adapters/vue.js +132 -0
- package/integrations/angular.js +35 -10
- package/integrations/react.js +65 -1
- package/integrations/vue.js +14 -0
- package/lib/codegen.js +160 -29
- package/lib/compiler-browser.js +23 -4
- package/lib/compiler.js +94 -1
- package/lib/parser-extractors.js +105 -1
- package/lib/tree-walker.js +33 -5
- package/lib/types.js +9 -0
- package/package.json +5 -2
package/lib/parser-extractors.js
CHANGED
|
@@ -596,7 +596,7 @@ export function extractSignals(source) {
|
|
|
596
596
|
/**
|
|
597
597
|
* Known macro/reactive call patterns that should NOT be treated as constants.
|
|
598
598
|
*/
|
|
599
|
-
export const REACTIVE_CALLS = /\b(?:signal|computed|effect|watch|defineProps|defineEmits|defineComponent|templateRef|defineExpose|onMount|onDestroy)\s*[<(]/;
|
|
599
|
+
export const REACTIVE_CALLS = /\b(?:signal|computed|effect|watch|defineProps|defineEmits|defineModel|defineComponent|templateRef|defineExpose|onMount|onDestroy)\s*[<(]/;
|
|
600
600
|
|
|
601
601
|
/**
|
|
602
602
|
* Extract plain const/let/var declarations that are NOT reactive calls.
|
|
@@ -1028,6 +1028,110 @@ export function extractRefs(source) {
|
|
|
1028
1028
|
return refs;
|
|
1029
1029
|
}
|
|
1030
1030
|
|
|
1031
|
+
// ── defineModel extraction ───────────────────────────────────────────
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Extract defineModel() declarations from source.
|
|
1035
|
+
* Pattern: const/let/var varName = defineModel({ name: 'propName', default: value })
|
|
1036
|
+
* const/let/var varName = defineModel({ name: 'propName', required: true })
|
|
1037
|
+
*
|
|
1038
|
+
* @param {string} source
|
|
1039
|
+
* @returns {{ varName: string, name: string, default: string, required: boolean }[]}
|
|
1040
|
+
*/
|
|
1041
|
+
export function extractModels(source) {
|
|
1042
|
+
/** @type {{ varName: string, name: string, default: string, required: boolean }[]} */
|
|
1043
|
+
const models = [];
|
|
1044
|
+
const re = /(?:const|let|var)\s+(\w+)\s*=\s*defineModel\(\s*\{/g;
|
|
1045
|
+
let m;
|
|
1046
|
+
|
|
1047
|
+
while ((m = re.exec(source)) !== null) {
|
|
1048
|
+
const varName = m[1];
|
|
1049
|
+
const objStart = m.index + m[0].length - 1; // position of '{'
|
|
1050
|
+
|
|
1051
|
+
// Use depth counting to extract the full object literal
|
|
1052
|
+
let depth = 0;
|
|
1053
|
+
let i = objStart;
|
|
1054
|
+
/** @type {string | null} */
|
|
1055
|
+
let inString = null;
|
|
1056
|
+
|
|
1057
|
+
for (; i < source.length; i++) {
|
|
1058
|
+
const ch = source[i];
|
|
1059
|
+
|
|
1060
|
+
if (inString) {
|
|
1061
|
+
if (ch === '\\') { i++; continue; }
|
|
1062
|
+
if (ch === inString) inString = null;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1067
|
+
inString = ch;
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (ch === '{') depth++;
|
|
1072
|
+
if (ch === '}') {
|
|
1073
|
+
depth--;
|
|
1074
|
+
if (depth === 0) { i++; break; }
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const objLiteral = source.slice(objStart, i).trim();
|
|
1079
|
+
// Remove outer braces
|
|
1080
|
+
const inner = objLiteral.slice(1, -1).trim();
|
|
1081
|
+
|
|
1082
|
+
// Extract 'name' property
|
|
1083
|
+
const nameMatch = inner.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
1084
|
+
const propName = nameMatch ? nameMatch[1] : '';
|
|
1085
|
+
|
|
1086
|
+
// Extract 'default' property using depth counting
|
|
1087
|
+
let defaultValue = 'undefined';
|
|
1088
|
+
const defaultIdx = inner.search(/\bdefault\s*:\s*/);
|
|
1089
|
+
if (defaultIdx !== -1) {
|
|
1090
|
+
const afterDefault = inner.slice(defaultIdx);
|
|
1091
|
+
const colonMatch = afterDefault.match(/^default\s*:\s*/);
|
|
1092
|
+
if (colonMatch) {
|
|
1093
|
+
const valStart = defaultIdx + colonMatch[0].length;
|
|
1094
|
+
let valDepth = 0;
|
|
1095
|
+
let pos = valStart;
|
|
1096
|
+
/** @type {string | null} */
|
|
1097
|
+
let valInString = null;
|
|
1098
|
+
|
|
1099
|
+
for (; pos < inner.length; pos++) {
|
|
1100
|
+
const ch = inner[pos];
|
|
1101
|
+
|
|
1102
|
+
if (valInString) {
|
|
1103
|
+
if (ch === '\\') { pos++; continue; }
|
|
1104
|
+
if (ch === valInString) valInString = null;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1109
|
+
valInString = ch;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (ch === '(' || ch === '[' || ch === '{') valDepth++;
|
|
1114
|
+
if (ch === ')' || ch === ']' || ch === '}') valDepth--;
|
|
1115
|
+
|
|
1116
|
+
if (valDepth === 0 && ch === ',') {
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
defaultValue = inner.slice(valStart, pos).trim();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Extract 'required' property
|
|
1126
|
+
const requiredMatch = inner.match(/required\s*:\s*true/);
|
|
1127
|
+
const required = !!requiredMatch;
|
|
1128
|
+
|
|
1129
|
+
models.push({ varName, name: propName, default: defaultValue, required });
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return models;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1031
1135
|
// ── defineExpose extraction ─────────────────────────────────────────
|
|
1032
1136
|
|
|
1033
1137
|
/**
|
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, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding } from './types.js' */
|
|
18
|
+
/** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, ModelPropBinding, 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[], childComponents: ChildComponentBinding[] }}
|
|
27
|
+
* @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], modelBindings: ModelBinding[], modelPropBindings: ModelPropBinding[], attrBindings: AttrBinding[], slots: SlotBinding[], childComponents: ChildComponentBinding[] }}
|
|
28
28
|
*/
|
|
29
29
|
export function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
30
30
|
/** @type {Binding[]} */
|
|
@@ -35,6 +35,8 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
35
35
|
const showBindings = [];
|
|
36
36
|
/** @type {ModelBinding[]} */
|
|
37
37
|
const modelBindings = [];
|
|
38
|
+
/** @type {ModelPropBinding[]} */
|
|
39
|
+
const modelPropBindings = [];
|
|
38
40
|
/** @type {AttrBinding[]} */
|
|
39
41
|
const attrBindings = [];
|
|
40
42
|
/** @type {SlotBinding[]} */
|
|
@@ -45,6 +47,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
45
47
|
let eventIdx = 0;
|
|
46
48
|
let showIdx = 0;
|
|
47
49
|
let modelIdx = 0;
|
|
50
|
+
let modelPropIdx = 0;
|
|
48
51
|
let attrIdx = 0;
|
|
49
52
|
let slotIdx = 0;
|
|
50
53
|
let childIdx = 0;
|
|
@@ -121,7 +124,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
121
124
|
const propBindings = [];
|
|
122
125
|
for (const attr of Array.from(el.attributes)) {
|
|
123
126
|
// Skip directive attributes (@event, :bind, show, model, etc.)
|
|
124
|
-
if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:')) continue;
|
|
127
|
+
if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:') || attr.name.startsWith('model:')) continue;
|
|
125
128
|
if (['show', 'model', 'if', 'else-if', 'else', 'each', 'ref'].includes(attr.name)) continue;
|
|
126
129
|
|
|
127
130
|
// Check for {{interpolation}} in attribute value
|
|
@@ -245,6 +248,29 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
245
248
|
modelBindings.push({ varName, signal: signalName, prop, event, coerce, radioValue, path: [...pathParts] });
|
|
246
249
|
el.removeAttribute('model');
|
|
247
250
|
}
|
|
251
|
+
|
|
252
|
+
// Detect model:propName="signalName" attributes (for custom element binding)
|
|
253
|
+
const modelPropAttrsToRemove = [];
|
|
254
|
+
for (const attr of Array.from(el.attributes)) {
|
|
255
|
+
if (attr.name.startsWith('model:')) {
|
|
256
|
+
const propName = attr.name.slice(6); // after 'model:'
|
|
257
|
+
const signal = attr.value;
|
|
258
|
+
const tag = el.tagName.toLowerCase();
|
|
259
|
+
|
|
260
|
+
// Validate the element is a custom element (tag contains a hyphen)
|
|
261
|
+
if (!tag.includes('-')) {
|
|
262
|
+
const error = new Error(`model:propName is only valid on custom elements (tag must contain a hyphen)`);
|
|
263
|
+
/** @ts-expect-error — custom error code */
|
|
264
|
+
error.code = 'MODEL_PROP_INVALID_TARGET';
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const varName = `__modelProp${modelPropIdx++}`;
|
|
269
|
+
modelPropBindings.push({ varName, propName, signal, path: [...pathParts] });
|
|
270
|
+
modelPropAttrsToRemove.push(attr.name);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
modelPropAttrsToRemove.forEach((a) => el.removeAttribute(a));
|
|
248
274
|
}
|
|
249
275
|
|
|
250
276
|
// --- Text node with interpolations ---
|
|
@@ -317,7 +343,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
317
343
|
}
|
|
318
344
|
|
|
319
345
|
walk(rootEl, []);
|
|
320
|
-
return { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents };
|
|
346
|
+
return { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents };
|
|
321
347
|
}
|
|
322
348
|
|
|
323
349
|
// ── Conditional chain processing (if / else-if / else) ──────────────
|
|
@@ -363,7 +389,7 @@ function isChainPredecessor(el) {
|
|
|
363
389
|
* @param {Set<string>} signalNames
|
|
364
390
|
* @param {Set<string>} computedNames
|
|
365
391
|
* @param {Set<string>} propNames
|
|
366
|
-
* @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], attrBindings: AttrBinding[], modelBindings: ModelBinding[], slots: SlotBinding[], processedHtml: string }}
|
|
392
|
+
* @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], attrBindings: AttrBinding[], modelBindings: ModelBinding[], modelPropBindings: ModelPropBinding[], slots: SlotBinding[], processedHtml: string }}
|
|
367
393
|
*/
|
|
368
394
|
export function walkBranch(html, signalNames, computedNames, propNames) {
|
|
369
395
|
const { document } = parseHTML(`<div id="__branchRoot">${html}</div>`);
|
|
@@ -398,6 +424,7 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
|
|
|
398
424
|
stripFirstSegment(result.showBindings);
|
|
399
425
|
stripFirstSegment(result.attrBindings);
|
|
400
426
|
stripFirstSegment(result.modelBindings);
|
|
427
|
+
stripFirstSegment(result.modelPropBindings);
|
|
401
428
|
stripFirstSegment(result.slots);
|
|
402
429
|
stripFirstSegment(result.childComponents);
|
|
403
430
|
|
|
@@ -418,6 +445,7 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
|
|
|
418
445
|
showBindings: result.showBindings,
|
|
419
446
|
attrBindings: result.attrBindings,
|
|
420
447
|
modelBindings: result.modelBindings,
|
|
448
|
+
modelPropBindings: result.modelPropBindings,
|
|
421
449
|
slots: result.slots,
|
|
422
450
|
childComponents: result.childComponents,
|
|
423
451
|
forBlocks,
|
package/lib/types.js
CHANGED
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
* @property {LifecycleHook[]} onMountHooks — Mount lifecycle hooks (empty array if none)
|
|
94
94
|
* @property {LifecycleHook[]} onDestroyHooks — Destroy lifecycle hooks (empty array if none)
|
|
95
95
|
* @property {ModelBinding[]} modelBindings — Model bindings (empty array if none)
|
|
96
|
+
* @property {ModelPropBinding[]} modelPropBindings — Model prop bindings from model:propName directives (empty array if none)
|
|
96
97
|
* @property {AttrBinding[]} attrBindings — Attribute bindings (empty array if none)
|
|
97
98
|
* @property {SlotBinding[]} slots — Slot bindings (empty array if no slots)
|
|
98
99
|
* @property {RefDeclaration[]} refs — templateRef declarations from script (empty array if none)
|
|
@@ -168,6 +169,14 @@
|
|
|
168
169
|
* @property {string[]} path — DOM path from root to the element
|
|
169
170
|
*/
|
|
170
171
|
|
|
172
|
+
/**
|
|
173
|
+
* @typedef {Object} ModelPropBinding
|
|
174
|
+
* @property {string} varName — Internal name: '__modelProp0', '__modelProp1', ...
|
|
175
|
+
* @property {string} propName — The prop name after 'model:' (e.g., 'value')
|
|
176
|
+
* @property {string} signal — Parent signal name (e.g., 'searchText')
|
|
177
|
+
* @property {string[]} path — DOM path to the child element
|
|
178
|
+
*/
|
|
179
|
+
|
|
171
180
|
/**
|
|
172
181
|
* @typedef {Object} RefDeclaration
|
|
173
182
|
* @property {string} varName — Variable name from script (e.g., 'canvas')
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprlab/wccompiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./lib/compiler.js",
|
|
8
8
|
"./integrations/vue": "./integrations/vue.js",
|
|
9
9
|
"./integrations/react": "./integrations/react.js",
|
|
10
|
-
"./integrations/angular": "./integrations/angular.js"
|
|
10
|
+
"./integrations/angular": "./integrations/angular.js",
|
|
11
|
+
"./adapters/vue": "./adapters/vue.js",
|
|
12
|
+
"./adapters/angular": "./adapters/angular.js"
|
|
11
13
|
},
|
|
12
14
|
"bin": {
|
|
13
15
|
"wcc": "./bin/wcc.js"
|
|
@@ -17,6 +19,7 @@
|
|
|
17
19
|
"lib/*.js",
|
|
18
20
|
"!lib/*.test.js",
|
|
19
21
|
"integrations/",
|
|
22
|
+
"adapters/",
|
|
20
23
|
"types/",
|
|
21
24
|
"README.md"
|
|
22
25
|
],
|