diff --git a/decl/watchman.js b/decl/watchman.js new file mode 100644 index 00000000..deded8d7 --- /dev/null +++ b/decl/watchman.js @@ -0,0 +1,21 @@ +declare module 'fb-watchman' { + declare class Client { + on(event: string, listener: Function): Client; + + end(): void; + + command( + args: Array, + callback: (err: ?Error, resp: { + clock: Object, + watch: Object, + relative_path: ?string + }) => void + ): void; + + capabilityCheck( + options: {}, + callback: (err: ?Error, resp: {}) => void + ): void; + } +} diff --git a/src/packages/cli/index.js b/src/packages/cli/index.js index d1359bec..0e5ef38a 100644 --- a/src/packages/cli/index.js +++ b/src/packages/cli/index.js @@ -84,14 +84,17 @@ export default function CLI() { hot = (environment === 'development') } = {}) => { return tryCatch(async () => { + port = parseInt(port, 10); + + process.env.PORT = port; process.env.NODE_ENV = environment; if (hot) { - const watcher = new Watcher(PWD); + const watcher = await new Watcher(PWD); - watcher.on('change', async (type, file) => { + watcher.on('change', async (changed) => { await compile(PWD, environment); - process.emit('update'); + process.emit('update', changed); }); } diff --git a/src/packages/controller/middleware/sanitize-params.js b/src/packages/controller/middleware/sanitize-params.js index 56d305b0..dfce2477 100644 --- a/src/packages/controller/middleware/sanitize-params.js +++ b/src/packages/controller/middleware/sanitize-params.js @@ -1,12 +1,40 @@ +// @flow import { camelize } from 'inflection'; import pick from '../../../utils/pick'; import entries from '../../../utils/entries'; -export default function sanitizeParams(req, res) { - const { modelName, model: { relationshipNames } } = this; +import type { IncomingMessage, ServerResponse } from 'http'; + +/** + * @private + */ +export default function sanitizeParams( + req: IncomingMessage, + res: ServerResponse +): void { + const { + modelName, + model: { + relationshipNames + } + }: { + modelName: string, + model: { + relationshipNames: Array + } + } = this; + const params = { ...req.params }; - let { page, limit, sort, filter, include, fields } = params; + + let { + page, + limit, + sort, + filter, + include, + fields + } = params; if (!page) { page = 1; @@ -92,7 +120,12 @@ export default function sanitizeParams(req, res) { }; if (/^(POST|PATCH)$/g.test(req.method)) { - let { type, attributes } = params.data; + let { + data: { + type, + attributes = {} + } + } = params; Object.assign(req.params, { data: { diff --git a/src/packages/database/model/index.js b/src/packages/database/model/index.js index 9a0600d4..e4c3e854 100644 --- a/src/packages/database/model/index.js +++ b/src/packages/database/model/index.js @@ -15,23 +15,72 @@ import entries from '../../../utils/entries'; import underscore from '../../../utils/underscore'; class Model { + /** + * @private + */ static table; + + /** + * @private + */ static store; - static logger; - static serializer; - static attributes; - static belongsTo; - static hasOne; - static hasMany; - static _tableName; + /** + * + */ + static logger; - static hooks = {}; - static validates = {}; - static primaryKey = 'id'; - static defaultPerPage = 25; + /** + * @private + */ + static serializer; - constructor(props = {}, initialize = true) { + /** + * @private + */ + static attributes: {}; + + /** + * + */ + static belongsTo: {}; + + /** + * + */ + static hasOne: {}; + + /** + * + */ + static hasMany: {}; + + /** + * @private + */ + static _tableName: ?string; + + /** + * + */ + static hooks: {} = {}; + + /** + * + */ + static validates: {} = {}; + + /** + * + */ + static primaryKey: string = 'id'; + + /** + * + */ + static defaultPerPage: number = 25; + + constructor(attrs: {} = {}, initialize: boolean = true): Model { const { constructor: { attributeNames, @@ -64,34 +113,34 @@ class Model { Object.assign( this, - pick(props, ...attributeNames, ...relationshipNames) + pick(attrs, ...attributeNames, ...relationshipNames) ); return this; } - get isDirty() { + get isDirty(): boolean { return Boolean(this.dirtyAttributes.size); } - get modelName() { + get modelName(): string { return this.constructor.modelName; } - static get modelName() { + static get modelName(): string { return dasherize(underscore(this.name)); } - static get tableName() { + static get tableName(): string { return this._tableName ? this._tableName : pluralize(underscore(this.name)); } - static set tableName(value) { + static set tableName(value): void { this._tableName = value; } - static get relationships() { + static get relationships(): {} { const { belongsTo, hasOne, @@ -105,15 +154,15 @@ class Model { }; } - static get attributeNames() { + static get attributeNames(): Array { return Object.keys(this.attributes); } - static get relationshipNames() { + static get relationshipNames(): Array { return Object.keys(this.relationships); } - async update(props = {}) { + async update(props = {}): Model { const { constructor: { primaryKey, @@ -170,7 +219,7 @@ class Model { return this; } - async destroy() { + async destroy(): Model { const { constructor: { primaryKey, @@ -212,7 +261,7 @@ class Model { return this; } - format(dest, ...only) { + format(dest: string, ...only: Array): {} { const { constructor: { attributes @@ -469,7 +518,7 @@ class Model { return record ? record : null; } - static getColumn(key) { + static getColumn(key): {} { const { attributes: { [key]: column @@ -479,7 +528,7 @@ class Model { return column; } - static getColumnName(key) { + static getColumnName(key): string { const column = this.getColumn(key); if (column) { @@ -487,7 +536,7 @@ class Model { } } - static getRelationship(key) { + static getRelationship(key): {} { const { relationships: { [key]: relationship diff --git a/src/packages/database/validation/index.js b/src/packages/database/validation/index.js index 5c6cf9b2..42c83ca1 100644 --- a/src/packages/database/validation/index.js +++ b/src/packages/database/validation/index.js @@ -1,19 +1,60 @@ +// @flow +import typeof Model from '../model'; + class Validation { - key; - value; - model; - validator; - - constructor({ key, value, model, validator = () => true } = {}) { - return Object.assign(this, { - key, - value, - model, - validator + key: string; + + value: mixed; + + model: Model; + + validator: () => boolean; + + constructor({ + key, + value, + model, + validator = () => true + }: { + key: string, + value: mixed, + model: Model, + validator: () => boolean + } = {}) { + Object.defineProperties(this, { + key: { + value: key, + writable: false, + enumerable: true, + configurable: false + }, + + value: { + value, + writable: false, + enumerable: true, + configurable: false + }, + + model: { + value: model, + writable: false, + enumerable: false, + configurable: false + }, + + validator: { + value: validator, + writable: false, + enumerable: false, + configurable: false + } }); + + return this; } - get isValid() { + get isValid(): boolean { const { model, value, diff --git a/src/packages/pm/cluster/index.js b/src/packages/pm/cluster/index.js index 4326d343..42b7673c 100644 --- a/src/packages/pm/cluster/index.js +++ b/src/packages/pm/cluster/index.js @@ -64,7 +64,14 @@ class Cluster extends EventEmitter { exec: joinPath(path, 'dist/boot.js') }); - process.on('update', () => this.reload()); + process.on('update', (changed) => { + changed.forEach(({ name: filename }) => { + logger.info(`${green('update')} ${filename}`); + }); + + this.reload(); + }); + this.forkAll().then(() => this.emit('ready')); return this; diff --git a/src/packages/watcher/index.js b/src/packages/watcher/index.js index 55a4aa75..fbd7a339 100644 --- a/src/packages/watcher/index.js +++ b/src/packages/watcher/index.js @@ -1,11 +1,9 @@ // @flow +import { Client } from 'fb-watchman'; +import { FSWatcher } from 'fs'; import { EventEmitter } from 'events'; -import { join as joinPath } from 'path'; -import { watch } from 'fs'; -import isJSFile from '../fs/utils/is-js-file'; - -import type { FSWatcher } from 'fs'; +import initialize from './initialize'; /** * @private @@ -13,42 +11,21 @@ import type { FSWatcher } from 'fs'; class Watcher extends EventEmitter { path: string; - client: FSWatcher; + client: Client | FSWatcher; - constructor(path: string): Watcher { + constructor(path: string): Promise { super(); - - path = joinPath(path, 'app'); - - const client = watch(path, { - recursive: true - }, (type, filename) => { - if (isJSFile(filename)) { - this.emit('change', type, filename); - } - }); - - Object.defineProperties(this, { - path: { - value: path, - writable: false, - enumerable: true, - configurable: false - }, - - client: { - value: client, - writable: false, - enumerable: true, - configurable: false - } - }); - - return this; + return initialize(this, path); } destroy(): void { - this.client.close(); + const { client } = this; + + if (client instanceof FSWatcher) { + client.close(); + } else if (client instanceof Client) { + client.end(); + } } } diff --git a/src/packages/watcher/initialize.js b/src/packages/watcher/initialize.js new file mode 100644 index 00000000..cf16b757 --- /dev/null +++ b/src/packages/watcher/initialize.js @@ -0,0 +1,122 @@ +import fs from 'fs'; +import { Client } from 'fb-watchman'; +import { join as joinPath } from 'path'; + +import exec from '../../utils/exec'; +import tryCatch from '../../utils/try-catch'; +import isJSFile from '../fs/utils/is-js-file'; + +import type Watcher from './'; +import type { FSWatcher } from 'fs'; + +const SUBSCRIPTION_NAME = 'lux-watcher'; + +function fallback(instance: Watcher, path: string): FSWatcher { + return fs.watch(path, { + recursive: true + }, (type, name) => { + if (isJSFile(name)) { + instance.emit('change', [{ name, type }]); + } + }); +} + +function setupWatchmen(instance: Watcher, path: string): Promise { + return new Promise((resolve, reject) => { + const client = new Client(); + + client.capabilityCheck({}, (capabilityErr) => { + if (capabilityErr) { + return reject(capabilityErr); + } + + client.command(['watch-project', path], (watchErr, { + watch, + relative_path + } = {}) => { + if (watchErr) { + return reject(watchErr); + } + + client.command(['clock', watch], (clockErr, { clock: since }) => { + if (clockErr) { + return reject(clockErr); + } + + client.command(['subscribe', watch, SUBSCRIPTION_NAME, { + since, + relative_root: relative_path, + + fields: [ + 'name', + 'size', + 'exists', + 'type' + ], + + expression: [ + 'allof', [ + 'match', + '*.js' + ] + ] + }], (subscribeErr) => { + if (subscribeErr) { + return reject(subscribeErr); + } + + client.on('subscription', ({ + files, + subscription + }: { + files: Array, + subscription: string + }): void => { + if (subscription === SUBSCRIPTION_NAME) { + instance.emit('change', files); + } + }); + + resolve(client); + }); + }); + }); + }); + }); +} + +export default async function initialize( + instance: Watcher, + path: string +): Promise { + let client; + + path = joinPath(path, 'app'); + + await tryCatch(async () => { + await exec('which watchman'); + client = await setupWatchmen(instance, path); + }, () => { + client = fallback(instance, path); + }); + + if (client) { + Object.defineProperties(instance, { + path: { + value: path, + writable: false, + enumerable: true, + configurable: false + }, + + client: { + value: client, + writable: false, + enumerable: true, + configurable: false + } + }); + } + + return instance; +}