Skip to content

Commit 8aa4ea8

Browse files
committed
feat(compiler-sfc): support relative imported types in macros
1 parent 1c06fe1 commit 8aa4ea8

File tree

6 files changed

+472
-152
lines changed

6 files changed

+472
-152
lines changed

packages/compiler-core/src/babelUtils.ts

+1-15
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import type {
66
Function,
77
ObjectProperty,
88
BlockStatement,
9-
Program,
10-
ImportDefaultSpecifier,
11-
ImportNamespaceSpecifier,
12-
ImportSpecifier
9+
Program
1310
} from '@babel/types'
1411
import { walk } from 'estree-walker'
1512

@@ -246,17 +243,6 @@ export const isStaticProperty = (node: Node): node is ObjectProperty =>
246243
export const isStaticPropertyKey = (node: Node, parent: Node) =>
247244
isStaticProperty(parent) && parent.key === node
248245

249-
export function getImportedName(
250-
specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier
251-
) {
252-
if (specifier.type === 'ImportSpecifier')
253-
return specifier.imported.type === 'Identifier'
254-
? specifier.imported.name
255-
: specifier.imported.value
256-
else if (specifier.type === 'ImportNamespaceSpecifier') return '*'
257-
return 'default'
258-
}
259-
260246
/**
261247
* Copied from https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/isReferenced.ts
262248
* To avoid runtime dependency on @babel/types (which includes process references)

packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

+100-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { parse } from '../../src'
33
import { ScriptCompileContext } from '../../src/script/context'
44
import {
55
inferRuntimeType,
6+
recordImports,
67
resolveTypeElements
78
} from '../../src/script/resolveType'
89

@@ -246,6 +247,85 @@ describe('resolveType', () => {
246247
})
247248
})
248249

250+
describe('external type imports', () => {
251+
test('relative ts', () => {
252+
expect(
253+
resolve(
254+
`
255+
import { P } from './foo'
256+
import { Y as PP } from './bar'
257+
type Target = P & PP
258+
`,
259+
{
260+
'foo.ts': 'export type P = { foo: number }',
261+
'bar.d.ts': 'type X = { bar: string }; export { X as Y }'
262+
}
263+
).props
264+
).toStrictEqual({
265+
foo: ['Number'],
266+
bar: ['String']
267+
})
268+
})
269+
270+
test('relative vue', () => {
271+
expect(
272+
resolve(
273+
`
274+
import { P } from './foo.vue'
275+
import { P as PP } from './bar.vue'
276+
type Target = P & PP
277+
`,
278+
{
279+
'foo.vue':
280+
'<script lang="ts">export type P = { foo: number }</script>',
281+
'bar.vue':
282+
'<script setup lang="tsx">export type P = { bar: string }</script>'
283+
}
284+
).props
285+
).toStrictEqual({
286+
foo: ['Number'],
287+
bar: ['String']
288+
})
289+
})
290+
291+
test('relative (chained)', () => {
292+
expect(
293+
resolve(
294+
`
295+
import { P } from './foo'
296+
type Target = P
297+
`,
298+
{
299+
'foo.ts': `import type { P as PP } from './nested/bar.vue'
300+
export type P = { foo: number } & PP`,
301+
'nested/bar.vue':
302+
'<script setup lang="ts">export type P = { bar: string }</script>'
303+
}
304+
).props
305+
).toStrictEqual({
306+
foo: ['Number'],
307+
bar: ['String']
308+
})
309+
})
310+
311+
test('relative (chained, re-export)', () => {
312+
expect(
313+
resolve(
314+
`
315+
import { PP as P } from './foo'
316+
type Target = P
317+
`,
318+
{
319+
'foo.ts': `export { P as PP } from './bar'`,
320+
'bar.ts': 'export type P = { bar: string }'
321+
}
322+
).props
323+
).toStrictEqual({
324+
bar: ['String']
325+
})
326+
})
327+
})
328+
249329
describe('errors', () => {
250330
test('error on computed keys', () => {
251331
expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
@@ -255,9 +335,26 @@ describe('resolveType', () => {
255335
})
256336
})
257337

258-
function resolve(code: string) {
259-
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`)
260-
const ctx = new ScriptCompileContext(descriptor, { id: 'test' })
338+
function resolve(code: string, files: Record<string, string> = {}) {
339+
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`, {
340+
filename: 'Test.vue'
341+
})
342+
const ctx = new ScriptCompileContext(descriptor, {
343+
id: 'test',
344+
fs: {
345+
fileExists(file) {
346+
return !!files[file]
347+
},
348+
readFile(file) {
349+
return files[file]
350+
}
351+
}
352+
})
353+
354+
// ctx.userImports is collected when calling compileScript(), but we are
355+
// skipping that here, so need to manually register imports
356+
ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any
357+
261358
const targetDecl = ctx.scriptSetupAst!.body.find(
262359
s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
263360
) as TSTypeAliasDeclaration

packages/compiler-sfc/src/compileScript.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import {
22
BindingTypes,
33
UNREF,
44
isFunctionType,
5-
walkIdentifiers,
6-
getImportedName
5+
walkIdentifiers
76
} from '@vue/compiler-dom'
87
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
98
import { parse as _parse, ParserPlugin } from '@babel/parser'
@@ -45,7 +44,12 @@ import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
4544
import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
4645
import { processDefineSlots } from './script/defineSlots'
4746
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
48-
import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils'
47+
import {
48+
isLiteralNode,
49+
unwrapTSNode,
50+
isCallOf,
51+
getImportedName
52+
} from './script/utils'
4953
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
5054
import { isImportUsed } from './script/importUsageCheck'
5155
import { processAwait } from './script/topLevelAwait'
@@ -106,6 +110,13 @@ export interface SFCScriptCompileOptions {
106110
* (**Experimental**) Enable macro `defineModel`
107111
*/
108112
defineModel?: boolean
113+
/**
114+
*
115+
*/
116+
fs?: {
117+
fileExists(file: string): boolean
118+
readFile(file: string): string
119+
}
109120
}
110121

