+ | ^"
+`;
diff --git a/packages/compiler-core/__tests__/codeframe.spec.ts b/packages/compiler-core/__tests__/codeframe.spec.ts
new file mode 100644
index 00000000..2b64afaf
--- /dev/null
+++ b/packages/compiler-core/__tests__/codeframe.spec.ts
@@ -0,0 +1,46 @@
+import { generateCodeFrame } from '../src'
+
+describe('compiler: codeframe', () => {
+ const source = `
+
+ `.trim()
+
+ test('line near top', () => {
+ const keyStart = source.indexOf(`key="one"`)
+ const keyEnd = keyStart + `key="one"`.length
+ expect(generateCodeFrame(source, keyStart, keyEnd)).toMatchSnapshot()
+ })
+
+ test('line in middle', () => {
+ // should cover 5 lines
+ const forStart = source.indexOf(`v-for=`)
+ const forEnd = forStart + `v-for="foobar"`.length
+ expect(generateCodeFrame(source, forStart, forEnd)).toMatchSnapshot()
+ })
+
+ test('line near bottom', () => {
+ const keyStart = source.indexOf(`key="two"`)
+ const keyEnd = keyStart + `key="two"`.length
+ expect(generateCodeFrame(source, keyStart, keyEnd)).toMatchSnapshot()
+ })
+
+ test('multi-line highlights', () => {
+ const source = `
+
+
+ `.trim()
+
+ const attrStart = source.indexOf(`attr=`)
+ const attrEnd = source.indexOf(`">`) + 1
+ expect(generateCodeFrame(source, attrStart, attrEnd)).toMatchSnapshot()
+ })
+})
diff --git a/packages/compiler-core/src/codeframe.ts b/packages/compiler-core/src/codeframe.ts
new file mode 100644
index 00000000..acea12bf
--- /dev/null
+++ b/packages/compiler-core/src/codeframe.ts
@@ -0,0 +1,37 @@
+const range: number = 2
+
+export function generateCodeFrame(
+ source: string,
+ start: number = 0,
+ end: number = source.length
+): string {
+ const lines = source.split(/\r?\n/)
+ let count = 0
+ const res = []
+ for (let i = 0; i < lines.length; i++) {
+ count += lines[i].length + 1
+ if (count >= start) {
+ for (let j = i - range; j <= i + range || end > count; j++) {
+ if (j < 0 || j >= lines.length) continue
+ res.push(
+ `${j + 1}${' '.repeat(3 - String(j + 1).length)}| ${lines[j]}`
+ )
+ const lineLength = lines[j].length
+ if (j === i) {
+ // push underline
+ const pad = start - (count - lineLength) + 1
+ const length = end > count ? lineLength - pad : end - start
+ res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length))
+ } else if (j > i) {
+ if (end > count) {
+ const length = Math.min(end - count, lineLength)
+ res.push(` | ` + '^'.repeat(length))
+ }
+ count += lineLength + 1
+ }
+ }
+ break
+ }
+ }
+ return res.join('\n')
+}
diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts
index ab5655d9..32c7f529 100644
--- a/packages/compiler-core/src/index.ts
+++ b/packages/compiler-core/src/index.ts
@@ -99,6 +99,7 @@ export {
} from './errors'
export * from './ast'
export * from './utils'
+export * from './codeframe'
export { registerRuntimeHelpers } from './runtimeHelpers'
// expose transforms so higher-order compilers can import and extend them
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 77ed68ab..08cc7cbe 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -24,7 +24,11 @@ import {
isObject
} from '@vue/shared'
import { SuspenseBoundary } from './suspense'
-import { CompilerOptions } from '@vue/compiler-dom'
+import {
+ CompilerError,
+ CompilerOptions,
+ generateCodeFrame
+} from '@vue/compiler-dom'
export type Data = { [key: string]: unknown }
@@ -319,10 +323,17 @@ function finishComponentSetup(
if (Component.template && !Component.render) {
if (compile) {
Component.render = compile(Component.template, {
- onError(err) {
+ onError(err: CompilerError) {
if (__DEV__) {
- // TODO use err.loc to provide codeframe like Vue 2
- warn(`Template compilation error: ${err.message}`)
+ const message = `Template compilation error: ${err.message}`
+ const codeFrame =
+ err.loc &&
+ generateCodeFrame(
+ Component.template!,
+ err.loc.start.offset,
+ err.loc.end.offset
+ )
+ warn(codeFrame ? `${message}\n${codeFrame}` : message)
}
}
})