diff --git a/.changeset/dull-seals-behave.md b/.changeset/dull-seals-behave.md new file mode 100644 index 0000000..de00977 --- /dev/null +++ b/.changeset/dull-seals-behave.md @@ -0,0 +1,5 @@ +--- +"@marko/vite": patch +--- + +Fix issue with hot module reloading virtual dependencies. diff --git a/.changeset/shaggy-llamas-drive.md b/.changeset/shaggy-llamas-drive.md new file mode 100644 index 0000000..c4132b7 --- /dev/null +++ b/.changeset/shaggy-llamas-drive.md @@ -0,0 +1,5 @@ +--- +"@marko/vite": patch +--- + +Fix issue with new tags not discovered during HMR. diff --git a/src/index.ts b/src/index.ts index d8a0b75..456a444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,11 +49,24 @@ interface ServerManifest { chunksNeedingAssets: string[]; } +interface VirtualFile { + code: string; + map?: any; +} + +type DeferredPromise = Promise & { + resolve: (value: T) => void; + reject: (error: Error) => void; +}; + const normalizePath = path.sep === "\\" ? (id: string) => id.replace(/\\/g, "/") : (id: string) => id; -const virtualFiles = new Map(); +const virtualFiles = new Map< + string, + VirtualFile | DeferredPromise +>(); const queryReg = /\?marko-.+$/; const browserEntryQuery = "?marko-browser-entry"; const serverEntryQuery = "?marko-server-entry"; @@ -62,14 +75,13 @@ const manifestFileName = "manifest.json"; const markoExt = ".marko"; const htmlExt = ".html"; const resolveOpts = { skipSelf: true }; -const cache = new Map(); +const cache = new Map(); const thisFile = typeof __filename === "string" ? __filename : fileURLToPath(import.meta.url); export default function markoPlugin(opts: Options = {}): vite.Plugin[] { let compiler: typeof Compiler; const { runtimeId, linked = true } = opts; - const baseConfig: Compiler.Config = { cache, runtimeId, @@ -97,10 +109,8 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { if (devServer) { const prev = virtualFiles.get(id); - if (prev && prev.code !== dep.code) { - devServer.moduleGraph.invalidateModule( - devServer.moduleGraph.getModuleById(id)! - ); + if (isDeferredPromise(prev)) { + prev.resolve(dep); } } @@ -224,6 +234,8 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { devServer.watcher.on("all", (type, filename) => { if (type === "unlink") { entrySources.delete(filename); + transformWatchFiles.delete(filename); + transformOptionalFiles.delete(filename); } for (const [id, files] of transformWatchFiles) { @@ -233,24 +245,28 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { } if (type === "add" || type === "unlink") { - let clearedCache = false; for (const [id, files] of transformOptionalFiles) { if (anyMatch(files, filename)) { - if (!clearedCache) { - baseConfig.cache!.clear(); - clearedCache = true; - } devServer.watcher.emit("change", id); } } } }); }, + + handleHotUpdate(ctx) { + compiler.taglib.clearCaches(); + baseConfig.cache!.clear(); + + for (const mod of ctx.modules) { + if (mod.id && virtualFiles.has(mod.id)) { + virtualFiles.set(mod.id, createDeferredPromise()); + } + } + }, + async buildStart(inputOptions) { if (isBuild && linked && !isSSRBuild) { - // Is this needed? - //this.addWatchFile(serverMetaFile); - try { serverManifest = JSON.parse( (await store.get(manifestFileName))! @@ -433,7 +449,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { let { code } = compiled; if (query !== browserEntryQuery && devServer) { - code += `\nif (import.meta.hot) import.meta.hot.accept();`; + code += `\nif (import.meta.hot) import.meta.hot.accept(() => {});`; } if (devServer) { @@ -443,7 +459,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { path.sep + (templateName === "index" ? "" : `${templateName}.`); - for (const file of meta.watchFiles!) { + for (const file of meta.watchFiles) { this.addWatchFile(file); } @@ -454,7 +470,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { `${optionalFilePrefix}marko-tag.json`, ]); - transformWatchFiles.set(id, meta.watchFiles!); + transformWatchFiles.set(id, meta.watchFiles); } return { code, map, meta: isBuild ? { source } : undefined }; }, @@ -604,6 +620,22 @@ function getBasenameWithoutExt(file: string): string { return file.slice(baseStart, extStart); } +function createDeferredPromise() { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }) as DeferredPromise; + promise.resolve = resolve; + promise.reject = reject; + return promise; +} + +function isDeferredPromise(obj: unknown): obj is DeferredPromise { + return typeof (obj as Promise)?.then === "function"; +} + function isEmpty(obj: unknown) { for (const _ in obj as Record) { return false;