]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compile-sfc): handle inline template source map in prod build (#12701)
authoredison <daiwei521@126.com>
Tue, 20 May 2025 00:46:01 +0000 (08:46 +0800)
committerGitHub <noreply@github.com>
Tue, 20 May 2025 00:46:01 +0000 (08:46 +0800)
close #12682
close vitejs/vite-plugin-vue#500

packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/__tests__/compileTemplate.spec.ts
packages/compiler-sfc/__tests__/utils.ts
packages/compiler-sfc/src/compileScript.ts

index 11b5661c16cc25d4852690e6efb1ea7d2edd5e10..73c6d316a40a77e2cbc95008834fdc96a2fb69a4 100644 (file)
@@ -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 <script setup>', () => {
   test('should compile JS syntax', () => {
@@ -690,6 +696,27 @@ describe('SFC compile <script setup>', () => {
       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', () => {
index 22623299a02521e30ea36528cfd5807c6e81a0ef..81cf75a912d57710e29985c79fc6b2a866dbbde3 100644 (file)
@@ -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<SFCTemplateCompileOptions, 'id'>) {
   return compileTemplate({
@@ -511,36 +512,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
-}
index 5a58a6b58aedc4c20f66f202e9123c1f126d15ac..b5cfc9606d5b13ff9d77c7ebaf56fcde7d7f6349 100644 (file)
@@ -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
+}
index 36bb2cfd2dff29762aa5325715b00110c937f9e7..18d460ace9037d379972c96068b7bdd2f6c764e4 100644 (file)
@@ -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,
@@ -809,6 +813,7 @@ export function compileScript(
     args += `, { ${destructureElements.join(', ')} }`
   }
 
+  let templateMap
   // 9. generate return statement
   let returned
   if (
@@ -858,7 +863,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,
@@ -876,6 +881,7 @@ export function compileScript(
           bindingMetadata: ctx.bindingMetadata,
         },
       })
+      templateMap = map
       if (tips.length) {
         tips.forEach(warnOnce)
       }
@@ -1014,19 +1020,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,
@@ -1284,3 +1299,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()
+}