@mulanjs/mulanjs 1.0.1-dev.20260218170441 → 1.0.1-dev.20260219055338
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/dist/compiler/compiler.js +3 -2
- package/dist/compiler/script-compiler.js +86 -31
- package/dist/types/compiler/compiler.d.ts +1 -1
- package/dist/types/compiler/script-compiler.d.ts +1 -1
- package/dist/types/compiler.d.ts +1 -1
- package/dist/types/script-compiler.d.ts +1 -1
- package/package.json +3 -2
- package/src/cli/index.js +1 -0
- package/src/compiler/compiler.ts +3 -2
- package/src/compiler/script-compiler.js +369 -0
- package/src/compiler/script-compiler.ts +100 -35
- package/src/compiler/sfc-parser.js +93 -0
- package/src/loader/index.js +4 -2
|
@@ -5,12 +5,13 @@ const sfc_parser_1 = require("./sfc-parser");
|
|
|
5
5
|
const script_compiler_1 = require("./script-compiler");
|
|
6
6
|
const template_compiler_1 = require("./template-compiler");
|
|
7
7
|
const style_compiler_1 = require("./style-compiler");
|
|
8
|
-
function compileSFC(source, filename) {
|
|
8
|
+
async function compileSFC(source, filename) {
|
|
9
9
|
// 1. Parse
|
|
10
10
|
const descriptor = (0, sfc_parser_1.parseMUJS)(source, filename);
|
|
11
11
|
// 2. Script
|
|
12
|
-
const scriptResult = (0, script_compiler_1.compileScript)(descriptor);
|
|
12
|
+
const scriptResult = await (0, script_compiler_1.compileScript)(descriptor);
|
|
13
13
|
// Replace export default to capture the component
|
|
14
|
+
// If the script already has 'export default', we intercept it.
|
|
14
15
|
let scriptCode = scriptResult.code.replace('export default', 'const __component__ =');
|
|
15
16
|
if (!scriptCode.includes('const __component__ =')) {
|
|
16
17
|
scriptCode = scriptCode.replace(/export\s+default/, 'const __component__ =');
|
|
@@ -35,14 +35,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.compileScript = compileScript;
|
|
37
37
|
const ts = __importStar(require("typescript"));
|
|
38
|
-
|
|
38
|
+
const source_map_1 = require("source-map");
|
|
39
|
+
async function compileScript(descriptor) {
|
|
39
40
|
const script = descriptor.script;
|
|
40
41
|
if (!script)
|
|
41
42
|
return { code: 'export default {}', errors: [] };
|
|
42
43
|
const isSetup = !!script.attrs.setup;
|
|
43
44
|
const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx';
|
|
44
45
|
if (isSetup) {
|
|
45
|
-
return compileSetupScript(script, isTs, descriptor);
|
|
46
|
+
return await compileSetupScript(script, isTs, descriptor);
|
|
46
47
|
}
|
|
47
48
|
else {
|
|
48
49
|
// Standard Options API - just pass through, assuming it has export default
|
|
@@ -51,7 +52,7 @@ function compileScript(descriptor) {
|
|
|
51
52
|
}
|
|
52
53
|
// Global list of Mulan hooks for auto-imports and reactivity exemption
|
|
53
54
|
const COMMON_HOOKS = ['muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo', 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent'];
|
|
54
|
-
function compileSetupScript(script, isTs, descriptor) {
|
|
55
|
+
async function compileSetupScript(script, isTs, descriptor) {
|
|
55
56
|
var _a;
|
|
56
57
|
// 1. Pre-process: Natural Reactivity Transformation ($ syntax)
|
|
57
58
|
// We do this BEFORE extraction so that the rest of the compiler sees standard 'muState' code.
|
|
@@ -59,14 +60,31 @@ function compileSetupScript(script, isTs, descriptor) {
|
|
|
59
60
|
// 2. Parse the TRANSFORMED code
|
|
60
61
|
const sourceFile = ts.createSourceFile('script.' + (isTs ? 'ts' : 'js'), transformedContent, ts.ScriptTarget.Latest, true);
|
|
61
62
|
const imports = [];
|
|
62
|
-
|
|
63
|
+
// We will build the body by replacing imports with newlines in the original content
|
|
64
|
+
// This preserves line numbers for the user's code relative to the script block start.
|
|
65
|
+
let scriptBody = transformedContent;
|
|
66
|
+
// Collect imports and bindings
|
|
63
67
|
const bindings = [];
|
|
68
|
+
// We need to remove imports from scriptBody but keep newlines.
|
|
69
|
+
// We'll use a replacement list and apply it end-to-start to avoid index shifting
|
|
70
|
+
const replacements = [];
|
|
64
71
|
sourceFile.statements.forEach(stmt => {
|
|
65
72
|
if (ts.isImportDeclaration(stmt)) {
|
|
66
|
-
|
|
73
|
+
const importText = transformedContent.substring(stmt.pos, stmt.end);
|
|
74
|
+
// Add to imports list (trimmed for cleanliness in preamble)
|
|
75
|
+
imports.push(importText.trim());
|
|
76
|
+
// Calculate number of newlines in this import statement to preserve layout
|
|
77
|
+
const numNewlines = (importText.match(/\n/g) || []).length;
|
|
78
|
+
const replacement = '\n'.repeat(numNewlines) + ' '.repeat(importText.length - numNewlines); // Keep length/lines roughly same? actually just newlines is enough for line mapping
|
|
79
|
+
// Better: we just want to preserve LINE COUNT.
|
|
80
|
+
// If we replace the whole range with newlines, we keep the vertical layout.
|
|
81
|
+
replacements.push({
|
|
82
|
+
start: stmt.pos,
|
|
83
|
+
end: stmt.end,
|
|
84
|
+
text: '\n'.repeat(numNewlines) || ' ' // at least a space if no newlines, to avoid merging lines?
|
|
85
|
+
});
|
|
67
86
|
}
|
|
68
87
|
else {
|
|
69
|
-
statements.push(transformedContent.substring(stmt.pos, stmt.end).trim());
|
|
70
88
|
// Extract top-level declarations for template exposure
|
|
71
89
|
if (ts.isVariableStatement(stmt)) {
|
|
72
90
|
stmt.declarationList.declarations.forEach(decl => {
|
|
@@ -83,16 +101,21 @@ function compileSetupScript(script, isTs, descriptor) {
|
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
});
|
|
104
|
+
// Apply replacements (reverse order)
|
|
105
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
106
|
+
replacements.forEach(r => {
|
|
107
|
+
scriptBody = scriptBody.substring(0, r.start) + r.text + scriptBody.substring(r.end);
|
|
108
|
+
});
|
|
86
109
|
// Reconstruct
|
|
87
|
-
// We import defineComponent from mulanjs if not present?
|
|
110
|
+
// We import defineComponent from @mulanjs/mulanjs if not present?
|
|
88
111
|
// Ideally user imports what they need, but for 'setup' wrapping we need defineComponent.
|
|
89
112
|
// Let's assume user might not have imported it, so we import it as _defineComponent to avoid collision.
|
|
90
113
|
// Check if defineComponent is imported
|
|
91
114
|
const hasDefineComponent = imports.some(i => i.includes('defineComponent'));
|
|
92
|
-
const bootImports = hasDefineComponent ? '' : `import { defineComponent as _defineComponent } from 'mulanjs';`;
|
|
115
|
+
const bootImports = hasDefineComponent ? '' : `import { defineComponent as _defineComponent } from '@mulanjs/mulanjs';`;
|
|
93
116
|
// ADDED: Check if we introduced 'muState' but it wasn't imported (because user used $)
|
|
94
117
|
const usesMuState = transformedContent.includes('muState');
|
|
95
|
-
const hasMuStateImport = imports.some(i => i.includes('muState') || i.includes('mulanjs'));
|
|
118
|
+
const hasMuStateImport = imports.some(i => i.includes('muState') || i.includes('@mulanjs/mulanjs'));
|
|
96
119
|
let autoImports = (usesMuState && !hasMuStateImport) ? [`muState`] : [];
|
|
97
120
|
// Auto-import common hooks
|
|
98
121
|
COMMON_HOOKS.forEach(hook => {
|
|
@@ -104,7 +127,7 @@ function compileSetupScript(script, isTs, descriptor) {
|
|
|
104
127
|
}
|
|
105
128
|
});
|
|
106
129
|
const autoImportStmt = autoImports.length > 0
|
|
107
|
-
? `import { ${autoImports.join(', ')} } from 'mulanjs';`
|
|
130
|
+
? `import { ${autoImports.join(', ')} } from '@mulanjs/mulanjs';`
|
|
108
131
|
: '';
|
|
109
132
|
const filename = descriptor.filename;
|
|
110
133
|
const componentName = filename ? ((_a = filename.split(/[/\\]/).pop()) === null || _a === void 0 ? void 0 : _a.split('.')[0].replace(/\W/g, '')) || 'App' : 'App';
|
|
@@ -117,29 +140,24 @@ function compileSetupScript(script, isTs, descriptor) {
|
|
|
117
140
|
return exposed;
|
|
118
141
|
`
|
|
119
142
|
: 'return {};';
|
|
120
|
-
|
|
143
|
+
// Construct final code with preserved body layout
|
|
144
|
+
const preamble = `
|
|
121
145
|
${bootImports}
|
|
122
146
|
${autoImportStmt}
|
|
123
147
|
${imports.join('\n')}
|
|
124
148
|
|
|
125
149
|
export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
126
150
|
setup() {
|
|
127
|
-
|
|
151
|
+
`;
|
|
152
|
+
// We intentionally put scriptBody on a new line.
|
|
153
|
+
// Note: scriptBody has imports replaced by whitespace/newlines, so its internal line numbers relative to its start are preserved.
|
|
154
|
+
const postamble = `
|
|
128
155
|
${bindingString}
|
|
129
156
|
}
|
|
130
157
|
});
|
|
131
158
|
`;
|
|
159
|
+
const finalCode = preamble + scriptBody + postamble;
|
|
132
160
|
if (isTs) {
|
|
133
|
-
// Calculate line offset for Source Maps (Padding Strategy)
|
|
134
|
-
// Find how many newlines are before the script content in the original source
|
|
135
|
-
// This is a rough estimation but better than nothing for 1.0 dev
|
|
136
|
-
const linesBefore = descriptor.source.substring(0, script.start).split('\n').length - 1;
|
|
137
|
-
const padding = '\n'.repeat(linesBefore);
|
|
138
|
-
// Pad the content so line numbers match original file
|
|
139
|
-
// Note: This only works perfectly if we are transpiling the RAW content.
|
|
140
|
-
// Since we are extracting/transforming (Natural Reactivity), line numbers might shift.
|
|
141
|
-
// For accurate 1-to-1 mapping, we rely on TS source maps of the 'finalCode'.
|
|
142
|
-
// To make 'finalCode' map back to 'filename', we need to construct it carefully.
|
|
143
161
|
// Transpile extracted TS code to JS
|
|
144
162
|
const result = ts.transpileModule(finalCode, {
|
|
145
163
|
compilerOptions: {
|
|
@@ -149,7 +167,7 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
|
149
167
|
inlineSources: true,
|
|
150
168
|
sourceRoot: '/',
|
|
151
169
|
},
|
|
152
|
-
fileName: filename //
|
|
170
|
+
fileName: filename // We ask TS to map to filename, but it maps 'finalCode' -> 'filename'
|
|
153
171
|
});
|
|
154
172
|
// Fix: Remove the //# sourceMappingURL= comment added by TS
|
|
155
173
|
const codeWithoutMapComment = result.outputText.replace(/\/\/# sourceMappingURL=.*$/gm, '');
|
|
@@ -157,19 +175,56 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
|
157
175
|
let finalMap = result.sourceMapText;
|
|
158
176
|
if (finalMap) {
|
|
159
177
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
// Determine offsets
|
|
179
|
+
const preambleLines = preamble.split('\n').length - 1;
|
|
180
|
+
// scriptBody starts at line `preambleLines` (0-indexed) in finalCode.
|
|
181
|
+
// Determine where script starts in original .mujs file
|
|
182
|
+
const originalSource = descriptor.source;
|
|
183
|
+
const linesBeforeScript = originalSource.substring(0, script.start).split('\n').length - 1;
|
|
184
|
+
const consumer = await new source_map_1.SourceMapConsumer(finalMap);
|
|
185
|
+
const generator = new source_map_1.SourceMapGenerator({
|
|
186
|
+
file: filename,
|
|
187
|
+
sourceRoot: ''
|
|
188
|
+
});
|
|
189
|
+
// If inside src, use path starting from src. Else just basename.
|
|
162
190
|
const normalizedPath = filename.replace(/\\/g, '/');
|
|
163
191
|
const srcIndex = normalizedPath.indexOf('/src/');
|
|
164
|
-
// If inside src, use path starting from src. Else just basename.
|
|
165
|
-
// Example: c:/.../src/pages/Home.mujs -> src/pages/Home.mujs
|
|
166
192
|
let relativePath = srcIndex !== -1
|
|
167
193
|
? normalizedPath.substring(srcIndex + 1)
|
|
168
194
|
: filename.split(/[/\\]/).pop() || 'unknown.mujs';
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
generator.setSourceContent(relativePath, originalSource);
|
|
196
|
+
consumer.eachMapping(m => {
|
|
197
|
+
if (m.originalLine === null)
|
|
198
|
+
return;
|
|
199
|
+
// m.originalLine is the line in `finalCode` (1-based because SourceMapConsumer uses 1-based)
|
|
200
|
+
// We need to convert it to line in `.mujs`.
|
|
201
|
+
// Check if this line belongs to scriptBody
|
|
202
|
+
// scriptBody spans from (preambleLines + 1) to (preambleLines + scriptBodyLines)
|
|
203
|
+
const lineInFinalCode = m.originalLine;
|
|
204
|
+
// Check if strictly inside body
|
|
205
|
+
if (lineInFinalCode > preambleLines) {
|
|
206
|
+
// Calculate offset inside scriptBody
|
|
207
|
+
const lineOffsetInBody = lineInFinalCode - preambleLines; // 1-based offset (1st line of body is 1)
|
|
208
|
+
// Calculate original line in .mujs
|
|
209
|
+
// If lineOffsetInBody is 1 (first line of body), it corresponds to (linesBeforeScript + 1)
|
|
210
|
+
const originalLineInMujs = linesBeforeScript + lineOffsetInBody;
|
|
211
|
+
// Add mapping
|
|
212
|
+
generator.addMapping({
|
|
213
|
+
generated: {
|
|
214
|
+
line: m.generatedLine,
|
|
215
|
+
column: m.generatedColumn
|
|
216
|
+
},
|
|
217
|
+
original: {
|
|
218
|
+
line: originalLineInMujs,
|
|
219
|
+
column: m.originalColumn // Column should be preserved if we didn't change indentation
|
|
220
|
+
},
|
|
221
|
+
source: relativePath,
|
|
222
|
+
name: m.name
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
finalMap = generator.toString();
|
|
227
|
+
consumer.destroy();
|
|
173
228
|
}
|
|
174
229
|
catch (e) {
|
|
175
230
|
console.warn('[MulanJS] Failed to patch source map:', e);
|
package/dist/types/compiler.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulanjs/mulanjs",
|
|
3
|
-
"version": "1.0.1-dev.
|
|
3
|
+
"version": "1.0.1-dev.20260219055338",
|
|
4
4
|
"description": "A powerful, secure, and enterprise-grade JavaScript framework.",
|
|
5
5
|
"main": "dist/mulan.js",
|
|
6
6
|
"module": "dist/mulan.esm.js",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"webpack-cli": "^6.0.1",
|
|
53
53
|
"webpack-dev-server": "^5.2.0",
|
|
54
54
|
"ts-loader": "^9.5.4",
|
|
55
|
-
"typescript": "^5.0.0"
|
|
55
|
+
"typescript": "^5.0.0",
|
|
56
|
+
"source-map": "^0.7.4"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/fs-extra": "^11.0.4",
|
package/src/cli/index.js
CHANGED
package/src/compiler/compiler.ts
CHANGED
|
@@ -11,13 +11,14 @@ export interface CompileResult {
|
|
|
11
11
|
map?: string; // Source Map
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function compileSFC(source: string, filename: string): CompileResult {
|
|
14
|
+
export async function compileSFC(source: string, filename: string): Promise<CompileResult> {
|
|
15
15
|
// 1. Parse
|
|
16
16
|
const descriptor = parseMUJS(source, filename);
|
|
17
17
|
|
|
18
18
|
// 2. Script
|
|
19
|
-
const scriptResult = compileScript(descriptor);
|
|
19
|
+
const scriptResult = await compileScript(descriptor);
|
|
20
20
|
// Replace export default to capture the component
|
|
21
|
+
// If the script already has 'export default', we intercept it.
|
|
21
22
|
let scriptCode = scriptResult.code.replace('export default', 'const __component__ =');
|
|
22
23
|
if (!scriptCode.includes('const __component__ =')) {
|
|
23
24
|
scriptCode = scriptCode.replace(/export\s+default/, 'const __component__ =');
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.compileScript = compileScript;
|
|
37
|
+
const ts = __importStar(require("typescript"));
|
|
38
|
+
const source_map_1 = require("source-map");
|
|
39
|
+
async function compileScript(descriptor) {
|
|
40
|
+
const script = descriptor.script;
|
|
41
|
+
if (!script)
|
|
42
|
+
return { code: 'export default {}', errors: [] };
|
|
43
|
+
const isSetup = !!script.attrs.setup;
|
|
44
|
+
const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx';
|
|
45
|
+
if (isSetup) {
|
|
46
|
+
return await compileSetupScript(script, isTs, descriptor);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Standard Options API - just pass through, assuming it has export default
|
|
50
|
+
return { code: script.content, errors: [] };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Global list of Mulan hooks for auto-imports and reactivity exemption
|
|
54
|
+
const COMMON_HOOKS = ['muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo', 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent'];
|
|
55
|
+
async function compileSetupScript(script, isTs, descriptor) {
|
|
56
|
+
var _a;
|
|
57
|
+
// 1. Pre-process: Natural Reactivity Transformation ($ syntax)
|
|
58
|
+
// We do this BEFORE extraction so that the rest of the compiler sees standard 'muState' code.
|
|
59
|
+
const transformedContent = transformNaturalReactivity(script.content, isTs);
|
|
60
|
+
// 2. Parse the TRANSFORMED code
|
|
61
|
+
const sourceFile = ts.createSourceFile('script.' + (isTs ? 'ts' : 'js'), transformedContent, ts.ScriptTarget.Latest, true);
|
|
62
|
+
const imports = [];
|
|
63
|
+
// We will build the body by replacing imports with newlines in the original content
|
|
64
|
+
// This preserves line numbers for the user's code relative to the script block start.
|
|
65
|
+
let scriptBody = transformedContent;
|
|
66
|
+
// Collect imports and bindings
|
|
67
|
+
const bindings = [];
|
|
68
|
+
// We need to remove imports from scriptBody but keep newlines.
|
|
69
|
+
// We'll use a replacement list and apply it end-to-start to avoid index shifting
|
|
70
|
+
const replacements = [];
|
|
71
|
+
sourceFile.statements.forEach(stmt => {
|
|
72
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
73
|
+
const importText = transformedContent.substring(stmt.pos, stmt.end);
|
|
74
|
+
// Add to imports list (trimmed for cleanliness in preamble)
|
|
75
|
+
imports.push(importText.trim());
|
|
76
|
+
// Calculate number of newlines in this import statement to preserve layout
|
|
77
|
+
const numNewlines = (importText.match(/\n/g) || []).length;
|
|
78
|
+
const replacement = '\n'.repeat(numNewlines) + ' '.repeat(importText.length - numNewlines); // Keep length/lines roughly same? actually just newlines is enough for line mapping
|
|
79
|
+
// Better: we just want to preserve LINE COUNT.
|
|
80
|
+
// If we replace the whole range with newlines, we keep the vertical layout.
|
|
81
|
+
replacements.push({
|
|
82
|
+
start: stmt.pos,
|
|
83
|
+
end: stmt.end,
|
|
84
|
+
text: '\n'.repeat(numNewlines) || ' ' // at least a space if no newlines, to avoid merging lines?
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Extract top-level declarations for template exposure
|
|
89
|
+
if (ts.isVariableStatement(stmt)) {
|
|
90
|
+
stmt.declarationList.declarations.forEach(decl => {
|
|
91
|
+
if (ts.isIdentifier(decl.name)) {
|
|
92
|
+
bindings.push(decl.name.text);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
97
|
+
bindings.push(stmt.name.text);
|
|
98
|
+
}
|
|
99
|
+
else if (ts.isClassDeclaration(stmt) && stmt.name) {
|
|
100
|
+
bindings.push(stmt.name.text);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Apply replacements (reverse order)
|
|
105
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
106
|
+
replacements.forEach(r => {
|
|
107
|
+
scriptBody = scriptBody.substring(0, r.start) + r.text + scriptBody.substring(r.end);
|
|
108
|
+
});
|
|
109
|
+
// Reconstruct
|
|
110
|
+
// We import defineComponent from @mulanjs/mulanjs if not present?
|
|
111
|
+
// Ideally user imports what they need, but for 'setup' wrapping we need defineComponent.
|
|
112
|
+
// Let's assume user might not have imported it, so we import it as _defineComponent to avoid collision.
|
|
113
|
+
// Check if defineComponent is imported
|
|
114
|
+
const hasDefineComponent = imports.some(i => i.includes('defineComponent'));
|
|
115
|
+
const bootImports = hasDefineComponent ? '' : `import { defineComponent as _defineComponent } from '@mulanjs/mulanjs';`;
|
|
116
|
+
// ADDED: Check if we introduced 'muState' but it wasn't imported (because user used $)
|
|
117
|
+
const usesMuState = transformedContent.includes('muState');
|
|
118
|
+
const hasMuStateImport = imports.some(i => i.includes('muState') || i.includes('@mulanjs/mulanjs'));
|
|
119
|
+
let autoImports = (usesMuState && !hasMuStateImport) ? [`muState`] : [];
|
|
120
|
+
// Auto-import common hooks
|
|
121
|
+
COMMON_HOOKS.forEach(hook => {
|
|
122
|
+
if (transformedContent.includes(hook) && !imports.some(i => i.includes(hook))) {
|
|
123
|
+
// Avoid duplicates in autoImports
|
|
124
|
+
if (!autoImports.includes(hook)) {
|
|
125
|
+
autoImports.push(hook);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const autoImportStmt = autoImports.length > 0
|
|
130
|
+
? `import { ${autoImports.join(', ')} } from '@mulanjs/mulanjs';`
|
|
131
|
+
: '';
|
|
132
|
+
const filename = descriptor.filename;
|
|
133
|
+
const componentName = filename ? ((_a = filename.split(/[/\\]/).pop()) === null || _a === void 0 ? void 0 : _a.split('.')[0].replace(/\W/g, '')) || 'App' : 'App';
|
|
134
|
+
const bindingString = bindings.length > 0
|
|
135
|
+
? `
|
|
136
|
+
const exposed = { ${bindings.join(', ')} };
|
|
137
|
+
if (typeof window !== 'undefined') {
|
|
138
|
+
window["${componentName}"] = exposed;
|
|
139
|
+
}
|
|
140
|
+
return exposed;
|
|
141
|
+
`
|
|
142
|
+
: 'return {};';
|
|
143
|
+
// Construct final code with preserved body layout
|
|
144
|
+
const preamble = `
|
|
145
|
+
${bootImports}
|
|
146
|
+
${autoImportStmt}
|
|
147
|
+
${imports.join('\n')}
|
|
148
|
+
|
|
149
|
+
export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
150
|
+
setup() {
|
|
151
|
+
`;
|
|
152
|
+
// We intentionally put scriptBody on a new line.
|
|
153
|
+
// Note: scriptBody has imports replaced by whitespace/newlines, so its internal line numbers relative to its start are preserved.
|
|
154
|
+
const postamble = `
|
|
155
|
+
${bindingString}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
`;
|
|
159
|
+
const finalCode = preamble + scriptBody + postamble;
|
|
160
|
+
if (isTs) {
|
|
161
|
+
// Transpile extracted TS code to JS
|
|
162
|
+
const result = ts.transpileModule(finalCode, {
|
|
163
|
+
compilerOptions: {
|
|
164
|
+
module: ts.ModuleKind.ESNext,
|
|
165
|
+
target: ts.ScriptTarget.ES2019,
|
|
166
|
+
sourceMap: true, // ENABLE SOURCE MAPS
|
|
167
|
+
inlineSources: true,
|
|
168
|
+
sourceRoot: '/',
|
|
169
|
+
},
|
|
170
|
+
fileName: filename // We ask TS to map to filename, but it maps 'finalCode' -> 'filename'
|
|
171
|
+
});
|
|
172
|
+
// Fix: Remove the //# sourceMappingURL= comment added by TS
|
|
173
|
+
const codeWithoutMapComment = result.outputText.replace(/\/\/# sourceMappingURL=.*$/gm, '');
|
|
174
|
+
// Enhance Source Map to show ORIGINAL .mujs content
|
|
175
|
+
let finalMap = result.sourceMapText;
|
|
176
|
+
if (finalMap) {
|
|
177
|
+
try {
|
|
178
|
+
// Determine offsets
|
|
179
|
+
const preambleLines = preamble.split('\n').length - 1;
|
|
180
|
+
// scriptBody starts at line `preambleLines` (0-indexed) in finalCode.
|
|
181
|
+
// Determine where script starts in original .mujs file
|
|
182
|
+
const originalSource = descriptor.source;
|
|
183
|
+
const linesBeforeScript = originalSource.substring(0, script.start).split('\n').length - 1;
|
|
184
|
+
const consumer = await new source_map_1.SourceMapConsumer(finalMap);
|
|
185
|
+
const generator = new source_map_1.SourceMapGenerator({
|
|
186
|
+
file: filename,
|
|
187
|
+
sourceRoot: ''
|
|
188
|
+
});
|
|
189
|
+
// If inside src, use path starting from src. Else just basename.
|
|
190
|
+
const normalizedPath = filename.replace(/\\/g, '/');
|
|
191
|
+
const srcIndex = normalizedPath.indexOf('/src/');
|
|
192
|
+
let relativePath = srcIndex !== -1
|
|
193
|
+
? normalizedPath.substring(srcIndex + 1)
|
|
194
|
+
: filename.split(/[/\\]/).pop() || 'unknown.mujs';
|
|
195
|
+
generator.setSourceContent(relativePath, originalSource);
|
|
196
|
+
consumer.eachMapping(m => {
|
|
197
|
+
if (m.originalLine === null)
|
|
198
|
+
return;
|
|
199
|
+
// m.originalLine is the line in `finalCode` (1-based because SourceMapConsumer uses 1-based)
|
|
200
|
+
// We need to convert it to line in `.mujs`.
|
|
201
|
+
// Check if this line belongs to scriptBody
|
|
202
|
+
// scriptBody spans from (preambleLines + 1) to (preambleLines + scriptBodyLines)
|
|
203
|
+
const lineInFinalCode = m.originalLine;
|
|
204
|
+
// Check if strictly inside body
|
|
205
|
+
if (lineInFinalCode > preambleLines) {
|
|
206
|
+
// Calculate offset inside scriptBody
|
|
207
|
+
const lineOffsetInBody = lineInFinalCode - preambleLines; // 1-based offset (1st line of body is 1)
|
|
208
|
+
// Calculate original line in .mujs
|
|
209
|
+
// If lineOffsetInBody is 1 (first line of body), it corresponds to (linesBeforeScript + 1)
|
|
210
|
+
const originalLineInMujs = linesBeforeScript + lineOffsetInBody;
|
|
211
|
+
// Add mapping
|
|
212
|
+
generator.addMapping({
|
|
213
|
+
generated: {
|
|
214
|
+
line: m.generatedLine,
|
|
215
|
+
column: m.generatedColumn
|
|
216
|
+
},
|
|
217
|
+
original: {
|
|
218
|
+
line: originalLineInMujs,
|
|
219
|
+
column: m.originalColumn // Column should be preserved if we didn't change indentation
|
|
220
|
+
},
|
|
221
|
+
source: relativePath,
|
|
222
|
+
name: m.name
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
finalMap = generator.toString();
|
|
227
|
+
consumer.destroy();
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
console.warn('[MulanJS] Failed to patch source map:', e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
code: codeWithoutMapComment,
|
|
235
|
+
bindings,
|
|
236
|
+
errors: [],
|
|
237
|
+
map: finalMap // Return the enhanced map
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
code: finalCode,
|
|
242
|
+
bindings,
|
|
243
|
+
errors: [],
|
|
244
|
+
map: undefined
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// --- Natural Reactivity Transformer ---
|
|
248
|
+
function transformNaturalReactivity(code, isTs) {
|
|
249
|
+
// If no '$(' or '$q(' or '$qr(' usage, return original code for safety/speed
|
|
250
|
+
if (!code.includes('$(') && !code.includes('$q(') && !code.includes('$qr('))
|
|
251
|
+
return code;
|
|
252
|
+
const sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true);
|
|
253
|
+
const reactiveVars = new Set();
|
|
254
|
+
const quantumVars = new Set();
|
|
255
|
+
const registerVars = new Set();
|
|
256
|
+
// Pass 1: Identification
|
|
257
|
+
function findReactiveVars(node) {
|
|
258
|
+
if (ts.isVariableStatement(node)) {
|
|
259
|
+
node.declarationList.declarations.forEach(decl => {
|
|
260
|
+
if (decl.initializer && ts.isCallExpression(decl.initializer)) {
|
|
261
|
+
const expr = decl.initializer.expression;
|
|
262
|
+
if (ts.isIdentifier(expr)) {
|
|
263
|
+
if (expr.text === '$') {
|
|
264
|
+
if (ts.isIdentifier(decl.name))
|
|
265
|
+
reactiveVars.add(decl.name.text);
|
|
266
|
+
}
|
|
267
|
+
else if (expr.text === '$q') {
|
|
268
|
+
if (ts.isIdentifier(decl.name))
|
|
269
|
+
quantumVars.add(decl.name.text);
|
|
270
|
+
}
|
|
271
|
+
else if (expr.text === '$qr') {
|
|
272
|
+
if (ts.isIdentifier(decl.name))
|
|
273
|
+
registerVars.add(decl.name.text);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
ts.forEachChild(node, findReactiveVars);
|
|
280
|
+
}
|
|
281
|
+
findReactiveVars(sourceFile);
|
|
282
|
+
if (reactiveVars.size === 0 && quantumVars.size === 0 && registerVars.size === 0)
|
|
283
|
+
return code;
|
|
284
|
+
// Pass 2: Transformation
|
|
285
|
+
const transformerFactory = (context) => {
|
|
286
|
+
return (rootNode) => {
|
|
287
|
+
function visit(node) {
|
|
288
|
+
if (ts.isVariableStatement(node)) {
|
|
289
|
+
const newDecls = [];
|
|
290
|
+
let changed = false;
|
|
291
|
+
node.declarationList.declarations.forEach(decl => {
|
|
292
|
+
if (ts.isIdentifier(decl.name)) {
|
|
293
|
+
const name = decl.name.text;
|
|
294
|
+
if (reactiveVars.has(name) || quantumVars.has(name) || registerVars.has(name)) {
|
|
295
|
+
changed = true;
|
|
296
|
+
let hookName = 'muState';
|
|
297
|
+
if (quantumVars.has(name))
|
|
298
|
+
hookName = 'muQubit';
|
|
299
|
+
else if (registerVars.has(name))
|
|
300
|
+
hookName = 'muRegister';
|
|
301
|
+
const init = decl.initializer;
|
|
302
|
+
newDecls.push(ts.factory.updateVariableDeclaration(decl, decl.name, decl.exclamationToken, decl.type, ts.factory.createCallExpression(ts.factory.createIdentifier(hookName), undefined, init.arguments)));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
newDecls.push(decl);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
newDecls.push(decl);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
if (changed) {
|
|
313
|
+
return ts.factory.createVariableStatement(node.modifiers, ts.factory.createVariableDeclarationList(newDecls, ts.NodeFlags.Const));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (ts.isCallExpression(node)) {
|
|
317
|
+
const expr = node.expression;
|
|
318
|
+
if (ts.isIdentifier(expr)) {
|
|
319
|
+
const name = expr.text;
|
|
320
|
+
const isMacro = name === '$' || name === '$q' || name === '$qr';
|
|
321
|
+
if (isMacro) {
|
|
322
|
+
// If this call is the initializer of a variable declaration being handled,
|
|
323
|
+
// it will be transformed by the VariableStatement logic above.
|
|
324
|
+
// However, we want to transform it here if it's used as a STANDALONE expression (e.g. assignment).
|
|
325
|
+
const parent = node.parent;
|
|
326
|
+
const isDeclarationInit = ts.isVariableDeclaration(parent) && parent.initializer === node;
|
|
327
|
+
if (!isDeclarationInit) {
|
|
328
|
+
let hookName = 'muState';
|
|
329
|
+
if (name === '$q')
|
|
330
|
+
hookName = 'muQubit';
|
|
331
|
+
else if (name === '$qr')
|
|
332
|
+
hookName = 'muRegister';
|
|
333
|
+
// Transform $(x) -> muState(x).value
|
|
334
|
+
return ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createIdentifier(hookName), undefined, node.arguments), ts.factory.createIdentifier('value'));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (ts.isIdentifier(node)) {
|
|
340
|
+
const name = node.text;
|
|
341
|
+
if (reactiveVars.has(name) || quantumVars.has(name) || registerVars.has(name)) {
|
|
342
|
+
const parent = node.parent;
|
|
343
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node)
|
|
344
|
+
return node;
|
|
345
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
346
|
+
return node;
|
|
347
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === node && parent.name.text === 'value')
|
|
348
|
+
return node;
|
|
349
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node)
|
|
350
|
+
return node;
|
|
351
|
+
// EXEMPTION: Do not add .value if the identifier is a direct argument to a Mulan hook
|
|
352
|
+
// These hooks (muGate, muMeasure, etc.) expect the signal wrapper.
|
|
353
|
+
if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression)) {
|
|
354
|
+
const hook = parent.expression.text;
|
|
355
|
+
if (COMMON_HOOKS.includes(hook))
|
|
356
|
+
return node;
|
|
357
|
+
}
|
|
358
|
+
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), ts.factory.createIdentifier('value'));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return ts.visitEachChild(node, visit, context);
|
|
362
|
+
}
|
|
363
|
+
return ts.visitNode(rootNode, visit);
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
const result = ts.transform(sourceFile, [transformerFactory]);
|
|
367
|
+
const printer = ts.createPrinter();
|
|
368
|
+
return printer.printFile(result.transformed[0]);
|
|
369
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { SFCDescriptor, SFCBlock } from './sfc-parser';
|
|
3
3
|
import * as ts from 'typescript';
|
|
4
|
+
import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
|
|
4
5
|
|
|
5
6
|
export interface ScriptCompileResult {
|
|
6
7
|
code: string;
|
|
@@ -9,7 +10,7 @@ export interface ScriptCompileResult {
|
|
|
9
10
|
map?: string; // Source Map
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function compileScript(descriptor: SFCDescriptor): ScriptCompileResult {
|
|
13
|
+
export async function compileScript(descriptor: SFCDescriptor): Promise<ScriptCompileResult> {
|
|
13
14
|
const script = descriptor.script;
|
|
14
15
|
if (!script) return { code: 'export default {}', errors: [] };
|
|
15
16
|
|
|
@@ -17,7 +18,7 @@ export function compileScript(descriptor: SFCDescriptor): ScriptCompileResult {
|
|
|
17
18
|
const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx';
|
|
18
19
|
|
|
19
20
|
if (isSetup) {
|
|
20
|
-
return compileSetupScript(script, isTs, descriptor);
|
|
21
|
+
return await compileSetupScript(script, isTs, descriptor);
|
|
21
22
|
} else {
|
|
22
23
|
// Standard Options API - just pass through, assuming it has export default
|
|
23
24
|
return { code: script.content, errors: [] };
|
|
@@ -27,7 +28,7 @@ export function compileScript(descriptor: SFCDescriptor): ScriptCompileResult {
|
|
|
27
28
|
// Global list of Mulan hooks for auto-imports and reactivity exemption
|
|
28
29
|
const COMMON_HOOKS = ['muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo', 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent'];
|
|
29
30
|
|
|
30
|
-
function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDescriptor): ScriptCompileResult {
|
|
31
|
+
async function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDescriptor): Promise<ScriptCompileResult> {
|
|
31
32
|
// 1. Pre-process: Natural Reactivity Transformation ($ syntax)
|
|
32
33
|
// We do this BEFORE extraction so that the rest of the compiler sees standard 'muState' code.
|
|
33
34
|
const transformedContent = transformNaturalReactivity(script.content, isTs);
|
|
@@ -41,15 +42,34 @@ function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDesc
|
|
|
41
42
|
);
|
|
42
43
|
|
|
43
44
|
const imports: string[] = [];
|
|
44
|
-
|
|
45
|
+
// We will build the body by replacing imports with newlines in the original content
|
|
46
|
+
// This preserves line numbers for the user's code relative to the script block start.
|
|
47
|
+
let scriptBody = transformedContent;
|
|
48
|
+
|
|
49
|
+
// Collect imports and bindings
|
|
45
50
|
const bindings: string[] = [];
|
|
46
51
|
|
|
52
|
+
// We need to remove imports from scriptBody but keep newlines.
|
|
53
|
+
// We'll use a replacement list and apply it end-to-start to avoid index shifting
|
|
54
|
+
const replacements: { start: number, end: number, text: string }[] = [];
|
|
55
|
+
|
|
47
56
|
sourceFile.statements.forEach(stmt => {
|
|
48
57
|
if (ts.isImportDeclaration(stmt)) {
|
|
49
|
-
|
|
58
|
+
const importText = transformedContent.substring(stmt.pos, stmt.end);
|
|
59
|
+
// Add to imports list (trimmed for cleanliness in preamble)
|
|
60
|
+
imports.push(importText.trim());
|
|
61
|
+
|
|
62
|
+
// Calculate number of newlines in this import statement to preserve layout
|
|
63
|
+
const numNewlines = (importText.match(/\n/g) || []).length;
|
|
64
|
+
const replacement = '\n'.repeat(numNewlines) + ' '.repeat(importText.length - numNewlines); // Keep length/lines roughly same? actually just newlines is enough for line mapping
|
|
65
|
+
// Better: we just want to preserve LINE COUNT.
|
|
66
|
+
// If we replace the whole range with newlines, we keep the vertical layout.
|
|
67
|
+
replacements.push({
|
|
68
|
+
start: stmt.pos,
|
|
69
|
+
end: stmt.end,
|
|
70
|
+
text: '\n'.repeat(numNewlines) || ' ' // at least a space if no newlines, to avoid merging lines?
|
|
71
|
+
});
|
|
50
72
|
} else {
|
|
51
|
-
statements.push(transformedContent.substring(stmt.pos, stmt.end).trim());
|
|
52
|
-
|
|
53
73
|
// Extract top-level declarations for template exposure
|
|
54
74
|
if (ts.isVariableStatement(stmt)) {
|
|
55
75
|
stmt.declarationList.declarations.forEach(decl => {
|
|
@@ -65,18 +85,24 @@ function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDesc
|
|
|
65
85
|
}
|
|
66
86
|
});
|
|
67
87
|
|
|
88
|
+
// Apply replacements (reverse order)
|
|
89
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
90
|
+
replacements.forEach(r => {
|
|
91
|
+
scriptBody = scriptBody.substring(0, r.start) + r.text + scriptBody.substring(r.end);
|
|
92
|
+
});
|
|
93
|
+
|
|
68
94
|
// Reconstruct
|
|
69
|
-
// We import defineComponent from mulanjs if not present?
|
|
95
|
+
// We import defineComponent from @mulanjs/mulanjs if not present?
|
|
70
96
|
// Ideally user imports what they need, but for 'setup' wrapping we need defineComponent.
|
|
71
97
|
// Let's assume user might not have imported it, so we import it as _defineComponent to avoid collision.
|
|
72
98
|
|
|
73
99
|
// Check if defineComponent is imported
|
|
74
100
|
const hasDefineComponent = imports.some(i => i.includes('defineComponent'));
|
|
75
|
-
const bootImports = hasDefineComponent ? '' : `import { defineComponent as _defineComponent } from 'mulanjs';`;
|
|
101
|
+
const bootImports = hasDefineComponent ? '' : `import { defineComponent as _defineComponent } from '@mulanjs/mulanjs';`;
|
|
76
102
|
|
|
77
103
|
// ADDED: Check if we introduced 'muState' but it wasn't imported (because user used $)
|
|
78
104
|
const usesMuState = transformedContent.includes('muState');
|
|
79
|
-
const hasMuStateImport = imports.some(i => i.includes('muState') || i.includes('mulanjs'));
|
|
105
|
+
const hasMuStateImport = imports.some(i => i.includes('muState') || i.includes('@mulanjs/mulanjs'));
|
|
80
106
|
let autoImports = (usesMuState && !hasMuStateImport) ? [`muState`] : [];
|
|
81
107
|
|
|
82
108
|
// Auto-import common hooks
|
|
@@ -90,7 +116,7 @@ function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDesc
|
|
|
90
116
|
});
|
|
91
117
|
|
|
92
118
|
const autoImportStmt = autoImports.length > 0
|
|
93
|
-
? `import { ${autoImports.join(', ')} } from 'mulanjs';`
|
|
119
|
+
? `import { ${autoImports.join(', ')} } from '@mulanjs/mulanjs';`
|
|
94
120
|
: '';
|
|
95
121
|
|
|
96
122
|
const filename = descriptor.filename;
|
|
@@ -106,32 +132,27 @@ function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDesc
|
|
|
106
132
|
`
|
|
107
133
|
: 'return {};';
|
|
108
134
|
|
|
109
|
-
|
|
135
|
+
// Construct final code with preserved body layout
|
|
136
|
+
const preamble = `
|
|
110
137
|
${bootImports}
|
|
111
138
|
${autoImportStmt}
|
|
112
139
|
${imports.join('\n')}
|
|
113
140
|
|
|
114
141
|
export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
115
142
|
setup() {
|
|
116
|
-
|
|
143
|
+
`;
|
|
144
|
+
// We intentionally put scriptBody on a new line.
|
|
145
|
+
// Note: scriptBody has imports replaced by whitespace/newlines, so its internal line numbers relative to its start are preserved.
|
|
146
|
+
|
|
147
|
+
const postamble = `
|
|
117
148
|
${bindingString}
|
|
118
149
|
}
|
|
119
150
|
});
|
|
120
151
|
`;
|
|
121
152
|
|
|
122
|
-
|
|
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.
|
|
153
|
+
const finalCode = preamble + scriptBody + postamble;
|
|
134
154
|
|
|
155
|
+
if (isTs) {
|
|
135
156
|
// Transpile extracted TS code to JS
|
|
136
157
|
const result = ts.transpileModule(finalCode, {
|
|
137
158
|
compilerOptions: {
|
|
@@ -141,7 +162,7 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
|
141
162
|
inlineSources: true,
|
|
142
163
|
sourceRoot: '/',
|
|
143
164
|
},
|
|
144
|
-
fileName: filename //
|
|
165
|
+
fileName: filename // We ask TS to map to filename, but it maps 'finalCode' -> 'filename'
|
|
145
166
|
});
|
|
146
167
|
|
|
147
168
|
// Fix: Remove the //# sourceMappingURL= comment added by TS
|
|
@@ -151,23 +172,67 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
|
|
|
151
172
|
let finalMap = result.sourceMapText;
|
|
152
173
|
if (finalMap) {
|
|
153
174
|
try {
|
|
154
|
-
|
|
175
|
+
// Determine offsets
|
|
176
|
+
const preambleLines = preamble.split('\n').length - 1;
|
|
177
|
+
// scriptBody starts at line `preambleLines` (0-indexed) in finalCode.
|
|
178
|
+
|
|
179
|
+
// Determine where script starts in original .mujs file
|
|
180
|
+
const originalSource = descriptor.source;
|
|
181
|
+
const linesBeforeScript = originalSource.substring(0, script.start).split('\n').length - 1;
|
|
182
|
+
|
|
183
|
+
const consumer = await new SourceMapConsumer(finalMap);
|
|
184
|
+
const generator = new SourceMapGenerator({
|
|
185
|
+
file: filename,
|
|
186
|
+
sourceRoot: ''
|
|
187
|
+
});
|
|
155
188
|
|
|
156
|
-
//
|
|
189
|
+
// If inside src, use path starting from src. Else just basename.
|
|
157
190
|
const normalizedPath = filename.replace(/\\/g, '/');
|
|
158
191
|
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
192
|
let relativePath = srcIndex !== -1
|
|
163
193
|
? normalizedPath.substring(srcIndex + 1)
|
|
164
194
|
: filename.split(/[/\\]/).pop() || 'unknown.mujs';
|
|
165
195
|
|
|
166
|
-
|
|
167
|
-
|
|
196
|
+
generator.setSourceContent(relativePath, originalSource);
|
|
197
|
+
|
|
198
|
+
consumer.eachMapping(m => {
|
|
199
|
+
if (m.originalLine === null) return;
|
|
200
|
+
|
|
201
|
+
// m.originalLine is the line in `finalCode` (1-based because SourceMapConsumer uses 1-based)
|
|
202
|
+
// We need to convert it to line in `.mujs`.
|
|
203
|
+
|
|
204
|
+
// Check if this line belongs to scriptBody
|
|
205
|
+
// scriptBody spans from (preambleLines + 1) to (preambleLines + scriptBodyLines)
|
|
206
|
+
const lineInFinalCode = m.originalLine;
|
|
207
|
+
|
|
208
|
+
// Check if strictly inside body
|
|
209
|
+
if (lineInFinalCode > preambleLines) {
|
|
210
|
+
// Calculate offset inside scriptBody
|
|
211
|
+
const lineOffsetInBody = lineInFinalCode - preambleLines; // 1-based offset (1st line of body is 1)
|
|
212
|
+
|
|
213
|
+
// Calculate original line in .mujs
|
|
214
|
+
// If lineOffsetInBody is 1 (first line of body), it corresponds to (linesBeforeScript + 1)
|
|
215
|
+
const originalLineInMujs = linesBeforeScript + lineOffsetInBody;
|
|
216
|
+
|
|
217
|
+
// Add mapping
|
|
218
|
+
generator.addMapping({
|
|
219
|
+
generated: {
|
|
220
|
+
line: m.generatedLine,
|
|
221
|
+
column: m.generatedColumn
|
|
222
|
+
},
|
|
223
|
+
original: {
|
|
224
|
+
line: originalLineInMujs,
|
|
225
|
+
column: m.originalColumn // Column should be preserved if we didn't change indentation
|
|
226
|
+
},
|
|
227
|
+
source: relativePath,
|
|
228
|
+
name: m.name
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
finalMap = generator.toString();
|
|
234
|
+
consumer.destroy();
|
|
168
235
|
|
|
169
|
-
mapObj.sourcesContent = [descriptor.source];
|
|
170
|
-
finalMap = JSON.stringify(mapObj);
|
|
171
236
|
} catch (e) {
|
|
172
237
|
console.warn('[MulanJS] Failed to patch source map:', e);
|
|
173
238
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMUJS = parseMUJS;
|
|
4
|
+
/**
|
|
5
|
+
* Standardized State-Machine Parser for MulanJS
|
|
6
|
+
* Parses .mujs files safely, handling attributes, quotes, and nested content.
|
|
7
|
+
*/
|
|
8
|
+
function parseMUJS(source, filename) {
|
|
9
|
+
const descriptor = {
|
|
10
|
+
filename,
|
|
11
|
+
source,
|
|
12
|
+
template: null,
|
|
13
|
+
script: null,
|
|
14
|
+
scripts: [],
|
|
15
|
+
styles: [],
|
|
16
|
+
customBlocks: []
|
|
17
|
+
};
|
|
18
|
+
let cursor = 0;
|
|
19
|
+
const length = source.length;
|
|
20
|
+
while (cursor < length) {
|
|
21
|
+
// Look for start tag '<'
|
|
22
|
+
const start = source.indexOf('<', cursor);
|
|
23
|
+
if (start === -1)
|
|
24
|
+
break; // End of file
|
|
25
|
+
// Must be safe start (not in comment/string - simplified for top-level blocks)
|
|
26
|
+
// We assume SFC top-level blocks are valid HTML-like tags.
|
|
27
|
+
// Check if closing tag (ignore)
|
|
28
|
+
if (source[start + 1] === '/') {
|
|
29
|
+
cursor = start + 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Parse Tag Name
|
|
33
|
+
let nameEnd = start + 1;
|
|
34
|
+
while (nameEnd < length && !/\s|>/.test(source[nameEnd])) {
|
|
35
|
+
nameEnd++;
|
|
36
|
+
}
|
|
37
|
+
const tagName = source.slice(start + 1, nameEnd);
|
|
38
|
+
// Parse Attributes
|
|
39
|
+
let attrStart = nameEnd;
|
|
40
|
+
const attrs = {};
|
|
41
|
+
let tagEnd = source.indexOf('>', attrStart);
|
|
42
|
+
if (tagEnd === -1)
|
|
43
|
+
break;
|
|
44
|
+
const attrString = source.slice(attrStart, tagEnd);
|
|
45
|
+
// Simple regex for attrs is safe *inside* the tag definition
|
|
46
|
+
const attrRegex = /([a-zA-Z0-9:-]+)(?:=["']([^"']*)["'])?/g;
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = attrRegex.exec(attrString)) !== null) {
|
|
49
|
+
attrs[match[1]] = match[2] || 'true';
|
|
50
|
+
}
|
|
51
|
+
// Find Closing Tag
|
|
52
|
+
// We need to handle nested content.
|
|
53
|
+
// For <template>, we just look for </template>.
|
|
54
|
+
// For <script>, same.
|
|
55
|
+
// "Naive" approach: indexOf('</' + tagName) is actually standard for SFC parsers
|
|
56
|
+
// because top-level blocks shouldn't contain their own closing tag as text
|
|
57
|
+
// (except unless escaped, which we can handle if needed, but rarely an issue for top-level).
|
|
58
|
+
const contentStart = tagEnd + 1;
|
|
59
|
+
const closeTag = `</${tagName}>`;
|
|
60
|
+
const contentEnd = source.indexOf(closeTag, contentStart);
|
|
61
|
+
if (contentEnd === -1) {
|
|
62
|
+
// Unclosed block, invalid SFC.
|
|
63
|
+
console.warn(`[MulanJS Parser] Unclosed block: <${tagName}>`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const content = source.slice(contentStart, contentEnd);
|
|
67
|
+
const block = {
|
|
68
|
+
type: tagName,
|
|
69
|
+
content: content.trim(), // Trim content for cleanliness
|
|
70
|
+
attrs,
|
|
71
|
+
start: start,
|
|
72
|
+
end: contentEnd + closeTag.length
|
|
73
|
+
};
|
|
74
|
+
// Add to descriptor
|
|
75
|
+
if (tagName === 'template' || tagName === 'mu-template') {
|
|
76
|
+
descriptor.template = block;
|
|
77
|
+
}
|
|
78
|
+
else if (tagName === 'script') {
|
|
79
|
+
descriptor.scripts.push(block);
|
|
80
|
+
if (attrs.setup || !descriptor.script) {
|
|
81
|
+
descriptor.script = block;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (tagName === 'style') {
|
|
85
|
+
descriptor.styles.push(block);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
descriptor.customBlocks.push(block);
|
|
89
|
+
}
|
|
90
|
+
cursor = contentEnd + closeTag.length;
|
|
91
|
+
}
|
|
92
|
+
return descriptor;
|
|
93
|
+
}
|
package/src/loader/index.js
CHANGED
|
@@ -58,9 +58,11 @@ module.exports = function (content) {
|
|
|
58
58
|
const compilerUrl = pathToFileURL(compilerRef).href;
|
|
59
59
|
|
|
60
60
|
// Use dynamic import to load ESM compiler from CommonJS loader
|
|
61
|
-
|
|
61
|
+
|
|
62
|
+
// Use dynamic import to load ESM compiler from CommonJS loader
|
|
63
|
+
import(compilerUrl).then(async compiler => {
|
|
62
64
|
try {
|
|
63
|
-
const result = compiler.compileSFC(content, filePath);
|
|
65
|
+
const result = await compiler.compileSFC(content, filePath);
|
|
64
66
|
|
|
65
67
|
if (result.errors && result.errors.length > 0) {
|
|
66
68
|
this.emitError(new Error(result.errors.join('\n')));
|