@mulanjs/mulanjs 1.0.1-dev.20260830064028 → 1.0.1-dev.20261016101332

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
@@ -63,16 +63,40 @@ Adaptive non-blocking loops (`muBurst`) and true multicore parallelism (`muSurge
63
63
 
64
64
  ### Native SCSS Support 🎨
65
65
  Built-in support for `<style lang="scss">`. No config required. Just design.
66
- ## 🚀 Quick Start Example
67
66
 
68
- MulanJS components (`.mujs`) allow you to write powerful, reactive UI with minimal boilerplate.
67
+ ---
68
+
69
+ ## ⚡ Mulan Minicode (AI-First, Ultra-Dense Syntax)
70
+
71
+ MulanJS supports **Minicode** — an ultra-compact single-file component syntax designed for maximum developer ergonomics and AI code-generation efficiency (**~20 tokens** per component, ~40% fewer tokens than Svelte 5):
72
+
73
+ - **No mandatory imports**: Ambient `$state`, `$derived`, `$effect`, and `$vault` runes.
74
+ - **Implicit Root Template**: Drop `<mu-template>` entirely — top-level markup is automatically compiled.
75
+ - **Single-Brace Interpolation**: `{count}` alongside `${count}` and `{{count}}`.
76
+ - **Terse Directives**: `@click="count++"` or `@click="count--"` or `:disabled="count === 0"`.
77
+
78
+ ```html
79
+ <mu>
80
+ count = $state(0)
81
+ double = $derived(count * 2)
82
+ </mu>
83
+
84
+ <button class="mu-btn" @click="count++">
85
+ Count: {count} | Double: {double}
86
+ </button>
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 🚀 Quick Start Example (Classic Syntax)
92
+
93
+ MulanJS components (`.mujs`) also support classic explicit syntax with `<mu-template>`:
69
94
 
70
95
  ```javascript
71
96
  <mu>
72
- // The <mu> tag is the new, powerful replacement for <script setup lang="ts">
73
- import { muState } from 'mulanjs';
97
+ // The <mu> tag is the powerful replacement for <script setup lang="ts">
98
+ import { muState } from '@mulanjs/mulanjs';
74
99
 
75
- // Create reactive state natively
76
100
  state = muState({ count: 0 });
77
101
 
78
102
  debugClick() →
@@ -81,7 +105,6 @@ debugClick() →
81
105
  </mu>
82
106
  ```
83
107
  ```html
84
-
85
108
  <mu-template>
86
109
  <div class="page">
87
110
  <div class="hero">
@@ -90,7 +113,6 @@ debugClick() →
90
113
 
91
114
  <div class="counter-box">
92
115
  <p>Interactive Counter:</p>
93
- <!-- Native Mulan Event Binding -->
94
116
  <button class="mu-btn" mu-click="debugClick()">
95
117
  Count is: ${state.count}
96
118
  </button>
@@ -152,11 +152,62 @@ function parse(template, errors) {
152
152
  }
153
153
  cursor = innerCursor;
154
154
  }
155
+ // 5. Single-Brace Interpolation { } (Svelte / Minicode)
156
+ else if (template.startsWith('{', cursor) && !template.startsWith('{{', cursor) && !template.startsWith('${', cursor)) {
157
+ let innerCursor = cursor + 1;
158
+ let depth = 1;
159
+ while (innerCursor < template.length && depth > 0) {
160
+ if (template[innerCursor] === '{')
161
+ depth++;
162
+ if (template[innerCursor] === '}')
163
+ depth--;
164
+ innerCursor++;
165
+ }
166
+ if (depth === 0) {
167
+ const content = template.slice(cursor + 1, innerCursor - 1).trim();
168
+ const startPos = getPos(cursor);
169
+ const endPos = getPos(innerCursor);
170
+ stack[stack.length - 1].children.push({
171
+ type: 'Interpolation',
172
+ content,
173
+ ...startPos,
174
+ endLine: endPos.startLine,
175
+ endColumn: endPos.startColumn
176
+ });
177
+ cursor = innerCursor;
178
+ }
179
+ else {
180
+ const startPos = getPos(cursor);
181
+ const endPos = getPos(cursor + 1);
182
+ const lastChild = stack[stack.length - 1].children[stack[stack.length - 1].children.length - 1];
183
+ if (lastChild && lastChild.type === 'Text') {
184
+ lastChild.content += '{';
185
+ }
186
+ else {
187
+ stack[stack.length - 1].children.push({
188
+ type: 'Text',
189
+ content: '{',
190
+ ...startPos,
191
+ endLine: endPos.startLine,
192
+ endColumn: endPos.startColumn
193
+ });
194
+ }
195
+ cursor++;
196
+ }
197
+ }
155
198
  // 5. Text Nodes
156
199
  else {
157
200
  let nextTag = template.indexOf('<', cursor);
158
201
  let nextInterp = template.indexOf('{{', cursor);
159
202
  let nextNative = template.indexOf('${', cursor);
203
+ let nextBrace = template.indexOf('{', cursor);
204
+ // Skip {{ and ${ for nextBrace
205
+ while (nextBrace !== -1) {
206
+ if (template[nextBrace + 1] !== '{' && (nextBrace === 0 || template[nextBrace - 1] !== '$')) {
207
+ break;
208
+ }
209
+ nextBrace = template.indexOf('{', nextBrace + 1);
210
+ }
160
211
  let end = template.length;
161
212
  // Heuristic for nextTag: it must look like a tag start
162
213
  while (nextTag !== -1) {
@@ -171,6 +222,8 @@ function parse(template, errors) {
171
222
  end = nextInterp;
172
223
  if (nextNative !== -1 && nextNative < end)
173
224
  end = nextNative;
225
+ if (nextBrace !== -1 && nextBrace < end)
226
+ end = nextBrace;
174
227
  const rawContent = template.slice(cursor, end);
175
228
  const content = decodeEntities(rawContent);
176
229
  if (content) {
@@ -287,6 +340,19 @@ function parseTag(content) {
287
340
  value = attrStr.slice(valStart, i);
288
341
  i++;
289
342
  }
343
+ else if (i < attrStr.length && attrStr[i] === '{') {
344
+ const valStart = i + 1;
345
+ let depth = 1;
346
+ i++;
347
+ while (i < attrStr.length && depth > 0) {
348
+ if (attrStr[i] === '{')
349
+ depth++;
350
+ if (attrStr[i] === '}')
351
+ depth--;
352
+ i++;
353
+ }
354
+ value = attrStr.slice(valStart, i - 1).trim();
355
+ }
290
356
  else {
291
357
  const valStart = i;
292
358
  while (i < attrStr.length && !/\s/.test(attrStr[i])) {
@@ -121,12 +121,19 @@ if (typeof document !== 'undefined') {
121
121
  ${templateResult.code}
122
122
 
123
123
  // Attach template to component
124
- if (__component__.prototype) {
124
+ if (typeof __component__ === 'function' && __component__.prototype) {
125
125
  // Class component
126
126
  __component__.prototype.template = render;
127
- } else if (typeof __component__ === 'object') {
128
- // Options object
129
- __component__.template = render;
127
+ } else if (typeof __component__ === 'object' && __component__ !== null) {
128
+ // Wrap Options object or plain object in defineComponent so it is constructible
129
+ try {
130
+ const _mulanDef = (typeof _defineComponent !== 'undefined' ? _defineComponent : (typeof Mulan !== 'undefined' && Mulan.defineComponent ? Mulan.defineComponent : require('@mulanjs/mulanjs').defineComponent));
131
+ __component__.template = render;
132
+ __component__ = _mulanDef(__component__);
133
+ __component__.prototype.template = render;
134
+ } catch (e) {
135
+ __component__.template = render;
136
+ }
130
137
  }
131
138
 
132
139
  ${cssInjection}
@@ -165,6 +172,9 @@ if (typeof module !== 'undefined' && module.hot) {
165
172
 
166
173
  // console.log('[MulanJS] Component created:', __component__);
167
174
  module.exports = __component__;
175
+ if (__component__) {
176
+ module.exports.default = __component__;
177
+ }
168
178
 
169
179
  // Helper to get relative path for sourceURL
170
180
  // This ensures the generated file appears in a readable folder structure in DevTools
@@ -389,7 +389,12 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
389
389
  if (element._domBindings) {
390
390
  for (const b of element._domBindings) {
391
391
  if (b.type === 'prop') {
392
- chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = _va('${b.name}', ${b.expr}); }, ${id});`);
392
+ if (b.name === 'class' || b.name === 'className') {
393
+ chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = _va("class", ${b.expr}); }, ${id});`);
394
+ }
395
+ else {
396
+ chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = _va('${b.name}', ${b.expr}); }, ${id});`);
397
+ }
393
398
  }
