diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts index 7d96ec51928..4b8f4182d2c 100644 --- a/packages/compiler-core/src/babelUtils.ts +++ b/packages/compiler-core/src/babelUtils.ts @@ -6,10 +6,7 @@ import type { Function, ObjectProperty, BlockStatement, - Program, - ImportDefaultSpecifier, - ImportNamespaceSpecifier, - ImportSpecifier + Program } from '@babel/types' import { walk } from 'estree-walker' @@ -246,17 +243,6 @@ export const isStaticProperty = (node: Node): node is ObjectProperty => export const isStaticPropertyKey = (node: Node, parent: Node) => isStaticProperty(parent) && parent.key === node -export function getImportedName( - specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier -) { - if (specifier.type === 'ImportSpecifier') - return specifier.imported.type === 'Identifier' - ? specifier.imported.name - : specifier.imported.value - else if (specifier.type === 'ImportNamespaceSpecifier') return '*' - return 'default' -} - /** * Copied from https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/isReferenced.ts * To avoid runtime dependency on @babel/types (which includes process references) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 38bcdf988ee..7aa08d595f5 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1,20 +1,26 @@ -import { TSTypeAliasDeclaration } from '@babel/types' +import { Identifier } from '@babel/types' import { parse } from '../../src' import { ScriptCompileContext } from '../../src/script/context' import { inferRuntimeType, - resolveTypeElements + invalidateTypeCache, + recordImports, + resolveTypeElements, + registerTS } from '../../src/script/resolveType' +import ts from 'typescript' +registerTS(ts) + describe('resolveType', () => { test('type literal', () => { - const { props, calls } = resolve(`type Target = { + const { props, calls } = resolve(`defineProps<{ foo: number // property bar(): void // method 'baz': string // string literal key (e: 'foo'): void // call signature (e: 'bar'): void - }`) + }>()`) expect(props).toStrictEqual({ foo: ['Number'], bar: ['Function'], @@ -27,7 +33,7 @@ describe('resolveType', () => { expect( resolve(` type Aliased = { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -38,7 +44,7 @@ describe('resolveType', () => { expect( resolve(` export type Aliased = { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -49,7 +55,7 @@ describe('resolveType', () => { expect( resolve(` interface Aliased { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -60,7 +66,7 @@ describe('resolveType', () => { expect( resolve(` export interface Aliased { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ foo: ['Number'] @@ -74,7 +80,7 @@ describe('resolveType', () => { export interface B extends A { b: boolean } interface C { c: string } interface Aliased extends B, C { foo: number } - type Target = Aliased + defineProps() `).props ).toStrictEqual({ a: ['Function'], @@ -84,10 +90,21 @@ describe('resolveType', () => { }) }) + test('reference class', () => { + expect( + resolve(` + class Foo {} + defineProps<{ foo: Foo }>() + `).props + ).toStrictEqual({ + foo: ['Object'] + }) + }) + test('function type', () => { expect( resolve(` - type Target = (e: 'foo') => void + defineProps<(e: 'foo') => void>() `).calls?.length ).toBe(1) }) @@ -96,7 +113,7 @@ describe('resolveType', () => { expect( resolve(` type Fn = (e: 'foo') => void - type Target = Fn + defineProps() `).calls?.length ).toBe(1) }) @@ -107,7 +124,7 @@ describe('resolveType', () => { type Foo = { foo: number } type Bar = { bar: string } type Baz = { bar: string | boolean } - type Target = { self: any } & Foo & Bar & Baz + defineProps<{ self: any } & Foo & Bar & Baz>() `).props ).toStrictEqual({ self: ['Unknown'], @@ -137,7 +154,7 @@ describe('resolveType', () => { note: string } - type Target = CommonProps & ConditionalProps + defineProps() `).props ).toStrictEqual({ size: ['String'], @@ -152,9 +169,9 @@ describe('resolveType', () => { resolve(` type T = 'foo' | 'bar' type S = 'x' | 'y' - type Target = { + defineProps<{ [\`_\${T}_\${S}_\`]: string - } + }>() `).props ).toStrictEqual({ _foo_x_: ['String'], @@ -168,7 +185,7 @@ describe('resolveType', () => { expect( resolve(` type T = 'foo' | 'bar' - type Target = { [K in T]: string | number } & { + defineProps<{ [K in T]: string | number } & { [K in 'optional']?: boolean } & { [K in Capitalize]: string @@ -176,7 +193,7 @@ describe('resolveType', () => { [K in Uppercase>]: string } & { [K in \`x\${T}\`]: string - } + }>() `).props ).toStrictEqual({ foo: ['String', 'Number'], @@ -195,7 +212,7 @@ describe('resolveType', () => { resolve(` type T = { foo: number, bar: string, baz: boolean } type K = 'foo' | 'bar' - type Target = Pick + defineProps>() `).props ).toStrictEqual({ foo: ['Number'], @@ -208,25 +225,56 @@ describe('resolveType', () => { resolve(` type T = { foo: number, bar: string, baz: boolean } type K = 'foo' | 'bar' - type Target = Omit + defineProps>() `).props ).toStrictEqual({ baz: ['Boolean'] }) }) - test('indexed access type', () => { + test('indexed access type (literal)', () => { expect( resolve(` type T = { bar: number } type S = { nested: { foo: T['bar'] }} - type Target = S['nested'] + defineProps() `).props ).toStrictEqual({ foo: ['Number'] }) }) + test('indexed access type (advanced)', () => { + expect( + resolve(` + type K = 'foo' | 'bar' + type T = { foo: string, bar: number } + type S = { foo: { foo: T[string] }, bar: { bar: string } } + defineProps() + `).props + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String'] + }) + }) + + test('indexed access type (number)', () => { + expect( + resolve(` + type A = (string | number)[] + type AA = Array + type T = [1, 'foo'] + type TT = [foo: 1, bar: 'foo'] + defineProps<{ foo: A[number], bar: AA[number], tuple: T[number], namedTuple: TT[number] }>() + `).props + ).toStrictEqual({ + foo: ['String', 'Number'], + bar: ['String'], + tuple: ['Number', 'String'], + namedTuple: ['Number', 'String'] + }) + }) + test('namespace', () => { expect( resolve(` @@ -239,29 +287,198 @@ describe('resolveType', () => { } } } - type Target = Foo.Bar.A + defineProps() `).props ).toStrictEqual({ foo: ['Number'] }) }) + describe('external type imports', () => { + const files = { + '/foo.ts': 'export type P = { foo: number }', + '/bar.d.ts': 'type X = { bar: string }; export { X as Y }' + } + test('relative ts', () => { + const { props, deps } = resolve( + ` + import { P } from './foo' + import { Y as PP } from './bar' + defineProps

() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['Number'], + bar: ['String'] + }) + expect(deps && [...deps]).toStrictEqual(Object.keys(files)) + }) + + 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('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' + ]) + }) + }) + describe('errors', () => { - test('error on computed keys', () => { - expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( - `computed keys are not supported in types referenced by SFC macros` + 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 improt source resolve', () => { + expect(() => + resolve(`import { X } from './foo'; defineProps()`) + ).toThrow(`Failed to resolve import source "./foo" for type X`) + }) + + 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() + }) }) }) -function resolve(code: string) { - const { descriptor } = parse(``) - const ctx = new ScriptCompileContext(descriptor, { id: 'test' }) - const targetDecl = ctx.scriptSetupAst!.body.find( - s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' - ) as TSTypeAliasDeclaration - const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation) +function resolve(code: string, files: Record = {}) { + const { descriptor } = parse(``, { + filename: '/Test.vue' + }) + const ctx = new ScriptCompileContext(descriptor, { + id: 'test', + fs: { + fileExists(file) { + return !!files[file] + }, + readFile(file) { + return files[file] + } + } + }) + + 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]) @@ -269,6 +486,6 @@ function resolve(code: string) { return { props, calls: raw.calls, - raw + deps: ctx.deps } } diff --git a/packages/compiler-sfc/src/cache.ts b/packages/compiler-sfc/src/cache.ts index 510dfee3547..eb6bad0f86d 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) + 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 c828d77f6d5..e5e1bea4fd1 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -2,8 +2,7 @@ import { BindingTypes, UNREF, isFunctionType, - walkIdentifiers, - getImportedName + walkIdentifiers } from '@vue/compiler-dom' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse' import { parse as _parse, ParserPlugin } from '@babel/parser' @@ -45,7 +44,12 @@ 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 } from './script/utils' +import { + isLiteralNode, + unwrapTSNode, + isCallOf, + getImportedName +} from './script/utils' import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { isImportUsed } from './script/importUsageCheck' import { processAwait } from './script/topLevelAwait' @@ -106,6 +110,15 @@ export interface SFCScriptCompileOptions { * (**Experimental**) Enable macro `defineModel` */ defineModel?: 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 + } } export interface ImportBinding { @@ -1022,7 +1035,8 @@ export function compileScript( }) as unknown as RawSourceMap) : undefined, scriptAst: scriptAst?.body, - scriptSetupAst: scriptSetupAst?.body + scriptSetupAst: scriptSetupAst?.body, + deps: ctx.deps ? [...ctx.deps] : undefined } } diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index 6ba097b2466..e171ac0885c 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -28,6 +28,9 @@ export { isStaticProperty } from '@vue/compiler-core' +// Internals for type resolution +export { invalidateTypeCache, registerTS } from './script/resolveType' + // Types export type { SFCParseOptions, diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts index b46c5ea1332..29c91cc1977 100644 --- a/packages/compiler-sfc/src/parse.ts +++ b/packages/compiler-sfc/src/parse.ts @@ -47,6 +47,13 @@ export interface SFCScriptBlock extends SFCBlock { imports?: Record scriptAst?: import('@babel/types').Statement[] scriptSetupAst?: import('@babel/types').Statement[] + warnings?: string[] + /** + * Fully resolved dependency file paths (unix slashes) with imported types + * used in macros, used for HMR cache busting in @vitejs/plugin-vue and + * vue-loader. + */ + deps?: string[] } export interface SFCStyleBlock extends SFCBlock { diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 718f23da5ca..9141b95c572 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -1,7 +1,7 @@ import { Node, ObjectPattern, Program } from '@babel/types' import { SFCDescriptor } from '../parse' import { generateCodeFrame } from '@vue/shared' -import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser' +import { parse as babelParse, ParserPlugin } from '@babel/parser' import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' import { PropsDestructureBindings } from './defineProps' import { ModelDecl } from './defineModel' @@ -56,13 +56,17 @@ export class ScriptCompileContext { // codegen bindingMetadata: BindingMetadata = {} - helperImports: Set = new Set() helper(key: string): string { this.helperImports.add(key) return `_${key}` } + /** + * to be exposed on compiled script block for HMR cache busting + */ + deps?: Set + constructor( public descriptor: SFCDescriptor, public options: SFCScriptCompileOptions @@ -83,31 +87,17 @@ export class ScriptCompileContext { scriptSetupLang === 'tsx' // resolve parser plugins - const plugins: ParserPlugin[] = [] - if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') { - plugins.push('jsx') - } else { - // If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins - if (options.babelParserPlugins) - options.babelParserPlugins = options.babelParserPlugins.filter( - n => n !== 'jsx' - ) - } - if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins) - if (this.isTS) { - plugins.push('typescript') - if (!plugins.includes('decorators')) { - plugins.push('decorators-legacy') - } - } + const plugins: ParserPlugin[] = resolveParserPlugins( + (scriptLang || scriptSetupLang)!, + options.babelParserPlugins + ) - function parse( - input: string, - options: ParserOptions, - offset: number - ): Program { + function parse(input: string, offset: number): Program { try { - return babelParse(input, options).program + return babelParse(input, { + plugins, + sourceType: 'module' + }).program } catch (e: any) { e.message = `[@vue/compiler-sfc] ${e.message}\n\n${ descriptor.filename @@ -124,23 +114,12 @@ export class ScriptCompileContext { this.descriptor.script && parse( this.descriptor.script.content, - { - plugins, - sourceType: 'module' - }, this.descriptor.script.loc.start.offset ) this.scriptSetupAst = this.descriptor.scriptSetup && - parse( - this.descriptor.scriptSetup!.content, - { - plugins: [...plugins, 'topLevelAwait'], - sourceType: 'module' - }, - this.startOffset! - ) + parse(this.descriptor.scriptSetup!.content, this.startOffset!) } getString(node: Node, scriptSetup = true): string { @@ -150,19 +129,40 @@ export class ScriptCompileContext { return block.content.slice(node.start!, node.end!) } - error( - msg: string, - node: Node, - end: number = node.end! + this.startOffset! - ): never { + error(msg: string, node: Node, scope?: TypeScope): never { + const offset = scope ? scope.offset : this.startOffset! throw new Error( `[@vue/compiler-sfc] ${msg}\n\n${ - this.descriptor.filename + (scope || this.descriptor).filename }\n${generateCodeFrame( - this.descriptor.source, - node.start! + this.startOffset!, - end + (scope || this.descriptor).source, + node.start! + offset, + node.end! + offset )}` ) } } + +export function resolveParserPlugins( + lang: string, + userPlugins?: ParserPlugin[] +) { + const plugins: ParserPlugin[] = [] + if (lang === 'jsx' || lang === 'tsx') { + plugins.push('jsx') + } else if (userPlugins) { + // If don't match the case of adding jsx + // should remove the jsx from user options + userPlugins = userPlugins.filter(p => p !== 'jsx') + } + if (lang === 'ts' || lang === 'tsx') { + plugins.push('typescript') + if (!plugins.includes('decorators')) { + plugins.push('decorators-legacy') + } + } + if (userPlugins) { + plugins.push(...userPlugins) + } + return plugins +} diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index f3b20a7ee15..f1fce65a287 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -1,11 +1,14 @@ import { + Expression, Identifier, - Node as _Node, + Node, Statement, TSCallSignatureDeclaration, TSEnumDeclaration, TSExpressionWithTypeArguments, TSFunctionType, + TSIndexedAccessType, + TSInterfaceDeclaration, TSMappedType, TSMethodSignature, TSModuleBlock, @@ -18,96 +21,114 @@ import { TSTypeReference, TemplateLiteral } from '@babel/types' -import { UNKNOWN_TYPE } from './utils' -import { ScriptCompileContext } from './context' -import { ImportBinding } from '../compileScript' -import { TSInterfaceDeclaration } from '@babel/types' +import { + UNKNOWN_TYPE, + createGetCanonicalFileName, + getId, + getImportedName +} from './utils' +import { ScriptCompileContext, resolveParserPlugins } from './context' +import { ImportBinding, SFCScriptCompileOptions } from '../compileScript' import { capitalize, hasOwn } from '@vue/shared' -import { Expression } from '@babel/types' +import { parse as babelParse } from '@babel/parser' +import { parse } from '../parse' +import { createCache } from '../cache' +import type TS from 'typescript' +import { join, extname, dirname } from 'path' + +type Import = Pick export interface TypeScope { filename: string - imports: Record - types: Record - parent?: TypeScope + source: string + offset: number + imports: Record + types: Record< + string, + Node & { + // scope types always has ownerScope attached + _ownerScope: TypeScope + } + > + exportedTypes: Record< + string, + Node & { + // scope types always has ownerScope attached + _ownerScope: TypeScope + } + > } -interface WithScope { +export interface WithScope { _ownerScope?: TypeScope } interface ResolvedElements { - props: Record + props: Record< + string, + (TSPropertySignature | TSMethodSignature) & { + // resolved props always has ownerScope attached + _ownerScope: TypeScope + } + > calls?: (TSCallSignatureDeclaration | TSFunctionType)[] } -type Node = _Node & - WithScope & { - _resolvedElements?: ResolvedElements - } - /** * Resolve arbitrary type node to a list of type elements that can be then * mapped to runtime props or emits. */ export function resolveTypeElements( ctx: ScriptCompileContext, - node: Node + node: Node & WithScope & { _resolvedElements?: ResolvedElements }, + scope?: TypeScope ): ResolvedElements { if (node._resolvedElements) { return node._resolvedElements } - return (node._resolvedElements = innerResolveTypeElements(ctx, node)) + return (node._resolvedElements = innerResolveTypeElements( + ctx, + node, + node._ownerScope || scope || ctxToScope(ctx) + )) } function innerResolveTypeElements( ctx: ScriptCompileContext, - node: Node + node: Node, + scope: TypeScope ): ResolvedElements { switch (node.type) { case 'TSTypeLiteral': - return typeElementsToMap(ctx, node.members, node._ownerScope) + return typeElementsToMap(ctx, node.members, scope) case 'TSInterfaceDeclaration': - return resolveInterfaceMembers(ctx, node) + return resolveInterfaceMembers(ctx, node, scope) case 'TSTypeAliasDeclaration': case 'TSParenthesizedType': - return resolveTypeElements(ctx, node.typeAnnotation) + return resolveTypeElements(ctx, node.typeAnnotation, scope) case 'TSFunctionType': { return { props: {}, calls: [node] } } case 'TSUnionType': case 'TSIntersectionType': return mergeElements( - node.types.map(t => resolveTypeElements(ctx, t)), + node.types.map(t => resolveTypeElements(ctx, t, scope)), node.type ) case 'TSMappedType': - return resolveMappedType(ctx, node) + return resolveMappedType(ctx, node, scope) case 'TSIndexedAccessType': { - if ( - node.indexType.type === 'TSLiteralType' && - node.indexType.literal.type === 'StringLiteral' - ) { - const resolved = resolveTypeElements(ctx, node.objectType) - const key = node.indexType.literal.value - const targetType = resolved.props[key].typeAnnotation - if (targetType) { - return resolveTypeElements(ctx, targetType.typeAnnotation) - } else { - break - } - } else { - ctx.error( - `Unsupported index type: ${node.indexType.type}`, - node.indexType - ) - } + const types = resolveIndexType(ctx, node, scope) + return mergeElements( + types.map(t => resolveTypeElements(ctx, t, t._ownerScope)), + 'TSUnionType' + ) } case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSTypeReference': { - const resolved = resolveTypeReference(ctx, node) + const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return resolveTypeElements(ctx, resolved) + return resolveTypeElements(ctx, resolved, resolved._ownerScope) } else { const typeName = getReferenceName(node) if ( @@ -115,16 +136,17 @@ function innerResolveTypeElements( // @ts-ignore SupportedBuiltinsSet.has(typeName) ) { - return resolveBuiltin(ctx, node, typeName as any) + return resolveBuiltin(ctx, node, typeName as any, scope) } ctx.error( - `Failed to resolved type reference, or unsupported built-in utlility type.`, - node + `Unresolvable type reference or unsupported built-in utlility type`, + node, + scope ) } } } - ctx.error(`Unresolvable type in SFC macro: ${node.type}`, node) + ctx.error(`Unresolvable type: ${node.type}`, node, scope) } function typeElementsToMap( @@ -135,23 +157,19 @@ function typeElementsToMap( const res: ResolvedElements = { props: {} } for (const e of elements) { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { - ;(e as Node)._ownerScope = scope - const name = - e.key.type === 'Identifier' - ? e.key.name - : e.key.type === 'StringLiteral' - ? e.key.value - : null + ;(e as WithScope)._ownerScope = scope + const name = getId(e.key) if (name && !e.computed) { - res.props[name] = e + res.props[name] = e as ResolvedElements['props'][string] } else if (e.key.type === 'TemplateLiteral') { - for (const key of resolveTemplateKeys(ctx, e.key)) { - res.props[key] = e + for (const key of resolveTemplateKeys(ctx, e.key, scope)) { + res.props[key] = e as ResolvedElements['props'][string] } } else { ctx.error( - `computed keys are not supported in types referenced by SFC macros.`, - e + `Unsupported computed key in type referenced by a macro`, + e.key, + scope ) } } else if (e.type === 'TSCallSignatureDeclaration') { @@ -165,6 +183,7 @@ function mergeElements( maps: ResolvedElements[], type: 'TSUnionType' | 'TSIntersectionType' ): ResolvedElements { + if (maps.length === 1) return maps[0] const res: ResolvedElements = { props: {} } const { props: baseProps } = res for (const { props, calls } of maps) { @@ -172,11 +191,15 @@ function mergeElements( if (!hasOwn(baseProps, key)) { baseProps[key] = props[key] } else { - baseProps[key] = createProperty(baseProps[key].key, { - type, - // @ts-ignore - types: [baseProps[key], props[key]] - }) + baseProps[key] = createProperty( + baseProps[key].key, + { + type, + // @ts-ignore + types: [baseProps[key], props[key]] + }, + baseProps[key]._ownerScope + ) } } if (calls) { @@ -188,8 +211,9 @@ function mergeElements( function createProperty( key: Expression, - typeAnnotation: TSType -): TSPropertySignature { + typeAnnotation: TSType, + scope: TypeScope +): TSPropertySignature & { _ownerScope: TypeScope } { return { type: 'TSPropertySignature', key, @@ -197,18 +221,20 @@ function createProperty( typeAnnotation: { type: 'TSTypeAnnotation', typeAnnotation - } + }, + _ownerScope: scope } } function resolveInterfaceMembers( ctx: ScriptCompileContext, - node: TSInterfaceDeclaration & WithScope + node: TSInterfaceDeclaration & WithScope, + scope: TypeScope ): ResolvedElements { const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) if (node.extends) { for (const ext of node.extends) { - const { props } = resolveTypeElements(ctx, ext) + const { props } = resolveTypeElements(ctx, ext, scope) for (const key in props) { if (!hasOwn(base.props, key)) { base.props[key] = props[key] @@ -221,44 +247,107 @@ function resolveInterfaceMembers( function resolveMappedType( ctx: ScriptCompileContext, - node: TSMappedType + node: TSMappedType, + scope: TypeScope ): ResolvedElements { const res: ResolvedElements = { props: {} } - if (!node.typeParameter.constraint) { - ctx.error(`mapped type used in macros must have a finite constraint.`, node) - } - const keys = resolveStringType(ctx, node.typeParameter.constraint) + const keys = resolveStringType(ctx, node.typeParameter.constraint!, scope) for (const key of keys) { res.props[key] = createProperty( { type: 'Identifier', name: key }, - node.typeAnnotation! + node.typeAnnotation!, + scope ) } return res } -function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] { +function resolveIndexType( + ctx: ScriptCompileContext, + node: TSIndexedAccessType, + scope: TypeScope +): (TSType & WithScope)[] { + if (node.indexType.type === 'TSNumberKeyword') { + return resolveArrayElementType(ctx, node.objectType, scope) + } + + const { indexType, objectType } = node + const types: TSType[] = [] + let keys: string[] + let resolved: ResolvedElements + if (indexType.type === 'TSStringKeyword') { + resolved = resolveTypeElements(ctx, objectType, scope) + keys = Object.keys(resolved.props) + } else { + keys = resolveStringType(ctx, indexType, scope) + resolved = resolveTypeElements(ctx, objectType, scope) + } + for (const key of keys) { + const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation + if (targetType) { + ;(targetType as TSType & WithScope)._ownerScope = + resolved.props[key]._ownerScope + types.push(targetType) + } + } + return types +} + +function resolveArrayElementType( + ctx: ScriptCompileContext, + node: Node, + scope: TypeScope +): TSType[] { + // type[] + if (node.type === 'TSArrayType') { + return [node.elementType] + } + // tuple + if (node.type === 'TSTupleType') { + return node.elementTypes.map(t => + t.type === 'TSNamedTupleMember' ? t.elementType : t + ) + } + if (node.type === 'TSTypeReference') { + // Array + if (getReferenceName(node) === 'Array' && node.typeParameters) { + return node.typeParameters.params + } else { + const resolved = resolveTypeReference(ctx, node, scope) + if (resolved) { + return resolveArrayElementType(ctx, resolved, scope) + } + } + } + ctx.error('Failed to resolve element type from target type', node) +} + +function resolveStringType( + ctx: ScriptCompileContext, + node: Node, + scope: TypeScope +): string[] { switch (node.type) { case 'StringLiteral': return [node.value] case 'TSLiteralType': - return resolveStringType(ctx, node.literal) + return resolveStringType(ctx, node.literal, scope) case 'TSUnionType': - return node.types.map(t => resolveStringType(ctx, t)).flat() + return node.types.map(t => resolveStringType(ctx, t, scope)).flat() case 'TemplateLiteral': { - return resolveTemplateKeys(ctx, node) + return resolveTemplateKeys(ctx, node, scope) } case 'TSTypeReference': { - const resolved = resolveTypeReference(ctx, node) + const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return resolveStringType(ctx, resolved) + return resolveStringType(ctx, resolved, scope) } if (node.typeName.type === 'Identifier') { const getParam = (index = 0) => - resolveStringType(ctx, node.typeParameters!.params[index]) + resolveStringType(ctx, node.typeParameters!.params[index], scope) switch (node.typeName.name) { case 'Extract': return getParam(1) @@ -275,17 +364,22 @@ function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] { case 'Uncapitalize': return getParam().map(s => s[0].toLowerCase() + s.slice(1)) default: - ctx.error('Failed to resolve type reference', node) + ctx.error( + 'Unsupported type when resolving index type', + node.typeName, + scope + ) } } } } - ctx.error('Failed to resolve string type into finite keys', node) + ctx.error('Failed to resolve index type into finite keys', node, scope) } function resolveTemplateKeys( ctx: ScriptCompileContext, - node: TemplateLiteral + node: TemplateLiteral, + scope: TypeScope ): string[] { if (!node.expressions.length) { return [node.quasis[0].value.raw] @@ -295,12 +389,16 @@ function resolveTemplateKeys( const e = node.expressions[0] const q = node.quasis[0] const leading = q ? q.value.raw : `` - const resolved = resolveStringType(ctx, e) - const restResolved = resolveTemplateKeys(ctx, { - ...node, - expressions: node.expressions.slice(1), - quasis: q ? node.quasis.slice(1) : node.quasis - }) + const resolved = resolveStringType(ctx, e, scope) + const restResolved = resolveTemplateKeys( + ctx, + { + ...node, + expressions: node.expressions.slice(1), + quasis: q ? node.quasis.slice(1) : node.quasis + }, + scope + ) for (const r of resolved) { for (const rr of restResolved) { @@ -324,7 +422,8 @@ type GetSetType = T extends Set ? V : never function resolveBuiltin( ctx: ScriptCompileContext, node: TSTypeReference | TSExpressionWithTypeArguments, - name: GetSetType + name: GetSetType, + scope: TypeScope ): ResolvedElements { const t = resolveTypeElements(ctx, node.typeParameters!.params[0]) switch (name) { @@ -333,7 +432,11 @@ function resolveBuiltin( case 'Readonly': return t case 'Pick': { - const picked = resolveStringType(ctx, node.typeParameters!.params[1]) + const picked = resolveStringType( + ctx, + node.typeParameters!.params[1], + scope + ) const res: ResolvedElements = { props: {}, calls: t.calls } for (const key of picked) { res.props[key] = t.props[key] @@ -341,7 +444,11 @@ function resolveBuiltin( return res } case 'Omit': - const omitted = resolveStringType(ctx, node.typeParameters!.params[1]) + const omitted = resolveStringType( + ctx, + node.typeParameters!.params[1], + scope + ) const res: ResolvedElements = { props: {}, calls: t.calls } for (const key in t.props) { if (!omitted.includes(key)) { @@ -357,32 +464,52 @@ function resolveTypeReference( node: (TSTypeReference | TSExpressionWithTypeArguments) & { _resolvedReference?: Node }, - scope = ctxToScope(ctx) -): Node | undefined { + scope?: TypeScope, + name?: string, + onlyExported = false +): (Node & WithScope) | undefined { if (node._resolvedReference) { return node._resolvedReference } - const name = getReferenceName(node) - return (node._resolvedReference = innerResolveTypeReference(scope, name)) + return (node._resolvedReference = innerResolveTypeReference( + ctx, + scope || ctxToScope(ctx), + name || getReferenceName(node), + node, + onlyExported + )) } function innerResolveTypeReference( + ctx: ScriptCompileContext, scope: TypeScope, - name: string | string[] + name: string | string[], + node: TSTypeReference | TSExpressionWithTypeArguments, + onlyExported: boolean ): Node | undefined { if (typeof name === 'string') { if (scope.imports[name]) { - // TODO external import - } else if (scope.types[name]) { - return scope.types[name] + return resolveTypeFromImport(ctx, node, name, scope) + } else { + const types = onlyExported ? scope.exportedTypes : scope.types + return types[name] } } else { - const ns = innerResolveTypeReference(scope, name[0]) + const ns = innerResolveTypeReference( + ctx, + scope, + name[0], + node, + onlyExported + ) if (ns && ns.type === 'TSModuleDeclaration') { const childScope = moduleDeclToScope(ns, scope) return innerResolveTypeReference( + ctx, childScope, - name.length > 2 ? name.slice(1) : name[name.length - 1] + name.length > 2 ? name.slice(1) : name[name.length - 1], + node, + true ) } } @@ -407,20 +534,256 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] { } } +let ts: typeof TS + +export function registerTS(_ts: any) { + ts = _ts +} + +type FS = NonNullable + +function resolveTypeFromImport( + ctx: ScriptCompileContext, + node: TSTypeReference | TSExpressionWithTypeArguments, + name: string, + scope: TypeScope +): Node | undefined { + const fs: FS = ctx.options.fs || ts?.sys + if (!fs) { + ctx.error( + `No fs option provided to \`compileScript\` in non-Node environment. ` + + `File system access is required for resolving imported types.`, + node + ) + } + + const containingFile = scope.filename + const { source, imported } = scope.imports[name] + + let resolved: string | undefined + + if (source.startsWith('.')) { + // relative import - fast path + const filename = join(containingFile, '..', source) + resolved = resolveExt(filename, fs) + } else { + // module or aliased import - use full TS resolution, only supported in Node + if (!__NODE_JS__) { + ctx.error( + `Type import from non-relative sources is not supported in the browser build.`, + node, + scope + ) + } + if (!ts) { + ctx.error( + `Failed to resolve type ${imported} from module ${JSON.stringify( + source + )}. ` + + `typescript is required as a peer dep for vue in order ` + + `to support resolving types from module imports.`, + node, + scope + ) + } + resolved = resolveWithTS(containingFile, source, fs) + } + + if (resolved) { + // (hmr) register dependency file on ctx + ;(ctx.deps || (ctx.deps = new Set())).add(resolved) + + return resolveTypeReference( + ctx, + node, + fileToScope(ctx, resolved, fs), + imported, + true + ) + } else { + ctx.error( + `Failed to resolve import source ${JSON.stringify( + source + )} for type ${name}`, + node, + scope + ) + } +} + +function resolveExt(filename: string, fs: FS) { + const tryResolve = (filename: string) => { + if (fs.fileExists(filename)) return filename + } + return ( + tryResolve(filename) || + tryResolve(filename + `.ts`) || + tryResolve(filename + `.d.ts`) || + tryResolve(filename + `/index.ts`) || + tryResolve(filename + `/index.d.ts`) + ) +} + +const tsConfigCache = createCache<{ + options: TS.CompilerOptions + cache: TS.ModuleResolutionCache +}>() + +function resolveWithTS( + containingFile: string, + source: string, + fs: FS +): string | undefined { + if (!__NODE_JS__) return + + // 1. resolve tsconfig.json + const configPath = ts.findConfigFile(containingFile, fs.fileExists) + // 2. load tsconfig.json + let options: TS.CompilerOptions + let cache: TS.ModuleResolutionCache | undefined + if (configPath) { + const cached = tsConfigCache.get(configPath) + if (!cached) { + // The only case where `fs` is NOT `ts.sys` is during tests. + // parse config host requires an extra `readDirectory` method + // during tests, which is stubbed. + const parseConfigHost = __TEST__ + ? { + ...fs, + useCaseSensitiveFileNames: true, + readDirectory: () => [] + } + : ts.sys + const parsed = ts.parseJsonConfigFileContent( + ts.readConfigFile(configPath, fs.readFile).config, + parseConfigHost, + dirname(configPath), + undefined, + configPath + ) + options = parsed.options + cache = ts.createModuleResolutionCache( + process.cwd(), + createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames), + options + ) + tsConfigCache.set(configPath, { options, cache }) + } else { + ;({ options, cache } = cached) + } + } else { + options = {} + } + + // 3. resolve + const res = ts.resolveModuleName(source, containingFile, options, fs, cache) + + if (res.resolvedModule) { + return res.resolvedModule.resolvedFileName + } +} + +const fileToScopeCache = createCache() + +export function invalidateTypeCache(filename: string) { + fileToScopeCache.delete(filename) + tsConfigCache.delete(filename) +} + +function fileToScope( + ctx: ScriptCompileContext, + filename: string, + fs: FS +): TypeScope { + const cached = fileToScopeCache.get(filename) + if (cached) { + return cached + } + + const source = fs.readFile(filename) || '' + const body = parseFile(ctx, filename, source) + const scope: TypeScope = { + filename, + source, + offset: 0, + types: Object.create(null), + exportedTypes: Object.create(null), + imports: recordImports(body) + } + recordTypes(body, scope) + + fileToScopeCache.set(filename, scope) + return scope +} + +function parseFile( + ctx: ScriptCompileContext, + filename: string, + content: string +): Statement[] { + const ext = extname(filename) + if (ext === '.ts' || ext === '.tsx') { + return babelParse(content, { + plugins: resolveParserPlugins( + ext.slice(1), + ctx.options.babelParserPlugins + ), + sourceType: 'module' + }).program.body + } else if (ext === '.vue') { + const { + descriptor: { script, scriptSetup } + } = parse(content) + if (!script && !scriptSetup) { + return [] + } + + // ensure the correct offset with original source + const scriptOffset = script ? script.loc.start.offset : Infinity + const scriptSetupOffset = scriptSetup + ? scriptSetup.loc.start.offset + : Infinity + const firstBlock = scriptOffset < scriptSetupOffset ? script : scriptSetup + const secondBlock = scriptOffset < scriptSetupOffset ? scriptSetup : script + + let scriptContent = + ' '.repeat(Math.min(scriptOffset, scriptSetupOffset)) + + firstBlock!.content + if (secondBlock) { + scriptContent += + ' '.repeat(secondBlock.loc.start.offset - script!.loc.end.offset) + + secondBlock.content + } + const lang = script?.lang || scriptSetup?.lang + return babelParse(scriptContent, { + plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins), + sourceType: 'module' + }).program.body + } + return [] +} + function ctxToScope(ctx: ScriptCompileContext): TypeScope { if (ctx.scope) { return ctx.scope } + const scope: TypeScope = { + filename: ctx.descriptor.filename, + source: ctx.descriptor.source, + offset: ctx.startOffset!, + imports: Object.create(ctx.userImports), + types: Object.create(null), + exportedTypes: Object.create(null) + } + const body = ctx.scriptAst ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body] : ctx.scriptSetupAst!.body - return (ctx.scope = { - filename: ctx.descriptor.filename, - imports: ctx.userImports, - types: recordTypes(body) - }) + recordTypes(body, scope) + + return (ctx.scope = scope) } function moduleDeclToScope( @@ -430,34 +793,64 @@ function moduleDeclToScope( if (node._resolvedChildScope) { return node._resolvedChildScope } - const types: TypeScope['types'] = Object.create(parent.types) const scope: TypeScope = { - filename: parent.filename, - imports: Object.create(parent.imports), - types: recordTypes((node.body as TSModuleBlock).body, types), - parent - } - for (const key of Object.keys(types)) { - types[key]._ownerScope = scope + ...parent, + types: Object.create(parent.types), + imports: Object.create(parent.imports) } + recordTypes((node.body as TSModuleBlock).body, scope) return (node._resolvedChildScope = scope) } -function recordTypes( - body: Statement[], - types: Record = Object.create(null) -) { - for (const s of body) { - recordType(s, types) +function recordTypes(body: Statement[], scope: TypeScope) { + const { types, exportedTypes, imports } = scope + for (const stmt of body) { + recordType(stmt, types) + } + for (const stmt of body) { + if (stmt.type === 'ExportNamedDeclaration') { + if (stmt.declaration) { + recordType(stmt.declaration, types) + recordType(stmt.declaration, exportedTypes) + } else { + for (const spec of stmt.specifiers) { + if (spec.type === 'ExportSpecifier') { + const local = spec.local.name + const exported = getId(spec.exported) + if (stmt.source) { + // re-export, register an import + export as a type reference + imports[local] = { + source: stmt.source.value, + imported: local + } + exportedTypes[exported] = { + type: 'TSTypeReference', + typeName: { + type: 'Identifier', + name: local + }, + _ownerScope: scope + } + } else if (types[local]) { + // exporting local defined type + exportedTypes[exported] = types[local] + } + } + } + } + } + } + for (const key of Object.keys(types)) { + types[key]._ownerScope = scope } - return types } function recordType(node: Node, types: Record) { switch (node.type) { case 'TSInterfaceDeclaration': case 'TSEnumDeclaration': - case 'TSModuleDeclaration': { + case 'TSModuleDeclaration': + case 'ClassDeclaration': { const id = node.id.type === 'Identifier' ? node.id.name : node.id.value types[id] = node break @@ -465,12 +858,6 @@ function recordType(node: Node, types: Record) { case 'TSTypeAliasDeclaration': types[node.id.name] = node.typeAnnotation break - case 'ExportNamedDeclaration': { - if (node.declaration) { - recordType(node.declaration, types) - } - break - } case 'VariableDeclaration': { if (node.declare) { for (const decl of node.declarations) { @@ -486,9 +873,29 @@ function recordType(node: Node, types: Record) { } } +export function recordImports(body: Statement[]) { + const imports: TypeScope['imports'] = Object.create(null) + for (const s of body) { + recordImport(s, imports) + } + return imports +} + +function recordImport(node: Node, imports: TypeScope['imports']) { + if (node.type !== 'ImportDeclaration') { + return + } + for (const s of node.specifiers) { + imports[s.local.name] = { + imported: getImportedName(s), + source: node.source.value + } + } +} + export function inferRuntimeType( ctx: ScriptCompileContext, - node: Node, + node: Node & WithScope, scope = node._ownerScope || ctxToScope(ctx) ): string[] { switch (node.type) { @@ -549,7 +956,7 @@ export function inferRuntimeType( if (node.typeName.type === 'Identifier') { const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return inferRuntimeType(ctx, resolved, scope) + return inferRuntimeType(ctx, resolved, resolved._ownerScope) } switch (node.typeName.name) { case 'Array': @@ -627,16 +1034,18 @@ export function inferRuntimeType( return ['Symbol'] case 'TSIndexedAccessType': { - if ( - node.indexType.type === 'TSLiteralType' && - node.indexType.literal.type === 'StringLiteral' - ) { - const resolved = resolveTypeElements(ctx, node.objectType) - const key = node.indexType.literal.value - return inferRuntimeType(ctx, resolved.props[key]) + try { + const types = resolveIndexType(ctx, node, scope) + return flattenTypes(ctx, types, scope) + } catch (e) { + // avoid hard error, fallback to unknown + return [UNKNOWN_TYPE] } } + case 'ClassDeclaration': + return ['Object'] + default: return [UNKNOWN_TYPE] // no runtime check } @@ -647,6 +1056,9 @@ function flattenTypes( types: TSType[], scope: TypeScope ): string[] { + if (types.length === 1) { + return inferRuntimeType(ctx, types[0], scope) + } return [ ...new Set( ([] as string[]).concat( diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 11bc011820e..6d874f8a6db 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -1,4 +1,13 @@ -import { CallExpression, Node } from '@babel/types' +import { + CallExpression, + Expression, + Identifier, + ImportDefaultSpecifier, + ImportNamespaceSpecifier, + ImportSpecifier, + Node, + StringLiteral +} from '@babel/types' import { TS_NODE_TYPES } from '@vue/compiler-dom' export const UNKNOWN_TYPE = 'Unknown' @@ -48,3 +57,43 @@ export function isCallOf( export function toRuntimeTypeString(types: string[]) { return types.length > 1 ? `[${types.join(', ')}]` : types[0] } + +export function getImportedName( + specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier +) { + if (specifier.type === 'ImportSpecifier') + return specifier.imported.type === 'Identifier' + ? specifier.imported.name + : specifier.imported.value + else if (specifier.type === 'ImportNamespaceSpecifier') return '*' + return 'default' +} + +export function getId(node: Identifier | StringLiteral): string +export function getId(node: Expression): string | null +export function getId(node: Expression) { + return node.type === 'Identifier' + ? node.name + : node.type === 'StringLiteral' + ? node.value + : null +} + +const identity = (str: string) => str +const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g +const toLowerCase = (str: string) => str.toLowerCase() + +function toFileNameLowerCase(x: string) { + return fileNameLowerCaseRegExp.test(x) + ? x.replace(fileNameLowerCaseRegExp, toLowerCase) + : x +} + +/** + * We need `getCanonicalFileName` when creating ts module resolution cache, + * but TS does not expose it directly. This implementation is repllicated from + * the TS source code. + */ +export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean) { + return useCaseSensitiveFileNames ? identity : toFileNameLowerCase +} diff --git a/packages/sfc-playground/src/Header.vue b/packages/sfc-playground/src/Header.vue index 91ce3efc46e..b55f0240906 100644 --- a/packages/sfc-playground/src/Header.vue +++ b/packages/sfc-playground/src/Header.vue @@ -6,7 +6,7 @@ import Moon from './icons/Moon.vue' import Share from './icons/Share.vue' import Download from './icons/Download.vue' import GitHub from './icons/GitHub.vue' -import { ReplStore } from '@vue/repl' +import type { ReplStore } from '@vue/repl' const props = defineProps<{ store: ReplStore diff --git a/packages/sfc-playground/vite.config.ts b/packages/sfc-playground/vite.config.ts index 44d5a53509f..5176b9cf061 100644 --- a/packages/sfc-playground/vite.config.ts +++ b/packages/sfc-playground/vite.config.ts @@ -7,7 +7,18 @@ import execa from 'execa' const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7) export default defineConfig({ - plugins: [vue(), copyVuePlugin()], + plugins: [ + vue({ + script: { + // @ts-ignore + fs: { + fileExists: fs.existsSync, + readFile: file => fs.readFileSync(file, 'utf-8') + } + } + }), + copyVuePlugin() + ], define: { __COMMIT__: JSON.stringify(commit), __VUE_PROD_DEVTOOLS__: JSON.stringify(true) diff --git a/packages/vue/compiler-sfc/index.js b/packages/vue/compiler-sfc/index.js index 774f9da2742..2b85ad129ef 100644 --- a/packages/vue/compiler-sfc/index.js +++ b/packages/vue/compiler-sfc/index.js @@ -1 +1,3 @@ module.exports = require('@vue/compiler-sfc') + +require('./register-ts.js') diff --git a/packages/vue/compiler-sfc/index.mjs b/packages/vue/compiler-sfc/index.mjs index 8df9a989d18..ae5d6e8e5ca 100644 --- a/packages/vue/compiler-sfc/index.mjs +++ b/packages/vue/compiler-sfc/index.mjs @@ -1 +1,3 @@ -export * from '@vue/compiler-sfc' \ No newline at end of file +export * from '@vue/compiler-sfc' + +import './register-ts.js' diff --git a/packages/vue/compiler-sfc/package.json b/packages/vue/compiler-sfc/package.json index 1b15fb844ac..778c7ebf51c 100644 --- a/packages/vue/compiler-sfc/package.json +++ b/packages/vue/compiler-sfc/package.json @@ -2,4 +2,4 @@ "main": "index.js", "module": "index.mjs", "types": "index.d.ts" -} \ No newline at end of file +} diff --git a/packages/vue/compiler-sfc/register-ts.js b/packages/vue/compiler-sfc/register-ts.js new file mode 100644 index 00000000000..87f61b64863 --- /dev/null +++ b/packages/vue/compiler-sfc/register-ts.js @@ -0,0 +1,5 @@ +if (typeof require !== 'undefined') { + try { + require('@vue/compiler-sfc').registerTS(require('typescript')) + } catch (e) {} +}