Makes the Metro CLI commands injectable

Summary: This diff makes the Metro CLI commands injectable into any Yargs object, so that we can override various aspects of it, add extra commands, etc.

Reviewed By: davidaurelio

Differential Revision: D6988250

fbshipit-source-id: 217c661e4c1b05282b6cdbf4d745d1b29f5cbbe0
This commit is contained in:
Maël Nison 2018-02-22 06:06:37 -08:00 committed by Facebook Github Bot
parent 60f16aafc2
commit eb72ad3794
4 changed files with 172 additions and 131 deletions

View File

@ -14,4 +14,6 @@
const yargs = require('yargs');
yargs.demandCommand(1).commandDir(`${__dirname}/commands`).argv;
const {attachMetroCli} = require('./index');
attachMetroCli(yargs.demandCommand(1)).argv;

View File

@ -18,76 +18,81 @@ const {Terminal} = require('metro-core');
import typeof Yargs from 'yargs';
exports.command = 'build <entry>';
exports.builder = (yargs: Yargs) => {
yargs.option('project-roots', {
alias: 'P',
type: 'string',
array: true,
});
yargs.option('out', {alias: 'O', type: 'string', demandOption: true});
yargs.option('platform', {alias: 'p', type: 'string'});
yargs.option('output-type', {alias: 't', type: 'string'});
yargs.option('max-workers', {alias: 'j', type: 'number'});
yargs.option('optimize', {alias: 'z', type: 'boolean'});
yargs.option('dev', {alias: 'g', type: 'boolean'});
yargs.option('source-map', {type: 'boolean'});
yargs.option('source-map-url', {type: 'string'});
yargs.option('legacy-bundler', {type: 'boolean'});
yargs.option('config', {alias: 'c', type: 'string'});
// Deprecated
yargs.option('reset-cache', {type: 'boolean', describe: null});
};
const term = new Terminal(process.stdout);
const updateReporter = new TerminalReporter(term);
// eslint-disable-next-line lint/no-unclear-flowtypes
exports.handler = makeAsyncCommand(async (argv: any) => {
// $FlowFixMe: Flow + Promises don't work consistently https://fb.facebook.com/groups/flow/permalink/1772334656148475/
const config = await MetroApi.loadMetroConfig(argv.config);
module.exports = () => ({
command: 'build <entry>',
if (argv.projectRoots) {
config.getProjectRoots = () => argv.projectRoots;
}
description:
'Generates a JavaScript bundle containing the specified entrypoint and its descendants',
await MetroApi.runBuild({
...argv,
config,
onBegin: () => {
updateReporter.update({
buildID: '$',
type: 'bundle_build_started',
bundleDetails: {
entryFile: argv.O,
platform: argv.platform,
dev: !!argv.dev,
minify: !!argv.optimize,
bundleType: 'Bundle',
},
});
},
onProgress: (transformedFileCount, totalFileCount) => {
updateReporter.update({
buildID: '$',
type: 'bundle_transform_progressed_throttled',
transformedFileCount,
totalFileCount,
});
},
onComplete: () => {
updateReporter.update({
buildID: '$',
type: 'bundle_build_done',
});
},
});
builder: (yargs: Yargs) => {
yargs.option('project-roots', {
alias: 'P',
type: 'string',
array: true,
});
yargs.option('out', {alias: 'O', type: 'string', demandOption: true});
yargs.option('platform', {alias: 'p', type: 'string'});
yargs.option('output-type', {alias: 't', type: 'string'});
yargs.option('max-workers', {alias: 'j', type: 'number'});
yargs.option('optimize', {alias: 'z', type: 'boolean'});
yargs.option('dev', {alias: 'g', type: 'boolean'});
yargs.option('source-map', {type: 'boolean'});
yargs.option('source-map-url', {type: 'string'});
yargs.option('legacy-bundler', {type: 'boolean'});
yargs.option('config', {alias: 'c', type: 'string'});
// Deprecated
yargs.option('reset-cache', {type: 'boolean', describe: null});
},
// eslint-disable-next-line lint/no-unclear-flowtypes
handler: makeAsyncCommand(async (argv: any) => {
// $FlowFixMe: Flow + Promises don't work consistently https://fb.facebook.com/groups/flow/permalink/1772334656148475/
const config = await MetroApi.loadMetroConfig(argv.config);
if (argv.projectRoots) {
config.getProjectRoots = () => argv.projectRoots;
}
await MetroApi.runBuild({
...argv,
config,
onBegin: () => {
updateReporter.update({
buildID: '$',
type: 'bundle_build_started',
bundleDetails: {
entryFile: argv.entry,
platform: argv.platform,
dev: !!argv.dev,
minify: !!argv.optimize,
bundleType: 'Bundle',
},
});
},
onProgress: (transformedFileCount, totalFileCount) => {
updateReporter.update({
buildID: '$',
type: 'bundle_transform_progressed_throttled',
transformedFileCount,
totalFileCount,
});
},
onComplete: () => {
updateReporter.update({
buildID: '$',
type: 'bundle_build_done',
});
},
});
}),
});

View File

@ -17,70 +17,75 @@ const {promisify} = require('util');
import typeof Yargs from 'yargs';
exports.command = 'serve';
module.exports = () => ({
command: 'serve',
exports.builder = (yargs: Yargs) => {
yargs.option('project-roots', {
alias: 'P',
type: 'string',
array: true,
});
description:
'Starts a Metro server on the given port, building bundles on the fly',
yargs.option('host', {alias: 'h', type: 'string', default: 'localhost'});
yargs.option('port', {alias: 'p', type: 'number', default: 8080});
yargs.option('max-workers', {alias: 'j', type: 'number'});
yargs.option('secure', {type: 'boolean'});
yargs.option('secure-key', {type: 'string'});
yargs.option('secure-cert', {type: 'string'});
yargs.option('hmr-enabled', {alias: 'hmr', type: 'boolean'});
yargs.option('config', {alias: 'c', type: 'string'});
// Deprecated
yargs.option('reset-cache', {type: 'boolean', describe: null});
};
// eslint-disable-next-line lint/no-unclear-flowtypes
exports.handler = makeAsyncCommand(async (argv: any) => {
let server = null;
let restarting = false;
async function restart() {
if (restarting) {
return;
} else {
restarting = true;
}
if (server) {
// eslint-disable-next-line no-console
console.log('Configuration changed. Restarting the server...');
await promisify(server.close).call(server);
}
// $FlowFixMe: Flow + Promises don't work consistently https://fb.facebook.com/groups/flow/permalink/1772334656148475/
const config = await MetroApi.loadMetroConfig(argv.config);
if (argv.projectRoots) {
config.getProjectRoots = () => argv.projectRoots;
}
server = await MetroApi.runServer({
...argv,
config,
builder: (yargs: Yargs) => {
yargs.option('project-roots', {
alias: 'P',
type: 'string',
array: true,
});
restarting = false;
}
yargs.option('host', {alias: 'h', type: 'string', default: 'localhost'});
yargs.option('port', {alias: 'p', type: 'number', default: 8080});
const metroConfigLocation = await MetroApi.findMetroConfig(argv.config);
yargs.option('max-workers', {alias: 'j', type: 'number'});
if (metroConfigLocation) {
await watchFile(metroConfigLocation, restart);
} else {
await restart();
}
yargs.option('secure', {type: 'boolean'});
yargs.option('secure-key', {type: 'string'});
yargs.option('secure-cert', {type: 'string'});
yargs.option('hmr-enabled', {alias: 'hmr', type: 'boolean'});
yargs.option('config', {alias: 'c', type: 'string'});
// Deprecated
yargs.option('reset-cache', {type: 'boolean', describe: null});
},
// eslint-disable-next-line lint/no-unclear-flowtypes
handler: makeAsyncCommand(async (argv: any) => {
let server = null;
let restarting = false;
async function restart() {
if (restarting) {
return;
} else {
restarting = true;
}
if (server) {
// eslint-disable-next-line no-console
console.log('Configuration changed. Restarting the server...');
await promisify(server.close).call(server);
}
// $FlowFixMe: Flow + Promises don't work consistently https://fb.facebook.com/groups/flow/permalink/1772334656148475/
const config = await MetroApi.loadMetroConfig(argv.config);
if (argv.projectRoots) {
config.getProjectRoots = () => argv.projectRoots;
}
server = await MetroApi.runServer({
...argv,
config,
});
restarting = false;
}
const metroConfigLocation = await MetroApi.findMetroConfig(argv.config);
if (metroConfigLocation) {
await watchFile(metroConfigLocation, restart);
} else {
await restart();
}
}),
});

View File

@ -22,6 +22,8 @@ const fs = require('fs');
const getMaxWorkers = require('./lib/getMaxWorkers');
const http = require('http');
const https = require('https');
const makeBuildCommand = require('./commands/build');
const makeServeCommand = require('./commands/serve');
const outputBundle = require('./shared/output/bundle');
const path = require('path');
@ -31,12 +33,13 @@ const {Terminal} = require('metro-core');
import type {ConfigT} from './Config';
import type {GlobalTransformCache} from './lib/GlobalTransformCache';
import type {Options as ServerOptions} from './shared/types.flow';
import type {TransformCache} from './lib/TransformCaching';
import type {Reporter} from './lib/reporting';
import type {RequestOptions, OutputOptions} from './shared/types.flow.js';
import type {TransformCache} from './lib/TransformCaching';
import type {Options as ServerOptions} from './shared/types.flow';
import type {Server as HttpServer} from 'http';
import type {Server as HttpsServer} from 'https';
import typeof Yargs from 'yargs';
export type {ConfigT} from './Config';
@ -400,7 +403,7 @@ exports.findMetroConfig = function(
exports.loadMetroConfig = function(
filename: ?string,
// $FlowFixMe: This is a known Flow issue where it doesn't detect that an empty object is a valid value for a strict shape where all the members are optionals
// $FlowFixMe TODO T26072405
searchOptions: MetroConfigSearchOptions = {},
): ConfigT {
const location = exports.findMetroConfig(filename, searchOptions);
@ -411,6 +414,32 @@ exports.loadMetroConfig = function(
return config ? Config.normalize(config) : Config.DEFAULT;
};
type BuildCommandOptions = {||} | null;
type ServeCommandOptions = {||} | null;
exports.attachMetroCli = function(
yargs: Yargs,
{
// $FlowFixMe TODO T26072405
build = {},
// $FlowFixMe TODO T26072405
serve = {},
}: {
build: BuildCommandOptions,
serve: ServeCommandOptions,
} = {},
) {
if (build) {
const {command, description, builder, handler} = makeBuildCommand();
yargs.command(command, description, builder, handler);
}
if (serve) {
const {command, description, builder, handler} = makeServeCommand();
yargs.command(command, description, builder, handler);
}
return yargs;
};
exports.Config = Config;
exports.defaults = defaults;