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

Commit afb9d04

Browse files
committed
feat: dev command ready for review
- data sources are transpiled by default - `--no-transpile` flag is respected - a custom gateway can be supplied for dev (#10) - temporary files are removed on process exit (#16) close #11, close #10, close #16
1 parent f029123 commit afb9d04

8 files changed

+301
-119
lines changed

bin/dev.js

+47-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
import { EOL } from 'os';
12
import path from 'path';
23
import yargs from 'yargs';
4+
import cleanup from 'node-cleanup';
5+
import { spawn } from 'cross-spawn';
36
import startDefaultGateway from '../lib/gateway';
7+
import {
8+
loadDataSources,
9+
transpileDataSources,
10+
cleanUpTempDir,
11+
} from '../lib/data-sources';
12+
import { log, success, warn } from '../lib/logger';
413

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

@@ -45,23 +54,49 @@ export const builder = yargs =>
4554
},
4655
});
4756

48-
export const handler = ({
57+
export const handler = async ({
4958
dataSources = [],
5059
mock = false,
5160
gateway,
5261
transpile,
5362
}) => {
54-
console.log({
55-
dataSources,
56-
gateway,
57-
mock,
58-
transpile,
59-
});
63+
warn('The GrAMPS CLI is intended for local development only.');
6064

61-
if (!gateway) {
62-
startDefaultGateway({
63-
dataSources,
64-
enableMockData: mock,
65-
});
65+
let loadedDataSources = [];
66+
if (dataSources.length) {
67+
loadedDataSources = await transpileDataSources(transpile, dataSources).then(
68+
loadDataSources,
69+
);
70+
}
71+
72+
// If a custom gateway was specified, set the env vars and start it.
73+
if (gateway) {
74+
// Define the `GRAMPS_DATA_SOURCES` env var.
75+
process.env.GRAMPS_DATA_SOURCES = dataSources.length
76+
? dataSources.join(',')
77+
: '';
78+
79+
// Start the user-specified gateway.
80+
spawn('node', [gateway], { stdio: 'inherit' });
81+
return;
6682
}
83+
84+
// If we get here, fire up the default gateway for development.
85+
startDefaultGateway({
86+
dataSources: loadedDataSources,
87+
enableMockData: mock,
88+
});
6789
};
90+
91+
cleanup((exitCode, signal) => {
92+
log(' -> cleaning up temporary files');
93+
// Delete the temporary directory.
94+
cleanUpTempDir().then(() => {
95+
success('Successfully shut down. Thanks for using GrAMPS!');
96+
process.kill(process.pid, signal);
97+
});
98+
99+
// Uninstall the handler to prevent an infinite loop.
100+
cleanup.uninstall();
101+
return false;
102+
});

bin/gramps.js

+1-14
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,8 @@
11
import { EOL } from 'os';
22
import yargs from 'yargs';
3-
import cleanup from 'node-cleanup';
4-
5-
import { description, version } from '../package.json';
63
import dev from './dev';
7-
// import cleanup from '../lib/cleanup';
84

95
yargs
106
.command(dev)
11-
.demandCommand()
7+
// .demandCommand()
128
.help().argv;
13-
14-
cleanup(
15-
() => {
16-
// console.log('exiting...');
17-
},
18-
{
19-
ctrl_C: `${EOL}${EOL}Thanks for using GrAMPS!`,
20-
},
21-
);

lib/cleanup.js

-20
This file was deleted.

lib/data-sources.js

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import cpy from 'cpy';
4+
import del from 'del';
5+
import mkdirp from 'mkdirp';
6+
import babel from 'babel-core';
7+
import globby from 'globby';
8+
import { error, log, warn } from './logger';
9+
10+
const TEMP_DIR = path.resolve(__dirname, '..', '.tmp');
11+
12+
const handleError = (err, msg, callback) => {
13+
if (!err) {
14+
return;
15+
}
16+
17+
error(msg || err);
18+
19+
if (callback) {
20+
callback(err);
21+
return;
22+
}
23+
24+
throw err;
25+
};
26+
27+
const getDirName = dir =>
28+
dir
29+
.split('/')
30+
.filter(str => str)
31+
.slice(-1)
32+
.pop();
33+
34+
export const cleanUpTempDir = () => del(TEMP_DIR);
35+
36+
const makeTempDir = tmpDir =>
37+
new Promise((resolve, reject) => {
38+
mkdirp(tmpDir, err => {
39+
handleError(err, `Unable to create ${tmpDir}`, reject);
40+
log(` -> created a temporary directory at ${tmpDir}`);
41+
resolve(tmpDir);
42+
});
43+
});
44+
45+
const makeParentTempDir = () =>
46+
new Promise((resolve, reject) => {
47+
cleanUpTempDir().then(() => {
48+
mkdirp(TEMP_DIR, err => {
49+
handleError(err, `Could not create ${TEMP_DIR}`);
50+
resolve(TEMP_DIR);
51+
});
52+
});
53+
});
54+
55+
const isValidDataSource = dataSourcePath => {
56+
if (!fs.existsSync(dataSourcePath)) {
57+
warn(`Could not load a data source from ${dataSourcePath}`);
58+
return false;
59+
}
60+
61+
return true;
62+
};
63+
64+
const loadDataSourceFromPath = dataSourcePath => {
65+
// eslint-disable-next-line global-require, import/no-dynamic-require
66+
const src = require(dataSourcePath);
67+
const dataSource = src.default || src;
68+
69+
// TODO check for required properties.
70+
71+
log(` -> successfully loaded ${dataSource.namespace}`);
72+
73+
return src.default || src;
74+
};
75+
76+
export const loadDataSources = pathArr => {
77+
return pathArr.filter(isValidDataSource).map(loadDataSourceFromPath);
78+
};
79+
80+
const writeTranspiledFile = ({ filename, tmpFile, transpiled }) =>
81+
new Promise((resolve, reject) => {
82+
fs.writeFile(tmpFile, transpiled.code, err => {
83+
handleError(err, `Unable to transpile ${tmpFile}`, reject);
84+
resolve(filename);
85+
});
86+
});
87+
88+
// For convenience, we transpile data sources using GrAMPS settings.
89+
const transpileJS = dataSource => tmpDir =>
90+
new Promise((resolve, reject) => {
91+
const filePromises = globby
92+
.sync(path.join(dataSource, '{src,}/*.js'))
93+
.map(file => {
94+
const filename = path.basename(file);
95+
96+
return {
97+
filename,
98+
tmpFile: path.join(tmpDir, filename),
99+
transpiled: babel.transformFileSync(file),
100+
};
101+
})
102+
.map(writeTranspiledFile);
103+
104+
Promise.all(filePromises).then(() => resolve(tmpDir));
105+
});
106+
107+
const copyGraphQLFiles = dataSource => tmpDir =>
108+
new Promise((resolve, reject) => {
109+
const filePromises = globby
110+
.sync(path.join(dataSource, '{src,}/*.graphql'))
111+
.map(file => cpy(file, tmpDir));
112+
113+
Promise.all(filePromises).then(() => resolve(tmpDir));
114+
});
115+
116+
// We have to symlink node_modules or we’ll get errors when requiring packages.
117+
const symlinkNodeModules = dataSource => tmpDir =>
118+
new Promise((resolve, reject) => {
119+
fs.symlink(
120+
path.join(dataSource, 'node_modules'),
121+
path.join(tmpDir, 'node_modules'),
122+
err => {
123+
handleError(err, 'Unable to symlink the Node modules folder', reject);
124+
resolve(tmpDir);
125+
},
126+
);
127+
});
128+
129+
const transpileDataSource = parentDir => dataSource =>
130+
new Promise((resolve, reject) => {
131+
const dirName = getDirName(dataSource);
132+
const tmpDir = path.join(parentDir, dirName);
133+
134+
// Make a temporary directory for the data source.
135+
makeTempDir(tmpDir)
136+
.then(transpileJS(dataSource))
137+
.then(copyGraphQLFiles(dataSource))
138+
.then(symlinkNodeModules(dataSource))
139+
.then(resolve)
140+
.catch(err => handleError(err, null, reject));
141+
});
142+
143+
export const transpileDataSources = (shouldTranspile, dataSources) =>
144+
new Promise((resolve, reject) => {
145+
if (!shouldTranspile) {
146+
resolve(dataSources);
147+
return;
148+
}
149+
150+
makeParentTempDir()
151+
.then(parentDir =>
152+
dataSources
153+
.filter(isValidDataSource)
154+
.map(transpileDataSource(parentDir)),
155+
)
156+
.then(dataSourcePromises => {
157+
Promise.all(dataSourcePromises).then(resolve);
158+
})
159+
.catch(reject);
160+
});

lib/gateway.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,34 @@ import gramps from '@gramps/gramps';
99
import { GraphQLSchema } from 'graphql';
1010
import { graphqlExpress } from 'apollo-server-express';
1111
import playground from 'graphql-playground-middleware-express';
12+
import { success } from './logger';
1213

1314
const GRAPHQL_ENDPOINT = '/graphql';
1415
const TESTING_ENDPOINT = '/playground';
1516
const DEFAULT_PORT = 8080;
1617

17-
async function startServer(app) {
18+
async function startServer(app, { enableMockData, dataSources }) {
1819
const PORT = await getPort(DEFAULT_PORT);
1920
app.listen(PORT, () => {
20-
console.log(`=> http://localhost:${PORT}${TESTING_ENDPOINT}`);
21+
const mode = enableMockData ? 'mock' : 'live';
22+
success([
23+
'='.repeat(65),
24+
'',
25+
` A GraphQL gateway has successfully started using ${mode} data.`,
26+
``,
27+
` The following GrAMPS data sources are running locally:`,
28+
...dataSources.map(src => ` - ${src.namespace}`),
29+
``,
30+
` For UI development, point your GraphQL client to:`,
31+
` http://localhost:${PORT}${GRAPHQL_ENDPOINT}`,
32+
``,
33+
` To test your data sources in the GraphQL Playground, visit:`,
34+
` http://localhost:${PORT}${TESTING_ENDPOINT}`,
35+
``,
36+
' To stop this gateway, press `control` + `C`.',
37+
``,
38+
'='.repeat(65),
39+
]);
2140
});
2241
}
2342

@@ -29,5 +48,5 @@ export default config => {
2948
app.use(GRAPHQL_ENDPOINT, graphqlExpress(GraphQLOptions));
3049
app.use(TESTING_ENDPOINT, playground({ endpoint: GRAPHQL_ENDPOINT }));
3150

32-
startServer(app);
51+
startServer(app, config);
3352
};

lib/logger.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { EOL } from 'os';
2+
import chalk from 'chalk';
3+
4+
const padMsg = msg =>
5+
['']
6+
.concat(msg)
7+
.concat('')
8+
.join(EOL);
9+
10+
export const error = msg => console.error(chalk.red.bold(padMsg(msg)));
11+
export const log = msg => console.log(chalk.dim(msg));
12+
export const success = msg => console.log(chalk.green(padMsg(msg)));
13+
export const warn = msg => console.warn(chalk.yellow.bold(padMsg(msg)));

package.json

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gramps/cli",
3-
"version": "0.0.0-development",
3+
"version": "1.0.0-beta.1",
44
"description": "CLI for creating and developing with GrAMPS data sources.",
55
"files": [
66
"bin",
@@ -19,42 +19,46 @@
1919
"gramps": "bin/index.js"
2020
},
2121
"scripts": {
22+
"gramps": "bin/index.js",
2223
"prepush": "npm test",
2324
"lint": "eslint {bin,lib}/**/*.js",
2425
"test": "npm run lint --silent",
2526
"test:unit": "cross-env NODE_ENV=test LOG4JS_LEVEL='OFF' jest --coverage",
2627
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
2728
},
2829
"dependencies": {
29-
"@gramps/gramps": "^1.0.0-beta-4",
3030
"apollo-server-express": "^1.2.0",
31+
"babel-core": "^6.26.0",
3132
"body-parser": "^1.18.2",
3233
"chalk": "^2.3.0",
34+
"cpy": "^6.0.0",
3335
"cross-env": "^5.1.1",
36+
"cross-spawn": "^5.1.0",
3437
"del": "^3.0.0",
3538
"express": "^4.16.2",
3639
"get-port": "^3.2.0",
40+
"globby": "^7.1.1",
3741
"graphql": "^0.11.7",
3842
"graphql-playground-middleware-express": "^1.3.8",
3943
"inquirer": "^4.0.1",
44+
"mkdirp": "^0.5.1",
4045
"node-cleanup": "^2.1.2",
4146
"reify": "^0.13.3",
4247
"yargs": "^10.0.3"
4348
},
4449
"peerDependencies": {
50+
"@gramps/gramps": "^1.0.0-beta-5",
4551
"graphql": "^0.11.7"
4652
},
4753
"devDependencies": {
54+
"@gramps/gramps": "^1.0.0-beta-7",
4855
"babel-cli": "^6.24.1",
49-
"babel-core": "^6.26.0",
5056
"babel-eslint": "^8.0.1",
5157
"babel-jest": "^21.2.0",
5258
"babel-plugin-inline-import": "^2.0.6",
5359
"babel-preset-env": "^1.6.0",
5460
"babel-preset-stage-2": "^6.24.1",
5561
"condition-travis-enterprise": "^1.0.0",
56-
"cpy-cli": "^1.0.1",
57-
"del-cli": "^1.0.0",
5862
"eslint": "^4.2.0",
5963
"eslint-config-airbnb-base": "^12.0.2",
6064
"eslint-config-prettier": "^2.3.0",

0 commit comments

Comments
 (0)