/**
* 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 MultipartResponse = require('./Server/MultipartResponse');
const baseJSBundle = require('./DeltaBundler/Serializers/baseJSBundle');
const bundleToString = require('./lib/bundle-modules/DeltaClient/bundleToString');
const deltaJSBundle = require('./DeltaBundler/Serializers/deltaJSBundle');
const getAllFiles = require('./DeltaBundler/Serializers/getAllFiles');
const getAssets = require('./DeltaBundler/Serializers/getAssets');
const getGraphId = require('./lib/getGraphId');
const getRamBundleInfo = require('./DeltaBundler/Serializers/getRamBundleInfo');
const sourceMapObject = require('./DeltaBundler/Serializers/sourceMapObject');
const sourceMapString = require('./DeltaBundler/Serializers/sourceMapString');
const splitBundleOptions = require('./lib/splitBundleOptions');
const debug = require('debug')('Metro:Server');
const formatBundlingError = require('./lib/formatBundlingError');
const mime = require('mime-types');
const parseOptionsFromUrl = require('./lib/parseOptionsFromUrl');
const transformHelpers = require('./lib/transformHelpers');
const parsePlatformFilePath = require('./node-haste/lib/parsePlatformFilePath');
const path = require('path');
const serializeDeltaJSBundle = require('./DeltaBundler/Serializers/helpers/serializeDeltaJSBundle');
const symbolicate = require('./Server/symbolicate/symbolicate');
const url = require('url');
const ResourceNotFoundError = require('./IncrementalBundler/ResourceNotFoundError');
const RevisionNotFoundError = require('./IncrementalBundler/RevisionNotFoundError');
const {getAsset} = require('./Assets');
import type {IncomingMessage, ServerResponse} from 'http';
import type {Reporter} from './lib/reporting';
import type {GraphId} from './lib/getGraphId';
import type {RamBundleInfo} from './DeltaBundler/Serializers/getRamBundleInfo';
import type {BundleOptions, SplitBundleOptions} from './shared/types.flow';
import type {
ConfigT,
VisualizerConfigT,
} from 'metro-config/src/configTypes.flow';
import type {MetroSourceMap} from 'metro-source-map';
import type {Symbolicate} from './Server/symbolicate/symbolicate';
import type {AssetData} from './Assets';
import type {RevisionId} from './IncrementalBundler';
import type {Graph, Module} from './DeltaBundler/types.flow';
const {
Logger,
Logger: {createActionStartEntry, createActionEndEntry, log},
} = require('metro-core');
import type {
ActionLogEntryData,
ActionStartLogEntry,
LogEntry,
} from 'metro-core/src/Logger';
type ProcessStartContext = {|
+mres: MultipartResponse,
+req: IncomingMessage,
+buildID: string,
+graphId: GraphId,
+revisionId: ?RevisionId,
+bundleOptions: BundleOptions,
...SplitBundleOptions,
|};
type ProcessEndContext<T> = {|
...ProcessStartContext,
+result: T,
|};
function debounceAndBatch(fn, delay) {
let timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(fn, delay);
};
}
const DELTA_ID_HEADER = 'X-Metro-Delta-ID';
const FILES_CHANGED_COUNT_HEADER = 'X-Metro-Files-Changed-Count';
class Server {
_config: ConfigT;
_changeWatchers: Array<{
req: IncomingMessage,
res: ServerResponse,
}>;
_createModuleId: (path: string) => number;
_reporter: Reporter;
_logger: typeof Logger;
_symbolicateInWorker: Symbolicate;
_platforms: Set<string>;
_nextBundleBuildID: number;
_bundler: IncrementalBundler;
constructor(config: ConfigT) {
this._config = config;
if (this._config.resetCache) {
this._config.cacheStores.forEach(store => store.clear());
this._config.reporter.update({type: 'transform_cache_reset'});
}
this._reporter = config.reporter;
this._logger = Logger;
this._changeWatchers = [];
this._platforms = new Set(this._config.resolver.platforms);
// TODO(T34760917): These two properties should eventually be instantiated
// elsewhere and passed as parameters, since they are also needed by
// the HmrServer.
// The whole bundling/serializing logic should follow as well.
this._createModuleId = config.serializer.createModuleIdFactory();
this._bundler = new IncrementalBundler(config);
const debouncedFileChangeHandler = debounceAndBatch(
() => this._informChangeWatchers(),
50,
);
// changes to the haste map can affect resolution of files in the bundle
this._bundler
.getBundler()
.getDependencyGraph()
.then(dependencyGraph => {
dependencyGraph.getWatcher().on('change', () => {
// Make sure the file watcher event runs through the system before
// we rebuild the bundles.
debouncedFileChangeHandler();
});
});
this._symbolicateInWorker = symbolicate.createWorker();
this._nextBundleBuildID = 1;
}
end() {
this._bundler.end();
}
getBundler(): IncrementalBundler {
return this._bundler;
}
getCreateModuleId(): (path: string) => number {
return this._createModuleId;
}
async build(options: BundleOptions): Promise<{code: string, map: string}> {
const {
entryFile,
transformOptions,
serializerOptions,
onProgress,
} = splitBundleOptions(options);
const {prepend, graph} = await this._bundler.buildGraph(
entryFile,
transformOptions,
{onProgress},
);
const entryPoint = path.resolve(this._config.projectRoot, entryFile);
const bundle = baseJSBundle(entryPoint, prepend, graph, {
processModuleFilter: this._config.serializer.processModuleFilter,
createModuleId: this._createModuleId,
getRunModuleStatement: this._config.serializer.getRunModuleStatement,
dev: transformOptions.dev,
projectRoot: this._config.projectRoot,
runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule(
path.relative(this._config.projectRoot, entryPoint),
),
runModule: serializerOptions.runModule,
sourceMapUrl: serializerOptions.sourceMapUrl,
inlineSourceMap: serializerOptions.inlineSourceMap,
});
return {
code: bundleToString(bundle).code,
map: sourceMapString([...prepend, ...this._getSortedModules(graph)], {
excludeSource: serializerOptions.excludeSource,
processModuleFilter: this._config.serializer.processModuleFilter,
}),
};
}
async getRamBundleInfo(options: BundleOptions): Promise<RamBundleInfo> {
const {
entryFile,
transformOptions,
serializerOptions,
onProgress,
} = splitBundleOptions(options);
const {prepend, graph} = await this._bundler.buildGraph(
entryFile,
transformOptions,
{onProgress},
);
const entryPoint = path.resolve(this._config.projectRoot, entryFile);
return await getRamBundleInfo(entryPoint, prepend, graph, {
processModuleFilter: this._config.serializer.processModuleFilter,
createModuleId: this._createModuleId,
dev: transformOptions.dev,
excludeSource: serializerOptions.excludeSource,
getRunModuleStatement: this._config.serializer.getRunModuleStatement,
getTransformOptions: this._config.transformer.getTransformOptions,
platform: transformOptions.platform,
projectRoot: this._config.projectRoot,
runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule(
path.relative(this._config.projectRoot, entryPoint),
),
runModule: serializerOptions.runModule,
sourceMapUrl: serializerOptions.sourceMapUrl,
inlineSourceMap: serializerOptions.inlineSourceMap,
});
}
async getAssets(options: BundleOptions): Promise<$ReadOnlyArray<AssetData>> {
const {entryFile, transformOptions, onProgress} = splitBundleOptions(
options,
);
const {graph} = await this._bundler.buildGraph(
entryFile,
transformOptions,
{onProgress},
);
return await getAssets(graph, {
processModuleFilter: this._config.serializer.processModuleFilter,
assetPlugins: this._config.transformer.assetPlugins,
platform: transformOptions.platform,
projectRoot: this._config.projectRoot,
publicPath: this._config.transformer.publicPath,
});
}
async getOrderedDependencyPaths(options: {
+entryFile: string,
+dev: boolean,
+minify: boolean,
+platform: string,
}): Promise<Array<string>> {
const {entryFile, transformOptions, onProgress} = splitBundleOptions({
...Server.DEFAULT_BUNDLE_OPTIONS,
...options,
bundleType: 'bundle',
});
const {prepend, graph} = await this._bundler.buildGraph(
entryFile,
transformOptions,
{onProgress},
);
const platform =
transformOptions.platform ||
parsePlatformFilePath(entryFile, this._platforms).platform;
return await getAllFiles(prepend, graph, {
platform,
processModuleFilter: this._config.serializer.processModuleFilter,
});
}
_informChangeWatchers() {
const watchers = this._changeWatchers;
const headers = {
'Content-Type': 'application/json; charset=UTF-8',
};
watchers.forEach(function(w) {
w.res.writeHead(205, headers);
w.res.end(JSON.stringify({changed: true}));
});
this._changeWatchers = [];
}
_processOnChangeRequest(req: IncomingMessage, res: ServerResponse) {
const watchers = this._changeWatchers;
watchers.push({
req,
res,
});
req.on('close', () => {
for (let i = 0; i < watchers.length; i++) {
if (watchers[i] && watchers[i].req === req) {
watchers.splice(i, 1);
break;
}
}
});
}
_rangeRequestMiddleware(
req: IncomingMessage,
res: ServerResponse,
data: string | Buffer,
assetPath: string,
) {
if (req.headers && req.headers.range) {
const [rangeStart, rangeEnd] = req.headers.range
.replace(/bytes=/, '')
.split('-');
const dataStart = parseInt(rangeStart, 10);
const dataEnd = rangeEnd ? parseInt(rangeEnd, 10) : data.length - 1;
const chunksize = dataEnd - dataStart + 1;
res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Length': chunksize.toString(),
'Content-Range': `bytes ${dataStart}-${dataEnd}/${data.length}`,
'Content-Type': mime.lookup(path.basename(assetPath[1])),
});
return data.slice(dataStart, dataEnd + 1);
}
return data;
}
async _processSingleAssetRequest(req: IncomingMessage, res: ServerResponse) {
const urlObj = url.parse(decodeURI(req.url), true);
/* $FlowFixMe: could be empty if the url is invalid */
const assetPath: string = urlObj.pathname.match(/^\/assets\/(.+)$/);
const processingAssetRequestLogEntry = log(
createActionStartEntry({
action_name: 'Processing asset request',
asset: assetPath[1],
}),
);
try {
const data = await getAsset(
assetPath[1],
this._config.projectRoot,
this._config.watchFolders,
/* $FlowFixMe: query may be empty for invalid URLs */
urlObj.query.platform,
);
// Tell clients to cache this for 1 year.
// This is safe as the asset url contains a hash of the asset.
if (process.env.REACT_NATIVE_ENABLE_ASSET_CACHING === true) {
res.setHeader('Cache-Control', 'max-age=31536000');
}
res.end(this._rangeRequestMiddleware(req, res, data, assetPath));
process.nextTick(() => {
log(createActionEndEntry(processingAssetRequestLogEntry));
});
} catch (error) {
console.error(error.stack);
res.writeHead(404);
res.end('Asset not found');
}
}
processRequest = (
req: IncomingMessage,
res: ServerResponse,
next: (?Error) => mixed,
) => {
this._processRequest(req, res, next).catch(next);
};
async _processRequest(
req: IncomingMessage,
res: ServerResponse,
next: (?Error) => mixed,
) {
const urlObj = url.parse(req.url, true);
const {host} = req.headers;
debug(`Handling request: ${host ? 'http://' + host : ''}${req.url}`);
/* $FlowFixMe: Could be empty if the URL is invalid. */
const pathname: string = urlObj.pathname;
if (pathname.match(/\.bundle$/)) {
await this._processBundleRequest(req, res);
} else if (pathname.match(/\.map$/)) {
await this._processSourceMapRequest(req, res);
} else if (pathname.match(/\.assets$/)) {
await this._processAssetsRequest(req, res);
} else if (pathname.match(/\.delta$/)) {
await this._processDeltaRequest(req, res);
} else if (pathname.match(/\.meta/)) {
await this._processMetadataRequest(req, res);
} else if (pathname.match(/^\/onchange\/?$/)) {
this._processOnChangeRequest(req, res);
} else if (pathname.match(/^\/assets\//)) {
await this._processSingleAssetRequest(req, res);
} else if (pathname === '/symbolicate') {
this._symbolicate(req, res);
} else {
next();
}
}
_createRequestProcessor<T>({
createStartEntry,
createEndEntry,
build,
finish,
}: {|
+createStartEntry: (context: ProcessStartContext) => ActionLogEntryData,
+createEndEntry: (
context: ProcessEndContext<T>,
) => $Rest<ActionStartLogEntry, LogEntry>,
+build: (context: ProcessStartContext) => Promise<T>,
+finish: (context: ProcessEndContext<T>) => void,
|}) {
return async function requestProcessor(
req: IncomingMessage,
res: ServerResponse,
) {
const mres = MultipartResponse.wrap(req, res);
const {revisionId, options: bundleOptions} = parseOptionsFromUrl(
url.format({
...url.parse(req.url),
protocol: 'http',
host: req.headers.host,
}),
new Set(this._config.resolver.platforms),
);
const {
entryFile,
transformOptions,
serializerOptions,
} = splitBundleOptions(bundleOptions);
/**
* `entryFile` is relative to projectRoot, we need to use resolution function
* to find the appropriate file with supported extensions.
*/
const resolutionFn = await transformHelpers.getResolveDependencyFn(
this._bundler.getBundler(),
transformOptions.platform,
);
const resolvedEntryFilePath = resolutionFn(
`${this._config.projectRoot}/.`,
entryFile,
);
const graphId = getGraphId(resolvedEntryFilePath, transformOptions);
const buildID = this.getNewBuildID();
let onProgress = null;
if (this._config.reporter) {
onProgress = (transformedFileCount, totalFileCount) => {
mres.writeChunk(
{'Content-Type': 'application/json'},
JSON.stringify({done: transformedFileCount, total: totalFileCount}),
);
this._reporter.update({
buildID,
type: 'bundle_transform_progressed',
transformedFileCount,
totalFileCount,
});
};
}
/* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.63 was deployed. To see the error delete this
* comment and run Flow. */
this._reporter.update({
buildID,
bundleDetails: {
entryFile: resolvedEntryFilePath,
platform: transformOptions.platform,
dev: transformOptions.dev,
minify: transformOptions.minify,
bundleType: bundleOptions.bundleType,
},
type: 'bundle_build_started',
});
const startContext = {
req,
mres,
revisionId,
buildID,
bundleOptions,
entryFile: resolvedEntryFilePath,
transformOptions,
serializerOptions,
onProgress,
graphId,
};
const logEntry = log(
createActionStartEntry(createStartEntry(startContext)),
);
let result;
try {
result = await build(startContext);
} catch (error) {
const formattedError = formatBundlingError(error);
const status = error instanceof ResourceNotFoundError ? 404 : 500;
mres.writeHead(status, {
'Content-Type': 'application/json; charset=UTF-8',
});
mres.end(JSON.stringify(formattedError));
this._reporter.update({error, type: 'bundling_error'});
log({
action_name: 'bundling_error',
error_type: formattedError.type,
log_entry_label: 'bundling_error',
bundle_id: graphId,
build_id: buildID,
stack: formattedError.message,
});
this._reporter.update({
buildID,
type: 'bundle_build_failed',
bundleOptions,
});
return;
}
const endContext = {
...startContext,
result,
};
finish(endContext);
this._reporter.update({
buildID,
type: 'bundle_build_done',
});
log(
createActionEndEntry({
...logEntry,
...createEndEntry(endContext),
}),
);
};
}
_processDeltaRequest = this._createRequestProcessor({
createStartEntry(context) {
return {
action_name: 'Requesting delta',
bundle_url: context.req.url,
entry_point: context.entryFile,
bundler: 'delta',
build_id: context.buildID,
bundle_options: context.bundleOptions,
bundle_hash: context.graphId,
};
},
createEndEntry(context) {
return {
outdated_modules: context.result.numModifiedFiles,
};
},
build: async ({
revisionId,
graphId,
entryFile,
transformOptions,
serializerOptions,
onProgress,
}) => {
// TODO(T34760593): We should eventually move to a model where this
// endpoint is placed at /delta/:revisionId, and requesting an unknown revisionId
// throws a 404.
// However, this would break existing delta clients, since they expect the
// endpoint to rebuild the graph, were it not found in cache.
let revPromise;
if (revisionId != null) {
revPromise = this._bundler.getRevision(revisionId);
}
// Even if we receive a revisionId, it might have expired.
if (revPromise == null) {
revPromise = this._bundler.getRevisionByGraphId(graphId);
}
let delta;
let revision;
if (revPromise != null) {
const prevRevision = await revPromise;
({delta, revision} = await this._bundler.updateGraph(
prevRevision,
prevRevision.id !== revisionId,
));
} else {
({delta, revision} = await this._bundler.initializeGraph(
entryFile,
transformOptions,
{onProgress},
));
}
const bundle = deltaJSBundle(
entryFile,
revision.prepend,
delta,
revision.id,
revision.graph,
{
processModuleFilter: this._config.serializer.processModuleFilter,
createModuleId: this._createModuleId,
dev: transformOptions.dev,
getRunModuleStatement: this._config.serializer.getRunModuleStatement,
projectRoot: this._config.projectRoot,
runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule(
path.relative(this._config.projectRoot, entryFile),
),
runModule: serializerOptions.runModule,
sourceMapUrl: serializerOptions.sourceMapUrl,
inlineSourceMap: serializerOptions.inlineSourceMap,
},
);
return {
numModifiedFiles:
delta.added.size + delta.modified.size + delta.deleted.size,
nextRevId: revision.id,
bundle,
};
},
finish({mres, result}) {
const bundle = serializeDeltaJSBundle.toJSON(result.bundle);
mres.setHeader(
FILES_CHANGED_COUNT_HEADER,
String(result.numModifiedFiles),
);
mres.setHeader(DELTA_ID_HEADER, String(result.nextRevId));
mres.setHeader('Content-Type', 'application/json');
mres.setHeader('Content-Length', String(Buffer.byteLength(bundle)));
mres.end(bundle);
},
});
_processBundleRequest = this._createRequestProcessor({
createStartEntry(context) {
return {
action_name: 'Requesting bundle',
bundle_url: context.req.url,
entry_point: context.entryFile,
bundler: 'delta',
build_id: context.buildID,
bundle_options: context.bundleOptions,
bundle_hash: context.graphId,
};
},
createEndEntry(context) {
return {
outdated_modules: context.result.numModifiedFiles,
};
},
build: async ({
graphId,
entryFile,
transformOptions,
serializerOptions,
onProgress,
}) => {
const revPromise = this._bundler.getRevisionByGraphId(graphId);
const {delta, revision} = await (revPromise != null
? this._bundler.updateGraph(await revPromise, false)
: this._bundler.initializeGraph(entryFile, transformOptions, {
onProgress,
}));
const serializer =
this._config.serializer.customSerializer ||
((...args) => bundleToString(baseJSBundle(...args)).code);
const bundle = serializer(entryFile, revision.prepend, revision.graph, {
processModuleFilter: this._config.serializer.processModuleFilter,
createModuleId: this._createModuleId,
getRunModuleStatement: this._config.serializer.getRunModuleStatement,
dev: transformOptions.dev,
projectRoot: this._config.projectRoot,
runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule(
path.relative(this._config.projectRoot, entryFile),
),
runModule: serializerOptions.runModule,
sourceMapUrl: serializerOptions.sourceMapUrl,
inlineSourceMap: serializerOptions.inlineSourceMap,
});
return {
numModifiedFiles: delta.reset
? delta.added.size + revision.prepend.length
: delta.added.size + delta.modified.size + delta.deleted.size,
lastModifiedDate: revision.date,
nextRevId: revision.id,
bundle,
};
},
finish({req, mres, result}) {
if (
// We avoid parsing the dates since the client should never send a more
// recent date than the one returned by the Delta Bundler (if that's the
// case it's fine to return the whole bundle).
req.headers['if-modified-since'] ===
result.lastModifiedDate.toUTCString()
) {
debug('Responding with 304');
mres.writeHead(304);
mres.end();
} else {
mres.setHeader(
FILES_CHANGED_COUNT_HEADER,
String(result.numModifiedFiles),
);
mres.setHeader(DELTA_ID_HEADER, String(result.nextRevId));
mres.setHeader('Content-Type', 'application/javascript');
mres.setHeader('Last-Modified', result.lastModifiedDate.toUTCString());
mres.setHeader(
'Content-Length',
String(Buffer.byteLength(result.bundle)),
);
mres.end(result.bundle);
}
},
});
// This function ensures that modules in source maps are sorted in the same
// order as in a plain JS bundle.
_getSortedModules(graph: Graph<>): $ReadOnlyArray<Module<>> {
return [...graph.dependencies.values()].sort(
(a, b) => this._createModuleId(a.path) - this._createModuleId(b.path),
);
}
_processSourceMapRequest = this._createRequestProcessor({
createStartEntry(context) {
return {
action_name: 'Requesting sourcemap',
bundle_url: context.req.url,
entry_point: context.entryFile,
bundler: 'delta',
};
},
createEndEntry(context) {
return {
bundler: 'delta',
};
},
build: async ({
entryFile,
transformOptions,
serializerOptions,
onProgress,
graphId,
}) => {
let revision;
const revPromise = this._bundler.getRevisionByGraphId(graphId);
if (revPromise == null) {
({revision} = await this._bundler.initializeGraph(
entryFile,
transformOptions,
{onProgress},
));
} else {
revision = await revPromise;
}
const {prepend, graph} = revision;
return sourceMapString([...prepend, ...this._getSortedModules(graph)], {
excludeSource: serializerOptions.excludeSource,
processModuleFilter: this._config.serializer.processModuleFilter,
});
},
finish({mres, result}) {
mres.setHeader('Content-Type', 'application/json');
mres.end(result.toString());
},
});
_processMetadataRequest = this._createRequestProcessor({
createStartEntry(context) {
return {
action_name: 'Requesting bundle metadata',
bundle_url: context.req.url,
entry_point: context.entryFile,
bundler: 'delta',
};
},
createEndEntry(context) {
return {
bundler: 'delta',
};
},
build: async ({
entryFile,
transformOptions,
serializerOptions,
onProgress,
revisionId,
}) => {
if (revisionId == null) {
throw new Error(
'You must provide a `revisionId` query parameter to the metadata endpoint.',
);
}
let revision;
const revPromise = this._bundler.getRevision(revisionId);
if (revPromise == null) {
throw new RevisionNotFoundError(revisionId);
} else {
revision = await revPromise;
}
const base = baseJSBundle(entryFile, revision.prepend, revision.graph, {
processModuleFilter: this._config.serializer.processModuleFilter,
createModuleId: this._createModuleId,
getRunModuleStatement: this._config.serializer.getRunModuleStatement,
dev: transformOptions.dev,
projectRoot: this._config.projectRoot,
runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule(
path.relative(this._config.projectRoot, entryFile),
),
runModule: serializerOptions.runModule,
sourceMapUrl: serializerOptions.sourceMapUrl,
inlineSourceMap: serializerOptions.inlineSourceMap,
});
return bundleToString(base).metadata;
},
finish({mres, result}) {
mres.setHeader('Content-Type', 'application/json');
mres.end(JSON.stringify(result));
},
});
_processAssetsRequest = this._createRequestProcessor({
createStartEntry(context) {
return {
action_name: 'Requesting assets',
bundle_url: context.req.url,
entry_point: context.entryFile,
bundler: 'delta',
};
},
createEndEntry(context) {
return {
bundler: 'delta',
};
},
build: async ({entryFile, transformOptions, onProgress}) => {
const {graph} = await this._bundler.buildGraph(
entryFile,
transformOptions,
{onProgress},
);
return await getAssets(graph, {
processModuleFilter: this._config.serializer.processModuleFilter,
assetPlugins: this._config.transformer.assetPlugins,
platform: transformOptions.platform,
publicPath: this._config.transformer.publicPath,
projectRoot: this._config.projectRoot,
});
},
finish({mres, result}) {
mres.setHeader('Content-Type', 'application/json');
mres.end(JSON.stringify(result));
},
});
_symbolicate(req: IncomingMessage, res: ServerResponse) {
const symbolicatingLogEntry = log(createActionStartEntry('Symbolicating'));
debug('Start symbolication');
/* $FlowFixMe: where is `rowBody` defined? Is it added by
* the `connect` framework? */
Promise.resolve(req.rawBody)
.then(body => {
const stack = JSON.parse(body).stack;
// In case of multiple bundles / HMR, some stack frames can have
// different URLs from others
const urls = new Set();
stack.forEach(frame => {
const sourceUrl = frame.file;
// Skip `/debuggerWorker.js` which drives remote debugging because it
// does not need to symbolication.
// Skip anything except http(s), because there is no support for that yet
if (
sourceUrl != null &&
!urls.has(sourceUrl) &&
!sourceUrl.endsWith('/debuggerWorker.js') &&
sourceUrl.startsWith('http')
) {
urls.add(sourceUrl);
}
});
const mapPromises = Array.from(urls.values()).map(
this._sourceMapForURL,
this,
);
debug('Getting source maps for symbolication');
return Promise.all(mapPromises).then(maps => {
debug('Sending stacks and maps to symbolication worker');
const urlsToMaps = zip(urls.values(), maps);
return this._symbolicateInWorker(stack, urlsToMaps);
});
})
.then(
stack => {
debug('Symbolication done');
res.end(JSON.stringify({stack}));
process.nextTick(() => {
log(createActionEndEntry(symbolicatingLogEntry));
});
},
error => {
console.error(error.stack || error);
res.statusCode = 500;
res.end(JSON.stringify({error: error.message}));
},
);
}
async _sourceMapForURL(reqUrl: string): Promise<MetroSourceMap> {
const {options} = parseOptionsFromUrl(
reqUrl,
new Set(this._config.resolver.platforms),
);
const {
entryFile,
transformOptions,
serializerOptions,
onProgress,
} = splitBundleOptions(options);
/**
* `entryFile` is relative to projectRoot, we need to use resolution function
* to find the appropriate file with supported extensions.
*/
const resolutionFn = await transformHelpers.getResolveDependencyFn(
this._bundler.getBundler(),
transformOptions.platform,
);
const resolvedEntryFilePath = resolutionFn(
`${this._config.projectRoot}/.`,
entryFile,
);
const graphId = getGraphId(resolvedEntryFilePath, transformOptions);
let revision;
const revPromise = this._bundler.getRevisionByGraphId(graphId);
if (revPromise == null) {
({revision} = await this._bundler.initializeGraph(
resolvedEntryFilePath,
transformOptions,
{onProgress},
));
} else {
revision = await revPromise;
}
const {prepend, graph} = revision;
return sourceMapObject([...prepend, ...this._getSortedModules(graph)], {
excludeSource: serializerOptions.excludeSource,
processModuleFilter: this._config.serializer.processModuleFilter,
});
}
getNewBuildID(): string {
return (this._nextBundleBuildID++).toString(36);
}
getPlatforms(): $ReadOnlyArray<string> {
return this._config.resolver.platforms;
}
getWatchFolders(): $ReadOnlyArray<string> {
return this._config.watchFolders;
}
getVisualizerConfig(): $ReadOnly<VisualizerConfigT> {
return this._config.visualizer;
}
static DEFAULT_GRAPH_OPTIONS = {
customTransformOptions: Object.create(null),
dev: true,
hot: false,
minify: false,
};
static DEFAULT_BUNDLE_OPTIONS = {
...Server.DEFAULT_GRAPH_OPTIONS,
excludeSource: false,
inlineSourceMap: false,
onProgress: null,
runModule: true,
sourceMapUrl: null,
};
}
function* zip<X, Y>(xs: Iterable<X>, ys: Iterable<Y>): Iterable<[X, Y]> {
//$FlowIssue #9324959
const ysIter: Iterator<Y> = ys[Symbol.iterator]();
for (const x of xs) {
const y = ysIter.next();
if (y.done) {
return;
}
yield [x, y.value];
}
}
module.exports = Server;