394
399
  else if (b.type === 'event') {
395
400
  chunks.push(`if (${id}) ${id}.addEventListener('${b.name}', (${b.expr}).bind(this));`);
@@ -482,17 +487,20 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
482
487
  domBindings.push({ type: 'event', name: 'input', expr: `($event) => { ${bound} = $event.target.value; }` });
483
488
  delete element.props[key];
484
489
  }
485
- else if (key.startsWith('.')) {
486
- const propName = key.slice(1);
490
+ else if (key.startsWith(':') || key.startsWith('v-bind:') || key.startsWith('.')) {
491
+ const propName = key.startsWith(':') ? key.slice(1) : (key.startsWith('v-bind:') ? key.slice(7) : key.slice(1));
487
492
  const rawValue = element.props[key];
488
493
  let expr = "''";
489
494
  if (rawValue.startsWith('${') && rawValue.endsWith('}')) {
490
495
  expr = processBindings(rawValue.slice(2, -1), scriptResult.bindings, localScope);
491
496
  }
497
+ else if (rawValue.startsWith('{') && rawValue.endsWith('}')) {
498
+ expr = processBindings(rawValue.slice(1, -1), scriptResult.bindings, localScope);
499
+ }
492
500
  else {
493
- expr = JSON.stringify(rawValue);
501
+ expr = processBindings(rawValue, scriptResult.bindings, localScope);
494
502
  }
495
- domBindings.push({ type: 'prop', name: propName, expr });
503
+ domBindings.push({ type: 'prop', name: propName, expr: `_s(${expr})` });
496
504
  delete element.props[key];
497
505
  }
498
506
  else if (key.startsWith('@') ||
@@ -515,7 +523,7 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
515
523
  eventName = key.slice(2);
516
524
  }
517
525
  let bound = processBindings(element.props[key], scriptResult.bindings, localScope);
518
- const expr = (bound.includes('(') || bound.includes('=') || bound.includes('++')) ? `($event) => { ${bound.replace(/`/g, '\\`')} }` : bound;
526
+ const expr = (bound.includes('(') || bound.includes('=') || bound.includes('++') || bound.includes('--')) ? `($event) => { ${bound.replace(/`/g, '\\`')} }` : bound;
519
527
  domBindings.push({ type: 'event', name: eventName, expr });
520
528
  delete element.props[key];
521
529
  }
@@ -49,7 +49,16 @@ async function compileScript(descriptor, options) {
49
49
  });
50
50
  finalMap = generator.toString();
51
51
  }
