mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:00:59 +08:00
Merge branch 'next' into next-passport
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
import program from "commander";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const config = JSON.parse(
|
||||
fs.readFileSync(path.resolve(__dirname, "../.graphqlconfig"), "utf8")
|
||||
);
|
||||
|
||||
program
|
||||
.version("0.1.0")
|
||||
.usage("<project>")
|
||||
.arguments("<project>")
|
||||
.description(
|
||||
"Returns the schema graph in `.graphqlconfig` based on <project>"
|
||||
)
|
||||
.action(project => {
|
||||
if (!config.projects) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error("Missing projects key in .graphqconfig");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.projects[project]) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(`Project ${project} not found in .graphqconfig`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.projects[project].schemaPath) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(
|
||||
`SchemaPath for project ${project} not found in .graphqconfig`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(config.projects[project].schemaPath);
|
||||
})
|
||||
.parse(process.argv);
|
||||
@@ -0,0 +1,81 @@
|
||||
import chokidar from "chokidar";
|
||||
import { Watcher, WatchOptions } from "./types";
|
||||
|
||||
export default class ChokidarWatcher implements Watcher {
|
||||
public watch(
|
||||
paths: ReadonlyArray<string>,
|
||||
options: WatchOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
const client = chokidar.watch(paths as string[], {
|
||||
ignored: options.ignore,
|
||||
});
|
||||
|
||||
// An array to hold all changes, that has not yet been yield.
|
||||
const queue: string[] = [];
|
||||
let firstError: Error | null = null;
|
||||
|
||||
// If this is set, a pending promise is waiting for the next result.
|
||||
let pending:
|
||||
| ({ resolve: (result: string) => void; reject: (error: Error) => void })
|
||||
| null = null;
|
||||
|
||||
// Listen for errors
|
||||
client.on("error", (error: Error) => {
|
||||
// Resolve pending request.
|
||||
if (pending) {
|
||||
pending.reject(error);
|
||||
pending = null;
|
||||
return;
|
||||
}
|
||||
if (!firstError) {
|
||||
firstError = error;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for changes
|
||||
client.on("change", (pathFile: string) => {
|
||||
// Resolve pending request.
|
||||
if (pending) {
|
||||
pending.resolve(pathFile);
|
||||
pending = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// There is no pending request, save it into the queue.
|
||||
queue.unshift(pathFile);
|
||||
});
|
||||
return {
|
||||
[Symbol.asyncIterator]() {
|
||||
return {
|
||||
next: () =>
|
||||
new Promise<IteratorResult<string>>((resolve, reject) => {
|
||||
const wrapped = {
|
||||
resolve: (pathFile: string) =>
|
||||
resolve({
|
||||
done: false,
|
||||
value: pathFile,
|
||||
}),
|
||||
reject: (error: Error) =>
|
||||
reject({
|
||||
done: true,
|
||||
value: error,
|
||||
}),
|
||||
};
|
||||
|
||||
// We already have a change to return
|
||||
if (firstError) {
|
||||
wrapped.reject(firstError);
|
||||
return;
|
||||
}
|
||||
if (queue.length) {
|
||||
wrapped.resolve(queue.pop()!);
|
||||
return;
|
||||
}
|
||||
// We need to wait for the next change event.
|
||||
pending = wrapped;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import spawn from "cross-spawn";
|
||||
import { Cancelable, debounce } from "lodash";
|
||||
|
||||
import { Executor } from "./types";
|
||||
|
||||
interface CommandExecutorOptions {
|
||||
args?: ReadonlyArray<string>;
|
||||
// If true, allow spawning multiple processes.
|
||||
spawnMutiple?: boolean;
|
||||
|
||||
// Specify the period in which the process is started at max once.
|
||||
debounce?: number | false;
|
||||
|
||||
// If true, will run command upon initialization.
|
||||
runOnInit?: boolean;
|
||||
}
|
||||
|
||||
export default class CommandExecutor implements Executor {
|
||||
private cmd: string;
|
||||
private args?: ReadonlyArray<string>;
|
||||
private spawnMultiple: boolean;
|
||||
private runOnInit: boolean;
|
||||
private isRunning: boolean = false;
|
||||
private shouldRespawn: boolean = false;
|
||||
private spawnProcessDebounced?: (() => void) & Cancelable;
|
||||
|
||||
constructor(cmd: string, opts: CommandExecutorOptions = {}) {
|
||||
this.cmd = cmd;
|
||||
this.args = opts.args;
|
||||
this.spawnMultiple = opts.spawnMutiple || false;
|
||||
this.runOnInit = opts.runOnInit || false;
|
||||
|
||||
const wait = opts.debounce === undefined ? 500 : opts.debounce;
|
||||
if (wait) {
|
||||
this.spawnProcessDebounced = debounce(() => this.spawnProcess(), wait);
|
||||
}
|
||||
}
|
||||
|
||||
// This is called before watching starts.
|
||||
public onInit(): void {
|
||||
if (this.runOnInit) {
|
||||
this.spawnProcessPotentiallyDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
private spawnProcessPotentiallyDebounced() {
|
||||
if (this.spawnProcessDebounced) {
|
||||
this.spawnProcessDebounced();
|
||||
return;
|
||||
}
|
||||
this.spawnProcess();
|
||||
}
|
||||
|
||||
private spawnProcess() {
|
||||
if (this.isRunning && !this.spawnMultiple) {
|
||||
this.shouldRespawn = true;
|
||||
return;
|
||||
}
|
||||
this.isRunning = true;
|
||||
this.shouldRespawn = false;
|
||||
const child = spawn(this.cmd, this.args as string[], {
|
||||
stdio: "inherit",
|
||||
shell: !this.args,
|
||||
});
|
||||
|
||||
child.on("close", (code: number) => {
|
||||
this.isRunning = false;
|
||||
if (code !== 0) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`We had an error building ${code}`);
|
||||
}
|
||||
if (this.shouldRespawn) {
|
||||
this.spawnProcessPotentiallyDebounced();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public execute(filePath: string) {
|
||||
this.spawnProcessPotentiallyDebounced();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ChildProcess } from "child_process";
|
||||
import spawn from "cross-spawn";
|
||||
import { Cancelable, debounce } from "lodash";
|
||||
import { Executor } from "./types";
|
||||
|
||||
interface LongRunningExecutorOptions {
|
||||
args?: ReadonlyArray<string>;
|
||||
|
||||
// Specify the period in which the process is restarted at max once.
|
||||
debounce?: number;
|
||||
}
|
||||
|
||||
export default class LongRunningExecutor implements Executor {
|
||||
private cmd: string;
|
||||
private args?: ReadonlyArray<string>;
|
||||
private process: ChildProcess | null = null;
|
||||
private isRunning: boolean = false;
|
||||
private shouldRestart: boolean = false;
|
||||
private restartDebounced: (() => void) & Cancelable;
|
||||
|
||||
constructor(cmd: string, opts: LongRunningExecutorOptions = {}) {
|
||||
this.cmd = cmd;
|
||||
this.args = opts.args;
|
||||
this.restartDebounced = debounce(
|
||||
() => this.restart(),
|
||||
opts.debounce || 500
|
||||
);
|
||||
}
|
||||
|
||||
private spawnProcess() {
|
||||
this.isRunning = true;
|
||||
this.process = spawn(this.cmd, this.args as string[], {
|
||||
stdio: "inherit",
|
||||
// Have all child processes in their own group.
|
||||
// See `process.kill` below.
|
||||
detached: true,
|
||||
shell: !this.args,
|
||||
});
|
||||
|
||||
this.process!.on("exit", (code: number) => {
|
||||
this.isRunning = false;
|
||||
|
||||
if (code !== 0 && code !== null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.error(`Exit code returned ${code}`);
|
||||
return;
|
||||
}
|
||||
if (this.shouldRestart) {
|
||||
this.spawnProcess();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private restart(): void {
|
||||
this.shouldRestart = true;
|
||||
// Using the `-` will kill all child procceses in the group.
|
||||
// See: https://azimi.me/2014/12/31/kill-child_process-node-js.html
|
||||
process.kill(-this.process!.pid, "SIGTERM");
|
||||
}
|
||||
|
||||
private kill(): void {
|
||||
this.shouldRestart = false;
|
||||
// Using the `-` will kill all child procceses in the group.
|
||||
// See: https://azimi.me/2014/12/31/kill-child_process-node-js.html
|
||||
process.kill(-this.process!.pid, "SIGTERM");
|
||||
}
|
||||
|
||||
// This is called before watching starts.
|
||||
public onInit(): void {
|
||||
this.spawnProcess();
|
||||
}
|
||||
|
||||
// This is called before exiting.
|
||||
public onCleanup() {
|
||||
this.restartDebounced.cancel();
|
||||
if (this.isRunning) {
|
||||
this.kill();
|
||||
}
|
||||
}
|
||||
|
||||
public execute(filePath: string) {
|
||||
if (this.isRunning) {
|
||||
this.restartDebounced();
|
||||
return;
|
||||
}
|
||||
this.spawnProcess();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
import program from "commander";
|
||||
import path from "path";
|
||||
import watch from "../";
|
||||
|
||||
program
|
||||
.version("0.1.0")
|
||||
.usage("<configFile>")
|
||||
.arguments("<configFile>")
|
||||
.description("Run watchers defined in <configFile>")
|
||||
.action(configFile => {
|
||||
let config: any = require(path.resolve(configFile));
|
||||
if (config.__esModule) {
|
||||
config = config.default;
|
||||
}
|
||||
watch(config);
|
||||
})
|
||||
.parse(process.argv);
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as ChokidarWatcher } from "./ChokidarWatcher";
|
||||
export { default as CommandExecutor } from "./CommandExecutor";
|
||||
export { default as LongRunningExecutor } from "./LongRunningExecutor";
|
||||
export { default } from "./watch";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,50 @@
|
||||
import Joi from "joi";
|
||||
|
||||
export interface WatchOptions {
|
||||
ignore?: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface Watcher {
|
||||
watch(
|
||||
paths: ReadonlyArray<string>,
|
||||
options?: WatchOptions
|
||||
): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
export interface Executor {
|
||||
onInit?(): void;
|
||||
onCleanup?(): void;
|
||||
execute(filePath: string): void;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
rootDir?: string;
|
||||
backend?: Watcher;
|
||||
watchers: {
|
||||
[key: string]: WatchConfig;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WatchConfig {
|
||||
paths: ReadonlyArray<string>;
|
||||
ignore?: ReadonlyArray<string>;
|
||||
executor: Executor;
|
||||
}
|
||||
|
||||
export const configSchema = Joi.object({
|
||||
rootDir: Joi.string().optional(),
|
||||
backend: Joi.object().optional(),
|
||||
watchers: Joi.object().pattern(
|
||||
/.*/,
|
||||
Joi.object({
|
||||
paths: Joi.array()
|
||||
.items(Joi.string())
|
||||
.unique(),
|
||||
ignore: Joi.array()
|
||||
.items(Joi.string())
|
||||
.unique()
|
||||
.optional(),
|
||||
executor: Joi.object(),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import Joi from "joi";
|
||||
import path from "path";
|
||||
|
||||
import ChokidarWatcher from "./ChokidarWatcher";
|
||||
import { Config, configSchema, WatchConfig, Watcher } from "./types";
|
||||
|
||||
// Polyfill the asyncIterator symbol.
|
||||
if (Symbol.asyncIterator === undefined) {
|
||||
(Symbol as any).asyncIterator = Symbol.for("asyncIterator");
|
||||
}
|
||||
|
||||
async function beginWatch(watcher: Watcher, key: string, config: WatchConfig) {
|
||||
const { paths, ignore, executor } = config;
|
||||
if (executor.onInit) {
|
||||
executor.onInit();
|
||||
}
|
||||
for await (const filePath of watcher.watch(paths, { ignore })) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`Execute "${key}"`);
|
||||
executor.execute(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
function prependRootDir(prepend: string, cfg: WatchConfig): WatchConfig {
|
||||
const prependFunc = (p: string) => path.resolve(prepend, p);
|
||||
return {
|
||||
...cfg,
|
||||
paths: cfg.paths.map(prependFunc),
|
||||
ignore: cfg.ignore ? cfg.ignore.map(prependFunc) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function setupCleanup(config: Config) {
|
||||
["SIGINT", "SIGTERM"].forEach(signal =>
|
||||
process.once(signal as any, () => {
|
||||
for (const key of Object.keys(config.watchers)) {
|
||||
if (config.watchers[key].executor.onCleanup) {
|
||||
config.watchers[key].executor.onCleanup!();
|
||||
}
|
||||
}
|
||||
process.exit(0);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default async function watch(config: Config) {
|
||||
Joi.assert(config, configSchema);
|
||||
const watcher = config.backend || new ChokidarWatcher();
|
||||
setupCleanup(config);
|
||||
for (const key of Object.keys(config.watchers)) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`Start watcher "${key}"`);
|
||||
const watcherConfig = config.rootDir
|
||||
? prependRootDir(config.rootDir, config.watchers[key])
|
||||
: config.watchers[key];
|
||||
beginWatch(watcher, key, watcherConfig);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user