Skip to content

Commit

Permalink
feat(compiler-sfc): support resolving type imports from modules
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Apr 15, 2023
1 parent 8451b92 commit 3982bef
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 44 deletions.
68 changes: 58 additions & 10 deletions packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import {
inferRuntimeType,
invalidateTypeCache,
recordImports,
resolveTypeElements
resolveTypeElements,
registerTS
} from '../../src/script/resolveType'

import ts from 'typescript'
registerTS(ts)

describe('resolveType', () => {
test('type literal', () => {
const { props, calls } = resolve(`type Target = {
Expand Down Expand Up @@ -86,6 +90,19 @@ describe('resolveType', () => {
})
})

test('reference class', () => {
expect(
resolve(`
class Foo {}
type Target = {
foo: Foo
}
`).props
).toStrictEqual({
foo: ['Object']
})
})

test('function type', () => {
expect(
resolve(`
Expand Down Expand Up @@ -258,8 +275,8 @@ describe('resolveType', () => {
type Target = P & PP
`,
{
'foo.ts': 'export type P = { foo: number }',
'bar.d.ts': 'type X = { bar: string }; export { X as Y }'
'/foo.ts': 'export type P = { foo: number }',
'/bar.d.ts': 'type X = { bar: string }; export { X as Y }'
}
).props
).toStrictEqual({
Expand All @@ -277,9 +294,9 @@ describe('resolveType', () => {
type Target = P & PP
`,
{
'foo.vue':
'/foo.vue':
'<script lang="ts">export type P = { foo: number }</script>',
'bar.vue':
'/bar.vue':
'<script setup lang="tsx">export type P = { bar: string }</script>'
}
).props
Expand All @@ -297,9 +314,9 @@ describe('resolveType', () => {
type Target = P
`,
{
'foo.ts': `import type { P as PP } from './nested/bar.vue'
'/foo.ts': `import type { P as PP } from './nested/bar.vue'
export type P = { foo: number } & PP`,
'nested/bar.vue':
'/nested/bar.vue':
'<script setup lang="ts">export type P = { bar: string }</script>'
}
).props
Expand All @@ -317,11 +334,42 @@ describe('resolveType', () => {
type Target = P
`,
{
'foo.ts': `export { P as PP } from './bar'`,
'bar.ts': 'export type P = { bar: string }'
'/foo.ts': `export { P as PP } from './bar'`,
'/bar.ts': 'export type P = { bar: string }'
}
).props
).toStrictEqual({
bar: ['String']
})
})

test('ts module resolve', () => {
expect(
resolve(
`
import { P } from 'foo'
import { PP } from 'bar'
type Target = P & PP
`,
{
'/node_modules/foo/package.json': JSON.stringify({
name: 'foo',
version: '1.0.0',
types: 'index.d.ts'
}),
'/node_modules/foo/index.d.ts': 'export type P = { foo: number }',
'/tsconfig.json': JSON.stringify({
compilerOptions: {
paths: {
bar: ['./other/bar.ts']
}
}
}),
'/other/bar.ts': 'export type PP = { bar: string }'
}
).props
).toStrictEqual({
foo: ['Number'],
bar: ['String']
})
})
Expand Down Expand Up @@ -356,7 +404,7 @@ describe('resolveType', () => {

function resolve(code: string, files: Record<string, string> = {}) {
const { descriptor } = parse(`<script setup lang="ts">\n${code}\n</script>`, {
filename: 'Test.vue'
filename: '/Test.vue'
})
const ctx = new ScriptCompileContext(descriptor, {
id: 'test',
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export interface SFCScriptCompileOptions {
*/
fs?: {
fileExists(file: string): boolean
readFile(file: string): string
readFile(file: string): string | undefined
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-sfc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript } from './compileScript'
export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault'
export { invalidateTypeCache } from './script/resolveType'
export {
shouldTransform as shouldTransformRef,
transform as transformRef,
Expand All @@ -29,6 +28,9 @@ export {
isStaticProperty
} from '@vue/compiler-core'

// Internals for type resolution
export { invalidateTypeCache, registerTS } from './script/resolveType'

// Types
export type {
SFCParseOptions,
Expand Down
154 changes: 126 additions & 28 deletions packages/compiler-sfc/src/script/resolveType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@ import {
TSTypeReference,
TemplateLiteral
} from '@babel/types'
import { UNKNOWN_TYPE, getId, getImportedName } from './utils'
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 path from 'path'
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<ImportBinding, 'source' | 'imported'>

Expand Down Expand Up @@ -480,54 +486,82 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] {
}
}

let ts: typeof TS

export function registerTS(_ts: any) {
ts = _ts
}

type FS = NonNullable<SFCScriptCompileOptions['fs']>

function resolveTypeFromImport(
ctx: ScriptCompileContext,
node: TSTypeReference | TSExpressionWithTypeArguments,
name: string,
scope: TypeScope
): Node | undefined {
const fs = ctx.options.fs
const fs: FS = ctx.options.fs || ts?.sys
if (!fs) {
ctx.error(
`fs options for compileScript are required for resolving imported types`,
node,
scope
`No fs option provided to \`compileScript\` in non-Node environment. ` +
`File system access is required for resolving imported types.`,
node
)
}
// TODO (hmr) register dependency file on ctx

const containingFile = scope.filename
const { source, imported } = scope.imports[name]

let resolved: string | undefined

if (source.startsWith('.')) {
// relative import - fast path
const filename = path.join(containingFile, '..', source)
const resolved = resolveExt(filename, fs)
if (resolved) {
return resolveTypeReference(
ctx,
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,
fileToScope(ctx, resolved, fs),
imported,
true
scope
)
} else {
}
if (!ts) {
ctx.error(
`Failed to resolve import source ${JSON.stringify(
`Failed to resolve type ${imported} from module ${JSON.stringify(
source
)} for type ${name}`,
)}. ` +
`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) {
// TODO (hmr) register dependency file on ctx
return resolveTypeReference(
ctx,
node,
fileToScope(ctx, resolved, fs),
imported,
true
)
} else {
// TODO module or aliased import - use full TS resolution
return
ctx.error(
`Failed to resolve import source ${JSON.stringify(
source
)} for type ${name}`,
node,
scope
)
}
}

function resolveExt(
filename: string,
fs: NonNullable<SFCScriptCompileOptions['fs']>
) {
function resolveExt(filename: string, fs: FS) {
const tryResolve = (filename: string) => {
if (fs.fileExists(filename)) return filename
}
Expand All @@ -540,23 +574,83 @@ function resolveExt(
)
}

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<TypeScope>()

export function invalidateTypeCache(filename: string) {
fileToScopeCache.delete(filename)
tsConfigCache.delete(filename)
}

function fileToScope(
ctx: ScriptCompileContext,
filename: string,
fs: NonNullable<SFCScriptCompileOptions['fs']>
fs: FS
): TypeScope {
const cached = fileToScopeCache.get(filename)
if (cached) {
return cached
}

const source = fs.readFile(filename)
const source = fs.readFile(filename) || ''
const body = parseFile(ctx, filename, source)
const scope: TypeScope = {
filename,
Expand All @@ -577,7 +671,7 @@ function parseFile(
filename: string,
content: string
): Statement[] {
const ext = path.extname(filename)
const ext = extname(filename)
if (ext === '.ts' || ext === '.tsx') {
return babelParse(content, {
plugins: resolveParserPlugins(
Expand Down Expand Up @@ -705,7 +799,8 @@ function recordType(node: Node, types: Record<string, Node>) {
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
Expand Down Expand Up @@ -899,6 +994,9 @@ export function inferRuntimeType(
}
}

case 'ClassDeclaration':
return ['Object']

default:
return [UNKNOWN_TYPE] // no runtime check
}
Expand Down
Loading

0 comments on commit 3982bef

Please sign in to comment.