/** * 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;