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 f0f773f88..4d7d90884 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1437,12 +1437,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", @@ -1457,6 +1476,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", @@ -1531,9 +1559,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/jsonwebtoken": { @@ -4988,23 +5016,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": { @@ -5156,6 +5185,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" + } + } } } } @@ -5349,9 +5391,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": { @@ -5840,12 +5882,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" } @@ -6973,6 +7017,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" + } } } } @@ -7510,6 +7565,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": { @@ -9586,6 +9654,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": { @@ -19704,6 +19780,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 b4da13f68..576662b45 100644 --- a/package.json +++ b/package.json @@ -5,26 +5,21 @@ "scripts": { "start": "node dist/index.js", "test": "node scripts/test.js --env=jsdom", - "build": "npm-run-all --parallel compile:* && npm-run-all --parallel build:*", + "build": "npm-run-all --parallel compile:* --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", - "watch:types": "nodemon --config ./config/nodemon/types.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", - "compile:server:types": "node ./scripts/types.js", - "start:development": "NODE_ENV=development ts-node -r tsconfig-paths/register src/index.ts", + "watch:types": "nodemon --config ./config/nodemon/types.json", + "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", - "docz:watch": "docz dev", - "postinstall": "node ./scripts/types.js", - "lint:scripts": "tslint ./config/**/*.js ./scripts/**/*.js ./doczrc.js ./src/**/.*.js" + "lint:scripts": "tslint --project ./tsconfig.json", + "docz:watch": "docz dev" }, "author": "", "license": "Apache-2.0", @@ -64,8 +59,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", @@ -93,8 +91,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", @@ -149,4 +150,4 @@ "webpack-hot-client": "^4.0.3", "webpack-manifest-plugin": "^2.0.3" } -} +} \ No newline at end of file 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