cfg-test
Version:
In-source testing using Node.js Test Runner
241 lines (202 loc) • 6.15 kB
text/typescript
import { existsSync, readFileSync } from "node:fs";
import { createRequire, register as load } from "node:module";
import { resolve } from "node:path";
import { sep } from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import type { Config } from "./config";
import { testEnv } from "./define";
import * as log from "./log";
const ARGV = ["/path/to/node", "/path/to/file"];
const fileIndex = ARGV.indexOf("/path/to/file");
const cwd = process.cwd();
const cwdUrl = pathToFileURL(
cwd.endsWith(sep) ? sep : (cwd + sep /*not file*/),
);
const require = createRequire(cwdUrl);
const parentUrl = cwdUrl.toString();
export interface RegisterOptions {
readonly argv?: readonly string[] | undefined;
readonly execArgv?: readonly string[] | undefined;
}
export function register(options: RegisterOptions | undefined = {}) {
const argv = options.argv || process.argv;
if (!(fileIndex in argv)) {
return;
}
const file = resolve(argv[fileIndex]!);
const execArgv = options.execArgv || process.execArgv;
const nodeOptions = `,${
process.env["NODE_OPTIONS"]
? execArgv.concat(process.env["NODE_OPTIONS"].split(/\s/g))
: execArgv
},`;
// ... --import cfg-test ...
const isEsmMode = /,--import,cfg-test[,/]/.test(nodeOptions);
const isWatchMode = /,--watch,/.test(nodeOptions);
const isDTsFile = file.endsWith(".d.ts");
const isTypeScript = /\.[cm]?tsx?$/i.test(file);
log.debug(() => [
`esm mode -> ${isEsmMode}`,
`watch mode -> ${isWatchMode}`,
`typescript file -> ${isTypeScript}`,
`declare file -> ${isDTsFile}`,
`argv -> ${argv.map(a => JSON.stringify(a)).join(" ")}`,
`execArgv -> ${execArgv.map(a => JSON.stringify(a)).join(" ")}`,
`cwd -> ${JSON.stringify(cwd)}`,
`parentUrl -> ${JSON.stringify(parentUrl)}`,
`target file -> ${JSON.stringify(file)}`,
]);
if (
isEsmMode
// @ts-expect-error
&& __IS_ESM_MODE__ !== true
) {
log.error(() => ["Cannot import `cfg-test` in CommonJS"]);
process.exit(1);
}
// env
const env = {
...testEnv,
CFG_TEST_CFG: process.env.CFG_TEST_CFG ?? `${[
".config/cfg-test",
".config/cfg-test/config",
"config/cfg-test",
"config/cfg-test/config",
"cfg-test",
]}`,
CFG_TEST_FILE: file,
};
if (isEsmMode) {
Object.assign(env, {
CFG_TEST_URL: pathToFileURL(file),
});
}
Object.assign(env, {
CFG_TEST_WATCH: `${isWatchMode}`,
});
const originalEnv = { ...process.env };
log.debug(() =>
Object.entries(env)
.filter(([k, v]) => [undefined, v].includes(originalEnv[k]))
.map(([k, v]) => `Added env.${k}=${JSON.stringify(v)} by cfg-test.`)
);
log.warn(() =>
Object.entries(env)
.filter(([k, v]) => [undefined, v].every(v => v !== originalEnv[k]))
.map(([k, v]) => `Updated env.${k}=${JSON.stringify(v)} by cfg-test.`)
);
Object.assign(process.env, env);
// utils
const cfgTest: CfgTest = new Proxy(require("node:test"), {
get(target, p, receiver) {
switch (p) {
case "url":
return process.env.CFG_TEST_URL;
case "file":
return process.env.CFG_TEST_FILE;
case "watch":
return process.env.CFG_TEST_WATCH === "true";
case "assert":
return require("node:assert/strict");
default:
return Reflect.get(target, p, receiver);
}
},
});
global.cfgTest = cfgTest;
// config
let cfg: Config | undefined;
for (const id of process.env.CFG_TEST_CFG!.split(",")) {
const cfgPath = id.endsWith(".json") ? id : `${id}.json`;
if (existsSync(cfgPath)) {
cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
break;
}
}
if (cfg && cfg.env) {
for (const [key, value] of Object.entries(cfg.env)) {
if (typeof value !== "string") {
continue;
}
if (process.env[key] === undefined) {
log.debug(() => [`Added env.${key} by config file.`]);
} else {
log.warn(() => [`Updated env.${key} by config file.`]);
}
process.env[key] = value;
}
}
if (cfg && cfg.globals) {
for (const [key, value] of Object.entries(cfg.globals)) {
if (key in global) {
log.warn(() => [`Updated global.${key} by config file.`]);
} else {
log.debug(() => [`Added global.${key} by config file.`]);
// @ts-expect-error
global[key] = value;
}
}
}
if (cfg && cfg.import) {
if (!Array.isArray(cfg.import)) {
cfg.import = [cfg.import];
}
for (const id of cfg.import) {
log.debug(() => [`Imported module ${id} by config file.`]);
load(id, parentUrl);
}
}
if (cfg && cfg.require) {
if (!Array.isArray(cfg.require)) {
cfg.require = [cfg.require];
}
for (const id of cfg.require) {
log.debug(() => [`Required module ${id} by config file.`]);
require(id);
}
}
const ctx = {
log,
argv,
file,
execArgv,
isEsmMode,
parentUrl,
isWatchMode,
isTypeScript,
import(id: string): void {
try {
log.debug(() => [`Register ESM module ${id}.`]);
load(id, parentUrl);
log.debug(() => [`Registered ESM module ${id}.`]);
} catch (e) {
log.error(() => [`Cannot register ESM module ${id}.`]);
throw e;
}
},
require(id: string, onLoad: (mod: any) => void): void {
try {
log.debug(() => [`Register CJS module ${id}`]);
const mod = require(id);
log.debug(() => [`Loaded CJS module ${id}`]);
onLoad(mod);
log.debug(() => [`Registered CJS module ${id}`]);
} catch (e) {
log.error(() => [`Cannot register CJS module ${id}.`]);
throw e;
}
},
};
if (isDTsFile) {
if (ctx.isEsmMode) {
ctx.import("cfg-test/dts-loader");
} else {
// CommonJS implementation of `cfg-test/dts-loader`
require("node:module")._extensions[".ts"] = () => "";
ctx.log.debug(() => ["Registered CJS module cfg-test/dts-loader."]);
}
return;
}
return ctx;
}