Skip to content

Commit

Permalink
feat: support 3.3 imported types in SFC macros
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Apr 17, 2023
1 parent 1f2155a commit c891652
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 32 deletions.
64 changes: 41 additions & 23 deletions packages/plugin-vue/src/handleHotUpdate.ts
Expand Up @@ -8,7 +8,11 @@ import {
getDescriptor,
setPrevDescriptor,
} from './utils/descriptorCache'
import { getResolvedScript, setResolvedScript } from './script'
import {
getResolvedScript,
invalidateScript,
setResolvedScript,
} from './script'
import type { ResolvedOptions } from '.'

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

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

const scriptChanged = hasScriptChanged(prevDescriptor, descriptor)
if (scriptChanged) {
let scriptModule: ModuleNode | undefined
if (
(descriptor.scriptSetup?.lang && !descriptor.scriptSetup.src) ||
(descriptor.script?.lang && !descriptor.script.src)
) {
const scriptModuleRE = new RegExp(
`type=script.*&lang\.${
descriptor.scriptSetup?.lang || descriptor.script?.lang
}$`,
)
scriptModule = modules.find((m) => scriptModuleRE.test(m.url))
}
affectedModules.add(scriptModule || mainModule)
affectedModules.add(getScriptModule(modules) || mainModule)
}

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

return false
}

function getMainModule(modules: ModuleNode[]) {
return (
modules
.filter((m) => !/type=/.test(m.url) || /type=script/.test(m.url))
// #9341
// We pick the module with the shortest URL in order to pick the module
// with the lowest number of query parameters.
.sort((m1, m2) => {
return m1.url.length - m2.url.length
})[0]
)
}

function getScriptModule(modules: ModuleNode[]) {
return modules.find((m) => /type=script.*&lang\.\w+$/.test(m.url))
}

