@mulanjs/mulanjs 1.0.1-dev.20260212143840

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/compiler/compiler.js +90 -0
  4. package/dist/compiler/script-compiler.js +314 -0
  5. package/dist/compiler/sfc-parser.js +93 -0
  6. package/dist/compiler/style-compiler.js +56 -0
  7. package/dist/compiler/template-compiler.js +442 -0
  8. package/dist/components/bloch-sphere.js +252 -0
  9. package/dist/core/component.js +145 -0
  10. package/dist/core/hooks.js +229 -0
  11. package/dist/core/quantum.js +284 -0
  12. package/dist/core/query.js +63 -0
  13. package/dist/core/reactive.js +105 -0
  14. package/dist/core/renderer.js +70 -0
  15. package/dist/core/vault.js +81 -0
  16. package/dist/index.js +52 -0
  17. package/dist/mulan.esm.js +1948 -0
  18. package/dist/mulan.js +215 -0
  19. package/dist/router/index.js +210 -0
  20. package/dist/security/sanitizer.js +47 -0
  21. package/dist/store/index.js +42 -0
  22. package/dist/types/compiler/compiler.d.ts +7 -0
  23. package/dist/types/compiler/script-compiler.d.ts +8 -0
  24. package/dist/types/compiler/sfc-parser.d.ts +21 -0
  25. package/dist/types/compiler/style-compiler.d.ts +7 -0
  26. package/dist/types/compiler/template-compiler.d.ts +7 -0
  27. package/dist/types/compiler.d.ts +7 -0
  28. package/dist/types/components/bloch-sphere.d.ts +16 -0
  29. package/dist/types/core/component.d.ts +54 -0
  30. package/dist/types/core/hooks.d.ts +49 -0
  31. package/dist/types/core/quantum.d.ts +50 -0
  32. package/dist/types/core/query.d.ts +14 -0
  33. package/dist/types/core/reactive.d.ts +21 -0
  34. package/dist/types/core/renderer.d.ts +4 -0
  35. package/dist/types/core/vault.d.ts +12 -0
  36. package/dist/types/index.d.ts +70 -0
  37. package/dist/types/router/index.d.ts +24 -0
  38. package/dist/types/script-compiler.d.ts +8 -0
  39. package/dist/types/security/sanitizer.d.ts +17 -0
  40. package/dist/types/sfc-parser.d.ts +21 -0
  41. package/dist/types/store/index.d.ts +10 -0
  42. package/dist/types/style-compiler.d.ts +7 -0
  43. package/dist/types/template-compiler.d.ts +7 -0
  44. package/package.json +64 -0
  45. package/src/cli/extensions/mulanjs-vscode-1.0.0.vsix +0 -0
  46. package/src/cli/index.js +600 -0
  47. package/src/compiler/compiler.ts +102 -0
  48. package/src/compiler/script-compiler.ts +336 -0
  49. package/src/compiler/sfc-parser.ts +118 -0
  50. package/src/compiler/style-compiler.ts +66 -0
  51. package/src/compiler/template-compiler.ts +519 -0
  52. package/src/compiler/tsconfig.json +13 -0
  53. package/src/loader/index.js +81 -0
