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 <script setup>', () => {
test('should compile JS syntax', () => {
expect(content).toMatch(`new (_unref(Foo)).Bar()`)
assertCode(content)
})
+
+ // #12682
+ test('source map', () => {
+ const source = `
+ <script setup>
+ const count = ref(0)
+ </script>
+ <template>
+ <button @click="throw new Error(\`msg\`);"></button>
+ </template>
+ `
+ 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', () => {
} from '../src/compileTemplate'
import { type SFCTemplateBlock, parse } from '../src/parse'
import { compileScript } from '../src'
+import { getPositionInCode } from './utils'
function compile(opts: Omit<SFCTemplateCompileOptions, 'id'>) {
return compileTemplate({
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
-}
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,
args += `, { ${destructureElements.join(', ')} }`
}
+ let templateMap
// 9. generate return statement
let returned
if (
}
// 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,
bindingMetadata: ctx.bindingMetadata,
},
})
+ templateMap = map
if (tips.length) {
tips.forEach(warnOnce)
}
)
}
+ 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,
}
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()
+}