'
+ }
+ 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))
+ })
+
+ // #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'])
+ })
+
+ 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 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']
+ })
+ })
+ })
+
+ 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']
+ })
+ })
+ })
+})
+
+function resolve(
+ code: string,
+ files: Record = {},
+ options?: Partial
+) {
+ const { descriptor } = parse(``, {
+ filename: '/Test.vue'
+ })
+ const ctx = new ScriptCompileContext(descriptor, {
+ id: 'test',
+ fs: {
+ fileExists(file) {
+ return !!files[file]
+ },
+ readFile(file) {
+ return files[file]
+ }
+ },
+ ...options
+ })
+
+ 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
+ }
+}
diff --git a/packages/compiler-sfc/__tests__/parse.spec.ts b/packages/compiler-sfc/__tests__/parse.spec.ts
index 5f1db5e2499..c7a17ab1739 100644
--- a/packages/compiler-sfc/__tests__/parse.spec.ts
+++ b/packages/compiler-sfc/__tests__/parse.spec.ts
@@ -1,6 +1,6 @@
import { parse } from '../src'
import { baseParse, baseCompile } from '@vue/compiler-core'
-import { SourceMapConsumer } from 'source-map'
+import { SourceMapConsumer } from 'source-map-js'
describe('compiler:sfc', () => {
describe('source map', () => {
diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json
index 8c97d3b3b9b..33a8c40d185 100644
--- a/packages/compiler-sfc/package.json
+++ b/packages/compiler-sfc/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
- "version": "3.3.0-alpha.9",
+ "version": "3.3.4",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@@ -33,15 +33,15 @@
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": {
"@babel/parser": "^7.20.15",
- "@vue/compiler-core": "3.3.0-alpha.9",
- "@vue/compiler-dom": "3.3.0-alpha.9",
- "@vue/compiler-ssr": "3.3.0-alpha.9",
- "@vue/reactivity-transform": "3.3.0-alpha.9",
- "@vue/shared": "3.3.0-alpha.9",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/reactivity-transform": "3.3.4",
+ "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0",
"postcss": "^8.1.10",
- "source-map": "^0.6.1"
+ "source-map-js": "^1.0.2"
},
"devDependencies": {
"@babel/types": "^7.21.3",
@@ -51,6 +51,7 @@
"hash-sum": "^2.0.0",
"lru-cache": "^5.1.1",
"merge-source-map": "^1.1.0",
+ "minimatch": "^9.0.0",
"postcss-modules": "^4.0.0",
"postcss-selector-parser": "^6.0.4",
"pug": "^3.0.1",
diff --git a/packages/compiler-sfc/src/cache.ts b/packages/compiler-sfc/src/cache.ts
index 510dfee3547..36d240810c7 100644
--- a/packages/compiler-sfc/src/cache.ts
+++ b/packages/compiler-sfc/src/cache.ts
@@ -1,7 +1,11 @@
import LRU from 'lru-cache'
-export function createCache(size = 500) {
- return __GLOBAL__ || __ESM_BROWSER__
- ? new Map()
- : (new LRU(size) as any as Map)
+export function createCache(size = 500): Map & { max?: number } {
+ if (__GLOBAL__ || __ESM_BROWSER__) {
+ return new Map()
+ }
+ const cache = new LRU(size)
+ // @ts-expect-error
+ cache.delete = cache.del.bind(cache)
+ return cache as any as Map
}
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 35e690bb00e..6f096ff3e04 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -1,78 +1,58 @@
-import MagicString from 'magic-string'
import {
- BindingMetadata,
BindingTypes,
- createRoot,
- NodeTypes,
- transform,
- parserOptions,
UNREF,
- SimpleExpressionNode,
isFunctionType,
- walkIdentifiers,
- getImportedName,
- unwrapTSNode,
- isCallOf
+ walkIdentifiers
} from '@vue/compiler-dom'
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
-import {
- parse as _parse,
- parseExpression,
- ParserOptions,
- ParserPlugin
-} from '@babel/parser'
-import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared'
+import { ParserPlugin } from '@babel/parser'
+import { generateCodeFrame } from '@vue/shared'
import {
Node,
Declaration,
ObjectPattern,
- ObjectExpression,
ArrayPattern,
Identifier,
ExportSpecifier,
- TSType,
- TSTypeLiteral,
- TSFunctionType,
- ObjectProperty,
- ArrayExpression,
Statement,
- CallExpression,
- RestElement,
- TSInterfaceBody,
- TSTypeElement,
- AwaitExpression,
- Program,
- ObjectMethod,
- LVal,
- Expression,
- TSEnumDeclaration
+ CallExpression
} from '@babel/types'
import { walk } from 'estree-walker'
-import { RawSourceMap } from 'source-map'
+import { RawSourceMap } from 'source-map-js'
import {
- CSS_VARS_HELPER,
- genCssVarsCode,
- genNormalScriptCssVarsCode
-} from './style/cssVars'
+ processNormalScript,
+ normalScriptDefaultVar
+} from './script/normalScript'
+import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { warnOnce } from './warn'
-import { rewriteDefaultAST } from './rewriteDefault'
-import { createCache } from './cache'
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
-import { transformDestructuredProps } from './script/propsDestructure'
-
-// Special compiler macros
-const DEFINE_PROPS = 'defineProps'
-const DEFINE_EMITS = 'defineEmits'
-const DEFINE_EXPOSE = 'defineExpose'
-const WITH_DEFAULTS = 'withDefaults'
-const DEFINE_OPTIONS = 'defineOptions'
-const DEFINE_SLOTS = 'defineSlots'
-const DEFINE_MODEL = 'defineModel'
-
-const isBuiltInDir = makeMap(
- `once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
-)
+import { transformDestructuredProps } from './script/definePropsDestructure'
+import { ScriptCompileContext } from './script/context'
+import {
+ processDefineProps,
+ genRuntimeProps,
+ DEFINE_PROPS,
+ WITH_DEFAULTS
+} from './script/defineProps'
+import {
+ processDefineEmits,
+ genRuntimeEmits,
+ DEFINE_EMITS
+} from './script/defineEmits'
+import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
+import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
+import { processDefineSlots } from './script/defineSlots'
+import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
+import {
+ isLiteralNode,
+ unwrapTSNode,
+ isCallOf,
+ getImportedName
+} from './script/utils'
+import { analyzeScriptBindings } from './script/analyzeScriptBindings'
+import { isImportUsed } from './script/importUsageCheck'
+import { processAwait } from './script/topLevelAwait'
export interface SFCScriptCompileOptions {
/**
@@ -93,13 +73,10 @@ export interface SFCScriptCompileOptions {
*/
babelParserPlugins?: ParserPlugin[]
/**
- * (Experimental) Enable syntax transform for using refs without `.value` and
- * using destructured props with reactivity
- * @deprecated the Reactivity Transform proposal has been dropped. This
- * feature will be removed from Vue core in 3.4. If you intend to continue
- * using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html).
+ * A list of files to parse for global types to be made available for type
+ * resolving in SFC macros. The list must be fully resolved file system paths.
*/
- reactivityTransform?: boolean
+ globalTypeFiles?: string[]
/**
* Compile the template and inline the resulting render function
* directly inside setup().
@@ -128,8 +105,31 @@ export interface SFCScriptCompileOptions {
hoistStatic?: boolean
/**
* (**Experimental**) Enable macro `defineModel`
+ * @default false
*/
defineModel?: boolean
+ /**
+ * (**Experimental**) Enable reactive destructure for `defineProps`
+ * @default false
+ */
+ propsDestructure?: boolean
+ /**
+ * File system access methods to be used when resolving types
+ * imported in SFC macros. Defaults to ts.sys in Node.js, can be overwritten
+ * to use a virtual file system for use in browsers (e.g. in REPLs)
+ */
+ fs?: {
+ fileExists(file: string): boolean
+ readFile(file: string): string | undefined
+ }
+ /**
+ * (Experimental) Enable syntax transform for using refs without `.value` and
+ * using destructured props with reactivity
+ * @deprecated the Reactivity Transform proposal has been dropped. This
+ * feature will be removed from Vue core in 3.4. If you intend to continue
+ * using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html).
+ */
+ reactivityTransform?: boolean
}
export interface ImportBinding {
@@ -141,25 +141,6 @@ export interface ImportBinding {
isUsedInTemplate: boolean
}
-export type PropsDestructureBindings = Record<
- string, // public prop key
- {
- local: string // local identifier, may be different
- default?: Expression
- }
->
-
-type FromNormalScript = T & { __fromNormalScript?: boolean | null }
-type PropsDeclType = FromNormalScript
-type EmitsDeclType = FromNormalScript<
- TSFunctionType | TSTypeLiteral | TSInterfaceBody
->
-interface ModelDecl {
- type: TSType | undefined
- options: string | undefined
- identifier: string | undefined
-}
-
/**
* Compile `