@@ -0,0 +1,519 @@
1
+ import { SFCDescriptor } from './sfc-parser';
2
+ import { ScriptCompileResult } from './script-compiler';
3
+
4
+ // --- AST Definitions ---
5
+
6
+ type NodeType = 'Element' | 'Text' | 'Interpolation';
7
+
8
+ type Node = ElementNode | TextNode | InterpolationNode;
9
+
10
+ interface BaseNode {
11
+ type: NodeType;
12
+ }
13
+
14
+ interface ElementNode extends BaseNode {
15
+ type: 'Element';
16
+ tag: string;
17
+ props: Record<string, string>;
18
+ children: Node[];
19
+ directives: {
20
+ vFor?: { item: string; list: string };
21
+ vIf?: string;
22
+ };
23
+ }
24
+
25
+ interface TextNode extends BaseNode {
26
+ type: 'Text';
27
+ content: string;
28
+ }
29
+
30
+ interface InterpolationNode extends BaseNode {
31
+ type: 'Interpolation';
32
+ content: string; // The expression inside {{ }}
33
+ }
34
+
35
+ // --- Compile Function ---
36
+
37
+ export interface TemplateCompileResult {
38
+ code: string;
39
+ errors: string[];
40
+ }
41
+
42
+ export function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptCompileResult, scopedId?: string): TemplateCompileResult {
43
+ console.log(`[MulanJS Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
44
+ const template = descriptor.template;
45
+ if (!template) return { code: 'function render() { return ""; }', errors: [] };
46
+
47
+ let html = template.content;
48
+ const errors: string[] = [];
49
+
50
+ // 1. Parsing Phase (HTML -> AST)
51
+ const ast = parse(html, errors);
52
+
53
+ // 2. Transform Phase (Scopes, Bindings)
54
+ transform(ast, scriptResult, scopedId, [], descriptor.filename);
55
+
56
+ // 3. Codegen Phase (AST -> JS Function)
57
+ const code = generate(ast, scriptResult.bindings || []);
58
+
59
+ return {
60
+ code: `function render() {
61
+ const _s = (v) => (v && typeof v === 'object' && 'value' in v) ? v.value : v;
62
+ const _h = (v, raw) => {
63
+ const val = _s(v);
64
+ if (raw || typeof val !== 'string') return val;
65
+ // IRON FORTRESS: Automatic XSS Shield
66
+ if (typeof Mulan !== 'undefined' && Mulan.Security) {
67
+ return Mulan.Security.sanitize(val);
68
+ }
69
+ return val.replace(/</g, '&lt;').replace(/>/g, '&gt;');
70
+ };
71
+ return ${code};
72
+ }`,
73
+ errors
74
+ };
75
+ }
76
+
77
+ // --- Parser (Recursive Descent) ---
78
+
79
+ function parse(template: string, errors: string[]): ElementNode {
80
+ // Root Wrapper
81
+ const root: ElementNode = {
82
+ type: 'Element',
83
+ tag: 'fragment', // Virtual root
84
+ props: {},
85
+ children: [],
86
+ directives: {}
87
+ };
88
+
89
+ const stack: ElementNode[] = [root];
90
+ let cursor = 0;
91
+
92
+ while (cursor < template.length) {
93
+ const char = template[cursor];
94
+
95
+ if (template.startsWith('<!--', cursor)) {
96
+ // Comment <!-- ... -->
97
+ const end = template.indexOf('-->', cursor);
98
+ if (end === -1) break;
99
+ // Just skip comments or keep them?
100
+ // Let's keep them as raw strings to preserve output structure if needed,
101
+ // but usually valid HTML comments shouldn't affect structure.
102
+ // For now, let's just append them to the previous text node or create a text node.
103
+ // Actually, simply ignoring them is safer for logic, but might remove user comments.
104
+ // Better: treat as Text so they are emitted as-is.
105
+ const content = template.slice(cursor, end + 3);
106
+ stack[stack.length - 1].children.push({ type: 'Text', content });
107
+ cursor = end + 3;
108
+ } else if (char === '<') {
109
+ // Tag
110
+ if (template[cursor + 1] === '/') {
111
+ // Closing Tag </tag>
112
+ const end = template.indexOf('>', cursor);
113
+ if (end === -1) break;
114
+ stack.pop();
115
+ cursor = end + 1;
116
+ } else {
117
+ // Opening Tag <tag ...>
118
+ const end = template.indexOf('>', cursor);
119
+ if (end === -1) break;
120
+
121
+ let tagContent = template.slice(cursor + 1, end);
122
+ const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr'].includes(tagContent.split(' ')[0]);
123
+
124
+ if (tagContent.endsWith('/')) {
125
+ tagContent = tagContent.slice(0, -1).trim();
126
+ }
127
+
128
+ const { tag, props, directives } = parseTag(tagContent);
129
+
130
+ const element: ElementNode = { type: 'Element', tag, props, children: [], directives };
131
+ stack[stack.length - 1].children.push(element);
132
+
133
+ if (!isSelfClosing) {
134
+ stack.push(element);
135
+ }
136
+
137
+ cursor = end + 1;
138
+ }
139
+ } else if (char === '{' && template[cursor + 1] === '{') {
140
+ // Interpolation {{ }}
141
+ const end = template.indexOf('}}', cursor);
142
+ if (end === -1) break; // formatting error
143
+
144
+ const content = template.slice(cursor + 2, end).trim();
145
+ stack[stack.length - 1].children.push({ type: 'Interpolation', content });
146
+ cursor = end + 2;
147
+ } else {
148
+ // Text
149
+ let nextTag = template.indexOf('<', cursor);
150
+ let nextInterp = template.indexOf('{{', cursor);
151
+
152
+ let end = template.length;
153
+ if (nextTag !== -1 && nextTag < end) end = nextTag;
154
+ if (nextInterp !== -1 && nextInterp < end) end = nextInterp;
155
+
156
+ const content = template.slice(cursor, end);
157
+ if (content) {
158
+ stack[stack.length - 1].children.push({ type: 'Text', content });
159
+ }
160
+ cursor = end;
161
+ }
162
+ }
163
+
164
+ return root;
165
+ }
166
+
167
+ function parseTag(content: string) {
168
+ // console.log('DEBUG: parseTag content:', content);
169
+ const parts = content.split(' ');
170
+ const tag = parts[0];
171
+ const props: Record<string, string> = {};
172
+ const directives: ElementNode['directives'] = {};
173
+
174
+ // Resume attribute parsing after tag
175
+ const attrStr = content.slice(tag.length).trim();
176
+ // console.log('DEBUG: attrStr:', attrStr);
177
+
178
+ let i = 0;
179
+ while (i < attrStr.length) {
180
+ // Skip spaces
181
+ if (/\s/.test(attrStr[i])) {
182
+ i++;
183
+ continue;
184
+ }
185
+
186
+ // Find key
187
+ const keyStart = i;
188
+ while (i < attrStr.length && !/\s|=/.test(attrStr[i])) {
189
+ i++;
190
+ }
191
+ const key = attrStr.slice(keyStart, i);
192
+
193
+ // Check for value
194
+ let value = 'true'; // Default for boolean attributes
195
+
196
+ // Skip potential spaces before '=' (e.g. class = "foo")
197
+ let peek = i;
198
+ while (peek < attrStr.length && /\s/.test(attrStr[peek])) peek++;
199
+
200
+ if (peek < attrStr.length && attrStr[peek] === '=') {
201
+ i = peek + 1; // Move past '='
202
+
203
+ // Skip spaces after '='
204
+ while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
205
+
206
+ if (i < attrStr.length && (attrStr[i] === '"' || attrStr[i] === "'")) {
207
+ const quote = attrStr[i];
208
+ i++; // skip quote
209
+ const valStart = i;
210
+ while (i < attrStr.length && attrStr[i] !== quote) {
211
+ if (attrStr[i] === '\\' && attrStr[i + 1] === quote) {
212
+ i += 2;
213
+ } else {
214
+ i++;
215
+ }
216
+ }
217
+ value = attrStr.slice(valStart, i);
218
+ i++; // skip closing quote
219
+ } else {
220
+ // unquoted value
221
+ const valStart = i;
222
+ while (i < attrStr.length && !/\s/.test(attrStr[i])) {
223
+ i++;
224
+ }
225
+ value = attrStr.slice(valStart, i);
226
+ }
227
+ } else {
228
+ // Boolean attribute, no value
229
+ // i remains at end of key
230
+ }
231
+
232
+ // Store
233
+ if (key === 'v-if' || key === 'mu-if') {
234
+ directives.vIf = value;
235
+ } else if (key === 'v-for' || key === 'mu-for') {
236
+ const parts = value.split(' in ');
237
+ if (parts.length < 2) {
238
+ console.warn(`[MulanJS Compiler] Warning: Invalid loop expression "${value}". Expected "item in list".`);
239
+ directives.vFor = { item: '_item', list: '[]' }; // Fallback with safe identifier
240
+ } else {
241
+ const item = parts[0];
242
+ const list = parts.slice(1).join(' in '); // Join rest in case list has 'in'
243
+ directives.vFor = { item: item.trim(), list: list.trim() };
244
+ }
245
+ } else if (key) {
246
+ props[key] = value;
247
+ }
248
+ }
249
+
250
+ return { tag, props, directives };
251
+ }
252
+
253
+ // --- Transformer ---
254
+
255
+ const JS_KEYWORDS = new Set([
256
+ 'true', 'false', 'null', 'undefined', 'this', 'window',
257
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'return',
258
+ 'var', 'let', 'const', 'new', 'function', 'class', 'import', 'export',
259
+ 'typeof', 'instanceof', 'void', 'delete', 'in', 'of', 'try', 'catch', 'throw', 'finally',
260
+ 'debugger', 'super', 'extends', 'async', 'await', 'yield'
261
+ ]);
262
+
263
+ const GLOBALS = new Set([
264
+ 'Math', 'Date', 'String', 'Number', 'Boolean', 'Object', 'Array', 'JSON',
265
+ 'RegExp', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'Symbol', 'Error',
266
+ 'console', 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'encodeURIComponent', 'decodeURIComponent'
267
+ ]);
268
+
269
+ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: string, localScope: string[] = [], filename?: string) {
270
+ if (node.type === 'Element') {
271
+ const element = node as ElementNode;
272
+ // Scoped ID
273
+ if (scopedId && element.tag !== 'fragment') {
274
+ element.props[scopedId] = '';
275
+ }
276
+
277
+ // IRON FORTRESS: Detect mu-raw/v-raw for XSS bypass
278
+ const isRaw = 'mu-raw' in element.props || 'v-raw' in element.props;
279
+ if (isRaw) {
280
+ delete element.props['mu-raw'];
281
+ delete element.props['v-raw'];
282
+ }
283
+
284
+ // 0. Update Local Scope for Children (v-for)
285
+ const childScope = [...localScope];
286
+ if (element.directives.vFor) {
287
+ childScope.push(element.directives.vFor.item);
288
+ }
289
+
290
+ // Bindings (Attributes)
291
+ const propertyBindings: string[] = []; // Stores generated side-effect code (props and events)
292
+
293
+ for (const key in element.props) {
294
+ // 0. Property Binding: .columns="${state.cols}"
295
+ if (key.startsWith('.')) {
296
+ if (!element.props['data-mu-id']) {
297
+ const id = 'mu_' + Math.random().toString(36).substr(2, 9);
298
+ element.props['data-mu-id'] = id;
299
+ }
300
+ const id = element.props['data-mu-id'];
301
+ const propName = key.slice(1);
302
+ const rawValue = element.props[key];
303
+
304
+ let expr = "''";
305
+ if (rawValue.startsWith('${') && rawValue.endsWith('}')) {
306
+ const inner = rawValue.slice(2, -1);
307
+ expr = processBindings(inner, scriptResult.bindings, localScope);
308
+ } else {
309
+ expr = JSON.stringify(rawValue);
310
+ }
311
+ propertyBindings.push(`this._b('${id}', '${propName}', ${expr})`);
312
+ delete element.props[key];
313
+ }
314
+ // 1. Event Handlers: @click="count++", v-on:click="toggle", or onclick="increment()"
315
+ else if (key.startsWith('@') || key.startsWith('v-on:') || (key.startsWith('on') && key.length > 2)) {
316
+ if (!element.props['data-mu-id']) {
317
+ const id = 'mu_' + Math.random().toString(36).substr(2, 9);
318
+ element.props['data-mu-id'] = id;
319
+ }
320
+ const id = element.props['data-mu-id'];
321
+
322
+ let eventName = '';
323
+ if (key.startsWith('@')) eventName = key.slice(1);
324
+ else if (key.startsWith('v-on:')) eventName = key.slice(5);
325
+ else eventName = key.slice(2); // standard 'on' prefix (onclick -> click)
326
+
327
+ const rawHandler = element.props[key];
328
+
329
+ let bound = processBindings(rawHandler, scriptResult.bindings, localScope);
330
+ // Wrap in anonymous function if it looks like a statement or expression with side effects
331
+ // Simple heuristic: if it has parentheses and isn't a simple function reference
332
+ const finalHandler = (bound.includes('(') || bound.includes('=') || bound.includes('++'))
333
+ ? `($event) => { ${bound} }`
334
+ : bound;
335
+
336
+ propertyBindings.push(`this._e('${id}', '${eventName}', ${finalHandler})`);
337
+ delete element.props[key];
338
+ }
339
+ // 2. Standard Attributes Interpolation: class="{{ active }}"
340
+ else {
341
+ let rawValue = element.props[key];
342
+ // Check for {{ }} -> convert to ${ }
343
+ if (rawValue.includes('{{')) {
344
+ // Capture content and wrap in ${}, trimming whitespace
345
+ rawValue = rawValue.replace(/\{\{\s*(.*?)\s*\}\}/g, '${$1}');
346
+ }
347
+
348
+ // Check for ${ } -> process internal bindings
349
+ if (rawValue.includes('${')) {
350
+ // It's a template literal now
351
+ element.props[key] = rawValue.replace(/\$\{(.*?)\}/g, (_, expr) => {
352
+ const bound = processBindings(expr, scriptResult.bindings, localScope);
353
+ return '${_h(' + bound + ')}';
354
+ });
355
+ }
356
+ }
357
+ // ... (rest of attributes)
358
+ }
359
+
360
+ // Store side-effects on the node for the Generator to use
361
+ if (propertyBindings.length > 0) {
362
+ // We'll attach it to a temporary property on the AST node
363
+ (element as any)._propertySideEffects = propertyBindings;
364
+ }
365
+
366
+ element.children.forEach(child => {
367
+ if (isRaw && (child.type === 'Interpolation' || child.type === 'Text')) {
368
+ (child as any).raw = true;
369
+ }
370
+ transform(child, scriptResult, scopedId, childScope, filename);
371
+ });
372
+ } else if (node.type === 'Interpolation') {
373
+ const interpolation = node as InterpolationNode;
374
+ interpolation.content = processBindings(interpolation.content, scriptResult.bindings, localScope);
375
+ } else if (node.type === 'Text') {
376
+ const text = node as TextNode;
377
+ // Support native template literals: ${ expr }
378
+ if (text.content.includes('${')) {
379
+ const componentName = filename ? filename.split(/[/\\]/).pop()?.split('.')[0].replace(/\W/g, '') || 'App' : 'App';
380
+ const bindPrefix = `window['${componentName}'].`;
381
+
382
+ text.content = text.content.replace(/\$\{(.*?)\}/g, (_, expr) => {
383
+ let bound = processBindings(expr, scriptResult.bindings, localScope);
384
+ // If processBindings added 'this.', replace it with global access for maximum safety in string templates
385
+ bound = bound.replace(/this\./g, bindPrefix);
386
+ return '${_h(' + bound + ')}';
387
+ });
388
+ }
389
+ }
390
+ }
391
+
392
+ function processBindings(exp: string, bindings: string[] | undefined, localScope: string[]) {
393
+ // Strategy:
394
+ // 1. Identifiers in 'bindings' (Setup API) -> prefix this.
395
+ // 2. Identifiers in 'localScope' (v-for) -> keep as is.
396
+ // 3. If 'bindings' is empty (Options API), prefix everything NOT in localScope/Keywords/Globals.
397
+
398
+ const isOptionsAPI = !bindings || bindings.length === 0;
399
+ const bindingSet = new Set(bindings || []);
400
+
401
+ // Regex for identifiers
402
+ return exp.replace(/\b([a-zA-Z_$][\w$]*)\b/g, (match, id, offset, str) => {
403
+ // 1. Skip if property access (dot before) (e.g. user.name -> user is checked, name is skipped)
404
+ if (offset > 0 && str[offset - 1] === '.') return match;
405
+
406
+ // 2. Skip object key (colon after) (e.g. { name: val } -> name skipped)
407
+ // Simple check: next char is ':'
408
+ let i = offset + match.length;
409
+ while (i < str.length && /\s/.test(str[i])) i++;
410
+ if (str[i] === ':' && str[i + 1] !== '=') return match; // : but not := (not that valid in JS contexts usually but safe)
411
+
412
+ // 3. Skip Keywords / Globals / Local Vars
413
+ if (JS_KEYWORDS.has(id)) return match;
414
+ if (GLOBALS.has(id)) return match;
415
+ if (localScope.includes(id)) return match;
416
+
417
+ // 4. Decision
418
+ if (isOptionsAPI) {
419
+ // Options API: Prefix everything unknown
420
+ return `this.${id}`;
421
+ } else {
422
+ // Setup API: Only prefix explicit bindings
423
+ if (bindingSet.has(id)) {
424
+ return `this.${id}`;
425
+ }
426
+ }
427
+
428
+ return match;
429
+ });
430
+ }
431
+
432
+ // --- Generator ---
433
+
434
+ function generate(node: Node, bindings: string[], localScope: string[] = []): string {
435
+ if (node.type === 'Text') {
436
+ const text = node as TextNode;
437
+ // Escape backticks
438
+ return `\`${text.content.replace(/`/g, '\\`')}\``;
439
+ }
440
+
441
+ if (node.type === 'Interpolation') {
442
+ const interp = node as InterpolationNode;
443
+ const isRaw = (node as any).raw === true;
444
+ return `String(_h(${interp.content}, ${isRaw}))`; // Safe cast with Heimdall Shield
445
+ }
446
+
447
+ if (node.type === 'Element') {
448
+ const element = node as ElementNode;
449
+
450
+ // Update Local Scope for Children (v-for)
451
+ const childScope = [...localScope];
452
+ if (element.directives.vFor) {
453
+ childScope.push(element.directives.vFor.item);
454
+ }
455
+
456
+ if (element.tag === 'fragment') {
457
+ return element.children.map(c => generate(c, bindings, childScope)).join(' + '); // Root fragment join
458
+ }
459
+
460
+ // Directives
461
+ if (element.directives.vIf) {
462
+ const condition = element.directives.vIf;
463
+ // Generate ternary: cond ? render() : ''
464
+ const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
465
+ const open = `<${element.tag}${genProps(element.props)}>`;
466
+ const close = `</${element.tag}>`;
467
+ // Apply bindings to v-if condition
468
+ // FIX: Wrap condition in _s() to unwrap signals (handling 'isOpen' vs 'isOpen.value')
469
+ return `(_s(${processBindings(condition, bindings, localScope)}) ? \`${open}\` + (${children}) + \`${close}\` : "")`;
470
+ }
471
+
472
+ if (element.directives.vFor) {
473
+ const { item, list } = element.directives.vFor;
474
+ // list.map(item => render).join('')
475
+
476
+ // Apply bindings to the list expression
477
+ // FIX: Wrap list in _s() to ensure we map over the Array, not the Signal Object
478
+ const boundList = `_s(${processBindings(list, bindings, localScope)})`;
479
+
480
+ const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
481
+ const open = `<${element.tag}${genProps(element.props)}>`;
482
+ const close = `</${element.tag}>`;
483
+
484
+ // Add safety check: ensure boundList is an Array before mapping
485
+ return `(Array.isArray(${boundList}) ? ${boundList}.map(${item} => \`${open}\` + (${children}) + \`${close}\`).join('') : "")`;
486
+ }
487
+
488
+ // Standard Element
489
+ const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
490
+ const open = `<${element.tag}${genProps(element.props)}>`;
491
+ const close = `</${element.tag}>`;
492
+
493
+ let code = `\`${open}\` + (${children}) + \`${close}\``;
494
+
495
+ // Inject Property Binding Side Effects using Comma Operator
496
+ // (this._b(..), this._b(..), `html`)
497
+ if ((element as any)._propertySideEffects) {
498
+ const effects = (element as any)._propertySideEffects.join(', ');
499
+ code = `(${effects}, ${code})`;
500
+ }
501
+
502
+ return code;
503
+ }
504
+
505
+ return '""';
506
+ }
507
+
508
+ function genProps(props: Record<string, string>) {
509
+ let str = '';
510
+ for (const key in props) {
511
+ str += ` ${key}`;
512
+ if (props[key] !== '') {
513
+ // Escape double quotes in value
514
+ const escaped = props[key].replace(/"/g, '&quot;');
515
+ str += `="${escaped}"`;
516
+ }
517
+ }
518
+ return str;
519
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "target": "es2019",
6
+ "outDir": "../../dist/compiler",
7
+ "moduleResolution": "node",
8
+ "esModuleInterop": true
9
+ },
10
+ "include": [
11
+ "./**/*"
12
+ ]
13
+ }
@@ -0,0 +1,81 @@
1
+
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const { pathToFileURL } = require('url');
5
+
6
+ module.exports = function (content) {
7
+ const callback = this.async();
8
+ const filePath = this.resourcePath;
9
+ const context = this.context || path.dirname(filePath);
10
+
11
+ // --- External File Processor ---
12
+ // Inlines <script src="..."> and <style src="..."> before compilation
13
+ const inlineExternalFiles = (source) => {
14
+ // Regex to match <tag ... src="...">... </tag> or <tag ... src="..." />
15
+ // Captures: 1=TagName, 2=Attributes before src, 3=Quote, 4=Path, 5=Attributes after src
16
+ const tagRegex = /<(script|style)([\s\S]*?)src=(["'])(.*?)\3([\s\S]*?)>(?:<\/\1>|\s*\/>)?/gi;
17
+
18
+ return source.replace(tagRegex, (match, tagName, attrsBefore, quote, srcPath, attrsAfter) => {
19
+ try {
20
+ // Resolve path
21
+ const absolutePath = path.resolve(context, srcPath);
22
+
23
+ // Add as dependency for Webpack HMR/Watch
24
+ this.addDependency(absolutePath);
25
+
26
+ // Read content
27
+ const externalContent = fs.readFileSync(absolutePath, 'utf-8');
28
+
29
+ console.log(`[MulanJS Loader] Inlined external ${tagName}: ${srcPath}`);
30
+
31
+ // Auto-detect TypeScript env
32
+ let finalAttrs = attrsBefore + attrsAfter;
33
+ if (srcPath.endsWith('.ts') && !finalAttrs.includes('lang=')) {
34
+ finalAttrs += ' lang="ts"';
35
+ }
36
+
37
+ // Return new tag with content, removing the src attribute
38
+ return `<${tagName}${finalAttrs}>\n${externalContent}\n</${tagName}>`;
39
+ } catch (err) {
40
+ this.emitError(new Error(`[MulanJS Loader] Failed to load external file: ${srcPath}\n${err.message}`));
41
+ return match; // Return original on error to likely fail later or show error
42
+ }
43
+ });
44
+ };
45
+
46
+ try {
47
+ content = inlineExternalFiles(content);
48
+ } catch (e) {
49
+ callback(e);
50
+ return;
51
+ }
52
+
53
+ // Resolve path to the compiled compiler module (ESM)
54
+ // Assuming structure: dist/loader/index.js and dist/compiler/compiler.js
55
+ // OR src/loader/index.js and dist/compiler/compiler.js
56
+ // We'll target the dist folder for the compiler.
57
+ const compilerRef = path.resolve(__dirname, '../../dist/compiler/compiler.js');
58
+ const compilerUrl = pathToFileURL(compilerRef).href;
59
+
60
+ // Use dynamic import to load ESM compiler from CommonJS loader
61
+ import(compilerUrl).then(compiler => {
62
+ try {
63
+ const result = compiler.compileSFC(content, filePath);
64
+
65
+ if (result.errors && result.errors.length > 0) {
66
+ this.emitError(new Error(result.errors.join('\n')));
67
+ }
68
+
69
+ // Pass source map to Webpack if available
70
+ const map = result.map ? JSON.parse(result.map) : null;
71
+ callback(null, result.code, map);
72
+ } catch (e) {
73
+ callback(e);
74
+ }
75
+ }).catch(err => {
76
+ // Fallback or helpful error
77
+ console.error(`[MulanJS Loader] Failed to load compiler from ${compilerUrl}`);
78
+ console.error(`Make sure to run 'npm run build' to generate the compiler.`);
79
+ callback(err);
80
+ });
81
+ };