From c891652e7cb806acb170275a5d9f8a258265a3e2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 17 Apr 2023 20:11:48 +0800 Subject: [PATCH] feat: support 3.3 imported types in SFC macros --- packages/plugin-vue/src/handleHotUpdate.ts | 64 ++++++++++++------- packages/plugin-vue/src/index.ts | 15 +++-- packages/plugin-vue/src/script.ts | 29 +++++++++ .../plugin-vue/src/utils/descriptorCache.ts | 2 +- playground/vue/Main.vue | 5 +- playground/vue/TypeProps.vue | 11 ++++ playground/vue/__tests__/vue.spec.ts | 46 ++++++++++++- playground/vue/tsconfig.json | 7 +- playground/vue/types-aliased.d.ts | 3 + playground/vue/types.ts | 3 + 10 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 playground/vue/TypeProps.vue create mode 100644 playground/vue/types-aliased.d.ts create mode 100644 playground/vue/types.ts diff --git a/packages/plugin-vue/src/handleHotUpdate.ts b/packages/plugin-vue/src/handleHotUpdate.ts index 4e256f64..97ceefc5 100644 --- a/packages/plugin-vue/src/handleHotUpdate.ts +++ b/packages/plugin-vue/src/handleHotUpdate.ts @@ -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') @@ -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 { const prevDescriptor = getDescriptor(file, options, false) @@ -35,31 +39,12 @@ export async function handleHotUpdate( let needRerender = false const affectedModules = new Set() - 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)) { @@ -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, + { modules, server: { moduleGraph } }: HmrContext, +): ModuleNode[] { + const affected = new Set() + 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] +} diff --git a/packages/plugin-vue/src/index.ts b/packages/plugin-vue/src/index.ts index 58b69d8e..afa72805 100644 --- a/packages/plugin-vue/src/index.ts +++ b/packages/plugin-vue/src/index.ts @@ -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' @@ -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) { diff --git a/packages/plugin-vue/src/script.ts b/packages/plugin-vue/src/script.ts index ee72e02d..fef8f53a 100644 --- a/packages/plugin-vue/src/script.ts +++ b/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() const ssrCache = new WeakMap() +export const depToSFCMap = new Map() + +export function invalidateScript(filename: string): void { + const desc = descriptorCache.get(filename) + if (desc) { + clientCache.delete(desc) + ssrCache.delete(desc) + } +} export function getResolvedScript( descriptor: SFCDescriptor, @@ -33,6 +43,8 @@ export function isUseInlineTemplate( export const scriptIdentifier = `_sfc_main` +export const typeDepToSFCMap = new Map>() + export function resolveScript( descriptor: SFCDescriptor, options: ResolvedOptions, @@ -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 } diff --git a/packages/plugin-vue/src/utils/descriptorCache.ts b/packages/plugin-vue/src/utils/descriptorCache.ts index e09524f9..2152cd21 100644 --- a/packages/plugin-vue/src/utils/descriptorCache.ts +++ b/packages/plugin-vue/src/utils/descriptorCache.ts @@ -11,7 +11,7 @@ export interface SFCParseResult { errors: Array } -const cache = new Map() +export const cache = new Map() const prevCache = new Map() export function createDescriptor( diff --git a/playground/vue/Main.vue b/playground/vue/Main.vue index b91d50f9..ce3661c7 100644 --- a/playground/vue/Main.vue +++ b/playground/vue/Main.vue @@ -1,6 +1,6 @@ + + diff --git a/playground/vue/__tests__/vue.spec.ts b/playground/vue/__tests__/vue.spec.ts index f0d15c7b..0974fa23 100644 --- a/playground/vue/__tests__/vue.spec.ts +++ b/playground/vue/__tests__/vue.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest' +import { version } from 'vue' import { browserLogs, editFile, @@ -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 () => { @@ -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, + ), + ) + }) +}) diff --git a/playground/vue/tsconfig.json b/playground/vue/tsconfig.json index bdc0eedc..340de3c5 100644 --- a/playground/vue/tsconfig.json +++ b/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": ["."] } diff --git a/playground/vue/types-aliased.d.ts b/playground/vue/types-aliased.d.ts new file mode 100644 index 00000000..d884e098 --- /dev/null +++ b/playground/vue/types-aliased.d.ts @@ -0,0 +1,3 @@ +export interface Aliased { + id: number +} diff --git a/playground/vue/types.ts b/playground/vue/types.ts new file mode 100644 index 00000000..7c9d47a6 --- /dev/null +++ b/playground/vue/types.ts @@ -0,0 +1,3 @@ +export interface Props { + msg: string +}