@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/codegen.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { reactiveRuntime } from './reactive-runtime.js';
|
|
17
17
|
import { scopeCSS } from './css-scoper.js';
|
|
18
|
+
import { camelToKebab } from './parser-extractors.js';
|
|
18
19
|
|
|
19
20
|
/** @import { ParseResult } from './types.js' */
|
|
20
21
|
|
|
@@ -75,7 +76,7 @@ function slotPropRef(source, signalNames, computedNames, propNames) {
|
|
|
75
76
|
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
76
77
|
* @returns {string}
|
|
77
78
|
*/
|
|
78
|
-
export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = []) {
|
|
79
|
+
export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = [], modelVarMap = new Map()) {
|
|
79
80
|
let result = expr;
|
|
80
81
|
|
|
81
82
|
// Transform emit calls: emitsObjectName( → this._emit(
|
|
@@ -111,6 +112,18 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
|
|
|
111
112
|
result = result.replace(bareRe, `this._s_${propName}()`);
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// Transform model signal reads: varName() → this._m_{propName}() (BEFORE regular signals)
|
|
116
|
+
for (const [varName, propNameVal] of modelVarMap) {
|
|
117
|
+
if (propsObjectName && varName === propsObjectName) continue;
|
|
118
|
+
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
119
|
+
// First: transform varName() calls → this._m_propName()
|
|
120
|
+
const callRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
|
|
121
|
+
result = result.replace(callRe, `this._m_${propNameVal}()`);
|
|
122
|
+
// Then: transform bare varName references (not followed by ( or .set()) → this._m_propName()
|
|
123
|
+
const bareRe = new RegExp(`\\b(${varName})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
124
|
+
result = result.replace(bareRe, `this._m_${propNameVal}()`);
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
// Transform computed names first (to avoid partial matches with signals)
|
|
115
128
|
for (const name of computedNames) {
|
|
116
129
|
// Skip propsObjectName and emitsObjectName
|
|
@@ -153,7 +166,9 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
|
|
|
153
166
|
*
|
|
154
167
|
* - `emitsObjectName(` → `this._emit(` (emit call)
|
|
155
168
|
* - `props.x` → `this._s_x()` (prop access)
|
|
169
|
+
* - `varName.set(value)` → `this._modelSet_{propName}(value)` (model signal write)
|
|
156
170
|
* - `x.set(value)` → `this._x(value)` (signal write via setter)
|
|
171
|
+
* - `varName()` → `this._m_{propName}()` (model signal read)
|
|
157
172
|
* - `x()` → `this._x()` (signal read)
|
|
158
173
|
* - Computed `x()` → `this._c_x()` (computed read)
|
|
159
174
|
*
|
|
@@ -164,9 +179,11 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
|
|
|
164
179
|
* @param {Set<string>} [propNames] — Set of prop names
|
|
165
180
|
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
166
181
|
* @param {string[]} [refVarNames] — Ref variable names from templateRef declarations
|
|
182
|
+
* @param {string[]} [constantNames] — Constant variable names
|
|
183
|
+
* @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
|
|
167
184
|
* @returns {string}
|
|
168
185
|
*/
|
|
169
|
-
export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = []) {
|
|
186
|
+
export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = [], modelVarMap = new Map()) {
|
|
170
187
|
let result = body;
|
|
171
188
|
|
|
172
189
|
// 0a. Transform emit calls: emitsObjectName( → this._emit(
|
|
@@ -192,6 +209,15 @@ export function transformMethodBody(body, signalNames, computedNames, propsObjec
|
|
|
192
209
|
result = result.replace(refRe, `this._${name}.value`);
|
|
193
210
|
}
|
|
194
211
|
|
|
212
|
+
// 0d. Transform model signal writes: varName.set(expr) → this._modelSet_{propName}(expr)
|
|
213
|
+
// Must run BEFORE regular signal .set() transforms
|
|
214
|
+
for (const [varName, propNameVal] of modelVarMap) {
|
|
215
|
+
if (propsObjectName && varName === propsObjectName) continue;
|
|
216
|
+
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
217
|
+
const setRe = new RegExp(`\\b${varName}\\.set\\(`, 'g');
|
|
218
|
+
result = result.replace(setRe, `this._modelSet_${propNameVal}(`);
|
|
219
|
+
}
|
|
220
|
+
|
|
195
221
|
// 1. Transform signal writes: x.set(value) → this._x(value)
|
|
196
222
|
for (const name of signalNames) {
|
|
197
223
|
if (propsObjectName && name === propsObjectName) continue;
|
|
@@ -200,6 +226,15 @@ export function transformMethodBody(body, signalNames, computedNames, propsObjec
|
|
|
200
226
|
result = result.replace(setRe, `this._${name}(`);
|
|
201
227
|
}
|
|
202
228
|
|
|
229
|
+
// 1b. Transform model signal reads: varName() → this._m_{propName}()
|
|
230
|
+
// Must run BEFORE regular signal read transforms
|
|
231
|
+
for (const [varName, propNameVal] of modelVarMap) {
|
|
232
|
+
if (propsObjectName && varName === propsObjectName) continue;
|
|
233
|
+
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
234
|
+
const readRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
|
|
235
|
+
result = result.replace(readRe, `this._m_${propNameVal}()`);
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
// 2. Transform computed reads: x() → this._c_x()
|
|
204
239
|
for (const name of computedNames) {
|
|
205
240
|
if (propsObjectName && name === propsObjectName) continue;
|
|
@@ -327,22 +362,23 @@ export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames,
|
|
|
327
362
|
* @param {Set<string>} propNames
|
|
328
363
|
* @param {string|null} emitsObjectName
|
|
329
364
|
* @param {string[]} constantNames
|
|
365
|
+
* @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
|
|
330
366
|
* @returns {string}
|
|
331
367
|
*/
|
|
332
|
-
export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames) {
|
|
368
|
+
export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap = new Map()) {
|
|
333
369
|
if (handler.includes('=>')) {
|
|
334
370
|
// Arrow function expression: (e) => removeItem(item)
|
|
335
371
|
const arrowIdx = handler.indexOf('=>');
|
|
336
372
|
const params = handler.slice(0, arrowIdx).trim();
|
|
337
373
|
let body = handler.slice(arrowIdx + 2).trim();
|
|
338
|
-
body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames);
|
|
374
|
+
body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames, modelVarMap);
|
|
339
375
|
return `${params} => { ${body}; }`;
|
|
340
376
|
} else if (handler.includes('(')) {
|
|
341
377
|
// Function call expression: removeItem(item)
|
|
342
378
|
const parenIdx = handler.indexOf('(');
|
|
343
379
|
const fnName = handler.slice(0, parenIdx).trim();
|
|
344
380
|
const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
|
|
345
|
-
const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames) : '';
|
|
381
|
+
const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap) : '';
|
|
346
382
|
return `(e) => { this._${fnName}(${transformedArgs}); }`;
|
|
347
383
|
} else {
|
|
348
384
|
// Simple method name
|
|
@@ -829,6 +865,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
829
865
|
onMountHooks = [],
|
|
830
866
|
onDestroyHooks = [],
|
|
831
867
|
modelBindings = [],
|
|
868
|
+
modelPropBindings = [],
|
|
832
869
|
attrBindings = [],
|
|
833
870
|
slots = [],
|
|
834
871
|
constantVars = [],
|
|
@@ -838,6 +875,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
838
875
|
childComponents = [],
|
|
839
876
|
childImports = [],
|
|
840
877
|
exposeNames = [],
|
|
878
|
+
modelDefs = [],
|
|
841
879
|
} = parseResult;
|
|
842
880
|
|
|
843
881
|
const signalNames = signals.map(s => s.name);
|
|
@@ -847,6 +885,12 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
847
885
|
const refVarNames = refs.map(r => r.varName);
|
|
848
886
|
const propNames = new Set(propDefs.map(p => p.name));
|
|
849
887
|
|
|
888
|
+
// Build model var name → prop name map for transform functions
|
|
889
|
+
const modelVarMap = new Map();
|
|
890
|
+
for (const md of modelDefs) {
|
|
891
|
+
modelVarMap.set(md.varName, md.name);
|
|
892
|
+
}
|
|
893
|
+
|
|
850
894
|
const lines = [];
|
|
851
895
|
|
|
852
896
|
// ── 0. Source comment ──
|
|
@@ -859,7 +903,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
859
903
|
// Tree-shake: only import what this component actually uses
|
|
860
904
|
const usedRuntime = new Set(['__signal']); // always need __signal
|
|
861
905
|
if (computeds.length > 0) usedRuntime.add('__computed');
|
|
862
|
-
if (effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || slots.some(s => s.slotProps.length > 0)) usedRuntime.add('__effect');
|
|
906
|
+
if (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)) usedRuntime.add('__effect');
|
|
863
907
|
if (watchers.length > 0) usedRuntime.add('__untrack');
|
|
864
908
|
const imports = [...usedRuntime].join(', ');
|
|
865
909
|
lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
|
|
@@ -897,10 +941,13 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
897
941
|
// ── 4. HTMLElement class ──
|
|
898
942
|
lines.push(`class ${className} extends HTMLElement {`);
|
|
899
943
|
|
|
900
|
-
// Static observedAttributes (if props exist)
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
944
|
+
// Static observedAttributes (if props or model props exist)
|
|
945
|
+
const modelAttrNames = modelDefs.map(md => camelToKebab(md.name));
|
|
946
|
+
if (propDefs.length > 0 || modelDefs.length > 0) {
|
|
947
|
+
const propAttrNames = propDefs.map(p => `'${p.attrName}'`);
|
|
948
|
+
const modelAttrEntries = modelAttrNames.map(a => `'${a}'`);
|
|
949
|
+
const allAttrNames = [...propAttrNames, ...modelAttrEntries].join(', ');
|
|
950
|
+
lines.push(` static get observedAttributes() { return [${allAttrNames}]; }`);
|
|
904
951
|
lines.push('');
|
|
905
952
|
}
|
|
906
953
|
|
|
@@ -918,6 +965,11 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
918
965
|
lines.push(` this._${s.name} = __signal(${s.value});`);
|
|
919
966
|
}
|
|
920
967
|
|
|
968
|
+
// Model signal initialization
|
|
969
|
+
for (const md of modelDefs) {
|
|
970
|
+
lines.push(` this._m_${md.name} = __signal(${md.default});`);
|
|
971
|
+
}
|
|
972
|
+
|
|
921
973
|
// Constant initialization
|
|
922
974
|
for (const c of constantVars) {
|
|
923
975
|
lines.push(` this._const_${c.name} = ${c.value};`);
|
|
@@ -925,7 +977,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
925
977
|
|
|
926
978
|
// Computed initialization
|
|
927
979
|
for (const c of computeds) {
|
|
928
|
-
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
980
|
+
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
929
981
|
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
930
982
|
}
|
|
931
983
|
|
|
@@ -1005,6 +1057,11 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1005
1057
|
lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
|
|
1006
1058
|
}
|
|
1007
1059
|
|
|
1060
|
+
// Assign DOM refs for model:propName bindings
|
|
1061
|
+
for (const mpb of modelPropBindings) {
|
|
1062
|
+
lines.push(` this.${mpb.varName} = ${pathExpr(mpb.path, '__root')};`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1008
1065
|
// Assign DOM refs for slot placeholders
|
|
1009
1066
|
for (const s of slots) {
|
|
1010
1067
|
lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
|
|
@@ -1107,7 +1164,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1107
1164
|
ref = `this._s_${propName}()`;
|
|
1108
1165
|
} else {
|
|
1109
1166
|
// Use transformExpr for complex expressions (e.g. items().length, ternary)
|
|
1110
|
-
ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1167
|
+
ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1111
1168
|
}
|
|
1112
1169
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1113
1170
|
lines.push(` this.${b.varName}.textContent = ${ref} ?? '';`);
|
|
@@ -1158,7 +1215,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1158
1215
|
|
|
1159
1216
|
// User effects
|
|
1160
1217
|
for (const eff of effects) {
|
|
1161
|
-
const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
1218
|
+
const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1162
1219
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1163
1220
|
// Indent each line of the body
|
|
1164
1221
|
const bodyLines = body.split('\n');
|
|
@@ -1171,7 +1228,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1171
1228
|
// Watcher effects
|
|
1172
1229
|
for (let idx = 0; idx < watchers.length; idx++) {
|
|
1173
1230
|
const w = watchers[idx];
|
|
1174
|
-
const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
1231
|
+
const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1175
1232
|
|
|
1176
1233
|
if (w.kind === 'signal') {
|
|
1177
1234
|
// Determine the signal reference for the watch target
|
|
@@ -1200,7 +1257,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1200
1257
|
lines.push(' }));');
|
|
1201
1258
|
} else {
|
|
1202
1259
|
// kind === 'getter' — transform the getter expression and use it directly
|
|
1203
|
-
const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
1260
|
+
const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1204
1261
|
const prevName = `__prev_watch${idx}`;
|
|
1205
1262
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1206
1263
|
lines.push(` const ${w.newParam} = ${getterExpr};`);
|
|
@@ -1222,13 +1279,13 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1222
1279
|
|
|
1223
1280
|
// Event listeners (with AbortController signal for cleanup)
|
|
1224
1281
|
for (const e of events) {
|
|
1225
|
-
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
1282
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
|
|
1226
1283
|
lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
|
|
1227
1284
|
}
|
|
1228
1285
|
|
|
1229
1286
|
// Show effects — one __effect per ShowBinding
|
|
1230
1287
|
for (const sb of showBindings) {
|
|
1231
|
-
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1288
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1232
1289
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1233
1290
|
lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
|
|
1234
1291
|
lines.push(' }));');
|
|
@@ -1268,9 +1325,35 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1268
1325
|
}
|
|
1269
1326
|
}
|
|
1270
1327
|
|
|
1328
|
+
// model:propName effects and listeners — bidirectional WCC-to-WCC binding
|
|
1329
|
+
for (const mpb of modelPropBindings) {
|
|
1330
|
+
// Determine the signal read/write expressions
|
|
1331
|
+
// If the signal is a model var, use this._m_propName(); otherwise use this._signalName()
|
|
1332
|
+
const isModelVar = modelVarMap.has(mpb.signal);
|
|
1333
|
+
const readExpr = isModelVar
|
|
1334
|
+
? `this._m_${modelVarMap.get(mpb.signal)}()`
|
|
1335
|
+
: `this._${mpb.signal}()`;
|
|
1336
|
+
const writeExpr = isModelVar
|
|
1337
|
+
? `this._m_${modelVarMap.get(mpb.signal)}`
|
|
1338
|
+
: `this._${mpb.signal}`;
|
|
1339
|
+
|
|
1340
|
+
// Reactive parent → child sync: set child's attribute from parent signal
|
|
1341
|
+
const attrName = camelToKebab(mpb.propName);
|
|
1342
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1343
|
+
lines.push(` this.${mpb.varName}.setAttribute('${attrName}', ${readExpr} ?? '');`);
|
|
1344
|
+
lines.push(' }));');
|
|
1345
|
+
|
|
1346
|
+
// Child → parent sync: listen for wcc:model on child, update parent signal
|
|
1347
|
+
lines.push(` this.${mpb.varName}.addEventListener('wcc:model', (e) => {`);
|
|
1348
|
+
lines.push(` if (e.detail.prop === '${mpb.propName}') {`);
|
|
1349
|
+
lines.push(` ${writeExpr}(e.detail.value);`);
|
|
1350
|
+
lines.push(' }');
|
|
1351
|
+
lines.push(' }, { signal: this.__ac.signal });');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1271
1354
|
// Attr binding effects — one __effect per AttrBinding
|
|
1272
1355
|
for (const ab of attrBindings) {
|
|
1273
|
-
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1356
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1274
1357
|
if (ab.kind === 'attr') {
|
|
1275
1358
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1276
1359
|
lines.push(` const __v = ${expr};`);
|
|
@@ -1322,10 +1405,10 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1322
1405
|
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
1323
1406
|
const branch = ifBlock.branches[i];
|
|
1324
1407
|
if (branch.type === 'if') {
|
|
1325
|
-
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1408
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1326
1409
|
lines.push(` if (${expr}) { __branch = ${i}; }`);
|
|
1327
1410
|
} else if (branch.type === 'else-if') {
|
|
1328
|
-
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1411
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1329
1412
|
lines.push(` else if (${expr}) { __branch = ${i}; }`);
|
|
1330
1413
|
} else {
|
|
1331
1414
|
// else
|
|
@@ -1435,7 +1518,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1435
1518
|
|
|
1436
1519
|
// Lifecycle: onMount hooks (at the very end of connectedCallback)
|
|
1437
1520
|
for (const hook of onMountHooks) {
|
|
1438
|
-
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
1521
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1439
1522
|
if (hook.async) {
|
|
1440
1523
|
lines.push(' ;(async () => {');
|
|
1441
1524
|
const bodyLines = body.split('\n');
|
|
@@ -1463,7 +1546,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1463
1546
|
lines.push(' this.__disposers.forEach(d => d());');
|
|
1464
1547
|
if (onDestroyHooks.length > 0) {
|
|
1465
1548
|
for (const hook of onDestroyHooks) {
|
|
1466
|
-
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
1549
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1467
1550
|
if (hook.async) {
|
|
1468
1551
|
lines.push(' ;(async () => {');
|
|
1469
1552
|
const bodyLines = body.split('\n');
|
|
@@ -1484,8 +1567,8 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1484
1567
|
lines.push(' }');
|
|
1485
1568
|
lines.push('');
|
|
1486
1569
|
|
|
1487
|
-
// attributeChangedCallback (if props exist)
|
|
1488
|
-
if (propDefs.length > 0) {
|
|
1570
|
+
// attributeChangedCallback (if props or model props exist)
|
|
1571
|
+
if (propDefs.length > 0 || modelDefs.length > 0) {
|
|
1489
1572
|
lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
|
|
1490
1573
|
for (const p of propDefs) {
|
|
1491
1574
|
const defaultVal = p.default;
|
|
@@ -1507,6 +1590,31 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1507
1590
|
|
|
1508
1591
|
lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
|
|
1509
1592
|
}
|
|
1593
|
+
|
|
1594
|
+
// Model props — update signal directly (NO event emission)
|
|
1595
|
+
for (let i = 0; i < modelDefs.length; i++) {
|
|
1596
|
+
const md = modelDefs[i];
|
|
1597
|
+
const attrName = modelAttrNames[i];
|
|
1598
|
+
const defaultVal = md.default;
|
|
1599
|
+
let updateExpr;
|
|
1600
|
+
|
|
1601
|
+
if (defaultVal === 'true' || defaultVal === 'false') {
|
|
1602
|
+
// Boolean coercion: attribute presence = true
|
|
1603
|
+
updateExpr = `this._m_${md.name}(newVal != null)`;
|
|
1604
|
+
} else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
|
|
1605
|
+
// Number coercion
|
|
1606
|
+
updateExpr = `this._m_${md.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
|
|
1607
|
+
} else if (defaultVal === 'undefined') {
|
|
1608
|
+
// Undefined default — pass through
|
|
1609
|
+
updateExpr = `this._m_${md.name}(newVal)`;
|
|
1610
|
+
} else {
|
|
1611
|
+
// String default — use nullish coalescing
|
|
1612
|
+
updateExpr = `this._m_${md.name}(newVal ?? ${defaultVal})`;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
lines.push(` if (name === '${attrName}') ${updateExpr};`);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1510
1618
|
lines.push(' }');
|
|
1511
1619
|
lines.push('');
|
|
1512
1620
|
|
|
@@ -1516,6 +1624,15 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1516
1624
|
lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
|
|
1517
1625
|
lines.push('');
|
|
1518
1626
|
}
|
|
1627
|
+
|
|
1628
|
+
// Public getters and setters for model props
|
|
1629
|
+
for (let i = 0; i < modelDefs.length; i++) {
|
|
1630
|
+
const md = modelDefs[i];
|
|
1631
|
+
const attrName = modelAttrNames[i];
|
|
1632
|
+
lines.push(` get ${md.name}() { return this._m_${md.name}(); }`);
|
|
1633
|
+
lines.push(` set ${md.name}(val) { this._m_${md.name}(val); this.setAttribute('${attrName}', String(val)); }`);
|
|
1634
|
+
lines.push('');
|
|
1635
|
+
}
|
|
1519
1636
|
}
|
|
1520
1637
|
|
|
1521
1638
|
// _emit method (if emits declared)
|
|
@@ -1526,9 +1643,23 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1526
1643
|
lines.push('');
|
|
1527
1644
|
}
|
|
1528
1645
|
|
|
1646
|
+
// _modelSet methods (one per defineModel prop — emits wcc:model on internal write)
|
|
1647
|
+
for (const md of modelDefs) {
|
|
1648
|
+
lines.push(` _modelSet_${md.name}(newVal) {`);
|
|
1649
|
+
lines.push(` const oldVal = this._m_${md.name}();`);
|
|
1650
|
+
lines.push(` this._m_${md.name}(newVal);`);
|
|
1651
|
+
lines.push(` this.dispatchEvent(new CustomEvent('wcc:model', {`);
|
|
1652
|
+
lines.push(` detail: { prop: '${md.name}', value: newVal, oldValue: oldVal },`);
|
|
1653
|
+
lines.push(` bubbles: true,`);
|
|
1654
|
+
lines.push(` composed: true`);
|
|
1655
|
+
lines.push(` }));`);
|
|
1656
|
+
lines.push(' }');
|
|
1657
|
+
lines.push('');
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1529
1660
|
// User methods (prefixed with _)
|
|
1530
1661
|
for (const m of methods) {
|
|
1531
|
-
const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
1662
|
+
const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1532
1663
|
lines.push(` _${m.name}(${m.params}) {`);
|
|
1533
1664
|
const bodyLines = body.split('\n');
|
|
1534
1665
|
for (const line of bodyLines) {
|
|
@@ -1604,7 +1735,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1604
1735
|
const propName = b.name.slice(propsObjectName.length + 1);
|
|
1605
1736
|
ref = `this._s_${propName}()`;
|
|
1606
1737
|
} else {
|
|
1607
|
-
ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1738
|
+
ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1608
1739
|
}
|
|
1609
1740
|
lines.push(` __effect(() => { ${b.varName}.textContent = ${ref} ?? ''; });`);
|
|
1610
1741
|
}
|
|
@@ -1612,21 +1743,21 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1612
1743
|
|
|
1613
1744
|
// Events: generate addEventListener
|
|
1614
1745
|
for (const e of branch.events) {
|
|
1615
|
-
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
1746
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
|
|
1616
1747
|
lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
|
|
1617
1748
|
lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
1618
1749
|
}
|
|
1619
1750
|
|
|
1620
1751
|
// Show bindings: generate effects
|
|
1621
1752
|
for (const sb of branch.showBindings) {
|
|
1622
|
-
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1753
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1623
1754
|
lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
|
|
1624
1755
|
lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
1625
1756
|
}
|
|
1626
1757
|
|
|
1627
1758
|
// Attr bindings: generate effects
|
|
1628
1759
|
for (const ab of branch.attrBindings) {
|
|
1629
|
-
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1760
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1630
1761
|
lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
|
|
1631
1762
|
lines.push(` __effect(() => {`);
|
|
1632
1763
|
lines.push(` const __val = ${expr};`);
|
package/lib/compiler-browser.js
CHANGED
|
@@ -69,10 +69,11 @@ function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
|
69
69
|
const events = [];
|
|
70
70
|
const showBindings = [];
|
|
71
71
|
const modelBindings = [];
|
|
72
|
+
const modelPropBindings = [];
|
|
72
73
|
const attrBindings = [];
|
|
73
74
|
const slots = [];
|
|
74
75
|
const childComponents = [];
|
|
75
|
-
let bindIdx = 0, eventIdx = 0, showIdx = 0, modelIdx = 0, attrIdx = 0, slotIdx = 0, childIdx = 0;
|
|
76
|
+
let bindIdx = 0, eventIdx = 0, showIdx = 0, modelIdx = 0, modelPropIdx = 0, attrIdx = 0, slotIdx = 0, childIdx = 0;
|
|
76
77
|
|
|
77
78
|
function bindingType(name) {
|
|
78
79
|
if (propNames.has(name)) return 'prop';
|
|
@@ -105,7 +106,7 @@ function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
|
105
106
|
if (tagLower.includes('-') && tagLower !== rootEl.tagName?.toLowerCase()) {
|
|
106
107
|
const propBindings = [];
|
|
107
108
|
for (const attr of Array.from(el.attributes)) {
|
|
108
|
-
if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:')) continue;
|
|
109
|
+
if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:') || attr.name.startsWith('model:')) continue;
|
|
109
110
|
if (['show', 'model', 'if', 'else-if', 'else', 'each', 'ref'].includes(attr.name)) continue;
|
|
110
111
|
const interpMatch = attr.value.match(/^\{\{([\w.]+)\}\}$/);
|
|
111
112
|
if (interpMatch) {
|
|
@@ -158,6 +159,24 @@ function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
|
158
159
|
modelBindings.push({ varName: `__model${modelIdx++}`, signal: signalName, prop, event, coerce, radioValue, path: [...pathParts] });
|
|
159
160
|
el.removeAttribute('model');
|
|
160
161
|
}
|
|
162
|
+
|
|
163
|
+
// Detect model:propName="signalName" attributes (for custom element binding)
|
|
164
|
+
const modelPropAttrsToRemove = [];
|
|
165
|
+
for (const attr of Array.from(el.attributes)) {
|
|
166
|
+
if (attr.name.startsWith('model:')) {
|
|
167
|
+
const propName = attr.name.slice(6);
|
|
168
|
+
const signal = attr.value;
|
|
169
|
+
const tag = el.tagName.toLowerCase();
|
|
170
|
+
if (!tag.includes('-')) {
|
|
171
|
+
const error = new Error(`model:propName is only valid on custom elements (tag must contain a hyphen)`);
|
|
172
|
+
error.code = 'MODEL_PROP_INVALID_TARGET';
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
modelPropBindings.push({ varName: `__modelProp${modelPropIdx++}`, propName, signal, path: [...pathParts] });
|
|
176
|
+
modelPropAttrsToRemove.push(attr.name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
modelPropAttrsToRemove.forEach(a => el.removeAttribute(a));
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
if (node.nodeType === 3 && /\{\{[\w.]+\}\}/.test(node.textContent)) {
|
|
@@ -201,7 +220,7 @@ function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
|
201
220
|
}
|
|
202
221
|
|
|
203
222
|
walk(rootEl, []);
|
|
204
|
-
return { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents };
|
|
223
|
+
return { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents };
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
function recomputeAnchorPath(rootEl, targetNode) {
|
|
@@ -486,7 +505,7 @@ export async function compileFromStrings({ script, template, style = '', tag, la
|
|
|
486
505
|
for (const ib of ifBlocks) ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
487
506
|
|
|
488
507
|
// 11. Walk tree
|
|
489
|
-
const { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNameSet, computedNameSet, propNameSet);
|
|
508
|
+
const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNameSet, computedNameSet, propNameSet);
|
|
490
509
|
|
|
491
510
|
// 12. Detect refs
|
|
492
511
|
const refBindings = detectRefs(rootEl);
|
package/lib/compiler.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
extractRefs,
|
|
34
34
|
extractConstants,
|
|
35
35
|
extractExpose,
|
|
36
|
+
extractModels,
|
|
36
37
|
validatePropsAssignment,
|
|
37
38
|
validateDuplicateProps,
|
|
38
39
|
validatePropsConflicts,
|
|
@@ -158,6 +159,7 @@ async function compileSFC(filePath, config) {
|
|
|
158
159
|
const refs = extractRefs(sourceForExtraction);
|
|
159
160
|
const constantVars = extractConstants(sourceForExtraction);
|
|
160
161
|
const exposeNames = extractExpose(source);
|
|
162
|
+
const modelDefs = extractModels(sourceForExtraction);
|
|
161
163
|
|
|
162
164
|
// 9. Extract props (array form — after type strip, if generic didn't find any)
|
|
163
165
|
const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
|
|
@@ -197,6 +199,58 @@ async function compileSFC(filePath, config) {
|
|
|
197
199
|
validateEmitsConflicts(emitsObjectName, signalNameSet, computedNameSet, constantNameSet, propNameSet, propsObjectName, filePath);
|
|
198
200
|
validateUndeclaredEmits(source, emitsObjectName, emitNames, filePath);
|
|
199
201
|
|
|
202
|
+
// 14b. Validate defineModel declarations
|
|
203
|
+
// MODEL_NO_ASSIGNMENT: detect bare defineModel() calls not assigned to a variable
|
|
204
|
+
const bareModelRe = /\bdefineModel\s*\(/g;
|
|
205
|
+
const assignedModelRe = /(?:const|let|var)\s+\w+\s*=\s*defineModel\s*\(/g;
|
|
206
|
+
const bareModelCount = (sourceForExtraction.match(bareModelRe) || []).length;
|
|
207
|
+
const assignedModelCount = (sourceForExtraction.match(assignedModelRe) || []).length;
|
|
208
|
+
if (bareModelCount > assignedModelCount) {
|
|
209
|
+
const error = new Error(`defineModel() must be assigned to a variable`);
|
|
210
|
+
/** @ts-expect-error — custom error code */
|
|
211
|
+
error.code = 'MODEL_NO_ASSIGNMENT';
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MODEL_MISSING_NAME: check each extracted model has a name property
|
|
216
|
+
for (const md of modelDefs) {
|
|
217
|
+
if (!md.name) {
|
|
218
|
+
const error = new Error(`defineModel() requires a 'name' property in the options object`);
|
|
219
|
+
/** @ts-expect-error — custom error code */
|
|
220
|
+
error.code = 'MODEL_MISSING_NAME';
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// MODEL_NAME_CONFLICT: check model prop names against signals, computeds, constants, and props
|
|
226
|
+
for (const md of modelDefs) {
|
|
227
|
+
if (!md.name) continue;
|
|
228
|
+
if (signalNameSet.has(md.name)) {
|
|
229
|
+
const error = new Error(`defineModel prop '${md.name}' conflicts with existing signal '${md.name}'`);
|
|
230
|
+
/** @ts-expect-error — custom error code */
|
|
231
|
+
error.code = 'MODEL_NAME_CONFLICT';
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
if (computedNameSet.has(md.name)) {
|
|
235
|
+
const error = new Error(`defineModel prop '${md.name}' conflicts with existing computed '${md.name}'`);
|
|
236
|
+
/** @ts-expect-error — custom error code */
|
|
237
|
+
error.code = 'MODEL_NAME_CONFLICT';
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
if (constantNameSet.has(md.name)) {
|
|
241
|
+
const error = new Error(`defineModel prop '${md.name}' conflicts with existing constant '${md.name}'`);
|
|
242
|
+
/** @ts-expect-error — custom error code */
|
|
243
|
+
error.code = 'MODEL_NAME_CONFLICT';
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
if (propNameSet.has(md.name)) {
|
|
247
|
+
const error = new Error(`defineModel prop '${md.name}' conflicts with existing prop '${md.name}'`);
|
|
248
|
+
/** @ts-expect-error — custom error code */
|
|
249
|
+
error.code = 'MODEL_NAME_CONFLICT';
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
200
254
|
// 15. Build initial ParseResult
|
|
201
255
|
/** @type {import('./types.js').ParseResult} */
|
|
202
256
|
const parseResult = {
|
|
@@ -223,6 +277,7 @@ async function compileSFC(filePath, config) {
|
|
|
223
277
|
onMountHooks,
|
|
224
278
|
onDestroyHooks,
|
|
225
279
|
modelBindings: [],
|
|
280
|
+
modelPropBindings: [],
|
|
226
281
|
attrBindings: [],
|
|
227
282
|
slots: [],
|
|
228
283
|
refs,
|
|
@@ -230,6 +285,7 @@ async function compileSFC(filePath, config) {
|
|
|
230
285
|
childComponents: [],
|
|
231
286
|
childImports: [],
|
|
232
287
|
exposeNames,
|
|
288
|
+
modelDefs,
|
|
233
289
|
};
|
|
234
290
|
|
|
235
291
|
// 16. Process template through linkedom → tree-walker → codegen
|
|
@@ -237,6 +293,10 @@ async function compileSFC(filePath, config) {
|
|
|
237
293
|
const rootEl = document.getElementById('__root');
|
|
238
294
|
|
|
239
295
|
const signalNames = new Set(signals.map(s => s.name));
|
|
296
|
+
// Add model var names so they are recognized as writable signals in tree-walker
|
|
297
|
+
for (const md of modelDefs) {
|
|
298
|
+
signalNames.add(md.varName);
|
|
299
|
+
}
|
|
240
300
|
const computedNames = new Set(computeds.map(c => c.name));
|
|
241
301
|
const propNamesSet = new Set(propDefs.map(p => p.name));
|
|
242
302
|
|
|
@@ -252,7 +312,7 @@ async function compileSFC(filePath, config) {
|
|
|
252
312
|
ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
253
313
|
}
|
|
254
314
|
|
|
255
|
-
const { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
|
|
315
|
+
const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
|
|
256
316
|
|
|
257
317
|
const refBindings = detectRefs(rootEl);
|
|
258
318
|
|
|
@@ -300,6 +360,38 @@ async function compileSFC(filePath, config) {
|
|
|
300
360
|
}
|
|
301
361
|
}
|
|
302
362
|
|
|
363
|
+
// 17c. Validate model:propName bindings
|
|
364
|
+
for (const mpb of modelPropBindings) {
|
|
365
|
+
const name = mpb.signal;
|
|
366
|
+
// Check if the referenced variable exists at all
|
|
367
|
+
const isKnown = signalNames.has(name) || computedNames.has(name) || propNamesSet.has(name) || constantNamesForModel.has(name);
|
|
368
|
+
if (!isKnown) {
|
|
369
|
+
const error = new Error(`model:propName references undeclared variable '${name}'`);
|
|
370
|
+
/** @ts-expect-error — custom error code */
|
|
371
|
+
error.code = 'MODEL_PROP_UNKNOWN_VAR';
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
// Check if the referenced variable is read-only
|
|
375
|
+
if (propNamesSet.has(name)) {
|
|
376
|
+
const error = new Error(`model:propName cannot bind to prop '${name}' (read-only)`);
|
|
377
|
+
/** @ts-expect-error — custom error code */
|
|
378
|
+
error.code = 'MODEL_PROP_READONLY';
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
if (computedNames.has(name)) {
|
|
382
|
+
const error = new Error(`model:propName cannot bind to computed '${name}' (read-only)`);
|
|
383
|
+
/** @ts-expect-error — custom error code */
|
|
384
|
+
error.code = 'MODEL_PROP_READONLY';
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
if (constantNamesForModel.has(name)) {
|
|
388
|
+
const error = new Error(`model:propName cannot bind to constant '${name}' (read-only)`);
|
|
389
|
+
/** @ts-expect-error — custom error code */
|
|
390
|
+
error.code = 'MODEL_PROP_READONLY';
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
303
395
|
// 18. Resolve child component imports (from main template + if branches + each blocks)
|
|
304
396
|
/** @type {import('./types.js').ChildComponentImport[]} */
|
|
305
397
|
const childImports = [];
|
|
@@ -339,6 +431,7 @@ async function compileSFC(filePath, config) {
|
|
|
339
431
|
parseResult.events = events;
|
|
340
432
|
parseResult.showBindings = showBindings;
|
|
341
433
|
parseResult.modelBindings = modelBindings;
|
|
434
|
+
parseResult.modelPropBindings = modelPropBindings;
|
|
342
435
|
parseResult.attrBindings = attrBindings;
|
|
343
436
|
parseResult.ifBlocks = ifBlocks;
|
|
344
437
|
parseResult.forBlocks = forBlocks;
|