node.js - Hur man laddar moduler från dynamiska TypeScript-filer


Många populära Node-paket stöder att skriva konfigurationsfiler i JS eller TS, som webpack, vite. Nu försöker jag också skapa ett paket som stöder JS- och TS-konfigurationsfiler, som skulle användas som: my-package --config path/to/config.ts.
Jag försökte först använda require direkt, vilket misslyckades för TS (det är ganska uppenbart, eftersom ingen loader för TS är specificerad).
Så jag försökte sedan använda typescript-paketet för att transpilerade konfigurationsfilen och använda require-from-string för att ladda modulen, vilket inte heller fungerar när den angivna konfigurationsfilen importerar några andra moduler.
Så mitt nuvarande problem är: hur man får konfigurationsmodulen att lösas under nuvarande kontext.
Observera att detta paket är utformat för att fungera som webpack-cli, genom att läggas till i devDependencies av andra paket och användas som ett verktyg för utveckling. Så nuvarande kontext hänvisar till paketet som installerar detta paket.
Här är några relaterade inlägg som jag har gått igenom och försökt (så föreslå inte en duplicering av dessa frågor):

  • Hur man läser .ts-konfigurationsfilen från projektets rot i npm-paketet (ESM)
  • Typescript dynamisk modul-inläsning vid körning

Svar #1

Efter att ha undersökt källan till webpack-cli lyckades jag äntligen få detta gjort med hjälp av rechoir. Här är koden:

import interpret from "interpret";
import rechoir from "rechoir";
import AsyncFs from "fs/promises";
import Path from "path";
import { pathToFileURL } from "URL";

// My package is build with `webpack`, so I need the following to bypass its dynamic require constraint.
declare var __non_webpack_require__: ((id: string) => any) | undefined;
const dynamicRequire = typeof __non_webpack_require__ === "function" ? __non_webpack_require__ : require;

async function tryRequireThenImport(module: string): Promise<any> {
    let result;
    try {
        result = dynamicRequire(module);
    } catch (error: any) {
        let importEsm: ((module: string) => Promise<{ default: any }>) | undefined;
        try {
            importEsm = new Function("id", "return import(id);") as any;
        } catch (e) {
            importEsm = undefined;
        }
        if (error.code === "ERR_REQUIRE_ESM" && importEsm) {
            const urlForConfig = pathToFileURL(module).href;
            result = (await importEsm(urlForConfig)).default;
            return result;
        }
        throw error;
    }
    // For babel/typescript
    if (result && typeof result === "object" && "default" in result)
        result = result.default || {};
    return result || {};
}

async function loadModule(path: string): Promise<any> {
    const ext = Path.extname(path);
    if (ext === ".json") {
        const content = await AsyncFs.readFile(path, "utf-8");
        return JSON.parse(content);
    }
    if (!Object.keys(interpret.jsVariants).includes(ext))
        throw new Error(`Unsupported file type: ${ext}`);
    // This is the key point that gets the configuration module loaded under current context
    rechoir.prepare(interpret.jsVariants, path);
    return await tryRequireThenImport(path);
}

Implementeringen är huvudsakligen lånad från webpack-cli. Här är källorna:

  • försökRequireSedanImportera
  • laddaKonfigurationEfterSökväg