mirror of
https://github.com/wassname/talk.git
synced 2026-07-20 12:40:47 +08:00
feat: added support for --only flag
Merge pull request #1731 from coralproject/next-watcher-flags Watcher --only Use JSDocs comments (#1727) Merge branch 'next' into prevent-compile-loop-relay Merge pull request #1726 from coralproject/prevent-compile-loop-relay [next] Adapt relay watch config [next] Remove nodemon (#1725) * Remove old nodemon configs * Remove nodemon [next] Jest implementation for React Components (#1733) * Make jest testing work with custom path and css modules * Add first test * feat: added unit tests to ci * fix: updated package-lock.json * Update cssTransform.js * Update cssTransform.js * Fix test in ci Adapt files.exclude (#1736) Permalink ui Adding Copy to clipboard functionality WIP clean request wip progress progress work in progress wip ui functionality Translations :/ wip Merge branch 'permalink' of github.com:coralproject/talk into permalink * 'permalink' of github.com:coralproject/talk: (42 commits) [next] Support server side jest testing (#1747) Update snapshots Add comments Remove precss Move react-responsive to dev deps Remove comment Mobile first approach Support standard css variables, dynamically set spacing-unit Add docs Fully implement Flex and MatchMedia Responsive Components <3 fix: linting fix: adjusted pageInfo Remove obsoloe snapshot Move jsdom to dev deps Mark comments as always returning a value Add comment Fix unit tests Translate, concept for translation and id strings Add aria props ... Adding Attachment and Popover Component merge conflicts progress Any Working Support for refs Ready
This commit is contained in:
+1
-1
@@ -19,11 +19,11 @@ const paths = require("../config/paths");
|
||||
|
||||
const jest = require("jest");
|
||||
let argv = process.argv.slice(2);
|
||||
argv.push("--config", paths.appJestConfig);
|
||||
|
||||
// Watch unless on CI or in coverage mode
|
||||
if (!process.env.CI && argv.indexOf("--coverage") < 0) {
|
||||
argv.push("--watch");
|
||||
argv.push("--config", paths.appJestConfig);
|
||||
}
|
||||
|
||||
jest.run(argv);
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import chokidar from "chokidar";
|
||||
import path from "path";
|
||||
import { Watcher, WatchOptions } from "./types";
|
||||
|
||||
function prependRootDir(
|
||||
prepend: string,
|
||||
paths: ReadonlyArray<string>
|
||||
): string[] {
|
||||
const prependFunc = (p: string) => path.resolve(prepend, p);
|
||||
return paths.map(prependFunc);
|
||||
}
|
||||
|
||||
export default class ChokidarWatcher implements Watcher {
|
||||
public watch(
|
||||
rootDir: string,
|
||||
paths: ReadonlyArray<string>,
|
||||
options: WatchOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
const client = chokidar.watch(paths as string[], {
|
||||
ignored: options.ignore,
|
||||
});
|
||||
const resolvedPaths = prependRootDir(rootDir, paths);
|
||||
|
||||
// An array to hold all changes, that has not yet been yield.
|
||||
const queue: string[] = [];
|
||||
@@ -19,31 +27,38 @@ export default class ChokidarWatcher implements Watcher {
|
||||
| ({ 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;
|
||||
}
|
||||
});
|
||||
// Only start client if we have something to watch.
|
||||
if (paths.length) {
|
||||
const client = chokidar.watch(resolvedPaths, {
|
||||
ignored: options.ignore && prependRootDir(rootDir, options.ignore),
|
||||
});
|
||||
|
||||
// Listen for changes
|
||||
client.on("change", (pathFile: string) => {
|
||||
// Resolve pending request.
|
||||
if (pending) {
|
||||
pending.resolve(pathFile);
|
||||
pending = null;
|
||||
return;
|
||||
}
|
||||
// Listen for errors
|
||||
client.on("error", (error: Error) => {
|
||||
// Resolve pending request.
|
||||
if (pending) {
|
||||
pending.reject(error);
|
||||
pending = null;
|
||||
return;
|
||||
}
|
||||
if (!firstError) {
|
||||
firstError = error;
|
||||
}
|
||||
});
|
||||
|
||||
// There is no pending request, save it into the queue.
|
||||
queue.unshift(pathFile);
|
||||
});
|
||||
// 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 {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import chalk from "chalk";
|
||||
import spawn from "cross-spawn";
|
||||
import { Cancelable, debounce } from "lodash";
|
||||
|
||||
@@ -5,13 +6,13 @@ import { Executor } from "./types";
|
||||
|
||||
interface CommandExecutorOptions {
|
||||
args?: ReadonlyArray<string>;
|
||||
// If true, allow spawning multiple processes.
|
||||
/** If true, allow spawning multiple processes. */
|
||||
spawnMutiple?: boolean;
|
||||
|
||||
// Specify the period in which the process is started at max once.
|
||||
/** Specify the period in which the process is started at max once. */
|
||||
debounce?: number | false;
|
||||
|
||||
// If true, will run command upon initialization.
|
||||
/** If true, will run command upon initialization. */
|
||||
runOnInit?: boolean;
|
||||
}
|
||||
|
||||
@@ -65,9 +66,9 @@ export default class CommandExecutor implements Executor {
|
||||
|
||||
child.on("close", (code: number) => {
|
||||
this.isRunning = false;
|
||||
if (code !== 0) {
|
||||
if (code !== 0 && code !== null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`We had an error building ${code}`);
|
||||
console.log(chalk.red(`Command exited with ${code}`));
|
||||
}
|
||||
if (this.shouldRespawn) {
|
||||
this.spawnProcessPotentiallyDebounced();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import chalk from "chalk";
|
||||
import { ChildProcess } from "child_process";
|
||||
import spawn from "cross-spawn";
|
||||
import { Cancelable, debounce } from "lodash";
|
||||
import psTree from "pstree.remy";
|
||||
|
||||
import { Executor } from "./types";
|
||||
|
||||
interface LongRunningExecutorOptions {
|
||||
args?: ReadonlyArray<string>;
|
||||
|
||||
// Specify the period in which the process is restarted at max once.
|
||||
/** Specify the period in which the process is restarted at max once. */
|
||||
debounce?: number;
|
||||
}
|
||||
|
||||
@@ -31,9 +34,6 @@ export default class LongRunningExecutor implements Executor {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -42,27 +42,53 @@ export default class LongRunningExecutor implements Executor {
|
||||
|
||||
if (code !== 0 && code !== null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.error(`Exit code returned ${code}`);
|
||||
console.log(chalk.red(`Command exited with ${code}`));
|
||||
return;
|
||||
}
|
||||
if (this.shouldRestart) {
|
||||
this.shouldRestart = false;
|
||||
this.spawnProcess();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private restart(): void {
|
||||
private restart() {
|
||||
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");
|
||||
return this.internalKill();
|
||||
}
|
||||
|
||||
private kill(): void {
|
||||
private kill() {
|
||||
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");
|
||||
return this.internalKill();
|
||||
}
|
||||
|
||||
private async internalKill(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const signal = "SIGTERM";
|
||||
if (process.platform === "win32") {
|
||||
// Force kill (/F) the whole child tree (/T) by PID
|
||||
spawn.sync("taskkill", [
|
||||
"/pid",
|
||||
this.process!.pid.toString(),
|
||||
"/T",
|
||||
"/F",
|
||||
]);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
psTree(this.process!.pid, (err, kids) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
spawn.sync("kill", [
|
||||
`-${signal}`,
|
||||
this.process!.pid.toString(),
|
||||
...kids.map(p => p.PID.toString()),
|
||||
]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// This is called before watching starts.
|
||||
@@ -71,10 +97,10 @@ export default class LongRunningExecutor implements Executor {
|
||||
}
|
||||
|
||||
// This is called before exiting.
|
||||
public onCleanup() {
|
||||
public async onCleanup() {
|
||||
this.restartDebounced.cancel();
|
||||
if (this.isRunning) {
|
||||
this.kill();
|
||||
await this.kill();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import chalk from "chalk";
|
||||
import { execSync } from "child_process";
|
||||
import sane from "sane";
|
||||
|
||||
import { Watcher, WatchOptions } from "./types";
|
||||
|
||||
interface SaneWatcherOptions {
|
||||
/**
|
||||
* Set to true to use watchman, false to disabled, and undefined
|
||||
* for automatic detection.
|
||||
*/
|
||||
watchman?: boolean;
|
||||
/** Use polling, this might be required for network file systems. */
|
||||
poll?: boolean;
|
||||
}
|
||||
|
||||
function canUseWatchman(): boolean {
|
||||
try {
|
||||
execSync("watchman --version", { stdio: ["ignore"] });
|
||||
return true;
|
||||
// tslint:disable-next-line:no-empty
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class SaneWatcher implements Watcher {
|
||||
private watchman?: boolean;
|
||||
private poll: boolean;
|
||||
|
||||
constructor(options: SaneWatcherOptions = {}) {
|
||||
this.watchman = options.watchman;
|
||||
this.poll = options.poll || false;
|
||||
|
||||
// Autodetect watchman.
|
||||
if (this.watchman === undefined && canUseWatchman()) {
|
||||
this.watchman = true;
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(chalk.grey(`Watchman detected`));
|
||||
}
|
||||
}
|
||||
|
||||
public watch(
|
||||
rootDir: string,
|
||||
paths: ReadonlyArray<string>,
|
||||
options: WatchOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
// An array to hold all changes, that has not yet been yield.
|
||||
const queue: string[] = [];
|
||||
|
||||
// If this is set, a pending promise is waiting for the next result.
|
||||
let pending: ({ resolve: (result: string) => void }) | null = null;
|
||||
|
||||
// Only start client if we have something to watch.
|
||||
if (paths.length) {
|
||||
// Setup client
|
||||
const client = sane(rootDir, {
|
||||
glob: paths as string[],
|
||||
ignored: options.ignore as string[],
|
||||
watchman: this.watchman,
|
||||
poll: this.poll,
|
||||
});
|
||||
|
||||
// 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,
|
||||
}),
|
||||
};
|
||||
|
||||
if (queue.length) {
|
||||
wrapped.resolve(queue.pop()!);
|
||||
return;
|
||||
}
|
||||
// We need to wait for the next change event.
|
||||
pending = wrapped;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,36 @@ import program from "commander";
|
||||
import path from "path";
|
||||
import watch from "../";
|
||||
|
||||
program
|
||||
async function run(
|
||||
args: ReadonlyArray<string>,
|
||||
options: Record<string, string>
|
||||
) {
|
||||
const only = args;
|
||||
const { config: configFile = "" } = options;
|
||||
if (!configFile) {
|
||||
throw new Error("Config file not specified");
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
let config: any = require(path.resolve(configFile));
|
||||
if (config.__esModule) {
|
||||
config = config.default;
|
||||
}
|
||||
|
||||
try {
|
||||
await watch(config, { only });
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const cmd = program
|
||||
.version("0.1.0")
|
||||
.usage("<configFile>")
|
||||
.arguments("<configFile>")
|
||||
.usage("[watchers or sets]")
|
||||
.option("-c, --config <configFile>", "Use given config file")
|
||||
.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);
|
||||
|
||||
run(cmd.args, cmd.opts());
|
||||
|
||||
@@ -5,24 +5,31 @@ export interface WatchOptions {
|
||||
}
|
||||
|
||||
export interface Watcher {
|
||||
onInit?(): void | Promise<void>;
|
||||
onCleanup?(): void | Promise<void>;
|
||||
watch(
|
||||
rootDir: string,
|
||||
paths: ReadonlyArray<string>,
|
||||
options?: WatchOptions
|
||||
): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
export interface Executor {
|
||||
onInit?(): void;
|
||||
onCleanup?(): void;
|
||||
onInit?(): void | Promise<void>;
|
||||
onCleanup?(): void | Promise<void>;
|
||||
execute(filePath: string): void;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
only?: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
rootDir?: string;
|
||||
backend?: Watcher;
|
||||
watchers: {
|
||||
[key: string]: WatchConfig;
|
||||
};
|
||||
watchers: Record<string, WatchConfig>;
|
||||
defaultSet?: string;
|
||||
sets?: Record<string, ReadonlyArray<string>>;
|
||||
}
|
||||
|
||||
export interface WatchConfig {
|
||||
@@ -47,4 +54,13 @@ export const configSchema = Joi.object({
|
||||
executor: Joi.object(),
|
||||
})
|
||||
),
|
||||
});
|
||||
defaultSet: Joi.string().optional(),
|
||||
sets: Joi.object()
|
||||
.pattern(
|
||||
/.*/,
|
||||
Joi.array()
|
||||
.items(Joi.string())
|
||||
.unique()
|
||||
)
|
||||
.optional(),
|
||||
}).with("defaultSet", "sets");
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
declare module "pstree.remy" {
|
||||
export default function psTree(
|
||||
pid: number,
|
||||
callback: (err: Error, kids: Array<{ PID: number }>) => void
|
||||
): void;
|
||||
}
|
||||
+76
-28
@@ -1,58 +1,106 @@
|
||||
import chalk from "chalk";
|
||||
import Joi from "joi";
|
||||
import path from "path";
|
||||
import { pickBy } from "lodash";
|
||||
|
||||
import ChokidarWatcher from "./ChokidarWatcher";
|
||||
import { Config, configSchema, WatchConfig, Watcher } from "./types";
|
||||
import SaneWatcher from "./SaneWatcher";
|
||||
import { Config, configSchema, Options, 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) {
|
||||
async function beginWatch(
|
||||
watcher: Watcher,
|
||||
key: string,
|
||||
config: WatchConfig,
|
||||
rootDir: string
|
||||
) {
|
||||
const { paths, ignore, executor } = config;
|
||||
if (executor.onInit) {
|
||||
executor.onInit();
|
||||
await executor.onInit();
|
||||
}
|
||||
for await (const filePath of watcher.watch(paths, { ignore })) {
|
||||
for await (const filePath of watcher.watch(rootDir, paths, { ignore })) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`Execute "${key}"`);
|
||||
console.log(chalk.cyanBright(`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) {
|
||||
function setupCleanup(watcher: Watcher, config: Config) {
|
||||
["SIGINT", "SIGTERM"].forEach(signal =>
|
||||
process.once(signal as any, () => {
|
||||
process.once(signal as any, async () => {
|
||||
const cleanups = [];
|
||||
if (watcher.onCleanup) {
|
||||
cleanups.push(watcher.onCleanup());
|
||||
}
|
||||
for (const key of Object.keys(config.watchers)) {
|
||||
if (config.watchers[key].executor.onCleanup) {
|
||||
config.watchers[key].executor.onCleanup!();
|
||||
cleanups.push(config.watchers[key].executor.onCleanup!());
|
||||
}
|
||||
}
|
||||
await Promise.all(cleanups);
|
||||
process.exit(0);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default async function watch(config: Config) {
|
||||
function resolveSets(
|
||||
sets: Record<string, ReadonlyArray<string>>,
|
||||
value: ReadonlyArray<string>
|
||||
) {
|
||||
const resolved: string[] = [];
|
||||
value.forEach(v => {
|
||||
if (v in sets) {
|
||||
resolved.push(...sets[v]);
|
||||
return;
|
||||
}
|
||||
resolved.push(v);
|
||||
});
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function filterOnly(
|
||||
watchers: Config["watchers"],
|
||||
only: ReadonlyArray<string>,
|
||||
sets?: Record<string, ReadonlyArray<string>>
|
||||
): Config["watchers"] {
|
||||
const resolved = sets ? resolveSets(sets, only) : only;
|
||||
const unknown = resolved.filter(r => !(r in watchers));
|
||||
if (unknown.length) {
|
||||
throw new Error(`Watcher Configuration or Set for ${unknown} not found`);
|
||||
}
|
||||
return pickBy(watchers, (value, key) => {
|
||||
if (resolved.indexOf(key) === -1) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(chalk.grey(`Disabled watcher "${key}"`));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}) as Config["watchers"];
|
||||
}
|
||||
|
||||
export default async function watch(config: Config, options: Options = {}) {
|
||||
Joi.assert(config, configSchema);
|
||||
const watcher = config.backend || new ChokidarWatcher();
|
||||
setupCleanup(config);
|
||||
for (const key of Object.keys(config.watchers)) {
|
||||
const watcher: Watcher = config.backend || new SaneWatcher();
|
||||
const rootDir = config.rootDir || process.cwd();
|
||||
const defaultSet = config.defaultSet && [config.defaultSet];
|
||||
const only = options.only && options.only.length ? options.only : defaultSet;
|
||||
|
||||
let watchersConfigs = config.watchers;
|
||||
if (only) {
|
||||
watchersConfigs = filterOnly(watchersConfigs, only, config.sets);
|
||||
}
|
||||
|
||||
setupCleanup(watcher, config);
|
||||
if (watcher.onInit) {
|
||||
await watcher.onInit();
|
||||
}
|
||||
|
||||
for (const key of Object.keys(watchersConfigs)) {
|
||||
// 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);
|
||||
console.log(chalk.cyanBright(`Start watcher "${key}"`));
|
||||
const watcherConfig = watchersConfigs[key];
|
||||
beginWatch(watcher, key, watcherConfig, rootDir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user