From b5571f0912201c3d278d1f02de62179ad23e6069 Mon Sep 17 00:00:00 2001 From: Zach Leatherman Date: Wed, 19 Jun 2024 15:25:13 -0500 Subject: [PATCH] Fixes #3270 --- src/Util/EsmResolver.js | 49 +++++++++++++++++++++++++++++++++++++++++ src/Util/Require.js | 27 ++++++++++++++++++++--- 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/Util/EsmResolver.js diff --git a/src/Util/EsmResolver.js b/src/Util/EsmResolver.js new file mode 100644 index 000000000..a1c88ae8a --- /dev/null +++ b/src/Util/EsmResolver.js @@ -0,0 +1,49 @@ +import debugUtil from "debug"; + +const debug = debugUtil("Eleventy:EsmResolver"); + +let lastModifiedPaths = new Map(); +export async function initialize({ port }) { + // From `eleventy.importCacheReset` event in Require.js + port.on("message", ({ path, newDate }) => { + lastModifiedPaths.set(path, newDate); + }); +} + +// Fixes issue https://github.com/11ty/eleventy/issues/3270 +// Docs: https://nodejs.org/docs/latest/api/module.html#resolvespecifier-context-nextresolve +export async function resolve(specifier, context, nextResolve) { + try { + // Not a relative import and not a file import + // Or from node_modules (perhaps better to check if the specifier is in the project directory instead) + if ( + (!specifier.startsWith("./") && !specifier.startsWith("file:")) || + context.parentURL.includes("/node_modules/") + ) { + return nextResolve(specifier); + } + + let fileUrl = new URL(specifier, context.parentURL); + if (fileUrl.searchParams.has("_cache_bust")) { + // already is cache busted outside resolver (wider compat, url was changed prior to import, probably in Require.js) + return nextResolve(specifier); + } + + let absolutePath = fileUrl.pathname; + // Bust the import cache if this is a recently modified file + if (lastModifiedPaths.has(absolutePath)) { + fileUrl.search = ""; // delete existing searchparams + fileUrl.searchParams.set("_cache_bust", lastModifiedPaths.get(absolutePath)); + debug("Cache busting %o to %o", specifier, fileUrl.toString()); + + return nextResolve(fileUrl.toString()); + } + } catch (e) { + debug("EsmResolver Error parsing specifier (%o): %o", specifier, e); + } + + return nextResolve(specifier); +} + +// export async function load(url, context, nextLoad) { +// } diff --git a/src/Util/Require.js b/src/Util/Require.js index e651f99b3..2a3b95876 100644 --- a/src/Util/Require.js +++ b/src/Util/Require.js @@ -1,14 +1,29 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { createRequire } from "node:module"; +import module from "node:module"; +import { MessageChannel } from "node:worker_threads"; import { TemplatePath } from "@11ty/eleventy-utils"; import eventBus from "../EventBus.js"; +const { port1, port2 } = new MessageChannel(); + +// ESM Cache Buster is an enhancement that works in Node 18.19+ +// https://nodejs.org/docs/latest/api/module.html#moduleregisterspecifier-parenturl-options +// Fixes https://github.com/11ty/eleventy/issues/3270 +if ("register" in module) { + module.register("./EsmResolver.js", import.meta.url, { + data: { + port: port2, + }, + transferList: [port2], + }); +} + // important to clear the require.cache in CJS projects -const require = createRequire(import.meta.url); +const require = module.createRequire(import.meta.url); // Used for JSON imports, suffering from Node warning that import assertions experimental but also // throwing an error if you try to import() a JSON file without an import assertion. @@ -37,7 +52,13 @@ let lastModifiedPaths = new Map(); eventBus.on("eleventy.importCacheReset", (fileQueue) => { for (let filePath of fileQueue) { let absolutePath = TemplatePath.absolutePath(filePath); - lastModifiedPaths.set(absolutePath, Date.now()); + let newDate = Date.now(); + lastModifiedPaths.set(absolutePath, newDate); + + // post to EsmResolver worker thread + if (port1) { + port1.postMessage({ path: absolutePath, newDate }); + } // ESM Eleventy when using `import()` on a CJS project file still adds to require.cache if (absolutePath in (require?.cache || {})) {