2

Many popular node packages support writing configuration files in JS or TS, like webpack, vite. Now I'm also trying to create a package supporting JS and TS configuration file, which would be used as: my-package --config path/to/config.ts.
I first tried using require directly, which failed for TS (that's quite an obvious result, since no loader for TS is specified).
So I then tried using typescript package to transpile the config file and using require-from-string to load the module, which doesn't work either when the specified config file import some other modules.
So my current problem is: how to get the configuration module resolved under current context.
Note that this package is designed to work like webpack-cli, being added to devDependencies by other packages and used as a tool for development. So the current context refers to the package that installs this package.
Here are some relative posts I've looked through and tried (so don't propose a duplicate to these questions):

0x269
  • 688
  • 8
  • 20

1 Answers1

2

After probing into the source of webpack-cli, I finally get this done with the help of rechoir. Here's the code:

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);
}

The implementation is mainly borrowed from webpack-cli. Here are the sources:

0x269
  • 688
  • 8
  • 20