',
}
const { props } = resolve(
`
import { B } from './foo'
defineProps()
`,
files,
)
expect(props).toStrictEqual({
n: ['Number'],
})
})
test('relative vue', () => {
const files = {
'/foo.vue':
'',
'/bar.vue':
'',
}
const { props, deps } = resolve(
`
import { P } from './foo.vue'
import { P as PP } from './bar.vue'
defineProps ()
`,
files,
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative (chained)', () => {
const files = {
'/foo.ts': `import type { P as PP } from './nested/bar.vue'
export type P = { foo: number } & PP`,
'/nested/bar.vue':
'',
}
const { props, deps } = resolve(
`
import { P } from './foo'
defineProps
()
`,
files,
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative (chained, re-export)', () => {
const files = {
'/foo.ts': `export { P as PP } from './bar'`,
'/bar.ts': 'export type P = { bar: string }',
}
const { props, deps } = resolve(
`
import { PP as P } from './foo'
defineProps
()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative (chained, export *)', () => {
const files = {
'/foo.ts': `export * from './bar'`,
'/bar.ts': 'export type P = { bar: string }',
}
const { props, deps } = resolve(
`
import { P } from './foo'
defineProps
()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative (default export)', () => {
const files = {
'/foo.ts': `export default interface P { foo: string }`,
'/bar.ts': `type X = { bar: string }; export default X`,
}
const { props, deps } = resolve(
`
import P from './foo'
import X from './bar'
defineProps
()
`,
files,
)
expect(props).toStrictEqual({
foo: ['String'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative (default re-export)', () => {
const files = {
'/bar.ts': `export { default } from './foo'`,
'/foo.ts': `export default interface P { foo: string }; export interface PP { bar: number }`,
'/baz.ts': `export { PP as default } from './foo'`,
}
const { props, deps } = resolve(
`
import P from './bar'
import PP from './baz'
defineProps
()
`,
files,
)
expect(props).toStrictEqual({
foo: ['String'],
bar: ['Number'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative (re-export /w same source type name)', () => {
const files = {
'/foo.ts': `export default interface P { foo: string }`,
'/bar.ts': `export default interface PP { bar: number }`,
'/baz.ts': `export { default as X } from './foo'; export { default as XX } from './bar'; `,
}
const { props, deps } = resolve(
`import { X, XX } from './baz'
defineProps()
`,
files,
)
expect(props).toStrictEqual({
foo: ['String'],
bar: ['Number'],
})
expect(deps && [...deps]).toStrictEqual(['/baz.ts', '/foo.ts', '/bar.ts'])
})
test('relative (dynamic import)', () => {
const files = {
'/foo.ts': `export type P = { foo: string, bar: import('./bar').N }`,
'/bar.ts': 'export type N = number',
}
const { props, deps } = resolve(
`
defineProps()
`,
files,
)
expect(props).toStrictEqual({
foo: ['String'],
bar: ['Number'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative import with indexed access type', () => {
const files = {
'/foo.ts': `
type Booleanish = boolean | 'true' | 'false';
export interface InputHTMLAttributes {
required?: Booleanish | undefined;
}
`,
}
const { props, deps } = resolve(
`
import { InputHTMLAttributes } from './foo.ts'
type ImportedType = InputHTMLAttributes['required']
defineProps<{
required: ImportedType,
}>()
`,
files,
)
expect(props).toStrictEqual({
required: ['Boolean', 'String', 'Unknown'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('relative import with indexed access type with unresolvable extends', () => {
const files = {
'/foo.ts': `
type EventHandlers = {
[K in keyof E]?: E[K] extends (...args: any) => any
? E[K]
: (payload: E[K]) => void;
};
export interface Events {
onCopy: ClipboardEvent;
}
type Booleanish = boolean | 'true' | 'false';
export interface InputHTMLAttributes extends EventHandlers{
required?: Booleanish | undefined;
}
`,
}
const { props, deps } = resolve(
`
import { InputHTMLAttributes } from './foo.ts'
type ImportedType = InputHTMLAttributes['required']
defineProps<{
required: ImportedType,
}>()
`,
files,
)
expect(props).toStrictEqual({
required: ['Boolean', 'String', 'Unknown'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
// #8339
test('relative, .js import', () => {
const files = {
'/foo.d.ts':
'import { PP } from "./bar.js"; export type P = { foo: PP }',
'/bar.d.ts': 'export type PP = "foo" | "bar"',
}
const { props, deps } = resolve(
`
import { P } from './foo'
defineProps()
`,
files,
)
expect(props).toStrictEqual({
foo: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('ts module resolve', () => {
const files = {
'/node_modules/foo/package.json': JSON.stringify({
types: 'index.d.ts',
}),
'/node_modules/foo/index.d.ts': 'export type P = { foo: number }',
'/tsconfig.json': JSON.stringify({
compilerOptions: {
paths: {
bar: ['./pp.ts'],
},
},
}),
'/pp.ts': 'export type PP = { bar: string }',
}
const { props, deps } = resolve(
`
import { P } from 'foo'
import { PP } from 'bar'
defineProps
()
`,
files,
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual([
'/node_modules/foo/index.d.ts',
'/pp.ts',
])
})
test('ts module resolve w/ project reference & extends', () => {
const files = {
'/tsconfig.json': JSON.stringify({
references: [
{
path: './tsconfig.app.json',
},
],
}),
'/tsconfig.app.json': JSON.stringify({
include: ['**/*.ts', '**/*.vue'],
extends: './tsconfig.web.json',
}),
'/tsconfig.web.json': JSON.stringify({
compilerOptions: {
composite: true,
paths: {
bar: ['./user.ts'],
},
},
}),
'/user.ts': 'export type User = { bar: string }',
}
const { props, deps } = resolve(
`
import { User } from 'bar'
defineProps()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
// #13484
test('ts module resolve w/ project reference & extends & ${configDir}', () => {
const files = {
'/tsconfig.json': JSON.stringify({
files: [],
references: [{ path: './tsconfig.app.json' }],
}),
'/tsconfig.app.json': JSON.stringify({
extends: ['./tsconfigs/base.json'],
}),
'/tsconfigs/base.json': JSON.stringify({
compilerOptions: {
paths: {
'@/*': ['${configDir}/src/*'],
},
},
include: ['${configDir}/src/**/*.ts', '${configDir}/src/**/*.vue'],
}),
'/src/types.ts':
'export type BaseProps = { foo?: string, bar?: string }',
}
const { props, deps } = resolve(
`
import { BaseProps } from '@/types.ts';
defineProps()
`,
files,
{},
'/src/components/Foo.vue',
)
expect(props).toStrictEqual({
foo: ['String'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/src/types.ts'])
})
test('ts module resolve w/ project reference folder', () => {
const files = {
'/tsconfig.json': JSON.stringify({
references: [
{
path: './web',
},
{
path: './empty',
},
{
path: './noexists-should-ignore',
},
],
}),
'/web/tsconfig.json': JSON.stringify({
include: ['../**/*.ts', '../**/*.vue'],
compilerOptions: {
composite: true,
paths: {
bar: ['../user.ts'],
},
},
}),
// tsconfig with no include / paths defined, should match nothing
'/empty/tsconfig.json': JSON.stringify({
compilerOptions: {
composite: true,
},
}),
'/user.ts': 'export type User = { bar: string }',
}
const { props, deps } = resolve(
`
import { User } from 'bar'
defineProps()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
// #11382
test('ts module resolve circular project reference', () => {
const files = {
'/tsconfig.json': JSON.stringify({
exclude: ['**/*.ts', '**/*.vue'],
references: [
{
path: './tsconfig.web.json',
},
],
}),
'/tsconfig.web.json': JSON.stringify({
include: ['**/*.ts', '**/*.vue'],
compilerOptions: {
composite: true,
paths: {
user: ['./user.ts'],
},
},
references: [
{
// circular reference
path: './tsconfig.json',
},
],
}),
'/user.ts': 'export type User = { bar: string }',
}
const { props, deps } = resolve(
`
import { User } from 'user'
defineProps()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
test('ts module resolve w/ path aliased vue file', () => {
const files = {
'/tsconfig.json': JSON.stringify({
compilerOptions: {
include: ['**/*.ts', '**/*.vue'],
paths: {
'@/*': ['./src/*'],
},
},
}),
'/src/Foo.vue':
'',
}
const { props, deps } = resolve(
`
import { P } from '@/Foo.vue'
defineProps()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/src/Foo.vue'])
})
test('global types', () => {
const files = {
// ambient
'/app.d.ts':
'declare namespace App { interface User { name: string } }',
// module - should only respect the declare global block
'/global.d.ts': `
declare type PP = { bar: number }
declare global {
type PP = { bar: string }
}
export {}
`,
}
const { props, deps } = resolve(`defineProps()`, files, {
globalTypeFiles: Object.keys(files),
})
expect(props).toStrictEqual({
name: ['String'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('global types with named exports', () => {
const files = {
'/global.d.ts': `
declare global {
export interface ExportedInterface { foo: number }
export type ExportedType = { bar: boolean }
}
export {}
`,
}
const globalTypeFiles = { globalTypeFiles: Object.keys(files) }
expect(
resolve(`defineProps()`, files, globalTypeFiles)
.props,
).toStrictEqual({
foo: ['Number'],
})
expect(
resolve(`defineProps()`, files, globalTypeFiles).props,
).toStrictEqual({
bar: ['Boolean'],
})
})
test('global types with ambient references', () => {
const files = {
// with references
'/backend.d.ts': `
declare namespace App.Data {
export type AircraftData = {
id: string
manufacturer: App.Data.Listings.ManufacturerData
}
}
declare namespace App.Data.Listings {
export type ManufacturerData = {
id: string
}
}
`,
}
const { props } = resolve(`defineProps()`, files, {
globalTypeFiles: Object.keys(files),
})
expect(props).toStrictEqual({
id: ['String'],
manufacturer: ['Object'],
})
})
test('declare global with indexed access type', () => {
const files = {
'/global.d.ts': `
declare global {
type Options = {
code: {
selected: boolean
}
}
}`,
}
const { props } = resolve(`defineProps()`, files, {
globalTypeFiles: Object.keys(files),
})
expect(props).toStrictEqual({
selected: ['Boolean'],
})
})
// #9871
test('shared generics with different args', () => {
const files = {
'/foo.ts': `export interface Foo { value: T }`,
}
const { props } = resolve(
`import type { Foo } from './foo'
defineProps>()`,
files,
undefined,
`/One.vue`,
)
expect(props).toStrictEqual({
value: ['String'],
})
const { props: props2 } = resolve(
`import type { Foo } from './foo'
defineProps>()`,
files,
undefined,
`/Two.vue`,
false /* do not invalidate cache */,
)
expect(props2).toStrictEqual({
value: ['Number'],
})
})
})
describe('errors', () => {
test('failed type reference', () => {
expect(() => resolve(`defineProps()`)).toThrow(
`Unresolvable type reference`,
)
})
test('unsupported computed keys', () => {
expect(() => resolve(`defineProps<{ [Foo]: string }>()`)).toThrow(
`Unsupported computed key in type referenced by a macro`,
)
})
test('unsupported index type', () => {
expect(() => resolve(`defineProps()`)).toThrow(
`Unsupported type when resolving index type`,
)
})
test('failed import source resolve', () => {
expect(() =>
resolve(`import { X } from './foo'; defineProps()`),
).toThrow(`Failed to resolve import source "./foo"`)
})
test('should not error on unresolved type when inferring runtime type', () => {
expect(() => resolve(`defineProps<{ foo: T }>()`)).not.toThrow()
expect(() => resolve(`defineProps<{ foo: T['bar'] }>()`)).not.toThrow()
expect(() =>
resolve(`
import type P from 'unknown'
defineProps<{ foo: P }>()
`),
).not.toThrow()
})
test('error against failed extends', () => {
expect(() =>
resolve(`
import type Base from 'unknown'
interface Props extends Base {}
defineProps()
`),
).toThrow(`@vue-ignore`)
})
test('allow ignoring failed extends', () => {
let res: any
expect(
() =>
(res = resolve(`
import type Base from 'unknown'
interface Props extends /*@vue-ignore*/ Base {
foo: string
}
defineProps()
`)),
).not.toThrow(`@vue-ignore`)
expect(res.props).toStrictEqual({
foo: ['String'],
})
})
})
describe('template literals', () => {
test('mapped types with string type', () => {
expect(
resolve(`
type X = 'a' | 'b'
defineProps<{[K in X as \`\${K}_foo\`]: string}>()
`).props,
).toStrictEqual({
a_foo: ['String'],
b_foo: ['String'],
})
})
// #10962
test('mapped types with generic parameters', () => {
const { props } = resolve(`
type Breakpoints = 'sm' | 'md' | 'lg'
type BreakpointFactory = {
[K in Breakpoints as \`\${T}\${Capitalize}\`]: V
}
type ColsBreakpoints = BreakpointFactory<'cols', number>
defineProps()
`)
expect(props).toStrictEqual({
colsSm: ['Number'],
colsMd: ['Number'],
colsLg: ['Number'],
})
})
test('allowArbitraryExtensions', () => {
const files = {
'/foo.d.vue.ts': 'export type Foo = number;',
'/foo.vue': '
',
'/bar.d.css.ts': 'export type Bar = string;',
'/bar.css': ':root { --color: red; }',
}
const { props } = resolve(
`
import { Foo } from './foo.vue'
import { Bar } from './bar.css'
defineProps<{ foo: Foo; bar: Bar }>()
`,
files,
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
})
})
// https://github.com/vuejs/router/issues/2611
test('modular js extension', () => {
const files = {
'/mts.mjs': 'export {}',
'/mts.d.mts': 'export type LinkProps = { activeClass: string }',
'/tsx.jsx': 'export {}',
'/tsx.d.ts': 'export type Foo = number',
'/mtsTyped.mjs': 'export {}',
'/mtsTyped.d.ts': 'export type Bar = string',
'/cts.cjs': 'module.exports = {}',
'/cts.d.cts': `export type Baz = boolean`,
}
let props!: Record
expect(() => {
props = resolve(
`
import type { LinkProps } from './mts.mjs'
import { Foo } from './tsx.jsx'
import { Bar } from './mtsTyped.mjs'
import type { Baz } from './cts.cjs'
defineProps()
`,
files,
).props
}).not.toThrow()
expect(props).not.toBe(undefined)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
baz: ['Boolean'],
activeClass: ['String'],
})
})
test('prefer .mts over .ts for .mjs import', () => {
const files = {
'/foo.mjs': 'export {}',
'/foo.ts': 'export type Foo = number',
'/foo.mts': 'export type Foo = string',
}
const { props } = resolve(
`
import type { Foo } from './foo.mjs'
defineProps<{ value: Foo }>()
`,
files,
)
expect(props).toStrictEqual({
value: ['String'],
})
})
test('prefer .d.mts over .d.ts for .mjs import', () => {
const files = {
'/foo.mjs': 'export {}',
'/foo.d.ts': 'export type Foo = number',
'/foo.d.mts': 'export type Foo = string',
}
const { props } = resolve(
`
import type { Foo } from './foo.mjs'
defineProps<{ value: Foo }>()
`,
files,
)
expect(props).toStrictEqual({
value: ['String'],
})
})
test('prefer .d.cts over .d.ts for .cjs import', () => {
const files = {
'/foo.cjs': 'module.exports = {}',
'/foo.d.ts': 'export type Foo = number',
'/foo.d.cts': 'export type Foo = boolean',
}
const { props } = resolve(
`
import type { Foo } from './foo.cjs'
defineProps<{ value: Foo }>()
`,
files,
)
expect(props).toStrictEqual({
value: ['Boolean'],
})
})
})
})
function resolve(
code: string,
files: Record = {},
options?: Partial,
sourceFileName: string = '/Test.vue',
invalidateCache = true,
) {
const { descriptor } = parse(``, {
filename: sourceFileName,
})
const ctx = new ScriptCompileContext(descriptor, {
id: 'test',
fs: {
fileExists(file) {
return !!(files[file] ?? files[normalize(file)])
},
readFile(file) {
return files[file] ?? files[normalize(file)]
},
},
...options,
})
if (invalidateCache) {
for (const file in files) {
invalidateTypeCache(file)
}
}
// ctx.userImports is collected when calling compileScript(), but we are
// skipping that here, so need to manually register imports
ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any
let target: any
for (const s of ctx.scriptSetupAst!.body) {
if (
s.type === 'ExpressionStatement' &&
s.expression.type === 'CallExpression' &&
(s.expression.callee as Identifier).name === 'defineProps'
) {
target = s.expression.typeParameters!.params[0]
}
}
const raw = resolveTypeElements(ctx, target)
const props: Record = {}
for (const key in raw.props) {
props[key] = inferRuntimeType(ctx, raw.props[key])
}
return {
props,
calls: raw.calls,
deps: ctx.deps,
raw,
}
}
================================================
FILE: packages/compiler-sfc/__tests__/compileScript.spec.ts
================================================
import { vi } from 'vitest'
import { BindingTypes } from '@vue/compiler-core'
import {
assertCode,
compileSFCScript as compile,
getPositionInCode,
mockId,
} from './utils'
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
vi.mock('../src/warn', () => ({
warn: vi.fn(),
warnOnce: vi.fn(),
}))
import { warnOnce } from '../src/warn'
const warnOnceMock = vi.mocked(warnOnce)
describe('SFC compile
`)
expect(content).toMatch(`return { a, b }`)
assertCode(content)
})
test('should expose top level declarations', () => {
const { content, bindings } = compile(`
`)
expect(content).toMatch(
`return { get aa() { return aa }, set aa(v) { aa = v }, ` +
`bb, cc, dd, get a() { return a }, set a(v) { a = v }, b, c, d, ` +
`get xx() { return xx }, get x() { return x } }`,
)
expect(bindings).toStrictEqual({
x: BindingTypes.SETUP_MAYBE_REF,
a: BindingTypes.SETUP_LET,
b: BindingTypes.SETUP_CONST,
c: BindingTypes.SETUP_CONST,
d: BindingTypes.SETUP_CONST,
xx: BindingTypes.SETUP_MAYBE_REF,
aa: BindingTypes.SETUP_LET,
bb: BindingTypes.LITERAL_CONST,
cc: BindingTypes.SETUP_CONST,
dd: BindingTypes.SETUP_CONST,
})
assertCode(content)
})
test('binding analysis for destructure', () => {
const { content, bindings } = compile(`
`)
expect(content).toMatch('return { foo, bar, baz, y, z }')
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF,
baz: BindingTypes.SETUP_MAYBE_REF,
y: BindingTypes.SETUP_MAYBE_REF,
z: BindingTypes.SETUP_MAYBE_REF,
})
assertCode(content)
})
test('demote const reactive binding to let when used in v-model', () => {
warnOnceMock.mockClear()
const { content, bindings } = compile(`
`)
expect(content).toMatch(
`let name = reactive({ first: 'john', last: 'doe' })`,
)
expect(bindings!.name).toBe(BindingTypes.SETUP_LET)
expect(warnOnceMock).toHaveBeenCalledTimes(1)
expect(warnOnceMock).toHaveBeenCalledWith(
expect.stringContaining(
'`v-model` cannot update a `const` reactive binding',
),
)
assertCode(content)
})
test('demote const reactive binding to let when used in v-model (inlineTemplate)', () => {
warnOnceMock.mockClear()
const { content, bindings } = compile(
`
`,
{ inlineTemplate: true },
)
expect(content).toMatch(
`let name = reactive({ first: 'john', last: 'doe' })`,
)
expect(bindings!.name).toBe(BindingTypes.SETUP_LET)
expect(warnOnceMock).toHaveBeenCalledTimes(1)
expect(warnOnceMock).toHaveBeenCalledWith(
expect.stringContaining(
'`v-model` cannot update a `const` reactive binding',
),
)
assertCode(content)
})
test('v-model should error on literal const bindings', () => {
expect(() =>
compile(
`
`,
{ inlineTemplate: true },
),
).toThrow('v-model cannot be used on a const binding')
})
describe('
`)
assertCode(content)
})
test('script setup first', () => {
const { content } = compile(`
`)
assertCode(content)
})
// #7805
test('keep original semi style', () => {
const { content } = compile(`
`)
assertCode(content)
expect(content).toMatch(`console.log('test')`)
expect(content).toMatch(`const props = __props;`)
expect(content).toMatch(`const emit = __emit;`)
expect(content).toMatch(`(function () {})()`)
})
test('script setup first, named default export', () => {
const { content } = compile(`
`)
assertCode(content)
})
// #4395
test('script setup first, lang="ts", script block content export default', () => {
const { content } = compile(`
`)
// ensure __default__ is declared before used
expect(content).toMatch(/const __default__[\S\s]*\.\.\.__default__/m)
assertCode(content)
})
describe('spaces in ExportDefaultDeclaration node', () => {
// #4371
test('with many spaces and newline', () => {
// #4371
const { content } = compile(`
`)
assertCode(content)
})
test('with minimal spaces', () => {
const { content } = compile(`
`)
assertCode(content)
})
})
test('export call expression as default', () => {
const { content } = compile(`
`)
assertCode(content)
})
})
describe('imports', () => {
test('should hoist and expose imports', () => {
assertCode(
compile(``).content,
)
})
test('should extract comment for import or type declarations', () => {
assertCode(
compile(`
`).content,
)
})
// #2740
test('should allow defineProps/Emit at the start of imports', () => {
assertCode(
compile(``).content,
)
})
test('dedupe between user & helper', () => {
const { content } = compile(
`
`,
)
assertCode(content)
expect(content).toMatch(
`import { useCssVars as _useCssVars, unref as _unref } from 'vue'`,
)
expect(content).toMatch(`import { useCssVars, ref } from 'vue'`)
})
test('import dedupe between
`)
assertCode(content)
expect(content.indexOf(`import { x }`)).toEqual(
content.lastIndexOf(`import { x }`),
)
})
describe('import ref/reactive function from other place', () => {
test('import directly', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
ref: BindingTypes.SETUP_MAYBE_REF,
reactive: BindingTypes.SETUP_MAYBE_REF,
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF,
})
})
test('import w/ alias', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
_reactive: BindingTypes.SETUP_MAYBE_REF,
_ref: BindingTypes.SETUP_MAYBE_REF,
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF,
})
})
test('aliased usage before import site', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
bar: BindingTypes.SETUP_REACTIVE_CONST,
x: BindingTypes.SETUP_CONST,
})
})
})
test('should support module string names syntax', () => {
const { content, bindings } = compile(`
`)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_MAYBE_REF,
})
})
})
describe('inlineTemplate mode', () => {
test('should work', () => {
const { content } = compile(
`
{{ count }}
static
`,
{ inlineTemplate: true },
)
// check snapshot and make sure helper imports and
// hoists are placed correctly.
assertCode(content)
// in inline mode, no need to call expose() since nothing is exposed
// anyway!
expect(content).not.toMatch(`expose()`)
})
test('with defineExpose()', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
assertCode(content)
expect(content).toMatch(`setup(__props, { expose: __expose })`)
expect(content).toMatch(`expose({ count })`)
})
test('referencing scope components and directives', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
expect(content).toMatch('[_unref(vMyDir)]')
expect(content).toMatch('_createVNode(ChildComp)')
// kebab-case component support
expect(content).toMatch('_createVNode(SomeOtherComp)')
assertCode(content)
})
test('avoid unref() when necessary', () => {
// function, const, component import
const { content } = compile(
`
{{ bar }}
{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}
{{ tree.foo() }}
`,
{ inlineTemplate: true },
)
// no need to unref vue component import
expect(content).toMatch(`createVNode(Foo,`)
// #2699 should unref named imports from .vue
expect(content).toMatch(`unref(bar)`)
// should unref other imports
expect(content).toMatch(`unref(other)`)
// no need to unref constant literals
expect(content).not.toMatch(`unref(constant)`)
// should directly use .value for known refs
expect(content).toMatch(`count.value`)
// should unref() on const bindings that may be refs
expect(content).toMatch(`unref(maybe)`)
// should unref() on let bindings
expect(content).toMatch(`unref(lett)`)
// no need to unref namespace import (this also preserves tree-shaking)
expect(content).toMatch(`tree.foo()`)
// no need to unref function declarations
expect(content).toMatch(`{ onClick: fn }`)
// no need to mark constant fns in patch flag
expect(content).not.toMatch(`PROPS`)
assertCode(content)
})
test('v-model codegen', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
// known const ref: set value
expect(content).toMatch(`(count).value = $event`)
// const but maybe ref: assign if ref, otherwise do nothing
expect(content).toMatch(`_isRef(maybe) ? (maybe).value = $event : null`)
// let: handle both cases
expect(content).toMatch(
`_isRef(lett) ? (lett).value = $event : lett = $event`,
)
assertCode(content)
})
test('v-model w/ newlines codegen', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
expect(content).toMatch(`_isRef(count) ? (count).value = $event : null`)
assertCode(content)
})
test('v-model should not generate ref assignment code for non-setup bindings', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
expect(content).not.toMatch(`_isRef(foo)`)
})
test('template assignment expression codegen', () => {
const { content } = compile(
`
{
let a = '' + lett
v = a
}"/>
{
// nested scopes
(()=>{
let x = a
(()=>{
let z = x
let z2 = z
})
let lz = z
})
v = a
}"/>
`,
{ inlineTemplate: true },
)
// known const ref: set value
expect(content).toMatch(`count.value = 1`)
// const but maybe ref: only assign after check
expect(content).toMatch(`maybe.value = count.value`)
// let: handle both cases
expect(content).toMatch(
`_isRef(lett) ? lett.value = count.value : lett = count.value`,
)
expect(content).toMatch(`_isRef(v) ? v.value += 1 : v += 1`)
expect(content).toMatch(`_isRef(v) ? v.value -= 1 : v -= 1`)
expect(content).toMatch(`_isRef(v) ? v.value = a : v = a`)
expect(content).toMatch(`_isRef(v) ? v.value = _ctx.a : v = _ctx.a`)
assertCode(content)
})
test('template update expression codegen', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
// known const ref: set value
expect(content).toMatch(`count.value++`)
expect(content).toMatch(`--count.value`)
// const but maybe ref (non-ref case ignored)
expect(content).toMatch(`maybe.value++`)
expect(content).toMatch(`--maybe.value`)
// let: handle both cases
expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
assertCode(content)
})
test('template destructure assignment codegen', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
// known const ref: set value
expect(content).toMatch(`({ count: count.value } = val)`)
// const but maybe ref (non-ref case ignored)
expect(content).toMatch(`[maybe.value] = val`)
// let: assumes non-ref
expect(content).toMatch(`{ lett: lett } = val`)
assertCode(content)
})
test('ssr codegen', () => {
const { content } = compile(
`
{{ count }}
static
`,
{
inlineTemplate: true,
templateOptions: {
ssr: true,
},
},
)
expect(content).toMatch(`\n __ssrInlineRender: true,\n`)
expect(content).toMatch(`return (_ctx, _push`)
expect(content).toMatch(`ssrInterpolate`)
expect(content).not.toMatch(`useCssVars`)
expect(content).toMatch(`":--${mockId}-count": (count.value)`)
expect(content).toMatch(`":--${mockId}-style\\\\.color": (style.color)`)
expect(content).toMatch(
`":--${mockId}-height\\\\ \\\\+\\\\ \\\\\\"px\\\\\\"": (height.value + "px")`,
)
assertCode(content)
})
test('the v-for wrapped in parentheses can be correctly parsed & inline is false', () => {
expect(() =>
compile(
`
`,
{
inlineTemplate: false,
},
),
).not.toThrowError()
})
test('unref + new expression', () => {
const { content } = compile(
`
{{ new Foo() }}
{{ new Foo.Bar() }}
`,
{ inlineTemplate: true },
)
expect(content).toMatch(`new (_unref(Foo))()`)
expect(content).toMatch(`new (_unref(Foo)).Bar()`)
assertCode(content)
})
// #12682
test('source map', () => {
const source = `
`
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', () => {
test('hoist type declarations', () => {
const { content } = compile(`
`)
assertCode(content)
})
test('runtime Enum', () => {
const { content, bindings } = compile(
``,
)
assertCode(content)
expect(bindings).toStrictEqual({
Foo: BindingTypes.LITERAL_CONST,
})
})
test('runtime Enum in normal script', () => {
const { content, bindings } = compile(
`
`,
)
assertCode(content)
expect(bindings).toStrictEqual({
D: BindingTypes.LITERAL_CONST,
C: BindingTypes.LITERAL_CONST,
B: BindingTypes.LITERAL_CONST,
Foo: BindingTypes.LITERAL_CONST,
})
})
test('const Enum', () => {
const { content, bindings } = compile(
``,
{ hoistStatic: true },
)
assertCode(content)
expect(bindings).toStrictEqual({
Foo: BindingTypes.LITERAL_CONST,
})
})
test('import type', () => {
const { content } = compile(
``,
)
expect(content).toMatch(`return { get Baz() { return Baz } }`)
assertCode(content)
})
test('with generic attribute', () => {
const { content } = compile(`
`)
assertCode(content)
})
})
describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(``)
if (shouldAsync) {
expect(content).toMatch(`let __temp, __restore`)
}
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
assertCode(content)
return content
}
test('expression statement', () => {
assertAwaitDetection(`await foo`)
})
test('variable', () => {
assertAwaitDetection(`const a = 1 + (await foo)`)
})
test('ref', () => {
assertAwaitDetection(`let a = ref(1 + (await foo))`)
})
// #4448
test('nested await', () => {
assertAwaitDetection(`await (await foo)`)
assertAwaitDetection(`await ((await foo))`)
assertAwaitDetection(`await (await (await foo))`)
})
// should prepend semicolon
test('nested leading await in expression statement', () => {
const code = assertAwaitDetection(`foo()\nawait 1 + await 2`)
expect(code).toMatch(`foo()\n;(`)
})
// #4596 should NOT prepend semicolon
test('single line conditions', () => {
const code = assertAwaitDetection(`if (false) await foo()`)
expect(code).not.toMatch(`if (false) ;(`)
})
test('nested statements', () => {
assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
})
test('multiple `if` nested statements', () => {
assertAwaitDetection(`if (ok) {
let a = 'foo'
await 0 + await 1
await 2
} else if (a) {
await 10
if (b) {
await 0 + await 1
} else {
let a = 'foo'
await 2
}
if (b) {
await 3
await 4
}
} else {
await 5
}`)
})
test('multiple `if while` nested statements', () => {
assertAwaitDetection(`if (ok) {
while (d) {
await 5
}
while (d) {
await 5
await 6
if (c) {
let f = 10
10 + await 7
} else {
await 8
await 9
}
}
}`)
})
test('multiple `if for` nested statements', () => {
assertAwaitDetection(`if (ok) {
for (let a of [1,2,3]) {
await a
}
for (let a of [1,2,3]) {
await a
await a
}
}`)
})
test('should ignore await inside functions', () => {
// function declaration
assertAwaitDetection(`async function foo() { await bar }`, false)
// function expression
assertAwaitDetection(`const foo = async () => { await bar }`, false)
// object method
assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
// class method
assertAwaitDetection(
`const cls = class Foo { async method() { await bar }}`,
false,
)
})
})
describe('errors', () => {
test('`),
).toThrow(``,
),
).toThrow(``),
).toThrow(moduleErrorMsg)
expect(() =>
compile(``),
).toThrow(moduleErrorMsg)
expect(() =>
compile(``),
).toThrow(moduleErrorMsg)
})
test('defineProps/Emit() referencing local var', () => {
expect(() =>
compile(``),
).toThrow(`cannot reference locally declared variables`)
expect(() =>
compile(``),
).toThrow(`cannot reference locally declared variables`)
// #4644
expect(() =>
compile(`
`),
).not.toThrow(`cannot reference locally declared variables`)
})
test('should allow defineProps/Emit() referencing scope var', () => {
assertCode(
compile(``).content,
)
})
test('should allow defineProps/Emit() referencing imported binding', () => {
assertCode(
compile(``).content,
)
})
test('defineModel() referencing local var', () => {
expect(() =>
compile(``),
).toThrow(`cannot reference locally declared variables`)
// allow const
expect(() =>
compile(``),
).not.toThrow(`cannot reference locally declared variables`)
// allow in get/set
expect(() =>
compile(``),
).not.toThrow(`cannot reference locally declared variables`)
})
})
})
describe('SFC analyze
`)
expect(scriptAst).toBeDefined()
})
it('recognizes props array declaration', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
bar: BindingTypes.PROPS,
})
expect(bindings!.__isScriptSetup).toBe(false)
})
it('recognizes props object declaration', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
bar: BindingTypes.PROPS,
baz: BindingTypes.PROPS,
qux: BindingTypes.PROPS,
})
expect(bindings!.__isScriptSetup).toBe(false)
})
it('recognizes setup return', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF,
})
expect(bindings!.__isScriptSetup).toBe(false)
})
it('recognizes exported vars', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.LITERAL_CONST,
})
})
it('recognizes async setup return', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF,
})
expect(bindings!.__isScriptSetup).toBe(false)
})
it('recognizes data return', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.DATA,
bar: BindingTypes.DATA,
})
})
it('recognizes methods', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS })
})
it('recognizes computeds', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.OPTIONS,
bar: BindingTypes.OPTIONS,
})
})
it('recognizes injections array declaration', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.OPTIONS,
bar: BindingTypes.OPTIONS,
})
})
it('recognizes injections object declaration', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.OPTIONS,
bar: BindingTypes.OPTIONS,
})
})
it('works for mixed bindings', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
foo: BindingTypes.OPTIONS,
bar: BindingTypes.PROPS,
baz: BindingTypes.SETUP_MAYBE_REF,
qux: BindingTypes.DATA,
quux: BindingTypes.OPTIONS,
quuz: BindingTypes.OPTIONS,
})
})
it('works for script setup', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
r: BindingTypes.SETUP_CONST,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_LET,
c: BindingTypes.LITERAL_CONST,
d: BindingTypes.SETUP_MAYBE_REF,
e: BindingTypes.SETUP_LET,
foo: BindingTypes.PROPS,
})
})
describe('auto name inference', () => {
test('basic', () => {
const { content } = compile(
`
{{ a }} `,
undefined,
{
filename: 'FooBar.vue',
},
)
expect(content).toMatch(`export default {
__name: 'FooBar'`)
assertCode(content)
})
test('do not overwrite manual name (object)', () => {
const { content } = compile(
`
{{ a }} `,
undefined,
{
filename: 'FooBar.vue',
},
)
expect(content).not.toMatch(`name: 'FooBar'`)
expect(content).toMatch(`name: 'Baz'`)
assertCode(content)
})
test('do not overwrite manual name (call)', () => {
const { content } = compile(
`
{{ a }} `,
undefined,
{
filename: 'FooBar.vue',
},
)
expect(content).not.toMatch(`name: 'FooBar'`)
expect(content).toMatch(`name: 'Baz'`)
assertCode(content)
})
})
})
describe('SFC genDefaultAs', () => {
test('normal `,
{
genDefaultAs: '_sfc_',
},
)
expect(content).not.toMatch('export default')
expect(content).toMatch(`const _sfc_ = {}`)
assertCode(content)
})
test('normal
`,
{
genDefaultAs: '_sfc_',
},
)
expect(content).not.toMatch('export default')
expect(content).not.toMatch('__default__')
expect(content).toMatch(`const _sfc_ = {}`)
assertCode(content)
})
test('
`,
{
genDefaultAs: '_sfc_',
},
)
expect(content).not.toMatch('export default')
expect(content).toMatch(
`const _sfc_ = /*@__PURE__*/Object.assign(__default__`,
)
assertCode(content)
})
test('
`,
{
genDefaultAs: '_sfc_',
},
)
expect(content).not.toMatch('export default')
expect(content).toMatch(
`const _sfc_ = /*@__PURE__*/Object.assign(__default__`,
)
assertCode(content)
})
test('`,
{
genDefaultAs: '_sfc_',
},
)
expect(content).not.toMatch('export default')
expect(content).toMatch(`const _sfc_ = {\n setup`)
assertCode(content)
})
test('`,
{
genDefaultAs: '_sfc_',
},
)
expect(content).not.toMatch('export default')
expect(content).toMatch(`const _sfc_ = /*@__PURE__*/_defineComponent(`)
assertCode(content)
})
test('
`,
{
genDefaultAs: '_sfc_',
},
)
expect(content).not.toMatch('export default')
expect(content).toMatch(
`const _sfc_ = /*@__PURE__*/_defineComponent({\n ...__default__`,
)
assertCode(content)
})
test('binding type for edge cases', () => {
const { bindings } = compile(
``,
)
expect(bindings).toStrictEqual({
toRef: BindingTypes.SETUP_CONST,
props: BindingTypes.SETUP_REACTIVE_CONST,
foo: BindingTypes.SETUP_REF,
})
})
describe('parser plugins', () => {
test('import attributes', () => {
const { content } = compile(`
`)
assertCode(content)
expect(() =>
compile(`
`),
).toThrow()
})
test('import attributes (user override for deprecated syntax)', () => {
const { content } = compile(
`
`,
{
babelParserPlugins: [
['importAttributes', { deprecatedAssertSyntax: true }],
],
},
)
assertCode(content)
})
})
})
describe('compileScript', () => {
test('should care about runtimeModuleName', () => {
const { content } = compile(
`
`,
{
templateOptions: {
compilerOptions: {
runtimeModuleName: 'npm:vue',
},
},
},
)
expect(content).toMatch(
`import { withAsyncContext as _withAsyncContext } from "npm:vue"\n`,
)
assertCode(content)
})
test('should not compile unrecognized language', () => {
const { content, lang, scriptAst } = compile(
``,
)
expect(content).toMatch(`export default
data: ->
myVal: 0`)
expect(lang).toBe('coffee')
expect(scriptAst).not.toBeDefined()
})
})
================================================
FILE: packages/compiler-sfc/__tests__/compileStyle.spec.ts
================================================
import {
type SFCStyleCompileOptions,
compileStyle,
compileStyleAsync,
} from '../src/compileStyle'
import path from 'node:path'
export function compileScoped(
source: string,
options?: Partial
,
): string {
const res = compileStyle({
source,
filename: 'test.css',
id: 'data-v-test',
scoped: true,
...options,
})
if (res.errors.length) {
res.errors.forEach(err => {
console.error(err)
})
expect(res.errors.length).toBe(0)
}
return res.code
}
describe('SFC scoped CSS', () => {
test('simple selectors', () => {
expect(compileScoped(`h1 { color: red; }`)).toMatch(
`h1[data-v-test] { color: red;`,
)
expect(compileScoped(`.foo { color: red; }`)).toMatch(
`.foo[data-v-test] { color: red;`,
)
})
test('descendent selector', () => {
expect(compileScoped(`h1 .foo { color: red; }`)).toMatch(
`h1 .foo[data-v-test] { color: red;`,
)
// #13387
expect(
compileScoped(`main {
width: 100%;
> * {
max-width: 200px;
}
}`),
).toMatchInlineSnapshot(`
"main {
&[data-v-test] {
width: 100%;
}
> *[data-v-test] {
max-width: 200px;
}
}"`)
})
test('nesting selector', () => {
expect(compileScoped(`h1 { color: red; .foo { color: red; } }`)).toMatch(
`h1 {\n&[data-v-test] { color: red;\n}\n.foo[data-v-test] { color: red;`,
)
})
test('nesting selector with atrule and comment', () => {
expect(
compileScoped(
`h1 {
color: red;
/*background-color: pink;*/
@media only screen and (max-width: 800px) {
background-color: green;
.bar { color: white }
}
.foo { color: red; }
}`,
),
).toMatch(
`h1 {
&[data-v-test] {
color: red
/*background-color: pink;*/
}
@media only screen and (max-width: 800px) {
&[data-v-test] {
background-color: green
}
.bar[data-v-test] { color: white
}
}
.foo[data-v-test] { color: red;
}
}`,
)
})
test('multiple selectors', () => {
expect(compileScoped(`h1 .foo, .bar, .baz { color: red; }`)).toMatch(
`h1 .foo[data-v-test], .bar[data-v-test], .baz[data-v-test] { color: red;`,
)
})
test('pseudo class', () => {
expect(compileScoped(`.foo:after { color: red; }`)).toMatch(
`.foo[data-v-test]:after { color: red;`,
)
})
test('pseudo element', () => {
expect(compileScoped(`::selection { display: none; }`)).toMatch(
'[data-v-test]::selection {',
)
})
test('spaces before pseudo element', () => {
const code = compileScoped(`.abc, ::selection { color: red; }`)
expect(code).toMatch('.abc[data-v-test],')
expect(code).toMatch('[data-v-test]::selection {')
})
test('::v-deep', () => {
expect(compileScoped(`:deep(.foo) { color: red; }`)).toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
}"
`)
expect(compileScoped(`::v-deep(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
}"
`)
expect(compileScoped(`::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
"[data-v-test] .foo .bar { color: red;
}"
`)
expect(compileScoped(`.baz .qux ::v-deep(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux[data-v-test] .foo .bar { color: red;
}"
`)
expect(compileScoped(`:is(.foo :deep(.bar)) { color: red; }`))
.toMatchInlineSnapshot(`
":is(.foo[data-v-test] .bar) { color: red;
}"
`)
expect(compileScoped(`:where(.foo :deep(.bar)) { color: red; }`))
.toMatchInlineSnapshot(`
":where(.foo[data-v-test] .bar) { color: red;
}"
`)
expect(compileScoped(`:deep(.foo) { color: red; .bar { color: red; } }`))
.toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
.bar { color: red;
}
}"
`)
})
test('::v-slotted', () => {
expect(compileScoped(`:slotted(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
".foo[data-v-test-s] { color: red;
}"
`)
expect(compileScoped(`::v-slotted(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
".foo[data-v-test-s] { color: red;
}"
`)
expect(compileScoped(`::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar[data-v-test-s] { color: red;
}"
`)
expect(compileScoped(`.baz .qux ::v-slotted(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".baz .qux .foo .bar[data-v-test-s] { color: red;
}"
`)
})
test('::v-global', () => {
expect(compileScoped(`:global(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
".foo { color: red;
}"
`)
expect(compileScoped(`::v-global(.foo) { color: red; }`))
.toMatchInlineSnapshot(`
".foo { color: red;
}"
`)
expect(compileScoped(`::v-global(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar { color: red;
}"
`)
// global ignores anything before it
expect(compileScoped(`.baz .qux ::v-global(.foo .bar) { color: red; }`))
.toMatchInlineSnapshot(`
".foo .bar { color: red;
}"
`)
})
test(':is() and :where() with multiple selectors', () => {
expect(compileScoped(`:is(.foo) { color: red; }`)).toMatchInlineSnapshot(`
":is(.foo[data-v-test]) { color: red;
}"
`)
expect(compileScoped(`:where(.foo, .bar) { color: red; }`))
.toMatchInlineSnapshot(`
":where(.foo[data-v-test], .bar[data-v-test]) { color: red;
}"
`)
expect(compileScoped(`:is(.foo, .bar) div { color: red; }`))
.toMatchInlineSnapshot(`
":is(.foo, .bar) div[data-v-test] { color: red;
}"
`)
})
// #10511
test(':is() and :where() in compound selectors', () => {
expect(
compileScoped(`.div { color: red; } .div:where(:hover) { color: blue; }`),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(:hover) { color: blue;
}"
`)
expect(
compileScoped(`.div { color: red; } .div:is(:hover) { color: blue; }`),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(:hover) { color: blue;
}"
`)
expect(
compileScoped(
`.div { color: red; } .div:where(.foo:hover) { color: blue; }`,
),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(.foo:hover) { color: blue;
}"
`)
expect(
compileScoped(
`.div { color: red; } .div:is(.foo:hover) { color: blue; }`,
),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(.foo:hover) { color: blue;
}"
`)
})
test('media query', () => {
expect(compileScoped(`@media print { .foo { color: red }}`))
.toMatchInlineSnapshot(`
"@media print {
.foo[data-v-test] { color: red
}}"
`)
})
test('supports query', () => {
expect(compileScoped(`@supports(display: grid) { .foo { display: grid }}`))
.toMatchInlineSnapshot(`
"@supports(display: grid) {
.foo[data-v-test] { display: grid
}}"
`)
})
test('scoped keyframes', () => {
const style = compileScoped(
`
.anim {
animation: color 5s infinite, other 5s;
}
.anim-2 {
animation-name: color;
animation-duration: 5s;
}
.anim-3 {
animation: 5s color infinite, 5s other;
}
.anim-multiple {
animation: color 5s infinite, opacity 2s;
}
.anim-multiple-2 {
animation-name: color, opacity;
animation-duration: 5s, 2s;
}
@keyframes color {
from { color: red; }
to { color: green; }
}
@-webkit-keyframes color {
from { color: red; }
to { color: green; }
}
@keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes opacity {
from { opacity: 0; }
to { opacity: 1; }
}
`,
{ id: 'data-v-test' },
)
expect(style).toContain(
`.anim[data-v-test] {\n animation: color-test 5s infinite, other 5s;`,
)
expect(style).toContain(
`.anim-2[data-v-test] {\n animation-name: color-test`,
)
expect(style).toContain(
`.anim-3[data-v-test] {\n animation: 5s color-test infinite, 5s other;`,
)
expect(style).toContain(`@keyframes color-test {`)
expect(style).toContain(`@-webkit-keyframes color-test {`)
expect(style).toContain(
`.anim-multiple[data-v-test] {\n animation: color-test 5s infinite,opacity-test 2s;`,
)
expect(style).toContain(
`.anim-multiple-2[data-v-test] {\n animation-name: color-test,opacity-test;`,
)
expect(style).toContain(`@keyframes opacity-test {\nfrom { opacity: 0;`)
expect(style).toContain(
`@-webkit-keyframes opacity-test {\nfrom { opacity: 0;`,
)
})
// vue-loader/#1370
test('spaces after selector', () => {
expect(compileScoped(`.foo , .bar { color: red; }`)).toMatchInlineSnapshot(`
".foo[data-v-test], .bar[data-v-test] { color: red;
}"
`)
})
describe('deprecated syntax', () => {
test('::v-deep as combinator', () => {
expect(compileScoped(`::v-deep .foo { color: red; }`))
.toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
}"
`)
expect(compileScoped(`.bar ::v-deep .foo { color: red; }`))
.toMatchInlineSnapshot(`
".bar[data-v-test] .foo { color: red;
}"
`)
expect(
`::v-deep usage as a combinator has been deprecated.`,
).toHaveBeenWarned()
})
test('>>> (deprecated syntax)', () => {
const code = compileScoped(`>>> .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
}"
`)
expect(
`the >>> and /deep/ combinators have been deprecated.`,
).toHaveBeenWarned()
})
test('/deep/ (deprecated syntax)', () => {
const code = compileScoped(`/deep/ .foo { color: red; }`)
expect(code).toMatchInlineSnapshot(`
"[data-v-test] .foo { color: red;
}"
`)
expect(
`the >>> and /deep/ combinators have been deprecated.`,
).toHaveBeenWarned()
})
})
})
describe('SFC CSS modules', () => {
test('should include resulting classes object in result', async () => {
const result = await compileStyleAsync({
source: `.red { color: red }\n.green { color: green }\n:global(.blue) { color: blue }`,
filename: `test.css`,
id: 'test',
modules: true,
})
expect(result.modules).toBeDefined()
expect(result.modules!.red).toMatch('_red_')
expect(result.modules!.green).toMatch('_green_')
expect(result.modules!.blue).toBeUndefined()
})
test('postcss-modules options', async () => {
const result = await compileStyleAsync({
source: `:local(.foo-bar) { color: red }\n.baz-qux { color: green }`,
filename: `test.css`,
id: 'test',
modules: true,
modulesOptions: {
scopeBehaviour: 'global',
generateScopedName: `[name]__[local]__[hash:base64:5]`,
localsConvention: 'camelCaseOnly',
},
})
expect(result.modules).toBeDefined()
expect(result.modules!.fooBar).toMatch('__foo-bar__')
expect(result.modules!.bazQux).toBeUndefined()
})
})
describe('SFC style preprocessors', () => {
test('scss @import', () => {
const res = compileStyle({
source: `
@import "./import.scss";
`,
filename: path.resolve(__dirname, './fixture/test.scss'),
id: '',
preprocessLang: 'scss',
})
expect([...res.dependencies]).toStrictEqual([
path.join(__dirname, './fixture/import.scss'),
])
})
test('scss respect user-defined string options.additionalData', () => {
const res = compileStyle({
preprocessOptions: {
additionalData: `
@mixin square($size) {
width: $size;
height: $size;
}`,
},
source: `
.square {
@include square(100px);
}
`,
filename: path.resolve(__dirname, './fixture/test.scss'),
id: '',
preprocessLang: 'scss',
})
expect(res.errors.length).toBe(0)
})
test('scss respect user-defined function options.additionalData', () => {
const source = `
.square {
@include square(100px);
}
`
const filename = path.resolve(__dirname, './fixture/test.scss')
const res = compileStyle({
preprocessOptions: {
additionalData: (s: string, f: string) => {
expect(s).toBe(source)
expect(f).toBe(filename)
return `
@mixin square($size) {
width: $size;
height: $size;
}`
},
},
source,
filename,
id: '',
preprocessLang: 'scss',
})
expect(res.errors.length).toBe(0)
})
test('should mount scope on correct selector when have universal selector', () => {
expect(compileScoped(`* { color: red; }`)).toMatchInlineSnapshot(`
"[data-v-test] { color: red;
}"
`)
expect(compileScoped('* .foo { color: red; }')).toMatchInlineSnapshot(`
".foo[data-v-test] { color: red;
}"
`)
expect(compileScoped(`*.foo { color: red; }`)).toMatchInlineSnapshot(`
".foo[data-v-test] { color: red;
}"
`)
expect(compileScoped(`.foo * { color: red; }`)).toMatchInlineSnapshot(`
".foo[data-v-test] * { color: red;
}"
`)
})
})
================================================
FILE: packages/compiler-sfc/__tests__/compileTemplate.spec.ts
================================================
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
import { parse as babelParse } from '@babel/parser'
import {
type SFCTemplateCompileOptions,
compileTemplate,
} from '../src/compileTemplate'
import { type SFCTemplateBlock, parse } from '../src/parse'
import { compileScript } from '../src'
import { getPositionInCode } from './utils'
function compile(opts: Omit) {
return compileTemplate({
...opts,
id: '',
})
}
test('should work', () => {
const source = ``
const result = compile({ filename: 'example.vue', source })
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
// should expose render fn
expect(result.code).toMatch(`export function render(`)
})
// #6807
test('should work with style comment', () => {
const source = `
{{ render }}
`
const result = compile({ filename: 'example.vue', source })
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
expect(result.code).toMatch(`{"width":"300px","height":"100px"}`)
})
test('preprocess pug', () => {
const template = parse(
`
body
h1 Pug Examples
div.container
p Cool Pug example!
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(0)
})
test('preprocess pug with indents and blank lines', () => {
const template = parse(
`
body
h1 The next line contains four spaces.
div.container
p The next line is empty.
p This is the last line.
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(0)
expect(result.source).toBe(
'The next line contains four spaces. This is the last line.
',
)
})
test('warn missing preprocessor', () => {
const template = parse(`hi \n`, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(1)
})
test('transform asset url options', () => {
const input = { source: ` `, filename: 'example.vue' }
// Object option
const { code: code1 } = compile({
...input,
transformAssetUrls: {
tags: { foo: ['bar'] },
},
})
expect(code1).toMatch(`import _imports_0 from 'baz'\n`)
// legacy object option (direct tags config)
const { code: code2 } = compile({
...input,
transformAssetUrls: {
foo: ['bar'],
},
})
expect(code2).toMatch(`import _imports_0 from 'baz'\n`)
// false option
const { code: code3 } = compile({
...input,
transformAssetUrls: false,
})
expect(code3).not.toMatch(`import _imports_0 from 'baz'\n`)
})
test('source map', () => {
const template = parse(
`
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template!
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
})
expect(map!.sources).toEqual([`example.vue`])
expect(map!.sourcesContent).toEqual([template.content])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
).toMatchObject(getPositionInCode(template.content, `foobar`))
})
test('source map: v-if generated comment should not have original position', () => {
const template = parse(
`
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template!
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
})
expect(map!.sources).toEqual([`example.vue`])
expect(map!.sourcesContent).toEqual([template.content])
const consumer = new SourceMapConsumer(map as RawSourceMap)
const commentNode = code.match(/_createCommentVNode\("v-if", true\)/)
expect(commentNode).not.toBeNull()
const commentPosition = getPositionInCode(code, commentNode![0])
const originalPosition = consumer.originalPositionFor(commentPosition)
// the comment node should not be mapped to the original source
expect(originalPosition.column).toBeNull()
expect(originalPosition.line).toBeNull()
expect(originalPosition.source).toBeNull()
})
test('should work w/ AST from descriptor', () => {
const source = `
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
expect(template.ast!.source).toBe(source)
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
ast: template.ast,
})
expect(map!.sources).toEqual([`example.vue`])
// when reusing AST from SFC parse for template compile,
// the source corresponds to the entire SFC
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
).toMatchObject(getPositionInCode(source, `foobar`))
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
}).code,
)
})
test('should work w/ AST from descriptor in SSR mode', () => {
const source = `
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
expect(template.ast!.source).toBe(source)
const { code, map } = compile({
filename: 'example.vue',
source: '', // make sure it's actually using the AST instead of source
ast: template.ast,
ssr: true,
})
expect(map!.sources).toEqual([`example.vue`])
// when reusing AST from SFC parse for template compile,
// the source corresponds to the entire SFC
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
).toMatchObject(getPositionInCode(source, `foobar`))
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
ssr: true,
}).code,
)
})
test('should not reuse AST if using custom compiler', () => {
const source = `
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
const { code } = compile({
filename: 'example.vue',
source: template.content,
ast: template.ast,
compiler: {
parse: () => null as any,
// @ts-expect-error
compile: input => ({ code: input }),
},
})
// what we really want to assert is that the `input` received by the custom
// compiler is the source string, not the AST.
expect(code).toBe(template.content)
})
test('should force re-parse on already transformed AST', () => {
const source = `
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
// force set to empty, if this is reused then it won't generate proper code
template.ast!.children = []
template.ast!.transformed = true
const { code } = compile({
filename: 'example.vue',
source: '',
ast: template.ast,
})
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
}).code,
)
})
test('should force re-parse with correct compiler in SSR mode', () => {
const source = `
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
// force set to empty, if this is reused then it won't generate proper code
template.ast!.children = []
template.ast!.transformed = true
const { code } = compile({
filename: 'example.vue',
source: '',
ast: template.ast,
ssr: true,
})
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
ssr: true,
}).code,
)
})
test('template errors', () => {
const result = compile({
filename: 'example.vue',
source: `
`,
})
expect(result.errors).toMatchSnapshot()
})
test('preprocessor errors', () => {
const template = parse(
`
div(class='class)
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(1)
const message = result.errors[0].toString()
expect(message).toMatch(`Error: example.vue:3:1`)
expect(message).toMatch(
`The end of the string reached with no closing bracket ) found.`,
)
})
// #3447
test('should generate the correct imports expression', () => {
const { code } = compile({
filename: 'example.vue',
source: `
`,
ssr: true,
})
expect(code).toMatch(`_ssrRenderAttr(\"src\", _imports_1)`)
expect(code).toMatch(`_createVNode(\"img\", { src: _imports_1 })`)
})
// #3874
test('should not hoist srcset URLs in SSR mode', () => {
const { code } = compile({
filename: 'example.vue',
source: `
`,
ssr: true,
})
expect(code).toMatchSnapshot()
})
// #6742
test('dynamic v-on + static v-on should merged', () => {
const source = ` `
const result = compile({ filename: 'example.vue', source })
expect(result.code).toMatchSnapshot()
})
// #9853 regression found in Nuxt tests
// walkIdentifiers can get called multiple times on the same node
// due to #9729 calling it during SFC template usage check.
// conditions needed:
// 1. `
{{ list.map((t, index) => ({ t: t })) }}
`
const { descriptor } = parse(src)
// compileScript triggers importUsageCheck
compileScript(descriptor, { id: 'xxx' })
const { code } = compileTemplate({
id: 'xxx',
filename: 'test.vue',
ast: descriptor.template!.ast,
source: descriptor.template!.content,
})
expect(code).not.toMatch(`_ctx.t`)
})
test('prefixing edge case for reused AST ssr mode', () => {
const src = `
`
const { descriptor } = parse(src)
// compileScript triggers importUsageCheck
compileScript(descriptor, { id: 'xxx' })
expect(() =>
compileTemplate({
id: 'xxx',
filename: 'test.vue',
ast: descriptor.template!.ast,
source: descriptor.template!.content,
ssr: true,
}),
).not.toThrowError()
})
// #10852
test('non-identifier expression in legacy filter syntax', () => {
const src = `
Today is
{{ new Date() | formatDate }}
`
const { descriptor } = parse(src)
const compilationResult = compileTemplate({
id: 'xxx',
filename: 'test.vue',
ast: descriptor.template!.ast,
source: descriptor.template!.content,
ssr: false,
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
})
expect(() => {
babelParse(compilationResult.code, { sourceType: 'module' })
}).not.toThrow()
})
test('prefixing props edge case in inline mode', () => {
const src = `
`
const { descriptor } = parse(src)
const { content } = compileScript(descriptor, {
id: 'xxx',
inlineTemplate: true,
})
expect(content).toMatchSnapshot()
expect(content).toMatch(`__props["Foo"]).Bar`)
})
================================================
FILE: packages/compiler-sfc/__tests__/cssVars.spec.ts
================================================
import { compileStyle, parse } from '../src'
import { assertCode, compileSFCScript, mockId } from './utils'
describe('CSS vars injection', () => {
test('generating correct code for nested paths', () => {
const { content } = compileSFCScript(
`\n` +
``,
)
expect(content).toMatch(`_useCssVars(_ctx => ({
"${mockId}-color": (_ctx.color),
"${mockId}-font\\.size": (_ctx.font.size)
})`)
assertCode(content)
})
test('w/ normal \n` +
``,
)
expect(content).toMatch(`_useCssVars(_ctx => ({
"${mockId}-size": (_ctx.size)
})`)
expect(content).toMatch(`import { useCssVars as _useCssVars } from 'vue'`)
assertCode(content)
})
test('w/ \n` +
``,
)
// should handle:
// 1. local const bindings
// 2. local potential ref bindings
// 3. props bindings (analyzed)
expect(content).toMatch(`_useCssVars(_ctx => ({
"${mockId}-color": (color),
"${mockId}-size": (size.value),
"${mockId}-foo": (__props.foo)
})`)
expect(content).toMatch(
`import { useCssVars as _useCssVars, unref as _unref } from 'vue'`,
)
assertCode(content)
})
test('should rewrite CSS vars in compileStyle', () => {
const { code } = compileStyle({
source: `.foo {
color: v-bind(color);
font-size: v-bind('font.size');
font-weight: v-bind(_φ);
font-size: v-bind(1-字号);
font-family: v-bind(フォント);
}`,
filename: 'test.css',
id: 'data-v-test',
})
expect(code).toMatchInlineSnapshot(`
".foo {
color: var(--test-color);
font-size: var(--test-font\\.size);
font-weight: var(--test-_φ);
font-size: var(--test-1-字号);
font-family: var(--test-フォント);
}"
`)
})
test('prod mode', () => {
const { content } = compileSFCScript(
`\n` +
``,
{ isProd: true },
)
expect(content).toMatch(`_useCssVars(_ctx => ({
"v4003f1a6": (_ctx.color),
"v41b6490a": (_ctx.font.size)
}))}`)
const { code } = compileStyle({
source: `.foo {
color: v-bind(color);
font-size: v-bind('font.size');
}`,
filename: 'test.css',
id: mockId,
isProd: true,
})
expect(code).toMatchInlineSnapshot(`
".foo {
color: var(--v4003f1a6);
font-size: var(--v41b6490a);
}"
`)
})
describe('codegen', () => {
test('\n` +
``,
).content,
)
})
test('\n` +
``,
).content,
)
})
test('\n` + ``,
).content,
)
})
test('w/ \n` +
``,
).content,
)
})
//#4185
test('should ignore comments', () => {
const { content } = compileSFCScript(
`\n` +
``,
)
expect(content).not.toMatch(`"${mockId}-color": (color)`)
expect(content).toMatch(`"${mockId}-width": (width)`)
assertCode(content)
})
test('w/ \n` +
``,
)
// color should only be injected once, even if it is twice in style
expect(content).toMatch(`_useCssVars(_ctx => ({
"${mockId}-color": (color)
})`)
assertCode(content)
})
test('should work with w/ complex expression', () => {
const { content } = compileSFCScript(
`\n` +
``,
)
expect(content).toMatch(`_useCssVars(_ctx => ({
"${mockId}-foo": (_unref(foo)),
"${mockId}-foo\\ \\+\\ \\'px\\'": (_unref(foo) + 'px'),
"${mockId}-\\(a\\ \\+\\ b\\)\\ \\/\\ 2\\ \\+\\ \\'px\\'": ((_unref(a) + _unref(b)) / 2 + 'px'),
"${mockId}-\\(\\(a\\ \\+\\ b\\)\\)\\ \\/\\ \\(2\\ \\*\\ a\\)": (((_unref(a) + _unref(b))) / (2 * _unref(a)))
}))`)
assertCode(content)
})
// #6022
test('should be able to parse incomplete expressions', () => {
const {
descriptor: { cssVars },
} = parse(
`
`,
)
expect(cssVars).toMatchObject([`count.toString(`, `xxx`])
})
// #7759
test('It should correctly parse the case where there is no space after the script tag', () => {
const { content } = compileSFCScript(
`
`,
)
expect(content).toMatch(
`export default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCssVars(_ctx => ({\n "xxxxxxxx-background": (_unref(background))\n}))`,
)
})
describe('skip codegen in SSR', () => {
test('script setup, inline', () => {
const { content } = compileSFCScript(
`\n` +
``,
{
inlineTemplate: true,
templateOptions: {
ssr: true,
},
},
)
expect(content).not.toMatch(`_useCssVars`)
})
// #6926
test('script, non-inline', () => {
const { content } = compileSFCScript(
`\n` +
``,
{
inlineTemplate: false,
templateOptions: {
ssr: true,
},
},
)
expect(content).not.toMatch(`_useCssVars`)
})
test('normal script', () => {
const { content } = compileSFCScript(
`\n` +
``,
{
templateOptions: {
ssr: true,
},
},
)
expect(content).not.toMatch(`_useCssVars`)
})
})
})
})
================================================
FILE: packages/compiler-sfc/__tests__/fixture/import.scss
================================================
div {
color: red;
}
================================================
FILE: packages/compiler-sfc/__tests__/parse.spec.ts
================================================
import { parse } from '../src'
import {
ElementTypes,
NodeTypes,
baseCompile,
createRoot,
} from '@vue/compiler-core'
import { SourceMapConsumer } from 'source-map-js'
describe('compiler:sfc', () => {
describe('source map', () => {
test('style block', () => {
// Padding determines how many blank lines will there be before the style block
const padding = Math.round(Math.random() * 10)
const src =
`${'\n'.repeat(padding)}` +
`
`
const {
descriptor: { styles },
} = parse(src)
expect(styles[0].map).not.toBeUndefined()
const consumer = new SourceMapConsumer(styles[0].map!)
const lineOffset =
src.slice(0, src.indexOf(`
{ "greeting": "hello" }
`
const padFalse = parse(content.trim(), { pad: false }).descriptor
expect(padFalse.template!.content).toBe('\n
\n')
expect(padFalse.script!.content).toBe('\nexport default {}\n')
expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
expect(padFalse.customBlocks[0].content).toBe('\n{ "greeting": "hello" }\n')
const padTrue = parse(content.trim(), { pad: true }).descriptor
expect(padTrue.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n',
)
expect(padTrue.styles[0].content).toBe(
Array(6 + 1).join('\n') + '\nh1 { color: red }\n',
)
expect(padTrue.customBlocks[0].content).toBe(
Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n',
)
const padLine = parse(content.trim(), { pad: 'line' }).descriptor
expect(padLine.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n',
)
expect(padLine.styles[0].content).toBe(
Array(6 + 1).join('\n') + '\nh1 { color: red }\n',
)
expect(padLine.customBlocks[0].content).toBe(
Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n',
)
const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
expect(padSpace.script!.content).toBe(
`\n
\n \n\n\n`.replace(
/./g,
' ',
) + '\n{ "greeting": "hello" }\n',
)
})
test('should parse correct range for root level self closing tag', () => {
const content = `\n
\n`
const { descriptor } = parse(`${content} `)
expect(descriptor.template).toBeTruthy()
expect(descriptor.template!.content).toBe(content)
expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 11, offset: 10 },
end: {
line: 3,
column: 1,
offset: 10 + content.length,
},
})
})
test('should parse correct range for blocks with no content (self closing)', () => {
const { descriptor } = parse(` `)
expect(descriptor.template).toBeTruthy()
expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 12, offset: 11 },
end: { line: 1, column: 12, offset: 11 },
})
})
test('should parse correct range for blocks with no content (explicit)', () => {
const { descriptor } = parse(` `)
expect(descriptor.template).toBeTruthy()
expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 11, offset: 10 },
end: { line: 1, column: 11, offset: 10 },
})
})
test('should ignore other nodes with no content', () => {
expect(parse(``).descriptor.script).toBe(null)
expect(parse(``).descriptor.script).toBe(null)
expect(parse(``).descriptor.styles.length).toBe(0)
expect(parse(``).descriptor.styles.length).toBe(0)
expect(parse(` `).descriptor.customBlocks.length).toBe(0)
expect(
parse(` \n\t `).descriptor.customBlocks.length,
).toBe(0)
})
test('handle empty nodes with src attribute', () => {
const { descriptor } = parse(``)
expect(descriptor.script).toBeTruthy()
expect(descriptor.script!.content).toBeFalsy()
expect(descriptor.script!.attrs['src']).toBe('com')
})
test('should not expose ast on template node if has src import', () => {
const { descriptor } = parse(` `)
expect(descriptor.template!.ast).toBeUndefined()
})
test('ignoreEmpty: false', () => {
const { descriptor } = parse(
`\n`,
{
ignoreEmpty: false,
},
)
expect(descriptor.script).toBeTruthy()
expect(descriptor.script!.loc).toMatchObject({
start: { line: 1, column: 9, offset: 8 },
end: { line: 1, column: 9, offset: 8 },
})
expect(descriptor.scriptSetup).toBeTruthy()
expect(descriptor.scriptSetup!.loc).toMatchObject({
start: { line: 2, column: 15, offset: 32 },
end: { line: 3, column: 1, offset: 33 },
})
})
test('nested templates', () => {
const content = `
ok
`
const { descriptor } = parse(`${content} `)
expect(descriptor.template!.content).toBe(content)
})
test('treat empty lang attribute as the html', () => {
const content = `ok
`
const { descriptor, errors } = parse(
`${content} `,
)
expect(descriptor.template!.content).toBe(content)
expect(errors.length).toBe(0)
})
// #1120
test('template with preprocessor lang should be treated as plain text', () => {
const content = `p(v-if="1 < 2") test
`
const { descriptor, errors } = parse(
`` + content + ` `,
)
expect(errors.length).toBe(0)
expect(descriptor.template!.content).toBe(content)
// should not attempt to parse the content
expect(descriptor.template!.ast!.children.length).toBe(1)
})
//#2566
test('div lang should not be treated as plain text', () => {
const { errors } = parse(`
`)
expect(errors.length).toBe(0)
})
test('slotted detection', async () => {
expect(parse(`hi `).descriptor.slotted).toBe(false)
expect(
parse(`hi `).descriptor
.slotted,
).toBe(false)
expect(
parse(
`hi `,
).descriptor.slotted,
).toBe(true)
expect(
parse(
`hi `,
).descriptor.slotted,
).toBe(true)
})
test('error tolerance', () => {
const { errors } = parse(``)
expect(errors.length).toBe(1)
})
test('should parse as DOM by default', () => {
const { errors } = parse(` `)
expect(errors.length).toBe(0)
})
test('custom compiler', () => {
const { errors } = parse(` `, {
compiler: {
parse: (_, options) => {
options.onError!(new Error('foo') as any)
return createRoot([])
},
compile: baseCompile,
},
})
expect(errors.length).toBe(2)
// error thrown by the custom parse
expect(errors[0].message).toBe('foo')
// error thrown based on the returned root
expect(errors[1].message).toMatch('At least one')
})
test('treat custom blocks as raw text', () => {
const { errors, descriptor } = parse(
` <-& `,
)
expect(errors.length).toBe(0)
expect(descriptor.customBlocks[0].content).toBe(` <-& `)
})
test('should accept parser options', () => {
const { errors, descriptor } = parse(` `, {
templateParseOptions: {
isCustomElement: t => t === 'hello',
},
})
expect(errors.length).toBe(0)
expect(descriptor.template!.ast!.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'hello',
tagType: ElementTypes.ELEMENT,
})
// test cache invalidation on different options
const { descriptor: d2 } = parse(` `, {
templateParseOptions: {
isCustomElement: t => t !== 'hello',
},
})
expect(d2.template!.ast!.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'hello',
tagType: ElementTypes.COMPONENT,
})
})
describe('warnings', () => {
function assertWarning(errors: Error[], msg: string) {
expect(errors.some(e => e.message.match(msg))).toBe(true)
}
test('should only allow single template element', () => {
assertWarning(
parse(`
`).errors,
`Single file component can contain only one element`,
)
})
test('should only allow single script element', () => {
assertWarning(
parse(``)
.errors,
`Single file component can contain only one `,
).errors,
`Single file component can contain only one `,
).errors.length,
).toBe(0)
})
// # 6676
test('should throw error if no or
click here to hydrate
0
================================================
FILE: packages/vue/__tests__/e2e/hydration-strat-idle.html
================================================
0
================================================
FILE: packages/vue/__tests__/e2e/hydration-strat-interaction.html
================================================
click to hydrate
0
================================================
FILE: packages/vue/__tests__/e2e/hydration-strat-media.html
================================================
resize the window width to < 500px to hydrate
0
================================================
FILE: packages/vue/__tests__/e2e/hydration-strat-visible.html
================================================
scroll to the bottom to hydrate
0
================================================
FILE: packages/vue/__tests__/e2e/hydrationStrategies.spec.ts
================================================
import path from 'node:path'
import { setupPuppeteer } from './e2eUtils'
import type { Ref } from '../../src/runtime'
declare const window: Window & {
isHydrated: boolean
isRootMounted: boolean
teardownCalled?: boolean
show: Ref
}
describe('async component hydration strategies', () => {
const { page, click, text, count } = setupPuppeteer(['--window-size=800,600'])
async function goToCase(name: string, query = '') {
const file = `file://${path.resolve(__dirname, `./hydration-strat-${name}.html${query}`)}`
await page().goto(file)
}
async function assertHydrationSuccess(n = '1') {
await click('button')
expect(await text('button')).toBe(n)
}
test('idle', async () => {
const messages: string[] = []
page().on('console', e => messages.push(e.text()))
await goToCase('idle')
// not hydrated yet
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
// wait for hydration
await page().waitForFunction(() => window.isHydrated)
// assert message order: hyration should happen after already queued main thread work
expect(messages.slice(1)).toMatchObject(['resolve', 'busy', 'hydrated'])
await assertHydrationSuccess()
})
test('visible', async () => {
await goToCase('visible')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
// scroll down
await page().evaluate(() => window.scrollTo({ top: 1000 }))
await page().waitForFunction(() => window.isHydrated)
await assertHydrationSuccess()
})
test('visible (with rootMargin)', async () => {
await goToCase('visible', '?rootMargin=1000')
await page().waitForFunction(() => window.isRootMounted)
// should hydrate without needing to scroll
await page().waitForFunction(() => window.isHydrated)
await assertHydrationSuccess()
})
test('visible (fragment)', async () => {
await goToCase('visible', '?fragment')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
expect(await count('span')).toBe(2)
// scroll down
await page().evaluate(() => window.scrollTo({ top: 1000 }))
await page().waitForFunction(() => window.isHydrated)
await assertHydrationSuccess()
})
test('visible (root v-if) should not throw error', async () => {
const spy = vi.fn()
const currentPage = page()
currentPage.on('pageerror', spy)
await goToCase('visible', '?v-if')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
expect(spy).toBeCalledTimes(0)
currentPage.off('pageerror', spy)
})
test('media query', async () => {
await goToCase('media')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
// resize
await page().setViewport({ width: 400, height: 600 })
await page().waitForFunction(() => window.isHydrated)
await assertHydrationSuccess()
})
// #13255
test('media query (patched before hydration)', async () => {
const spy = vi.fn()
const currentPage = page()
currentPage.on('pageerror', spy)
const warn: any[] = []
currentPage.on('console', e => warn.push(e.text()))
await goToCase('media')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
// patch
await page().evaluate(() => (window.show.value = false))
await click('button')
expect(await text('button')).toBe('1')
// resize
await page().setViewport({ width: 400, height: 600 })
await page().waitForFunction(() => window.isHydrated)
await assertHydrationSuccess('2')
expect(spy).toBeCalledTimes(0)
currentPage.off('pageerror', spy)
expect(
warn.some(w => w.includes('Skipping lazy hydration for component')),
).toBe(true)
})
test('interaction', async () => {
await goToCase('interaction')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
await click('button')
await page().waitForFunction(() => window.isHydrated)
// should replay event
expect(await text('button')).toBe('1')
await assertHydrationSuccess('2')
})
test('interaction (fragment)', async () => {
await goToCase('interaction', '?fragment')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
await click('button')
await page().waitForFunction(() => window.isHydrated)
// should replay event
expect(await text('button')).toBe('1')
await assertHydrationSuccess('2')
})
test('custom', async () => {
await goToCase('custom')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
await click('#custom-trigger')
await page().waitForFunction(() => window.isHydrated)
await assertHydrationSuccess()
})
test('custom teardown', async () => {
await goToCase('custom')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
await page().evaluate(() => (window.show.value = false))
expect(await text('#app')).toBe('off')
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
expect(await page().evaluate(() => window.teardownCalled)).toBe(true)
})
})
================================================
FILE: packages/vue/__tests__/e2e/markdown.spec.ts
================================================
import path from 'node:path'
import { E2E_TIMEOUT, expectByPolling, setupPuppeteer } from './e2eUtils'
describe('e2e: markdown', () => {
const { page, isVisible, value, html } = setupPuppeteer()
async function testMarkdown(apiType: 'classic' | 'composition') {
const baseUrl = `file://${path.resolve(
__dirname,
`../../examples/${apiType}/markdown.html#test`,
)}`
await page().goto(baseUrl)
expect(await isVisible('#editor')).toBe(true)
expect(await value('textarea')).toBe('# hello')
expect(await html('#editor div')).toBe('hello \n')
await page().type('textarea', '\n## foo\n\n- bar\n- baz')
// assert the output is not updated yet because of debounce
// debounce has become unstable on CI so this assertion is disabled
// expect(await html('#editor div')).toBe('hello \n')
await expectByPolling(
() => html('#editor div'),
'hello \n' +
'foo \n' +
'\n',
)
}
test(
'classic',
async () => {
await testMarkdown('classic')
},
E2E_TIMEOUT,
)
test(
'composition',
async () => {
await testMarkdown('composition')
},
E2E_TIMEOUT,
)
})
================================================
FILE: packages/vue/__tests__/e2e/memory-leak.spec.ts
================================================
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
import path from 'node:path'
const { page, html, click } = setupPuppeteer()
beforeEach(async () => {
await page().setContent(`
`)
await page().addScriptTag({
path: path.resolve(__dirname, '../../dist/vue.global.js'),
})
})
describe('not leaking', async () => {
// #13661
test(
'cached text vnodes should not retaining detached DOM nodes',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
Comp1: {
template: `
{{ test.length }}
`,
setup() {
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error
window.__REF__ = new WeakRef(test)
return { test }
},
},
Comp2: {
template: `comp2 `,
},
},
template: `
button
text node
`,
setup() {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})
expect(await html('#app')).toBe(
`button ` +
`` +
` ` +
`
comp2 ` +
` text node ` +
`` +
`` +
`3000
`,
)
await click('#toggleBtn')
expect(await html('#app')).toBe(
`button `,
)
const isCollected = async () =>
// @ts-expect-error
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
// #13211
test(
'cached array vnodes should not retaining detached DOM nodes',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
Comp1: {
template: `
{{ test.length }}
`,
setup() {
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error
window.__REF__ = new WeakRef(test)
return { test }
},
},
},
template: `
button
slot content
`,
setup() {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})
expect(await html('#app')).toBe(
`button ` +
`` +
`slot content` +
` ` +
`3000
`,
)
await click('#toggleBtn')
expect(await html('#app')).toBe(
`button `,
)
const isCollected = async () =>
// @ts-expect-error
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
// https://github.com/element-plus/element-plus/issues/21408
test(
'cached text nodes in Fragment should not retaining detached DOM nodes',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
Comp: {
template: `{{ test.length }}
`,
setup() {
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error
window.__REF__ = new WeakRef(test)
return { test }
},
},
},
template: `
add
button
`,
setup() {
const toggle = ref(true)
const items = ref([1])
const click = () => (toggle.value = !toggle.value)
const add = () => items.value.push(2)
return { toggle, click, items, add }
},
}).mount('#app')
})
expect(await html('#app')).toBe(
`add ` +
`button ` +
`` +
` text ` +
`
1
` +
`
3000
`,
)
await click('#addBtn')
expect(await html('#app')).toBe(
`add ` +
`button ` +
`` +
` text ` +
`
1
` +
` text ` +
`
2
` +
`
3000
`,
)
await click('#toggleBtn')
expect(await html('#app')).toBe(
`add ` +
`button `,
)
const isCollected = async () =>
// @ts-expect-error
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
})
================================================
FILE: packages/vue/__tests__/e2e/ssr-custom-element.spec.ts
================================================
import path from 'node:path'
import { setupPuppeteer } from './e2eUtils'
const { page, click, text } = setupPuppeteer()
beforeEach(async () => {
await page().addScriptTag({
path: path.resolve(__dirname, '../../dist/vue.global.js'),
})
})
async function setContent(html: string) {
await page().setContent(`${html}
`)
}
// this must be tested in actual Chrome because jsdom does not support
// declarative shadow DOM
test('ssr custom element hydration', async () => {
await setContent(
`1 1 `,
)
await page().evaluate(() => {
const {
h,
ref,
defineSSRCustomElement,
defineAsyncComponent,
onMounted,
useHost,
} = (window as any).Vue
const def = {
setup() {
const count = ref(1)
const el = useHost()
onMounted(() => (el.style.border = '1px solid red'))
return () => h('button', { onClick: () => count.value++ }, count.value)
},
}
customElements.define('my-element', defineSSRCustomElement(def))
customElements.define(
'my-element-async',
defineSSRCustomElement(
defineAsyncComponent(
() =>
new Promise(r => {
;(window as any).resolve = () => r(def)
}),
),
),
)
})
function getColor() {
return page().evaluate(() => {
return [
(document.querySelector('my-element') as any).style.border,
(document.querySelector('my-element-async') as any).style.border,
]
})
}
expect(await getColor()).toMatchObject(['1px solid red', ''])
await page().evaluate(() => (window as any).resolve()) // exposed by test
expect(await getColor()).toMatchObject(['1px solid red', '1px solid red'])
async function assertInteraction(el: string) {
const selector = `${el} >>> button`
expect(await text(selector)).toBe('1')
await click(selector)
expect(await text(selector)).toBe('2')
}
await assertInteraction('my-element')
await assertInteraction('my-element-async')
})
test('work with Teleport (shadowRoot: false)', async () => {
await setContent(
`
default `,
)
await page().evaluate(() => {
const { h, defineSSRCustomElement, Teleport, renderSlot } = (window as any)
.Vue
const Y = defineSSRCustomElement(
{
render() {
return h(
Teleport,
{ to: '#test' },
{
default: () => [renderSlot(this.$slots, 'default')],
},
)
},
},
{ shadowRoot: false },
)
customElements.define('my-y', Y)
const P = defineSSRCustomElement(
{
render() {
return renderSlot(this.$slots, 'default')
},
},
{ shadowRoot: false },
)
customElements.define('my-p', P)
})
function getInnerHTML() {
return page().evaluate(() => {
return (document.querySelector('#test') as any).innerHTML
})
}
expect(await getInnerHTML()).toBe('default ')
})
// #11641
test('pass key to custom element', async () => {
const messages: string[] = []
page().on('console', e => messages.push(e.text()))
await setContent(
`1
`,
)
await page().evaluate(() => {
const {
h,
ref,
defineSSRCustomElement,
onBeforeUnmount,
onMounted,
createSSRApp,
renderList,
} = (window as any).Vue
const MyElement = defineSSRCustomElement({
props: {
str: String,
},
setup(props: any) {
onMounted(() => {
console.log('child mounted')
})
onBeforeUnmount(() => {
console.log('child unmount')
})
return () => h('div', props.str)
},
})
customElements.define('my-element', MyElement)
createSSRApp({
setup() {
const arr = ref(['1'])
// pass key to custom element
return () =>
renderList(arr.value, (i: string) =>
h('my-element', { key: i, str: i }, null),
)
},
}).mount('#app')
})
expect(messages.includes('child mounted')).toBe(true)
expect(messages.includes('child unmount')).toBe(false)
expect(await text('my-element >>> div')).toBe('1')
})
================================================
FILE: packages/vue/__tests__/e2e/svg.spec.ts
================================================
import path from 'node:path'
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
declare const globalStats: {
label: string
value: number
}[]
declare function valueToPoint(
value: number,
index: number,
total: number,
): {
x: number
y: number
}
describe('e2e: svg', () => {
const { page, click, count, setValue, typeValue } = setupPuppeteer()
// assert the shape of the polygon is correct
async function assertPolygon(total: number) {
expect(
await page().evaluate(
([total]) => {
const points = globalStats
.map((stat, i) => {
const point = valueToPoint(stat.value, i, total)
return point.x + ',' + point.y
})
.join(' ')
return (
document.querySelector('polygon')!.attributes[0].value === points
)
},
[total],
),
).toBe(true)
}
// assert the position of each label is correct
async function assertLabels(total: number) {
const positions = await page().evaluate(
([total]) => {
return globalStats.map((stat, i) => {
const point = valueToPoint(+stat.value + 10, i, total)
return [point.x, point.y]
})
},
[total],
)
for (let i = 0; i < total; i++) {
const textPosition = await page().$eval(
`text:nth-child(${i + 3})`,
node => [+node.attributes[0].value, +node.attributes[1].value],
)
expect(textPosition).toEqual(positions[i])
}
}
// assert each value of stats is correct
async function assertStats(expected: number[]) {
const statsValue = await page().evaluate(() => {
return globalStats.map(stat => +stat.value)
})
expect(statsValue).toEqual(expected)
}
function nthRange(n: number) {
return `#demo div:nth-child(${n + 1}) input[type="range"]`
}
async function testSvg(apiType: 'classic' | 'composition') {
const baseUrl = `file://${path.resolve(
__dirname,
`../../examples/${apiType}/svg.html`,
)}`
await page().goto(baseUrl)
await page().waitForSelector('svg')
expect(await count('g')).toBe(1)
expect(await count('polygon')).toBe(1)
expect(await count('circle')).toBe(1)
expect(await count('text')).toBe(6)
expect(await count('label')).toBe(6)
expect(await count('button')).toBe(7)
expect(await count('input[type="range"]')).toBe(6)
await assertPolygon(6)
await assertLabels(6)
await assertStats([100, 100, 100, 100, 100, 100])
await setValue(nthRange(1), '10')
await assertPolygon(6)
await assertLabels(6)
await assertStats([10, 100, 100, 100, 100, 100])
await click('button.remove')
expect(await count('text')).toBe(5)
expect(await count('label')).toBe(5)
expect(await count('button')).toBe(6)
expect(await count('input[type="range"]')).toBe(5)
await assertPolygon(5)
await assertLabels(5)
await assertStats([100, 100, 100, 100, 100])
await typeValue('input[name="newlabel"]', 'foo')
await click('#add > button')
expect(await count('text')).toBe(6)
expect(await count('label')).toBe(6)
expect(await count('button')).toBe(7)
expect(await count('input[type="range"]')).toBe(6)
await assertPolygon(6)
await assertLabels(6)
await assertStats([100, 100, 100, 100, 100, 100])
await setValue(nthRange(1), '10')
await assertPolygon(6)
await assertLabels(6)
await assertStats([10, 100, 100, 100, 100, 100])
await setValue(nthRange(2), '20')
await assertPolygon(6)
await assertLabels(6)
await assertStats([10, 20, 100, 100, 100, 100])
await setValue(nthRange(6), '60')
await assertPolygon(6)
await assertLabels(6)
await assertStats([10, 20, 100, 100, 100, 60])
await click('button.remove')
await assertPolygon(5)
await assertLabels(5)
await assertStats([20, 100, 100, 100, 60])
await setValue(nthRange(1), '10')
await assertPolygon(5)
await assertLabels(5)
await assertStats([10, 100, 100, 100, 60])
}
test(
'classic',
async () => {
await testSvg('classic')
},
E2E_TIMEOUT,
)
test(
'composition',
async () => {
await testSvg('composition')
},
E2E_TIMEOUT,
)
})
================================================
FILE: packages/vue/__tests__/e2e/todomvc.spec.ts
================================================
import path from 'node:path'
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
describe('e2e: todomvc', () => {
const {
page,
click,
isVisible,
count,
text,
value,
isChecked,
isFocused,
classList,
enterValue,
clearValue,
timeout,
} = setupPuppeteer()
async function removeItemAt(n: number) {
const item = (await page().$('.todo:nth-child(' + n + ')'))!
const itemBBox = (await item.boundingBox())!
await page().mouse.move(itemBBox.x + 10, itemBBox.y + 10)
await click('.todo:nth-child(' + n + ') .destroy')
}
async function testTodomvc(apiType: 'classic' | 'composition') {
const baseUrl = `file://${path.resolve(
__dirname,
`../../examples/${apiType}/todomvc.html`,
)}`
await page().goto(baseUrl)
expect(await isVisible('.main')).toBe(false)
expect(await isVisible('.footer')).toBe(false)
expect(await count('.filters .selected')).toBe(1)
expect(await text('.filters .selected')).toBe('All')
expect(await count('.todo')).toBe(0)
await enterValue('.new-todo', 'test')
expect(await count('.todo')).toBe(1)
expect(await isVisible('.todo .edit')).toBe(false)
expect(await text('.todo label')).toBe('test')
expect(await text('.todo-count strong')).toBe('1')
expect(await isChecked('.todo .toggle')).toBe(false)
expect(await isVisible('.main')).toBe(true)
expect(await isVisible('.footer')).toBe(true)
expect(await isVisible('.clear-completed')).toBe(false)
expect(await value('.new-todo')).toBe('')
await enterValue('.new-todo', 'test2')
expect(await count('.todo')).toBe(2)
expect(await text('.todo:nth-child(2) label')).toBe('test2')
expect(await text('.todo-count strong')).toBe('2')
// toggle
await click('.todo .toggle')
expect(await count('.todo.completed')).toBe(1)
expect(await classList('.todo:nth-child(1)')).toContain('completed')
expect(await text('.todo-count strong')).toBe('1')
expect(await isVisible('.clear-completed')).toBe(true)
await enterValue('.new-todo', 'test3')
expect(await count('.todo')).toBe(3)
expect(await text('.todo:nth-child(3) label')).toBe('test3')
expect(await text('.todo-count strong')).toBe('2')
await enterValue('.new-todo', 'test4')
await enterValue('.new-todo', 'test5')
expect(await count('.todo')).toBe(5)
expect(await text('.todo-count strong')).toBe('4')
// toggle more
await click('.todo:nth-child(4) .toggle')
await click('.todo:nth-child(5) .toggle')
expect(await count('.todo.completed')).toBe(3)
expect(await text('.todo-count strong')).toBe('2')
// remove
await removeItemAt(1)
expect(await count('.todo')).toBe(4)
expect(await count('.todo.completed')).toBe(2)
expect(await text('.todo-count strong')).toBe('2')
await removeItemAt(2)
expect(await count('.todo')).toBe(3)
expect(await count('.todo.completed')).toBe(2)
expect(await text('.todo-count strong')).toBe('1')
// remove all
await click('.clear-completed')
expect(await count('.todo')).toBe(1)
expect(await text('.todo label')).toBe('test2')
expect(await count('.todo.completed')).toBe(0)
expect(await text('.todo-count strong')).toBe('1')
expect(await isVisible('.clear-completed')).toBe(false)
// prepare to test filters
await enterValue('.new-todo', 'test')
await enterValue('.new-todo', 'test')
await click('.todo:nth-child(2) .toggle')
await click('.todo:nth-child(3) .toggle')
// active filter
await click('.filters li:nth-child(2) a')
await timeout(1)
expect(await count('.todo')).toBe(1)
expect(await count('.todo.completed')).toBe(0)
// add item with filter active
await enterValue('.new-todo', 'test')
expect(await count('.todo')).toBe(2)
// completed filter
await click('.filters li:nth-child(3) a')
await timeout(1)
expect(await count('.todo')).toBe(2)
expect(await count('.todo.completed')).toBe(2)
// filter on page load
await page().goto(`${baseUrl}#active`)
expect(await count('.todo')).toBe(2)
expect(await count('.todo.completed')).toBe(0)
expect(await text('.todo-count strong')).toBe('2')
// completed on page load
await page().goto(`${baseUrl}#completed`)
expect(await count('.todo')).toBe(2)
expect(await count('.todo.completed')).toBe(2)
expect(await text('.todo-count strong')).toBe('2')
// toggling with filter active
await click('.todo .toggle')
expect(await count('.todo')).toBe(1)
await click('.filters li:nth-child(2) a')
await timeout(1)
expect(await count('.todo')).toBe(3)
await click('.todo .toggle')
expect(await count('.todo')).toBe(2)
// editing triggered by blur
await click('.filters li:nth-child(1) a')
await timeout(1)
await click('.todo:nth-child(1) label', { count: 2 })
expect(await count('.todo.editing')).toBe(1)
expect(await isFocused('.todo:nth-child(1) .edit')).toBe(true)
await clearValue('.todo:nth-child(1) .edit')
await page().type('.todo:nth-child(1) .edit', 'edited!')
await click('.new-todo') // blur
expect(await count('.todo.editing')).toBe(0)
expect(await text('.todo:nth-child(1) label')).toBe('edited!')
// editing triggered by enter
await click('.todo label', { count: 2 })
await enterValue('.todo:nth-child(1) .edit', 'edited again!')
expect(await count('.todo.editing')).toBe(0)
expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
// cancel
await click('.todo label', { count: 2 })
await clearValue('.todo:nth-child(1) .edit')
await page().type('.todo:nth-child(1) .edit', 'edited!')
await page().keyboard.press('Escape')
expect(await count('.todo.editing')).toBe(0)
expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
// empty value should remove
await click('.todo label', { count: 2 })
await enterValue('.todo:nth-child(1) .edit', ' ')
expect(await count('.todo')).toBe(3)
// toggle all
await click('.toggle-all+label')
expect(await count('.todo.completed')).toBe(3)
await click('.toggle-all+label')
expect(await count('.todo:not(.completed)')).toBe(3)
}
test(
'classic',
async () => {
await testTodomvc('classic')
},
E2E_TIMEOUT,
)
test(
'composition',
async () => {
await testTodomvc('composition')
},
E2E_TIMEOUT,
)
})
================================================
FILE: packages/vue/__tests__/e2e/transition.html
================================================
================================================
FILE: packages/vue/__tests__/e2e/tree.spec.ts
================================================
import path from 'node:path'
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
describe('e2e: tree', () => {
const { page, click, count, text, children, isVisible } = setupPuppeteer()
async function testTree(apiType: 'classic' | 'composition') {
const baseUrl = `file://${path.resolve(
__dirname,
`../../examples/${apiType}/tree.html`,
)}`
await page().goto(baseUrl)
expect(await count('.item')).toBe(12)
expect(await count('.add')).toBe(4)
expect(await count('.item > ul')).toBe(4)
expect(await isVisible('#demo li ul')).toBe(false)
expect(await text('#demo li div span')).toBe('[+]')
// expand root
await click('.bold')
expect(await isVisible('#demo ul')).toBe(true)
expect((await children('#demo li ul')).length).toBe(4)
expect(await text('#demo li div span')).toContain('[-]')
expect(await text('#demo > .item > ul > .item:nth-child(1)')).toContain(
'hello',
)
expect(await text('#demo > .item > ul > .item:nth-child(2)')).toContain(
'wat',
)
expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
'child folder',
)
expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
'[+]',
)
// add items to root
await click('#demo > .item > ul > .add')
expect((await children('#demo li ul')).length).toBe(5)
expect(await text('#demo > .item > ul > .item:nth-child(1)')).toContain(
'hello',
)
expect(await text('#demo > .item > ul > .item:nth-child(2)')).toContain(
'wat',
)
expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
'child folder',
)
expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
'[+]',
)
expect(await text('#demo > .item > ul > .item:nth-child(4)')).toContain(
'new stuff',
)
// add another item
await click('#demo > .item > ul > .add')
expect((await children('#demo li ul')).length).toBe(6)
expect(await text('#demo > .item > ul > .item:nth-child(1)')).toContain(
'hello',
)
expect(await text('#demo > .item > ul > .item:nth-child(2)')).toContain(
'wat',
)
expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
'child folder',
)
expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
'[+]',
)
expect(await text('#demo > .item > ul > .item:nth-child(4)')).toContain(
'new stuff',
)
expect(await text('#demo > .item > ul > .item:nth-child(5)')).toContain(
'new stuff',
)
await click('#demo ul .bold')
expect(await isVisible('#demo ul ul')).toBe(true)
expect(await text('#demo ul > .item:nth-child(3)')).toContain('[-]')
expect((await children('#demo ul ul')).length).toBe(5)
await click('.bold')
expect(await isVisible('#demo ul')).toBe(false)
expect(await text('#demo li div span')).toContain('[+]')
await click('.bold')
expect(await isVisible('#demo ul')).toBe(true)
expect(await text('#demo li div span')).toContain('[-]')
await click('#demo ul > .item div', { count: 2 })
expect(await count('.item')).toBe(15)
expect(await count('.item > ul')).toBe(5)
expect(await text('#demo ul > .item:nth-child(1)')).toContain('[-]')
expect((await children('#demo ul > .item:nth-child(1) > ul')).length).toBe(
2,
)
}
test(
'classic',
async () => {
await testTree('classic')
},
E2E_TIMEOUT,
)
test(
'composition',
async () => {
await testTree('composition')
},
E2E_TIMEOUT,
)
})
================================================
FILE: packages/vue/__tests__/e2e/trusted-types.html
================================================
Vue App
================================================
FILE: packages/vue/__tests__/e2e/trusted-types.spec.ts
================================================
import { once } from 'node:events'
import { createServer } from 'node:http'
import path from 'node:path'
import { beforeAll } from 'vitest'
import serveHandler from 'serve-handler'
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
// use the `vue` package root as the public directory
// because we need to serve the Vue runtime for the tests
const serverRoot = path.resolve(import.meta.dirname, '../../')
const testPort = 9090
const basePath = path.relative(
serverRoot,
path.resolve(import.meta.dirname, './trusted-types.html'),
)
const baseUrl = `http://localhost:${testPort}/${basePath}`
const { page, html } = setupPuppeteer()
let server: ReturnType
beforeAll(async () => {
// sets up the static server
server = createServer((req, res) => {
return serveHandler(req, res, {
public: serverRoot,
cleanUrls: false,
})
})
server.listen(testPort)
await once(server, 'listening')
})
afterAll(async () => {
server.close()
await once(server, 'close')
})
describe('e2e: trusted types', () => {
beforeEach(async () => {
await page().goto(baseUrl)
await page().waitForSelector('#app')
})
test(
'should render the hello world app',
async () => {
await page().evaluate(() => {
const { createApp, ref, h } = (window as any).Vue
createApp({
setup() {
const msg = ref('✅success: hello world')
return function render() {
return h('div', msg.value)
}
},
}).mount('#app')
})
expect(await html('#app')).toContain('✅success: hello world
')
},
E2E_TIMEOUT,
)
test(
'should render static vnode without error',
async () => {
await page().evaluate(() => {
const { createApp, createStaticVNode } = (window as any).Vue
createApp({
render() {
return createStaticVNode('✅success: static vnode
')
},
}).mount('#app')
})
expect(await html('#app')).toContain('✅success: static vnode
')
},
E2E_TIMEOUT,
)
test(
'should accept v-html with custom policy',
async () => {
await page().evaluate(() => {
const testPolicy = (window as any).trustedTypes.createPolicy('test', {
createHTML: (input: string): string => input,
})
const { createApp, ref, h } = (window as any).Vue
createApp({
setup() {
const msg = ref('✅success: v-html')
return function render() {
return h('div', { innerHTML: testPolicy.createHTML(msg.value) })
}
},
}).mount('#app')
})
expect(await html('#app')).toContain('✅success: v-html
')
},
E2E_TIMEOUT,
)
})
================================================
FILE: packages/vue/__tests__/e2e/vModel.spec.ts
================================================
import path from 'node:path'
import { setupPuppeteer } from './e2eUtils'
const { page, click, isChecked } = setupPuppeteer()
import { nextTick } from 'vue'
beforeEach(async () => {
await page().addScriptTag({
path: path.resolve(__dirname, '../../dist/vue.global.js'),
})
await page().setContent(`
`)
})
// #12144
test('checkbox click with v-model', async () => {
await page().evaluate(() => {
const { createApp } = (window as any).Vue
createApp({
template: `
First
Second
`,
data() {
return {
first: true,
second: false,
}
},
methods: {
secondClick(this: any) {
this.first = false
},
},
}).mount('#app')
})
expect(await isChecked('#first')).toBe(true)
expect(await isChecked('#second')).toBe(false)
await click('#second')
await nextTick()
expect(await isChecked('#first')).toBe(false)
expect(await isChecked('#second')).toBe(true)
})
================================================
FILE: packages/vue/__tests__/index.spec.ts
================================================
import { EMPTY_ARR } from '@vue/shared'
import { createApp, nextTick, reactive, ref } from '../src'
describe('compiler + runtime integration', () => {
it('should support runtime template compilation', () => {
const container = document.createElement('div')
const App = {
template: `{{ count }}`,
data() {
return {
count: 0,
}
},
}
createApp(App).mount(container)
expect(container.innerHTML).toBe(`0`)
})
it('keep-alive with compiler + runtime integration', async () => {
const container = document.createElement('div')
const one = {
name: 'one',
template: 'one',
created: vi.fn(),
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
const toggle = ref(true)
const App = {
template: `
`,
data() {
return {
toggle,
}
},
components: {
One: one,
},
}
createApp(App).mount(container)
expect(container.innerHTML).toBe(`one`)
expect(one.created).toHaveBeenCalledTimes(1)
expect(one.mounted).toHaveBeenCalledTimes(1)
expect(one.activated).toHaveBeenCalledTimes(1)
expect(one.deactivated).toHaveBeenCalledTimes(0)
expect(one.unmounted).toHaveBeenCalledTimes(0)
toggle.value = false
await nextTick()
expect(container.innerHTML).toBe(``)
expect(one.created).toHaveBeenCalledTimes(1)
expect(one.mounted).toHaveBeenCalledTimes(1)
expect(one.activated).toHaveBeenCalledTimes(1)
expect(one.deactivated).toHaveBeenCalledTimes(1)
expect(one.unmounted).toHaveBeenCalledTimes(0)
toggle.value = true
await nextTick()
expect(container.innerHTML).toBe(`one`)
expect(one.created).toHaveBeenCalledTimes(1)
expect(one.mounted).toHaveBeenCalledTimes(1)
expect(one.activated).toHaveBeenCalledTimes(2)
expect(one.deactivated).toHaveBeenCalledTimes(1)
expect(one.unmounted).toHaveBeenCalledTimes(0)
})
it('should support runtime template via CSS ID selector', () => {
const container = document.createElement('div')
const template = document.createElement('div')
template.id = 'template'
template.innerHTML = '{{ count }}'
document.body.appendChild(template)
const App = {
template: `#template`,
data() {
return {
count: 0,
}
},
}
createApp(App).mount(container)
expect(container.innerHTML).toBe(`0`)
})
it('should support runtime template via direct DOM node', () => {
const container = document.createElement('div')
const template = document.createElement('div')
template.id = 'template'
template.innerHTML = '{{ count }}'
const App = {
template,
data() {
return {
count: 0,
}
},
}
createApp(App).mount(container)
expect(container.innerHTML).toBe(`0`)
})
it('should warn template compilation errors with codeframe', () => {
const container = document.createElement('div')
const App = {
template: ``,
}
createApp(App).mount(container)
expect(
`Template compilation error: Element is missing end tag`,
).toHaveBeenWarned()
expect(
`
1 |
| ^`.trim(),
).toHaveBeenWarned()
expect(`v-if/v-else-if is missing expression`).toHaveBeenWarned()
expect(
`
1 |
| ^^^^`.trim(),
).toHaveBeenWarned()
})
it('should support custom element via config.isCustomElement (deprecated)', () => {
const app = createApp({
template: '
',
})
const container = document.createElement('div')
app.config.isCustomElement = tag => tag === 'custom'
app.mount(container)
expect(container.innerHTML).toBe('
')
})
it('should support custom element via config.compilerOptions.isCustomElement', () => {
const app = createApp({
template: '
',
})
const container = document.createElement('div')
app.config.compilerOptions.isCustomElement = tag => tag === 'custom'
app.mount(container)
expect(container.innerHTML).toBe('
')
})
it('should support using element innerHTML as template', () => {
const app = createApp({
data: () => ({
msg: 'hello',
}),
})
const container = document.createElement('div')
container.innerHTML = '{{msg}}'
app.mount(container)
expect(container.innerHTML).toBe('hello')
})
it('should support selector of rootContainer', () => {
const container = document.createElement('div')
const origin = document.querySelector
document.querySelector = vi.fn().mockReturnValue(container)
const App = {
template: `{{ count }}`,
data() {
return {
count: 0,
}
},
}
createApp(App).mount('#app')
expect(container.innerHTML).toBe(`0`)
document.querySelector = origin
})
it('should warn when template is not available', () => {
const app = createApp({
template: {},
})
const container = document.createElement('div')
app.mount(container)
expect('[Vue warn]: invalid template option:').toHaveBeenWarned()
})
it('should warn when template is is not found', () => {
const app = createApp({
template: '#not-exist-id',
})
const container = document.createElement('div')
app.mount(container)
expect(
'[Vue warn]: Template element not found or is empty: #not-exist-id',
).toHaveBeenWarned()
})
it('should warn when container is not found', () => {
const origin = document.querySelector
document.querySelector = vi.fn().mockReturnValue(null)
const App = {
template: `{{ count }}`,
data() {
return {
count: 0,
}
},
}
createApp(App).mount('#not-exist-id')
expect(
'[Vue warn]: Failed to mount app: mount target selector "#not-exist-id" returned null.',
).toHaveBeenWarned()
document.querySelector = origin
})
// #1813
it('should not report an error when "0" as patchFlag value', async () => {
const container = document.createElement('div')
const target = document.createElement('div')
const count = ref(0)
const origin = document.querySelector
document.querySelector = vi.fn().mockReturnValue(target)
const App = {
template: `
`,
data() {
return {
count,
}
},
}
createApp(App).mount(container)
expect(container.innerHTML).toBe(``)
expect(target.innerHTML).toBe(`
`)
count.value++
await nextTick()
expect(container.innerHTML).toBe(``)
expect(target.innerHTML).toBe(`
`)
count.value++
await nextTick()
expect(container.innerHTML).toBe(``)
expect(target.innerHTML).toBe(``)
document.querySelector = origin
})
test('v-if + v-once', async () => {
const ok = ref(true)
const App = {
setup() {
return { ok }
},
template: `
`,
}
const container = document.createElement('div')
createApp(App).mount(container)
expect(container.innerHTML).toBe(`
`)
ok.value = false
await nextTick()
expect(container.innerHTML).toBe(`
`)
})
test('v-for + v-once', async () => {
const list = reactive([1])
const App = {
setup() {
return { list }
},
template: `
`,
}
const container = document.createElement('div')
createApp(App).mount(container)
expect(container.innerHTML).toBe(`
`)
list.push(2)
await nextTick()
expect(container.innerHTML).toBe(`
`)
})
// #2413
it('EMPTY_ARR should not change', () => {
const App = {
template: `
{{ v }}
`,
}
const container = document.createElement('div')
createApp(App).mount(container)
expect(EMPTY_ARR.length).toBe(0)
})
test('BigInt support', () => {
const app = createApp({
template: `
{{ BigInt(BigInt(100000111)) + BigInt(2000000000n) * 30000000n }}
`,
})
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('
60000000100000111
')
})
})
================================================
FILE: packages/vue/__tests__/mathmlNamespace.spec.ts
================================================
// MathML logic is technically dom-specific, but the logic is placed in core
// because splitting it out of core would lead to unnecessary complexity in both
// the renderer and compiler implementations.
// Related files:
// - runtime-core/src/renderer.ts
// - compiler-core/src/transforms/transformElement.ts
import { vtcKey } from '../../runtime-dom/src/components/Transition'
import { h, nextTick, ref, render } from '../src'
describe('MathML support', () => {
afterEach(() => {
document.body.innerHTML = ''
})
test('should mount elements with correct html namespace', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const App = {
template: `
x
2
+
y
`,
}
render(h(App), root)
const e0 = document.getElementById('e0')!
expect(e0.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e4')!.namespaceURI).toMatch('xhtml')
expect(e0.querySelector('#e5')!.namespaceURI).toMatch('svg')
})
test('should patch elements with correct namespaces', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const cls = ref('foo')
const App = {
setup: () => ({ cls }),
template: `
`,
}
render(h(App), root)
const f1 = document.querySelector('#f1')!
const f2 = document.querySelector('#f2')!
expect(f1.getAttribute('class')).toBe('foo')
expect(f2.className).toBe('foo')
// set a transition class on the
- which is only respected on non-svg
// patches
;(f2 as any)[vtcKey] = ['baz']
cls.value = 'bar'
await nextTick()
expect(f1.getAttribute('class')).toBe('bar')
expect(f2.className).toBe('bar baz')
})
})
================================================
FILE: packages/vue/__tests__/runtimeCompilerOptions.spec.ts
================================================
import { createApp } from 'vue'
describe('config.compilerOptions', () => {
test('isCustomElement', () => {
const app = createApp({
template: `
`,
})
app.config.compilerOptions.isCustomElement = (tag: string) => tag === 'foo'
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('
')
})
test('comments', () => {
const app = createApp({
template: `
`,
})
app.config.compilerOptions.comments = true
// the comments option is only relevant in production mode
__DEV__ = false
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('
')
__DEV__ = true
})
test('whitespace', () => {
const app = createApp({
template: `
\n
`,
})
app.config.compilerOptions.whitespace = 'preserve'
const root = document.createElement('div')
app.mount(root)
expect(root.firstChild!.childNodes.length).toBe(3)
expect(root.firstChild!.childNodes[1].nodeType).toBe(Node.TEXT_NODE)
})
test('delimiters', () => {
const app = createApp({
data: () => ({ foo: 'hi' }),
template: `[[ foo ]]`,
})
app.config.compilerOptions.delimiters = [`[[`, `]]`]
const root = document.createElement('div')
app.mount(root)
expect(root.textContent).toBe('hi')
})
})
describe('per-component compilerOptions', () => {
test('isCustomElement', () => {
const app = createApp({
template: `
`,
compilerOptions: {
isCustomElement: (tag: string) => tag === 'foo',
},
})
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('
')
})
test('comments', () => {
const app = createApp({
template: `
`,
compilerOptions: {
comments: true,
},
})
app.config.compilerOptions.comments = false
// the comments option is only relevant in production mode
__DEV__ = false
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('
')
__DEV__ = true
})
test('whitespace', () => {
const app = createApp({
template: `
\n
`,
compilerOptions: {
whitespace: 'preserve',
},
})
const root = document.createElement('div')
app.mount(root)
expect(root.firstChild!.childNodes.length).toBe(3)
expect(root.firstChild!.childNodes[1].nodeType).toBe(Node.TEXT_NODE)
})
test('delimiters', () => {
const app = createApp({
data: () => ({ foo: 'hi' }),
template: `[[ foo ]]`,
compilerOptions: {
delimiters: [`[[`, `]]`],
},
})
const root = document.createElement('div')
app.mount(root)
expect(root.textContent).toBe('hi')
})
})
================================================
FILE: packages/vue/__tests__/svgNamespace.spec.ts
================================================
// SVG logic is technically dom-specific, but the logic is placed in core
// because splitting it out of core would lead to unnecessary complexity in both
// the renderer and compiler implementations.
// Related files:
// - runtime-core/src/renderer.ts
// - compiler-core/src/transforms/transformElement.ts
import { vtcKey } from '../../runtime-dom/src/components/Transition'
import { h, nextTick, ref, render } from '../src'
describe('SVG support', () => {
afterEach(() => {
document.body.innerHTML = ''
})
test('should mount elements with correct html namespace', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const App = {
template: `
`,
}
render(h(App), root)
const e0 = document.getElementById('e0')!
expect(e0.namespaceURI).toMatch('xhtml')
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('xhtml')
expect(e0.querySelector('#e4')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e5')!.namespaceURI).toMatch('Math')
})
test('should patch elements with correct namespaces', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const cls = ref('foo')
const App = {
setup: () => ({ cls }),
template: `
`,
}
render(h(App), root)
const f1 = document.querySelector('#f1')!
const f2 = document.querySelector('#f2')!
expect(f1.getAttribute('class')).toBe('foo')
expect(f2.className).toBe('foo')
// set a transition class on the
- which is only respected on non-svg
// patches
;(f2 as any)[vtcKey] = ['baz']
cls.value = 'bar'
await nextTick()
expect(f1.getAttribute('class')).toBe('bar')
expect(f2.className).toBe('bar baz')
})
})
================================================
FILE: packages/vue/compiler-sfc/index.browser.js
================================================
module.exports = require('@vue/compiler-sfc')
================================================
FILE: packages/vue/compiler-sfc/index.browser.mjs
================================================
export * from '@vue/compiler-sfc'
================================================
FILE: packages/vue/compiler-sfc/index.d.mts
================================================
export * from '@vue/compiler-sfc'
================================================
FILE: packages/vue/compiler-sfc/index.d.ts
================================================
export * from '@vue/compiler-sfc'
================================================
FILE: packages/vue/compiler-sfc/index.js
================================================
module.exports = require('@vue/compiler-sfc')
require('./register-ts.js')
================================================
FILE: packages/vue/compiler-sfc/index.mjs
================================================
export * from '@vue/compiler-sfc'
import './register-ts.js'
================================================
FILE: packages/vue/compiler-sfc/package.json
================================================
{
"main": "index.js",
"module": "index.mjs"
}
================================================
FILE: packages/vue/compiler-sfc/register-ts.js
================================================
if (typeof require !== 'undefined') {
require('@vue/compiler-sfc').registerTS(() => require('typescript'))
}
================================================
FILE: packages/vue/examples/classic/commits.html
================================================
================================================
FILE: packages/vue/examples/classic/grid.html
================================================
================================================
FILE: packages/vue/examples/classic/markdown.html
================================================
================================================
FILE: packages/vue/examples/classic/svg.html
================================================
================================================
FILE: packages/vue/examples/classic/todomvc.html
================================================
================================================
FILE: packages/vue/examples/classic/tree.html
================================================
(You can double click on an item to turn it into a folder.)
================================================
FILE: packages/vue/examples/composition/commits.html
================================================
================================================
FILE: packages/vue/examples/composition/grid.html
================================================
================================================
FILE: packages/vue/examples/composition/markdown.html
================================================
================================================
FILE: packages/vue/examples/composition/svg.html
================================================
================================================
FILE: packages/vue/examples/composition/todomvc.html
================================================
================================================
FILE: packages/vue/examples/composition/tree.html
================================================
(You can double click on an item to turn it into a folder.)
================================================
FILE: packages/vue/examples/transition/list.html
================================================
insert at random index
reset
shuffle
-
================================================
FILE: packages/vue/examples/transition/modal.html
================================================
Show Modal
custom header
================================================
FILE: packages/vue/index.js
================================================
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/vue.cjs.prod.js')
} else {
module.exports = require('./dist/vue.cjs.js')
}
================================================
FILE: packages/vue/index.mjs
================================================
export * from './index.js'
================================================
FILE: packages/vue/jsx-runtime/index.d.ts
================================================
/* eslint-disable @typescript-eslint/ban-ts-comment */
import type { NativeElements, ReservedProps, VNode } from '@vue/runtime-dom'
/**
* JSX namespace for usage with @jsxImportsSource directive
* when ts compilerOptions.jsx is 'react-jsx' or 'react-jsxdev'
* https://www.typescriptlang.org/tsconfig#jsxImportSource
*/
export { h as jsx, h as jsxDEV, Fragment, h as jsxs } from '@vue/runtime-dom'
export namespace JSX {
export interface Element extends VNode {}
export interface ElementClass {
$props: {}
}
export interface ElementAttributesProperty {
$props: {}
}
export interface IntrinsicElements extends NativeElements {
// allow arbitrary elements
// @ts-ignore suppress ts:2374 = Duplicate string index signature.
[name: string]: any
}
export interface IntrinsicAttributes extends ReservedProps {}
}
================================================
FILE: packages/vue/jsx-runtime/index.js
================================================
const { h, Fragment } = require('vue')
function jsx(type, props, key) {
const { children } = props
delete props.children
if (arguments.length > 2) {
props.key = key
}
return h(type, props, children)
}
exports.jsx = jsx
exports.jsxs = jsx
exports.jsxDEV = jsx
exports.Fragment = Fragment
================================================
FILE: packages/vue/jsx-runtime/index.mjs
================================================
import { h, Fragment } from 'vue'
function jsx(type, props, key) {
const { children } = props
delete props.children
if (arguments.length > 2) {
props.key = key
}
return h(type, props, children)
}
export { Fragment, jsx, jsx as jsxs, jsx as jsxDEV }
================================================
FILE: packages/vue/jsx-runtime/package.json
================================================
{
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts"
}
================================================
FILE: packages/vue/jsx.d.ts
================================================
/* eslint-disable @typescript-eslint/ban-ts-comment */
// global JSX namespace registration
// somehow we have to copy=pase the jsx-runtime types here to make TypeScript happy
import type { NativeElements, ReservedProps, VNode } from '@vue/runtime-dom'
declare global {
namespace JSX {
export interface Element extends VNode {}
export interface ElementClass {
$props: {}
}
export interface ElementAttributesProperty {
$props: {}
}
export interface IntrinsicElements extends NativeElements {
// allow arbitrary elements
// @ts-ignore suppress ts:2374 = Duplicate string index signature.
[name: string]: any
}
export interface IntrinsicAttributes extends ReservedProps {}
}
}
================================================
FILE: packages/vue/package.json
================================================
{
"name": "vue",
"version": "3.5.30",
"description": "The progressive JavaScript framework for building modern web UI.",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
"types": "dist/vue.d.ts",
"unpkg": "dist/vue.global.js",
"jsdelivr": "dist/vue.global.js",
"files": [
"index.js",
"index.mjs",
"dist",
"compiler-sfc",
"server-renderer",
"jsx-runtime",
"jsx.d.ts"
],
"exports": {
".": {
"import": {
"types": "./dist/vue.d.mts",
"node": "./index.mjs",
"default": "./dist/vue.runtime.esm-bundler.js"
},
"require": {
"types": "./dist/vue.d.ts",
"node": {
"production": "./dist/vue.cjs.prod.js",
"development": "./dist/vue.cjs.js",
"default": "./index.js"
},
"default": "./index.js"
}
},
"./server-renderer": {
"import": {
"types": "./server-renderer/index.d.mts",
"default": "./server-renderer/index.mjs"
},
"require": {
"types": "./server-renderer/index.d.ts",
"default": "./server-renderer/index.js"
}
},
"./compiler-sfc": {
"import": {
"types": "./compiler-sfc/index.d.mts",
"browser": "./compiler-sfc/index.browser.mjs",
"default": "./compiler-sfc/index.mjs"
},
"require": {
"types": "./compiler-sfc/index.d.ts",
"browser": "./compiler-sfc/index.browser.js",
"default": "./compiler-sfc/index.js"
}
},
"./jsx-runtime": {
"types": "./jsx-runtime/index.d.ts",
"import": "./jsx-runtime/index.mjs",
"require": "./jsx-runtime/index.js"
},
"./jsx-dev-runtime": {
"types": "./jsx-runtime/index.d.ts",
"import": "./jsx-runtime/index.mjs",
"require": "./jsx-runtime/index.js"
},
"./jsx": "./jsx.d.ts",
"./dist/*": "./dist/*",
"./package.json": "./package.json"
},
"buildOptions": {
"name": "Vue",
"formats": [
"esm-bundler",
"esm-bundler-runtime",
"cjs",
"global",
"global-runtime",
"esm-browser",
"esm-browser-runtime"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/core.git"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/core/issues"
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme",
"dependencies": {
"@vue/shared": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/runtime-dom": "workspace:*",
"@vue/compiler-sfc": "workspace:*",
"@vue/server-renderer": "workspace:*"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}
================================================
FILE: packages/vue/server-renderer/index.d.mts
================================================
export * from '@vue/server-renderer'
================================================
FILE: packages/vue/server-renderer/index.d.ts
================================================
export * from '@vue/server-renderer'
================================================
FILE: packages/vue/server-renderer/index.js
================================================
module.exports = require('@vue/server-renderer')
================================================
FILE: packages/vue/server-renderer/index.mjs
================================================
export * from '@vue/server-renderer'
================================================
FILE: packages/vue/server-renderer/package.json
================================================
{
"main": "index.js",
"module": "index.mjs"
}
================================================
FILE: packages/vue/src/dev.ts
================================================
import { initCustomFormatter } from '@vue/runtime-dom'
export function initDev(): void {
if (__BROWSER__) {
if (!__ESM_BUNDLER__) {
console.info(
`You are running a development build of Vue.\n` +
`Make sure to use the production build (*.prod.js) when deploying for production.`,
)
}
initCustomFormatter()
}
}
================================================
FILE: packages/vue/src/index.ts
================================================
// This entry is the "full-build" that includes both the runtime
// and the compiler, and supports on-the-fly compilation of the template option.
import { initDev } from './dev'
import {
type CompilerError,
type CompilerOptions,
compile,
} from '@vue/compiler-dom'
import {
type RenderFunction,
registerRuntimeCompiler,
warn,
} from '@vue/runtime-dom'
import * as runtimeDom from '@vue/runtime-dom'
import {
NOOP,
extend,
genCacheKey,
generateCodeFrame,
isString,
} from '@vue/shared'
import type { InternalRenderFunction } from 'packages/runtime-core/src/component'
if (__DEV__) {
initDev()
}
const compileCache: Record
= Object.create(null)
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions,
): RenderFunction {
if (!isString(template)) {
if (template.nodeType) {
template = template.innerHTML
} else {
__DEV__ && warn(`invalid template option: `, template)
return NOOP
}
}
const key = genCacheKey(template, options)
const cached = compileCache[key]
if (cached) {
return cached
}
if (template[0] === '#') {
const el = document.querySelector(template)
if (__DEV__ && !el) {
warn(`Template element not found or is empty: ${template}`)
}
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's rendered
// by the server, the template should not contain any user data.
template = el ? el.innerHTML : ``
}
const opts = extend(
{
hoistStatic: true,
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP,
} as CompilerOptions,
options,
)
if (!opts.isCustomElement && typeof customElements !== 'undefined') {
opts.isCustomElement = tag => !!customElements.get(tag)
}
const { code } = compile(template, opts)
function onError(err: CompilerError, asWarning = false) {
const message = asWarning
? err.message
: `Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset,
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
}
// The wildcard import results in a huge object with every export
// with keys that cannot be mangled, and can be quite heavy size-wise.
// In the global build we know `Vue` is available globally so we can avoid
// the wildcard object.
const render = (
__GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
) as RenderFunction
// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true
return (compileCache[key] = render)
}
registerRuntimeCompiler(compileToFunction)
export { compileToFunction as compile }
export * from '@vue/runtime-dom'
================================================
FILE: packages/vue/src/runtime.ts
================================================
// This entry exports the runtime only, and is built as
// `dist/vue.esm-bundler.js` which is used by default for bundlers.
import { initDev } from './dev'
import { warn } from '@vue/runtime-dom'
if (__DEV__) {
initDev()
}
export * from '@vue/runtime-dom'
export const compile = (): void => {
if (__DEV__) {
warn(
`Runtime compilation is not supported in this build of Vue.` +
(__ESM_BUNDLER__
? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
: __ESM_BROWSER__
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */,
)
}
}
================================================
FILE: packages/vue-compat/LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: packages/vue-compat/README.md
================================================
## Overview
`@vue/compat` (aka "the migration build") is a build of Vue 3 that provides configurable Vue 2 compatible behavior.
The migration build runs in Vue 2 mode by default - most public APIs behave exactly like Vue 2, with only a few exceptions. Usage of features that have changed or been deprecated in Vue 3 will emit runtime warnings. A feature's compatibility can also be enabled/disabled on a per-component basis.
### Intended Use Cases
- Upgrading a Vue 2 application to Vue 3 (with [limitations](#known-limitations))
- Migrating a library to support Vue 3
- For experienced Vue 2 developers who have not tried Vue 3 yet, the migration build can be used in place of Vue 3 to help learn the difference between versions.
### Known Limitations
While we've tried hard to make the migration build mimic Vue 2 behavior as much as possible, there are some limitations that may prevent your app from being eligible for upgrading:
- Dependencies that rely on Vue 2 internal APIs or undocumented behavior. The most common case is usage of private properties on `VNodes`. If your project relies on component libraries like [Vuetify](https://vuetifyjs.com/en/), [Quasar](https://quasar.dev/) or [ElementUI](https://element.eleme.io/#/en-US), it is best to wait for their Vue 3 compatible versions.
- Internet Explorer 11 support: [Vue 3 has officially dropped the plan for IE11 support](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0038-vue3-ie11-support.md). If you still need to support IE11 or below, you will have to stay on Vue 2.
- Server-side rendering: the migration build can be used for SSR, but migrating a custom SSR setup is much more involved. The general idea is replacing `vue-server-renderer` with [`@vue/server-renderer`](https://github.com/vuejs/core/tree/main/packages/server-renderer). Vue 3 no longer provides a bundle renderer and it is recommended to use Vue 3 SSR with [Vite](https://vitejs.dev/guide/ssr.html). If you are using [Nuxt.js](https://nuxtjs.org/), it is probably better to wait for Nuxt 3.
### Expectations
Please note that the migration build aims to cover only publicly documented Vue 2 APIs and behavior. If your application fails to run under the migration build due to reliance on undocumented behavior, it is unlikely that we'll tweak the migration build to cater to your specific case. Consider refactoring to remove reliance on the behavior in question instead.
A word of caution: if your application is large and complex, migration will likely be a challenge even with the migration build. If your app is unfortunately not suitable for upgrade, do note that we are planning to backport Composition API and some other Vue 3 features to the 2.7 release (estimated late Q3 2021).
If you do get your app running on the migration build, you **can** ship it to production before the migration is complete. Although there is a small performance/size overhead, it should not noticeably affect production UX. You may have to do so when you have dependencies that rely on Vue 2 behavior, and cannot be upgraded/replaced.
The migration build will be provided starting with 3.1, and will continue to be published alongside the 3.2 release line. We do plan to eventually stop publishing the migration build in a future minor version (no earlier than EOY 2021), so you should still aim to switch to the standard build before then.
## Upgrade Workflow
The following workflow walks through the steps of migrating an actual Vue 2 app (Vue HackerNews 2.0) to Vue 3. The full commits can be found [here](https://github.com/vuejs/vue-hackernews-2.0/compare/migration). Note that the actual steps required for your project may vary, and these steps should be treated as general guidance rather than strict instructions.
### Preparations
- If you are still using the [deprecated named / scoped slot syntax](https://vuejs.org/v2/guide/components-slots.html#Deprecated-Syntax), update it to the latest syntax first (which is already supported in 2.6).
### Installation
1. Upgrade tooling if applicable.
- If using custom webpack setup: Upgrade `vue-loader` to `^16.0.0`.
- If using `vue-cli`: upgrade to the latest `@vue/cli-service` with `vue upgrade`
- (Alternative) migrate to [Vite](https://vitejs.dev/) + [vite-plugin-vue2](https://github.com/underfin/vite-plugin-vue2). [[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/565b948919eb58f22a32afca7e321b490cb3b074)]
2. In `package.json`, update `vue` to 3.1, install `@vue/compat` of the same version, and replace `vue-template-compiler` (if present) with `@vue/compiler-sfc`:
```diff
"dependencies": {
- "vue": "^2.6.12",
+ "vue": "^3.1.0",
+ "@vue/compat": "^3.1.0"
...
},
"devDependencies": {
- "vue-template-compiler": "^2.6.12"
+ "@vue/compiler-sfc": "^3.1.0"
}
```
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/14f6f1879b43f8610add60342661bf915f5c4b20)
3. In the build setup, alias `vue` to `@vue/compat` and enable compat mode via Vue compiler options.
**Example Configs**
vue-cli
```js
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
}
})
},
}
```
Plain webpack
```js
// webpack.config.js
module.exports = {
resolve: {
alias: {
vue: '@vue/compat',
},
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
},
},
],
},
}
```
Vite
```js
// vite.config.js
export default {
resolve: {
alias: {
vue: '@vue/compat',
},
},
plugins: [
vue({
template: {
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
},
}),
],
}
```
4. At this point, your application may encounter some compile-time errors / warnings (e.g. use of filters). Fix them first. If all compiler warnings are gone, you can also set the compiler to Vue 3 mode.
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/b05d9555f6e115dea7016d7e5a1a80e8f825be52)
5. After fixing the errors, the app should be able to run if it is not subject to the [limitations](#known-limitations) mentioned above.
You will likely see a LOT of warnings from both the command line and the browser console. Here are some general tips:
- You can filter for specific warnings in the browser console. It's a good idea to use the filter and focus on fixing one item at a time. You can also use negated filters like `-GLOBAL_MOUNT`.
- You can suppress specific deprecations via [compat configuration](#compat-configuration).
- Some warnings may be caused by a dependency that you use (e.g. `vue-router`). You can check this from the warning's component trace or stack trace (expanded on click). Focus on fixing the warnings that originate from your own source code first.
- If you are using `vue-router`, note `` and `` will not work with `` until you upgrade to `vue-router` v4.
6. Update [`` class names](https://v3-migration.vuejs.org/breaking-changes/transition.html). This is the only feature that does not have a runtime warning. You can do a project-wide search for `.*-enter` and `.*-leave` CSS class names.
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/d300103ba622ae26ac26a82cd688e0f70b6c1d8f)
7. Update app entry to use [new global mounting API](https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp).
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/a6e0c9ac7b1f4131908a4b1e43641f608593f714)
8. [Upgrade `vuex` to v4](https://next.vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html).
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/5bfd4c61ee50f358cd5daebaa584f2c3f91e0205)
9. [Upgrade `vue-router` to v4](https://next.router.vuejs.org/guide/migration/index.html). If you also use `vuex-router-sync`, you can replace it with a store getter.
After the upgrade, to use `` and `` with `` requires using the new [scoped-slot based syntax](https://next.router.vuejs.org/guide/migration/index.html#router-view-keep-alive-and-transition).
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/758961e73ac4089890079d4ce14996741cf9344b)
10. Pick off individual warnings. Note some features have conflicting behavior between Vue 2 and Vue 3 - for example, the render function API, or the functional component vs. async component change. To migrate to Vue 3 API without affecting the rest of the application, you can opt-in to Vue 3 behavior on a per-component basis with the [`compatConfig` option](#per-component-config).
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/d0c7d3ae789be71b8fd56ce79cb4cb1f921f893b)
11. When all warnings are fixed, you can remove the migration build and switch to Vue 3 proper. Note you may not be able to do so if you still have dependencies that rely on Vue 2 behavior.
[Example commit](https://github.com/vuejs/vue-hackernews-2.0/commit/9beb45490bc5f938c9e87b4ac1357cfb799565bd)
## Compat Configuration
### Global Config
Compat features can be disabled individually:
```js
import { configureCompat } from 'vue'
// disable compat for certain features
configureCompat({
FEATURE_ID_A: false,
FEATURE_ID_B: false,
})
```
Alternatively, the entire application can default to Vue 3 behavior, with only certain compat features enabled:
```js
import { configureCompat } from 'vue'
// default everything to Vue 3 behavior, and only enable compat
// for certain features
configureCompat({
MODE: 3,
FEATURE_ID_A: true,
FEATURE_ID_B: true,
})
```
### Per-Component Config
A component can use the `compatConfig` option, which expects the same options as the global `configureCompat` method:
```js
export default {
compatConfig: {
MODE: 3, // opt-in to Vue 3 behavior for this component only
FEATURE_ID_A: true, // features can also be toggled at component level
},
// ...
}
```
### Compiler-specific Config
Features that start with `COMPILER_` are compiler-specific: if you are using the full build (with in-browser compiler), they can be configured at runtime. However if using a build setup, they must be configured via the `compilerOptions` in the build config instead (see example configs above).
## Feature Reference
### Compatibility Types
- ✔ Fully compatible
- ◐ Partially Compatible with caveats
- ⨂ Incompatible (warning only)
- ⭘ Compat only (no warning)
### Incompatible
> Should be fixed upfront or will likely lead to errors
| ID | Type | Description | Docs |
| ------------------------------------- | ---- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| GLOBAL_MOUNT_CONTAINER | ⨂ | Mounted application does not replace the element it's mounted to | [link](https://v3-migration.vuejs.org/breaking-changes/mount-changes.html) |
| CONFIG_DEVTOOLS | ⨂ | production devtools is now a build-time flag | [link](https://github.com/vuejs/core/tree/main/packages/vue#bundler-build-feature-flags) |
| COMPILER_V_IF_V_FOR_PRECEDENCE | ⨂ | `v-if` and `v-for` precedence when used on the same element has changed | [link](https://v3-migration.vuejs.org/breaking-changes/v-if-v-for.html) |
| COMPILER_V_IF_SAME_KEY | ⨂ | `v-if` branches can no longer have the same key | [link](https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#on-conditional-branches) |
| COMPILER_V_FOR_TEMPLATE_KEY_PLACEMENT | ⨂ | `` key should now be placed on `` | [link](https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for) |
| COMPILER_SFC_FUNCTIONAL | ⨂ | `` is no longer supported in SFCs | [link](https://v3-migration.vuejs.org/breaking-changes/functional-components.html#single-file-components-sfcs) |
### Partially Compatible with Caveats
| ID | Type | Description | Docs |
| ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| CONFIG_IGNORED_ELEMENTS | ◐ | `config.ignoredElements` is now `config.compilerOptions.isCustomElement` (only in browser compiler build). If using build setup, `isCustomElement` must be passed via build configuration. | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#config-ignoredelements-is-now-config-iscustomelement) |
| COMPILER_INLINE_TEMPLATE | ◐ | `inline-template` removed (compat only supported in browser compiler build) | [link](https://v3-migration.vuejs.org/breaking-changes/inline-template-attribute.html) |
| PROPS_DEFAULT_THIS | ◐ | props default factory no longer have access to `this` (in compat mode, `this` is not a real instance - it only exposes props, `$options` and injections) | [link](https://v3-migration.vuejs.org/breaking-changes/props-default-this.html) |
| INSTANCE_DESTROY | ◐ | `$destroy` instance method removed (in compat mode, only supported on root instance) | |
| GLOBAL_PRIVATE_UTIL | ◐ | `Vue.util` is private and no longer available | |
| CONFIG_PRODUCTION_TIP | ◐ | `config.productionTip` no longer necessary | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#config-productiontip-removed) |
| CONFIG_SILENT | ◐ | `config.silent` removed |
### Compat only (no warning)
| ID | Type | Description | Docs |
| ------------------ | ---- | -------------------------------------- | ----------------------------------------------------------------------- |
| TRANSITION_CLASSES | ⭘ | Transition enter/leave classes changed | [link](https://v3-migration.vuejs.org/breaking-changes/transition.html) |
### Fully Compatible
| ID | Type | Description | Docs |
| ---------------------------- | ---- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| GLOBAL_MOUNT | ✔ | new Vue() -> createApp | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#mounting-app-instance) |
| GLOBAL_EXTEND | ✔ | Vue.extend removed (use `defineComponent` or `extends` option) | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-extend-replaced-by-definecomponent) |
| GLOBAL_PROTOTYPE | ✔ | `Vue.prototype` -> `app.config.globalProperties` | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-prototype-replaced-by-config-globalproperties) |
| GLOBAL_SET | ✔ | `Vue.set` removed (no longer needed) | |
| GLOBAL_DELETE | ✔ | `Vue.delete` removed (no longer needed) | |
| GLOBAL_OBSERVABLE | ✔ | `Vue.observable` removed (use `reactive`) | [link](https://vuejs.org/api/reactivity-core.html#reactive) |
| CONFIG_KEY_CODES | ✔ | config.keyCodes removed | [link](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) |
| CONFIG_WHITESPACE | ✔ | In Vue 3 whitespace defaults to `"condense"` | |
| INSTANCE_SET | ✔ | `vm.$set` removed (no longer needed) | |
| INSTANCE_DELETE | ✔ | `vm.$delete` removed (no longer needed) | |
| INSTANCE_EVENT_EMITTER | ✔ | `vm.$on`, `vm.$off`, `vm.$once` removed | [link](https://v3-migration.vuejs.org/breaking-changes/events-api.html) |
| INSTANCE_EVENT_HOOKS | ✔ | Instance no longer emits `hook:x` events | [link](https://v3-migration.vuejs.org/breaking-changes/vnode-lifecycle-events.html) |
| INSTANCE_CHILDREN | ✔ | `vm.$children` removed | [link](https://v3-migration.vuejs.org/breaking-changes/children.html) |
| INSTANCE_LISTENERS | ✔ | `vm.$listeners` removed | [link](https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html) |
| INSTANCE_SCOPED_SLOTS | ✔ | `vm.$scopedSlots` removed; `vm.$slots` now exposes functions | [link](https://v3-migration.vuejs.org/breaking-changes/slots-unification.html) |
| INSTANCE_ATTRS_CLASS_STYLE | ✔ | `$attrs` now includes `class` and `style` | [link](https://v3-migration.vuejs.org/breaking-changes/attrs-includes-class-style.html) |
| OPTIONS_DATA_FN | ✔ | `data` must be a function in all cases | [link](https://v3-migration.vuejs.org/breaking-changes/data-option.html) |
| OPTIONS_DATA_MERGE | ✔ | `data` from mixin or extension is now shallow merged | [link](https://v3-migration.vuejs.org/breaking-changes/data-option.html) |
| OPTIONS_BEFORE_DESTROY | ✔ | `beforeDestroy` -> `beforeUnmount` | |
| OPTIONS_DESTROYED | ✔ | `destroyed` -> `unmounted` | |
| WATCH_ARRAY | ✔ | watching an array no longer triggers on mutation unless deep | [link](https://v3-migration.vuejs.org/breaking-changes/watch.html) |
| V_ON_KEYCODE_MODIFIER | ✔ | `v-on` no longer supports keyCode modifiers | [link](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) |
| CUSTOM_DIR | ✔ | Custom directive hook names changed | [link](https://v3-migration.vuejs.org/breaking-changes/custom-directives.html) |
| ATTR_FALSE_VALUE | ✔ | No longer removes attribute if binding value is boolean `false` | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) |
| ATTR_ENUMERATED_COERCION | ✔ | No longer special case enumerated attributes | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) |
| TRANSITION_GROUP_ROOT | ✔ | `` no longer renders a root element by default | [link](https://v3-migration.vuejs.org/breaking-changes/transition-group.html) |
| COMPONENT_ASYNC | ✔ | Async component API changed (now requires `defineAsyncComponent`) | [link](https://v3-migration.vuejs.org/breaking-changes/async-components.html) |
| COMPONENT_FUNCTIONAL | ✔ | Functional component API changed (now must be plain functions) | [link](https://v3-migration.vuejs.org/breaking-changes/functional-components.html) |
| COMPONENT_V_MODEL | ✔ | Component v-model reworked | [link](https://v3-migration.vuejs.org/breaking-changes/v-model.html) |
| RENDER_FUNCTION | ✔ | Render function API changed | [link](https://v3-migration.vuejs.org/breaking-changes/render-function-api.html) |
| FILTERS | ✔ | Filters removed (this option affects only runtime filter APIs) | [link](https://v3-migration.vuejs.org/breaking-changes/filters.html) |
| COMPILER_IS_ON_ELEMENT | ✔ | `is` usage is now restricted to `` only | [link](https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html) |
| COMPILER_V_BIND_SYNC | ✔ | `v-bind.sync` replaced by `v-model` with arguments | [link](https://v3-migration.vuejs.org/breaking-changes/v-model.html) |
| COMPILER_V_BIND_OBJECT_ORDER | ✔ | `v-bind="object"` is now order sensitive | [link](https://v3-migration.vuejs.org/breaking-changes/v-bind.html) |
| COMPILER_V_ON_NATIVE | ✔ | `v-on.native` modifier removed | [link](https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html) |
| COMPILER_V_FOR_REF | ✔ | `ref` in `v-for` (compiler support) | |
| COMPILER_NATIVE_TEMPLATE | ✔ | `` with no special directives now renders as native element | |
| COMPILER_FILTERS | ✔ | filters (compiler support) | |
================================================
FILE: packages/vue-compat/__tests__/compiler.spec.ts
================================================
import Vue from '@vue/compat'
import { nextTick } from '@vue/runtime-core'
import { CompilerDeprecationTypes } from '@vue/compiler-core'
import { toggleDeprecationWarning } from '../../runtime-core/src/compat/compatConfig'
import { triggerEvent } from './utils'
beforeEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({
MODE: 2,
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
// COMPILER_V_FOR_REF is tested in ./refInfor.spec.ts
// COMPILER_FILTERS is tested in ./filters.spec.ts
test('COMPILER_IS_ON_ELEMENT', () => {
const MyButton = {
template: `
`,
}
const vm = new Vue({
template: `text `,
components: {
MyButton,
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.outerHTML).toBe(`text
`)
expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
})
test('COMPILER_IS_ON_ELEMENT (dynamic)', () => {
const MyButton = {
template: `
`,
}
const vm = new Vue({
template: `text `,
components: {
MyButton,
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.outerHTML).toBe(`text
`)
expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
})
test('COMPILER_V_BIND_SYNC', async () => {
const MyButton = {
props: ['foo'],
template: `{{ foo }} `,
}
const vm = new Vue({
data() {
return {
foo: 0,
}
},
template: ` `,
components: {
MyButton,
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLButtonElement)
expect(vm.$el.textContent).toBe(`0`)
triggerEvent(vm.$el as Element, 'click')
await nextTick()
expect(vm.$el.textContent).toBe(`1`)
expect(CompilerDeprecationTypes.COMPILER_V_BIND_SYNC).toHaveBeenWarned()
})
test('COMPILER_V_BIND_OBJECT_ORDER', () => {
const vm = new Vue({
template: `
`,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.id).toBe('foo')
expect(vm.$el.className).toBe('baz')
expect(
CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER,
).toHaveBeenWarned()
})
test('should not warn COMPILER_V_BIND_OBJECT_ORDER work with vFor', () => {
const vm = new Vue({
template: ``,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(
CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER,
).not.toHaveBeenWarned()
})
test('COMPILER_V_ON_NATIVE', () => {
const spy = vi.fn()
const vm = new Vue({
template: ` `,
components: {
child: {
template: ` `,
},
},
methods: {
spy,
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLButtonElement)
triggerEvent(vm.$el as HTMLButtonElement, 'click')
expect(spy).toHaveBeenCalledTimes(1)
expect(CompilerDeprecationTypes.COMPILER_V_ON_NATIVE).toHaveBeenWarned()
})
test('COMPILER_V_IF_V_FOR_PRECEDENCE', () => {
new Vue({ template: `
` }).$mount()
expect(
CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
).toHaveBeenWarned()
})
test('COMPILER_NATIVE_TEMPLATE', () => {
const vm = new Vue({
template: ``,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.innerHTML).toBe(`
`)
expect(CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE).toHaveBeenWarned()
})
test('COMPILER_INLINE_TEMPLATE', () => {
const vm = new Vue({
template: `{{ n }}
`,
components: {
foo: {
data() {
return { n: 123 }
},
},
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el?.outerHTML).toBe(`123
`)
expect(CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE).toHaveBeenWarned()
})
================================================
FILE: packages/vue-compat/__tests__/componentAsync.spec.ts
================================================
import Vue from '@vue/compat'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
beforeEach(() => {
toggleDeprecationWarning(true)
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
const timeout = (n: number) => new Promise(r => setTimeout(r, n))
describe('COMPONENT_ASYNC', () => {
test('resolve/reject', async () => {
let resolve: any
const comp = (r: any) => {
resolve = r
}
const vm = new Vue({
template: `
`,
components: { comp },
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.innerHTML).toBe(``)
resolve({ template: 'foo' })
await timeout(0)
expect(vm.$el.innerHTML).toBe(`foo`)
expect(
(deprecationData[DeprecationTypes.COMPONENT_ASYNC].message as Function)(
comp,
),
).toHaveBeenWarned()
})
test('Promise', async () => {
const comp = () => Promise.resolve({ template: 'foo' })
const vm = new Vue({
template: `
`,
components: { comp },
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.innerHTML).toBe(``)
await timeout(0)
expect(vm.$el.innerHTML).toBe(`foo`)
expect(
(deprecationData[DeprecationTypes.COMPONENT_ASYNC].message as Function)(
comp,
),
).toHaveBeenWarned()
})
test('object syntax', async () => {
const comp = () => ({
component: Promise.resolve({ template: 'foo' }),
})
const vm = new Vue({
template: `
`,
components: { comp },
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.innerHTML).toBe(``)
await timeout(0)
expect(vm.$el.innerHTML).toBe(`foo`)
expect(
(deprecationData[DeprecationTypes.COMPONENT_ASYNC].message as Function)(
comp,
),
).toHaveBeenWarned()
})
})
================================================
FILE: packages/vue-compat/__tests__/componentFunctional.spec.ts
================================================
import Vue from '@vue/compat'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
beforeEach(() => {
toggleDeprecationWarning(true)
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
describe('COMPONENT_FUNCTIONAL', () => {
test('basic usage', async () => {
const func = {
name: 'Func',
functional: true,
props: {
x: String,
},
inject: ['foo'],
render: (h: any, { data, props, injections, slots }: any) => {
return h('div', { id: props.x, class: data.class }, [
h('div', { class: 'inject' }, injections.foo),
h('div', { class: 'slot' }, slots().default),
])
},
}
const vm = new Vue({
provide() {
return {
foo: 123,
}
},
components: {
func,
},
template: `hello `,
}).$mount()
expect(vm.$el.id).toBe('foo')
expect(vm.$el.className).toBe('foo')
expect(vm.$el.querySelector('.inject').textContent).toBe('123')
expect(vm.$el.querySelector('.slot').textContent).toBe('hello')
expect(vm.$el.outerHTML).toMatchInlineSnapshot(
`""`,
)
expect(
(
deprecationData[DeprecationTypes.COMPONENT_FUNCTIONAL]
.message as Function
)(func),
).toHaveBeenWarned()
})
test('copies compatConfig option', () => {
const func = {
name: 'Func',
functional: true,
compatConfig: {
ATTR_FALSE_VALUE: 'suppress-warning' as const,
},
render: (h: any) => {
// should not render required: false due to compatConfig
return h('div', { 'data-some-attr': false })
},
}
const vm = new Vue({
components: { func },
template: `hello `,
}).$mount()
expect(vm.$el.outerHTML).toMatchInlineSnapshot(`"
"`)
expect(
(
deprecationData[DeprecationTypes.COMPONENT_FUNCTIONAL]
.message as Function
)(func),
).toHaveBeenWarned()
expect(
(deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
func,
),
).not.toHaveBeenWarned()
})
})
================================================
FILE: packages/vue-compat/__tests__/componentVModel.spec.ts
================================================
import Vue from '@vue/compat'
import type { ComponentOptions } from '../../runtime-core/src/component'
import { nextTick } from '../../runtime-core/src/scheduler'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
import { triggerEvent } from './utils'
beforeEach(() => {
toggleDeprecationWarning(true)
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
describe('COMPONENT_V_MODEL', () => {
async function runTest(CustomInput: ComponentOptions) {
const vm = new Vue({
data() {
return {
text: 'foo',
}
},
components: { CustomInput },
template: `
{{ text }}
`,
}).$mount() as any
const input = vm.$el.querySelector('input')
const span = vm.$el.querySelector('span')
expect(input.value).toBe('foo')
expect(span.textContent).toBe('foo')
expect(
(deprecationData[DeprecationTypes.COMPONENT_V_MODEL].message as Function)(
CustomInput,
),
).toHaveBeenWarned()
input.value = 'bar'
triggerEvent(input, 'input')
await nextTick()
expect(input.value).toBe('bar')
expect(span.textContent).toBe('bar')
vm.text = 'baz'
await nextTick()
expect(input.value).toBe('baz')
expect(span.textContent).toBe('baz')
}
test('basic usage', async () => {
await runTest({
name: 'CustomInput',
props: ['value'],
template: ` `,
})
})
test('with model option', async () => {
await runTest({
name: 'CustomInput',
props: ['input'],
model: {
prop: 'input',
event: 'update',
},
template: ` `,
})
})
async function runTestWithModifier(CustomInput: ComponentOptions) {
const vm = new Vue({
data() {
return {
text: ' foo ',
}
},
components: {
CustomInput,
},
template: `
{{ text }}
`,
}).$mount() as any
const input = vm.$el.querySelector('input')
const span = vm.$el.querySelector('span')
expect(input.value).toBe(' foo ')
expect(span.textContent).toBe(' foo ')
expect(
(deprecationData[DeprecationTypes.COMPONENT_V_MODEL].message as Function)(
CustomInput,
),
).toHaveBeenWarned()
input.value = ' bar '
triggerEvent(input, 'input')
await nextTick()
expect(input.value).toBe('bar')
expect(span.textContent).toBe('bar')
}
test('with model modifiers', async () => {
await runTestWithModifier({
name: 'CustomInput',
props: ['value'],
template: ` `,
})
})
test('with model modifiers and model option', async () => {
await runTestWithModifier({
name: 'CustomInput',
props: ['foo'],
model: {
prop: 'foo',
event: 'bar',
},
template: ` `,
})
})
// #14202
test('should handle v-model deprecation warning with missing appContext', async () => {
const ChildComponent = {
template: `{{ value }}
`,
props: ['value'],
}
const vm = new Vue({
components: { ChildComponent },
data() {
return {
myVal: 'initial',
}
},
template: `
`,
}).$mount() as any
expect(vm.$el.textContent).toContain('initial')
expect(
(deprecationData[DeprecationTypes.COMPONENT_V_MODEL].message as Function)(
ChildComponent,
),
).toHaveBeenWarned()
// Should work correctly
const child = vm.$el.querySelector('div')
child.click()
await nextTick()
expect(vm.myVal).toBe('new val')
expect(vm.$el.textContent).toContain('new val')
})
})
================================================
FILE: packages/vue-compat/__tests__/filters.spec.ts
================================================
import Vue from '@vue/compat'
import { CompilerDeprecationTypes } from '../../compiler-core/src'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
beforeEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 2, GLOBAL_MOUNT: 'suppress-warning' })
})
afterEach(() => {
Vue.configureCompat({ MODE: 3 })
toggleDeprecationWarning(false)
})
describe('FILTERS', () => {
function upper(v: string) {
return v.toUpperCase()
}
function lower(v: string) {
return v.toLowerCase()
}
function reverse(v: string) {
return v.split('').reverse().join('')
}
function double(v: number) {
return v * 2
}
it('global registration', () => {
toggleDeprecationWarning(true)
Vue.filter('globalUpper', upper)
expect(Vue.filter('globalUpper')).toBe(upper)
const vm = new Vue({
template: '{{ msg | globalUpper }}
',
data: () => ({
msg: 'hi',
}),
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.textContent).toBe('HI')
expect(deprecationData[DeprecationTypes.FILTERS].message).toHaveBeenWarned()
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
Vue.filter('globalUpper', undefined)
})
it('basic usage', () => {
const vm = new Vue({
template: '{{ msg | upper }}
',
data: () => ({
msg: 'hi',
}),
filters: {
upper,
},
}).$mount()
expect(vm.$el.textContent).toBe('HI')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('chained usage', () => {
const vm = new Vue({
template: '{{ msg | upper | reverse }}
',
data: () => ({
msg: 'hi',
}),
filters: {
upper,
reverse,
},
}).$mount()
expect(vm.$el.textContent).toBe('IH')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('in v-bind', () => {
const vm = new Vue({
template: `
`,
filters: {
upper,
reverse,
lower,
},
data: () => ({
id: 'abc',
cls: 'foo',
ref: 'BAR',
}),
}).$mount()
expect(vm.$el.id).toBe('CBA')
expect(vm.$el.className).toBe('oof')
expect(vm.$refs.bar).toBe(vm.$el)
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('handle regex with pipe', () => {
const vm = new Vue({
template: ` `,
filters: { identity: (v: any) => v },
components: {
test: {
props: ['pattern'],
template: '
',
},
},
}).$mount() as any
expect(vm.$refs.test.pattern).toBeInstanceOf(RegExp)
expect(vm.$refs.test.pattern.toString()).toBe('/a|b\\//')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('handle division', () => {
const vm = new Vue({
data: () => ({ a: 2 }),
template: `{{ 1/a / 4 | double }}
`,
filters: { double },
}).$mount()
expect(vm.$el.textContent).toBe(String(1 / 4))
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('handle division with parenthesis', () => {
const vm = new Vue({
data: () => ({ a: 20 }),
template: `{{ (a*2) / 5 | double }}
`,
filters: { double },
}).$mount()
expect(vm.$el.textContent).toBe(String(16))
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('handle division with dot', () => {
const vm = new Vue({
template: `{{ 20. / 5 | double }}
`,
filters: { double },
}).$mount()
expect(vm.$el.textContent).toBe(String(8))
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('handle division with array values', () => {
const vm = new Vue({
data: () => ({ a: [20] }),
template: `{{ a[0] / 5 | double }}
`,
filters: { double },
}).$mount()
expect(vm.$el.textContent).toBe(String(8))
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('handle division with hash values', () => {
const vm = new Vue({
data: () => ({ a: { n: 20 } }),
template: `{{ a['n'] / 5 | double }}
`,
filters: { double },
}).$mount()
expect(vm.$el.textContent).toBe(String(8))
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('handle division with variable_', () => {
const vm = new Vue({
data: () => ({ a_: 8 }),
template: `{{ a_ / 2 | double }}
`,
filters: { double },
}).$mount()
expect(vm.$el.textContent).toBe(String(8))
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('arguments', () => {
const vm = new Vue({
template: `{{ msg | add(a, 3) }}
`,
data: () => ({
msg: 1,
a: 2,
}),
filters: {
add: (v: number, arg1: number, arg2: number) => v + arg1 + arg2,
},
}).$mount()
expect(vm.$el.textContent).toBe('6')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('quotes', () => {
const vm = new Vue({
template: `{{ msg + "b | c" + 'd' | upper }}
`,
data: () => ({
msg: 'a',
}),
filters: {
upper,
},
}).$mount()
expect(vm.$el.textContent).toBe('AB | CD')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('double pipe', () => {
const vm = new Vue({
template: `{{ b || msg | upper }}
`,
data: () => ({
b: false,
msg: 'a',
}),
filters: {
upper,
},
}).$mount()
expect(vm.$el.textContent).toBe('A')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('object literal', () => {
const vm = new Vue({
template: `{{ { a: 123 } | pick('a') }}
`,
filters: {
pick: (v: any, key: string) => v[key],
},
}).$mount()
expect(vm.$el.textContent).toBe('123')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('array literal', () => {
const vm = new Vue({
template: `{{ [1, 2, 3] | reverse }}
`,
filters: {
reverse: (arr: any[]) => arr.reverse().join(','),
},
}).$mount()
expect(vm.$el.textContent).toBe('3,2,1')
expect(CompilerDeprecationTypes.COMPILER_FILTERS).toHaveBeenWarned()
})
it('bigint support', () => {
const vm = new Vue({
template: `{{ BigInt(BigInt(10000000)) + BigInt(2000000000n) * 3000000n }}
`,
}).$mount()
expect(vm.$el.textContent).toBe('6000000010000000')
})
})
================================================
FILE: packages/vue-compat/__tests__/global.spec.ts
================================================
import Vue from '@vue/compat'
import { effect, isReactive } from '@vue/reactivity'
import { h, nextTick } from '@vue/runtime-core'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
import { singletonApp } from '../../runtime-core/src/compat/global'
import { createApp } from '../src/esm-index'
beforeEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 2 })
})
afterEach(() => {
Vue.configureCompat({ MODE: 3 })
toggleDeprecationWarning(false)
})
describe('GLOBAL_MOUNT', () => {
test('new Vue() with el', () => {
toggleDeprecationWarning(true)
const el = document.createElement('div')
el.innerHTML = `{{ msg }}`
new Vue({
el,
compatConfig: { GLOBAL_MOUNT: true },
data() {
return {
msg: 'hello',
}
},
})
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message,
).toHaveBeenWarned()
expect(el.innerHTML).toBe('hello')
})
test('new Vue() + $mount', () => {
const el = document.createElement('div')
el.innerHTML = `{{ msg }}`
new Vue({
data() {
return {
msg: 'hello',
}
},
}).$mount(el)
expect(el.innerHTML).toBe('hello')
})
})
describe('GLOBAL_MOUNT_CONTAINER', () => {
test('should warn', () => {
toggleDeprecationWarning(true)
const el = document.createElement('div')
el.innerHTML = `test`
el.setAttribute('v-bind:id', 'foo')
new Vue().$mount(el)
// warning only
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message,
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT_CONTAINER].message,
).toHaveBeenWarned()
})
})
describe('GLOBAL_EXTEND', () => {
// https://github.com/vuejs/vue/blob/dev/test/unit/features/global-api/extend.spec.js
it('should correctly merge options', () => {
toggleDeprecationWarning(true)
const Test = Vue.extend({
name: 'test',
a: 1,
b: 2,
})
expect(Test.options.a).toBe(1)
expect(Test.options.b).toBe(2)
expect(Test.super).toBe(Vue)
const t = new Test({
a: 2,
})
expect(t.$options.a).toBe(2)
expect(t.$options.b).toBe(2)
// inheritance
const Test2 = Test.extend({
a: 2,
})
expect(Test2.options.a).toBe(2)
expect(Test2.options.b).toBe(2)
const t2 = new Test2({
a: 3,
})
expect(t2.$options.a).toBe(3)
expect(t2.$options.b).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message,
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_EXTEND].message,
).toHaveBeenWarned()
})
it('should work when used as components', () => {
const foo = Vue.extend({
template: 'foo ',
})
const bar = Vue.extend({
template: 'bar ',
})
const vm = new Vue({
template: '
',
components: { foo, bar },
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.innerHTML).toBe('foo bar ')
})
it('should merge lifecycle hooks', () => {
const calls: number[] = []
const A = Vue.extend({
created() {
calls.push(1)
},
})
const B = A.extend({
created() {
calls.push(2)
},
})
new B({
created() {
calls.push(3)
},
})
expect(calls).toEqual([1, 2, 3])
})
it('should not merge nested mixins created with Vue.extend', () => {
const a = vi.fn()
const b = vi.fn()
const c = vi.fn()
const d = vi.fn()
const A = Vue.extend({
created: a,
})
const B = Vue.extend({
mixins: [A],
created: b,
})
const C = Vue.extend({
extends: B,
created: c,
})
const D = Vue.extend({
mixins: [C],
created: d,
render() {
return null
},
})
new D().$mount()
expect(a.mock.calls.length).toStrictEqual(1)
expect(b.mock.calls.length).toStrictEqual(1)
expect(c.mock.calls.length).toStrictEqual(1)
expect(d.mock.calls.length).toStrictEqual(1)
})
it('should merge methods', () => {
const A = Vue.extend({
methods: {
a() {
return this.n
},
},
})
const B = A.extend({
methods: {
b() {
return this.n + 1
},
},
})
const b = new B({
data: () => ({ n: 0 }),
methods: {
c() {
return this.n + 2
},
},
}) as any
expect(b.a()).toBe(0)
expect(b.b()).toBe(1)
expect(b.c()).toBe(2)
})
it('should merge assets', () => {
const A = Vue.extend({
components: {
aa: {
template: 'A
',
},
},
})
const B = A.extend({
components: {
bb: {
template: 'B
',
},
},
})
const b = new B({
template: '',
}).$mount()
expect(b.$el).toBeInstanceOf(HTMLDivElement)
expect(b.$el.innerHTML).toBe('A
B
')
})
it('caching', () => {
const options = {
template: '
',
}
const A = Vue.extend(options)
const B = Vue.extend(options)
expect(A).toBe(B)
})
it('extended options should use different identify from parent', () => {
const A = Vue.extend({ computed: {} })
const B = A.extend()
B.options.computed.b = () => 'foo'
expect(B.options.computed).not.toBe(A.options.computed)
expect(A.options.computed.b).toBeUndefined()
})
})
describe('GLOBAL_PROTOTYPE', () => {
test('plain properties', () => {
toggleDeprecationWarning(true)
Vue.prototype.$test = 1
const vm = new Vue() as any
expect(vm.$test).toBe(1)
delete Vue.prototype.$test
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message,
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_PROTOTYPE].message,
).toHaveBeenWarned()
})
test('method this context', () => {
Vue.prototype.$test = function () {
return this.msg
}
const vm = new Vue({
data() {
return { msg: 'method' }
},
}) as any
expect(vm.$test()).toBe('method')
delete Vue.prototype.$test
})
test('defined properties', () => {
Object.defineProperty(Vue.prototype, '$test', {
configurable: true,
get() {
return this.msg
},
})
const vm = new Vue({
data() {
return { msg: 'getter' }
},
}) as any
expect(vm.$test).toBe('getter')
delete Vue.prototype.$test
})
test('functions keeps additional properties', () => {
function test(this: any) {
return this.msg
}
test.additionalFn = () => {
return 'additional fn'
}
Vue.prototype.$test = test
const vm = new Vue({
data() {
return {
msg: 'test',
}
},
}) as any
expect(typeof vm.$test).toBe('function')
expect(typeof vm.$test.additionalFn).toBe('function')
expect(vm.$test.additionalFn()).toBe('additional fn')
delete Vue.prototype.$test
})
test('extended prototype', async () => {
const Foo = Vue.extend()
Foo.prototype.$test = 1
const vm = new Foo() as any
expect(vm.$test).toBe(1)
const plain = new Vue() as any
expect(plain.$test).toBeUndefined()
})
test('should affect apps created via createApp()', () => {
Vue.prototype.$test = 1
const vm = createApp({
template: 'foo',
}).mount(document.createElement('div')) as any
expect(vm.$test).toBe(1)
delete Vue.prototype.$test
})
})
describe('GLOBAL_SET/DELETE', () => {
test('set', () => {
toggleDeprecationWarning(true)
const obj: any = {}
Vue.set(obj, 'foo', 1)
expect(obj.foo).toBe(1)
expect(
deprecationData[DeprecationTypes.GLOBAL_SET].message,
).toHaveBeenWarned()
})
test('delete', () => {
toggleDeprecationWarning(true)
const obj: any = { foo: 1 }
Vue.delete(obj, 'foo')
expect('foo' in obj).toBe(false)
expect(
deprecationData[DeprecationTypes.GLOBAL_DELETE].message,
).toHaveBeenWarned()
})
})
describe('GLOBAL_OBSERVABLE', () => {
test('should work', () => {
toggleDeprecationWarning(true)
const obj = Vue.observable({})
expect(isReactive(obj)).toBe(true)
expect(
deprecationData[DeprecationTypes.GLOBAL_OBSERVABLE].message,
).toHaveBeenWarned()
})
})
describe('GLOBAL_PRIVATE_UTIL', () => {
test('defineReactive', () => {
toggleDeprecationWarning(true)
const obj: any = {}
Vue.util.defineReactive(obj, 'test', 1)
let n
effect(() => {
n = obj.test
})
expect(n).toBe(1)
obj.test++
expect(n).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_PRIVATE_UTIL].message,
).toHaveBeenWarned()
})
test('defineReactive on instance', async () => {
const vm = new Vue({
beforeCreate() {
Vue.util.defineReactive(this, 'foo', 1)
},
template: `{{ foo }}
`,
}).$mount() as any
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.textContent).toBe('1')
vm.foo = 2
await nextTick()
expect(vm.$el.textContent).toBe('2')
})
test('defineReactive on instance with key that starts with $', async () => {
const vm = new Vue({
beforeCreate() {
Vue.util.defineReactive(this, '$foo', 1)
},
template: `{{ $foo }}
`,
}).$mount() as any
expect(vm.$el.textContent).toBe('1')
vm.$foo = 2
await nextTick()
expect(vm.$el.textContent).toBe('2')
})
test('defineReactive with object value', () => {
const obj: any = {}
const val = { a: 1 }
Vue.util.defineReactive(obj, 'foo', val)
let n
effect(() => {
n = obj.foo.a
})
expect(n).toBe(1)
// mutating original
val.a++
expect(n).toBe(2)
})
test('defineReactive with array value', () => {
const obj: any = {}
const val = [1]
Vue.util.defineReactive(obj, 'foo', val)
let n
effect(() => {
n = obj.foo.length
})
expect(n).toBe(1)
// mutating original
val.push(2)
expect(n).toBe(2)
})
})
test('global asset registration should affect apps created via createApp', () => {
Vue.component('foo', { template: 'foo' })
const vm = createApp({
template: ' ',
}).mount(document.createElement('div')) as any
expect(vm.$el.textContent).toBe('foo')
delete singletonApp._context.components.foo
})
test('post-facto global asset registration should affect apps created via createApp', () => {
const app = createApp({
template: ' ',
})
Vue.component('foo', { template: 'foo' })
const vm = app.mount(document.createElement('div'))
expect(vm.$el.textContent).toBe('foo')
delete singletonApp._context.components.foo
})
test('local asset registration should not affect other local apps', () => {
const app1 = createApp({})
const app2 = createApp({})
app1.component('foo', {})
app2.component('foo', {})
expect(
`Component "foo" has already been registered in target app`,
).not.toHaveBeenWarned()
})
test('local app-level mixin registration should not affect other local apps', () => {
const app1 = createApp({ render: () => h('div') })
const app2 = createApp({})
const mixin = { created: vi.fn() }
app1.mixin(mixin)
app2.mixin(mixin)
expect(`Mixin has already been applied`).not.toHaveBeenWarned()
app1.mount(document.createElement('div'))
expect(mixin.created).toHaveBeenCalledTimes(1)
})
// #5699
test('local app config should not affect other local apps in v3 mode', () => {
Vue.configureCompat({ MODE: 3 })
const app1 = createApp({
render: () => h('div'),
provide() {
return {
test: 123,
}
},
})
app1.config.globalProperties.test = () => {}
app1.mount(document.createElement('div'))
const app2 = createApp({})
expect(app2.config.globalProperties.test).toBe(undefined)
})
================================================
FILE: packages/vue-compat/__tests__/globalConfig.spec.ts
================================================
import Vue from '@vue/compat'
import {
DeprecationTypes,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
import { createApp } from '../src/esm-index'
import { triggerEvent } from './utils'
beforeEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 2 })
})
afterEach(() => {
Vue.configureCompat({ MODE: 3 })
toggleDeprecationWarning(false)
})
// only testing config options that affect runtime behavior.
test('GLOBAL_KEY_CODES', () => {
Vue.config.keyCodes = {
foo: 86,
bar: [38, 87],
}
const onFoo = vi.fn()
const onBar = vi.fn()
const el = document.createElement('div')
new Vue({
el,
template: ` `,
methods: {
onFoo,
onBar,
},
})
triggerEvent(el.children[0], 'keyup', e => {
e.key = '_'
e.keyCode = 86
})
expect(onFoo).toHaveBeenCalledTimes(1)
expect(onBar).toHaveBeenCalledTimes(0)
triggerEvent(el.children[0], 'keyup', e => {
e.key = '_'
e.keyCode = 38
})
expect(onFoo).toHaveBeenCalledTimes(1)
expect(onBar).toHaveBeenCalledTimes(1)
triggerEvent(el.children[0], 'keyup', e => {
e.key = '_'
e.keyCode = 87
})
expect(onFoo).toHaveBeenCalledTimes(1)
expect(onBar).toHaveBeenCalledTimes(2)
})
test('GLOBAL_IGNORED_ELEMENTS', () => {
Vue.config.ignoredElements = [/^v-/, 'foo']
const el = document.createElement('div')
new Vue({
el,
template: ` `,
})
expect(el.innerHTML).toBe(` `)
})
test('singleton config should affect apps created with createApp()', () => {
Vue.config.ignoredElements = [/^v-/, 'foo']
const el = document.createElement('div')
createApp({
template: ` `,
}).mount(el)
expect(el.innerHTML).toBe(` `)
})
test('config.optionMergeStrategies', () => {
toggleDeprecationWarning(true)
expect(typeof Vue.config.optionMergeStrategies.created).toBe('function')
expect(DeprecationTypes.CONFIG_OPTION_MERGE_STRATS).toHaveBeenWarned()
})
================================================
FILE: packages/vue-compat/__tests__/instance.spec.ts
================================================
import type { Mock } from 'vitest'
import Vue from '@vue/compat'
import type { Slots } from '../../runtime-core/src/componentSlots'
import { Text } from '../../runtime-core/src/vnode'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
import type { LegacyPublicInstance } from '../../runtime-core/src/compat/instance'
beforeEach(() => {
toggleDeprecationWarning(true)
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
PRIVATE_APIS: 'suppress-warning',
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
test('INSTANCE_SET', () => {
const obj: any = {}
new Vue().$set(obj, 'foo', 1)
expect(obj.foo).toBe(1)
expect(
deprecationData[DeprecationTypes.INSTANCE_SET].message,
).toHaveBeenWarned()
})
test('INSTANCE_DELETE', () => {
const obj: any = { foo: 1 }
new Vue().$delete(obj, 'foo')
expect('foo' in obj).toBe(false)
expect(
deprecationData[DeprecationTypes.INSTANCE_DELETE].message,
).toHaveBeenWarned()
})
test('INSTANCE_DESTROY', () => {
new Vue({ template: 'foo' }).$mount().$destroy()
expect(
deprecationData[DeprecationTypes.INSTANCE_DESTROY].message,
).toHaveBeenWarned()
})
// https://github.com/vuejs/vue/blob/dev/test/unit/features/instance/methods-events.spec.js
describe('INSTANCE_EVENT_EMITTER', () => {
let vm: LegacyPublicInstance
let spy: Mock
beforeEach(() => {
vm = new Vue()
spy = vi.fn()
})
it('$on', () => {
vm.$on('test', function (this: any) {
// expect correct context
expect(this).toBe(vm)
spy.apply(this, arguments as unknown as any[])
})
vm.$emit('test', 1, 2, 3, 4)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(1, 2, 3, 4)
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$on multi event', () => {
vm.$on(['test1', 'test2'], function (this: any) {
expect(this).toBe(vm)
spy.apply(this, arguments as unknown as any[])
})
vm.$emit('test1', 1, 2, 3, 4)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(1, 2, 3, 4)
vm.$emit('test2', 5, 6, 7, 8)
expect(spy).toHaveBeenCalledTimes(2)
expect(spy).toHaveBeenCalledWith(5, 6, 7, 8)
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$off multi event', () => {
vm.$on(['test1', 'test2', 'test3'], spy)
vm.$off(['test1', 'test2'], spy)
vm.$emit('test1')
vm.$emit('test2')
expect(spy).not.toHaveBeenCalled()
vm.$emit('test3', 1, 2, 3, 4)
expect(spy).toHaveBeenCalledTimes(1)
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$off multi event without callback', () => {
vm.$on(['test1', 'test2'], spy)
vm.$off(['test1', 'test2'])
vm.$emit('test1')
expect(spy).not.toHaveBeenCalled()
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$once', () => {
vm.$once('test', spy)
vm.$emit('test', 1, 2, 3)
vm.$emit('test', 2, 3, 4)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(1, 2, 3)
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$off event added by $once', () => {
vm.$once('test', spy)
vm.$off('test', spy) // test off event and this event added by once
vm.$emit('test', 1, 2, 3)
expect(spy).not.toHaveBeenCalled()
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$off', () => {
vm.$on('test1', spy)
vm.$on('test2', spy)
vm.$off()
vm.$emit('test1')
vm.$emit('test2')
expect(spy).not.toHaveBeenCalled()
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$off event', () => {
vm.$on('test1', spy)
vm.$on('test2', spy)
vm.$off('test1')
vm.$off('test1') // test off something that's already off
vm.$emit('test1', 1)
vm.$emit('test2', 2)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(2)
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
it('$off event + fn', () => {
const spy2 = vi.fn()
vm.$on('test', spy)
vm.$on('test', spy2)
vm.$off('test', spy)
vm.$emit('test', 1, 2, 3)
expect(spy).not.toHaveBeenCalled()
expect(spy2).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledWith(1, 2, 3)
expect(
deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message,
).toHaveBeenWarned()
})
})
describe('INSTANCE_EVENT_HOOKS', () => {
test('instance API', () => {
const spy = vi.fn()
const vm = new Vue({ template: 'foo' })
vm.$on('hook:mounted', spy)
vm.$mount()
expect(spy).toHaveBeenCalled()
expect(
(
deprecationData[DeprecationTypes.INSTANCE_EVENT_HOOKS]
.message as Function
)('hook:mounted'),
).toHaveBeenWarned()
})
test('via template', () => {
const spy = vi.fn()
new Vue({
template: ` `,
methods: { spy },
components: {
child: {
template: 'foo',
},
},
}).$mount()
expect(spy).toHaveBeenCalled()
expect(
(
deprecationData[DeprecationTypes.INSTANCE_EVENT_HOOKS]
.message as Function
)('hook:mounted'),
).toHaveBeenWarned()
})
})
test('INSTANCE_EVENT_CHILDREN', () => {
const vm = new Vue({
template: `
`,
components: {
child: {
template: 'foo',
data() {
return { n: 1 }
},
},
},
}).$mount()
expect(vm.$children.length).toBe(4)
vm.$children.forEach((c: any) => {
expect(c.n).toBe(1)
})
expect(
deprecationData[DeprecationTypes.INSTANCE_CHILDREN].message,
).toHaveBeenWarned()
})
test('INSTANCE_LISTENERS', () => {
const foo = () => 'foo'
const bar = () => 'bar'
let listeners: Record
new Vue({
template: ` `,
methods: { foo, bar },
components: {
child: {
template: `
`,
mounted() {
listeners = this.$listeners
},
},
},
}).$mount()
expect(Object.keys(listeners!)).toMatchObject(['click', 'custom'])
expect(listeners!.click()).toBe('foo')
expect(listeners!.custom()).toBe('bar')
expect(
deprecationData[DeprecationTypes.INSTANCE_LISTENERS].message,
).toHaveBeenWarned()
})
describe('INSTANCE_SCOPED_SLOTS', () => {
test('explicit usage', () => {
let slots: Slots
new Vue({
template: `{{ msg }} `,
components: {
child: {
compatConfig: { RENDER_FUNCTION: false },
render() {
slots = this.$scopedSlots
},
},
},
}).$mount()
expect(slots!.default!({ msg: 'hi' })).toMatchObject([
{
type: Text,
children: 'hi',
},
])
expect(
deprecationData[DeprecationTypes.INSTANCE_SCOPED_SLOTS].message,
).toHaveBeenWarned()
})
test('should include legacy slot usage in $scopedSlots', () => {
let normalSlots: Slots
let scopedSlots: Slots
new Vue({
template: `default
`,
components: {
child: {
compatConfig: { RENDER_FUNCTION: false },
render(this: LegacyPublicInstance) {
normalSlots = this.$slots
scopedSlots = this.$scopedSlots
},
},
},
}).$mount()
expect('default' in normalSlots!).toBe(true)
expect('default' in scopedSlots!).toBe(true)
expect(
deprecationData[DeprecationTypes.INSTANCE_SCOPED_SLOTS].message,
).toHaveBeenWarned()
})
})
test('INSTANCE_ATTR_CLASS_STYLE', () => {
const vm = new Vue({
template: ` `,
components: {
child: {
inheritAttrs: false,
template: ``,
},
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.outerHTML).toBe(
``,
)
expect(
(
deprecationData[DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE]
.message as Function
)('Anonymous'),
).toHaveBeenWarned()
})
test('$options mutation', () => {
const Comp = {
props: ['id'],
template: '
',
data() {
return {
foo: '',
}
},
created(this: any) {
expect(this.$options.parent).toBeDefined()
expect(this.$options.test).toBeUndefined()
this.$options.test = this.id
expect(this.$options.test).toBe(this.id)
},
}
new Vue({
template: `
`,
components: { Comp },
}).$mount()
})
test('other private APIs', () => {
new Vue({
created() {
expect(this.$createElement).toBeTruthy()
},
})
new Vue({
compatConfig: {
PRIVATE_APIS: false,
},
created() {
expect(this.$createElement).toBeUndefined()
},
})
})
================================================
FILE: packages/vue-compat/__tests__/misc.spec.ts
================================================
import Vue from '@vue/compat'
import { nextTick } from '../../runtime-core/src/scheduler'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
import { triggerEvent } from './utils'
import { h } from '@vue/runtime-core'
beforeEach(() => {
toggleDeprecationWarning(true)
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
test('mode as function', () => {
const Foo = {
name: 'Foo',
render: (h: any) => h('div', 'foo'),
}
const Bar = {
name: 'Bar',
data: () => ({ msg: 'bar' }),
render: (ctx: any) => h('div', ctx.msg),
}
toggleDeprecationWarning(false)
Vue.configureCompat({
MODE: comp => (comp && comp.name === 'Bar' ? 3 : 2),
})
const vm = new Vue({
components: { Foo, Bar },
template: `
`,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.innerHTML).toBe(`foo
bar
`)
})
test('WATCH_ARRAY', async () => {
const spy = vi.fn()
const vm = new Vue({
data() {
return {
foo: [],
}
},
watch: {
foo: spy,
},
}) as any
expect(
deprecationData[DeprecationTypes.WATCH_ARRAY].message,
).toHaveBeenWarned()
expect(spy).not.toHaveBeenCalled()
vm.foo.push(1)
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
})
test('PROPS_DEFAULT_THIS', () => {
let thisCtx: any
const Child = {
customOption: 1,
inject: ['provided'],
props: {
foo: null,
bar: {
default(this: any) {
// copy values since injection must be sync
thisCtx = {
foo: this.foo,
$options: this.$options,
provided: this.provided,
}
return this.foo + 1
},
},
},
template: `{{ bar }}`,
}
const vm = new Vue({
components: { Child },
provide: {
provided: 2,
},
template: ` `,
}).$mount()
expect(vm.$el.textContent).toBe('1')
// other props
expect(thisCtx.foo).toBe(0)
// $options
expect(thisCtx.$options.customOption).toBe(1)
// injections
expect(thisCtx.provided).toBe(2)
expect(
(deprecationData[DeprecationTypes.PROPS_DEFAULT_THIS].message as Function)(
'bar',
),
).toHaveBeenWarned()
})
test('V_ON_KEYCODE_MODIFIER', () => {
const spy = vi.fn()
const vm = new Vue({
template: ` `,
methods: { spy },
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLInputElement)
triggerEvent(vm.$el, 'keyup', e => {
e.key = '_'
e.keyCode = 1
})
expect(spy).toHaveBeenCalled()
expect(
deprecationData[DeprecationTypes.V_ON_KEYCODE_MODIFIER].message,
).toHaveBeenWarned()
})
test('CUSTOM_DIR', async () => {
const myDir = {
bind: vi.fn(),
inserted: vi.fn(),
update: vi.fn(),
componentUpdated: vi.fn(),
unbind: vi.fn(),
} as any
const getCalls = () =>
Object.keys(myDir).map(key => myDir[key].mock.calls.length)
const vm = new Vue({
data() {
return {
ok: true,
foo: 1,
}
},
template: `
`,
directives: {
myDir,
},
}).$mount() as any
expect(getCalls()).toMatchObject([1, 1, 0, 0, 0])
expect(
(deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
'bind',
'beforeMount',
),
).toHaveBeenWarned()
expect(
(deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
'inserted',
'mounted',
),
).toHaveBeenWarned()
vm.foo++
await nextTick()
expect(getCalls()).toMatchObject([1, 1, 1, 1, 0])
expect(
(deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
'update',
'updated',
),
).toHaveBeenWarned()
expect(
(deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
'componentUpdated',
'updated',
),
).toHaveBeenWarned()
})
test('ATTR_FALSE_VALUE', () => {
const vm = new Vue({
template: `
`,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.hasAttribute('id')).toBe(false)
expect(vm.$el.hasAttribute('foo')).toBe(false)
expect(
(deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
'id',
),
).toHaveBeenWarned()
expect(
(deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
'foo',
),
).toHaveBeenWarned()
})
test('ATTR_FALSE_VALUE with false on input value', () => {
const vm = new Vue({
template: ` `,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLInputElement)
expect(vm.$el.hasAttribute('value')).toBe(true)
expect(vm.$el.getAttribute('value')).toBe('false')
expect(
(deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
'value',
),
).not.toHaveBeenWarned()
})
test("ATTR_FALSE_VALUE with false value shouldn't throw warning", () => {
const vm = new Vue({
template: `
`,
compatConfig: {
ATTR_FALSE_VALUE: false,
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.hasAttribute('id')).toBe(true)
expect(vm.$el.getAttribute('id')).toBe('false')
expect(vm.$el.hasAttribute('foo')).toBe(true)
expect(vm.$el.getAttribute('foo')).toBe('false')
expect(
(deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
'id',
),
).not.toHaveBeenWarned()
expect(
(deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
'foo',
),
).not.toHaveBeenWarned()
})
test('ATTR_ENUMERATED_COERCION', () => {
const vm = new Vue({
template: `
`,
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.getAttribute('draggable')).toBe('false')
expect(vm.$el.getAttribute('spellcheck')).toBe('true')
expect(vm.$el.getAttribute('contenteditable')).toBe('true')
expect(
(
deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERCION]
.message as Function
)('draggable', null, 'false'),
).toHaveBeenWarned()
expect(
(
deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERCION]
.message as Function
)('spellcheck', 0, 'true'),
).toHaveBeenWarned()
expect(
(
deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERCION]
.message as Function
)('contenteditable', 'foo', 'true'),
).toHaveBeenWarned()
})
test('ATTR_ENUMERATED_COERCION, coercing "false"', () => {
const vm = new Vue({
template: ``,
}).$mount()
expect(vm.$el.innerHTML).toBe(
`hello
`,
)
expect(
(
deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERCION]
.message as Function
)('draggable', 'false', 'false'),
).toHaveBeenWarned()
expect(
(
deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERCION]
.message as Function
)('spellcheck', 'false', 'false'),
).toHaveBeenWarned()
})
================================================
FILE: packages/vue-compat/__tests__/options.spec.ts
================================================
import Vue from '@vue/compat'
import { nextTick } from '../../runtime-core/src/scheduler'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
beforeEach(() => {
toggleDeprecationWarning(true)
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
GLOBAL_EXTEND: 'suppress-warning',
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
test('root data plain object', () => {
const vm = new Vue({
data: { foo: 1 } as any,
template: `{{ foo }}`,
}).$mount()
expect(vm.$el.textContent).toBe('1')
expect(
deprecationData[DeprecationTypes.OPTIONS_DATA_FN].message,
).toHaveBeenWarned()
})
test('data deep merge', () => {
const mixin = {
data() {
return {
foo: {
baz: 2,
},
}
},
}
const vm = new Vue({
mixins: [mixin],
data: () => ({
foo: {
bar: 1,
},
selfData: 3,
}),
template: `{{ { selfData, foo } }}`,
}).$mount()
expect(vm.$el.textContent).toBe(
JSON.stringify({ selfData: 3, foo: { baz: 2, bar: 1 } }, null, 2),
)
expect(
(deprecationData[DeprecationTypes.OPTIONS_DATA_MERGE].message as Function)(
'foo',
),
).toHaveBeenWarned()
})
// #3852
test('data deep merge w/ extended constructor', () => {
const App = Vue.extend({
template: `{{ { mixinData, selfData } }} `,
mixins: [{ data: () => ({ mixinData: 'mixinData' }) }],
data: () => ({ selfData: 'selfData' }),
})
const vm = new App().$mount()
expect(vm.$el.textContent).toBe(
JSON.stringify(
{
mixinData: 'mixinData',
selfData: 'selfData',
},
null,
2,
),
)
})
test('beforeDestroy/destroyed', async () => {
const beforeDestroy = vi.fn()
const destroyed = vi.fn()
const child = {
template: `foo`,
beforeDestroy,
destroyed,
}
const vm = new Vue({
template: ` `,
data() {
return { ok: true }
},
components: { child },
}).$mount() as any
vm.ok = false
await nextTick()
expect(beforeDestroy).toHaveBeenCalled()
expect(destroyed).toHaveBeenCalled()
expect(
deprecationData[DeprecationTypes.OPTIONS_BEFORE_DESTROY].message,
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.OPTIONS_DESTROYED].message,
).toHaveBeenWarned()
})
test('beforeDestroy/destroyed in Vue.extend components', async () => {
const beforeDestroy = vi.fn()
const destroyed = vi.fn()
const child = Vue.extend({
template: `foo`,
beforeDestroy,
destroyed,
})
const vm = new Vue({
template: ` `,
data() {
return { ok: true }
},
components: { child },
}).$mount() as any
vm.ok = false
await nextTick()
expect(beforeDestroy).toHaveBeenCalled()
expect(destroyed).toHaveBeenCalled()
expect(
deprecationData[DeprecationTypes.OPTIONS_BEFORE_DESTROY].message,
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.OPTIONS_DESTROYED].message,
).toHaveBeenWarned()
})
================================================
FILE: packages/vue-compat/__tests__/renderFn.spec.ts
================================================
import { ShapeFlags } from '@vue/shared'
import Vue from '@vue/compat'
import { createComponentInstance } from '../../runtime-core/src/component'
import { setCurrentRenderingInstance } from '../../runtime-core/src/componentRenderContext'
import type { DirectiveBinding } from '../../runtime-core/src/directives'
import { createVNode } from '../../runtime-core/src/vnode'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning,
} from '../../runtime-core/src/compat/compatConfig'
import { compatH as h } from '../../runtime-core/src/compat/renderFn'
beforeEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
})
})
afterEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 3 })
})
describe('compat: render function', () => {
const mockDir = {}
const mockChildComp = {}
const mockComponent = {
directives: {
mockDir,
},
components: {
foo: mockChildComp,
},
}
const mockInstance = createComponentInstance(
createVNode(mockComponent),
null,
null,
)
beforeEach(() => {
setCurrentRenderingInstance(mockInstance)
})
afterEach(() => {
setCurrentRenderingInstance(null)
})
test('string component lookup', () => {
expect(h('foo')).toMatchObject({
type: mockChildComp,
})
})
test('class / style / attrs / domProps / props', () => {
expect(
h('div', {
class: 'foo',
style: { color: 'red' },
attrs: {
id: 'foo',
},
domProps: {
innerHTML: 'hi',
},
props: {
myProp: 'foo',
},
}),
).toMatchObject({
props: {
class: 'foo',
style: { color: 'red' },
id: 'foo',
innerHTML: 'hi',
myProp: 'foo',
},
})
})
test('staticClass + class', () => {
expect(
h('div', {
class: { foo: true },
staticClass: 'bar',
}),
).toMatchObject({
props: {
class: 'bar foo',
},
})
})
test('staticStyle + style', () => {
expect(
h('div', {
style: { color: 'red' },
staticStyle: { fontSize: '14px' },
}),
).toMatchObject({
props: {
style: {
color: 'red',
fontSize: '14px',
},
},
})
})
test('on / nativeOn', () => {
const fn = () => {}
expect(
h('div', {
on: {
click: fn,
fooBar: fn,
},
nativeOn: {
click: fn,
'bar-baz': fn,
},
}),
).toMatchObject({
props: {
onClick: fn,
onClickNative: fn,
onFooBar: fn,
'onBar-bazNative': fn,
},
})
})
test('v2 legacy event prefixes', () => {
const fn = () => {}
expect(
h('div', {
on: {
'&click': fn,
'~keyup': fn,
'!touchend': fn,
},
}),
).toMatchObject({
props: {
onClickPassive: fn,
onKeyupOnce: fn,
onTouchendCapture: fn,
},
})
})
test('directives', () => {
expect(
h('div', {
directives: [
{
name: 'mock-dir',
value: '2',
// expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true,
},
},
],
}),
).toMatchObject({
dirs: [
{
dir: mockDir,
instance: mockInstance.proxy,
value: '2',
oldValue: void 0,
arg: 'foo',
modifiers: {
bar: true,
},
},
] as DirectiveBinding[],
})
})
test('scopedSlots', () => {
const scopedSlots = {
default() {},
}
const vnode = h(mockComponent, {
scopedSlots,
})
expect(vnode).toMatchObject({
children: scopedSlots,
})
expect('scopedSlots' in vnode.props!).toBe(false)
expect(vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN).toBeTruthy()
})
test('legacy named slot', () => {
const vnode = h(mockComponent, [
'text',
h('div', { slot: 'foo' }, 'one'),
h('div', { slot: 'bar' }, 'two'),
h('div', { slot: 'foo' }, 'three'),
h('div', 'four'),
])
expect(vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN).toBeTruthy()
const slots = vnode.children as any
// default
expect(slots.default()).toMatchObject(['text', { children: 'four' }])
expect(slots.foo()).toMatchObject([
{ children: 'one' },
{ children: 'three' },
])
expect(slots.bar()).toMatchObject([{ children: 'two' }])
})
test('in component usage', () => {
toggleDeprecationWarning(true)
const vm = new Vue({
render(h: any) {
return h(
'div',
{
class: 'foo',
attrs: { id: 'bar' },
},
'hello',
)
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.outerHTML).toBe(`hello
`)
expect(
deprecationData[DeprecationTypes.RENDER_FUNCTION].message,
).toHaveBeenWarned()
})
test('should detect v3 compiled render fn', () => {
const vm = new Vue({
data() {
return {
a: 'hello',
}
},
// check is arg length based
render(c: any, _c: any) {
return createVNode('div', null, c.a)
},
}).$mount()
expect(vm.$el).toBeInstanceOf(HTMLDivElement)
expect(vm.$el.outerHTML).toBe(`hello
`)
})
})
================================================
FILE: packages/vue-compat/__tests__/utils.ts
================================================
export function triggerEvent(
target: Element,
event: string,
process?: (e: any) => any,
): Event {
const e = new Event(event, {
bubbles: true,
cancelable: true,
})
if (process) process(e)
target.dispatchEvent(e)
return e
}
================================================
FILE: packages/vue-compat/index.js
================================================
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/vue.cjs.prod.js')
} else {
module.exports = require('./dist/vue.cjs.js')
}
================================================
FILE: packages/vue-compat/package.json
================================================
{
"name": "@vue/compat",
"version": "3.5.30",
"description": "Vue 3 compatibility build for Vue 2",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
"unpkg": "dist/vue.global.js",
"jsdelivr": "dist/vue.global.js",
"files": [
"index.js",
"dist"
],
"exports": {
".": {
"types": "./dist/vue.d.ts",
"node": {
"production": "./dist/vue.cjs.prod.js",
"development": "./dist/vue.cjs.js",
"default": "./index.js"
},
"module": "./dist/vue.esm-bundler.js",
"import": "./dist/vue.esm-bundler.js",
"require": "./index.js"
},
"./*": "./*"
},
"buildOptions": {
"name": "Vue",
"filename": "vue",
"compat": true,
"formats": [
"esm-bundler",
"esm-bundler-runtime",
"cjs",
"global",
"global-runtime",
"esm-browser",
"esm-browser-runtime"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/core.git"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/core/issues"
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/vue-compat#readme",
"dependencies": {
"@babel/parser": "catalog:",
"entities": "catalog:",
"estree-walker": "catalog:",
"source-map-js": "catalog:"
},
"peerDependencies": {
"vue": "workspace:*"
}
}
================================================
FILE: packages/vue-compat/src/createCompatVue.ts
================================================
// This entry exports the runtime only, and is built as
// `dist/vue.esm-bundler.js` which is used by default for bundlers.
import { initDev } from './dev'
import {
type CompatVue,
DeprecationTypes,
KeepAlive,
Transition,
TransitionGroup,
compatUtils,
createApp,
vModelDynamic,
vShow,
} from '@vue/runtime-dom'
import { extend } from '@vue/shared'
if (__DEV__) {
initDev()
}
import * as runtimeDom from '@vue/runtime-dom'
function wrappedCreateApp(...args: any[]) {
// @ts-expect-error
const app = createApp(...args)
if (compatUtils.isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, null)) {
// register built-in components so that they can be resolved via strings
// in the legacy h() call. The __compat__ prefix is to ensure that v3 h()
// doesn't get affected.
app.component('__compat__transition', Transition)
app.component('__compat__transition-group', TransitionGroup)
app.component('__compat__keep-alive', KeepAlive)
// built-in directives. No need for prefix since there's no render fn API
// for resolving directives via string in v3.
app._context.directives.show = vShow
app._context.directives.model = vModelDynamic
}
return app
}
export function createCompatVue(): CompatVue {
const Vue = compatUtils.createCompatVue(createApp, wrappedCreateApp)
extend(Vue, runtimeDom)
return Vue
}
================================================
FILE: packages/vue-compat/src/dev.ts
================================================
import { initCustomFormatter } from '@vue/runtime-dom'
export function initDev(): void {
if (__BROWSER__) {
if (!__ESM_BUNDLER__) {
console.info(
`You are running a development build of Vue.\n` +
`Make sure to use the production build (*.prod.js) when deploying for production.`,
)
}
initCustomFormatter()
}
}
================================================
FILE: packages/vue-compat/src/esm-index.ts
================================================
import Vue from './index'
export default Vue
export * from '@vue/runtime-dom'
const configureCompat: typeof Vue.configureCompat = Vue.configureCompat
export { configureCompat }
================================================
FILE: packages/vue-compat/src/esm-runtime.ts
================================================
import Vue from './runtime'
export default Vue
export * from '@vue/runtime-dom'
const configureCompat: typeof Vue.configureCompat = Vue.configureCompat
export { configureCompat }
================================================
FILE: packages/vue-compat/src/index.ts
================================================
// This entry is the "full-build" that includes both the runtime
// and the compiler, and supports on-the-fly compilation of the template option.
import { createCompatVue } from './createCompatVue'
import {
type CompilerError,
type CompilerOptions,
compile,
} from '@vue/compiler-dom'
import {
type CompatVue,
type RenderFunction,
registerRuntimeCompiler,
warn,
} from '@vue/runtime-dom'
import {
NOOP,
extend,
genCacheKey,
generateCodeFrame,
isString,
} from '@vue/shared'
import type { InternalRenderFunction } from 'packages/runtime-core/src/component'
import * as runtimeDom from '@vue/runtime-dom'
import {
DeprecationTypes,
warnDeprecation,
} from '../../runtime-core/src/compat/compatConfig'
const compileCache: Record = Object.create(null)
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions,
): RenderFunction {
if (!isString(template)) {
if (template.nodeType) {
template = template.innerHTML
} else {
__DEV__ && warn(`invalid template option: `, template)
return NOOP
}
}
const key = genCacheKey(template, options)
const cached = compileCache[key]
if (cached) {
return cached
}
if (template[0] === '#') {
const el = document.querySelector(template)
if (__DEV__ && !el) {
warn(`Template element not found or is empty: ${template}`)
}
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's rendered
// by the server, the template should not contain any user data.
template = el ? el.innerHTML : ``
}
if (__DEV__ && !__TEST__ && (!options || !options.whitespace)) {
warnDeprecation(DeprecationTypes.CONFIG_WHITESPACE, null)
}
const { code } = compile(
template,
extend(
{
hoistStatic: true,
whitespace: 'preserve',
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP,
} as CompilerOptions,
options,
),
)
function onError(err: CompilerError, asWarning = false) {
const message = asWarning
? err.message
: `Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset,
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
}
// The wildcard import results in a huge object with every export
// with keys that cannot be mangled, and can be quite heavy size-wise.
// In the global build we know `Vue` is available globally so we can avoid
// the wildcard object.
const render = (
__GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
) as RenderFunction
// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true
return (compileCache[key] = render)
}
registerRuntimeCompiler(compileToFunction)
const Vue: CompatVue = createCompatVue()
Vue.compile = compileToFunction
export default Vue
================================================
FILE: packages/vue-compat/src/runtime.ts
================================================
// This entry exports the runtime only, and is built as
// `dist/vue.esm-bundler.js` which is used by default for bundlers.
import { createCompatVue } from './createCompatVue'
import { type CompatVue, warn } from '@vue/runtime-core'
const Vue: CompatVue = createCompatVue()
Vue.compile = (() => {
if (__DEV__) {
warn(
`Runtime compilation is not supported in this build of Vue.` +
(__ESM_BUNDLER__
? ` Configure your bundler to alias "vue" to "@vue/compat/dist/vue.esm-bundler.js".`
: __ESM_BROWSER__
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */,
)
}
}) as any
export default Vue
================================================
FILE: packages-private/dts-built-test/README.md
================================================
# dts built-package test
This package is private and for testing only. It is used to verify edge cases for external libraries that build their types using Vue core types - e.g. Vuetify as in [#8376](https://github.com/vuejs/core/issues/8376).
When running the `build-dts` task, this package's types are built alongside other packages. Then, during `test-dts-only` it is imported and used in [`packages-private/dts-test/built.test-d.ts`](https://github.com/vuejs/core/blob/main/packages-private/dts-test/built.test-d.ts) to verify that the built types work correctly.
================================================
FILE: packages-private/dts-built-test/package.json
================================================
{
"name": "dts-built-test",
"private": true,
"version": "0.0.0",
"types": "dist/index.d.ts",
"dependencies": {
"@vue/shared": "workspace:*",
"@vue/reactivity": "workspace:*",
"vue": "workspace:*"
}
}
================================================
FILE: packages-private/dts-built-test/src/index.ts
================================================
import { defineComponent } from 'vue'
const _CustomPropsNotErased = defineComponent({
props: {},
setup() {},
})
// #8376
export const CustomPropsNotErased =
_CustomPropsNotErased as typeof _CustomPropsNotErased & {
foo: string
}
================================================
FILE: packages-private/dts-built-test/tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"jsx": "preserve",
"module": "esnext",
"strict": true,
"moduleResolution": "Bundler",
"lib": ["esnext", "dom"],
"declaration": true,
"emitDeclarationOnly": true
},
"include": ["./src"]
}
================================================
FILE: packages-private/dts-test/README.md
================================================
# dts-test
Tests TypeScript types to ensure the types remain as expected.
- This directory is included in the root `tsconfig.json`, where package imports are aliased to `src` directories, so in IDEs and the `pnpm check` script the types are validated against source code.
- When running `tsc` with `packages-private/dts-test/tsconfig.test.json`, packages are resolved using normal `node` resolution, so the types are validated against actual **built** types. This requires the types to be built first via `pnpm build-dts`.
================================================
FILE: packages-private/dts-test/appDirective.test-d.ts
================================================
import { createApp } from 'vue'
import { expectType } from './utils'
const app = createApp({})
app.directive(
'custom',
{
mounted(el, binding) {
expectType(el)
expectType(binding.value)
expectType<{ prevent?: boolean; stop?: boolean }>(binding.modifiers)
expectType<'arg1' | 'arg2'>(binding.arg!)
// @ts-expect-error not any
expectType(binding.value)
},
},
)
================================================
FILE: packages-private/dts-test/appUse.test-d.ts
================================================
import { type App, type Plugin, createApp, defineComponent } from 'vue'
const app = createApp({})
// Plugin without types accept anything
const PluginWithoutType: Plugin = {
install(app: App) {},
}
app.use(PluginWithoutType)
app.use(PluginWithoutType, 2)
app.use(PluginWithoutType, { anything: 'goes' }, true)
type PluginOptions = {
/** option1 */
option1?: string
/** option2 */
option2: number
/** option3 */
option3: boolean
}
const PluginWithObjectOptions = {
install(app: App, options: PluginOptions) {
options.option1
options.option2
options.option3
},
}
const objectPluginOptional = {
install(app: App, options?: PluginOptions) {},
}
app.use(objectPluginOptional)
app.use(
objectPluginOptional,
// Test JSDoc and `go to definition` for options
{
option1: 'foo',
option2: 1,
option3: true,
},
)
for (const Plugin of [
PluginWithObjectOptions,
PluginWithObjectOptions.install,
]) {
// @ts-expect-error: no params
app.use(Plugin)
// @ts-expect-error option2 and option3 (required) missing
app.use(Plugin, {})
// @ts-expect-error type mismatch
app.use(Plugin, undefined)
// valid options
app.use(Plugin, { option2: 1, option3: true })
app.use(Plugin, { option1: 'foo', option2: 1, option3: true })
}
const PluginNoOptions = {
install(app: App) {},
}
for (const Plugin of [PluginNoOptions, PluginNoOptions.install]) {
// no args
app.use(Plugin)
// @ts-expect-error unexpected plugin option
app.use(Plugin, {})
// @ts-expect-error only no options is valid
app.use(Plugin, undefined)
}
const PluginMultipleArgs = {
install: (app: App, a: string, b: number) => {},
}
for (const Plugin of [PluginMultipleArgs, PluginMultipleArgs.install]) {
// @ts-expect-error: 2 arguments expected
app.use(Plugin, 'hey')
app.use(Plugin, 'hey', 2)
}
const PluginOptionalOptions = {
install(
app: App,
options: PluginOptions = { option2: 2, option3: true, option1: 'foo' },
) {
options.option1
options.option2
options.option3
},
}
for (const Plugin of [PluginOptionalOptions, PluginOptionalOptions.install]) {
// both version are valid
app.use(Plugin)
app.use(Plugin, undefined)
// @ts-expect-error option2 and option3 (required) missing
app.use(Plugin, {})
// valid options
app.use(Plugin, { option2: 1, option3: true })
app.use(Plugin, { option1: 'foo', option2: 1, option3: true })
}
// still valid but it's better to use the regular function because this one can accept an optional param
const PluginTyped: Plugin = (app, options) => {}
// @ts-expect-error: needs options
app.use(PluginTyped)
app.use(
PluginTyped,
// Test autocomplete for options
{
option1: '',
option2: 2,
option3: true,
},
)
const functionPluginOptional = (app: App, options?: PluginOptions) => {}
app.use(functionPluginOptional)
app.use(functionPluginOptional, { option2: 2, option3: true })
// type optional params
const functionPluginOptional2: Plugin<[options?: PluginOptions]> = (
app,
options,
) => {}
app.use(functionPluginOptional2)
app.use(functionPluginOptional2, { option2: 2, option3: true })
// vuetify usage
const key: string = ''
const aliases: Record = {}
app.component(
key,
defineComponent({
...aliases[key],
name: key,
aliasName: aliases[key].name,
}),
)
================================================
FILE: packages-private/dts-test/built.test-d.ts
================================================
import { CustomPropsNotErased } from 'dts-built-test/src/index'
import { describe, expectType } from './utils'
declare module 'vue' {
interface ComponentCustomProps {
custom?: number
}
}
// #8376 - custom props should not be erased
describe('Custom Props not erased', () => {
expectType(new CustomPropsNotErased().$props.custom)
})
================================================
FILE: packages-private/dts-test/compiler.test-d.ts
================================================
import {
Comment,
Fragment,
Static,
Suspense,
Teleport,
Text,
type VNode,
createBlock,
defineComponent,
} from 'vue'
import { expectType } from './utils'
expectType(createBlock(Teleport))
expectType(createBlock(Text))
expectType(createBlock(Static))
expectType(createBlock(Comment))
expectType