Skip to content

Commit 7f67152

Browse files
committed
Make Jest configs runnable scripts
Getting Jest to work with ESM modules has been painful, and the guide at https://jestjs.io/docs/ecmascript-modules doesn't seem to work when the Jest config is in a TypeScript file. Since I'm stubborn, I decided to try to find a workaround, and I found this one! We make the config a runnable script that runs the Jest command, rather than relying on the Jest command to read the config. This is a bit funny, but it seems to work, because Jest requires configuration to be JSON-serializable: https://jestjs.io/docs/configuration and the command-line config option (https://jestjs.io/docs/cli#--configpath) accepts a JSON string!
1 parent 72718d9 commit 7f67152

File tree

8 files changed

+105
-158
lines changed

8 files changed

+105
-158
lines changed

workspaces/adventure-pack/jest.config.js

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Config } from "jest";
2+
3+
const config: Config = {
4+
preset: "ts-jest",
5+
testEnvironment: "node",
6+
};
7+
8+
export default config;
9+
10+
// Hack to make this config runnable as a script, since the combination of
11+
// Jest, ESM, and a TypeScript config has been painful to get working otherwise.
12+
import("node:process").then(async ({ default: process }) => {
13+
if (import.meta.filename !== process.argv[1]) {
14+
return;
15+
}
16+
17+
try {
18+
const { spawnWithSafeStdio } = await import(
19+
"@code-chronicles/util/spawnWithSafeStdio"
20+
);
21+
await spawnWithSafeStdio(
22+
"jest",
23+
["--color", "-c", JSON.stringify(config), ...process.argv.slice(2)],
24+
{
25+
stdio: "inherit",
26+
env: {
27+
...process.env,
28+
NODE_OPTIONS: [
29+
"--experimental-vm-modules",
30+
process.env.NODE_OPTIONS?.trim(),
31+
]
32+
.filter(Boolean)
33+
.join(" "),
34+
},
35+
},
36+
);
37+
} catch (err) {
38+
console.error(
39+
(err as Record<string, unknown> | null | undefined)?.message ?? err,
40+
);
41+
// eslint-disable-next-line require-atomic-updates -- Updating `process.exitCode` on error is logical.
42+
process.exitCode = 1;
43+
}
44+
});

workspaces/adventure-pack/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@
4747
"goodies:python3:test": "bash goodies/python3/test.sh",
4848
"goodies:typescript:format": "prettier --color --write goodies/typescript",
4949
"goodies:typescript:install": "yarn",
50-
"goodies:typescript:test": "jest --color \"/goodies/typescript/\"",
50+
"goodies:typescript:test": "tsx ./jest.config.ts \"/goodies/typescript/\"",
5151
"build-app": "tsx src/scripts/build/main.ts",
5252
"build-chrome-extension": "tsx src/scripts/build/buildChromeExtension.ts",
53-
"package-goodies:test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --color --testPathIgnorePatterns=\"<rootDir>/goodies/\"",
53+
"package-goodies:test": "tsx ./jest.config.ts --testPathIgnorePatterns=\"<rootDir>/goodies/\"",
5454
"format": "yarn goodies:java:format && yarn goodies:kotlin:format && yarn goodies:python3:format && yarn goodies:typescript:format && prettier --color --write .",
5555
"lint": "eslint --color --max-warnings=0 .",
5656
"postinstall": "yarn goodies:java:install && yarn goodies:kotlin:install && yarn goodies:python3:install",

workspaces/util/jest.config.mts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Config } from "jest";
2+
3+
const config: Config = {
4+
preset: "ts-jest",
5+
testEnvironment: "node",
6+
};
7+
8+
export default config;
9+
10+
// Hack to make this config runnable as a script, since the combination of
11+
// Jest, ESM, and a TypeScript config has been painful to get working otherwise.
12+
import("node:process").then(async ({ default: process }) => {
13+
if (import.meta.filename !== process.argv[1]) {
14+
return;
15+
}
16+
17+
try {
18+
const { spawnWithSafeStdio } = await import(
19+
"@code-chronicles/util/spawnWithSafeStdio"
20+
);
21+
await spawnWithSafeStdio(
22+
"jest",
23+
["--color", "-c", JSON.stringify(config), ...process.argv.slice(2)],
24+
{
25+
stdio: "inherit",
26+
env: {
27+
...process.env,
28+
NODE_OPTIONS: [
29+
"--experimental-vm-modules",
30+
process.env.NODE_OPTIONS?.trim(),
31+
]
32+
.filter(Boolean)
33+
.join(" "),
34+
},
35+
},
36+
);
37+
} catch (err) {
38+
console.error(
39+
(err as Record<string, unknown> | null | undefined)?.message ?? err,
40+
);
41+
// eslint-disable-next-line require-atomic-updates -- Updating `process.exitCode` on error is logical.
42+
process.exitCode = 1;
43+
}
44+
});

workspaces/util/jest.config.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

workspaces/util/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"scripts": {
2020
"format": "prettier --color --write .",
2121
"lint": "eslint --color --max-warnings=0 .",
22-
"test": "jest --color .",
22+
"test": "tsx ./jest.config.mts",
2323
"typecheck": "tsc --pretty --project ."
2424
},
2525
"dependencies": {
@@ -35,7 +35,7 @@
3535
"jest": "29.7.0",
3636
"prettier": "3.3.3",
3737
"ts-jest": "29.2.5",
38-
"ts-node": "10.9.2",
38+
"tsx": "4.19.1",
3939
"type-fest": "4.26.1",
4040
"typescript": "5.6.2"
4141
}

workspaces/util/src/spawnWithSafeStdio.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import process from "node:process";
22
import { spawn, type SpawnOptions } from "node:child_process";
33

4+
// TODO: audit callsites since the behavior is changing
5+
46
export function spawnWithSafeStdio(
57
command: string,
68
args: readonly string[],
7-
options?: Omit<SpawnOptions, "stdio">,
9+
options?: SpawnOptions,
810
): Promise<void> {
911
return new Promise((resolve, reject) => {
1012
const childProcess = spawn(command, args, {
11-
...options,
13+
// When using "inherit" mode, it sometimes broke the interaction with
14+
// the actions/github-script@v7 GitHub Action, so we default to piping.
15+
stdio: ["ignore", "pipe", "pipe"],
1216

1317
// Without a shell specified, many comands seem to fail to spawn on
1418
// Windows. I verified that it's not a PATH issue. It seems like it's
1519
// probably https://github.com/nodejs/node-v0.x-archive/issues/5841
1620
...(process.platform === "win32" && { shell: options?.shell ?? "bash" }),
1721

18-
// When using "inherit" mode, it sometimes broke the interaction with
19-
// the actions/github-script@v7 GitHub Action...
20-
stdio: ["ignore", "pipe", "pipe"],
22+
...options,
2123
});
2224

23-
childProcess.stdout.pipe(process.stdout);
24-
childProcess.stderr.pipe(process.stderr);
25+
childProcess.stdout?.pipe(process.stdout);
26+
childProcess.stderr?.pipe(process.stderr);
2527

2628
childProcess.on("error", reject);
2729
childProcess.on("exit", (exitCode) => {

0 commit comments

Comments
 (0)