/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const IncrementalBundler = require('./IncrementalBundler');
const MetroHmrServer = require('./HmrServer');
const MetroServer = require('./Server');
const attachWebsocketServer = require('./lib/attachWebsocketServer');
const http = require('http');
const https = require('https');
const makeBuildCommand = require('./commands/build');
const makeServeCommand = require('./commands/serve');
const makeDependenciesCommand = require('./commands/dependencies');
const outputBundle = require('./shared/output/bundle');
const {readFile} = require('fs-extra');
const {loadConfig, mergeConfig, getDefaultConfig} = require('metro-config');
import type {Graph} from './DeltaBundler';
import type {CustomTransformOptions} from './JSTransformer/worker';
import type {RequestOptions, OutputOptions} from './shared/types.flow.js';
import type {Server as HttpServer} from 'http';
import type {Server as HttpsServer} from 'https';
import type {ConfigT, InputConfigT} from 'metro-config/src/configTypes.flow';
import typeof Yargs from 'yargs';
async function getConfig(config: InputConfigT): Promise<ConfigT> {
const defaultConfig = await getDefaultConfig(config.projectRoot);
return mergeConfig(defaultConfig, config);
}
async function runMetro(config: InputConfigT): Promise<MetroServer> {
const mergedConfig = await getConfig(config);
mergedConfig.reporter.update({
type: 'initialize_started',
port: mergedConfig.server.port,
// FIXME: We need to change that to watchFolders. It will be a
// breaking since it affects custom reporter API.
projectRoots: mergedConfig.watchFolders,
});
return new MetroServer(mergedConfig);
}
exports.runMetro = runMetro;
exports.loadConfig = loadConfig;
exports.createConnectMiddleware = async function(config: ConfigT) {
const metroServer = await runMetro(config);
let enhancedMiddleware = metroServer.processRequest;
// Enhance the resulting middleware using the config options
if (config.server.enhanceMiddleware) {
enhancedMiddleware = config.server.enhanceMiddleware(
enhancedMiddleware,
metroServer,
);
}
return {
attachHmrServer(httpServer: HttpServer | HttpsServer) {
attachWebsocketServer({
httpServer,
path: '/hot',
websocketServer: new MetroHmrServer(
metroServer.getBundler(),
metroServer.getCreateModuleId(),
config,
),
});
},
metroServer,
middleware: enhancedMiddleware,
end() {
metroServer.end();
},
};
};
type RunServerOptions = {|
host?: string,
onReady?: (server: HttpServer | HttpsServer) => void,
onError?: (Error & {|code?: string|}) => void,
secure?: boolean,
secureKey?: string,
secureCert?: string,
hmrEnabled?: boolean,
|};
exports.runServer = async (
config: ConfigT,
{
host,
onReady,
onError,
secure = false,
secureKey,
secureCert,
hmrEnabled = false,
}: RunServerOptions,
) => {
// Lazy require
const connect = require('connect');
const serverApp = connect();
const {
attachHmrServer,
middleware,
metroServer,
end,
} = await exports.createConnectMiddleware(config);
serverApp.use(middleware);
if (config.server.enableVisualizer) {
let initializeVisualizerMiddleware;
try {
// eslint-disable-next-line import/no-extraneous-dependencies
({initializeVisualizerMiddleware} = require('metro-visualizer'));
} catch (e) {
console.warn(
"'config.server.enableVisualizer' is enabled but the 'metro-visualizer' package was not found - have you installed it?",
);
}
if (initializeVisualizerMiddleware) {
serverApp.use('/visualizer', initializeVisualizerMiddleware(metroServer));
}
}
let httpServer;
if (secure) {
httpServer = https.createServer(
{
key: await readFile(secureKey),
cert: await readFile(secureCert),
},
serverApp,
);
} else {
httpServer = http.createServer(serverApp);
}
httpServer.on('error', error => {
onError && onError(error);
end();
});
if (hmrEnabled) {
attachHmrServer(httpServer);
}
return new Promise((resolve, reject) => {
httpServer.listen(config.server.port, host, () => {
onReady && onReady(httpServer);
resolve(httpServer);
});
// Disable any kind of automatic timeout behavior for incoming
// requests in case it takes the packager more than the default
// timeout of 120 seconds to respond to a request.
httpServer.timeout = 0;
httpServer.on('error', error => {
end();
reject(error);
});
httpServer.on('close', () => {
end();
});
});
};
type BuildGraphOptions = {|
entries: $ReadOnlyArray<string>,
customTransformOptions?: CustomTransformOptions,
dev?: boolean,
minify?: boolean,
onProgress?: (transformedFileCount: number, totalFileCount: number) => void,
platform?: string,
type?: 'module' | 'script',
|};
type RunBuildOptions = {|
entry: string,
dev?: boolean,
out?: string,
onBegin?: () => void,
onComplete?: () => void,
onProgress?: (transformedFileCount: number, totalFileCount: number) => void,
minify?: boolean,
output?: {
build: (
MetroServer,
RequestOptions,
) => Promise<{code: string, map: string}>,
save: (
{code: string, map: string},
OutputOptions,
(...args: Array<string>) => void,
) => Promise<mixed>,
},
platform?: string,
sourceMap?: boolean,
sourceMapUrl?: string,
|};
exports.runBuild = async (
config: ConfigT,
{
dev = false,
entry,
onBegin,
onComplete,
onProgress,
minify = true,
output = outputBundle,
out,
platform = 'web',
sourceMap = false,
sourceMapUrl,
}: RunBuildOptions,
) => {
const metroServer = await runMetro(config);
try {
const requestOptions: RequestOptions = {
dev,
entryFile: entry,
inlineSourceMap: sourceMap && !sourceMapUrl,
minify,
platform,
sourceMapUrl: sourceMap === false ? undefined : sourceMapUrl,
createModuleIdFactory: config.serializer.createModuleIdFactory,
onProgress,
};
if (onBegin) {
onBegin();
}
const metroBundle = await output.build(metroServer, requestOptions);
if (onComplete) {
onComplete();
}
if (out) {
const bundleOutput = out.replace(/(\.js)?$/, '.js');
const sourcemapOutput =
sourceMap === false ? undefined : out.replace(/(\.js)?$/, '.map');
const outputOptions: OutputOptions = {
bundleOutput,
sourcemapOutput,
dev,
platform,
};
// eslint-disable-next-line no-console
await output.save(metroBundle, outputOptions, console.log);
}
return metroBundle;
} finally {
await metroServer.end();
}
};
exports.buildGraph = async function(
config: InputConfigT,
{
customTransformOptions = Object.create(null),
dev = false,
entries,
minify = false,
onProgress,
platform = 'web',
type = 'module',
}: BuildGraphOptions,
): Promise<Graph<>> {
const mergedConfig = await getConfig(config);
const bundler = new IncrementalBundler(mergedConfig);
try {
return await bundler.buildGraphForEntries(entries, {
...MetroServer.DEFAULT_GRAPH_OPTIONS,
customTransformOptions,
dev,
minify,
platform,
type,
});
} finally {
bundler.end();
}
};
type BuildCommandOptions = {||} | null;
type ServeCommandOptions = {||} | null;
exports.attachMetroCli = function(
yargs: Yargs,
{
// $FlowFixMe TODO T26072405
build = {},
// $FlowFixMe TODO T26072405
serve = {},
dependencies = {},
}: {
build: BuildCommandOptions,
serve: ServeCommandOptions,
dependencies: any,
} = {},
) {
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);
}
if (dependencies) {
const {command, description, builder, handler} = makeDependenciesCommand();
yargs.command(command, description, builder, handler);
}
return yargs;
};
// The symbols below belong to the legacy API and should not be relied upon
Object.assign(exports, require('./legacy'));