@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.
@@ -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
  /**
@@ -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.7.3",
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
  ],