52
- return { code: 'export default {}', errors: [], map: finalMap };
52
+ const defaultCode = `import { defineComponent as _defineComponent } from '@mulanjs/mulanjs';\nexport default _defineComponent({});`;
53
+ const result = ts.transpileModule(defaultCode, {
54
+ compilerOptions: {
55
+ module: ts.ModuleKind.CommonJS,
56
+ target: ts.ScriptTarget.ES2019,
57
+ sourceMap: false
58
+ },
59
+ fileName: descriptor.filename || 'component.mujs'
60
+ });
61
+ return { code: result.outputText, errors: [], map: finalMap };
53
62
  }
54
63
  let content = script.content;
55
64
  let filename = descriptor.filename;
@@ -95,7 +104,11 @@ async function compileScript(descriptor, options) {
95
104
  }
96
105
  exports.compileScript = compileScript;
97
106
  // Global list of Mulan hooks for auto-imports and reactivity exemption
98
- const COMMON_HOOKS = ['muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo', 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent'];
107
+ const COMMON_HOOKS = [
108
+ 'muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo',
109
+ 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent',
110
+ 'muVault', 'muComputed', 'muWatch', 'onMuIdle', 'onMuResume'
111
+ ];
99
112
  async function compileSetupScript(script, isTs, descriptor, content, filename, isExternal, externalPath) {
100
113
  var _a;
101
114
  // 1. Pre-process: Natural Reactivity Transformation ($ syntax)
@@ -389,10 +402,32 @@ async function compileStandardScript(script, descriptor, content, filename, isEx
389
402
  map: finalMap
390
403
  };
391
404
  }
392
- // --- Natural Reactivity Transformer ---
405
+ // --- Natural Reactivity & Runes Transformer ---
406
+ function getRuneKind(expr) {
407
+ if (ts.isIdentifier(expr)) {
408
+ const text = expr.text;
409
+ if (text === '$' || text === '$state')
410
+ return 'state';
411
+ if (text === '$derived' || text === '$memo')
412
+ return 'derived';
413
+ if (text === '$effect')
414
+ return 'effect';
415
+ if (text === '$vault')
416
+ return 'vault';
417
+ if (text === '$q')
418
+ return 'qubit';
419
+ if (text === '$qr')
420
+ return 'register';
421
+ }
422
+ else if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === '$state') {
423
+ return 'state'; // e.g. $state.raw(...)
424
+ }
425
+ return null;
426
+ }
393
427
  function transformNaturalReactivity(code, isTs) {
394
- // If no '$(' or '$q(' or '$qr(' usage, return original code for safety/speed
395
- if (!code.includes('$(') && !code.includes('$q(') && !code.includes('$qr('))
428
+ // If no rune usage, return original code for safety/speed
429
+ const hasRune = /\$(state|derived|memo|effect|vault|q|qr|\()/.test(code);
430
+ if (!hasRune)
396
431
  return code;
397
432
  const sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true);
398
433
  const reactiveVars = new Set();
@@ -403,19 +438,16 @@ function transformNaturalReactivity(code, isTs) {
403
438
  if (ts.isVariableStatement(node)) {
404
439
  node.declarationList.declarations.forEach(decl => {
405
440
  if (decl.initializer && ts.isCallExpression(decl.initializer)) {
406
- const expr = decl.initializer.expression;
407
- if (ts.isIdentifier(expr)) {
408
- if (expr.text === '$') {
409
- if (ts.isIdentifier(decl.name))
410
- reactiveVars.add(decl.name.text);
441
+ const rune = getRuneKind(decl.initializer.expression);
442
+ if (ts.isIdentifier(decl.name)) {
443
+ if (rune === 'state' || rune === 'derived' || rune === 'vault') {
444
+ reactiveVars.add(decl.name.text);
411
445
  }
412
- else if (expr.text === '$q') {
413
- if (ts.isIdentifier(decl.name))
414
- quantumVars.add(decl.name.text);
446
+ else if (rune === 'qubit') {
447
+ quantumVars.add(decl.name.text);
415
448
  }
416
- else if (expr.text === '$qr') {
417
- if (ts.isIdentifier(decl.name))
418
- registerVars.add(decl.name.text);
449
+ else if (rune === 'register') {
450
+ registerVars.add(decl.name.text);
419
451
  }
420
452
  }
421
453
  }
@@ -424,8 +456,6 @@ function transformNaturalReactivity(code, isTs) {
424
456
  ts.forEachChild(node, findReactiveVars);
425
457
  }
426
458
  findReactiveVars(sourceFile);
427
- if (reactiveVars.size === 0 && quantumVars.size === 0 && registerVars.size === 0)
428
- return code;
429
459
  // Pass 2: Transformation
430
460
  const transformerFactory = (context) => {
431
461
  return (rootNode) => {
@@ -436,23 +466,47 @@ function transformNaturalReactivity(code, isTs) {
436
466
  node.declarationList.declarations.forEach(decl => {
437
467
  if (ts.isIdentifier(decl.name)) {
438
468
  const name = decl.name.text;
439
- if (reactiveVars.has(name) || quantumVars.has(name) || registerVars.has(name)) {
440
- changed = true;
441
- let hookName = 'muState';
442
- if (quantumVars.has(name))
443
- hookName = 'muQubit';
444
- else if (registerVars.has(name))
445
- hookName = 'muRegister';
446
- const init = decl.initializer;
447
- newDecls.push(ts.factory.updateVariableDeclaration(decl, decl.name, decl.exclamationToken, decl.type, ts.factory.createCallExpression(ts.factory.createIdentifier(hookName), undefined, init.arguments)));
469
+ if (decl.initializer && ts.isCallExpression(decl.initializer)) {
470
+ const rune = getRuneKind(decl.initializer.expression);
471
+ if (rune) {
472
+ changed = true;
473
+ if (rune === 'state') {
474
+ const stateArgs = decl.initializer.arguments.map(a => ts.visitNode(a, visit));
475
+ newDecls.push(ts.factory.updateVariableDeclaration(decl, decl.name, decl.exclamationToken, decl.type, ts.factory.createCallExpression(ts.factory.createIdentifier('muState'), undefined, stateArgs)));
476
+ return;
477
+ }
478
+ else if (rune === 'derived') {
479
+ const rawArg = decl.initializer.arguments[0];
480
+ const arg = rawArg ? ts.visitNode(rawArg, visit) : undefined;
481
+ const computeFn = (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)))
482
+ ? arg
483
+ : ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), arg || ts.factory.createIdentifier('undefined'));
484
+ newDecls.push(ts.factory.updateVariableDeclaration(decl, decl.name, decl.exclamationToken, decl.type, ts.factory.createCallExpression(ts.factory.createIdentifier('muMemo'), undefined, [computeFn])));
485
+ return;
486
+ }
487
+ else if (rune === 'vault') {
488
+ let vaultArgs = decl.initializer.arguments.map(a => ts.visitNode(a, visit));
489
+ if (vaultArgs.length === 1) {
490
+ vaultArgs = [
491
+ ts.factory.createStringLiteral(decl.name.text),
492
+ vaultArgs[0]
493
+ ];
494
+ }
495
+ newDecls.push(ts.factory.updateVariableDeclaration(decl, decl.name, decl.exclamationToken, decl.type, ts.factory.createCallExpression(ts.factory.createIdentifier('muVault'), undefined, vaultArgs)));
496
+ return;
497
+ }
498
+ else if (rune === 'qubit') {
499
+ newDecls.push(ts.factory.updateVariableDeclaration(decl, decl.name, decl.exclamationToken, decl.type, ts.factory.createCallExpression(ts.factory.createIdentifier('muQubit'), undefined, decl.initializer.arguments)));
500
+ return;
501
+ }
502
+ else if (rune === 'register') {
503
+ newDecls.push(ts.factory.updateVariableDeclaration(decl, decl.name, decl.exclamationToken, decl.type, ts.factory.createCallExpression(ts.factory.createIdentifier('muRegister'), undefined, decl.initializer.arguments)));
504
+ return;
505
+ }
506
+ }
448
507
  }
449
- else {
450
- newDecls.push(decl);
451
- }
452
- }
453
- else {
454
- newDecls.push(decl);
455
508
  }
509
+ newDecls.push(decl);
456
510
  });
457
511
  if (changed) {
458
512
  return ts.factory.createVariableStatement(node.modifiers, ts.factory.createVariableDeclarationList(newDecls, ts.NodeFlags.Const));
@@ -460,24 +514,20 @@ function transformNaturalReactivity(code, isTs) {
460
514
  }
461
515
  if (ts.isCallExpression(node)) {
462
516
  const expr = node.expression;
463
- if (ts.isIdentifier(expr)) {
464
- const name = expr.text;
465
- const isMacro = name === '$' || name === '$q' || name === '$qr';
466
- if (isMacro) {
467
- // If this call is the initializer of a variable declaration being handled,
468
- // it will be transformed by the VariableStatement logic above.
469
- // However, we want to transform it here if it's used as a STANDALONE expression (e.g. assignment).
470
- const parent = node.parent;
471
- const isDeclarationInit = ts.isVariableDeclaration(parent) && parent.initializer === node;
472
- if (!isDeclarationInit) {
473
- let hookName = 'muState';
474
- if (name === '$q')
475
- hookName = 'muQubit';
476
- else if (name === '$qr')
477
- hookName = 'muRegister';
478
- // Transform $(x) -> muState(x).value
479
- return ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createIdentifier(hookName), undefined, node.arguments), ts.factory.createIdentifier('value'));
480
- }
517
+ const rune = getRuneKind(expr);
518
+ if (rune === 'effect') {
519
+ return ts.factory.createCallExpression(ts.factory.createIdentifier('muEffect'), undefined, node.arguments);
520
+ }
521
+ if (rune === 'state' || rune === 'qubit' || rune === 'register') {
522
+ const parent = node.parent;
523
+ const isDeclarationInit = ts.isVariableDeclaration(parent) && parent.initializer === node;
524
+ if (!isDeclarationInit) {
525
+ let hookName = 'muState';
526
+ if (rune === 'qubit')
527
+ hookName = 'muQubit';
528
+ else if (rune === 'register')
529
+ hookName = 'muRegister';
530
+ return ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createIdentifier(hookName), undefined, node.arguments), ts.factory.createIdentifier('value'));
481
531
  }
482
532
  }
483
533
  }
@@ -494,7 +544,6 @@ function transformNaturalReactivity(code, isTs) {
494
544
  if (ts.isPropertyAssignment(parent) && parent.name === node)
495
545
  return node;
496
546
  // EXEMPTION: Do not add .value if the identifier is a direct argument to a Mulan hook
497
- // These hooks (muGate, muMeasure, etc.) expect the signal wrapper.
498
547
  if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression)) {
499
548
  const hook = parent.expression.text;
500
549
  if (COMMON_HOOKS.includes(hook))