Skip to content
This repository has been archived by the owner on Sep 10, 2019. It is now read-only.

fix: add watch mode #46

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ $(yarn bin)/gramps dev --data-source ../my-data-source
# Turn on mock data
$(yarn bin)/gramps dev --data-source ../my-data-source --mock
# $(npm bin)/gramps dev --data-source ../my-data-source --mock

# Turn on watch mode (hot reloading, doesn't restart server)
$(yarn bin)/gramps dev --data-source ../my-data-source --mock --watch
# $(npm bin)/gramps dev --data-source ../my-data-source --mock --watch
```

> **NOTE:** You can develop using multiple local data sources by passing multiple paths to the `--data-sources` option (an alias of `--data-source`):
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"babel-core": "^6.26.0",
"body-parser": "^1.18.2",
"chalk": "^2.3.0",
"chokidar": "^2.0.2",
"cpy": "^6.0.0",
"cross-env": "^5.1.1",
"cross-spawn": "^5.1.0",
Expand Down
53 changes: 37 additions & 16 deletions src/dev.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EOL } from 'os';
import path from 'path';
import http from 'http';
import yargs from 'yargs';
import cleanup from 'node-cleanup';
import { spawn } from 'cross-spawn';
Expand All @@ -14,12 +15,14 @@ import { warn } from './lib/logger';

const getDirPath = dir => path.resolve(process.cwd(), dir);

const startGateway = ({
const startGateway = async ({
mock,
watch,
gateway,
dataSourcePaths,
loadedDataSources,
processor,
dataSources,
}) => {
const { dataSourcePaths, loadedDataSources } = await processor();
// If a custom gateway was specified, set the env vars and start it.
if (gateway) {
// Define GrAMPS env vars.
Expand All @@ -35,8 +38,12 @@ const startGateway = ({

// If we get here, fire up the default gateway for development.
startDefaultGateway({
dataSources: loadedDataSources,
dataSourcePaths,
enableMockData: mock,
enableWatchMode: watch,
originalDataSources: dataSources,
dataSources: loadedDataSources,
processDataSources: processor,
});
};

Expand All @@ -57,6 +64,11 @@ export const builder = yargs =>
description: 'path to a GraphQL gateway start script',
type: 'string',
})
.group(['watch'], 'Choose whether to enable watch mode:')
.option('watch', {
alias: 'w',
description: 'watch file changes on data sources',
})
.coerce('d', srcArr => srcArr.map(getDirPath))
.coerce('g', getDirPath)
.group(['live', 'mock'], 'Choose real or mock data:')
Expand All @@ -81,14 +93,7 @@ export const builder = yargs =>
default: true,
});

export const handler = async ({
dataSources = [],
mock = false,
gateway,
transpile,
}) => {
warn('The GrAMPS CLI is intended for local development only.');

const processDataSources = (watch, transpile, dataSources) => async () => {
let dataSourcePaths = [];
let loadedDataSources = [];
if (dataSources.length) {
Expand All @@ -100,15 +105,31 @@ export const handler = async ({
// If something went wrong loading data sources, log it, tidy up, and die.
console.error(error);
await cleanUpTempDir();
process.exit(2); // eslint-disable-line no-process-exit
if (!watch) {
process.exit(2); // eslint-disable-line no-process-exit
}
}
}

startGateway({
return { dataSourcePaths, loadedDataSources };
};

export const handler = async ({
dataSources = [],
mock = false,
gateway,
transpile,
watch = false,
}) => {
warn('The GrAMPS CLI is intended for local development only.');
const processor = processDataSources(watch, transpile, dataSources);

await startGateway({
mock,
watch,
gateway,
dataSourcePaths,
loadedDataSources,
processor,
dataSources,
});
};

Expand Down
1 change: 1 addition & 0 deletions src/gateway/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default require.cache;
38 changes: 38 additions & 0 deletions src/gateway/hot-reload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Express from 'express';
import configureApp from './configure-app';
import watchPaths from '../lib/watcher';
import cache from './cache';
import { getDirName } from '../lib/data-sources';

export default (server, app, config) => {
if (config.enableWatchMode) {
let currentApp = app;
let currentSourcePaths = config.dataSourcePaths;

watchPaths(config.originalDataSources, async () => {
Object.keys(cache).forEach(id => {
if (
currentSourcePaths.filter(p => id.indexOf(getDirName(p)) !== -1)
.length > 0
) {
delete cache[id];
}
});
const {
dataSourcePaths: newDataSourcePaths,
loadedDataSources: newDataSources,
} = await config.processDataSources();
const newApp = configureApp(
Express(),
Object.assign({}, config, {
dataSources: newDataSources,
dataSourcePaths: newDataSourcePaths,
}),
);
server.removeListener('request', currentApp);
server.on('request', newApp);
currentApp = newApp;
currentSourcePaths = newDataSourcePaths;
});
}
};
7 changes: 5 additions & 2 deletions src/gateway/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import Express from 'express';

import configureApp from './configure-app';
import startServer from './start-server';
import hotReload from './hot-reload';
import { getDirName } from '../lib/data-sources';

export const GRAPHQL_ENDPOINT = '/graphql';
export const TESTING_ENDPOINT = '/playground';
export const DEFAULT_PORT = 8080;

export default config => {
export default async config => {
const app = configureApp(Express(), config);
startServer(app, config);
const server = await startServer(app, config);
hotReload(server, app, config);
};
8 changes: 6 additions & 2 deletions src/gateway/start-server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import getPort from 'get-port';

import http from 'http';
import { success } from '../lib/logger';
import { DEFAULT_PORT, GRAPHQL_ENDPOINT, TESTING_ENDPOINT } from '.';

Expand All @@ -8,7 +8,9 @@ export default async function startServer(
{ enableMockData, dataSources = [] } = {},
) {
const PORT = await getPort(DEFAULT_PORT);
app.listen(PORT, () => {
const server = http.createServer(app);

server.listen(PORT, error => {
const mode = enableMockData ? 'mock' : 'live';
success([
'='.repeat(65),
Expand All @@ -29,4 +31,6 @@ export default async function startServer(
'='.repeat(65),
]);
});

return server;
}
2 changes: 1 addition & 1 deletion src/lib/data-sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const handleError = (err, msg, callback) => {
throw err;
};

const getDirName = dir =>
export const getDirName = dir =>
dir
.split(path.sep)
.filter(str => str)
Expand Down
15 changes: 15 additions & 0 deletions src/lib/watcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import chokidar from 'chokidar';

export default (paths, invokeFn) => {
return new Promise((resolve, reject) => {
const watcher = chokidar.watch(paths, {
ignored: /node_modules|\.git/,
});
watcher.on('ready', () => {
watcher.on('all', (event, ...args) => {
invokeFn();
});
resolve();
});
});
};
22 changes: 19 additions & 3 deletions test/dev.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('gramps dev', () => {
expect(yargs.option).toBeCalledWith('data-source', expect.any(Object));
expect(yargs.option).toBeCalledWith('gateway', expect.any(Object));
expect(yargs.option).toBeCalledWith('transpile', expect.any(Object));
expect(yargs.option).toBeCalledWith('watch', expect.any(Object));
expect(yargs.options).toBeCalledWith(
expect.objectContaining({
live: expect.any(Object),
Expand Down Expand Up @@ -73,13 +74,14 @@ describe('gramps dev', () => {
);
});

it('starts the default gateway with no arguments', () => {
dev.handler({});
it('starts the default gateway with no arguments', async () => {
await dev.handler({});

expect(startDefaultGateway).toBeCalledWith(
expect.objectContaining({
dataSources: expect.any(Array),
enableMockData: expect.any(Boolean),
enableWatchMode: expect.any(Boolean),
processDataSources: expect.any(Function),
}),
);
});
Expand Down Expand Up @@ -135,5 +137,19 @@ describe('gramps dev', () => {
expect(dataSources.cleanUpTempDir).toBeCalled();
expect(process.exit).toBeCalledWith(2);
});

it('logs an error but not exit if loading data sources fails and watch mode is on', async () => {
console.error = jest.fn();
process.exit = jest.fn();

dataSources.loadDataSources.mockImplementationOnce(() => {
throw Error('test error');
});

await dev.handler({ dataSources: ['./one'], watch: true });
expect(console.error).toBeCalledWith(expect.any(Error));
expect(dataSources.cleanUpTempDir).toBeCalled();
expect(process.exit).not.toBeCalledWith(2);
});
});
});
74 changes: 74 additions & 0 deletions test/gateway/hot-reload.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Express from 'express';
import hotReload from '../../src/gateway/hot-reload';
import watchPaths from '../../src/lib/watcher';

jest.mock('../../src/lib/watcher', () => jest.fn((paths, cb) => cb()));

const mockApp = jest.fn();
const mockServer = {
removeListener: jest.fn(),
on: jest.fn(),
};

describe('gateway/hot-reload', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});

it('skips hot reload is watch mode is not enabled', () => {
hotReload(mockServer, mockApp, {
enableWatchMode: false,
});

expect(watchPaths).not.toHaveBeenCalled();
});

describe('hot reload is enabled', () => {
it('calls watchPaths', () => {
const config = {
enableWatchMode: true,
originalDataSources: [],
dataSourcePaths: ['dist/.tmp/path1', 'dist/.tmp/path2'],
processDataSources: jest.fn(() => ({
dataSourcePaths: ['dist/.tmp/path1', 'dist/.tmp/path2'],
})),
};

hotReload(mockServer, mockApp, config);

expect(watchPaths).toHaveBeenCalledWith(
config.originalDataSources,
expect.any(Function),
);
});

it('clears correct require cache', () => {
jest.resetModules();
jest.mock('../../src/gateway/cache', () => ({
[`datasource1/node_modules/a`]: {},
[`datasource2/node_modules/b`]: {},
[`node_modules/c`]: {},
}));
/* eslint-disable global-require */
const cache = require('../../src/gateway/cache');
const hotReload = require('../../src/gateway/hot-reload').default;
/* eslint-enable global-require */

const paths = ['datasource1', 'datasource2'];

hotReload(mockServer, mockApp, {
enableWatchMode: true,
originalDataSources: paths,
dataSourcePaths: ['dist/.tmp/datasource1', 'dist/.tmp/datasource2'],
processDataSources: jest.fn(() => ({
dataSourcePaths: ['dist/.tmp/datasource1', 'dist/.tmp/datasource2'],
})),
});

expect(cache).not.toHaveProperty('datasource1/node_modules/a');
expect(cache).not.toHaveProperty('datasource2/node_modules/b');
expect(cache).toHaveProperty('node_modules/c');
});
});
});
8 changes: 7 additions & 1 deletion test/gateway/start-server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import startServer from '../../src/gateway/start-server';
import * as logger from '../../src/lib/logger';

console.log = jest.fn();
const mockApp = { listen: jest.fn((port, cb) => cb()) };
const mockApp = jest.fn();
jest.mock('http', () => ({
createServer: () => ({
listen: jest.fn((port, cb) => cb()),
}),
}));
jest.mock('express', () => jest.fn());

describe('gateway/start-server', () => {
beforeEach(() => {
Expand Down
Loading