Merge branch 'next' into timestamp

This commit is contained in:
Kiwi
2018-07-13 19:53:34 -03:00
committed by GitHub
15 changed files with 2897 additions and 984 deletions
+16
View File
@@ -40,6 +40,22 @@ const config: Config = {
paths: [],
executor: new LongRunningExecutor("npm run start:webpackDevServer"),
},
runDocz: {
paths: [],
executor: new LongRunningExecutor("npm run docz -- dev"),
},
},
defaultSet: "client",
sets: {
server: ["runServer"],
client: [
"runServer",
"runWebpackDevServer",
"compileCSSTypes",
"compileRelayStream",
],
docz: ["runDocz", "compileCSSTypes"],
compile: ["compileCSSTypes", "compileRelayStream"],
},
};
+2 -1
View File
@@ -9,6 +9,7 @@ const paths = require("./paths");
const protocol = process.env.HTTPS === "true" ? "https" : "http";
const host = process.env.HOST || "0.0.0.0";
const serverPort = process.env.PORT || 3000;
const doczPort = process.env.DOCZ_PORT || 3030;
module.exports = function(proxy, allowedHost) {
return {
@@ -81,8 +82,8 @@ module.exports = function(proxy, allowedHost) {
disableDotRule: true,
},
public: allowedHost,
// Proxy to the graphql server.
proxy: proxy || {
// Proxy to the graphql server.
"/api": {
target: `http://localhost:${serverPort}`,
},
+1 -1
View File
@@ -14,7 +14,7 @@ export default {
source: "./src",
typescript: true,
host: process.env.HOST || "0.0.0.0",
port: parseInt(process.env.DOCZ_PORT, 10) || 3000,
port: parseInt(process.env.DOCZ_PORT, 10) || 3030,
modifyBundlerConfig: config => {
config.module.rules.push({
test: /\.css$/,
+2530 -847
View File
File diff suppressed because it is too large Load Diff
+32 -30
View File
@@ -8,7 +8,7 @@
"build": "npm-run-all compile --parallel build:*",
"build:client": "node ./scripts/build.js",
"build:server": "tsc -p ./src/tsconfig.json",
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts ./config/watcher.ts",
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts",
"compile": "npm-run-all --parallel compile:*",
"compile:css-types": "tcm src/core/client/",
"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",
@@ -19,14 +19,14 @@
"lint:server": "tslint --project ./src/tsconfig.json",
"lint:client": "tslint --project ./src/core/client/tsconfig.json",
"lint:scripts": "tslint --project ./tsconfig.json",
"docz:watch": "docz dev"
"docz": "docz"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"apollo-server-express": "^1.3.6",
"bunyan": "^1.8.12",
"convict": "^4.3.0",
"convict": "^4.3.1",
"dataloader": "^1.4.0",
"dotenv": "^6.0.0",
"dotenv-expand": "^4.2.0",
@@ -37,21 +37,21 @@
"graphql": "^0.13.2",
"graphql-config": "^2.0.1",
"graphql-redis-subscriptions": "^1.5.0",
"graphql-tools": "^3.0.2",
"graphql-tools": "^3.0.5",
"ioredis": "^3.2.2",
"joi": "^13.4.0",
"lodash": "^4.17.10",
"luxon": "^1.2.1",
"mongodb": "^3.0.10",
"luxon": "^1.3.1",
"mongodb": "^3.1.1",
"passport": "^0.4.0",
"performance-now": "^2.1.0",
"subscriptions-transport-ws": "^0.9.11",
"uuid": "^3.2.1"
"subscriptions-transport-ws": "^0.9.12",
"uuid": "^3.3.2"
},
"devDependencies": {
"@babel/core": "7.0.0-beta.49",
"@babel/plugin-syntax-dynamic-import": "7.0.0-beta.49",
"@babel/plugin-transform-modules-commonjs": "^7.0.0-beta.52",
"@babel/plugin-transform-modules-commonjs": "^7.0.0-beta.53",
"@babel/polyfill": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-react": "7.0.0-beta.49",
@@ -65,15 +65,15 @@
"@types/enzyme": "^3.1.11",
"@types/enzyme-adapter-react-16": "^1.0.2",
"@types/express": "^4.16.0",
"@types/graphql": "^0.13.1",
"@types/ioredis": "^3.2.8",
"@types/jest": "^23.1.1",
"@types/graphql": "^0.13.3",
"@types/ioredis": "^3.2.12",
"@types/jest": "^23.1.5",
"@types/joi": "^13.0.8",
"@types/jsdom": "^11.0.6",
"@types/lodash": "^4.14.109",
"@types/lodash": "^4.14.111",
"@types/luxon": "^0.5.3",
"@types/mongodb": "^3.0.19",
"@types/node": "^10.3.1",
"@types/mongodb": "^3.1.1",
"@types/node": "^10.5.2",
"@types/passport": "^0.4.5",
"@types/query-string": "^6.1.0",
"@types/react-dom": "^16.0.6",
@@ -82,10 +82,11 @@
"@types/react-test-renderer": "^16.0.1",
"@types/recompose": "^0.26.1",
"@types/relay-runtime": "github:coralproject/patched#types/relay-runtime",
"@types/sane": "^2.0.0",
"@types/sinon": "^5.0.1",
"@types/uuid": "^3.4.3",
"@types/ws": "^5.1.2",
"autoprefixer": "^8.6.0",
"autoprefixer": "^8.6.5",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.0-beta",
"babel-plugin-relay": "github:coralproject/patched#babel-plugin-relay",
@@ -98,21 +99,20 @@
"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",
"docz": "^0.5.8",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.4",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"final-form": "^4.8.1",
"flat": "^4.0.0",
"flat": "^4.1.0",
"fluent": "^0.6.4",
"fluent-intl-polyfill": "^0.1.0",
"fluent-langneg": "^0.1.0",
"fluent-react": "^0.7.0",
"graphql-playground-middleware-express": "^1.7.0",
"graphql-playground-middleware-express": "^1.7.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^23.3.0",
"jest": "^23.4.1",
"jsdom": "^11.11.0",
"loader-utils": "^1.1.0",
"npm-run-all": "^4.1.3",
@@ -121,17 +121,18 @@
"postcss-flexbugs-fixes": "^3.3.1",
"postcss-font-magician": "^2.2.1",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.5",
"postcss-loader": "^2.1.6",
"postcss-nested": "^3.0.0",
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^5.2.1",
"prettier": "^1.13.4",
"prettier": "^1.13.7",
"pstree.remy": "^1.1.0",
"query-string": "^6.1.0",
"raw-loader": "^0.5.1",
"react": "^16.4.0",
"react-dev-utils": "6.0.0-next.3e165448",
"react-dom": "^16.4.0",
"react-final-form": "^3.6.0",
"react-final-form": "^3.6.4",
"react-relay": "github:coralproject/patched#react-relay",
"react-responsive": "^4.1.0",
"react-test-renderer": "^16.4.1",
@@ -142,12 +143,13 @@
"relay-local-schema": "^0.7.0",
"relay-runtime": "github:coralproject/patched#relay-runtime",
"relay-test-utils": "github:coralproject/patched#relay-test-utils",
"sinon": "^6.1.2",
"sane": "^2.5.2",
"sinon": "^6.1.3",
"style-loader": "^0.21.0",
"ts-jest": "^23.0.0",
"ts-loader": "^4.3.1",
"ts-node": "^6.1.0",
"tsconfig-paths": "^3.4.0",
"ts-loader": "^4.4.2",
"ts-node": "^6.2.0",
"tsconfig-paths": "^3.4.2",
"tsconfig-paths-webpack-plugin": "^3.1.4",
"tslint": "^5.10.0",
"tslint-config-prettier": "^1.13.0",
@@ -155,12 +157,12 @@
"tslint-plugin-prettier": "^1.3.0",
"tslint-react": "^3.6.0",
"typed-css-modules": "^0.3.4",
"typescript": "^2.9.1",
"typescript": "^2.9.2",
"uglifyjs-webpack-plugin": "^1.2.5",
"webpack": "4.12.0",
"webpack-cli": "^3.0.2",
"webpack-dev-server": "^3.1.4",
"webpack-hot-client": "^4.0.3",
"webpack-hot-client": "^4.1.1",
"webpack-manifest-plugin": "^2.0.3"
}
}
+41 -26
View File
@@ -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 {
+3 -2
View File
@@ -1,3 +1,4 @@
import chalk from "chalk";
import spawn from "cross-spawn";
import { Cancelable, debounce } from "lodash";
@@ -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();
+40 -14
View File
@@ -1,6 +1,9 @@
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 {
@@ -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();
}
}
+101
View File
@@ -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;
}),
};
},
};
}
}
+28 -16
View File
@@ -4,24 +4,36 @@ import program from "commander";
import path from "path";
import watch from "../";
function list(val: string) {
return val.split(",");
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);
}
}
program
const cmd = program
.version("0.1.0")
.usage("<configFile>")
.option("-o, --only <watcher>", "only run the specified watcher", list)
.arguments("<configFile>")
.usage("[watchers or sets]")
.option("-c, --config <configFile>", "Use given config file")
.description("Run watchers defined in <configFile>")
.action((configFile, cmd) => {
const { only = [] } = cmd;
let config: any = require(path.resolve(configFile));
if (config.__esModule) {
config = config.default;
}
watch(config, { only });
})
.parse(process.argv);
run(cmd.args, cmd.opts());
+19 -7
View File
@@ -5,28 +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?: string[];
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 {
@@ -51,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
View File
@@ -0,0 +1,6 @@
declare module "pstree.remy" {
export default function psTree(
pid: number,
callback: (err: Error, kids: Array<{ PID: number }>) => void
): void;
}
+72 -37
View File
@@ -1,7 +1,8 @@
import chalk from "chalk";
import Joi from "joi";
import path from "path";
import { pickBy } from "lodash";
import ChokidarWatcher from "./ChokidarWatcher";
import SaneWatcher from "./SaneWatcher";
import { Config, configSchema, Options, WatchConfig, Watcher } from "./types";
// Polyfill the asyncIterator symbol.
@@ -9,63 +10,97 @@ 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);
})
);
}
function filterOnly(config: Config, only: string[]) {
for (const key of Object.keys(config.watchers)) {
if (only.indexOf(key) === -1) {
// tslint:disable-next-line:no-console
console.log(`Disabled watcher "${key}"`);
delete config.watchers[key];
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;
}
export default async function watch(config: Config, options?: Options) {
Joi.assert(config, configSchema);
const watcher = config.backend || new ChokidarWatcher();
if (options && options.only && options.only.length > 0) {
filterOnly(config, options.only);
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`);
}
setupCleanup(config);
for (const key of Object.keys(config.watchers)) {
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: 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);
}
}
+2 -2
View File
@@ -49,14 +49,14 @@ const config = convict({
mongodb: {
doc: "The MongoDB database to connect to.",
format: "mongo-uri",
default: "mongodb://localhost/talk",
default: "mongodb://127.0.0.1:27017/talk",
env: "MONGODB",
arg: "mongodb",
},
redis: {
doc: "The Redis database to connect to.",
format: "redis-uri",
default: "redis://localhost:6379",
default: "redis://127.0.0.1:6379",
env: "REDIS",
arg: "redis",
},
+4 -1
View File
@@ -8,7 +8,10 @@ import { Config } from "talk-server/config";
*/
export async function createMongoDB(config: Config): Promise<Db> {
// Connect and create a client for MongoDB.
const client = await MongoClient.connect(config.get("mongodb"));
const client = await MongoClient.connect(
config.get("mongodb"),
{ useNewUrlParser: true }
);
// Return the database handle, which defaults to the database name provided
// in the config connection string.