@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 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
- <script setup lang="ts">
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
- const state = muState({ count: 0 });
75
+ // Create reactive state natively
76
+ state = muState({ count: 0 });
76
77
 
77
- const debugClick = () => {
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" @click="debugClick()">
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. Generate the inner block rendering function
175
- const blockId = `_if_${uidRef.current++}`;
176
- const blockChunks = [];
177
- blockChunks.push(`const ${blockId}_frag = this._recoveryMode ? null : document.createDocumentFragment();`);
178
- blockChunks.push(`const _block_effects = [];`);
179
- // 2a. Override bindings for inner effects
180
- const localBindMacro = `(fn, targetNode) => { const stop = this._bindEffect(fn, targetNode); _block_effects.push(stop); return stop; }`;
181
- let originalLength = blockChunks.length;
182
- // 2b. Generate the actual block
183
- const elementWithoutIf = { ...element, directives: { ...element.directives, vIf: undefined } };
184
- const clonedChildId = generateDOMInstruction(elementWithoutIf, blockChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename);
185
- if (clonedChildId) {
186
- blockChunks.push(`if (${blockId}_frag && ${clonedChildId} && !this._recoveryMode) ${blockId}_frag.appendChild(${clonedChildId});`);
187
- }
188
- for (let i = originalLength; i < blockChunks.length; i++) {
189
- blockChunks[i] = blockChunks[i].replace(/this\._bindEffect/g, `(${localBindMacro})`);
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
- blockChunks.push(`return { fragment: ${blockId}_frag, effects: _block_effects };`);
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
- chunks.push(`this._bindEffect(() => { this._reconcileIf("${ifIdHash}", ${id}, !!(${condition}), ${renderBlockFn}); }, ${id});`);
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
- if (key.startsWith('.')) {
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('@') || key.startsWith('v-on:') || (key.startsWith('on') && key.length > 2)) {
364
- let eventName = key.startsWith('@') ? key.slice(1) : key.startsWith('v-on:') ? key.slice(5) : key.slice(2);
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;
@@ -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;
@@ -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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulanjs/mulanjs",
3
- "version": "1.0.1-dev.20260305165012",
3
+ "version": "1.0.1-dev.20260310065102",
4
4
  "description": "A powerful, secure, and enterprise-grade JavaScript framework.",
5
5
  "main": "dist/mulan.js",
6
6
  "module": "dist/mulan.esm.js",
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
- `<script setup lang="ts">
288
- </script>
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
- `<script setup lang="ts">
349
+ `<mu>
350
350
  import { muState } from '@mulanjs/mulanjs';
351
- const state = muState({ count: 0 });
352
- </script>
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" @click="state.count++">
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.0.vsix');
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 = `<script setup lang="ts">
577
+ const content = `<mu>
578
578
  import { muState } from '@mulanjs/mulanjs';
579
- const state = muState({ count: 0 });
580
- </script>
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.0.vsix');
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. Generate the inner block rendering function
206
- const blockId = `_if_${uidRef.current++}`;
207
- const blockChunks: string[] = [];
208
- blockChunks.push(`const ${blockId}_frag = this._recoveryMode ? null : document.createDocumentFragment();`);
209
- blockChunks.push(`const _block_effects = [];`);
210
-
211
- // 2a. Override bindings for inner effects
212
- const localBindMacro = `(fn, targetNode) => { const stop = this._bindEffect(fn, targetNode); _block_effects.push(stop); return stop; }`;
213
- let originalLength = blockChunks.length;
214
-
215
- // 2b. Generate the actual block
216
- const elementWithoutIf = { ...element, directives: { ...element.directives, vIf: undefined } };
217
- const clonedChildId = generateDOMInstruction(elementWithoutIf as ElementNode, blockChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename);
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 (clonedChildId) {
220
- blockChunks.push(`if (${blockId}_frag && ${clonedChildId} && !this._recoveryMode) ${blockId}_frag.appendChild(${clonedChildId});`);
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
- for (let i = originalLength; i < blockChunks.length; i++) {
224
- blockChunks[i] = blockChunks[i].replace(/this\._bindEffect/g, `(${localBindMacro})`);
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
- blockChunks.push(`return { fragment: ${blockId}_frag, effects: _block_effects };`);
228
-
229
- const renderBlockFn = `() => {\n ${blockChunks.join('\n ')}\n }`;
324
+ return id;
230
325
 
231
- // 3. Register the macro-effect that calls the Reconciler
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
- return id;
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
- if (key.startsWith('.')) {
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 (key.startsWith('@') || key.startsWith('v-on:') || (key.startsWith('on') && key.length > 2)) {
419
- let eventName = key.startsWith('@') ? key.slice(1) : key.startsWith('v-on:') ? key.slice(5) : key.slice(2);
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) {