Skip to content

Commit

Permalink
feat(compiler-sfc): support relative imported types in macros
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Apr 15, 2023
1 parent 1c06fe1 commit 8aa4ea8
Show file tree
Hide file tree
Showing 6 changed files with 472 additions and 152 deletions.
16 changes: 1 addition & 15 deletions packages/compiler-core/src/babelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import type {
Function,
ObjectProperty,
BlockStatement,
Program,
ImportDefaultSpecifier,
ImportNamespaceSpecifier,
ImportSpecifier
Program
} from '@babel/types'
import { walk } from 'estree-walker'

Expand Down Expand Up @@ -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)
Expand Down
103 changes: 100 additions & 3 deletions packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { parse } from '../../src'
import { ScriptCompileContext } from '../../src/script/context'
import {
inferRuntimeType,
recordImports,
resolveTypeElements
} from '../../src/script/resolveType'

Expand Down Expand Up @@ -246,6 +247,85 @@ describe('resolveType', () => {
})
})

describe('external type imports', () => {
test('relative ts', () => {
expect(
resolve(
`
import { P } from './foo'
import { Y as PP } from './bar'
type Target = P & PP
`,
{
'foo.ts': 'export type P = { foo: number }',
'bar.d.ts': 'type X = { bar: string }; export { X as Y }'
}
).props
).toStrictEqual({
foo: ['Number'],
bar: ['String']
})
})

test('relative vue', () => {
expect(
resolve(
`
import { P } from './foo.vue'
import { P as PP } from './bar.vue'
type Target = P & PP
`,
{
'foo.vue':
'<script lang="ts">export type P = { foo: number }</script>',
'bar.vue':
'<script setup lang="tsx">export type P = { bar: string }</script>'
}
).props
).toStrictEqual({
foo: ['Number'],
bar: ['String']
})
})

test('relative (chained)', () => {
expect(
resolve(
`
import { P } from './foo'
type Target = P
`,
{
'foo.ts': `import type { P as PP } from './nested/bar.vue'
export type P = { foo: number } & PP`,
'nested/bar.vue':
'<script setup lang="ts">export type P = { bar: string }</script>'
}
).props
).toStrictEqual({
foo: ['Number'],
bar: ['String']
})
})

test('relative (chained, re-export)', () => {
expect(
resolve(
`
import { PP as P } from './foo'
type Target = P
`,
{
'foo.ts': `export { P as PP } from './bar'`,
'bar.ts': 'export type P = { bar: string }'
}
).props
).toStrictEqual({
bar: ['String']
})
})
})

describe('errors', () => {
test('error on computed keys', () => {
expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
Expand All @@ -255,9 +335,26 @@ describe('resolveType', () => {
})
})

function resolve(code: string) {
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`)
const ctx = new ScriptCompileContext(descriptor, { id: 'test' })
function resolve(code: string, files: Record<string, string> = {}) {
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`, {
filename: 'Test.vue'
})
const ctx = new ScriptCompileContext(descriptor, {
id: 'test',
fs: {
fileExists(file) {
return !!files[file]
},
readFile(file) {
return files[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

const targetDecl = ctx.scriptSetupAst!.body.find(
s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
) as TSTypeAliasDeclaration
Expand Down
17 changes: 14 additions & 3 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -106,6 +110,13 @@ export interface SFCScriptCompileOptions {
* (**Experimental**) Enable macro `defineModel`
*/
defineModel?: boolean
/**
*
*/
fs?: {
fileExists(file: string): boolean
readFile(file: string): string
}
}

export interface ImportBinding {
Expand Down
81 changes: 38 additions & 43 deletions packages/compiler-sfc/src/script/context.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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'
import { BindingMetadata } from '../../../compiler-core/src'
import MagicString from 'magic-string'
import { TypeScope } from './resolveType'
import { TypeScope, WithScope } from './resolveType'

export class ScriptCompileContext {
isJS: boolean
Expand Down Expand Up @@ -83,31 +83,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
Expand All @@ -124,23 +110,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 {
Expand All @@ -150,19 +125,39 @@ 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 & WithScope, scope?: TypeScope): never {
throw new Error(
`[@vue/compiler-sfc] ${msg}\n\n${
this.descriptor.filename
}\n${generateCodeFrame(
this.descriptor.source,
node.start! + this.startOffset!,
end
node.end! + this.startOffset!
)}`
)
}
}

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
}
Loading

0 comments on commit 8aa4ea8

Please sign in to comment.