Skip to content

Commit c891652

Browse files
committed
feat: support 3.3 imported types in SFC macros
1 parent 1f2155a commit c891652

File tree

10 files changed

+153
-32
lines changed

10 files changed

+153
-32
lines changed

packages/plugin-vue/src/handleHotUpdate.ts

+41-23
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
getDescriptor,
99
setPrevDescriptor,
1010
} from './utils/descriptorCache'
11-
import { getResolvedScript, setResolvedScript } from './script'
11+
import {
12+
getResolvedScript,
13+
invalidateScript,
14+
setResolvedScript,
15+
} from './script'
1216
import type { ResolvedOptions } from '.'
1317

1418
const debug = _debug('vite:hmr')
@@ -19,7 +23,7 @@ const directRequestRE = /(?:\?|&)direct\b/
1923
* Vite-specific HMR handling
2024
*/
2125
export async function handleHotUpdate(
22-
{ file, modules, read, server }: HmrContext,
26+
{ file, modules, read }: HmrContext,
2327
options: ResolvedOptions,
2428
): Promise<ModuleNode[] | void> {
2529
const prevDescriptor = getDescriptor(file, options, false)
@@ -35,31 +39,12 @@ export async function handleHotUpdate(
3539

3640
let needRerender = false
3741
const affectedModules = new Set<ModuleNode | undefined>()
38-
const mainModule = modules
39-
.filter((m) => !/type=/.test(m.url) || /type=script/.test(m.url))
40-
// #9341
41-
// We pick the module with the shortest URL in order to pick the module
42-
// with the lowest number of query parameters.
43-
.sort((m1, m2) => {
44-
return m1.url.length - m2.url.length
45-
})[0]
42+
const mainModule = getMainModule(modules)
4643
const templateModule = modules.find((m) => /type=template/.test(m.url))
4744

4845
const scriptChanged = hasScriptChanged(prevDescriptor, descriptor)
4946
if (scriptChanged) {
50-
let scriptModule: ModuleNode | undefined
51-
if (
52-
(descriptor.scriptSetup?.lang && !descriptor.scriptSetup.src) ||
53-
(descriptor.script?.lang && !descriptor.script.src)
54-
) {
55-
const scriptModuleRE = new RegExp(
56-
`type=script.*&lang\.${
57-
descriptor.scriptSetup?.lang || descriptor.script?.lang
58-
}$`,
59-
)
60-
scriptModule = modules.find((m) => scriptModuleRE.test(m.url))
61-
}
62-
affectedModules.add(scriptModule || mainModule)
47+
affectedModules.add(getScriptModule(modules) || mainModule)
6348
}
6449

6550
if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
@@ -218,3 +203,36 @@ function hasScriptChanged(prev: SFCDescriptor, next: SFCDescriptor): boolean {
218203

219204
return false
220205
}
206+
207+
function getMainModule(modules: ModuleNode[]) {
208+
return (
209+
modules
210+
.filter((m) => !/type=/.test(m.url) || /type=script/.test(m.url))
211+
// #9341
212+
// We pick the module with the shortest URL in order to pick the module
213+
// with the lowest number of query parameters.
214+
.sort((m1, m2) => {
215+
return m1.url.length - m2.url.length
216+
})[0]
217+
)
218+
}
219+
220+
function getScriptModule(modules: ModuleNode[]) {
221+
return modules.find((m) => /type=script.*&lang\.\w+$/.test(m.url))
222+
}
223+
224+
export function handleTypeDepChange(
225+
affectedComponents: Set<string>,
226+
{ modules, server: { moduleGraph } }: HmrContext,
227+
): ModuleNode[] {
228+
const affected = new Set<ModuleNode>()
229+
for (const file of affectedComponents) {
230+
invalidateScript(file)
231+
const mods = moduleGraph.getModulesByFile(file)
232+
if (mods) {
233+
const arr = [...mods]
234+
affected.add(getScriptModule(arr) || getMainModule(arr))
235+
}
236+
}
237+
return [...modules, ...affected]
238+
}

packages/plugin-vue/src/index.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import type * as _compiler from 'vue/compiler-sfc'
1313
import { resolveCompiler } from './compiler'
1414
import { parseVueRequest } from './utils/query'
1515
import { getDescriptor, getSrcDescriptor } from './utils/descriptorCache'
16-
import { getResolvedScript } from './script'
16+
import { getResolvedScript, typeDepToSFCMap } from './script'
1717
import { transformMain } from './main'
18-
import { handleHotUpdate } from './handleHotUpdate'
18+
import { handleHotUpdate, handleTypeDepChange } from './handleHotUpdate'
1919
import { transformTemplateAsModule } from './template'
2020
import { transformStyle } from './style'
2121
import { EXPORT_HELPER_ID, helperCode } from './helper'
@@ -120,10 +120,15 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
120120
name: 'vite:vue',
121121

122122
handleHotUpdate(ctx) {
123-
if (!filter(ctx.file)) {
124-
return
123+
if (options.compiler.invalidateTypeCache) {
124+
options.compiler.invalidateTypeCache(ctx.file)
125+
}
126+
if (typeDepToSFCMap.has(ctx.file)) {
127+
return handleTypeDepChange(typeDepToSFCMap.get(ctx.file)!, ctx)
128+
}
129+
if (filter(ctx.file)) {
130+
return handleHotUpdate(ctx, options)
125131
}
126-
return handleHotUpdate(ctx, options)
127132
},
128133

129134
config(config) {

packages/plugin-vue/src/script.ts

+29
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import type { SFCDescriptor, SFCScriptBlock } from 'vue/compiler-sfc'
22
import { resolveTemplateCompilerOptions } from './template'
3+
import { cache as descriptorCache } from './utils/descriptorCache'
34
import type { ResolvedOptions } from '.'
45

56
// ssr and non ssr builds would output different script content
67
const clientCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
78
const ssrCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
9+
export const depToSFCMap = new Map<string, string>()
10+
11+
export function invalidateScript(filename: string): void {
12+
const desc = descriptorCache.get(filename)
13+
if (desc) {
14+
clientCache.delete(desc)
15+
ssrCache.delete(desc)
16+
}
17+
}
818

919
export function getResolvedScript(
1020
descriptor: SFCDescriptor,
@@ -33,6 +43,8 @@ export function isUseInlineTemplate(
3343

3444
export const scriptIdentifier = `_sfc_main`
3545

46+
export const typeDepToSFCMap = new Map<string, Set<string>>()
47+
3648
export function resolveScript(
3749
descriptor: SFCDescriptor,
3850
options: ResolvedOptions,
@@ -63,6 +75,23 @@ export function resolveScript(
6375
: undefined,
6476
})
6577

78+
if (resolved?.deps) {
79+
for (const [key, sfcs] of typeDepToSFCMap) {
80+
if (sfcs.has(descriptor.filename) && !resolved.deps.includes(key)) {
81+
sfcs.delete(descriptor.filename)
82+
}
83+
}
84+
85+
for (const dep of resolved.deps) {
86+
const existingSet = typeDepToSFCMap.get(dep)
87+
if (!existingSet) {
88+
typeDepToSFCMap.set(dep, new Set([descriptor.filename]))
89+
} else {
90+
existingSet.add(descriptor.filename)
91+
}
92+
}
93+
}
94+
6695
cacheToUse.set(descriptor, resolved)
6796
return resolved
6897
}

packages/plugin-vue/src/utils/descriptorCache.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface SFCParseResult {
1111
errors: Array<CompilerError | SyntaxError>
1212
}
1313

14-
const cache = new Map<string, SFCDescriptor>()
14+
export const cache = new Map<string, SFCDescriptor>()
1515
const prevCache = new Map<string, SFCDescriptor | undefined>()
1616

1717
export function createDescriptor(

playground/vue/Main.vue

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<template>
2+
<h1>Vue version {{ version }}</h1>
23
<div class="comments"><!--hello--></div>
3-
<h1>Vue SFCs</h1>
44
<pre>{{ time as string }}</pre>
55
<div class="hmr-block">
66
<Hmr />
77
</div>
88
<div class="hmr-tsx-block">
99
<HmrTsx />
1010
</div>
11+
<TypeProps msg="msg" bar="bar" :id="123" />
1112
<Syntax />
1213
<PreProcessors />
1314
<CssModules />
@@ -29,6 +30,7 @@
2930
</template>
3031

3132
<script setup lang="ts">
33+
import { version } from 'vue'
3234
import Hmr from './Hmr.vue'
3335
import HmrTsx from './HmrTsx.vue'
3436
import Syntax from './Syntax.vue'
@@ -46,6 +48,7 @@ import SetupImportTemplate from './setup-import-template/SetupImportTemplate.vue
4648
import WorkerTest from './worker.vue'
4749
import { ref } from 'vue'
4850
import Url from './Url.vue'
51+
import TypeProps from './TypeProps.vue'
4952
5053
const time = ref('loading...')
5154

playground/vue/TypeProps.vue

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
import type { Props } from './types'
3+
import type { Aliased } from '~types'
4+
5+
const props = defineProps<Props & Aliased & { bar: string }>()
6+
</script>
7+
8+
<template>
9+
<h2>Imported Type Props</h2>
10+
<pre class="type-props">{{ props }}</pre>
11+
</template>

playground/vue/__tests__/vue.spec.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, test } from 'vitest'
2+
import { version } from 'vue'
23
import {
34
browserLogs,
45
editFile,
@@ -11,7 +12,7 @@ import {
1112
} from '~utils'
1213

1314
test('should render', async () => {
14-
expect(await page.textContent('h1')).toMatch('Vue SFCs')
15+
expect(await page.textContent('h1')).toMatch(`Vue version ${version}`)
1516
})
1617

1718
test('should update', async () => {
@@ -279,3 +280,46 @@ describe('import with ?url', () => {
279280
)
280281
})
281282
})
283+
284+
describe('macro imported types', () => {
285+
test('should resolve and render correct props', async () => {
286+
expect(await page.textContent('.type-props')).toMatch(
287+
JSON.stringify(
288+
{
289+
msg: 'msg',
290+
bar: 'bar',
291+
id: 123,
292+
},
293+
null,
294+
2,
295+
),
296+
)
297+
})
298+
299+
test('should hmr', async () => {
300+
editFile('types.ts', (code) => code.replace('msg: string', ''))
301+
await untilUpdated(
302+
() => page.textContent('.type-props'),
303+
JSON.stringify(
304+
{
305+
bar: 'bar',
306+
id: 123,
307+
},
308+
null,
309+
2,
310+
),
311+
)
312+
313+
editFile('types-aliased.d.ts', (code) => code.replace('id: number', ''))
314+
await untilUpdated(
315+
() => page.textContent('.type-props'),
316+
JSON.stringify(
317+
{
318+
bar: 'bar',
319+
},
320+
null,
321+
2,
322+
),
323+
)
324+
})
325+
})

playground/vue/tsconfig.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
{
22
"compilerOptions": {
33
// esbuild transpile should ignore this
4-
"target": "ES5"
4+
"target": "ES5",
5+
"jsx": "preserve",
6+
"paths": {
7+
"~utils": ["../test-utils.ts"],
8+
"~types": ["./types-aliased.d.ts"]
9+
}
510
},
611
"include": ["."]
712
}

playground/vue/types-aliased.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Aliased {
2+
id: number
3+
}

playground/vue/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Props {
2+
msg: string
3+
}

0 commit comments

Comments
 (0)