diff --git a/packages/compiler-sfc/__tests__/compileScript.spec.ts b/packages/compiler-sfc/__tests__/compileScript.spec.ts index 11b5661c16c..73c6d316a40 100644 --- a/packages/compiler-sfc/__tests__/compileScript.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript.spec.ts @@ -1,5 +1,11 @@ import { BindingTypes } from '@vue/compiler-core' -import { assertCode, compileSFCScript as compile, mockId } from './utils' +import { + assertCode, + compileSFCScript as compile, + getPositionInCode, + mockId, +} from './utils' +import { type RawSourceMap, SourceMapConsumer } from 'source-map-js' describe('SFC compile + + ` + const { content, map } = compile(source, { inlineTemplate: true }) + expect(map).not.toBeUndefined() + const consumer = new SourceMapConsumer(map as RawSourceMap) + expect( + consumer.originalPositionFor(getPositionInCode(content, 'count')), + ).toMatchObject(getPositionInCode(source, `count`)) + expect( + consumer.originalPositionFor(getPositionInCode(content, 'Error')), + ).toMatchObject(getPositionInCode(source, `Error`)) + }) }) describe('with TypeScript', () => { diff --git a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts index 2ea1eb9d378..d4ddc763812 100644 --- a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts +++ b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts @@ -6,6 +6,7 @@ import { } from '../src/compileTemplate' import { type SFCTemplateBlock, parse } from '../src/parse' import { compileScript } from '../src' +import { getPositionInCode } from './utils' function compile(opts: Omit) { return compileTemplate({ @@ -482,36 +483,3 @@ test('non-identifier expression in legacy filter syntax', () => { babelParse(compilationResult.code, { sourceType: 'module' }) }).not.toThrow() }) - -interface Pos { - line: number - column: number - name?: string -} - -function getPositionInCode( - code: string, - token: string, - expectName: string | boolean = false, -): Pos { - const generatedOffset = code.indexOf(token) - let line = 1 - let lastNewLinePos = -1 - for (let i = 0; i < generatedOffset; i++) { - if (code.charCodeAt(i) === 10 /* newline char code */) { - line++ - lastNewLinePos = i - } - } - const res: Pos = { - line, - column: - lastNewLinePos === -1 - ? generatedOffset - : generatedOffset - lastNewLinePos - 1, - } - if (expectName) { - res.name = typeof expectName === 'string' ? expectName : token - } - return res -} diff --git a/packages/compiler-sfc/__tests__/utils.ts b/packages/compiler-sfc/__tests__/utils.ts index 5a58a6b58ae..b5cfc9606d5 100644 --- a/packages/compiler-sfc/__tests__/utils.ts +++ b/packages/compiler-sfc/__tests__/utils.ts @@ -40,3 +40,36 @@ export function assertCode(code: string): void { } expect(code).toMatchSnapshot() } + +interface Pos { + line: number + column: number + name?: string +} + +export function getPositionInCode( + code: string, + token: string, + expectName: string | boolean = false, +): Pos { + const generatedOffset = code.indexOf(token) + let line = 1 + let lastNewLinePos = -1 + for (let i = 0; i < generatedOffset; i++) { + if (code.charCodeAt(i) === 10 /* newline char code */) { + line++ + lastNewLinePos = i + } + } + const res: Pos = { + line, + column: + lastNewLinePos === -1 + ? generatedOffset + : generatedOffset - lastNewLinePos - 1, + } + if (expectName) { + res.name = typeof expectName === 'string' ? expectName : token + } + return res +} diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index fee05beed96..67af92df5e7 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -23,7 +23,11 @@ import type { Statement, } from '@babel/types' import { walk } from 'estree-walker' -import type { RawSourceMap } from 'source-map-js' +import { + type RawSourceMap, + SourceMapConsumer, + SourceMapGenerator, +} from 'source-map-js' import { normalScriptDefaultVar, processNormalScript, @@ -817,6 +821,7 @@ export function compileScript( args += `, { ${destructureElements.join(', ')} }` } + let templateMap // 9. generate return statement let returned if ( @@ -866,7 +871,7 @@ export function compileScript( } // inline render function mode - we are going to compile the template and // inline it right here - const { code, ast, preamble, tips, errors } = compileTemplate({ + const { code, ast, preamble, tips, errors, map } = compileTemplate({ filename, ast: sfc.template.ast, source: sfc.template.content, @@ -884,6 +889,7 @@ export function compileScript( bindingMetadata: ctx.bindingMetadata, }, }) + templateMap = map if (tips.length) { tips.forEach(warnOnce) } @@ -1022,19 +1028,28 @@ export function compileScript( ) } + const content = ctx.s.toString() + let map = + options.sourceMap !== false + ? (ctx.s.generateMap({ + source: filename, + hires: true, + includeContent: true, + }) as unknown as RawSourceMap) + : undefined + // merge source maps of the script setup and template in inline mode + if (templateMap && map) { + const offset = content.indexOf(returned) + const templateLineOffset = + content.slice(0, offset).split(/\r?\n/).length - 1 + map = mergeSourceMaps(map, templateMap, templateLineOffset) + } return { ...scriptSetup, bindings: ctx.bindingMetadata, imports: ctx.userImports, - content: ctx.s.toString(), - map: - options.sourceMap !== false - ? (ctx.s.generateMap({ - source: filename, - hires: true, - includeContent: true, - }) as unknown as RawSourceMap) - : undefined, + content, + map, scriptAst: scriptAst?.body, scriptSetupAst: scriptSetupAst?.body, deps: ctx.deps ? [...ctx.deps] : undefined, @@ -1291,3 +1306,42 @@ function isStaticNode(node: Node): boolean { } return false } + +export function mergeSourceMaps( + scriptMap: RawSourceMap, + templateMap: RawSourceMap, + templateLineOffset: number, +): RawSourceMap { + const generator = new SourceMapGenerator() + const addMapping = (map: RawSourceMap, lineOffset = 0) => { + const consumer = new SourceMapConsumer(map) + ;(consumer as any).sources.forEach((sourceFile: string) => { + ;(generator as any)._sources.add(sourceFile) + const sourceContent = consumer.sourceContentFor(sourceFile) + if (sourceContent != null) { + generator.setSourceContent(sourceFile, sourceContent) + } + }) + consumer.eachMapping(m => { + if (m.originalLine == null) return + generator.addMapping({ + generated: { + line: m.generatedLine + lineOffset, + column: m.generatedColumn, + }, + original: { + line: m.originalLine, + column: m.originalColumn, + }, + source: m.source, + name: m.name, + }) + }) + } + + addMapping(scriptMap) + addMapping(templateMap, templateLineOffset) + ;(generator as any)._sourceRoot = scriptMap.sourceRoot + ;(generator as any)._file = scriptMap.file + return (generator as any).toJSON() +}