111122
export interface ImportBinding {

packages/compiler-sfc/src/script/context.ts

+38-43
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Node, ObjectPattern, Program } from '@babel/types'
22
import { SFCDescriptor } from '../parse'
33
import { generateCodeFrame } from '@vue/shared'
4-
import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser'
4+
import { parse as babelParse, ParserPlugin } from '@babel/parser'
55
import { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
66
import { PropsDestructureBindings } from './defineProps'
77
import { ModelDecl } from './defineModel'
88
import { BindingMetadata } from '../../../compiler-core/src'
99
import MagicString from 'magic-string'
10-
import { TypeScope } from './resolveType'
10+
import { TypeScope, WithScope } from './resolveType'
1111

1212
export class ScriptCompileContext {
1313
isJS: boolean
@@ -83,31 +83,17 @@ export class ScriptCompileContext {
8383
scriptSetupLang === 'tsx'
8484

8585
// resolve parser plugins
86-
const plugins: ParserPlugin[] = []
87-
if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
88-
plugins.push('jsx')
89-
} else {
90-
// If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins
91-
if (options.babelParserPlugins)
92-
options.babelParserPlugins = options.babelParserPlugins.filter(
93-
n => n !== 'jsx'
94-
)
95-
}
96-
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
97-
if (this.isTS) {
98-
plugins.push('typescript')
99-
if (!plugins.includes('decorators')) {
100-
plugins.push('decorators-legacy')
101-
}
102-
}
86+
const plugins: ParserPlugin[] = resolveParserPlugins(
87+
(scriptLang || scriptSetupLang)!,
88+
options.babelParserPlugins
89+
)
10390

104-
function parse(
105-
input: string,
106-
options: ParserOptions,
107-
offset: number
108-
): Program {
91+
function parse(input: string, offset: number): Program {
10992
try {
110-
return babelParse(input, options).program
93+
return babelParse(input, {
94+
plugins,
95+
sourceType: 'module'
96+
}).program
11197
} catch (e: any) {
11298
e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
11399
descriptor.filename
@@ -124,23 +110,12 @@ export class ScriptCompileContext {
124110
this.descriptor.script &&
125111
parse(
126112
this.descriptor.script.content,
127-
{
128-
plugins,
129-
sourceType: 'module'
130-
},
131113
this.descriptor.script.loc.start.offset
132114
)
133115

134116
this.scriptSetupAst =
135117
this.descriptor.scriptSetup &&
136-
parse(
137-
this.descriptor.scriptSetup!.content,
138-
{
139-
plugins: [...plugins, 'topLevelAwait'],
140-
sourceType: 'module'
141-
},
142-
this.startOffset!
143-
)
118+
parse(this.descriptor.scriptSetup!.content, this.startOffset!)
144119
}
145120

146121
getString(node: Node, scriptSetup = true): string {
@@ -150,19 +125,39 @@ export class ScriptCompileContext {
150125
return block.content.slice(node.start!, node.end!)
151126
}
152127

153-
error(
154-
msg: string,
155-
node: Node,
156-
end: number = node.end! + this.startOffset!
157-
): never {
128+
error(msg: string, node: Node & WithScope, scope?: TypeScope): never {
158129
throw new Error(
159130
`[@vue/compiler-sfc] ${msg}\n\n${
160131
this.descriptor.filename
161132
}\n${generateCodeFrame(
162133
this.descriptor.source,
163134
node.start! + this.startOffset!,
164-
end
135+
node.end! + this.startOffset!
165136
)}`
166137
)
167138
}
168139
}
140+
141+
export function resolveParserPlugins(
142+
lang: string,
143+
userPlugins?: ParserPlugin[]
144+
) {
145+
const plugins: ParserPlugin[] = []
146+
if (lang === 'jsx' || lang === 'tsx') {
147+
plugins.push('jsx')
148+
} else if (userPlugins) {
149+
// If don't match the case of adding jsx
150+
// should remove the jsx from user options
151+
userPlugins = userPlugins.filter(p => p !== 'jsx')
152+
}
153+
if (lang === 'ts' || lang === 'tsx') {
154+
plugins.push('typescript')
155+
if (!plugins.includes('decorators')) {
156+
plugins.push('decorators-legacy')
157+
}
158+
}
159+
if (userPlugins) {
160+
plugins.push(...userPlugins)
161+
}
162+
return plugins
163+
}

0 commit comments

Comments
 (0)