@mulanjs/mulanjs 1.0.1-dev.20260226191839 → 1.0.1-dev.20260227135307

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.
@@ -1,17 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.compileTemplate = void 0;
4
- // --- Compile Function ---
4
+ const ast_parser_1 = require("./ast-parser");
5
5
  const source_map_1 = require("source-map");
6
6
  function compileTemplate(descriptor, scriptResult, scopedId) {
7
- console.log(`[MulanJS Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
7
+ console.log(`[MulanJS Template Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
8
8
  const template = descriptor.template;
9
9
  if (!template)
10
10
  return { code: 'function render() { return ""; }', errors: [] };
11
11
  let html = template.content;
12
12
  const errors = [];
13
- // 1. Parsing Phase (HTML -> AST)
14
- const ast = parse(html, errors);
13
+ // 1. Parsing Phase (HTML -> AST) - Unified Parser
14
+ const ast = (0, ast_parser_1.parse)(html, errors);
15
15
  // 2. Transform Phase (Scopes, Bindings)
16
16
  transform(ast, scriptResult, scopedId, [], descriptor.filename);
17
17
  // 3. Codegen Phase (AST -> JS Function)
@@ -30,38 +30,23 @@ function compileTemplate(descriptor, scriptResult, scopedId) {
30
30
  return ${code};
31
31
  }`;
32
32
  // 4. Source Map Generation
33
- // We map the entire render function to the starting line of the template in the source file
34
33
  let map = undefined;
35
34
  if (descriptor.filename) {
36
35
  const generator = new source_map_1.SourceMapGenerator({
37
- file: descriptor.filename + '.template.js', // Virtual file name
36
+ file: descriptor.filename + '.template.js',
38
37
  });
39
- // Find start line of template in original source
40
- // descriptor.template.start is the index of content start
41
38
  const sourceBefore = descriptor.source.substring(0, template.start);
42
39
  const startLine = sourceBefore.split(/\r?\n/).length;
43
- // Simple mapping: One-to-one mapping isn't easy without AST location tracking
44
- // But we can map the 'return' statement to the template start
45
- // The render function has about 12 lines of preamble.
46
- // Let's just mapping the whole block to the start line for now.
47
- // This ensures the file shows up in devtools.
48
40
  generator.addMapping({
49
41
  generated: { line: 1, column: 0 },
50
- source: descriptor.filename, // This should be the .mujs file path
42
+ source: descriptor.filename,
51
43
  original: { line: startLine, column: 0 }
52
44
  });
53
- // Also map the return line (approx line 12)
54
45
  generator.addMapping({
55
46
  generated: { line: 12, column: 0 },
56
47
  source: descriptor.filename,
57
48
  original: { line: startLine, column: 0 }
58
49
  });
59
- // We must include the "source content" so DevTools can display it!
60
- // IMPORTANT: We want the FULL .mujs content here, so it matches what Webpack sees?
61
- // Actually, if we use the same filename as script-compiler, they might conflict or merge.
62
- // Script compiler uses 'webpack:///...'
63
- // Let's use the same convention.
64
- // But we just use the filename here, compiler.ts will handle the final path adjustment if needed.
65
50
  generator.setSourceContent(descriptor.filename, descriptor.source);
66
51
  map = generator.toString();
67
52
  }
@@ -72,174 +57,6 @@ function compileTemplate(descriptor, scriptResult, scopedId) {
72
57
  };
73
58
  }
74
59
  exports.compileTemplate = compileTemplate;
75
- // --- Parser (Recursive Descent) ---
76
- function parse(template, errors) {
77
- // Root Wrapper
78
- const root = {
79
- type: 'Element',
80
- tag: 'fragment', // Virtual root
81
- props: {},
82
- children: [],
83
- directives: {}
84
- };
85
- const stack = [root];
86
- let cursor = 0;
87
- while (cursor < template.length) {
88
- const char = template[cursor];
89
- if (template.startsWith('<!--', cursor)) {
90
- // Comment <!-- ... -->
91
- const end = template.indexOf('-->', cursor);
92
- if (end === -1)
93
- break;
94
- // Just skip comments or keep them?
95
- // Let's keep them as raw strings to preserve output structure if needed,
96
- // but usually valid HTML comments shouldn't affect structure.
97
- // For now, let's just append them to the previous text node or create a text node.
98
- // Actually, simply ignoring them is safer for logic, but might remove user comments.
99
- // Better: treat as Text so they are emitted as-is.
100
- const content = template.slice(cursor, end + 3);
101
- stack[stack.length - 1].children.push({ type: 'Text', content });
102
- cursor = end + 3;
103
- }
104
- else if (char === '<') {
105
- // Tag
106
- if (template[cursor + 1] === '/') {
107
- // Closing Tag </tag>
108
- const end = template.indexOf('>', cursor);
109
- if (end === -1)
110
- break;
111
- stack.pop();
112
- cursor = end + 1;
113
- }
114
- else {
115
- // Opening Tag <tag ...>
116
- const end = template.indexOf('>', cursor);
117
- if (end === -1)
118
- break;
119
- let tagContent = template.slice(cursor + 1, end);
120
- const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr'].includes(tagContent.split(' ')[0]);
121
- if (tagContent.endsWith('/')) {
122
- tagContent = tagContent.slice(0, -1).trim();
123
- }
124
- const { tag, props, directives } = parseTag(tagContent);
125
- const element = { type: 'Element', tag, props, children: [], directives };
126
- stack[stack.length - 1].children.push(element);
127
- if (!isSelfClosing) {
128
- stack.push(element);
129
- }
130
- cursor = end + 1;
131
- }
132
- }
133
- else if (char === '{' && template[cursor + 1] === '{') {
134
- // Interpolation {{ }}
135
- const end = template.indexOf('}}', cursor);
136
- if (end === -1)
137
- break; // formatting error
138
- const content = template.slice(cursor + 2, end).trim();
139
- stack[stack.length - 1].children.push({ type: 'Interpolation', content });
140
- cursor = end + 2;
141
- }
142
- else {
143
- // Text
144
- let nextTag = template.indexOf('<', cursor);
145
- let nextInterp = template.indexOf('{{', cursor);
146
- let end = template.length;
147
- if (nextTag !== -1 && nextTag < end)
148
- end = nextTag;
149
- if (nextInterp !== -1 && nextInterp < end)
150
- end = nextInterp;
151
- const content = template.slice(cursor, end);
152
- if (content) {
153
- stack[stack.length - 1].children.push({ type: 'Text', content });
154
- }
155
- cursor = end;
156
- }
157
- }
158
- return root;
159
- }
160
- function parseTag(content) {
161
- // console.log('DEBUG: parseTag content:', content);
162
- const parts = content.split(' ');
163
- const tag = parts[0];
164
- const props = {};
165
- const directives = {};
166
- // Resume attribute parsing after tag
167
- const attrStr = content.slice(tag.length).trim();
168
- // console.log('DEBUG: attrStr:', attrStr);
169
- let i = 0;
170
- while (i < attrStr.length) {
171
- // Skip spaces
172
- if (/\s/.test(attrStr[i])) {
173
- i++;
174
- continue;
175
- }
176
- // Find key
177
- const keyStart = i;
178
- while (i < attrStr.length && !/\s|=/.test(attrStr[i])) {
179
- i++;
180
- }
181
- const key = attrStr.slice(keyStart, i);
182
- // Check for value
183
- let value = 'true'; // Default for boolean attributes
184
- // Skip potential spaces before '=' (e.g. class = "foo")
185
- let peek = i;
186
- while (peek < attrStr.length && /\s/.test(attrStr[peek]))
187
- peek++;
188
- if (peek < attrStr.length && attrStr[peek] === '=') {
189
- i = peek + 1; // Move past '='
190
- // Skip spaces after '='
191
- while (i < attrStr.length && /\s/.test(attrStr[i]))
192
- i++;
193
- if (i < attrStr.length && (attrStr[i] === '"' || attrStr[i] === "'")) {
194
- const quote = attrStr[i];
195
- i++; // skip quote
196
- const valStart = i;
197
- while (i < attrStr.length && attrStr[i] !== quote) {
198
- if (attrStr[i] === '\\' && attrStr[i + 1] === quote) {
199
- i += 2;
200
- }
201
- else {
202
- i++;
203
- }
204
- }
205
- value = attrStr.slice(valStart, i);
206
- i++; // skip closing quote
207
- }
208
- else {
209
- // unquoted value
210
- const valStart = i;
211
- while (i < attrStr.length && !/\s/.test(attrStr[i])) {
212
- i++;
213
- }
214
- value = attrStr.slice(valStart, i);
215
- }
216
- }
217
- else {
218
- // Boolean attribute, no value
219
- // i remains at end of key
220
- }
221
- // Store
222
- if (key === 'v-if' || key === 'mu-if') {
223
- directives.vIf = value;
224
- }
225
- else if (key === 'v-for' || key === 'mu-for') {
226
- const parts = value.split(' in ');
227
- if (parts.length < 2) {
228
- console.warn(`[MulanJS Compiler] Warning: Invalid loop expression "${value}". Expected "item in list".`);
229
- directives.vFor = { item: '_item', list: '[]' }; // Fallback with safe identifier
230
- }
231
- else {
232
- const item = parts[0];
233
- const list = parts.slice(1).join(' in '); // Join rest in case list has 'in'
234
- directives.vFor = { item: item.trim(), list: list.trim() };
235
- }
236
- }
237
- else if (key) {
238
- props[key] = value;
239
- }
240
- }
241
- return { tag, props, directives };
242
- }
243
60
  // --- Transformer ---
244
61
  const JS_KEYWORDS = new Set([
245
62
  'true', 'false', 'null', 'undefined', 'this', 'window',
@@ -257,25 +74,20 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
257
74
  var _a;
258
75
  if (node.type === 'Element') {
259
76
  const element = node;
260
- // Scoped ID
261
77
  if (scopedId && element.tag !== 'fragment') {
262
78
  element.props[scopedId] = '';
263
79
  }
264
- // IRON FORTRESS: Detect mu-raw/v-raw for XSS bypass
265
80
  const isRaw = 'mu-raw' in element.props || 'v-raw' in element.props;
266
81
  if (isRaw) {
267
82
  delete element.props['mu-raw'];
268
83
  delete element.props['v-raw'];
269
84
  }
270
- // 0. Update Local Scope for Children (v-for)
271
85
  const childScope = [...localScope];
272
86
  if (element.directives.vFor) {
273
87
  childScope.push(element.directives.vFor.item);
274
88
  }
275
- // Bindings (Attributes)
276
- const propertyBindings = []; // Stores generated side-effect code (props and events)
89
+ const propertyBindings = [];
277
90
  for (const key in element.props) {
278
- // 0. Property Binding: .columns="${state.cols}"
279
91
  if (key.startsWith('.')) {
280
92
  if (!element.props['data-mu-id']) {
281
93
  const id = 'mu_' + Math.random().toString(36).substr(2, 9);
@@ -295,7 +107,6 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
295
107
  propertyBindings.push(`this._b('${id}', '${propName}', ${expr})`);
296
108
  delete element.props[key];
297
109
  }
298
- // 1. Event Handlers: @click="count++", v-on:click="toggle", or onclick="increment()"
299
110
  else if (key.startsWith('@') || key.startsWith('v-on:') || (key.startsWith('on') && key.length > 2)) {
300
111
  if (!element.props['data-mu-id']) {
301
112
  const id = 'mu_' + Math.random().toString(36).substr(2, 9);
@@ -308,39 +119,29 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
308
119
  else if (key.startsWith('v-on:'))
309
120
  eventName = key.slice(5);
310
121
  else
311
- eventName = key.slice(2); // standard 'on' prefix (onclick -> click)
122
+ eventName = key.slice(2);
312
123
  const rawHandler = element.props[key];
313
124
  let bound = processBindings(rawHandler, scriptResult.bindings, localScope);
314
- // Wrap in anonymous function if it looks like a statement or expression with side effects
315
- // Simple heuristic: if it has parentheses and isn't a simple function reference
316
125
  const finalHandler = (bound.includes('(') || bound.includes('=') || bound.includes('++'))
317
126
  ? `($event) => { ${bound} }`
318
127
  : bound;
319
128
  propertyBindings.push(`this._e('${id}', '${eventName}', ${finalHandler})`);
320
129
  delete element.props[key];
321
130
  }
322
- // 2. Standard Attributes Interpolation: class="{{ active }}"
323
131
  else {
324
132
  let rawValue = element.props[key];
325
- // Check for {{ }} -> convert to ${ }
326
133
  if (rawValue.includes('{{')) {
327
- // Capture content and wrap in ${}, trimming whitespace
328
134
  rawValue = rawValue.replace(/\{\{\s*(.*?)\s*\}\}/g, '${$1}');
329
135
  }
330
- // Check for ${ } -> process internal bindings
331
136
  if (rawValue.includes('${')) {
332
- // It's a template literal now
333
137
  element.props[key] = rawValue.replace(/\$\{(.*?)\}/g, (_, expr) => {
334
138
  const bound = processBindings(expr, scriptResult.bindings, localScope);
335
139
  return '${_h(' + bound + ')}';
336
140
  });
337
141
  }
338
142
  }
339
- // ... (rest of attributes)
340
143
  }
341
- // Store side-effects on the node for the Generator to use
342
144
  if (propertyBindings.length > 0) {
343
- // We'll attach it to a temporary property on the AST node
344
145
  element._propertySideEffects = propertyBindings;
345
146
  }
346
147
  element.children.forEach(child => {
@@ -356,13 +157,11 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
356
157
  }
357
158
  else if (node.type === 'Text') {
358
159
  const text = node;
359
- // Support native template literals: ${ expr }
360
160
  if (text.content.includes('${')) {
361
161
  const componentName = filename ? ((_a = filename.split(/[/\\]/).pop()) === null || _a === void 0 ? void 0 : _a.split('.')[0].replace(/\W/g, '')) || 'App' : 'App';
362
162
  const bindPrefix = `window['${componentName}'].`;
363
163
  text.content = text.content.replace(/\$\{(.*?)\}/g, (_, expr) => {
364
164
  let bound = processBindings(expr, scriptResult.bindings, localScope);
365
- // If processBindings added 'this.', replace it with global access for maximum safety in string templates
366
165
  bound = bound.replace(/this\./g, bindPrefix);
367
166
  return '${_h(' + bound + ')}';
368
167
  });
@@ -370,38 +169,26 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
370
169
  }
371
170
  }
372
171
  function processBindings(exp, bindings, localScope) {
373
- // Strategy:
374
- // 1. Identifiers in 'bindings' (Setup API) -> prefix this.
375
- // 2. Identifiers in 'localScope' (v-for) -> keep as is.
376
- // 3. If 'bindings' is empty (Options API), prefix everything NOT in localScope/Keywords/Globals.
377
172
  const isOptionsAPI = !bindings || bindings.length === 0;
378
173
  const bindingSet = new Set(bindings || []);
379
- // Regex for identifiers
380
174
  return exp.replace(/\b([a-zA-Z_$][\w$]*)\b/g, (match, id, offset, str) => {
381
- // 1. Skip if property access (dot before) (e.g. user.name -> user is checked, name is skipped)
382
175
  if (offset > 0 && str[offset - 1] === '.')
383
176
  return match;
384
- // 2. Skip object key (colon after) (e.g. { name: val } -> name skipped)
385
- // Simple check: next char is ':'
386
177
  let i = offset + match.length;
387
178
  while (i < str.length && /\s/.test(str[i]))
388
179
  i++;
389
180
  if (str[i] === ':' && str[i + 1] !== '=')
390
- return match; // : but not := (not that valid in JS contexts usually but safe)
391
- // 3. Skip Keywords / Globals / Local Vars
181
+ return match;
392
182
  if (JS_KEYWORDS.has(id))
393
183
  return match;
394
184
  if (GLOBALS.has(id))
395
185
  return match;
396
186
  if (localScope.includes(id))
397
187
  return match;
398
- // 4. Decision
399
188
  if (isOptionsAPI) {
400
- // Options API: Prefix everything unknown
401
189
  return `this.${id}`;
402
190
  }
403
191
  else {
404
- // Setup API: Only prefix explicit bindings
405
192
  if (bindingSet.has(id)) {
406
193
  return `this.${id}`;
407
194
  }
@@ -413,54 +200,41 @@ function processBindings(exp, bindings, localScope) {
413
200
  function generate(node, bindings, localScope = []) {
414
201
  if (node.type === 'Text') {
415
202
  const text = node;
416
- // Escape backticks
417
203
  return `\`${text.content.replace(/`/g, '\\`')}\``;
418
204
  }
