/* eslint-disable no-invalid-this */ /* eslint-disable @typescript-eslint/naming-convention */ import { readJSON } from "fs-extra" import { fork, ChildProcessPromise as ChildProcess, Output, } from "promisify-child-process" import type { PackageJson } from "gatsby" import path from "path" import detectPort from "detect-port" import { LogObject, StructuredEventType, GlobalStatus, Status, IPCMessageType, IPCMessage, } from "./ipc-types" // Gatsby package type is missing these type PackageJsonWithBin = PackageJson & { bin: { gatsby: string } } async function getPackageJson(root: string): Promise<PackageJsonWithBin> { return readJSON(`${root}/node_modules/gatsby/package.json`) } async function isGatsbySite(root: string): Promise<boolean> { try { const packageJson = await getPackageJson(root) return packageJson?.name === `gatsby` } catch (e) { console.warn({ e }) return false } } interface IAction { type: string payload: unknown } export interface Message { type: "message" | "error" message: unknown } export class SiteLauncher { public status: Status = GlobalStatus.NotStarted public logs: Array<LogObject> = [] public rawLogs: Array<string> = [] private proc: ChildProcess | undefined private listener?: (event: Message) => void private logAction = (action: IAction): void => { if (action.type === StructuredEventType.Log) { this.logs.push(action.payload as LogObject) } if (action.type === StructuredEventType.SetStatus) { console.log(`set status`, action.payload) this.status = action.payload as Status } this.postMessage({ type: `message`, message: { type: `LOG_ACTION`, action }, }) } private handleExit = (code: number | null): void => { this.logAction({ type: `EXIT`, payload: code || 0 }) } private sendRawLog = (message: string): void => { this.rawLogs.push(message) this.logAction({ type: `RAW_LOG`, payload: message }) } constructor(public root: string, public hash: string) { // https://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties } public setListener(listener: (event: Message) => void): void { this.listener = listener } public removeListener(): void { this.listener = undefined } private postMessage(event: Message): void { this.listener?.(event) } private spawnProcess( command: string, args: Array<string> = [], env: Record<string, string> = {} ): ChildProcess { const proc = fork(command, args, { // The Gatsby process detects the IPC channel and uses it to send // structured logs stdio: [`pipe`, `pipe`, `pipe`, `ipc`], cwd: this.root, env: { ...process.env, FORCE_COLOR: `1`, ...env, }, }) proc.stderr?.setEncoding(`utf8`) proc.stdout?.setEncoding(`utf8`) proc.stderr?.on(`data`, this.sendRawLog) proc.stdout?.on(`data`, this.sendRawLog) proc.on(`message`, (message: IPCMessage) => { if (message.type === IPCMessageType.LogAction) { this.logAction(message.action as IAction) } }) return proc } public async start(clean: boolean = false): Promise<number> { if (!(await isGatsbySite(this.root))) { this.postMessage({ type: `error`, message: `${this.root} is not a Gatsby site`, }) console.log(`Not a gatsby site`) return 0 } console.log(`Is a gatsby site. Launching`) if (this.proc) { // We're restarting, so don't want to notify of exit this.proc.off(`exit`, this.handleExit) this.proc.kill() this.proc = undefined } this.logs = [] this.rawLogs = [] const port = await detectPort(8000) console.log(`Running on port ${port}`) this.logAction({ type: `SET_PORT`, payload: port }) const packageJson = await getPackageJson(this.root) const bin = packageJson?.bin?.gatsby || `dist/bin/gatsby.js` const cmd = path.resolve(this.root, `node_modules`, `gatsby`, bin) // this.logAction({ // type: StructuredEventType.SetStatus, // payload: GlobalStatus.InProgress, // }) if (clean) { let result: Output | undefined try { result = await this.spawnProcess(cmd, [`clean`]) } catch (err) { console.error(err) } if (!result || result.code !== 0) { this.postMessage({ type: `error`, message: `Failed to clean site`, }) return 0 } } // Runs `gatsby develop` in the site root this.proc = this.spawnProcess(cmd, [`develop`, `--port=${port}`], { GATSBY_EXPERIMENTAL_ENABLE_ADMIN: `1`, }) this.logAction({ type: `SET_PID`, payload: this.proc.pid }) this.proc.on(`exit`, this.handleExit) return port } public stop(): void { if (this.proc?.connected) { this.proc.kill() this.proc = undefined } else { console.log(`Not running`) } } }