From 044e1c2863e414a07236e0ba54c869afb88a3ae6 Mon Sep 17 00:00:00 2001 From: Kiwi Date: Tue, 3 Jul 2018 15:21:58 -0300 Subject: [PATCH] Watcher infrastructure (#1724) * wip * Adding chokidar and types * specifiying build tasks * new structure, new types, executor and watchers * Adding log * Fully implemented watchers * adapt vscode launc * Add .babelrc.js to toplevel tsconfig project * Typo * Get schema path from .graphqlconfig * Use watcher binary * Add joi validation to watcher * Remove fb-watchman for now * Use correct ignore path * Fix dist folder * Allow setting watcher * Per default only spawn one process at a time * Support runOnInit * Rename RestartingExecutor to LongRunningExecutor * Use debounce instead of throttle * Remove console log * Debounce command execution * Simplify debounce * Watcher name change * Typos * Rename "watcher" root level config to "backend" --- .vscode/launch.json | 2 + .vscode/settings.json | 3 + config/watcher.ts | 41 ++++++++ package-lock.json | 140 ++++++++++++++++++------- package.json | 23 ++-- scripts/schemaPath.ts | 39 +++++++ scripts/watcher/ChokidarWatcher.ts | 81 ++++++++++++++ scripts/watcher/CommandExecutor.ts | 81 ++++++++++++++ scripts/watcher/LongRunningExecutor.ts | 88 ++++++++++++++++ scripts/watcher/bin/watcher.ts | 19 ++++ scripts/watcher/index.ts | 5 + scripts/watcher/types.ts | 50 +++++++++ scripts/watcher/watch.ts | 58 ++++++++++ src/core/client/tsconfig.json | 3 +- src/tsconfig.json | 34 ++++++ tsconfig.json | 29 ++--- 16 files changed, 627 insertions(+), 69 deletions(-) create mode 100644 config/watcher.ts create mode 100644 scripts/schemaPath.ts create mode 100644 scripts/watcher/ChokidarWatcher.ts create mode 100644 scripts/watcher/CommandExecutor.ts create mode 100644 scripts/watcher/LongRunningExecutor.ts create mode 100644 scripts/watcher/bin/watcher.ts create mode 100644 scripts/watcher/index.ts create mode 100644 scripts/watcher/types.ts create mode 100644 scripts/watcher/watch.ts create mode 100644 src/tsconfig.json diff --git a/.vscode/launch.json b/.vscode/launch.json index be87834af..9c4f6d92d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,8 @@ "cwd": "${workspaceFolder}", "program": "${workspaceFolder}/node_modules/.bin/ts-node", "args": [ + "--project", + "${workspaceFolder}/src/tsconfig.json", "-r", "tsconfig-paths/register", "${workspaceFolder}/src/index.ts" diff --git a/.vscode/settings.json b/.vscode/settings.json index 45930bf87..195b73de5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "editor.formatOnSave": true, + "files.associations": { + "*.css": "postcss" + }, "files.exclude": { "**/.git": true, "**/.svn": true, diff --git a/config/watcher.ts b/config/watcher.ts new file mode 100644 index 000000000..305193045 --- /dev/null +++ b/config/watcher.ts @@ -0,0 +1,41 @@ +import path from "path"; +import { + CommandExecutor, + Config, + LongRunningExecutor, +} from "../scripts/watcher"; + +const config: Config = { + rootDir: path.resolve(__dirname, "../src"), + watchers: { + compileRelayStream: { + paths: [ + "core/client/stream/**/*.ts", + "core/client/stream/**/*.tsx", + "core/client/stream/**/*.graphql", + "core/client/server/**/*.graphql", + ], + ignore: ["core/**/*.d.ts"], + executor: new CommandExecutor("npm run compile:relay-stream", { + runOnInit: true, + }), + }, + compileCSSTypes: { + paths: ["**/*.css"], + executor: new CommandExecutor("npm run compile:css-types", { + runOnInit: true, + }), + }, + runServer: { + paths: ["core/**/*.ts", "core/locales/**/*.ftl"], + ignore: ["core/client/**/*"], + executor: new LongRunningExecutor("npm run start:development"), + }, + runWebpackDevServer: { + paths: [], + executor: new LongRunningExecutor("npm run start:webpackDevServer"), + }, + }, +}; + +export default config; diff --git a/package-lock.json b/package-lock.json index d11b39f23..b3622a48d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1438,12 +1438,31 @@ "@types/node": "*" } }, + "@types/chokidar": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.5.tgz", + "integrity": "sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, "@types/classnames": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.4.tgz", "integrity": "sha512-UWUmNYhaIGDx8Kv0NSqFRwP6HWnBMXam4nBacOrjIiPBKKCdWMCe77+Nbn6rI9+Us9c+BhE26u84xeYQv2bKeA==", "dev": true }, + "@types/commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==", + "dev": true, + "requires": { + "commander": "*" + } + }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", @@ -1459,6 +1478,15 @@ "integrity": "sha512-p+gNRe4RPjpl1lTBUomFJ42P8ymArH/P93DFJ0iY873BJ4ZmogcKc6TbHgZQmtQMsy3jxcAo0HcTjidXwo8uKg==", "dev": true }, + "@types/cross-spawn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.0.tgz", + "integrity": "sha512-evp2ZGsFw9YKprDbg8ySgC9NA15g3YgiI8ANkGmKKvvi0P2aDGYLPxQIC5qfeKNUOe3TjABVGuah6omPRpIYhg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/dotenv": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-4.0.3.tgz", @@ -1519,9 +1547,9 @@ "dev": true }, "@types/joi": { - "version": "13.0.8", - "resolved": "https://registry.npmjs.org/@types/joi/-/joi-13.0.8.tgz", - "integrity": "sha512-GXYdIVpwBP5ZBOlHitSYfQdH+vWXVahhkeQwalX0LkoX7Mx0D3L3tg4vXXhr6nYHkEpWlAzWuEjgWEBtcp5NZA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-13.3.0.tgz", + "integrity": "sha512-nOnsbHvoo5DsQEh8VGlbQlfg9+/iFSxE5RQKLNkAODIqyupdEkBCZf6RCNxR+9X0egMIkJ43NnwkEJKxLogsIA==", "dev": true }, "@types/lodash": { @@ -4943,23 +4971,24 @@ "dev": true }, "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", "dev": true, "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.0", "braces": "^2.3.0", - "fsevents": "^1.1.2", + "fsevents": "^1.2.2", "glob-parent": "^3.1.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", "normalize-path": "^2.1.1", "path-is-absolute": "^1.0.0", "readdirp": "^2.0.0", - "upath": "^1.0.0" + "upath": "^1.0.5" } }, "chownr": { @@ -5111,6 +5140,19 @@ "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } } } } @@ -5306,9 +5348,9 @@ } }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", + "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", "dev": true }, "commondir": { @@ -5798,12 +5840,14 @@ } }, "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "lru-cache": "^4.0.1", + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } @@ -6933,6 +6977,17 @@ "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } } } } @@ -7463,6 +7518,19 @@ "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } } }, "exit": { @@ -8255,14 +8323,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8277,20 +8343,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -8407,8 +8470,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -8420,7 +8482,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8435,7 +8496,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8443,14 +8503,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -8469,7 +8527,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -8550,8 +8607,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8563,7 +8619,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8685,7 +8740,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9554,6 +9608,14 @@ "param-case": "2.1.x", "relateurl": "0.2.x", "uglify-js": "3.4.x" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + } } }, "html-webpack-plugin": { @@ -19555,6 +19617,12 @@ "source-map": "~0.6.1" }, "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 3b262478b..5f615b41f 100644 --- a/package.json +++ b/package.json @@ -7,20 +7,17 @@ "test": "node scripts/test.js --env=jsdom", "build": "npm-run-all --parallel compile:* && npm-run-all --parallel build:*", "build:client": "node ./scripts/build.js", - "build:server": "tsc", - "watch": "NODE_ENV=development npm-run-all compile:* --parallel watch:*", - "watch:client": "node ./scripts/start.js", - "watch:css-types": "tcm src/core/client/ --watch", - "watch:relay-stream": "nodemon --config ./config/nodemon/relay-stream.json", - "watch:server": "nodemon --config ./config/nodemon/server.json", + "build:server": "tsc -p ./src/tsconfig.json", + "watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts ./config/watcher.ts", "compile:css-types": "tcm src/core/client/", - "compile:relay-stream": "relay-compiler --src ./src/core/client/stream --schema ./src/core/server/graph/tenant/schema/schema.graphql --language typescript --artifactDirectory ./src/core/client/stream/__generated__ --no-watchman", - "start:development": "NODE_ENV=development ts-node -r tsconfig-paths/register src/index.ts", + "compile:relay-stream": "relay-compiler --src ./src/core/client/stream --schema $(ts-node ./scripts/schemaPath.ts tenant) --language typescript --artifactDirectory ./src/core/client/stream/__generated__ --no-watchman", + "start:development": "NODE_ENV=development ts-node --project ./src/tsconfig.json -r tsconfig-paths/register ./src/index.ts", + "start:webpackDevServer": "node ./scripts/start.js", "lint-fix": "npm run lint:server -- --fix && npm run lint:client -- --fix && npm run lint:scripts -- --fix", "lint": "npm-run-all --parallel lint:*", - "lint:server": "tslint --project ./tsconfig.json", + "lint:server": "tslint --project ./src/tsconfig.json", "lint:client": "tslint --project ./src/core/client/tsconfig.json", - "lint:scripts": "tslint ./config/**/*.js ./scripts/**/*.js ./doczrc.js ./src/**/.*.js", + "lint:scripts": "tslint --project ./tsconfig.json", "docz:watch": "docz dev" }, "author": "", @@ -57,8 +54,11 @@ "@babel/preset-env": "7.0.0-beta.49", "@babel/preset-react": "7.0.0-beta.49", "@types/bunyan": "^1.8.4", + "@types/chokidar": "^1.7.5", "@types/classnames": "^2.2.4", + "@types/commander": "^2.12.2", "@types/convict": "^4.2.0", + "@types/cross-spawn": "^6.0.0", "@types/dotenv": "^4.0.3", "@types/express": "^4.16.0", "@types/graphql": "^0.13.1", @@ -83,8 +83,11 @@ "babel-preset-react-optimize": "^1.0.1", "case-sensitive-paths-webpack-plugin": "^2.1.2", "chalk": "^2.4.1", + "chokidar": "^2.0.4", "classnames": "^2.2.5", + "commander": "^2.16.0", "copy-webpack-plugin": "^4.5.1", + "cross-spawn": "^6.0.5", "css-loader": "^0.28.11", "docz": "^0.2.6", "docz-theme-default": "^0.2.10", diff --git a/scripts/schemaPath.ts b/scripts/schemaPath.ts new file mode 100644 index 000000000..135ca9e6d --- /dev/null +++ b/scripts/schemaPath.ts @@ -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("") + .arguments("") + .description( + "Returns the schema graph in `.graphqlconfig` based on " + ) + .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); diff --git a/scripts/watcher/ChokidarWatcher.ts b/scripts/watcher/ChokidarWatcher.ts new file mode 100644 index 000000000..cbc91d705 --- /dev/null +++ b/scripts/watcher/ChokidarWatcher.ts @@ -0,0 +1,81 @@ +import chokidar from "chokidar"; +import { Watcher, WatchOptions } from "./types"; + +export default class ChokidarWatcher implements Watcher { + public watch( + paths: ReadonlyArray, + options: WatchOptions = {} + ): AsyncIterable { + 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>((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; + }), + }; + }, + }; + } +} diff --git a/scripts/watcher/CommandExecutor.ts b/scripts/watcher/CommandExecutor.ts new file mode 100644 index 000000000..60cf15e6a --- /dev/null +++ b/scripts/watcher/CommandExecutor.ts @@ -0,0 +1,81 @@ +import spawn from "cross-spawn"; +import { Cancelable, debounce } from "lodash"; + +import { Executor } from "./types"; + +interface CommandExecutorOptions { + args?: ReadonlyArray; + // 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; + 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(); + } +} diff --git a/scripts/watcher/LongRunningExecutor.ts b/scripts/watcher/LongRunningExecutor.ts new file mode 100644 index 000000000..cffe6f7db --- /dev/null +++ b/scripts/watcher/LongRunningExecutor.ts @@ -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; + + // 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; + 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(); + } +} diff --git a/scripts/watcher/bin/watcher.ts b/scripts/watcher/bin/watcher.ts new file mode 100644 index 000000000..32209c064 --- /dev/null +++ b/scripts/watcher/bin/watcher.ts @@ -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("") + .arguments("") + .description("Run watchers defined in ") + .action(configFile => { + let config: any = require(path.resolve(configFile)); + if (config.__esModule) { + config = config.default; + } + watch(config); + }) + .parse(process.argv); diff --git a/scripts/watcher/index.ts b/scripts/watcher/index.ts new file mode 100644 index 000000000..10cab02c3 --- /dev/null +++ b/scripts/watcher/index.ts @@ -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"; diff --git a/scripts/watcher/types.ts b/scripts/watcher/types.ts new file mode 100644 index 000000000..f78daee24 --- /dev/null +++ b/scripts/watcher/types.ts @@ -0,0 +1,50 @@ +import Joi from "joi"; + +export interface WatchOptions { + ignore?: ReadonlyArray; +} + +export interface Watcher { + watch( + paths: ReadonlyArray, + options?: WatchOptions + ): AsyncIterable; +} + +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; + ignore?: ReadonlyArray; + 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(), + }) + ), +}); diff --git a/scripts/watcher/watch.ts b/scripts/watcher/watch.ts new file mode 100644 index 000000000..572f7107b --- /dev/null +++ b/scripts/watcher/watch.ts @@ -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); + } +} diff --git a/src/core/client/tsconfig.json b/src/core/client/tsconfig.json index b639cc08b..ffb2142b6 100644 --- a/src/core/client/tsconfig.json +++ b/src/core/client/tsconfig.json @@ -4,8 +4,7 @@ "target": "es2015", "module": "esnext", "jsx": "preserve", - "noEmit": true, - "strictNullChecks": true, + "allowJs": false, "lib": [ "dom", "es7", diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..eddf98d29 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "allowJs": false, + "sourceMap": true, + "pretty": false, + "removeComments": true, + "noEmit": false, + "outDir": "../dist", + // See https://github.com/prismagraphql/graphql-request/issues/26 for why we + // have to include "dom" here. + "lib": [ + "es6", + "esnext.asynciterable", + "dom" + ], + "baseUrl": "./", + "paths": { + "talk-server/*": [ + "./core/server/*" + ], + "talk-common/*": [ + "./core/common/*" + ] + } + }, + "include": [ + "./**/*" + ], + "exclude": [ + "node_modules", + "./core/client" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a4c173d20..8e15c0223 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "es5", "module": "commonjs", "allowJs": true, + "noEmit": true, "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, @@ -12,32 +13,18 @@ "noImplicitAny": true, "strictNullChecks": true, "noErrorTruncation": true, - "sourceMap": true, - "pretty": false, - "removeComments": true, - "outDir": "dist", - // See https://github.com/prismagraphql/graphql-request/issues/26 for why we - // have to include "dom" here. "lib": [ "es6", - "esnext.asynciterable", - "dom" - ], - "baseUrl": "./", - "paths": { - "talk-server/*": [ - "./src/core/server/*" - ], - "talk-common/*": [ - "./src/core/common/*" - ] - } + "esnext.asynciterable" + ] }, "include": [ - "src/**/*" + "./src/**/.*.js", + "./scripts/**/*", + "./config/**/*", + "*.js" ], "exclude": [ - "node_modules", - "./src/core/client" + "node_modules" ] } \ No newline at end of file