globload
Version:
A Node.js module loader for importing files using glob patterns, supporting specific exports and YAML parsing.
105 lines (101 loc) • 4.76 kB
JavaScript
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import yaml from "js-yaml";
import { glob } from "tinyglobby";
//#region src/utils.ts
function normalizePath(p) {
return path.posix.normalize(p).replace(/\\/g, "/");
}
function createURL(input, base) {
if (compareNodeVersions(process.version, "v22.1") >= 0) return URL.parse(input, base);
return new URL(input, base);
}
function compareNodeVersions(v1, v2) {
const parseVersion = (vStr) => {
const normalized = vStr.startsWith("v") ? vStr.substring(1) : vStr;
const parts = normalized.split(".").map(Number);
return {
major: parts[0] || 0,
minor: parts[1] || 0,
patch: parts[2] || 0
};
};
const ver1 = parseVersion(v1);
const ver2 = parseVersion(v2);
if (ver1.major !== ver2.major) return ver1.major > ver2.major ? 1 : -1;
if (ver1.minor !== ver2.minor) return ver1.minor > ver2.minor ? 1 : -1;
if (ver1.patch !== ver2.patch) return ver1.patch > ver2.patch ? 1 : -1;
return 0;
}
//#endregion
//#region src/loader.ts
const resolve = (specifier, context, nextResolve) => {
const url = createURL(specifier, context.parentURL);
if (!url?.searchParams.has("glob")) return nextResolve(specifier, context);
return {
shortCircuit: true,
url: url.toString(),
importAttributes: context.importAttributes
};
};
const load = async (url, context, nextLoad) => {
const absoluteUrl = createURL(url);
if (!absoluteUrl?.searchParams.has("glob")) return nextLoad(url, context);
const absoluteGlobPattern = normalizePath(fileURLToPath(absoluteUrl.toString()));
const searchParams = absoluteUrl.searchParams;
const mode = searchParams.has("eager") ? "eager" : "lazy";
const importKey = searchParams.get("import");
const files = await glob(absoluteGlobPattern, { absolute: true });
let importCounter = 0;
const importStatements = [];
const properties = [];
const importAttributesJson = JSON.stringify(context.importAttributes);
const hasImportAttributes = Object.keys(context.importAttributes).length > 0;
for (const absoluteFilePath of files) {
const fileExtension = path.extname(absoluteFilePath).toLowerCase();
const isYaml = [".yaml", ".yml"].includes(fileExtension);
const moduleFileUrl = pathToFileURL(absoluteFilePath).toString();
const relativePathKey = normalizePath(path.relative(process.cwd(), absoluteFilePath));
if (isYaml) if (mode === "eager") {
const fileContent = await fs.readFile(absoluteFilePath, "utf-8");
let parsedContent = yaml.load(fileContent);
if (importKey && importKey !== "default" && typeof parsedContent === "object" && parsedContent !== null) parsedContent = parsedContent[importKey];
properties.push(`'${relativePathKey}': ${JSON.stringify(parsedContent)}`);
} else {
const physicalPath = normalizePath(fileURLToPath(moduleFileUrl));
let propertyValue;
if (importKey && importKey !== "default") propertyValue = `async () => { const c = await import('node:fs/promises').then(({ readFile }) => readFile('${physicalPath}', 'utf-8')); const p = await import('js-yaml').then(({ load }) => load(c)); return typeof p === 'object' && p !== null ? p['${importKey}'] : undefined; }`;
else propertyValue = `async () => { const c = await import('node:fs/promises').then(({ readFile }) => readFile('${physicalPath}', 'utf-8')); return await import('js-yaml').then(({ load }) => load(c)); }`;
properties.push(`'${relativePathKey}': ${propertyValue}`);
}
else if (mode === "eager") {
const importName = `__globbed_eager_${importCounter++}`;
let importStatement;
if (importKey) if (importKey === "default") importStatement = `import { default as ${importName} } from '${moduleFileUrl}' ${hasImportAttributes ? `with ${importAttributesJson}` : ""};`;
else importStatement = `import { ${importKey} as ${importName} } from '${moduleFileUrl}' ${hasImportAttributes ? `with ${importAttributesJson}` : ""};`;
else importStatement = `import * as ${importName} from '${moduleFileUrl}' ${hasImportAttributes ? `with ${importAttributesJson}` : ""};`;
importStatements.push(importStatement);
properties.push(`'${relativePathKey}': ${importName}`);
} else {
let importAccess = "";
if (importKey) if (importKey === "default") importAccess = ".then(m => m.default)";
else importAccess = `.then(m => m.${importKey})`;
properties.push(`'${relativePathKey}': () => import('${moduleFileUrl}' ${hasImportAttributes ? `, { with: ${importAttributesJson} }` : ""})${importAccess}`);
}
}
const source = `
// Dynamically generated by globload
${importStatements.join("\n")}
export default {
${properties.join(",\n")}
};
`;
return {
shortCircuit: true,
format: "module",
source
};
};
//#endregion
export { load, resolve };