259 lines
7.5 KiB
JavaScript
259 lines
7.5 KiB
JavaScript
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import module from "node:module";
|
||
import { MessageChannel } from "node:worker_threads";
|
||
|
||
import { TemplatePath } from "@11ty/eleventy-utils";
|
||
|
||
import EleventyBaseError from "../Errors/EleventyBaseError.js";
|
||
import eventBus from "../EventBus.js";
|
||
|
||
class EleventyImportError extends EleventyBaseError {}
|
||
|
||
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
|
||
// ENV variable for https://github.com/11ty/eleventy/issues/3371
|
||
if ("register" in module && !process?.env?.ELEVENTY_SKIP_ESM_RESOLVER) {
|
||
module.register("./EsmResolver.js", import.meta.url, {
|
||
parentURL: import.meta.url,
|
||
data: {
|
||
port: port2,
|
||
},
|
||
transferList: [port2],
|
||
});
|
||
}
|
||
|
||
// important to clear the require.cache in CJS projects
|
||
const require = module.createRequire(import.meta.url);
|
||
|
||
const requestPromiseCache = new Map();
|
||
|
||
function getImportErrorMessage(filePath, type) {
|
||
return `There was a problem importing '${path.relative(".", filePath)}' via ${type}`;
|
||
}
|
||
|
||
// 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.
|
||
/**
|
||
*
|
||
* @returns {string|undefined}
|
||
*/
|
||
function loadContents(path, options = {}) {
|
||
let rawInput;
|
||
/** @type {string} */
|
||
let encoding = "utf8"; // JSON is utf8
|
||
if (options?.encoding || options?.encoding === null) {
|
||
encoding = options.encoding;
|
||
}
|
||
|
||
try {
|
||
// @ts-expect-error This is an error in the upstream types
|
||
rawInput = fs.readFileSync(path, encoding);
|
||
} catch (error) {
|
||
// @ts-expect-error Temporary
|
||
if (error?.code === "ENOENT") {
|
||
// if file does not exist, return nothing
|
||
return;
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
|
||
// Can return a buffer, string, etc
|
||
if (typeof rawInput === "string") {
|
||
rawInput = rawInput.trim();
|
||
}
|
||
|
||
return rawInput;
|
||
}
|
||
|
||
let lastModifiedPaths = new Map();
|
||
eventBus.on("eleventy.importCacheReset", (fileQueue) => {
|
||
for (let filePath of fileQueue) {
|
||
let absolutePath = TemplatePath.absolutePath(filePath);
|
||
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 || {})) {
|
||
delete require.cache[absolutePath];
|
||
}
|
||
}
|
||
});
|
||
|
||
// raw means we don’t normalize away the `default` export
|
||
async function dynamicImportAbsolutePath(absolutePath, options = {}) {
|
||
let { type, returnRaw, cacheBust } = Object.assign(
|
||
{
|
||
type: undefined,
|
||
returnRaw: false,
|
||
cacheBust: false, // force cache bust
|
||
},
|
||
options,
|
||
);
|
||
|
||
// Short circuit for JSON files (that are optional and can be empty)
|
||
if (absolutePath.endsWith(".json") || type === "json") {
|
||
try {
|
||
// https://v8.dev/features/import-assertions#dynamic-import() is still experimental in Node 20
|
||
let rawInput = loadContents(absolutePath);
|
||
if (!rawInput) {
|
||
// should not error when file exists but is _empty_
|
||
return;
|
||
}
|
||
return JSON.parse(rawInput);
|
||
} catch (e) {
|
||
return Promise.reject(
|
||
new EleventyImportError(getImportErrorMessage(absolutePath, "fs.readFile(json)"), e),
|
||
);
|
||
}
|
||
}
|
||
|
||
// Removed a `require` short circuit from this piece originally added
|
||
// in https://github.com/11ty/eleventy/pull/3493 Was a bit faster but
|
||
// error messaging was worse for require(esm)
|
||
|
||
let urlPath;
|
||
try {
|
||
let u = new URL(`file:${absolutePath}`);
|
||
|
||
// Bust the import cache if this is the last modified file (or cache busting is forced)
|
||
if (cacheBust) {
|
||
lastModifiedPaths.set(absolutePath, Date.now());
|
||
}
|
||
|
||
if (cacheBust || lastModifiedPaths.has(absolutePath)) {
|
||
u.searchParams.set("_cache_bust", lastModifiedPaths.get(absolutePath));
|
||
}
|
||
|
||
urlPath = u.toString();
|
||
} catch (e) {
|
||
urlPath = absolutePath;
|
||
}
|
||
|
||
let promise;
|
||
if (requestPromiseCache.has(urlPath)) {
|
||
promise = requestPromiseCache.get(urlPath);
|
||
} else {
|
||
promise = import(urlPath);
|
||
requestPromiseCache.set(urlPath, promise);
|
||
}
|
||
|
||
return promise.then(
|
||
(target) => {
|
||
if (returnRaw) {
|
||
return target;
|
||
}
|
||
|
||
// If the only export is `default`, elevate to top (for ESM and CJS)
|
||
if (Object.keys(target).length === 1 && "default" in target) {
|
||
return target.default;
|
||
}
|
||
|
||
// When using import() on a CommonJS file that exports an object sometimes it
|
||
// returns duplicated values in `default` key, e.g. `{ default: {key: value}, key: value }`
|
||
|
||
// A few examples:
|
||
// module.exports = { key: false };
|
||
// returns `{ default: {key: false}, key: false }` as not expected.
|
||
// module.exports = { key: true };
|
||
// module.exports = { key: null };
|
||
// module.exports = { key: undefined };
|
||
// module.exports = { key: class {} };
|
||
|
||
// A few examples where it does not duplicate:
|
||
// module.exports = { key: 1 };
|
||
// returns `{ default: {key: 1} }` as expected.
|
||
// module.exports = { key: "value" };
|
||
// module.exports = { key: {} };
|
||
// module.exports = { key: [] };
|
||
|
||
if (type === "cjs" && "default" in target) {
|
||
let match = true;
|
||
for (let key in target) {
|
||
if (key === "default") {
|
||
continue;
|
||
}
|
||
if (key === "module.exports") {
|
||
continue;
|
||
}
|
||
if (target[key] !== target.default[key]) {
|
||
match = false;
|
||
}
|
||
}
|
||
|
||
if (match) {
|
||
return target.default;
|
||
}
|
||
}
|
||
|
||
// Otherwise return { default: value, named: value }
|
||
// Object.assign here so we can add things to it in JavaScript.js
|
||
return Object.assign({}, target);
|
||
},
|
||
(error) => {
|
||
return Promise.reject(
|
||
new EleventyImportError(getImportErrorMessage(absolutePath, `import(${type})`), error),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
function normalizeFilePathInEleventyPackage(file) {
|
||
// Back up relative paths from ./src/Util/Require.js
|
||
return path.resolve(fileURLToPath(import.meta.url), "../../../", file);
|
||
}
|
||
|
||
async function dynamicImportFromEleventyPackage(file) {
|
||
// points to files relative to the top level Eleventy directory
|
||
let filePath = normalizeFilePathInEleventyPackage(file);
|
||
|
||
// Returns promise
|
||
return dynamicImportAbsolutePath(filePath, { type: "esm" });
|
||
}
|
||
|
||
async function dynamicImport(localPath, type, options = {}) {
|
||
let absolutePath = TemplatePath.absolutePath(localPath);
|
||
options.type = type;
|
||
|
||
// Returns promise
|
||
return dynamicImportAbsolutePath(absolutePath, options);
|
||
}
|
||
|
||
/* Used to import default Eleventy configuration file, raw means we don’t normalize away the `default` export */
|
||
async function dynamicImportRawFromEleventyPackage(file) {
|
||
// points to files relative to the top level Eleventy directory
|
||
let filePath = normalizeFilePathInEleventyPackage(file);
|
||
|
||
// Returns promise
|
||
return dynamicImportAbsolutePath(filePath, { type: "esm", returnRaw: true });
|
||
}
|
||
|
||
/* Used to import app configuration files, raw means we don’t normalize away the `default` export */
|
||
async function dynamicImportRaw(localPath, type) {
|
||
let absolutePath = TemplatePath.absolutePath(localPath);
|
||
|
||
// Returns promise
|
||
return dynamicImportAbsolutePath(absolutePath, { type, returnRaw: true });
|
||
}
|
||
|
||
export {
|
||
loadContents as EleventyLoadContent,
|
||
dynamicImport as EleventyImport,
|
||
dynamicImportRaw as EleventyImportRaw,
|
||
normalizeFilePathInEleventyPackage,
|
||
|
||
// no longer used in core
|
||
dynamicImportFromEleventyPackage as EleventyImportFromEleventy,
|
||
dynamicImportRawFromEleventyPackage as EleventyImportRawFromEleventy,
|
||
};
|