@mulanjs/mulanjs 1.0.1-dev.20260302074037 → 1.0.1-dev.20260309125238
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/dist/compiler/ast-parser.js +11 -2
- package/dist/compiler/dom-compiler.js +187 -25
- package/dist/compiler/script-compiler.js +91 -1
- package/dist/compiler/sfc-parser.js +8 -0
- package/dist/types/ast-parser.d.ts +6 -0
- package/dist/types/compiler/ast-parser.d.ts +6 -0
- package/dist/types/compiler/script-compiler.d.ts +1 -0
- package/dist/types/compiler/sfc-parser.d.ts +1 -1
- package/dist/types/script-compiler.d.ts +1 -0
- package/dist/types/sfc-parser.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/index.js +9 -9
- package/src/compiler/ast-parser.ts +11 -2
- package/src/compiler/dom-compiler.ts +181 -29
- package/src/compiler/script-compiler.ts +97 -0
- package/src/compiler/sfc-parser.ts +8 -1
|
@@ -214,14 +214,14 @@ function markStatic(node) {
|
|
|
214
214
|
const element = node;
|
|
215
215
|
let isStatic = true;
|
|
216
216
|
// 1. Directives make it dynamic
|
|
217
|
-
if (element.directives.vIf || element.directives.vFor) {
|
|
217
|
+
if (element.directives.vIf || element.directives.vElseIf || element.directives.vElse || element.directives.vFor || element.directives.vShow) {
|
|
218
218
|
isStatic = false;
|
|
219
219
|
}
|
|
220
220
|
// 2. Dynamic properties or event listeners make it dynamic
|
|
221
221
|
if (isStatic) {
|
|
222
222
|
for (const key in element.props) {
|
|
223
223
|
const val = element.props[key];
|
|
224
|
-
if (key.startsWith('@') || key.startsWith('v-on:') || key.startsWith('on') || key.startsWith(':') || key.startsWith('.')) {
|
|
224
|
+
if (key.startsWith('@') || key.startsWith('v-on:') || key.startsWith('on') || key.startsWith(':') || key.startsWith('.') || key.startsWith('mu-') || key === 'v-model') {
|
|
225
225
|
isStatic = false;
|
|
226
226
|
break;
|
|
227
227
|
}
|
|
@@ -298,6 +298,15 @@ function parseTag(content) {
|
|
|
298
298
|
if (key === 'v-if' || key === 'mu-if') {
|
|
299
299
|
directives.vIf = value;
|
|
300
300
|
}
|
|
301
|
+
else if (key === 'v-else-if' || key === 'mu-else-if') {
|
|
302
|
+
directives.vElseIf = value;
|
|
303
|
+
}
|
|
304
|
+
else if (key === 'v-else' || key === 'mu-else') {
|
|
305
|
+
directives.vElse = true;
|
|
306
|
+
}
|
|
307
|
+
else if (key === 'v-show' || key === 'mu-show') {
|
|
308
|
+
directives.vShow = value;
|
|
309
|
+
}
|
|
301
310
|
else if (key === 'v-for' || key === 'mu-for') {
|
|
302
311
|
const parts = value.split(' in ');
|
|
303
312
|
if (parts.length < 2) {
|
|
@@ -25,8 +25,13 @@ function compileToDOM(descriptor, scriptResult, scopedId) {
|
|
|
25
25
|
const generator = descriptor.filename ? new source_map_1.SourceMapGenerator({
|
|
26
26
|
file: descriptor.filename + '.template.js'
|
|
27
27
|
}) : null;
|
|
28
|
+
// Annotate every node with its parent's children array and its sibling index,
|
|
29
|
+
// so the mu-if handler can scan forward for mu-else-if / mu-else siblings.
|
|
30
|
+
annotateChildren(ast);
|
|
28
31
|
// Generate code for all top-level children and append them to the fragment
|
|
29
|
-
ast.children.forEach(child => {
|
|
32
|
+
ast.children.forEach((child, _idx) => {
|
|
33
|
+
if (child.__skip__)
|
|
34
|
+
return; // consumed by a preceding mu-if chain
|
|
30
35
|
const rootId = generateDOMInstruction(child, codeChunks, getUid, getHoistId, hoists, uidRef, scriptResult.bindings || [], [], generator, descriptor.filename);
|
|
31
36
|
if (rootId) {
|
|
32
37
|
codeChunks.push(`if (_frag) _frag.appendChild(${rootId});`);
|
|
@@ -66,6 +71,23 @@ function compileToDOM(descriptor, scriptResult, scopedId) {
|
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
exports.compileToDOM = compileToDOM;
|
|
74
|
+
// --- Sibling Annotation (for mu-else-if / mu-else chaining) ---
|
|
75
|
+
function annotateChildren(node) {
|
|
76
|
+
let children;
|
|
77
|
+
if (node.type === 'Root') {
|
|
78
|
+
children = node.children;
|
|
79
|
+
}
|
|
80
|
+
else if (node.type === 'Element') {
|
|
81
|
+
children = node.children;
|
|
82
|
+
}
|
|
83
|
+
if (children) {
|
|
84
|
+
children.forEach((child, idx) => {
|
|
85
|
+
child.__parent_children__ = children;
|
|
86
|
+
child.__sibling_index__ = idx;
|
|
87
|
+
annotateChildren(child);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
69
91
|
// --- Code Generator (AST -> Instructions) ---
|
|
70
92
|
function renderStaticHTML(node) {
|
|
71
93
|
if (node.type === 'Text') {
|
|
@@ -171,30 +193,119 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
171
193
|
const condition = `_s(${processBindings(element.directives.vIf, bindings, localScope)})`;
|
|
172
194
|
// 1. Create the anchor Comment node where the block belongs
|
|
173
195
|
chunks.push(`const ${id} = document.createComment("mu-if:${element.directives.vIf}");`);
|
|
174
|
-
// 2.
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
// 2. Helper to build a render-block function from any element
|
|
197
|
+
const buildBlockFn = (el) => {
|
|
198
|
+
const blockId = `_if_${uidRef.current++}`;
|
|
199
|
+
const blockChunks = [];
|
|
200
|
+
blockChunks.push(`const ${blockId}_frag = this._recoveryMode ? null : document.createDocumentFragment();`);
|
|
201
|
+
blockChunks.push(`const _block_effects = [];`);
|
|
202
|
+
const localBindMacro = `(fn, targetNode) => { const stop = this._bindEffect(fn, targetNode); _block_effects.push(stop); return stop; }`;
|
|
203
|
+
const origLen = blockChunks.length;
|
|
204
|
+
const elNoDirective = { ...el, directives: { ...el.directives, vIf: undefined, vElseIf: undefined, vElse: undefined } };
|
|
205
|
+
const clonedChildId = generateDOMInstruction(elNoDirective, blockChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename);
|
|
206
|
+
if (clonedChildId) {
|
|
207
|
+
blockChunks.push(`if (${blockId}_frag && ${clonedChildId} && !this._recoveryMode) ${blockId}_frag.appendChild(${clonedChildId});`);
|
|
208
|
+
}
|
|
209
|
+
for (let bi = origLen; bi < blockChunks.length; bi++) {
|
|
210
|
+
blockChunks[bi] = blockChunks[bi].replace(/this\._bindEffect/g, `(${localBindMacro})`);
|
|
211
|
+
}
|
|
212
|
+
blockChunks.push(`return { fragment: ${blockId}_frag, effects: _block_effects };`);
|
|
213
|
+
return `() => {\n ${blockChunks.join('\n ')}\n }`;
|
|
214
|
+
};
|
|
215
|
+
// 3. Collect else-if / else sibling branches from the AST parent's children
|
|
216
|
+
// We look at the parent stack's current top children after this node.
|
|
217
|
+
// Since we can't access the parent here directly, we use a forward-scan trick:
|
|
218
|
+
// Siblings are already on `chunks` as separate calls from the parent forEach.
|
|
219
|
+
// Instead we emit a chained condition as a ternary inside the reconciler.
|
|
220
|
+
// Build the if-condition string for the Mulan reconciler
|
|
221
|
+
// Format: [ [cond1, renderFn1], [cond2, renderFn2], ..., [null, elseFn] ]
|
|
222
|
+
// IMPORTANT: we compute the primary block function ONCE and reuse it to
|
|
223
|
+
// avoid calling buildBlockFn(element) twice (which caused infinite recursion
|
|
224
|
+
// because element still has vIf set on the second call).
|
|
225
|
+
const ifBlockFn = buildBlockFn(element);
|
|
226
|
+
const branches = [
|
|
227
|
+
`[() => !!(${condition}), ${ifBlockFn}]`
|
|
228
|
+
];
|
|
229
|
+
// Scan forward in the parent's children for else-if / else
|
|
230
|
+
// We mark them so the parent forEach loop skips them
|
|
231
|
+
const parentChildren = element.__parent_children__;
|
|
232
|
+
let sibIdx = element.__sibling_index__;
|
|
233
|
+
if (parentChildren !== undefined && sibIdx !== undefined) {
|
|
234
|
+
sibIdx++;
|
|
235
|
+
while (sibIdx < parentChildren.length) {
|
|
236
|
+
const sib = parentChildren[sibIdx];
|
|
237
|
+
if (sib.type === 'Text') {
|
|
238
|
+
const text = sib;
|
|
239
|
+
if (!text.content.trim()) {
|
|
240
|
+
// Skip whitespace between tags
|
|
241
|
+
sib.__skip__ = true;
|
|
242
|
+
sibIdx++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
break; // Real text breaks the chain
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (sib.type !== 'Element')
|
|
250
|
+
break;
|
|
251
|
+
const sibEl = sib;
|
|
252
|
+
if (sibEl.directives.vElseIf) {
|
|
253
|
+
const sibCond = `_s(${processBindings(sibEl.directives.vElseIf, bindings, localScope)})`;
|
|
254
|
+
branches.push(`[() => !!(${sibCond}), ${buildBlockFn(sibEl)}]`);
|
|
255
|
+
sibEl.__skip__ = true;
|
|
256
|
+
sibIdx++;
|
|
257
|
+
}
|
|
258
|
+
else if (sibEl.directives.vElse) {
|
|
259
|
+
branches.push(`[null, ${buildBlockFn(sibEl)}]`);
|
|
260
|
+
sibEl.__skip__ = true;
|
|
261
|
+
sibIdx++;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
190
268
|
}
|
|
191
|
-
|
|
192
|
-
const renderBlockFn = `() => {\n ${blockChunks.join('\n ')}\n }`;
|
|
193
|
-
// 3. Register the macro-effect that calls the Reconciler
|
|
269
|
+
// 4. Emit reconciler call — simple if, or chained if / else-if / else
|
|
194
270
|
const ifIdHash = `if_${Math.random().toString(36).substr(2, 6)}`;
|
|
195
|
-
|
|
271
|
+
if (branches.length === 1) {
|
|
272
|
+
// Simple if (no else): reuse ifBlockFn computed above
|
|
273
|
+
chunks.push(`this._bindEffect(() => { this._reconcileIf("${ifIdHash}", ${id}, !!(${condition}), ${ifBlockFn}); }, ${id});`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// Chain: evaluate each condition in order
|
|
277
|
+
const branchArray = `[${branches.join(', ')}]`;
|
|
278
|
+
chunks.push(`this._bindEffect(() => {
|
|
279
|
+
const _branches = ${branchArray};
|
|
280
|
+
let _matched = false;
|
|
281
|
+
for (const [_cond, _renderFn] of _branches) {
|
|
282
|
+
if (_cond === null || _cond()) {
|
|
283
|
+
this._reconcileIf("${ifIdHash}", ${id}, true, _renderFn);
|
|
284
|
+
_matched = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!_matched) { this._reconcileIf("${ifIdHash}", ${id}, false, () => ({ fragment: null, effects: [] })); }
|
|
289
|
+
}, ${id});`);
|
|
290
|
+
}
|
|
196
291
|
return id;
|
|
197
292
|
}
|
|
293
|
+
// --- mu-show: CSS visibility toggle (element stays in DOM) ---
|
|
294
|
+
if (element.directives.vShow) {
|
|
295
|
+
const showId = getUid();
|
|
296
|
+
const showCondition = processBindings(element.directives.vShow, bindings, localScope);
|
|
297
|
+
// Create the element without the show directive
|
|
298
|
+
const elNoShow = { ...element, directives: { ...element.directives, vShow: undefined } };
|
|
299
|
+
// Generate the element normally (it renders into DOM)
|
|
300
|
+
const innerChunks = [];
|
|
301
|
+
const innerEl = generateDOMInstruction(elNoShow, innerChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename);
|
|
302
|
+
chunks.push(...innerChunks);
|
|
303
|
+
// Bind a reactive effect that toggles display
|
|
304
|
+
if (innerEl) {
|
|
305
|
+
chunks.push(`this._bindEffect(() => { if (${innerEl}) ${innerEl}.style.display = _s(${showCondition}) ? '' : 'none'; }, ${innerEl});`);
|
|
306
|
+
}
|
|
307
|
+
return innerEl;
|
|
308
|
+
}
|
|
198
309
|
// Handle mu-for (Phase 3 - The Reconciler)
|
|
199
310
|
if (element.directives.vFor) {
|
|
200
311
|
const { item, list } = element.directives.vFor;
|
|
@@ -248,7 +359,12 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
248
359
|
}
|
|
249
360
|
}
|
|
250
361
|
else if (key === 'id') {
|
|
251
|
-
|
|
362
|
+
if (value.includes('${')) {
|
|
363
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.id = _va("id", \`${value}\`); }, ${id});`);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
chunks.push(`if (${id}) ${id}.id = _va("id", ${JSON.stringify(value)});`);
|
|
367
|
+
}
|
|
252
368
|
}
|
|
253
369
|
else if (key === 'data-mu-id') {
|
|
254
370
|
// Ignore internal string compiler metadata
|
|
@@ -283,6 +399,8 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
283
399
|
// Recursively generate children
|
|
284
400
|
const childScope = [...localScope];
|
|
285
401
|
element.children.forEach(child => {
|
|
402
|
+
if (child.__skip__)
|
|
403
|
+
return; // consumed by mu-if else-if/else chain
|
|
286
404
|
const childId = generateDOMInstruction(child, chunks, getUid, getHoistId, hoists, uidRef, bindings, childScope, generator, filename);
|
|
287
405
|
if (childId) {
|
|
288
406
|
chunks.push(`if (${id} && ${childId} && !this._recoveryMode) ${id}.appendChild(${childId});`);
|
|
@@ -341,8 +459,30 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
341
459
|
const childScope = [...localScope];
|
|
342
460
|
if (element.directives.vFor)
|
|
343
461
|
childScope.push(element.directives.vFor.item);
|
|
462
|
+
// Also add loop variable when inside mu-for with destructured tuple e.g. (item, i)
|
|
463
|
+
if (element.directives.vFor) {
|
|
464
|
+
const itemStr = element.directives.vFor.item;
|
|
465
|
+
const tupleMatch = itemStr.match(/^\(([^)]+)\)$/);
|
|
466
|
+
if (tupleMatch) {
|
|
467
|
+
tupleMatch[1].split(',').forEach(v => {
|
|
468
|
+
const trimmed = v.trim();
|
|
469
|
+
if (trimmed && !childScope.includes(trimmed))
|
|
470
|
+
childScope.push(trimmed);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
344
474
|
for (const key in element.props) {
|
|
345
|
-
|
|
475
|
+
// --- mu-model: two-way binding ---
|
|
476
|
+
if (key === 'mu-model' || key === 'v-model') {
|
|
477
|
+
const stateExpr = element.props[key];
|
|
478
|
+
const bound = processBindings(stateExpr, scriptResult.bindings, localScope);
|
|
479
|
+
// Read: bind value property reactively
|
|
480
|
+
domBindings.push({ type: 'prop', name: 'value', expr: `_s(${bound})` });
|
|
481
|
+
// Write: listen to 'input' event and update state
|
|
482
|
+
domBindings.push({ type: 'event', name: 'input', expr: `($event) => { ${bound} = $event.target.value; }` });
|
|
483
|
+
delete element.props[key];
|
|
484
|
+
}
|
|
485
|
+
else if (key.startsWith('.')) {
|
|
346
486
|
const propName = key.slice(1);
|
|
347
487
|
const rawValue = element.props[key];
|
|
348
488
|
let expr = "''";
|
|
@@ -355,13 +495,35 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
355
495
|
domBindings.push({ type: 'prop', name: propName, expr });
|
|
356
496
|
delete element.props[key];
|
|
357
497
|
}
|
|
358
|
-
else if (key.startsWith('@') ||
|
|
359
|
-
|
|
498
|
+
else if (key.startsWith('@') ||
|
|
499
|
+
key.startsWith('v-on:') ||
|
|
500
|
+
// mu-* event aliases
|
|
501
|
+
key === 'mu-click' || key === 'mu-input' || key === 'mu-change' || key === 'mu-submit' ||
|
|
502
|
+
(key.startsWith('on') && key.length > 2)) {
|
|
503
|
+
let eventName;
|
|
504
|
+
if (key.startsWith('@')) {
|
|
505
|
+
eventName = key.slice(1);
|
|
506
|
+
}
|
|
507
|
+
else if (key.startsWith('v-on:')) {
|
|
508
|
+
eventName = key.slice(5);
|
|
509
|
+
}
|
|
510
|
+
else if (key.startsWith('mu-')) {
|
|
511
|
+
// mu-click → click, mu-submit → submit, etc.
|
|
512
|
+
eventName = key.slice(3);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
eventName = key.slice(2);
|
|
516
|
+
}
|
|
360
517
|
let bound = processBindings(element.props[key], scriptResult.bindings, localScope);
|
|
361
518
|
const expr = (bound.includes('(') || bound.includes('=') || bound.includes('++')) ? `($event) => { ${bound.replace(/`/g, '\\`')} }` : bound;
|
|
362
519
|
domBindings.push({ type: 'event', name: eventName, expr });
|
|
363
520
|
delete element.props[key];
|
|
364
521
|
}
|
|
522
|
+
else {
|
|
523
|
+
if (typeof element.props[key] === 'string' && element.props[key].includes('${')) {
|
|
524
|
+
element.props[key] = element.props[key].replace(/\$\{(.*?)\}/g, (_, expr) => '${' + processBindings(expr, scriptResult.bindings, localScope) + '}');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
365
527
|
}
|
|
366
528
|
element.children.forEach(child => transform(child, scriptResult, scopedId, childScope, filename));
|
|
367
529
|
}
|
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.compileScript = void 0;
|
|
26
|
+
exports.transformMuSyntax = exports.compileScript = void 0;
|
|
27
27
|
const ts = __importStar(require("typescript"));
|
|
28
28
|
const source_map_1 = require("source-map");
|
|
29
29
|
const path = __importStar(require("path"));
|
|
@@ -76,8 +76,13 @@ async function compileScript(descriptor, options) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
const isSetup = !!script.attrs.setup;
|
|
79
|
+
const isMu = !!script.attrs.mu;
|
|
79
80
|
const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx' || externalPath.endsWith('.ts') || externalPath.endsWith('.tsx');
|
|
80
81
|
if (isSetup) {
|
|
82
|
+
// Transform <mu> natural syntax first if this is a <mu> block
|
|
83
|
+
if (isMu) {
|
|
84
|
+
content = transformMuSyntax(content);
|
|
85
|
+
}
|
|
81
86
|
return await compileSetupScript(script, isTs, descriptor, content, filename, isExternal, externalPath);
|
|
82
87
|
}
|
|
83
88
|
else if (isTs) {
|
|
@@ -507,3 +512,88 @@ function transformNaturalReactivity(code, isTs) {
|
|
|
507
512
|
const printer = ts.createPrinter();
|
|
508
513
|
return printer.printFile(result.transformed[0]);
|
|
509
514
|
}
|
|
515
|
+
// --- Mu Syntax Transformer ---
|
|
516
|
+
// Converts clean <mu> block natural-language syntax into valid TypeScript
|
|
517
|
+
// that the standard setup-script pipeline can consume.
|
|
518
|
+
//
|
|
519
|
+
// Supported transformations:
|
|
520
|
+
// name = value → let name = value
|
|
521
|
+
// fn() → → function fn() {
|
|
522
|
+
// fn(a, b) → → function fn(a, b) {
|
|
523
|
+
// fn() → expr → function fn() { return expr } (single-line)
|
|
524
|
+
// (indented body after →) → { body... }
|
|
525
|
+
function transformMuSyntax(code) {
|
|
526
|
+
var _a, _b, _c, _d;
|
|
527
|
+
const lines = code.split(/\r?\n/);
|
|
528
|
+
const out = [];
|
|
529
|
+
let i = 0;
|
|
530
|
+
while (i < lines.length) {
|
|
531
|
+
const raw = lines[i];
|
|
532
|
+
const trimmed = raw.trim();
|
|
533
|
+
// Skip blank lines
|
|
534
|
+
if (!trimmed) {
|
|
535
|
+
out.push(raw);
|
|
536
|
+
i++;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
// ─── Already valid TS/JS — preserve as-is ──────────────────────────────
|
|
540
|
+
// Lines that already start with a keyword don't need transformation
|
|
541
|
+
if (/^(const|let|var|function|class|import|export|return|if|else|for|while|switch|throw|try|catch|finally|\/{2}|\*)/.test(trimmed)) {
|
|
542
|
+
out.push(raw);
|
|
543
|
+
i++;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
// ─── Arrow-function definition: name(args) → ───────────────────────────
|
|
547
|
+
// Two forms:
|
|
548
|
+
// (A) fn() → — block body follows on next lines
|
|
549
|
+
// (B) fn() → expression — single-line body
|
|
550
|
+
const arrowMatch = trimmed.match(/^([a-zA-Z_$][\w$]*)\s*\(([^)]*)\)\s*→\s*(.*)$/);
|
|
551
|
+
if (arrowMatch) {
|
|
552
|
+
const [, fnName, params, bodyExpr] = arrowMatch;
|
|
553
|
+
const indent = (_b = (_a = raw.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
|
|
554
|
+
if (bodyExpr.trim()) {
|
|
555
|
+
// Single-line: fn() → expr → function fn(...) { return expr }
|
|
556
|
+
out.push(`${indent}function ${fnName}(${params.trim()}) { return ${bodyExpr.trim()} }`);
|
|
557
|
+
i++;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Block form: collect indented body lines
|
|
561
|
+
out.push(`${indent}function ${fnName}(${params.trim()}) {`);
|
|
562
|
+
i++;
|
|
563
|
+
const baseIndent = raw.search(/\S/);
|
|
564
|
+
while (i < lines.length) {
|
|
565
|
+
const bodyRaw = lines[i];
|
|
566
|
+
const bodyTrimmed = bodyRaw.trim();
|
|
567
|
+
if (!bodyTrimmed) {
|
|
568
|
+
out.push('');
|
|
569
|
+
i++;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const bodyIndent = bodyRaw.search(/\S/);
|
|
573
|
+
// If the next line is at the same or lower indent → end of function body
|
|
574
|
+
if (bodyIndent <= baseIndent)
|
|
575
|
+
break;
|
|
576
|
+
out.push(bodyRaw);
|
|
577
|
+
i++;
|
|
578
|
+
}
|
|
579
|
+
out.push(`${indent}}`);
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
// ─── Bare assignment: name = value ─────────────────────────────────────
|
|
584
|
+
// Only matches simple top-level assignments, not ===, !==, +=, etc.
|
|
585
|
+
const assignMatch = trimmed.match(/^([a-zA-Z_$][\w$]*)\s*=(?!=)\s*(.+)$/);
|
|
586
|
+
if (assignMatch) {
|
|
587
|
+
const indent = (_d = (_c = raw.match(/^(\s*)/)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : '';
|
|
588
|
+
const [, varName, valueExpr] = assignMatch;
|
|
589
|
+
out.push(`${indent}let ${varName} = ${valueExpr}`);
|
|
590
|
+
i++;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
// Fallback — emit as-is
|
|
594
|
+
out.push(raw);
|
|
595
|
+
i++;
|
|
596
|
+
}
|
|
597
|
+
return out.join('\n');
|
|
598
|
+
}
|
|
599
|
+
exports.transformMuSyntax = transformMuSyntax;
|
|
@@ -75,6 +75,14 @@ function parseMUJS(source, filename) {
|
|
|
75
75
|
if (tagName === 'template' || tagName === 'mu-template') {
|
|
76
76
|
descriptor.template = block;
|
|
77
77
|
}
|
|
78
|
+
else if (tagName === 'mu') {
|
|
79
|
+
// <mu> is the MulanJS branded script block — equivalent to <script setup lang="ts">
|
|
80
|
+
// Mark attrs so downstream compilers treat it as setup + TypeScript + mu-native syntax
|
|
81
|
+
block.attrs = { ...block.attrs, setup: 'true', lang: 'ts', mu: 'true' };
|
|
82
|
+
block.type = 'script';
|
|
83
|
+
descriptor.scripts.push(block);
|
|
84
|
+
descriptor.script = block;
|
|
85
|
+
}
|
|
78
86
|
else if (tagName === 'script') {
|
|
79
87
|
descriptor.scripts.push(block);
|
|
80
88
|
if (attrs.setup || !descriptor.script) {
|
|
@@ -19,6 +19,9 @@ export interface ElementNode extends BaseNode {
|
|
|
19
19
|
list: string;
|
|
20
20
|
};
|
|
21
21
|
vIf?: string;
|
|
22
|
+
vElseIf?: string;
|
|
23
|
+
vElse?: boolean;
|
|
24
|
+
vShow?: string;
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
27
|
export interface TextNode extends BaseNode {
|
|
@@ -48,5 +51,8 @@ export declare function parseTag(content: string): {
|
|
|
48
51
|
list: string;
|
|
49
52
|
} | undefined;
|
|
50
53
|
vIf?: string | undefined;
|
|
54
|
+
vElseIf?: string | undefined;
|
|
55
|
+
vElse?: boolean | undefined;
|
|
56
|
+
vShow?: string | undefined;
|
|
51
57
|
};
|
|
52
58
|
};
|
|
@@ -19,6 +19,9 @@ export interface ElementNode extends BaseNode {
|
|
|
19
19
|
list: string;
|
|
20
20
|
};
|
|
21
21
|
vIf?: string;
|
|
22
|
+
vElseIf?: string;
|
|
23
|
+
vElse?: boolean;
|
|
24
|
+
vShow?: string;
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
27
|
export interface TextNode extends BaseNode {
|
|
@@ -48,5 +51,8 @@ export declare function parseTag(content: string): {
|
|
|
48
51
|
list: string;
|
|
49
52
|
} | undefined;
|
|
50
53
|
vIf?: string | undefined;
|
|
54
|
+
vElseIf?: string | undefined;
|
|
55
|
+
vElse?: boolean | undefined;
|
|
56
|
+
vShow?: string | undefined;
|
|
51
57
|
};
|
|
52
58
|
};
|
|
@@ -9,3 +9,4 @@ export interface CompilerOptions {
|
|
|
9
9
|
readFileSync?: (file: string) => string;
|
|
10
10
|
}
|
|
11
11
|
export declare function compileScript(descriptor: SFCDescriptor, options?: CompilerOptions): Promise<ScriptCompileResult>;
|
|
12
|
+
export declare function transformMuSyntax(code: string): string;
|
|
@@ -9,3 +9,4 @@ export interface CompilerOptions {
|
|
|
9
9
|
readFileSync?: (file: string) => string;
|
|
10
10
|
}
|
|
11
11
|
export declare function compileScript(descriptor: SFCDescriptor, options?: CompilerOptions): Promise<ScriptCompileResult>;
|
|
12
|
+
export declare function transformMuSyntax(code: string): string;
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -284,8 +284,8 @@ module.exports = {
|
|
|
284
284
|
// --- COMPONENT GENERATION (Unified Templates) ---
|
|
285
285
|
|
|
286
286
|
const headerTemplate = (isMuJS) ?
|
|
287
|
-
`<
|
|
288
|
-
</
|
|
287
|
+
`<mu>
|
|
288
|
+
</mu>
|
|
289
289
|
<mu-template>
|
|
290
290
|
<header>
|
|
291
291
|
<div class="logo-container">
|
|
@@ -346,10 +346,10 @@ export class Header extends Component {
|
|
|
346
346
|
|
|
347
347
|
// Home Page with Counter
|
|
348
348
|
const homeTemplate = (isMuJS) ?
|
|
349
|
-
`<
|
|
349
|
+
`<mu>
|
|
350
350
|
import { muState } from '@mulanjs/mulanjs';
|
|
351
|
-
|
|
352
|
-
</
|
|
351
|
+
state = muState({ count: 0 });
|
|
352
|
+
</mu>
|
|
353
353
|
<mu-template>
|
|
354
354
|
<div class="page">
|
|
355
355
|
<div class="hero">
|
|
@@ -358,7 +358,7 @@ const state = muState({ count: 0 });
|
|
|
358
358
|
|
|
359
359
|
<div class="counter-box">
|
|
360
360
|
<p>Interactive Counter:</p>
|
|
361
|
-
<button class="mu-btn"
|
|
361
|
+
<button class="mu-btn" mu-click="state.count++">
|
|
362
362
|
Count is: \${state.count}
|
|
363
363
|
</button>
|
|
364
364
|
</div>
|
|
@@ -574,10 +574,10 @@ program
|
|
|
574
574
|
const compName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
575
575
|
console.log(`⚡ Generating Component: ${compName}...`);
|
|
576
576
|
const ext = 'mujs';
|
|
577
|
-
const content = `<
|
|
577
|
+
const content = `<mu>
|
|
578
578
|
import { muState } from '@mulanjs/mulanjs';
|
|
579
|
-
|
|
580
|
-
</
|
|
579
|
+
state = muState({ count: 0 });
|
|
580
|
+
</mu>
|
|
581
581
|
<mu-template>
|
|
582
582
|
<div class="mu-comp">
|
|
583
583
|
<h2>${compName} Component</h2>
|
|
@@ -22,6 +22,9 @@ export interface ElementNode extends BaseNode {
|
|
|
22
22
|
directives: {
|
|
23
23
|
vFor?: { item: string; list: string };
|
|
24
24
|
vIf?: string;
|
|
25
|
+
vElseIf?: string;
|
|
26
|
+
vElse?: boolean;
|
|
27
|
+
vShow?: string;
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -258,7 +261,7 @@ export function markStatic(node: Node): boolean {
|
|
|
258
261
|
let isStatic = true;
|
|
259
262
|
|
|
260
263
|
// 1. Directives make it dynamic
|
|
261
|
-
if (element.directives.vIf || element.directives.vFor) {
|
|
264
|
+
if (element.directives.vIf || element.directives.vElseIf || element.directives.vElse || element.directives.vFor || element.directives.vShow) {
|
|
262
265
|
isStatic = false;
|
|
263
266
|
}
|
|
264
267
|
|
|
@@ -266,7 +269,7 @@ export function markStatic(node: Node): boolean {
|
|
|
266
269
|
if (isStatic) {
|
|
267
270
|
for (const key in element.props) {
|
|
268
271
|
const val = element.props[key];
|
|
269
|
-
if (key.startsWith('@') || key.startsWith('v-on:') || key.startsWith('on') || key.startsWith(':') || key.startsWith('.')) {
|
|
272
|
+
if (key.startsWith('@') || key.startsWith('v-on:') || key.startsWith('on') || key.startsWith(':') || key.startsWith('.') || key.startsWith('mu-') || key === 'v-model') {
|
|
270
273
|
isStatic = false;
|
|
271
274
|
break;
|
|
272
275
|
}
|
|
@@ -350,6 +353,12 @@ export function parseTag(content: string) {
|
|
|
350
353
|
|
|
351
354
|
if (key === 'v-if' || key === 'mu-if') {
|
|
352
355
|
directives.vIf = value;
|
|
356
|
+
} else if (key === 'v-else-if' || key === 'mu-else-if') {
|
|
357
|
+
directives.vElseIf = value;
|
|
358
|
+
} else if (key === 'v-else' || key === 'mu-else') {
|
|
359
|
+
directives.vElse = true;
|
|
360
|
+
} else if (key === 'v-show' || key === 'mu-show') {
|
|
361
|
+
directives.vShow = value;
|
|
353
362
|
} else if (key === 'v-for' || key === 'mu-for') {
|
|
354
363
|
const parts = value.split(' in ');
|
|
355
364
|
if (parts.length < 2) {
|
|
@@ -39,8 +39,13 @@ export function compileToDOM(descriptor: SFCDescriptor, scriptResult: ScriptComp
|
|
|
39
39
|
file: descriptor.filename + '.template.js'
|
|
40
40
|
}) : null;
|
|
41
41
|
|
|
42
|
+
// Annotate every node with its parent's children array and its sibling index,
|
|
43
|
+
// so the mu-if handler can scan forward for mu-else-if / mu-else siblings.
|
|
44
|
+
annotateChildren(ast);
|
|
45
|
+
|
|
42
46
|
// Generate code for all top-level children and append them to the fragment
|
|
43
|
-
ast.children.forEach(child => {
|
|
47
|
+
ast.children.forEach((child, _idx) => {
|
|
48
|
+
if ((child as any).__skip__) return; // consumed by a preceding mu-if chain
|
|
44
49
|
const rootId = generateDOMInstruction(child, codeChunks, getUid, getHoistId, hoists, uidRef, scriptResult.bindings || [], [], generator, descriptor.filename);
|
|
45
50
|
if (rootId) {
|
|
46
51
|
codeChunks.push(`if (_frag) _frag.appendChild(${rootId});`);
|
|
@@ -83,6 +88,24 @@ export function compileToDOM(descriptor: SFCDescriptor, scriptResult: ScriptComp
|
|
|
83
88
|
};
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
// --- Sibling Annotation (for mu-else-if / mu-else chaining) ---
|
|
92
|
+
function annotateChildren(node: Node): void {
|
|
93
|
+
let children: Node[] | undefined;
|
|
94
|
+
if ((node as any).type === 'Root') {
|
|
95
|
+
children = (node as any).children;
|
|
96
|
+
} else if (node.type === 'Element') {
|
|
97
|
+
children = (node as any).children;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (children) {
|
|
101
|
+
children.forEach((child, idx) => {
|
|
102
|
+
(child as any).__parent_children__ = children;
|
|
103
|
+
(child as any).__sibling_index__ = idx;
|
|
104
|
+
annotateChildren(child);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
// --- Code Generator (AST -> Instructions) ---
|
|
87
110
|
function renderStaticHTML(node: Node): string {
|
|
88
111
|
if (node.type === 'Text') {
|
|
@@ -202,37 +225,121 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
|
|
|
202
225
|
// 1. Create the anchor Comment node where the block belongs
|
|
203
226
|
chunks.push(`const ${id} = document.createComment("mu-if:${element.directives.vIf}");`);
|
|
204
227
|
|
|
205
|
-
// 2.
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
// 2. Helper to build a render-block function from any element
|
|
229
|
+
const buildBlockFn = (el: ElementNode) => {
|
|
230
|
+
const blockId = `_if_${uidRef.current++}`;
|
|
231
|
+
const blockChunks: string[] = [];
|
|
232
|
+
blockChunks.push(`const ${blockId}_frag = this._recoveryMode ? null : document.createDocumentFragment();`);
|
|
233
|
+
blockChunks.push(`const _block_effects = [];`);
|
|
234
|
+
const localBindMacro = `(fn, targetNode) => { const stop = this._bindEffect(fn, targetNode); _block_effects.push(stop); return stop; }`;
|
|
235
|
+
const origLen = blockChunks.length;
|
|
236
|
+
const elNoDirective = { ...el, directives: { ...el.directives, vIf: undefined, vElseIf: undefined, vElse: undefined } };
|
|
237
|
+
const clonedChildId = generateDOMInstruction(elNoDirective as ElementNode, blockChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename);
|
|
238
|
+
if (clonedChildId) {
|
|
239
|
+
blockChunks.push(`if (${blockId}_frag && ${clonedChildId} && !this._recoveryMode) ${blockId}_frag.appendChild(${clonedChildId});`);
|
|
240
|
+
}
|
|
241
|
+
for (let bi = origLen; bi < blockChunks.length; bi++) {
|
|
242
|
+
blockChunks[bi] = blockChunks[bi].replace(/this\._bindEffect/g, `(${localBindMacro})`);
|
|
243
|
+
}
|
|
244
|
+
blockChunks.push(`return { fragment: ${blockId}_frag, effects: _block_effects };`);
|
|
245
|
+
return `() => {\n ${blockChunks.join('\n ')}\n }`;
|
|
246
|
+
};
|
|
218
247
|
|
|
219
|
-
if
|
|
220
|
-
|
|
248
|
+
// 3. Collect else-if / else sibling branches from the AST parent's children
|
|
249
|
+
// We look at the parent stack's current top children after this node.
|
|
250
|
+
// Since we can't access the parent here directly, we use a forward-scan trick:
|
|
251
|
+
// Siblings are already on `chunks` as separate calls from the parent forEach.
|
|
252
|
+
// Instead we emit a chained condition as a ternary inside the reconciler.
|
|
253
|
+
|
|
254
|
+
// Build the if-condition string for the Mulan reconciler
|
|
255
|
+
// Format: [ [cond1, renderFn1], [cond2, renderFn2], ..., [null, elseFn] ]
|
|
256
|
+
// IMPORTANT: we compute the primary block function ONCE and reuse it to
|
|
257
|
+
// avoid calling buildBlockFn(element) twice (which caused infinite recursion
|
|
258
|
+
// because element still has vIf set on the second call).
|
|
259
|
+
const ifBlockFn = buildBlockFn(element);
|
|
260
|
+
const branches: string[] = [
|
|
261
|
+
`[() => !!(${condition}), ${ifBlockFn}]`
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
// Scan forward in the parent's children for else-if / else
|
|
265
|
+
// We mark them so the parent forEach loop skips them
|
|
266
|
+
const parentChildren = (element as any).__parent_children__ as Node[] | undefined;
|
|
267
|
+
let sibIdx = (element as any).__sibling_index__ as number | undefined;
|
|
268
|
+
if (parentChildren !== undefined && sibIdx !== undefined) {
|
|
269
|
+
sibIdx++;
|
|
270
|
+
while (sibIdx < parentChildren.length) {
|
|
271
|
+
const sib = parentChildren[sibIdx] as Node;
|
|
272
|
+
if (sib.type === 'Text') {
|
|
273
|
+
const text = sib as TextNode;
|
|
274
|
+
if (!text.content.trim()) {
|
|
275
|
+
// Skip whitespace between tags
|
|
276
|
+
(sib as any).__skip__ = true;
|
|
277
|
+
sibIdx++;
|
|
278
|
+
continue;
|
|
279
|
+
} else {
|
|
280
|
+
break; // Real text breaks the chain
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (sib.type !== 'Element') break;
|
|
284
|
+
|
|
285
|
+
const sibEl = sib as ElementNode;
|
|
286
|
+
if (sibEl.directives.vElseIf) {
|
|
287
|
+
const sibCond = `_s(${processBindings(sibEl.directives.vElseIf, bindings, localScope)})`;
|
|
288
|
+
branches.push(`[() => !!(${sibCond}), ${buildBlockFn(sibEl)}]`);
|
|
289
|
+
(sibEl as any).__skip__ = true;
|
|
290
|
+
sibIdx++;
|
|
291
|
+
} else if (sibEl.directives.vElse) {
|
|
292
|
+
branches.push(`[null, ${buildBlockFn(sibEl)}]`);
|
|
293
|
+
(sibEl as any).__skip__ = true;
|
|
294
|
+
sibIdx++;
|
|
295
|
+
break;
|
|
296
|
+
} else {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
221
300
|
}
|
|
222
301
|
|
|
223
|
-
|
|
224
|
-
|
|
302
|
+
// 4. Emit reconciler call — simple if, or chained if / else-if / else
|
|
303
|
+
const ifIdHash = `if_${Math.random().toString(36).substr(2, 6)}`;
|
|
304
|
+
if (branches.length === 1) {
|
|
305
|
+
// Simple if (no else): reuse ifBlockFn computed above
|
|
306
|
+
chunks.push(`this._bindEffect(() => { this._reconcileIf("${ifIdHash}", ${id}, !!(${condition}), ${ifBlockFn}); }, ${id});`);
|
|
307
|
+
} else {
|
|
308
|
+
// Chain: evaluate each condition in order
|
|
309
|
+
const branchArray = `[${branches.join(', ')}]`;
|
|
310
|
+
chunks.push(`this._bindEffect(() => {
|
|
311
|
+
const _branches = ${branchArray};
|
|
312
|
+
let _matched = false;
|
|
313
|
+
for (const [_cond, _renderFn] of _branches) {
|
|
314
|
+
if (_cond === null || _cond()) {
|
|
315
|
+
this._reconcileIf("${ifIdHash}", ${id}, true, _renderFn);
|
|
316
|
+
_matched = true;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!_matched) { this._reconcileIf("${ifIdHash}", ${id}, false, () => ({ fragment: null, effects: [] })); }
|
|
321
|
+
}, ${id});`);
|
|
225
322
|
}
|
|
226
323
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const renderBlockFn = `() => {\n ${blockChunks.join('\n ')}\n }`;
|
|
324
|
+
return id;
|
|
230
325
|
|
|
231
|
-
|
|
232
|
-
const ifIdHash = `if_${Math.random().toString(36).substr(2, 6)}`;
|
|
233
|
-
chunks.push(`this._bindEffect(() => { this._reconcileIf("${ifIdHash}", ${id}, !!(${condition}), ${renderBlockFn}); }, ${id});`);
|
|
326
|
+
}
|
|
234
327
|
|
|
235
|
-
|
|
328
|
+
// --- mu-show: CSS visibility toggle (element stays in DOM) ---
|
|
329
|
+
if (element.directives.vShow) {
|
|
330
|
+
const showId = getUid();
|
|
331
|
+
const showCondition = processBindings(element.directives.vShow, bindings, localScope);
|
|
332
|
+
// Create the element without the show directive
|
|
333
|
+
const elNoShow = { ...element, directives: { ...element.directives, vShow: undefined } };
|
|
334
|
+
// Generate the element normally (it renders into DOM)
|
|
335
|
+
const innerChunks: string[] = [];
|
|
336
|
+
const innerEl = generateDOMInstruction(elNoShow as ElementNode, innerChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename);
|
|
337
|
+
chunks.push(...innerChunks);
|
|
338
|
+
// Bind a reactive effect that toggles display
|
|
339
|
+
if (innerEl) {
|
|
340
|
+
chunks.push(`this._bindEffect(() => { if (${innerEl}) ${innerEl}.style.display = _s(${showCondition}) ? '' : 'none'; }, ${innerEl});`);
|
|
341
|
+
}
|
|
342
|
+
return innerEl;
|
|
236
343
|
}
|
|
237
344
|
|
|
238
345
|
// Handle mu-for (Phase 3 - The Reconciler)
|
|
@@ -301,7 +408,11 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
|
|
|
301
408
|
chunks.push(`if (${id}) ${id}.className = _va("class", ${JSON.stringify(value)});`);
|
|
302
409
|
}
|
|
303
410
|
} else if (key === 'id') {
|
|
304
|
-
|
|
411
|
+
if (value.includes('${')) {
|
|
412
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.id = _va("id", \`${value}\`); }, ${id});`);
|
|
413
|
+
} else {
|
|
414
|
+
chunks.push(`if (${id}) ${id}.id = _va("id", ${JSON.stringify(value)});`);
|
|
415
|
+
}
|
|
305
416
|
} else if (key === 'data-mu-id') {
|
|
306
417
|
// Ignore internal string compiler metadata
|
|
307
418
|
} else {
|
|
@@ -335,6 +446,7 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
|
|
|
335
446
|
// Recursively generate children
|
|
336
447
|
const childScope = [...localScope];
|
|
337
448
|
element.children.forEach(child => {
|
|
449
|
+
if ((child as any).__skip__) return; // consumed by mu-if else-if/else chain
|
|
338
450
|
const childId = generateDOMInstruction(child, chunks, getUid, getHoistId, hoists, uidRef, bindings, childScope, generator, filename);
|
|
339
451
|
if (childId) {
|
|
340
452
|
chunks.push(`if (${id} && ${childId} && !this._recoveryMode) ${id}.appendChild(${childId});`);
|
|
@@ -398,9 +510,29 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
398
510
|
|
|
399
511
|
const childScope = [...localScope];
|
|
400
512
|
if (element.directives.vFor) childScope.push(element.directives.vFor.item);
|
|
513
|
+
// Also add loop variable when inside mu-for with destructured tuple e.g. (item, i)
|
|
514
|
+
if (element.directives.vFor) {
|
|
515
|
+
const itemStr = element.directives.vFor.item;
|
|
516
|
+
const tupleMatch = itemStr.match(/^\(([^)]+)\)$/);
|
|
517
|
+
if (tupleMatch) {
|
|
518
|
+
tupleMatch[1].split(',').forEach(v => {
|
|
519
|
+
const trimmed = v.trim();
|
|
520
|
+
if (trimmed && !childScope.includes(trimmed)) childScope.push(trimmed);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
401
524
|
|
|
402
525
|
for (const key in element.props) {
|
|
403
|
-
|
|
526
|
+
// --- mu-model: two-way binding ---
|
|
527
|
+
if (key === 'mu-model' || key === 'v-model') {
|
|
528
|
+
const stateExpr = element.props[key];
|
|
529
|
+
const bound = processBindings(stateExpr, scriptResult.bindings, localScope);
|
|
530
|
+
// Read: bind value property reactively
|
|
531
|
+
domBindings.push({ type: 'prop', name: 'value', expr: `_s(${bound})` });
|
|
532
|
+
// Write: listen to 'input' event and update state
|
|
533
|
+
domBindings.push({ type: 'event', name: 'input', expr: `($event) => { ${bound} = $event.target.value; }` });
|
|
534
|
+
delete element.props[key];
|
|
535
|
+
} else if (key.startsWith('.')) {
|
|
404
536
|
const propName = key.slice(1);
|
|
405
537
|
const rawValue = element.props[key];
|
|
406
538
|
let expr = "''";
|
|
@@ -411,12 +543,32 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
411
543
|
}
|
|
412
544
|
domBindings.push({ type: 'prop', name: propName, expr });
|
|
413
545
|
delete element.props[key];
|
|
414
|
-
} else if (
|
|
415
|
-
|
|
546
|
+
} else if (
|
|
547
|
+
key.startsWith('@') ||
|
|
548
|
+
key.startsWith('v-on:') ||
|
|
549
|
+
// mu-* event aliases
|
|
550
|
+
key === 'mu-click' || key === 'mu-input' || key === 'mu-change' || key === 'mu-submit' ||
|
|
551
|
+
(key.startsWith('on') && key.length > 2)
|
|
552
|
+
) {
|
|
553
|
+
let eventName: string;
|
|
554
|
+
if (key.startsWith('@')) {
|
|
555
|
+
eventName = key.slice(1);
|
|
556
|
+
} else if (key.startsWith('v-on:')) {
|
|
557
|
+
eventName = key.slice(5);
|
|
558
|
+
} else if (key.startsWith('mu-')) {
|
|
559
|
+
// mu-click → click, mu-submit → submit, etc.
|
|
560
|
+
eventName = key.slice(3);
|
|
561
|
+
} else {
|
|
562
|
+
eventName = key.slice(2);
|
|
563
|
+
}
|
|
416
564
|
let bound = processBindings(element.props[key], scriptResult.bindings, localScope);
|
|
417
565
|
const expr = (bound.includes('(') || bound.includes('=') || bound.includes('++')) ? `($event) => { ${bound.replace(/`/g, '\\`')} }` : bound;
|
|
418
566
|
domBindings.push({ type: 'event', name: eventName, expr });
|
|
419
567
|
delete element.props[key];
|
|
568
|
+
} else {
|
|
569
|
+
if (typeof element.props[key] === 'string' && element.props[key].includes('${')) {
|
|
570
|
+
element.props[key] = element.props[key].replace(/\$\{(.*?)\}/g, (_: any, expr: string) => '${' + processBindings(expr, scriptResult.bindings, localScope) + '}');
|
|
571
|
+
}
|
|
420
572
|
}
|
|
421
573
|
}
|
|
422
574
|
element.children.forEach(child => transform(child, scriptResult, scopedId, childScope, filename));
|
|
@@ -65,9 +65,14 @@ export async function compileScript(descriptor: SFCDescriptor, options?: Compile
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const isSetup = !!script.attrs.setup;
|
|
68
|
+
const isMu = !!script.attrs.mu;
|
|
68
69
|
const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx' || externalPath.endsWith('.ts') || externalPath.endsWith('.tsx');
|
|
69
70
|
|
|
70
71
|
if (isSetup) {
|
|
72
|
+
// Transform <mu> natural syntax first if this is a <mu> block
|
|
73
|
+
if (isMu) {
|
|
74
|
+
content = transformMuSyntax(content);
|
|
75
|
+
}
|
|
71
76
|
return await compileSetupScript(script, isTs, descriptor, content, filename, isExternal, externalPath);
|
|
72
77
|
} else if (isTs) {
|
|
73
78
|
return await compileStandardScript(script, descriptor, content, filename, isExternal, externalPath);
|
|
@@ -579,3 +584,95 @@ function transformNaturalReactivity(code: string, isTs: boolean): string {
|
|
|
579
584
|
const printer = ts.createPrinter();
|
|
580
585
|
return printer.printFile(result.transformed[0]);
|
|
581
586
|
}
|
|
587
|
+
|
|
588
|
+
// --- Mu Syntax Transformer ---
|
|
589
|
+
// Converts clean <mu> block natural-language syntax into valid TypeScript
|
|
590
|
+
// that the standard setup-script pipeline can consume.
|
|
591
|
+
//
|
|
592
|
+
// Supported transformations:
|
|
593
|
+
// name = value → let name = value
|
|
594
|
+
// fn() → → function fn() {
|
|
595
|
+
// fn(a, b) → → function fn(a, b) {
|
|
596
|
+
// fn() → expr → function fn() { return expr } (single-line)
|
|
597
|
+
// (indented body after →) → { body... }
|
|
598
|
+
export function transformMuSyntax(code: string): string {
|
|
599
|
+
const lines = code.split(/\r?\n/);
|
|
600
|
+
const out: string[] = [];
|
|
601
|
+
let i = 0;
|
|
602
|
+
|
|
603
|
+
while (i < lines.length) {
|
|
604
|
+
const raw = lines[i];
|
|
605
|
+
const trimmed = raw.trim();
|
|
606
|
+
|
|
607
|
+
// Skip blank lines
|
|
608
|
+
if (!trimmed) {
|
|
609
|
+
out.push(raw);
|
|
610
|
+
i++;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ─── Already valid TS/JS — preserve as-is ──────────────────────────────
|
|
615
|
+
// Lines that already start with a keyword don't need transformation
|
|
616
|
+
if (
|
|
617
|
+
/^(const|let|var|function|class|import|export|return|if|else|for|while|switch|throw|try|catch|finally|\/{2}|\*)/.test(trimmed)
|
|
618
|
+
) {
|
|
619
|
+
out.push(raw);
|
|
620
|
+
i++;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ─── Arrow-function definition: name(args) → ───────────────────────────
|
|
625
|
+
// Two forms:
|
|
626
|
+
// (A) fn() → — block body follows on next lines
|
|
627
|
+
// (B) fn() → expression — single-line body
|
|
628
|
+
const arrowMatch = trimmed.match(/^([a-zA-Z_$][\w$]*)\s*\(([^)]*)\)\s*→\s*(.*)$/);
|
|
629
|
+
if (arrowMatch) {
|
|
630
|
+
const [, fnName, params, bodyExpr] = arrowMatch;
|
|
631
|
+
const indent = raw.match(/^(\s*)/)?.[1] ?? '';
|
|
632
|
+
|
|
633
|
+
if (bodyExpr.trim()) {
|
|
634
|
+
// Single-line: fn() → expr → function fn(...) { return expr }
|
|
635
|
+
out.push(`${indent}function ${fnName}(${params.trim()}) { return ${bodyExpr.trim()} }`);
|
|
636
|
+
i++;
|
|
637
|
+
} else {
|
|
638
|
+
// Block form: collect indented body lines
|
|
639
|
+
out.push(`${indent}function ${fnName}(${params.trim()}) {`);
|
|
640
|
+
i++;
|
|
641
|
+
const baseIndent = raw.search(/\S/);
|
|
642
|
+
while (i < lines.length) {
|
|
643
|
+
const bodyRaw = lines[i];
|
|
644
|
+
const bodyTrimmed = bodyRaw.trim();
|
|
645
|
+
if (!bodyTrimmed) {
|
|
646
|
+
out.push('');
|
|
647
|
+
i++;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const bodyIndent = bodyRaw.search(/\S/);
|
|
651
|
+
// If the next line is at the same or lower indent → end of function body
|
|
652
|
+
if (bodyIndent <= baseIndent) break;
|
|
653
|
+
out.push(bodyRaw);
|
|
654
|
+
i++;
|
|
655
|
+
}
|
|
656
|
+
out.push(`${indent}}`);
|
|
657
|
+
}
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ─── Bare assignment: name = value ─────────────────────────────────────
|
|
662
|
+
// Only matches simple top-level assignments, not ===, !==, +=, etc.
|
|
663
|
+
const assignMatch = trimmed.match(/^([a-zA-Z_$][\w$]*)\s*=(?!=)\s*(.+)$/);
|
|
664
|
+
if (assignMatch) {
|
|
665
|
+
const indent = raw.match(/^(\s*)/)?.[1] ?? '';
|
|
666
|
+
const [, varName, valueExpr] = assignMatch;
|
|
667
|
+
out.push(`${indent}let ${varName} = ${valueExpr}`);
|
|
668
|
+
i++;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Fallback — emit as-is
|
|
673
|
+
out.push(raw);
|
|
674
|
+
i++;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return out.join('\n');
|
|
678
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface SFCBlock {
|
|
2
|
-
type: 'script' | 'template' | 'style';
|
|
2
|
+
type: 'script' | 'template' | 'style' | 'mu';
|
|
3
3
|
content: string;
|
|
4
4
|
attrs: Record<string, string>;
|
|
5
5
|
start: number;
|
|
@@ -100,6 +100,13 @@ export function parseMUJS(source: string, filename: string): SFCDescriptor {
|
|
|
100
100
|
// Add to descriptor
|
|
101
101
|
if (tagName === 'template' || tagName === 'mu-template') {
|
|
102
102
|
descriptor.template = block;
|
|
103
|
+
} else if (tagName === 'mu') {
|
|
104
|
+
// <mu> is the MulanJS branded script block — equivalent to <script setup lang="ts">
|
|
105
|
+
// Mark attrs so downstream compilers treat it as setup + TypeScript + mu-native syntax
|
|
106
|
+
block.attrs = { ...block.attrs, setup: 'true', lang: 'ts', mu: 'true' };
|
|
107
|
+
block.type = 'script';
|
|
108
|
+
descriptor.scripts.push(block);
|
|
109
|
+
descriptor.script = block;
|
|
103
110
|
} else if (tagName === 'script') {
|
|
104
111
|
descriptor.scripts.push(block);
|
|
105
112
|
if (attrs.setup || !descriptor.script) {
|