@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,336 @@
|
|
|
1
|
+
|
|
2
|
+
import { SFCDescriptor, SFCBlock } from './sfc-parser';
|
|
3
|
+
import * as ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
export interface ScriptCompileResult {
|
|
6
|
+
code: string;
|
|
7
|
+
bindings?: string[]; // Variables exposed to template
|
|
8
|
+
errors: string[];
|
|
9
|
+
map?: string; // Source Map
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function compileScript(descriptor: SFCDescriptor): ScriptCompileResult {
|
|
13
|
+
const script = descriptor.script;
|
|
14
|
+
if (!script) return { code: 'export default {}', errors: [] };
|
|
15
|
+
|
|
16
|
+
const isSetup = !!script.attrs.setup;
|
|
17
|
+
const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx';
|
|
18
|
+
|
|
19
|
+
if (isSetup) {
|
|
20
|
+
return compileSetupScript(script, isTs, descriptor);
|
|
21
|
+
} else {
|
|
22
|
+
// Standard Options API - just pass through, assuming it has export default
|
|
23
|
+
return { code: script.content, errors: [] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Global list of Mulan hooks for auto-imports and reactivity exemption
|
|
28
|
+
const COMMON_HOOKS = ['muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo', 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent'];
|
|
29
|
+
|
|
30
|
+
function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDescriptor): ScriptCompileResult {
|
|
31
|
+
// 1. Pre-process: Natural Reactivity Transformation ($ syntax)
|
|
32
|
+
// We do this BEFORE extraction so that the rest of the compiler sees standard 'muState' code.
|
|
33
|
+
const transformedContent = transformNaturalReactivity(script.content, isTs);
|
|
34
|
+
|
|
35
|
+
// 2. Parse the TRANSFORMED code
|
|
36
|
+
const sourceFile = ts.createSourceFile(
|
|
37
|
+
'script.' + (isTs ? 'ts' : 'js'),
|
|
38
|
+
transformedContent,
|
|
39
|
+
ts.ScriptTarget.Latest,
|
|
40
|
+
true
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const imports: string[] = [];
|
|
44
|
+
const statements: string[] = [];
|
|
45
|
+
const bindings: string[] = [];
|
|
46
|
+
|
|
47
|
+
sourceFile.statements.forEach(stmt => {
|
|
48
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
49
|
+
imports.push(transformedContent.substring(stmt.pos, stmt.end).trim());
|
|
50
|
+
} else {
|
|
51
|
+
statements.push(transformedContent.substring(stmt.pos, stmt.end).trim());
|
|
52
|
+
|
|
53
|
+
// Extract top-level declarations for template exposure
|
|
54
|
+
if (ts.isVariableStatement(stmt)) {
|
|
55
|
+
stmt.declarationList.declarations.forEach(decl => {
|
|
56
|
+
if (ts.isIdentifier(decl.name)) {
|
|
57
|
+
bindings.push(decl.name.text);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
} else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
61
|
+
bindings.push(stmt.name.text);
|
|
62
|
+
} else if (ts.isClassDeclaration(stmt) && stmt.name) {
|
|
63
|
+
bindings.push(stmt.name.text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Reconstruct
|
|
69
|
+
// We import defineComponent from mulanjs if not present?
|
|
70
|
+
// Ideally user imports what they need, but for 'setup' wrapping we need defineComponent.
|
|
71
|
+
// Let's assume user might not have imported it, so we import it as _defineComponent to avoid collision.
|
|
72
|
+
|
|
73
|
+
// Check if defineComponent is imported
|
|
74
|
+
const hasDefineComponent = imports.some(i => i.includes('defineComponent'));
|
|
75
|
+
const bootImports = hasDefineComponent ? '' : `import { defineComponent as _defineComponent } from 'mulanjs';`;
|
|
76
|
+
|
|
77
|
+
// ADDED: Check if we introduced 'muState' but it wasn't imported (because user used $)
|
|
78
|
+
const usesMuState = transformedContent.includes('muState');
|
|
79
|
+
const hasMuStateImport = imports.some(i => i.includes('muState') || i.includes('mulanjs'));
|
|
80
|
+
let autoImports = (usesMuState && !hasMuStateImport) ? [`muState`] : [];
|
|
81
|
+
|
|
82
|
+
// Auto-import common hooks
|
|
83
|
+
COMMON_HOOKS.forEach(hook => {
|
|
84
|
+
if (transformedContent.includes(hook) && !imports.some(i => i.includes(hook))) {
|
|
85
|
+
// Avoid duplicates in autoImports
|
|
86
|
+
if (!autoImports.includes(hook)) {
|
|
87
|
+
autoImports.push(hook);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const autoImportStmt = autoImports.length > 0
|
|
93
|
+
? `import { ${autoImports.join(', ')} } from 'mulanjs';`
|
|
94
|
+
: '';
|
|
95
|
+
|
|
96
|
+
const filename = descriptor.filename;
|
|
97
|
+
const componentName = filename ? filename.split(/[/\\]/).pop()?.split('.')[0].replace(/\W/g, '') || 'App' : 'App';
|
|
98
|
+
|
|
99
|
+
const bindingString = bindings.length > 0
|
|
100
|
+
? `
|
|
101
|
+
const exposed = { ${bindings.join(', ')} };
|
|
102
|
+
if (typeof window !== 'undefined') {
|
|
103
|
+
window["${componentName}"] = exposed;
|
|
104
|
+
}
|
|
105
|
+
return exposed;
|
|
106
|
+
`
|
|
107
|
+
: 'return {};';
|
|
108
|
+
|
|
109
|
+
const finalCode = `
|
|
110
|
+
${bootImports}
|
|
111
|
+
${autoImportStmt}
|
|
112
|
+
${imports.join('\n')}
|
|
113
|
+
|
|
114
|
+
export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
115
|
+
setup() {
|
|
116
|
+
${statements.join('\n')}
|
|
117
|
+
${bindingString}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
if (isTs) {
|
|
123
|
+
// Calculate line offset for Source Maps (Padding Strategy)
|
|
124
|
+
// Find how many newlines are before the script content in the original source
|
|
125
|
+
// This is a rough estimation but better than nothing for 1.0 dev
|
|
126
|
+
const linesBefore = descriptor.source.substring(0, script.start).split('\n').length - 1;
|
|
127
|
+
const padding = '\n'.repeat(linesBefore);
|
|
128
|
+
|
|
129
|
+
// Pad the content so line numbers match original file
|
|
130
|
+
// Note: This only works perfectly if we are transpiling the RAW content.
|
|
131
|
+
// Since we are extracting/transforming (Natural Reactivity), line numbers might shift.
|
|
132
|
+
// For accurate 1-to-1 mapping, we rely on TS source maps of the 'finalCode'.
|
|
133
|
+
// To make 'finalCode' map back to 'filename', we need to construct it carefully.
|
|
134
|
+
|
|
135
|
+
// Transpile extracted TS code to JS
|
|
136
|
+
const result = ts.transpileModule(finalCode, {
|
|
137
|
+
compilerOptions: {
|
|
138
|
+
module: ts.ModuleKind.ESNext,
|
|
139
|
+
target: ts.ScriptTarget.ES2019,
|
|
140
|
+
sourceMap: true, // ENABLE SOURCE MAPS
|
|
141
|
+
inlineSources: true,
|
|
142
|
+
sourceRoot: '/',
|
|
143
|
+
},
|
|
144
|
+
fileName: filename // Important for source map
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Fix: Remove the //# sourceMappingURL= comment added by TS
|
|
148
|
+
const codeWithoutMapComment = result.outputText.replace(/\/\/# sourceMappingURL=.*$/gm, '');
|
|
149
|
+
|
|
150
|
+
// Enhance Source Map to show ORIGINAL .mujs content
|
|
151
|
+
let finalMap = result.sourceMapText;
|
|
152
|
+
if (finalMap) {
|
|
153
|
+
try {
|
|
154
|
+
const mapObj = JSON.parse(finalMap);
|
|
155
|
+
|
|
156
|
+
// Try to get relative path from 'src' to preserve folder structure in DevTools
|
|
157
|
+
const normalizedPath = filename.replace(/\\/g, '/');
|
|
158
|
+
const srcIndex = normalizedPath.indexOf('/src/');
|
|
159
|
+
|
|
160
|
+
// If inside src, use path starting from src. Else just basename.
|
|
161
|
+
// Example: c:/.../src/pages/Home.mujs -> src/pages/Home.mujs
|
|
162
|
+
let relativePath = srcIndex !== -1
|
|
163
|
+
? normalizedPath.substring(srcIndex + 1)
|
|
164
|
+
: filename.split(/[/\\]/).pop() || 'unknown.mujs';
|
|
165
|
+
|
|
166
|
+
// Use relative path directly. Webpack will handle the namespace.
|
|
167
|
+
mapObj.sources = [relativePath];
|
|
168
|
+
|
|
169
|
+
mapObj.sourcesContent = [descriptor.source];
|
|
170
|
+
finalMap = JSON.stringify(mapObj);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.warn('[MulanJS] Failed to patch source map:', e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
code: codeWithoutMapComment,
|
|
178
|
+
bindings,
|
|
179
|
+
errors: [],
|
|
180
|
+
map: finalMap // Return the enhanced map
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
code: finalCode,
|
|
186
|
+
bindings,
|
|
187
|
+
errors: [],
|
|
188
|
+
map: undefined
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Natural Reactivity Transformer ---
|
|
193
|
+
|
|
194
|
+
function transformNaturalReactivity(code: string, isTs: boolean): string {
|
|
195
|
+
// If no '$(' or '$q(' or '$qr(' usage, return original code for safety/speed
|
|
196
|
+
if (!code.includes('$(') && !code.includes('$q(') && !code.includes('$qr(')) return code;
|
|
197
|
+
|
|
198
|
+
const sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true);
|
|
199
|
+
const reactiveVars = new Set<string>();
|
|
200
|
+
const quantumVars = new Set<string>();
|
|
201
|
+
const registerVars = new Set<string>();
|
|
202
|
+
|
|
203
|
+
// Pass 1: Identification
|
|
204
|
+
function findReactiveVars(node: ts.Node) {
|
|
205
|
+
if (ts.isVariableStatement(node)) {
|
|
206
|
+
node.declarationList.declarations.forEach(decl => {
|
|
207
|
+
if (decl.initializer && ts.isCallExpression(decl.initializer)) {
|
|
208
|
+
const expr = decl.initializer.expression;
|
|
209
|
+
if (ts.isIdentifier(expr)) {
|
|
210
|
+
if (expr.text === '$') {
|
|
211
|
+
if (ts.isIdentifier(decl.name)) reactiveVars.add(decl.name.text);
|
|
212
|
+
} else if (expr.text === '$q') {
|
|
213
|
+
if (ts.isIdentifier(decl.name)) quantumVars.add(decl.name.text);
|
|
214
|
+
} else if (expr.text === '$qr') {
|
|
215
|
+
if (ts.isIdentifier(decl.name)) registerVars.add(decl.name.text);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
ts.forEachChild(node, findReactiveVars);
|
|
222
|
+
}
|
|
223
|
+
findReactiveVars(sourceFile);
|
|
224
|
+
|
|
225
|
+
if (reactiveVars.size === 0 && quantumVars.size === 0 && registerVars.size === 0) return code;
|
|
226
|
+
|
|
227
|
+
// Pass 2: Transformation
|
|
228
|
+
const transformerFactory: ts.TransformerFactory<ts.SourceFile> = (context) => {
|
|
229
|
+
return (rootNode) => {
|
|
230
|
+
function visit(node: ts.Node): ts.Node {
|
|
231
|
+
if (ts.isVariableStatement(node)) {
|
|
232
|
+
const newDecls: ts.VariableDeclaration[] = [];
|
|
233
|
+
let changed = false;
|
|
234
|
+
|
|
235
|
+
node.declarationList.declarations.forEach(decl => {
|
|
236
|
+
if (ts.isIdentifier(decl.name)) {
|
|
237
|
+
const name = decl.name.text;
|
|
238
|
+
if (reactiveVars.has(name) || quantumVars.has(name) || registerVars.has(name)) {
|
|
239
|
+
changed = true;
|
|
240
|
+
let hookName = 'muState';
|
|
241
|
+
if (quantumVars.has(name)) hookName = 'muQubit';
|
|
242
|
+
else if (registerVars.has(name)) hookName = 'muRegister';
|
|
243
|
+
|
|
244
|
+
const init = decl.initializer as ts.CallExpression;
|
|
245
|
+
newDecls.push(ts.factory.updateVariableDeclaration(
|
|
246
|
+
decl,
|
|
247
|
+
decl.name,
|
|
248
|
+
decl.exclamationToken,
|
|
249
|
+
decl.type,
|
|
250
|
+
ts.factory.createCallExpression(
|
|
251
|
+
ts.factory.createIdentifier(hookName),
|
|
252
|
+
undefined,
|
|
253
|
+
init.arguments
|
|
254
|
+
)
|
|
255
|
+
));
|
|
256
|
+
} else {
|
|
257
|
+
newDecls.push(decl);
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
newDecls.push(decl);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (changed) {
|
|
265
|
+
return ts.factory.createVariableStatement(
|
|
266
|
+
node.modifiers,
|
|
267
|
+
ts.factory.createVariableDeclarationList(newDecls, ts.NodeFlags.Const)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (ts.isCallExpression(node)) {
|
|
273
|
+
const expr = node.expression;
|
|
274
|
+
if (ts.isIdentifier(expr)) {
|
|
275
|
+
const name = expr.text;
|
|
276
|
+
const isMacro = name === '$' || name === '$q' || name === '$qr';
|
|
277
|
+
|
|
278
|
+
if (isMacro) {
|
|
279
|
+
// If this call is the initializer of a variable declaration being handled,
|
|
280
|
+
// it will be transformed by the VariableStatement logic above.
|
|
281
|
+
// However, we want to transform it here if it's used as a STANDALONE expression (e.g. assignment).
|
|
282
|
+
const parent = node.parent;
|
|
283
|
+
const isDeclarationInit = ts.isVariableDeclaration(parent) && parent.initializer === node;
|
|
284
|
+
|
|
285
|
+
if (!isDeclarationInit) {
|
|
286
|
+
let hookName = 'muState';
|
|
287
|
+
if (name === '$q') hookName = 'muQubit';
|
|
288
|
+
else if (name === '$qr') hookName = 'muRegister';
|
|
289
|
+
|
|
290
|
+
// Transform $(x) -> muState(x).value
|
|
291
|
+
return ts.factory.createPropertyAccessExpression(
|
|
292
|
+
ts.factory.createCallExpression(
|
|
293
|
+
ts.factory.createIdentifier(hookName),
|
|
294
|
+
undefined,
|
|
295
|
+
node.arguments
|
|
296
|
+
),
|
|
297
|
+
ts.factory.createIdentifier('value')
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (ts.isIdentifier(node)) {
|
|
305
|
+
const name = node.text;
|
|
306
|
+
if (reactiveVars.has(name) || quantumVars.has(name) || registerVars.has(name)) {
|
|
307
|
+
const parent = node.parent;
|
|
308
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node) return node;
|
|
309
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) return node;
|
|
310
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === node && parent.name.text === 'value') return node;
|
|
311
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node) return node;
|
|
312
|
+
|
|
313
|
+
// EXEMPTION: Do not add .value if the identifier is a direct argument to a Mulan hook
|
|
314
|
+
// These hooks (muGate, muMeasure, etc.) expect the signal wrapper.
|
|
315
|
+
if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression)) {
|
|
316
|
+
const hook = parent.expression.text;
|
|
317
|
+
if (COMMON_HOOKS.includes(hook)) return node;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return ts.factory.createPropertyAccessExpression(
|
|
321
|
+
ts.factory.createIdentifier(name),
|
|
322
|
+
ts.factory.createIdentifier('value')
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return ts.visitEachChild(node, visit, context);
|
|
328
|
+
}
|
|
329
|
+
return ts.visitNode(rootNode, visit) as ts.SourceFile;
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const result = ts.transform(sourceFile, [transformerFactory]);
|
|
334
|
+
const printer = ts.createPrinter();
|
|
335
|
+
return printer.printFile(result.transformed[0]);
|
|
336
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export interface SFCBlock {
|
|
2
|
+
type: 'script' | 'template' | 'style';
|
|
3
|
+
content: string;
|
|
4
|
+
attrs: Record<string, string>;
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SFCDescriptor {
|
|
10
|
+
filename: string;
|
|
11
|
+
source: string;
|
|
12
|
+
template: SFCBlock | null;
|
|
13
|
+
script: SFCBlock | null;
|
|
14
|
+
scripts: SFCBlock[];
|
|
15
|
+
styles: SFCBlock[];
|
|
16
|
+
customBlocks: SFCBlock[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Standardized State-Machine Parser for MulanJS
|
|
21
|
+
* Parses .mujs files safely, handling attributes, quotes, and nested content.
|
|
22
|
+
*/
|
|
23
|
+
export function parseMUJS(source: string, filename: string): SFCDescriptor {
|
|
24
|
+
const descriptor: SFCDescriptor = {
|
|
25
|
+
filename,
|
|
26
|
+
source,
|
|
27
|
+
template: null,
|
|
28
|
+
script: null,
|
|
29
|
+
scripts: [],
|
|
30
|
+
styles: [],
|
|
31
|
+
customBlocks: []
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let cursor = 0;
|
|
35
|
+
const length = source.length;
|
|
36
|
+
|
|
37
|
+
while (cursor < length) {
|
|
38
|
+
// Look for start tag '<'
|
|
39
|
+
const start = source.indexOf('<', cursor);
|
|
40
|
+
if (start === -1) break; // End of file
|
|
41
|
+
|
|
42
|
+
// Must be safe start (not in comment/string - simplified for top-level blocks)
|
|
43
|
+
// We assume SFC top-level blocks are valid HTML-like tags.
|
|
44
|
+
|
|
45
|
+
// Check if closing tag (ignore)
|
|
46
|
+
if (source[start + 1] === '/') {
|
|
47
|
+
cursor = start + 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse Tag Name
|
|
52
|
+
let nameEnd = start + 1;
|
|
53
|
+
while (nameEnd < length && !/\s|>/.test(source[nameEnd])) {
|
|
54
|
+
nameEnd++;
|
|
55
|
+
}
|
|
56
|
+
const tagName = source.slice(start + 1, nameEnd);
|
|
57
|
+
|
|
58
|
+
// Parse Attributes
|
|
59
|
+
let attrStart = nameEnd;
|
|
60
|
+
const attrs: Record<string, string> = {};
|
|
61
|
+
|
|
62
|
+
let tagEnd = source.indexOf('>', attrStart);
|
|
63
|
+
if (tagEnd === -1) break;
|
|
64
|
+
|
|
65
|
+
const attrString = source.slice(attrStart, tagEnd);
|
|
66
|
+
// Simple regex for attrs is safe *inside* the tag definition
|
|
67
|
+
const attrRegex = /([a-zA-Z0-9:-]+)(?:=["']([^"']*)["'])?/g;
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = attrRegex.exec(attrString)) !== null) {
|
|
70
|
+
attrs[match[1]] = match[2] || 'true';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Find Closing Tag
|
|
74
|
+
// We need to handle nested content.
|
|
75
|
+
// For <template>, we just look for </template>.
|
|
76
|
+
// For <script>, same.
|
|
77
|
+
// "Naive" approach: indexOf('</' + tagName) is actually standard for SFC parsers
|
|
78
|
+
// because top-level blocks shouldn't contain their own closing tag as text
|
|
79
|
+
// (except unless escaped, which we can handle if needed, but rarely an issue for top-level).
|
|
80
|
+
|
|
81
|
+
const contentStart = tagEnd + 1;
|
|
82
|
+
const closeTag = `</${tagName}>`;
|
|
83
|
+
const contentEnd = source.indexOf(closeTag, contentStart);
|
|
84
|
+
|
|
85
|
+
if (contentEnd === -1) {
|
|
86
|
+
// Unclosed block, invalid SFC.
|
|
87
|
+
console.warn(`[MulanJS Parser] Unclosed block: <${tagName}>`);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const content = source.slice(contentStart, contentEnd);
|
|
92
|
+
const block: SFCBlock = {
|
|
93
|
+
type: tagName as any,
|
|
94
|
+
content: content.trim(), // Trim content for cleanliness
|
|
95
|
+
attrs,
|
|
96
|
+
start: start,
|
|
97
|
+
end: contentEnd + closeTag.length
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Add to descriptor
|
|
101
|
+
if (tagName === 'template' || tagName === 'mu-template') {
|
|
102
|
+
descriptor.template = block;
|
|
103
|
+
} else if (tagName === 'script') {
|
|
104
|
+
descriptor.scripts.push(block);
|
|
105
|
+
if (attrs.setup || !descriptor.script) {
|
|
106
|
+
descriptor.script = block;
|
|
107
|
+
}
|
|
108
|
+
} else if (tagName === 'style') {
|
|
109
|
+
descriptor.styles.push(block);
|
|
110
|
+
} else {
|
|
111
|
+
descriptor.customBlocks.push(block);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cursor = contentEnd + closeTag.length;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return descriptor;
|
|
118
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
import { SFCDescriptor } from './sfc-parser';
|
|
3
|
+
|
|
4
|
+
export interface StyleCompileResult {
|
|
5
|
+
css: string;
|
|
6
|
+
scopedId?: string;
|
|
7
|
+
errors: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function compileStyle(descriptor: SFCDescriptor, filename: string): StyleCompileResult {
|
|
11
|
+
const styles = descriptor.styles;
|
|
12
|
+
if (!styles || styles.length === 0) return { css: '', errors: [] };
|
|
13
|
+
|
|
14
|
+
let combinedCss = '';
|
|
15
|
+
const scopedId = 'data-v-' + hash(filename);
|
|
16
|
+
|
|
17
|
+
styles.forEach(style => {
|
|
18
|
+
let content = style.content;
|
|
19
|
+
|
|
20
|
+
if (style.attrs.scoped) {
|
|
21
|
+
// SCSS Compilation
|
|
22
|
+
if (style.attrs.lang === 'scss' || style.attrs.lang === 'sass') {
|
|
23
|
+
try {
|
|
24
|
+
const sass = require('sass');
|
|
25
|
+
const result = sass.compileString(content, {
|
|
26
|
+
syntax: style.attrs.lang === 'sass' ? 'indented' : 'scss'
|
|
27
|
+
});
|
|
28
|
+
content = result.css;
|
|
29
|
+
} catch (e: any) {
|
|
30
|
+
return { css: '', errors: [`SCSS Compilation Error: ${e.message}`] };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Rewrite selectors
|
|
35
|
+
// Very naive regex replacer: "selector {" -> "selector[data-v-id] {"
|
|
36
|
+
// This is fragile but works for a PoC.
|
|
37
|
+
content = content.replace(/([^\r\n,{}]+)(?=\{)/g, (match) => {
|
|
38
|
+
const selectors = match.split(',');
|
|
39
|
+
return selectors.map(s => {
|
|
40
|
+
const selector = s.trim();
|
|
41
|
+
if (selector.startsWith('@') || selector === 'from' || selector === 'to') return selector; // Skip keyframes/media
|
|
42
|
+
return `${selector}[${scopedId}]`;
|
|
43
|
+
}).join(', ');
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
combinedCss += content + '\n';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
css: combinedCss,
|
|
52
|
+
scopedId,
|
|
53
|
+
errors: []
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Simple hash for ID generation
|
|
58
|
+
function hash(str: string): string {
|
|
59
|
+
let hash = 0;
|
|
60
|
+
for (let i = 0; i < str.length; i++) {
|
|
61
|
+
const char = str.charCodeAt(i);
|
|
62
|
+
hash = (hash << 5) - hash + char;
|
|
63
|
+
hash &= hash; // Convert to 32bit integer
|
|
64
|
+
}
|
|
65
|
+
return Math.abs(hash).toString(36);
|
|
66
|
+
}
|