419
205
  if (node.type === 'Interpolation') {
420
206
  const interp = node;
421
207
  const isRaw = node.raw === true;
422
- return `String(_h(${interp.content}, ${isRaw}))`; // Safe cast with Heimdall Shield
208
+ return `String(_h(${interp.content}, ${isRaw}))`;
423
209
  }
424
210
  if (node.type === 'Element') {
425
211
  const element = node;
426
- // Update Local Scope for Children (v-for)
427
212
  const childScope = [...localScope];
428
213
  if (element.directives.vFor) {
429
214
  childScope.push(element.directives.vFor.item);
430
215
  }
431
216
  if (element.tag === 'fragment') {
432
- return element.children.map(c => generate(c, bindings, childScope)).join(' + '); // Root fragment join
217
+ return element.children.map(c => generate(c, bindings, childScope)).join(' + ');
433
218
  }
434
- // Directives
435
219
  if (element.directives.vIf) {
436
220
  const condition = element.directives.vIf;
437
- // Generate ternary: cond ? render() : ''
438
221
  const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
439
222
  const open = `<${element.tag}${genProps(element.props)}>`;
440
223
  const close = `</${element.tag}>`;
441
- // Apply bindings to v-if condition
442
- // FIX: Wrap condition in _s() to unwrap signals (handling 'isOpen' vs 'isOpen.value')
443
224
  return `(_s(${processBindings(condition, bindings, localScope)}) ? \`${open}\` + (${children}) + \`${close}\` : "")`;
444
225
  }
445
226
  if (element.directives.vFor) {
446
227
  const { item, list } = element.directives.vFor;
447
- // list.map(item => render).join('')
448
- // Apply bindings to the list expression
449
- // FIX: Wrap list in _s() to ensure we map over the Array, not the Signal Object
450
228
  const boundList = `_s(${processBindings(list, bindings, localScope)})`;
451
229
  const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
452
230
  const open = `<${element.tag}${genProps(element.props)}>`;
453
231
  const close = `</${element.tag}>`;
454
- // Add safety check: ensure boundList is an Array before mapping
455
232
  return `(Array.isArray(${boundList}) ? ${boundList}.map(${item} => \`${open}\` + (${children}) + \`${close}\`).join('') : "")`;
456
233
  }
457
- // Standard Element
458
234
  const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
459
235
  const open = `<${element.tag}${genProps(element.props)}>`;
460
236
  const close = `</${element.tag}>`;
461
237
  let code = `\`${open}\` + (${children}) + \`${close}\``;
462
- // Inject Property Binding Side Effects using Comma Operator
463
- // (this._b(..), this._b(..), `html`)
464
238
  if (element._propertySideEffects) {
465
239
  const effects = element._propertySideEffects.join(', ');
466
240
  code = `(${effects}, ${code})`;
@@ -474,7 +248,6 @@ function genProps(props) {
474
248
  for (const key in props) {
475
249
  str += ` ${key}`;
476
250
  if (props[key] !== '') {
477
- // Escape double quotes in value
478
251
  const escaped = props[key].replace(/"/g, '&quot;');
479
252
  str += `="${escaped}"`;
480
253
  }
@@ -145,6 +145,8 @@ export class MuComponent {
145
145
  const cache = this._listCaches.get(listId);
146
146
  const newKeys = new Set();
147
147
  const parent = anchorToken.parentNode;
148
+ if (!parent)
149
+ return;
148
150
  // 1. Array Iteration: Match against cache or Create
149
151
  // Use a DocumentFragment to batch new node insertions natively
150
152
  const collectorFrag = document.createDocumentFragment();
@@ -247,6 +249,8 @@ export class MuComponent {
247
249
  }
248
250
  cached.isMounted = true;
249
251
  const parent = anchorToken.parentNode;
252
+ if (!parent)
253
+ return;
250
254
  const targetSibling = anchorToken.nextSibling;
251
255
  nodesToMount.forEach(node => {
252
256
  parent.insertBefore(node, targetSibling);
package/dist/mulan.esm.js CHANGED
@@ -911,6 +911,8 @@ class MuComponent {
911
911
  const cache = this._listCaches.get(listId);
912
912
  const newKeys = new Set();
913
913
  const parent = anchorToken.parentNode;
914
+ if (!parent)
915
+ return;
914
916
  // 1. Array Iteration: Match against cache or Create
915
917
  // Use a DocumentFragment to batch new node insertions natively
916
918
  const collectorFrag = document.createDocumentFragment();
@@ -1013,6 +1015,8 @@ class MuComponent {
1013
1015
  }
1014
1016
  cached.isMounted = true;
1015
1017
  const parent = anchorToken.parentNode;
1018
+ if (!parent)
1019
+ return;
1016
1020
  const targetSibling = anchorToken.nextSibling;
1017
1021
  nodesToMount.forEach(node => {
1018
1022
  parent.insertBefore(node, targetSibling);