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:
parent
cc16bc9bf4
commit
d4f713e5c5
7 changed files with 200 additions and 5 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
/dist
|
/dist
|
||||||
|
/.test-dist
|
||||||
public/
|
public/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@
|
||||||
"preversion": "npm run reset && npm test",
|
"preversion": "npm run reset && npm test",
|
||||||
"release": "npm version",
|
"release": "npm version",
|
||||||
"reset": "npm run clean && npm ci",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/node": "~10",
|
"@types/node": "~10",
|
||||||
|
|
|
||||||
|
|
@ -182,8 +182,8 @@ const watchFolders = async (
|
||||||
// Try to load node-watch, falling back to fs watch if node-watch isn't
|
// Try to load node-watch, falling back to fs watch if node-watch isn't
|
||||||
// available.
|
// available.
|
||||||
try {
|
try {
|
||||||
const { default: watch } = await import("node-watch");
|
const nodeWatch = await import("node-watch");
|
||||||
watch(folders, { recursive: true }, listener);
|
nodeWatch.default(folders, { recursive: true }, listener);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== "MODULE_NOT_FOUND") {
|
if (error.code !== "MODULE_NOT_FOUND") {
|
||||||
|
|
|
||||||
171
test/e2e/dev.tsx
Normal file
171
test/e2e/dev.tsx
Normal 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
23
test/run-e2e.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,6 @@ import * as path from "path";
|
||||||
import { shuffle } from "./lib/utils";
|
import { shuffle } from "./lib/utils";
|
||||||
|
|
||||||
const TEST_SUITES_DIR = path.join(__dirname, "test-suites");
|
const TEST_SUITES_DIR = path.join(__dirname, "test-suites");
|
||||||
|
|
||||||
const files = fs.readdirSync(TEST_SUITES_DIR);
|
const files = fs.readdirSync(TEST_SUITES_DIR);
|
||||||
// Shuffle test suites to detect ordering dependencies between them.
|
// Shuffle test suites to detect ordering dependencies between them.
|
||||||
shuffle(files);
|
shuffle(files);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "ES2018",
|
"target": "ES2018",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue