@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.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/compiler/compiler.js +90 -0
- package/dist/compiler/script-compiler.js +314 -0
- package/dist/compiler/sfc-parser.js +93 -0
- package/dist/compiler/style-compiler.js +56 -0
- package/dist/compiler/template-compiler.js +442 -0
- package/dist/components/bloch-sphere.js +252 -0
- package/dist/core/component.js +145 -0
- package/dist/core/hooks.js +229 -0
- package/dist/core/quantum.js +284 -0
- package/dist/core/query.js +63 -0
- package/dist/core/reactive.js +105 -0
- package/dist/core/renderer.js +70 -0
- package/dist/core/vault.js +81 -0
- package/dist/index.js +52 -0
- package/dist/mulan.esm.js +1948 -0
- package/dist/mulan.js +215 -0
- package/dist/router/index.js +210 -0
- package/dist/security/sanitizer.js +47 -0
- package/dist/store/index.js +42 -0
- package/dist/types/compiler/compiler.d.ts +7 -0
- package/dist/types/compiler/script-compiler.d.ts +8 -0
- package/dist/types/compiler/sfc-parser.d.ts +21 -0
- package/dist/types/compiler/style-compiler.d.ts +7 -0
- package/dist/types/compiler/template-compiler.d.ts +7 -0
- package/dist/types/compiler.d.ts +7 -0
- package/dist/types/components/bloch-sphere.d.ts +16 -0
- package/dist/types/core/component.d.ts +54 -0
- package/dist/types/core/hooks.d.ts +49 -0
- package/dist/types/core/quantum.d.ts +50 -0
- package/dist/types/core/query.d.ts +14 -0
- package/dist/types/core/reactive.d.ts +21 -0
- package/dist/types/core/renderer.d.ts +4 -0
- package/dist/types/core/vault.d.ts +12 -0
- package/dist/types/index.d.ts +70 -0
- package/dist/types/router/index.d.ts +24 -0
- package/dist/types/script-compiler.d.ts +8 -0
- package/dist/types/security/sanitizer.d.ts +17 -0
- package/dist/types/sfc-parser.d.ts +21 -0
- package/dist/types/store/index.d.ts +10 -0
- package/dist/types/style-compiler.d.ts +7 -0
- package/dist/types/template-compiler.d.ts +7 -0
- package/package.json +64 -0
- package/src/cli/extensions/mulanjs-vscode-1.0.0.vsix +0 -0
- package/src/cli/index.js +600 -0
- package/src/compiler/compiler.ts +102 -0
- package/src/compiler/script-compiler.ts +336 -0
- package/src/compiler/sfc-parser.ts +118 -0
- package/src/compiler/style-compiler.ts +66 -0
- package/src/compiler/template-compiler.ts +519 -0
- package/src/compiler/tsconfig.json +13 -0
- 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, '<').replace(/>/g, '>');
|
|
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, '"');
|
|
515
|
+
str += `="${escaped}"`;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return str;
|
|
519
|
+
}
|
|
@@ -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
|
+
};
|