fix: dont require config file
Fixes a couple of issues with config files in websnacks projects. First, config files are no longer required and the dev and build commands will no longer error out if a websnacks.ts/js file doesn't exist. Second, all optional user config params are now actually optional - before some parameters were assumed to exist and would error out if not present (e.g. the "watch" parameter). Finally, e2e tests were added to prevent regressions on these issues and test helpers were extracted to a separate file.
This commit is contained in:
parent
09296464d7
commit
701f85baef
5 changed files with 410 additions and 120 deletions
|
|
@ -5,20 +5,21 @@
|
|||
|
||||
import { existsSync, promises as fs, watch } from "fs";
|
||||
import * as http from "http";
|
||||
import * as net from "net";
|
||||
import * as path from "path";
|
||||
|
||||
import { renderSite } from "../../build";
|
||||
import { Config, loadConfig } from "../../config";
|
||||
import { Command, UsageError } from "../types";
|
||||
|
||||
const SERVER_PORT = 8080;
|
||||
const DEFAULT_SERVER_PORT = 8080;
|
||||
|
||||
const injectLiveReloadScript = (htmlContents: string): string =>
|
||||
const injectLiveReloadScript = (htmlContents: string, port: number): string =>
|
||||
htmlContents.replace(
|
||||
"</html>",
|
||||
`
|
||||
<script>
|
||||
const ws = new WebSocket("ws://127.0.0.1:${SERVER_PORT}");
|
||||
const ws = new WebSocket("ws://127.0.0.1:${port}");
|
||||
ws.onmessage = function() {
|
||||
console.log('dev server requested reload, reloading...');
|
||||
location.reload();
|
||||
|
|
@ -118,8 +119,21 @@ const guessMimeType = (ext: string): string => {
|
|||
return mimeType;
|
||||
};
|
||||
|
||||
const serve = (publicDir: string): http.Server => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const portFromServer = (server: Pick<net.Server, "address">): number => {
|
||||
const addrInfo = server.address();
|
||||
if (addrInfo == null) {
|
||||
throw new Error(`server address is null (this should never happen!)`);
|
||||
}
|
||||
if (typeof addrInfo === "string") {
|
||||
throw new Error(
|
||||
`server address is a string (this should never happen!)`
|
||||
);
|
||||
}
|
||||
return addrInfo.port;
|
||||
};
|
||||
|
||||
const startHttpServer = async (publicDir: string): Promise<http.Server> => {
|
||||
const httpServer = http.createServer(async (req, res) => {
|
||||
if (req.url == null) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
|
|
@ -144,18 +158,36 @@ const serve = (publicDir: string): http.Server => {
|
|||
}
|
||||
const mimeType = guessMimeType(reqExt);
|
||||
if (mimeType === "text/html") {
|
||||
contents = injectLiveReloadScript(contents.toString("utf8"));
|
||||
const port = portFromServer(req.socket);
|
||||
contents = injectLiveReloadScript(contents.toString("utf8"), port);
|
||||
}
|
||||
res.writeHead(200, {
|
||||
"Content-Type": mimeType,
|
||||
});
|
||||
res.end(contents);
|
||||
});
|
||||
return server;
|
||||
const listen = async (port?: number): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
httpServer
|
||||
.once("error", (error) => reject(error))
|
||||
.once("listening", () => resolve())
|
||||
.listen(port);
|
||||
});
|
||||
try {
|
||||
await listen(DEFAULT_SERVER_PORT);
|
||||
} catch (error) {
|
||||
if (error.code !== "EADDRINUSE") {
|
||||
throw error;
|
||||
}
|
||||
await listen();
|
||||
}
|
||||
const port = portFromServer(httpServer);
|
||||
console.log(`Listening at http://127.0.0.1:${port}`);
|
||||
return httpServer;
|
||||
};
|
||||
|
||||
const startWebSocketServer = async (
|
||||
server: http.Server
|
||||
httpServer: http.Server
|
||||
): Promise<import("ws").Server | undefined> => {
|
||||
// Attempt to load the ws module, aborting if it isn't available.
|
||||
let ws;
|
||||
|
|
@ -168,7 +200,7 @@ const startWebSocketServer = async (
|
|||
console.warn(`'ws' module not found, live-reloading will be disabled`);
|
||||
return;
|
||||
}
|
||||
const wsServer = new ws.Server({ server });
|
||||
const wsServer = new ws.Server({ server: httpServer });
|
||||
wsServer.on("connection", () => {
|
||||
console.log("connected to dev site");
|
||||
});
|
||||
|
|
@ -244,15 +276,12 @@ const devCommand: Command = {
|
|||
};
|
||||
const config = await rebuild();
|
||||
const { outDir } = config.paths;
|
||||
const httpServer = serve(outDir);
|
||||
const httpServer = await startHttpServer(outDir);
|
||||
const wsServer = await startWebSocketServer(httpServer);
|
||||
httpServer.listen(SERVER_PORT, () => {
|
||||
console.log(`Listening at http://127.0.0.1:${SERVER_PORT}`);
|
||||
});
|
||||
const watchedFolders = config.watch.filter((filePath) =>
|
||||
existsSync(filePath)
|
||||
);
|
||||
watchFolders(watchedFolders, async (event, filePath) => {
|
||||
await watchFolders(watchedFolders, async (event, filePath) => {
|
||||
console.log(`${filePath}:${event} triggering rebuild...`);
|
||||
await rebuild();
|
||||
if (wsServer != null) {
|
||||
|
|
|
|||
|
|
@ -58,13 +58,31 @@ const noop = () => {};
|
|||
* @return Fully-realized configuration.
|
||||
*/
|
||||
export const loadConfig = async (rootDir: string): Promise<Config> => {
|
||||
const configPath = require.resolve(path.resolve(rootDir, "websnacks"));
|
||||
purgeModuleAndDepsFromCache(configPath);
|
||||
// TODO: validate user config.
|
||||
const userConfig = await import(configPath);
|
||||
let configPath;
|
||||
let userConfig: UserConfig = {};
|
||||
// Attempt to load a websnacks.ts/js file in rootDir.
|
||||
try {
|
||||
configPath = require.resolve(path.resolve(rootDir, "websnacks"));
|
||||
purgeModuleAndDepsFromCache(configPath);
|
||||
// TODO: validate user config.
|
||||
userConfig = await import(configPath);
|
||||
} catch (error) {
|
||||
// Use default config;
|
||||
}
|
||||
const outDir = path.join(rootDir, "public");
|
||||
const pagesDir = path.join(rootDir, "pages");
|
||||
const staticAssetsDir = path.join(rootDir, "static");
|
||||
|
||||
const watch = [pagesDir, staticAssetsDir];
|
||||
if (configPath != null) {
|
||||
watch.push(path.relative(rootDir, configPath));
|
||||
}
|
||||
if (userConfig.watch != null) {
|
||||
for (const userWatch of userConfig.watch) {
|
||||
watch.push(path.relative(rootDir, userWatch));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paths: {
|
||||
rootDir,
|
||||
|
|
@ -76,11 +94,6 @@ export const loadConfig = async (rootDir: string): Promise<Config> => {
|
|||
afterSiteRender: noop,
|
||||
...userConfig.hooks,
|
||||
},
|
||||
watch: [
|
||||
...userConfig.watch.map((p: string) => path.relative(rootDir, p)),
|
||||
path.relative(rootDir, configPath),
|
||||
pagesDir,
|
||||
staticAssetsDir,
|
||||
],
|
||||
watch,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
121
test/e2e/build.tsx
Normal file
121
test/e2e/build.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/* 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 { promises as fs } from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { runCommand, WEBSNACKS_BIN_PATH, WEBSNACKS_REPO_ROOT, withTempDir } from "../helpers/e2e";
|
||||
import { testSuite } from "../lib";
|
||||
|
||||
testSuite("build command", ({ test }) => {
|
||||
test("runs 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", "build"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
}
|
||||
);
|
||||
await cmd.complete;
|
||||
});
|
||||
});
|
||||
|
||||
test("works without config file", 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",
|
||||
}
|
||||
);
|
||||
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", "build"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
}
|
||||
);
|
||||
await cmd.complete;
|
||||
});
|
||||
});
|
||||
});
|
||||
152
test/e2e/dev.tsx
152
test/e2e/dev.tsx
|
|
@ -3,106 +3,14 @@
|
|||
* 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 {
|
||||
runCommand, wait, WEBSNACKS_BIN_PATH, WEBSNACKS_REPO_ROOT, withTempDir
|
||||
} from "../helpers/e2e";
|
||||
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) => {
|
||||
|
|
@ -162,7 +70,59 @@ testSuite("dev command", ({ test, expect }) => {
|
|||
}
|
||||
);
|
||||
// FIXME: This test is a bit brittle due to relying on timeouts.
|
||||
await wait(2_000);
|
||||
await wait(10_000);
|
||||
cmd.process.kill();
|
||||
const stdout = await cmd.complete;
|
||||
expect(stdout).toStartWith("Listening at");
|
||||
});
|
||||
});
|
||||
|
||||
test("works without config file", 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",
|
||||
}
|
||||
);
|
||||
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(10_000);
|
||||
cmd.process.kill();
|
||||
const stdout = await cmd.complete;
|
||||
expect(stdout).toStartWith("Listening at");
|
||||
|
|
|
|||
167
test/helpers/e2e.ts
Normal file
167
test/helpers/e2e.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Set a timeout and wait for at least the specified number of milliseconds,
|
||||
* resolving the promise once the event loop meets or exceeds timeMs.
|
||||
*
|
||||
* @param timeMs Time in milliseconds to wait.
|
||||
*/
|
||||
export const wait = async (timeMs: number): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), timeMs);
|
||||
});
|
||||
};
|
||||
|
||||
const TEST_DIST_PATH = path.resolve(__dirname, "..", "..", ".test-dist");
|
||||
|
||||
/**
|
||||
* Perform an operation within a unique temporary directory created within a
|
||||
* special .test-dist folder within this websnacks repository.
|
||||
*
|
||||
* @note Currently the temporary folder is **not** cleaned up once the operation
|
||||
* has finished. I've had issues with losing work due to buggy removal
|
||||
* code and haven't been willing to risk it again. To cleanup these
|
||||
* temporary folders it should be as easy as removing the whole
|
||||
* ".test-dist" folder from your checkout.
|
||||
*
|
||||
* @param op Operation to perform which receives the fully resolved temp
|
||||
* directory path as its only argument.
|
||||
*/
|
||||
export 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}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fully resolved path to the root of this websnacks repository.
|
||||
*/
|
||||
export const WEBSNACKS_REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||
/**
|
||||
* Fully resolved path to the websnacks CLI script in this repository.
|
||||
*/
|
||||
export const WEBSNACKS_BIN_PATH = path.join(
|
||||
WEBSNACKS_REPO_ROOT,
|
||||
"bin",
|
||||
"websnacks.js"
|
||||
);
|
||||
|
||||
/**
|
||||
* A handle to an asynchronous shell command run in a subprocess.
|
||||
*/
|
||||
export interface AsyncCommand {
|
||||
/**
|
||||
* Promise that resolves with the stdout of the subprocess once the
|
||||
* subprocess exits with a zero-code.
|
||||
*
|
||||
* The promise rejects if the subprocess exits with a non-zero code, the
|
||||
* subprocess writes to its stderr, or the command failed to spawn.
|
||||
*/
|
||||
complete: Promise<string>;
|
||||
/**
|
||||
* Handle to to child process for event-based process manipulation.
|
||||
*/
|
||||
process: ChildProcess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to configure {@link runCommand}.
|
||||
*/
|
||||
export interface CliOptions {
|
||||
/**
|
||||
* Working directory where the command should be run. Defaults to the
|
||||
* current working directory.
|
||||
*/
|
||||
cwd?: string;
|
||||
/**
|
||||
* Timeout in milliseconds after which a command that hasn't exited will
|
||||
* reject the promise and kill the subprocess.
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CLI_OPTIONS = {
|
||||
timeoutMs: 15_000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute a shell command in a subprocess.
|
||||
*
|
||||
* This provides a more user-friendly promise-based interface to
|
||||
* {@link child_process.spawn}. The obj
|
||||
*
|
||||
* @param command Name of the shell command to run.
|
||||
* @param args Array of arguments to pass to the command.
|
||||
* @param options Parameters to change how the command is run and resolved.
|
||||
*
|
||||
* @returns Command object for handling in client code.
|
||||
*/
|
||||
export const runCommand = (
|
||||
command: string,
|
||||
args: string[] = [],
|
||||
options?: CliOptions
|
||||
): AsyncCommand => {
|
||||
const optionsWithDefaults = { ...DEFAULT_CLI_OPTIONS, ...options };
|
||||
const process = spawn(command, args, {
|
||||
...optionsWithDefaults,
|
||||
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 ${optionsWithDefaults.timeoutMs}ms reached`
|
||||
)
|
||||
);
|
||||
}, optionsWithDefaults.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,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue