@mulanjs/mulanjs 1.0.1-dev.20260219164037 → 1.0.1-dev.20260219183205

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/README.md CHANGED
@@ -16,6 +16,7 @@ MulanJS is a high-performance, next-generation web framework designed to bridge
16
16
  > Coming soon!
17
17
 
18
18
  ### 🚧 Development Release (Bleeding Edge)
19
+ **Current Version**: `1.0.1-dev.20260219182444`
19
20
 
20
21
  Want to try the latest features before they are stable? Use the development build.
21
22
 
@@ -5,11 +5,12 @@ 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
- async function compileSFC(source, filename) {
8
+ const source_map_1 = require("source-map");
9
+ async function compileSFC(source, filename, options) {
9
10
  // 1. Parse
10
11
  const descriptor = (0, sfc_parser_1.parseMUJS)(source, filename);
11
12
  // 2. Script
12
- const scriptResult = await (0, script_compiler_1.compileScript)(descriptor);
13
+ const scriptResult = await (0, script_compiler_1.compileScript)(descriptor, options);
13
14
  // Replace export default to capture the component
14
15
  // If the script already has 'export default', we intercept it.
15
16
  let scriptCode = scriptResult.code.replace('export default', 'const __component__ =');
@@ -17,9 +18,85 @@ async function compileSFC(source, filename) {
17
18
  scriptCode = scriptCode.replace(/export\s+default/, 'const __component__ =');
18
19
  }
19
20
  // 3. Style
20
- const styleResult = (0, style_compiler_1.compileStyle)(descriptor, filename);
21
+ const styleResult = (0, style_compiler_1.compileStyle)(descriptor, filename, options);
21
22
  // 4. Template
22
23
  const templateResult = (0, template_compiler_1.compileTemplate)(descriptor, scriptResult, styleResult.scopedId);
24
+ // Calculate offsets for source map merging
25
+ // 1. Script Code
26
+ // 2. Padding (newlines)
27
+ // 3. Template Code
28
+ // We need to count lines in scriptCode to know where template code starts in the final file
29
+ const scriptLineCount = scriptCode.split(/\r?\n/).length;
30
+ const paddingLines = 2; // Two newlines between script and template
31
+ const templateOffset = scriptLineCount + paddingLines;
32
+ let mergedMap;
33
+ // Merge Maps
34
+ if (filename) {
35
+ const generator = new source_map_1.SourceMapGenerator({
36
+ file: filename + '.js',
37
+ });
38
+ // 1. Apply Script Map
39
+ if (scriptResult.map) {
40
+ const scriptConsumer = await new source_map_1.SourceMapConsumer(JSON.parse(scriptResult.map));
41
+ scriptConsumer.eachMapping(mapping => {
42
+ if (mapping.source) {
43
+ generator.addMapping({
44
+ generated: {
45
+ line: mapping.generatedLine, // Script is at the top, so no offset needed
46
+ column: mapping.generatedColumn
47
+ },
48
+ original: {
49
+ line: mapping.originalLine,
50
+ column: mapping.originalColumn
51
+ },
52
+ source: mapping.source,
53
+ name: mapping.name
54
+ });
55
+ }
56
+ });
57
+ // Copy source content from script map
58
+ if (scriptConsumer.sources) {
59
+ scriptConsumer.sources.forEach(sourceFile => {
60
+ const content = scriptConsumer.sourceContentFor(sourceFile);
61
+ if (content) {
62
+ generator.setSourceContent(sourceFile, content);
63
+ }
64
+ });
65
+ }
66
+ scriptConsumer.destroy();
67
+ }
68
+ // 2. Apply Template Map
69
+ if (templateResult.map) {
70
+ const templateConsumer = await new source_map_1.SourceMapConsumer(JSON.parse(templateResult.map));
71
+ templateConsumer.eachMapping(mapping => {
72
+ if (mapping.source) {
73
+ generator.addMapping({
74
+ generated: {
75
+ line: mapping.generatedLine + templateOffset, // Shift by script length
76
+ column: mapping.generatedColumn
77
+ },
78
+ original: {
79
+ line: mapping.originalLine, // Original line in .mujs
80
+ column: mapping.originalColumn
81
+ },
82
+ source: mapping.source, // This should be the .mujs file
83
+ name: mapping.name
84
+ });
85
+ }
86
+ });
87
+ // Copy source content from template map
88
+ if (templateConsumer.sources) {
89
+ templateConsumer.sources.forEach(sourceFile => {
90
+ const content = templateConsumer.sourceContentFor(sourceFile);
91
+ if (content) {
92
+ generator.setSourceContent(sourceFile, content);
93
+ }
94
+ });
95
+ }
96
+ templateConsumer.destroy();
97
+ }
98
+ mergedMap = generator.toString();
99
+ }
23
100
  // 5. Assembly
24
101
  const cssInjection = styleResult.css ? `
25
102
  if (typeof document !== 'undefined') {
@@ -86,6 +163,6 @@ export default __component__;
86
163
  code: finalCode,
87
164
  css: styleResult.css,
88
165
  errors: [...scriptResult.errors, ...templateResult.errors],
89
- map: scriptResult.map // Pass the map
166
+ map: mergedMap || scriptResult.map // Fallback to script map if merge fails
90
167
  };
91
168
  }
@@ -36,27 +36,55 @@ 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
+ const path = __importStar(require("path"));
40
+ async function compileScript(descriptor, options) {
40
41
  const script = descriptor.script;
41
42
  if (!script)
42
43
  return { code: 'export default {}', errors: [] };
44
+ let content = script.content;
45
+ let filename = descriptor.filename;
46
+ let isExternal = false;
47
+ let externalPath = '';
48
+ // Handle External Script
49
+ if (script.attrs.src) {
50
+ if (options && options.readFileSync) {
51
+ try {
52
+ // Resolve path relative to the SFC file
53
+ const sfcDir = path.dirname(descriptor.filename);
54
+ externalPath = path.resolve(sfcDir, script.attrs.src);
55
+ content = options.readFileSync(externalPath);
56
+ // Keep filename as .mujs for now, but we will use externalPath for source mapping of the content
57
+ // Actually, if we want the external file to show up as ITSELF in devtools, we should map to externalPath.
58
+ isExternal = true;
59
+ }
60
+ catch (e) {
61
+ return { code: 'export default {}', errors: [`Failed to read external script: ${e.message}`] };
62
+ }
63
+ }
64
+ else {
65
+ return { code: 'export default {}', errors: [`External script found but no readFileSync option provided.`] };
66
+ }
67
+ }
43
68
  const isSetup = !!script.attrs.setup;
44
- const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx';
69
+ const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx' || externalPath.endsWith('.ts') || externalPath.endsWith('.tsx');
45
70
  if (isSetup) {
46
- return await compileSetupScript(script, isTs, descriptor);
71
+ return await compileSetupScript(script, isTs, descriptor, content, filename, isExternal, externalPath);
72
+ }
73
+ else if (isTs) {
74
+ return await compileStandardScript(script, descriptor, content, filename, isExternal, externalPath);
47
75
  }
48
76
  else {
49
77
  // Standard Options API - just pass through, assuming it has export default
50
- return { code: script.content, errors: [] };
78
+ return { code: content, errors: [] };
51
79
  }
52
80
  }
53
81
  // Global list of Mulan hooks for auto-imports and reactivity exemption
54
82
  const COMMON_HOOKS = ['muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo', 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent'];
55
- async function compileSetupScript(script, isTs, descriptor) {
83
+ async function compileSetupScript(script, isTs, descriptor, content, filename, isExternal, externalPath) {
56
84
  var _a;
57
85
  // 1. Pre-process: Natural Reactivity Transformation ($ syntax)
58
86
  // We do this BEFORE extraction so that the rest of the compiler sees standard 'muState' code.
59
- const transformedContent = transformNaturalReactivity(script.content, isTs);
87
+ const transformedContent = transformNaturalReactivity(content, isTs);
60
88
  // 2. Parse the TRANSFORMED code
61
89
  const sourceFile = ts.createSourceFile('script.' + (isTs ? 'ts' : 'js'), transformedContent, ts.ScriptTarget.Latest, true);
62
90
  const imports = [];
@@ -129,7 +157,6 @@ async function compileSetupScript(script, isTs, descriptor) {
129
157
  const autoImportStmt = autoImports.length > 0
130
158
  ? `import { ${autoImports.join(', ')} } from '@mulanjs/mulanjs';`
131
159
  : '';
132
- const filename = descriptor.filename;
133
160
  const componentName = filename ? ((_a = filename.split(/[/\\]/).pop()) === null || _a === void 0 ? void 0 : _a.split('.')[0].replace(/\W/g, '')) || 'App' : 'App';
134
161
  const bindingString = bindings.length > 0
135
162
  ? `
@@ -171,27 +198,53 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
171
198
  });
172
199
  // Fix: Remove the //# sourceMappingURL= comment added by TS
173
200
  const codeWithoutMapComment = result.outputText.replace(/\/\/# sourceMappingURL=.*$/gm, '');
174
- // Enhance Source Map to show ORIGINAL .mujs content
201
+ // Enhance Source Map to show ORIGINAL .mujs content or EXTERNAL content
175
202
  let finalMap = result.sourceMapText;
176
203
  if (finalMap) {
177
204
  try {
178
205
  // Determine offsets
179
206
  const preambleLines = preamble.split('\n').length - 1;
180
207
  // 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;
208
+ // Determine where script starts
209
+ // If External: Start at 0
210
+ // If Inline: Start at script.start line
211
+ let linesBeforeScript = 0;
212
+ let originalSource = content;
213
+ if (!isExternal) {
214
+ originalSource = descriptor.source;
215
+ linesBeforeScript = originalSource.substring(0, script.start).split('\n').length - 1;
216
+ }
184
217
  const consumer = await new source_map_1.SourceMapConsumer(finalMap);
185
218
  const generator = new source_map_1.SourceMapGenerator({
186
219
  file: filename,
187
220
  sourceRoot: ''
188
221
  });
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';
222
+ // Determine relative path for source
223
+ let relativePath = filename.split(/[/\\]/).pop() || 'unknown.mujs'; // Default to basename
224
+ if (isExternal) {
225
+ // For external files, we want Source Map to point to the actual TS file
226
+ // externalPath is absolute. Make it relative to project root or something reasonable?
227
+ // Webpack often likes 'webpack:///[resource-path]'
228
+ // We'll try to map it so it looks like a separate file
229
+ const normalized = externalPath.replace(/\\/g, '/');
230
+ const srcIdx = normalized.indexOf('/src/');
231
+ if (srcIdx !== -1) {
232
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1); // e.g. webpack:///src/logic.ts
233
+ }
234
+ else {
235
+ relativePath = 'webpack:///' + path.basename(externalPath);
236
+ }
237
+ }
238
+ else {
239
+ // Inline: Point to .mujs file
240
+ const normalized = filename.replace(/\\/g, '/');
241
+ const srcIdx = normalized.indexOf('/src/');
242
+ if (srcIdx !== -1) {
243
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1); // e.g. webpack:///src/App.mujs
244
+ }
245
+ }
246
+ // If inline, we set source content of .mujs (the whole file)
247
+ // If external, we set source content of the .ts file
195
248
  generator.setSourceContent(relativePath, originalSource);
196
249
  consumer.eachMapping(m => {
197
250
  if (m.originalLine === null)
@@ -205,9 +258,10 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
205
258
  if (lineInFinalCode > preambleLines) {
206
259
  // Calculate offset inside scriptBody
207
260
  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;
261
+ // Calculate original line
262
+ // If External: lineOffsetInBody is the line number
263
+ // If Inline: linesBeforeScript + lineOffsetInBody
264
+ const originalLine = linesBeforeScript + lineOffsetInBody;
211
265
  // Add mapping
212
266
  generator.addMapping({
213
267
  generated: {
@@ -215,7 +269,7 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
215
269
  column: m.generatedColumn
216
270
  },
217
271
  original: {
218
- line: originalLineInMujs,
272
+ line: originalLine,
219
273
  column: m.originalColumn // Column should be preserved if we didn't change indentation
220
274
  },
221
275
  source: relativePath,
@@ -244,6 +298,89 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
244
298
  map: undefined
245
299
  };
246
300
  }
301
+ async function compileStandardScript(script, descriptor, content, filename, isExternal, externalPath) {
302
+ // For standard script, we also might need source map if we transpiled it (e.g. from TS)
303
+ // and/or just to map it back to .mujs
304
+ let finalMap;
305
+ let code = content;
306
+ // Transpile if TS
307
+ if (externalPath.endsWith('.ts') || script.attrs.lang === 'ts') {
308
+ const result = ts.transpileModule(content, {
309
+ compilerOptions: {
310
+ module: ts.ModuleKind.ESNext,
311
+ target: ts.ScriptTarget.ES2019,
312
+ sourceMap: true,
313
+ inlineSources: true,
314
+ },
315
+ fileName: filename
316
+ });
317
+ code = result.outputText.replace(/\/\/# sourceMappingURL=.*$/gm, '');
318
+ finalMap = result.sourceMapText;
319
+ }
320
+ // Adjust Source Map
321
+ if (finalMap) {
322
+ try {
323
+ // Standard script content is usually 1:1 with source lines if we ignore transpilation shifts (which TS handles)
324
+ // But if it's inline, TS thinks it starts at line 0. We need to shift it by `linesBeforeScript`.
325
+ let linesBeforeScript = 0;
326
+ let originalSource = content;
327
+ if (!isExternal) {
328
+ originalSource = descriptor.source;
329
+ linesBeforeScript = descriptor.source.substring(0, script.start).split('\n').length - 1;
330
+ }
331
+ const consumer = await new source_map_1.SourceMapConsumer(finalMap);
332
+ const generator = new source_map_1.SourceMapGenerator({
333
+ file: filename,
334
+ sourceRoot: ''
335
+ });
336
+ // Determine relative path for source (same logic as Setup)
337
+ let relativePath = filename.split(/[/\\]/).pop() || 'unknown.mujs';
338
+ if (isExternal) {
339
+ const normalized = externalPath.replace(/\\/g, '/');
340
+ const srcIdx = normalized.indexOf('/src/');
341
+ if (srcIdx !== -1) {
342
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1);
343
+ }
344
+ else {
345
+ relativePath = 'webpack:///' + path.basename(externalPath);
346
+ }
347
+ }
348
+ else {
349
+ const normalized = filename.replace(/\\/g, '/');
350
+ const srcIdx = normalized.indexOf('/src/');
351
+ if (srcIdx !== -1) {
352
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1);
353
+ }
354
+ }
355
+ generator.setSourceContent(relativePath, originalSource);
356
+ consumer.eachMapping(m => {
357
+ if (m.originalLine === null)
358
+ return;
359
+ const originalLine = m.originalLine + linesBeforeScript;
360
+ generator.addMapping({
361
+ generated: {
362
+ line: m.generatedLine,
363
+ column: m.generatedColumn
364
+ },
365
+ original: {
366
+ line: originalLine,
367
+ column: m.originalColumn
368
+ },
369
+ source: relativePath,
370
+ name: m.name
371
+ });
372
+ });
373
+ finalMap = generator.toString();
374
+ consumer.destroy();
375
+ }
376
+ catch (e) { /* ignore */ }
377
+ }
378
+ return {
379
+ code: code,
380
+ errors: [],
381
+ map: finalMap
382
+ };
383
+ }
247
384
  // --- Natural Reactivity Transformer ---
248
385
  function transformNaturalReactivity(code, isTs) {
249
386
  // If no '$(' or '$q(' or '$qr(' usage, return original code for safety/speed
@@ -1,7 +1,41 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.compileStyle = compileStyle;
4
- function compileStyle(descriptor, filename) {
37
+ const path = __importStar(require("path"));
38
+ function compileStyle(descriptor, filename, options) {
5
39
  const styles = descriptor.styles;
6
40
  if (!styles || styles.length === 0)
7
41
  return { css: '', errors: [] };
@@ -9,18 +43,41 @@ function compileStyle(descriptor, filename) {
9
43
  const scopedId = 'data-v-' + hash(filename);
10
44
  styles.forEach(style => {
11
45
  let content = style.content;
46
+ // Handle External Style
47
+ if (style.attrs.src) {
48
+ if (options && options.readFileSync) {
49
+ try {
50
+ const sfcDir = path.dirname(filename);
51
+ const externalPath = path.resolve(sfcDir, style.attrs.src);
52
+ content = options.readFileSync(externalPath);
53
+ }
54
+ catch (e) {
55
+ // Start error with "Style Compilation Error" to be consistent? Or just add to errors list.
56
+ // But we are inside forEach, so we can't return easily. We'll add a comment in CSS or log.
57
+ // Better to collect errors.
58
+ // But function signature returns { errors: string[] }.
59
+ // We'll just append to errors? But we need to change forEach loop or use functional approach.
60
+ // For now, let's just ignore/log in CSS content?
61
+ content = `/* Failed to read external style: ${e.message} */`;
62
+ }
63
+ }
64
+ else {
65
+ content = `/* External style found but no readFileSync option provided */`;
66
+ }
67
+ }
12
68
  if (style.attrs.scoped) {
13
69
  // SCSS Compilation
14
- if (style.attrs.lang === 'scss' || style.attrs.lang === 'sass') {
70
+ if (style.attrs.lang === 'scss' || style.attrs.lang === 'sass' || (style.attrs.src && (style.attrs.src.endsWith('.scss') || style.attrs.src.endsWith('.sass')))) {
15
71
  try {
16
72
  const sass = require('sass');
17
73
  const result = sass.compileString(content, {
18
- syntax: style.attrs.lang === 'sass' ? 'indented' : 'scss'
74
+ syntax: (style.attrs.lang === 'sass' || (style.attrs.src && style.attrs.src.endsWith('.sass'))) ? 'indented' : 'scss'
19
75
  });
20
76
  content = result.css;
21
77
  }
22
78
  catch (e) {
23
- return { css: '', errors: [`SCSS Compilation Error: ${e.message}`] };
79
+ // return { css: '', errors: [`SCSS Compilation Error: ${e.message}`] };
80
+ content = `/* SCSS Compilation Error: ${e.message} */`;
24
81
  }
25
82
  }
26
83
  // Rewrite selectors
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.compileTemplate = compileTemplate;
4
+ // --- Compile Function ---
5
+ const source_map_1 = require("source-map");
4
6
  function compileTemplate(descriptor, scriptResult, scopedId) {
5
7
  console.log(`[MulanJS Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
6
8
  const template = descriptor.template;
@@ -14,8 +16,7 @@ function compileTemplate(descriptor, scriptResult, scopedId) {
14
16
  transform(ast, scriptResult, scopedId, [], descriptor.filename);
15
17
  // 3. Codegen Phase (AST -> JS Function)
16
18
  const code = generate(ast, scriptResult.bindings || []);
17
- return {
18
- code: `function render() {
19
+ const renderFn = `function render() {
19
20
  const _s = (v) => (v && typeof v === 'object' && 'value' in v) ? v.value : v;
20
21
  const _h = (v, raw) => {
21
22
  const val = _s(v);
@@ -27,8 +28,47 @@ function compileTemplate(descriptor, scriptResult, scopedId) {
27
28
  return val.replace(/</g, '&lt;').replace(/>/g, '&gt;');
28
29
  };
29
30
  return ${code};
30
- }`,
31
- errors
31
+ }`;
32
+ // 4. Source Map Generation
33
+ // We map the entire render function to the starting line of the template in the source file
34
+ let map = undefined;
35
+ if (descriptor.filename) {
36
+ const generator = new source_map_1.SourceMapGenerator({
37
+ file: descriptor.filename + '.template.js', // Virtual file name
38
+ });
39
+ // Find start line of template in original source
40
+ // descriptor.template.start is the index of content start
41
+ const sourceBefore = descriptor.source.substring(0, template.start);
42
+ const startLine = sourceBefore.split(/\r?\n/).length;
43
+ // Simple mapping: One-to-one mapping isn't easy without AST location tracking
44
+ // But we can map the 'return' statement to the template start
45
+ // The render function has about 12 lines of preamble.
46
+ // Let's just mapping the whole block to the start line for now.
47
+ // This ensures the file shows up in devtools.
48
+ generator.addMapping({
49
+ generated: { line: 1, column: 0 },
50
+ source: descriptor.filename, // This should be the .mujs file path
51
+ original: { line: startLine, column: 0 }
52
+ });
53
+ // Also map the return line (approx line 12)
54
+ generator.addMapping({
55
+ generated: { line: 12, column: 0 },
56
+ source: descriptor.filename,
57
+ original: { line: startLine, column: 0 }
58
+ });
59
+ // We must include the "source content" so DevTools can display it!
60
+ // IMPORTANT: We want the FULL .mujs content here, so it matches what Webpack sees?
61
+ // Actually, if we use the same filename as script-compiler, they might conflict or merge.
62
+ // Script compiler uses 'webpack:///...'
63
+ // Let's use the same convention.
64
+ // But we just use the filename here, compiler.ts will handle the final path adjustment if needed.
65
+ generator.setSourceContent(descriptor.filename, descriptor.source);
66
+ map = generator.toString();
67
+ }
68
+ return {
69
+ code: renderFn,
70
+ errors,
71
+ map
32
72
  };
33
73
  }
34
74
  // --- Parser (Recursive Descent) ---
@@ -1,7 +1,8 @@
1
+ import { CompilerOptions } from './script-compiler';
1
2
  export interface CompileResult {
2
3
  code: string;
3
4
  css: string;
4
5
  errors: string[];
5
6
  map?: string;
6
7
  }
7
- export declare function compileSFC(source: string, filename: string): Promise<CompileResult>;
8
+ export declare function compileSFC(source: string, filename: string, options?: CompilerOptions): Promise<CompileResult>;
@@ -5,4 +5,7 @@ export interface ScriptCompileResult {
5
5
  errors: string[];
6
6
  map?: string;
7
7
  }
8
- export declare function compileScript(descriptor: SFCDescriptor): Promise<ScriptCompileResult>;
8
+ export interface CompilerOptions {
9
+ readFileSync?: (file: string) => string;
10
+ }
11
+ export declare function compileScript(descriptor: SFCDescriptor, options?: CompilerOptions): Promise<ScriptCompileResult>;
@@ -1,7 +1,8 @@
1
1
  import { SFCDescriptor } from './sfc-parser';
2
+ import { CompilerOptions } from './script-compiler';
2
3
  export interface StyleCompileResult {
3
4
  css: string;
4
5
  scopedId?: string;
5
6
  errors: string[];
6
7
  }
7
- export declare function compileStyle(descriptor: SFCDescriptor, filename: string): StyleCompileResult;
8
+ export declare function compileStyle(descriptor: SFCDescriptor, filename: string, options?: CompilerOptions): StyleCompileResult;
@@ -3,5 +3,6 @@ import { ScriptCompileResult } from './script-compiler';
3
3
  export interface TemplateCompileResult {
4
4
  code: string;
5
5
  errors: string[];
6
+ map?: string;
6
7
  }
7
8
  export declare function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptCompileResult, scopedId?: string): TemplateCompileResult;
@@ -1,7 +1,8 @@
1
+ import { CompilerOptions } from './script-compiler';
1
2
  export interface CompileResult {
2
3
  code: string;
3
4
  css: string;
4
5
  errors: string[];
5
6
  map?: string;
6
7
  }
7
- export declare function compileSFC(source: string, filename: string): Promise<CompileResult>;
8
+ export declare function compileSFC(source: string, filename: string, options?: CompilerOptions): Promise<CompileResult>;
@@ -5,4 +5,7 @@ export interface ScriptCompileResult {
5
5
  errors: string[];
6
6
  map?: string;
7
7
  }
8
- export declare function compileScript(descriptor: SFCDescriptor): Promise<ScriptCompileResult>;
8
+ export interface CompilerOptions {
9
+ readFileSync?: (file: string) => string;
10
+ }
11
+ export declare function compileScript(descriptor: SFCDescriptor, options?: CompilerOptions): Promise<ScriptCompileResult>;
@@ -1,7 +1,8 @@
1
1
  import { SFCDescriptor } from './sfc-parser';
2
+ import { CompilerOptions } from './script-compiler';
2
3
  export interface StyleCompileResult {
3
4
  css: string;
4
5
  scopedId?: string;
5
6
  errors: string[];
6
7
  }
7
- export declare function compileStyle(descriptor: SFCDescriptor, filename: string): StyleCompileResult;
8
+ export declare function compileStyle(descriptor: SFCDescriptor, filename: string, options?: CompilerOptions): StyleCompileResult;
@@ -3,5 +3,6 @@ import { ScriptCompileResult } from './script-compiler';
3
3
  export interface TemplateCompileResult {
4
4
  code: string;
5
5
  errors: string[];
6
+ map?: string;
6
7
  }
7
8
  export declare function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptCompileResult, scopedId?: string): TemplateCompileResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulanjs/mulanjs",
3
- "version": "1.0.1-dev.20260219164037",
3
+ "version": "1.0.1-dev.20260219183205",
4
4
  "description": "A powerful, secure, and enterprise-grade JavaScript framework.",
5
5
  "main": "dist/mulan.js",
6
6
  "module": "dist/mulan.esm.js",
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { parseMUJS } from './sfc-parser';
3
- import { compileScript } from './script-compiler';
3
+ import { compileScript, CompilerOptions } from './script-compiler';
4
4
  import { compileTemplate } from './template-compiler';
5
5
  import { compileStyle } from './style-compiler';
6
6
 
@@ -11,12 +11,14 @@ export interface CompileResult {
11
11
  map?: string; // Source Map
12
12
  }
13
13
 
14
- export async function compileSFC(source: string, filename: string): Promise<CompileResult> {
14
+ import { SourceMapGenerator, SourceMapConsumer } from 'source-map';
15
+
16
+ export async function compileSFC(source: string, filename: string, options?: CompilerOptions): Promise<CompileResult> {
15
17
  // 1. Parse
16
18
  const descriptor = parseMUJS(source, filename);
17
19
 
18
20
  // 2. Script
19
- const scriptResult = await compileScript(descriptor);
21
+ const scriptResult = await compileScript(descriptor, options);
20
22
  // Replace export default to capture the component
21
23
  // If the script already has 'export default', we intercept it.
22
24
  let scriptCode = scriptResult.code.replace('export default', 'const __component__ =');
@@ -25,11 +27,94 @@ export async function compileSFC(source: string, filename: string): Promise<Comp
25
27
  }
26
28
 
27
29
  // 3. Style
28
- const styleResult = compileStyle(descriptor, filename);
30
+ const styleResult = compileStyle(descriptor, filename, options);
29
31
 
30
32
  // 4. Template
31
33
  const templateResult = compileTemplate(descriptor, scriptResult, styleResult.scopedId);
32
34
 
35
+ // Calculate offsets for source map merging
36
+ // 1. Script Code
37
+ // 2. Padding (newlines)
38
+ // 3. Template Code
39
+
40
+ // We need to count lines in scriptCode to know where template code starts in the final file
41
+ const scriptLineCount = scriptCode.split(/\r?\n/).length;
42
+ const paddingLines = 2; // Two newlines between script and template
43
+ const templateOffset = scriptLineCount + paddingLines;
44
+
45
+ let mergedMap: string | undefined;
46
+
47
+ // Merge Maps
48
+ if (filename) {
49
+ const generator = new SourceMapGenerator({
50
+ file: filename + '.js',
51
+ });
52
+
53
+ // 1. Apply Script Map
54
+ if (scriptResult.map) {
55
+ const scriptConsumer = await new SourceMapConsumer(JSON.parse(scriptResult.map));
56
+ scriptConsumer.eachMapping(mapping => {
57
+ if (mapping.source) {
58
+ generator.addMapping({
59
+ generated: {
60
+ line: mapping.generatedLine, // Script is at the top, so no offset needed
61
+ column: mapping.generatedColumn
62
+ },
63
+ original: {
64
+ line: mapping.originalLine,
65
+ column: mapping.originalColumn
66
+ },
67
+ source: mapping.source,
68
+ name: mapping.name
69
+ });
70
+ }
71
+ });
72
+ // Copy source content from script map
73
+ if (scriptConsumer.sources) {
74
+ scriptConsumer.sources.forEach(sourceFile => {
75
+ const content = scriptConsumer.sourceContentFor(sourceFile);
76
+ if (content) {
77
+ generator.setSourceContent(sourceFile, content);
78
+ }
79
+ });
80
+ }
81
+ scriptConsumer.destroy();
82
+ }
83
+
84
+ // 2. Apply Template Map
85
+ if (templateResult.map) {
86
+ const templateConsumer = await new SourceMapConsumer(JSON.parse(templateResult.map));
87
+ templateConsumer.eachMapping(mapping => {
88
+ if (mapping.source) {
89
+ generator.addMapping({
90
+ generated: {
91
+ line: mapping.generatedLine + templateOffset, // Shift by script length
92
+ column: mapping.generatedColumn
93
+ },
94
+ original: {
95
+ line: mapping.originalLine, // Original line in .mujs
96
+ column: mapping.originalColumn
97
+ },
98
+ source: mapping.source, // This should be the .mujs file
99
+ name: mapping.name
100
+ });
101
+ }
102
+ });
103
+ // Copy source content from template map
104
+ if (templateConsumer.sources) {
105
+ templateConsumer.sources.forEach(sourceFile => {
106
+ const content = templateConsumer.sourceContentFor(sourceFile);
107
+ if (content) {
108
+ generator.setSourceContent(sourceFile, content);
109
+ }
110
+ });
111
+ }
112
+ templateConsumer.destroy();
113
+ }
114
+
115
+ mergedMap = generator.toString();
116
+ }
117
+
33
118
  // 5. Assembly
34
119
  const cssInjection = styleResult.css ? `
35
120
  if (typeof document !== 'undefined') {
@@ -98,6 +183,6 @@ export default __component__;
98
183
  code: finalCode,
99
184
  css: styleResult.css,
100
185
  errors: [...scriptResult.errors, ...templateResult.errors],
101
- map: scriptResult.map // Pass the map
186
+ map: mergedMap || scriptResult.map // Fallback to script map if merge fails
102
187
  };
103
188
  }
@@ -2,6 +2,7 @@
2
2
  import { SFCDescriptor, SFCBlock } from './sfc-parser';
3
3
  import * as ts from 'typescript';
4
4
  import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
5
+ import * as path from 'path';
5
6
 
6
7
  export interface ScriptCompileResult {
7
8
  code: string;
@@ -10,28 +11,66 @@ export interface ScriptCompileResult {
10
11
  map?: string; // Source Map
11
12
  }
12
13
 
13
- export async function compileScript(descriptor: SFCDescriptor): Promise<ScriptCompileResult> {
14
+ export interface CompilerOptions {
15
+ readFileSync?: (file: string) => string;
16
+ }
17
+
18
+ export async function compileScript(descriptor: SFCDescriptor, options?: CompilerOptions): Promise<ScriptCompileResult> {
14
19
  const script = descriptor.script;
15
20
  if (!script) return { code: 'export default {}', errors: [] };
16
21
 
22
+ let content = script.content;
23
+ let filename = descriptor.filename;
24
+ let isExternal = false;
25
+ let externalPath = '';
26
+
27
+ // Handle External Script
28
+ if (script.attrs.src) {
29
+ if (options && options.readFileSync) {
30
+ try {
31
+ // Resolve path relative to the SFC file
32
+ const sfcDir = path.dirname(descriptor.filename);
33
+ externalPath = path.resolve(sfcDir, script.attrs.src);
34
+ content = options.readFileSync(externalPath);
35
+ // Keep filename as .mujs for now, but we will use externalPath for source mapping of the content
36
+ // Actually, if we want the external file to show up as ITSELF in devtools, we should map to externalPath.
37
+ isExternal = true;
38
+ } catch (e: any) {
39
+ return { code: 'export default {}', errors: [`Failed to read external script: ${e.message}`] };
40
+ }
41
+ } else {
42
+ return { code: 'export default {}', errors: [`External script found but no readFileSync option provided.`] };
43
+ }
44
+ }
45
+
17
46
  const isSetup = !!script.attrs.setup;
18
- const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx';
47
+ const isTs = script.attrs.lang === 'ts' || script.attrs.lang === 'tsx' || externalPath.endsWith('.ts') || externalPath.endsWith('.tsx');
19
48
 
20
49
  if (isSetup) {
21
- return await compileSetupScript(script, isTs, descriptor);
50
+ return await compileSetupScript(script, isTs, descriptor, content, filename, isExternal, externalPath);
51
+ } else if (isTs) {
52
+ return await compileStandardScript(script, descriptor, content, filename, isExternal, externalPath);
22
53
  } else {
23
54
  // Standard Options API - just pass through, assuming it has export default
24
- return { code: script.content, errors: [] };
55
+ return { code: content, errors: [] };
25
56
  }
26
57
  }
27
58
 
28
59
  // Global list of Mulan hooks for auto-imports and reactivity exemption
29
60
  const COMMON_HOOKS = ['muState', 'onMuMount', 'onMuInit', 'onMuDestroy', 'muEffect', 'muMemo', 'muQubit', 'muGate', 'muMeasure', 'muRegister', 'muEntangle', 'persistent'];
30
61
 
31
- async function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: SFCDescriptor): Promise<ScriptCompileResult> {
62
+ async function compileSetupScript(
63
+ script: SFCBlock,
64
+ isTs: boolean,
65
+ descriptor: SFCDescriptor,
66
+ content: string,
67
+ filename: string,
68
+ isExternal: boolean,
69
+ externalPath: string
70
+ ): Promise<ScriptCompileResult> {
32
71
  // 1. Pre-process: Natural Reactivity Transformation ($ syntax)
33
72
  // We do this BEFORE extraction so that the rest of the compiler sees standard 'muState' code.
34
- const transformedContent = transformNaturalReactivity(script.content, isTs);
73
+ const transformedContent = transformNaturalReactivity(content, isTs);
35
74
 
36
75
  // 2. Parse the TRANSFORMED code
37
76
  const sourceFile = ts.createSourceFile(
@@ -119,7 +158,6 @@ async function compileSetupScript(script: SFCBlock, isTs: boolean, descriptor: S
119
158
  ? `import { ${autoImports.join(', ')} } from '@mulanjs/mulanjs';`
120
159
  : '';
121
160
 
122
- const filename = descriptor.filename;
123
161
  const componentName = filename ? filename.split(/[/\\]/).pop()?.split('.')[0].replace(/\W/g, '') || 'App' : 'App';
124
162
 
125
163
  const bindingString = bindings.length > 0
@@ -168,7 +206,7 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
168
206
  // Fix: Remove the //# sourceMappingURL= comment added by TS
169
207
  const codeWithoutMapComment = result.outputText.replace(/\/\/# sourceMappingURL=.*$/gm, '');
170
208
 
171
- // Enhance Source Map to show ORIGINAL .mujs content
209
+ // Enhance Source Map to show ORIGINAL .mujs content or EXTERNAL content
172
210
  let finalMap = result.sourceMapText;
173
211
  if (finalMap) {
174
212
  try {
@@ -176,9 +214,16 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
176
214
  const preambleLines = preamble.split('\n').length - 1;
177
215
  // scriptBody starts at line `preambleLines` (0-indexed) in finalCode.
178
216
 
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;
217
+ // Determine where script starts
218
+ // If External: Start at 0
219
+ // If Inline: Start at script.start line
220
+ let linesBeforeScript = 0;
221
+ let originalSource = content;
222
+
223
+ if (!isExternal) {
224
+ originalSource = descriptor.source;
225
+ linesBeforeScript = originalSource.substring(0, script.start).split('\n').length - 1;
226
+ }
182
227
 
183
228
  const consumer = await new SourceMapConsumer(finalMap);
184
229
  const generator = new SourceMapGenerator({
@@ -186,13 +231,32 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
186
231
  sourceRoot: ''
187
232
  });
188
233
 
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';
234
+ // Determine relative path for source
235
+ let relativePath = filename.split(/[/\\]/).pop() || 'unknown.mujs'; // Default to basename
236
+
237
+ if (isExternal) {
238
+ // For external files, we want Source Map to point to the actual TS file
239
+ // externalPath is absolute. Make it relative to project root or something reasonable?
240
+ // Webpack often likes 'webpack:///[resource-path]'
241
+ // We'll try to map it so it looks like a separate file
242
+ const normalized = externalPath.replace(/\\/g, '/');
243
+ const srcIdx = normalized.indexOf('/src/');
244
+ if (srcIdx !== -1) {
245
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1); // e.g. webpack:///src/logic.ts
246
+ } else {
247
+ relativePath = 'webpack:///' + path.basename(externalPath);
248
+ }
249
+ } else {
250
+ // Inline: Point to .mujs file
251
+ const normalized = filename.replace(/\\/g, '/');
252
+ const srcIdx = normalized.indexOf('/src/');
253
+ if (srcIdx !== -1) {
254
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1); // e.g. webpack:///src/App.mujs
255
+ }
256
+ }
195
257
 
258
+ // If inline, we set source content of .mujs (the whole file)
259
+ // If external, we set source content of the .ts file
196
260
  generator.setSourceContent(relativePath, originalSource);
197
261
 
198
262
  consumer.eachMapping(m => {
@@ -210,9 +274,10 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
210
274
  // Calculate offset inside scriptBody
211
275
  const lineOffsetInBody = lineInFinalCode - preambleLines; // 1-based offset (1st line of body is 1)
212
276
 
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;
277
+ // Calculate original line
278
+ // If External: lineOffsetInBody is the line number
279
+ // If Inline: linesBeforeScript + lineOffsetInBody
280
+ const originalLine = linesBeforeScript + lineOffsetInBody;
216
281
 
217
282
  // Add mapping
218
283
  generator.addMapping({
@@ -221,7 +286,7 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
221
286
  column: m.generatedColumn
222
287
  },
223
288
  original: {
224
- line: originalLineInMujs,
289
+ line: originalLine,
225
290
  column: m.originalColumn // Column should be preserved if we didn't change indentation
226
291
  },
227
292
  source: relativePath,
@@ -254,6 +319,109 @@ export default ${hasDefineComponent ? 'defineComponent' : '_defineComponent'}({
254
319
  };
255
320
  }
256
321
 
322
+ async function compileStandardScript(
323
+ script: SFCBlock,
324
+ descriptor: SFCDescriptor,
325
+ content: string,
326
+ filename: string,
327
+ isExternal: boolean,
328
+ externalPath: string
329
+ ): Promise<ScriptCompileResult> {
330
+
331
+ // For standard script, we also might need source map if we transpiled it (e.g. from TS)
332
+ // and/or just to map it back to .mujs
333
+
334
+ let finalMap: string | undefined;
335
+ let code = content;
336
+
337
+ // Transpile if TS
338
+ if (externalPath.endsWith('.ts') || script.attrs.lang === 'ts') {
339
+ const result = ts.transpileModule(content, {
340
+ compilerOptions: {
341
+ module: ts.ModuleKind.ESNext,
342
+ target: ts.ScriptTarget.ES2019,
343
+ sourceMap: true,
344
+ inlineSources: true,
345
+ },
346
+ fileName: filename
347
+ });
348
+ code = result.outputText.replace(/\/\/# sourceMappingURL=.*$/gm, '');
349
+ finalMap = result.sourceMapText;
350
+ }
351
+
352
+ // Adjust Source Map
353
+ if (finalMap) {
354
+ try {
355
+ // Standard script content is usually 1:1 with source lines if we ignore transpilation shifts (which TS handles)
356
+ // But if it's inline, TS thinks it starts at line 0. We need to shift it by `linesBeforeScript`.
357
+
358
+ let linesBeforeScript = 0;
359
+ let originalSource = content;
360
+
361
+ if (!isExternal) {
362
+ originalSource = descriptor.source;
363
+ linesBeforeScript = descriptor.source.substring(0, script.start).split('\n').length - 1;
364
+ }
365
+
366
+ const consumer = await new SourceMapConsumer(finalMap);
367
+ const generator = new SourceMapGenerator({
368
+ file: filename,
369
+ sourceRoot: ''
370
+ });
371
+
372
+ // Determine relative path for source (same logic as Setup)
373
+ let relativePath = filename.split(/[/\\]/).pop() || 'unknown.mujs';
374
+
375
+ if (isExternal) {
376
+ const normalized = externalPath.replace(/\\/g, '/');
377
+ const srcIdx = normalized.indexOf('/src/');
378
+ if (srcIdx !== -1) {
379
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1);
380
+ } else {
381
+ relativePath = 'webpack:///' + path.basename(externalPath);
382
+ }
383
+ } else {
384
+ const normalized = filename.replace(/\\/g, '/');
385
+ const srcIdx = normalized.indexOf('/src/');
386
+ if (srcIdx !== -1) {
387
+ relativePath = 'webpack:///' + normalized.substring(srcIdx + 1);
388
+ }
389
+ }
390
+
391
+ generator.setSourceContent(relativePath, originalSource);
392
+
393
+ consumer.eachMapping(m => {
394
+ if (m.originalLine === null) return;
395
+
396
+ const originalLine = m.originalLine + linesBeforeScript;
397
+
398
+ generator.addMapping({
399
+ generated: {
400
+ line: m.generatedLine,
401
+ column: m.generatedColumn
402
+ },
403
+ original: {
404
+ line: originalLine,
405
+ column: m.originalColumn
406
+ },
407
+ source: relativePath,
408
+ name: m.name
409
+ });
410
+ });
411
+
412
+ finalMap = generator.toString();
413
+ consumer.destroy();
414
+
415
+ } catch (e) { /* ignore */ }
416
+ }
417
+
418
+ return {
419
+ code: code,
420
+ errors: [],
421
+ map: finalMap
422
+ };
423
+ }
424
+
257
425
  // --- Natural Reactivity Transformer ---
258
426
 
259
427
  function transformNaturalReactivity(code: string, isTs: boolean): string {
@@ -1,5 +1,7 @@
1
1
 
2
+ import * as path from 'path';
2
3
  import { SFCDescriptor } from './sfc-parser';
4
+ import { CompilerOptions } from './script-compiler'; // Re-use options interface
3
5
 
4
6
  export interface StyleCompileResult {
5
7
  css: string;
@@ -7,7 +9,7 @@ export interface StyleCompileResult {
7
9
  errors: string[];
8
10
  }
9
11
 
10
- export function compileStyle(descriptor: SFCDescriptor, filename: string): StyleCompileResult {
12
+ export function compileStyle(descriptor: SFCDescriptor, filename: string, options?: CompilerOptions): StyleCompileResult {
11
13
  const styles = descriptor.styles;
12
14
  if (!styles || styles.length === 0) return { css: '', errors: [] };
13
15
 
@@ -17,17 +19,39 @@ export function compileStyle(descriptor: SFCDescriptor, filename: string): Style
17
19
  styles.forEach(style => {
18
20
  let content = style.content;
19
21
 
22
+ // Handle External Style
23
+ if (style.attrs.src) {
24
+ if (options && options.readFileSync) {
25
+ try {
26
+ const sfcDir = path.dirname(filename);
27
+ const externalPath = path.resolve(sfcDir, style.attrs.src);
28
+ content = options.readFileSync(externalPath);
29
+ } catch (e: any) {
30
+ // Start error with "Style Compilation Error" to be consistent? Or just add to errors list.
31
+ // But we are inside forEach, so we can't return easily. We'll add a comment in CSS or log.
32
+ // Better to collect errors.
33
+ // But function signature returns { errors: string[] }.
34
+ // We'll just append to errors? But we need to change forEach loop or use functional approach.
35
+ // For now, let's just ignore/log in CSS content?
36
+ content = `/* Failed to read external style: ${e.message} */`;
37
+ }
38
+ } else {
39
+ content = `/* External style found but no readFileSync option provided */`;
40
+ }
41
+ }
42
+
20
43
  if (style.attrs.scoped) {
21
44
  // SCSS Compilation
22
- if (style.attrs.lang === 'scss' || style.attrs.lang === 'sass') {
45
+ if (style.attrs.lang === 'scss' || style.attrs.lang === 'sass' || (style.attrs.src && (style.attrs.src.endsWith('.scss') || style.attrs.src.endsWith('.sass')))) {
23
46
  try {
24
47
  const sass = require('sass');
25
48
  const result = sass.compileString(content, {
26
- syntax: style.attrs.lang === 'sass' ? 'indented' : 'scss'
49
+ syntax: (style.attrs.lang === 'sass' || (style.attrs.src && style.attrs.src.endsWith('.sass'))) ? 'indented' : 'scss'
27
50
  });
28
51
  content = result.css;
29
52
  } catch (e: any) {
30
- return { css: '', errors: [`SCSS Compilation Error: ${e.message}`] };
53
+ // return { css: '', errors: [`SCSS Compilation Error: ${e.message}`] };
54
+ content = `/* SCSS Compilation Error: ${e.message} */`;
31
55
  }
32
56
  }
33
57
 
@@ -34,9 +34,12 @@ interface InterpolationNode extends BaseNode {
34
34
 
35
35
  // --- Compile Function ---
36
36
 
37
+ import { SourceMapGenerator } from 'source-map';
38
+
37
39
  export interface TemplateCompileResult {
38
40
  code: string;
39
41
  errors: string[];
42
+ map?: string;
40
43
  }
41
44
 
42
45
  export function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptCompileResult, scopedId?: string): TemplateCompileResult {
@@ -56,8 +59,7 @@ export function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptC
56
59
  // 3. Codegen Phase (AST -> JS Function)
57
60
  const code = generate(ast, scriptResult.bindings || []);
58
61
 
59
- return {
60
- code: `function render() {
62
+ const renderFn = `function render() {
61
63
  const _s = (v) => (v && typeof v === 'object' && 'value' in v) ? v.value : v;
62
64
  const _h = (v, raw) => {
63
65
  const val = _s(v);
@@ -69,8 +71,54 @@ export function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptC
69
71
  return val.replace(/</g, '&lt;').replace(/>/g, '&gt;');
70
72
  };
71
73
  return ${code};
72
- }`,
73
- errors
74
+ }`;
75
+
76
+ // 4. Source Map Generation
77
+ // We map the entire render function to the starting line of the template in the source file
78
+ let map = undefined;
79
+ if (descriptor.filename) {
80
+ const generator = new SourceMapGenerator({
81
+ file: descriptor.filename + '.template.js', // Virtual file name
82
+ });
83
+
84
+ // Find start line of template in original source
85
+ // descriptor.template.start is the index of content start
86
+ const sourceBefore = descriptor.source.substring(0, template.start);
87
+ const startLine = sourceBefore.split(/\r?\n/).length;
88
+
89
+ // Simple mapping: One-to-one mapping isn't easy without AST location tracking
90
+ // But we can map the 'return' statement to the template start
91
+ // The render function has about 12 lines of preamble.
92
+ // Let's just mapping the whole block to the start line for now.
93
+ // This ensures the file shows up in devtools.
94
+ generator.addMapping({
95
+ generated: { line: 1, column: 0 },
96
+ source: descriptor.filename, // This should be the .mujs file path
97
+ original: { line: startLine, column: 0 }
98
+ });
99
+
100
+ // Also map the return line (approx line 12)
101
+ generator.addMapping({
102
+ generated: { line: 12, column: 0 },
103
+ source: descriptor.filename,
104
+ original: { line: startLine, column: 0 }
105
+ });
106
+
107
+ // We must include the "source content" so DevTools can display it!
108
+ // IMPORTANT: We want the FULL .mujs content here, so it matches what Webpack sees?
109
+ // Actually, if we use the same filename as script-compiler, they might conflict or merge.
110
+ // Script compiler uses 'webpack:///...'
111
+ // Let's use the same convention.
112
+ // But we just use the filename here, compiler.ts will handle the final path adjustment if needed.
113
+ generator.setSourceContent(descriptor.filename, descriptor.source);
114
+
115
+ map = generator.toString();
116
+ }
117
+
118
+ return {
119
+ code: renderFn,
120
+ errors,
121
+ map
74
122
  };
75
123
  }
76
124
 
@@ -9,45 +9,20 @@ module.exports = function (content) {
9
9
  const context = this.context || path.dirname(filePath);
10
10
 
11
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;
12
+ // REMOVED: Inlining is now handled by the compiler to support correct source maps.
13
+ // We still scan for dependencies.
17
14
 
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;
15
+ // Regex to match <tag ... src="...">... </tag> or <tag ... src="..." /> for DEPENDENCY TRACKING ONLY
16
+ const tagRegex = /<(script|style)([\s\S]*?)src=(["'])(.*?)\3/gi;
17
+ let match;
18
+ while ((match = tagRegex.exec(content)) !== null) {
19
+ try {
20
+ const srcPath = match[4];
21
+ const absolutePath = path.resolve(context, srcPath);
22
+ this.addDependency(absolutePath);
23
+ } catch (e) {
24
+ // Ignore resolution errors here, compiler will handle them
25
+ }
51
26
  }
52
27
 
53
28
  // Resolve path to the compiled compiler module (ESM)
@@ -57,12 +32,12 @@ module.exports = function (content) {
57
32
  const compilerRef = path.resolve(__dirname, '../../dist/compiler/compiler.js');
58
33
  const compilerUrl = pathToFileURL(compilerRef).href;
59
34
 
60
- // Use dynamic import to load ESM compiler from CommonJS loader
61
-
62
35
  // Use dynamic import to load ESM compiler from CommonJS loader
63
36
  import(compilerUrl).then(async compiler => {
64
37
  try {
65
- const result = await compiler.compileSFC(content, filePath);
38
+ const result = await compiler.compileSFC(content, filePath, {
39
+ readFileSync: (file) => fs.readFileSync(file, 'utf-8')
40
+ });
66
41
 
67
42
  if (result.errors && result.errors.length > 0) {
68
43
  this.emitError(new Error(result.errors.join('\n')));