export function handleTypeDepChange(
affectedComponents: Set<string>,
{ modules, server: { moduleGraph } }: HmrContext,
): ModuleNode[] {
const affected = new Set<ModuleNode>()
for (const file of affectedComponents) {
invalidateScript(file)
const mods = moduleGraph.getModulesByFile(file)
if (mods) {
const arr = [...mods]
affected.add(getScriptModule(arr) || getMainModule(arr))
}
}
return [...modules, ...affected]
}
15 changes: 10 additions & 5 deletions packages/plugin-vue/src/index.ts
Expand Up @@ -13,9 +13,9 @@ import type * as _compiler from 'vue/compiler-sfc'
import { resolveCompiler } from './compiler'
import { parseVueRequest } from './utils/query'
import { getDescriptor, getSrcDescriptor } from './utils/descriptorCache'
import { getResolvedScript } from './script'
import { getResolvedScript, typeDepToSFCMap } from './script'
import { transformMain } from './main'
import { handleHotUpdate } from './handleHotUpdate'
import { handleHotUpdate, handleTypeDepChange } from './handleHotUpdate'
import { transformTemplateAsModule } from './template'
import { transformStyle } from './style'
import { EXPORT_HELPER_ID, helperCode } from './helper'
Expand Down Expand Up @@ -120,10 +120,15 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin {
name: 'vite:vue',

handleHotUpdate(ctx) {
if (!filter(ctx.file)) {
return
if (options.compiler.invalidateTypeCache) {
options.compiler.invalidateTypeCache(ctx.file)
}
if (typeDepToSFCMap.has(ctx.file)) {
return handleTypeDepChange(typeDepToSFCMap.get(ctx.file)!, ctx)
}
if (filter(ctx.file)) {
return handleHotUpdate(ctx, options)
}
return handleHotUpdate(ctx, options)
},

config(config) {
Expand Down
29 changes: 29 additions & 0 deletions packages/plugin-vue/src/script.ts
@@ -1,10 +1,20 @@
import type { SFCDescriptor, SFCScriptBlock } from 'vue/compiler-sfc'
import { resolveTemplateCompilerOptions } from './template'
import { cache as descriptorCache } from './utils/descriptorCache'
import type { ResolvedOptions } from '.'

// ssr and non ssr builds would output different script content
const clientCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
const ssrCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
export const depToSFCMap = new Map<string, string>()

export function invalidateScript(filename: string): void {
const desc = descriptorCache.get(filename)
if (desc) {
clientCache.delete(desc)
ssrCache.delete(desc)
}
}

export function getResolvedScript(
descriptor: SFCDescriptor,
Expand Down Expand Up @@ -33,6 +43,8 @@ export function isUseInlineTemplate(

export const scriptIdentifier = `_sfc_main`

export const typeDepToSFCMap = new Map<string, Set<string>>()

export function resolveScript(
descriptor: SFCDescriptor,
options: ResolvedOptions,
Expand Down Expand Up @@ -63,6 +75,23 @@ export function resolveScript(
: undefined,
})

if (resolved?.deps) {
for (const [key, sfcs] of typeDepToSFCMap) {
if (sfcs.has(descriptor.filename) && !resolved.deps.includes(key)) {
sfcs.delete(descriptor.filename)
}
}

for (const dep of resolved.deps) {
const existingSet = typeDepToSFCMap.get(dep)
if (!existingSet) {
typeDepToSFCMap.set(dep, new Set([descriptor.filename]))
} else {
existingSet.add(descriptor.filename)
}
}
}

cacheToUse.set(descriptor, resolved)
return resolved
}
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-vue/src/utils/descriptorCache.ts
Expand Up @@ -11,7 +11,7 @@ export interface SFCParseResult {
errors: Array<CompilerError | SyntaxError>
}

const cache = new Map<string, SFCDescriptor>()
export const cache = new Map<string, SFCDescriptor>()
const prevCache = new Map<string, SFCDescriptor | undefined>()

export function createDescriptor(
Expand Down
5 changes: 4 additions & 1 deletion playground/vue/Main.vue
@@ -1,13 +1,14 @@
<template>
<h1>Vue version {{ version }}</h1>
<div class="comments"><!--hello--></div>
<h1>Vue SFCs</h1>
<pre>{{ time as string }}</pre>
<div class="hmr-block">
<Hmr />
</div>
<div class="hmr-tsx-block">
<HmrTsx />
</div>
<TypeProps msg="msg" bar="bar" :id="123" />
<Syntax />
<PreProcessors />
<CssModules />
Expand All @@ -29,6 +30,7 @@
</template>

<script setup lang="ts">
import { version } from 'vue'
import Hmr from './Hmr.vue'
import HmrTsx from './HmrTsx.vue'
import Syntax from './Syntax.vue'
Expand All @@ -46,6 +48,7 @@ import SetupImportTemplate from './setup-import-template/SetupImportTemplate.vue
import WorkerTest from './worker.vue'
import { ref } from 'vue'
import Url from './Url.vue'
import TypeProps from './TypeProps.vue'
const time = ref('loading...')
Expand Down
11 changes: 11 additions & 0 deletions playground/vue/TypeProps.vue
@@ -0,0 +1,11 @@
<script setup lang="ts">
import type { Props } from './types'
import type { Aliased } from '~types'
const props = defineProps<Props & Aliased & { bar: string }>()
</script>

<template>
<h2>Imported Type Props</h2>
<pre class="type-props">{{ props }}</pre>
</template>
46 changes: 45 additions & 1 deletion playground/vue/__tests__/vue.spec.ts
@@ -1,4 +1,5 @@
import { describe, expect, test } from 'vitest'
import { version } from 'vue'
import {
browserLogs,
editFile,
Expand All @@ -11,7 +12,7 @@ import {
} from '~utils'

test('should render', async () => {
expect(await page.textContent('h1')).toMatch('Vue SFCs')
expect(await page.textContent('h1')).toMatch(`Vue version ${version}`)
})

test('should update', async () => {
Expand Down Expand Up @@ -279,3 +280,46 @@ describe('import with ?url', () => {
)
})
})

describe('macro imported types', () => {
test('should resolve and render correct props', async () => {
expect(await page.textContent('.type-props')).toMatch(
JSON.stringify(
{
msg: 'msg',
bar: 'bar',
id: 123,
},
null,
2,
),
)
})

test('should hmr', async () => {
editFile('types.ts', (code) => code.replace('msg: string', ''))
await untilUpdated(
() => page.textContent('.type-props'),
JSON.stringify(
{
bar: 'bar',
id: 123,
},
null,
2,
),
)

editFile('types-aliased.d.ts', (code) => code.replace('id: number', ''))
await untilUpdated(
() => page.textContent('.type-props'),
JSON.stringify(
{
bar: 'bar',
},
null,
2,
),
)
})
})
7 changes: 6 additions & 1 deletion playground/vue/tsconfig.json
@@ -1,7 +1,12 @@
{
"compilerOptions": {
// esbuild transpile should ignore this
"target": "ES5"
"target": "ES5",
"jsx": "preserve",
"paths": {
"~utils": ["../test-utils.ts"],
"~types": ["./types-aliased.d.ts"]
}
},
"include": ["."]
}
3 changes: 3 additions & 0 deletions playground/vue/types-aliased.d.ts
@@ -0,0 +1,3 @@
export interface Aliased {
id: number
}
3 changes: 3 additions & 0 deletions playground/vue/types.ts
@@ -0,0 +1,3 @@
export interface Props {
msg: string
}

0 comments on commit c891652

Please sign in to comment.