fix: dev cmd didn't watch files due to import mangling

Appearently the esModuleInterop flag in TypeScript is not compatible
with node-watch, and the import helper was mangling the import as a
result and causing an error in the dev command that resulted in files
not being watched.

This fixes that import issue and adds an e2e test to help prevent a
regression of this issue in the future.
This commit is contained in:
M. George Hansen 2020-06-09 13:56:11 -07:00
parent af44f1ebf2
commit b559d5e21a
Signed by: mgeorgehansen
SSH key fingerprint: SHA256:JlIGiQLPyQ2RHTH3a2oVlb20Xkh9Glr8DUF4YTXHJxM
7 changed files with 200 additions and 5 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/dist
/.test-dist
public/
node_modules/

View file

@ -29,7 +29,9 @@
"preversion": "npm run reset && npm test",
"release": "npm version",
"reset": "npm run clean && npm ci",
"test": "ts-node --project=test/tsconfig.json test/run-tests.ts"
"test": "npm run test:unit && npm run test:e2e",
"test:unit": "cd test && ts-node --script-mode ./run-tests.ts",
"test:e2e": "cd test && ts-node --script-mode ./run-e2e.ts"
},
"devDependencies": {
"@types/node": "~10",

View file

@ -182,8 +182,8 @@ const watchFolders = async (
// Try to load node-watch, falling back to fs watch if node-watch isn't
// available.
try {
const { default: watch } = await import("node-watch");
watch(folders, { recursive: true }, listener);
const nodeWatch = await import("node-watch");
nodeWatch.default(folders, { recursive: true }, listener);
return;
} catch (error) {
if (error.code !== "MODULE_NOT_FOUND") {

171
test/e2e/dev.tsx Normal file
View file

@ -0,0 +1,171 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { ChildProcess, spawn } from "child_process";
import { promises as fs } from "fs";
import * as path from "path";
import { testSuite } from "../lib";
const TEST_DIST_PATH = path.resolve(__dirname, "..", "..", ".test-dist");
const wait = async (timeMs: number): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => resolve(), timeMs);
});
};
const withTempDir = async (
op: (tempDirPath: string) => Promise<void> | void
): Promise<void> => {
await fs.mkdir(TEST_DIST_PATH, { recursive: true });
const tempDirPath = await fs.mkdtemp(`${TEST_DIST_PATH}/`);
try {
await op(tempDirPath);
} catch (error) {
throw new Error(`(${tempDirPath}): ${error}`);
}
};
const WEBSNACKS_REPO_ROOT = path.resolve(__dirname, "..", "..");
const WEBSNACKS_BIN_PATH = path.join(
WEBSNACKS_REPO_ROOT,
"bin",
"websnacks.js"
);
interface AsyncCommand {
complete: Promise<string>;
process: ChildProcess;
}
interface CliOptions {
cwd?: string;
timeoutMs?: number;
}
const DEFAULT_CLI_OPTIONS = {
timeoutMs: 5_000,
};
const runCommand = (
command: string,
args: string[] = [],
_options?: CliOptions
): AsyncCommand => {
const options = { ...DEFAULT_CLI_OPTIONS, ..._options };
const process = spawn(command, args, {
...options,
stdio: "pipe",
});
const complete = new Promise<string>((resolve, reject) => {
let threwError = false;
let stdout = "";
process.stdout.on("data", (data) => {
stdout += data.toString();
});
process.stderr.on("data", (data) => {
threwError = true;
process.kill();
reject(new Error(`command output to stderr: ${data.toString()}`));
});
const timer = setTimeout(() => {
threwError = true;
process.kill();
reject(new Error(`max timeout of ${options.timeoutMs}ms reached`));
}, options.timeoutMs);
process.on("exit", (code) => {
if (threwError) {
return;
}
clearTimeout(timer);
if (code !== null && code !== 0) {
reject(new Error(`command exited with non-zero code: ${code}`));
return;
}
resolve(stdout);
});
process.on("error", (error) => {
clearTimeout(timer);
if (!threwError) {
reject(new Error(`command errored: ${error}`));
threwError = true;
}
});
});
return {
complete,
process,
};
};
testSuite("dev command", ({ test, expect }) => {
test("starts without throwing error", async () => {
await withTempDir(async (tempDirPath) => {
await fs.writeFile(
path.join(tempDirPath, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
esModuleInterop: true,
module: "CommonJS",
moduleResolution: "node",
jsx: "react",
jsxFactory: "createElement",
target: "ES2018",
lib: ["ES2018"],
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
noImplicitReturns: true,
noFallthroughCasesInSwitch: true,
},
include: ["components/**/*", "pages/**/*"],
}),
{
encoding: "utf8",
}
);
await fs.writeFile(
path.join(tempDirPath, "websnacks.ts"),
`
import { Config } from "${WEBSNACKS_REPO_ROOT}";
const config: Config = {
watch: [],
};
export = config;
`,
{
encoding: "utf8",
}
);
const pagesPath = path.join(tempDirPath, "pages");
await fs.mkdir(pagesPath);
await fs.writeFile(
path.join(pagesPath, "index.tsx"),
`
import { createElement } from "${WEBSNACKS_REPO_ROOT}";
export const page = () => <html />;
`,
{
encoding: "utf8",
}
);
const cmd = runCommand(
"node",
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "dev"],
{
cwd: tempDirPath,
}
);
// FIXME: This test is a bit brittle due to relying on timeouts.
await wait(2_000);
cmd.process.kill();
const stdout = await cmd.complete;
expect(stdout).toStartWith("Listening at");
});
});
});

23
test/run-e2e.ts Normal file
View file

@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { fork } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { shuffle } from "./lib/utils";
const TEST_SUITES_DIR = path.join(__dirname, "e2e");
const files = fs.readdirSync(TEST_SUITES_DIR);
// Shuffle test suites to detect ordering dependencies between them.
shuffle(files);
for (const file of files) {
const fullPath = path.join(TEST_SUITES_DIR, file);
fork(path.relative(process.cwd(), fullPath)).on("exit", (code) => {
if (code !== 0) {
process.exitCode = 1;
}
});
}

View file

@ -10,7 +10,6 @@ import * as path from "path";
import { shuffle } from "./lib/utils";
const TEST_SUITES_DIR = path.join(__dirname, "test-suites");
const files = fs.readdirSync(TEST_SUITES_DIR);
// Shuffle test suites to detect ordering dependencies between them.
shuffle(files);

View file

@ -1,6 +1,5 @@
{
"compilerOptions": {
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "node",
"target": "ES2018",