@mulanjs/mulanjs 1.0.1-dev.20260305165012 → 1.0.1-dev.20260310065102
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/README.md +9 -8
- package/dist/compiler/ast-parser.js +11 -2
- package/dist/compiler/dom-compiler.js +176 -24
- 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/extensions/{mulanjs-vscode-1.0.0.vsix → mulanjs-vscode-1.0.1.vsix} +0 -0
- package/src/cli/index.js +11 -11
- package/src/compiler/ast-parser.ts +11 -2
- package/src/compiler/dom-compiler.ts +172 -28
- package/src/compiler/script-compiler.ts +97 -0
- package/src/compiler/sfc-parser.ts +8 -1
package/README.md
CHANGED
|
@@ -68,17 +68,17 @@ Built-in support for `<style lang="scss">`. No config required. Just design.
|
|
|
68
68
|
MulanJS components (`.mujs`) allow you to write powerful, reactive UI with minimal boilerplate.
|
|
69
69
|
|
|
70
70
|
```html
|
|
71
|
-
<
|
|
71
|
+
<mu>
|
|
72
|
+
// The <mu> tag is the new, powerful replacement for <script setup lang="ts">
|
|
72
73
|
import { muState } from 'mulanjs';
|
|
73
74
|
|
|
74
|
-
// Create reactive state
|
|
75
|
-
|
|
75
|
+
// Create reactive state natively
|
|
76
|
+
state = muState({ count: 0 });
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
debugClick() →
|
|
78
79
|
console.log('Button clicked! Current count:', state.count);
|
|
79
80
|
state.count++;
|
|
80
|
-
|
|
81
|
-
</script>
|
|
81
|
+
</mu>
|
|
82
82
|
|
|
83
83
|
<mu-template>
|
|
84
84
|
<div class="page">
|
|
@@ -88,8 +88,8 @@ const debugClick = () => {
|
|
|
88
88
|
|
|
89
89
|
<div class="counter-box">
|
|
90
90
|
<p>Interactive Counter:</p>
|
|
91
|
-
<!-- Native Event Binding -->
|
|
92
|
-
<button class="mu-btn"
|
|
91
|
+
<!-- Native Mulan Event Binding -->
|
|
92
|
+
<button class="mu-btn" mu-click="debugClick()">
|
|
93
93
|
Count is: ${state.count}
|
|
94
94
|
</button>
|
|
95
95
|
</div>
|
|
@@ -125,6 +125,7 @@ p { font-size: 1.2rem; color: #888; max-width: 600px; margin: 0 auto 40px; }
|
|
|
125
125
|
}
|
|
126
126
|
</style>
|
|
127
127
|
```
|
|
128
|
+
*(Note: MulanJS still retains 100% backward compatibility with standard `<script setup lang="ts">` and `<template>` tags).*
|
|
128
129
|
|
|
129
130
|
## Created By
|
|
130
131
|
**Nitin Kumar** (@nitinkumardev09) - *Creator of .mujs & MulanJS Framework*
|
|
@@ -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;
|
|
@@ -288,6 +399,8 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
288
399
|
// Recursively generate children
|
|
289
400
|
const childScope = [...localScope];
|
|
290
401
|
element.children.forEach(child => {
|
|
402
|
+
if (child.__skip__)
|
|
403
|
+
return; // consumed by mu-if else-if/else chain
|
|
291
404
|
const childId = generateDOMInstruction(child, chunks, getUid, getHoistId, hoists, uidRef, bindings, childScope, generator, filename);
|
|
292
405
|
if (childId) {
|
|
293
406
|
chunks.push(`if (${id} && ${childId} && !this._recoveryMode) ${id}.appendChild(${childId});`);
|
|
@@ -346,8 +459,30 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
346
459
|
const childScope = [...localScope];
|
|
347
460
|
if (element.directives.vFor)
|
|
348
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
|
+
}
|
|
349
474
|
for (const key in element.props) {
|
|
350
|
-
|
|
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('.')) {
|
|
351
486
|
const propName = key.slice(1);
|
|
352
487
|
const rawValue = element.props[key];
|
|
353
488
|
let expr = "''";
|
|
@@ -360,8 +495,25 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
360
495
|
domBindings.push({ type: 'prop', name: propName, expr });
|
|
361
496
|
delete element.props[key];
|
|
362
497
|
}
|
|
363
|
-
else if (key.startsWith('@') ||
|
|
364
|
-
|
|
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
|
+
}
|
|
365
517
|
let bound = processBindings(element.props[key], scriptResult.bindings, localScope);
|
|
366
518
|
const expr = (bound.includes('(') || bound.includes('=') || bound.includes('++')) ? `($event) => { ${bound.replace(/`/g, '\\`')} }` : bound;
|
|
367
519
|
domBindings.push({ type: 'event', name: eventName, expr });
|
|
@@ -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
|
Binary file
|
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>
|
|
@@ -494,7 +494,7 @@ if (app) {
|
|
|
494
494
|
|
|
495
495
|
// Auto-install VS Code Extension
|
|
496
496
|
try {
|
|
497
|
-
const vsixPath = path.join(__dirname, 'extensions', 'mulanjs-vscode-1.0.
|
|
497
|
+
const vsixPath = path.join(__dirname, 'extensions', 'mulanjs-vscode-1.0.1.vsix');
|
|
498
498
|
if (fs.existsSync(vsixPath)) {
|
|
499
499
|
console.log('\n📦 Installing MulanJS VS Code Extension...');
|
|
500
500
|
shell(`code --install-extension "${vsixPath}" --force`);
|
|
@@ -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>
|
|
@@ -606,7 +606,7 @@ program
|
|
|
606
606
|
console.log('📦 Locating MulanJS VS Code Extension...');
|
|
607
607
|
try {
|
|
608
608
|
// Find where mulanjs is installed. If run via npx, __dirname is the cli folder.
|
|
609
|
-
const vsixPath = path.join(__dirname, 'extensions', 'mulanjs-vscode-1.0.
|
|
609
|
+
const vsixPath = path.join(__dirname, 'extensions', 'mulanjs-vscode-1.0.1.vsix');
|
|
610
610
|
|
|
611
611
|
if (!fs.existsSync(vsixPath)) {
|
|
612
612
|
console.error('❌ Error: Extension file not found at:', vsixPath);
|
|
@@ -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)
|
|
@@ -339,6 +446,7 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
|
|
|
339
446
|
// Recursively generate children
|
|
340
447
|
const childScope = [...localScope];
|
|
341
448
|
element.children.forEach(child => {
|
|
449
|
+
if ((child as any).__skip__) return; // consumed by mu-if else-if/else chain
|
|
342
450
|
const childId = generateDOMInstruction(child, chunks, getUid, getHoistId, hoists, uidRef, bindings, childScope, generator, filename);
|
|
343
451
|
if (childId) {
|
|
344
452
|
chunks.push(`if (${id} && ${childId} && !this._recoveryMode) ${id}.appendChild(${childId});`);
|
|
@@ -402,9 +510,29 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
402
510
|
|
|
403
511
|
const childScope = [...localScope];
|
|
404
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
|
+
}
|
|
405
524
|
|
|
406
525
|
for (const key in element.props) {
|
|
407
|
-
|
|
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('.')) {
|
|
408
536
|
const propName = key.slice(1);
|
|
409
537
|
const rawValue = element.props[key];
|
|
410
538
|
let expr = "''";
|
|
@@ -415,8 +543,24 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
415
543
|
}
|
|
416
544
|
domBindings.push({ type: 'prop', name: propName, expr });
|
|
417
545
|
delete element.props[key];
|
|
418
|
-
} else if (
|
|
419
|
-
|
|
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
|
+
}
|
|
420
564
|
let bound = processBindings(element.props[key], scriptResult.bindings, localScope);
|
|
421
565
|
const expr = (bound.includes('(') || bound.includes('=') || bound.includes('++')) ? `($event) => { ${bound.replace(/`/g, '\\`')} }` : bound;
|
|
422
566
|
domBindings.push({ type: 'event', name: eventName, expr });
|
|
@@ -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) {
|