"use strict"; var _types = require("./shared/types.flow"); const { getAsset } = require("./Assets"); const baseJSBundle = require("./DeltaBundler/Serializers/baseJSBundle"); const getAllFiles = require("./DeltaBundler/Serializers/getAllFiles"); const getAssets = require("./DeltaBundler/Serializers/getAssets"); const { getExplodedSourceMap, } = require("./DeltaBundler/Serializers/getExplodedSourceMap"); const getRamBundleInfo = require("./DeltaBundler/Serializers/getRamBundleInfo"); const { sourceMapStringNonBlocking, } = require("./DeltaBundler/Serializers/sourceMapString"); const IncrementalBundler = require("./IncrementalBundler"); const ResourceNotFoundError = require("./IncrementalBundler/ResourceNotFoundError"); const bundleToString = require("./lib/bundleToString"); const formatBundlingError = require("./lib/formatBundlingError"); const getGraphId = require("./lib/getGraphId"); const parseOptionsFromUrl = require("./lib/parseOptionsFromUrl"); const splitBundleOptions = require("./lib/splitBundleOptions"); const transformHelpers = require("./lib/transformHelpers"); const { UnableToResolveError, } = require("./node-haste/DependencyGraph/ModuleResolution"); const parsePlatformFilePath = require("./node-haste/lib/parsePlatformFilePath"); const MultipartResponse = require("./Server/MultipartResponse"); const symbolicate = require("./Server/symbolicate"); const { codeFrameColumns } = require("@babel/code-frame"); const debug = require("debug")("Metro:Server"); const fs = require("graceful-fs"); const invariant = require("invariant"); const jscSafeUrl = require("jsc-safe-url"); const { Logger, Logger: { createActionStartEntry, createActionEndEntry, log }, } = require("metro-core"); const mime = require("mime-types"); const nullthrows = require("nullthrows"); const path = require("path"); const { performance } = require("perf_hooks"); const querystring = require("querystring"); const url = require("url"); const noopLogger = { start: () => {}, point: () => {}, annotate: () => {}, subSpan: () => noopLogger, end: () => {}, }; const DELTA_ID_HEADER = "X-Metro-Delta-ID"; const FILES_CHANGED_COUNT_HEADER = "X-Metro-Files-Changed-Count"; class Server { constructor(config, options) { this._config = config; this._serverOptions = options; 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._platforms = new Set(this._config.resolver.platforms); this._allowedSuffixesForSourceRequests = [ ...new Set( [ ...this._config.resolver.sourceExts, ...this._config.watcher.additionalExts, ...this._config.resolver.assetExts, ].map((ext) => "." + ext) ), ]; this._sourceRequestRoutingMap = [ ["/[metro-project]/", path.resolve(this._config.projectRoot)], ...this._config.watchFolders.map((watchFolder, index) => [ `/[metro-watchFolders]/${index}/`, path.resolve(watchFolder), ]), ]; this._isEnded = false; this._createModuleId = config.serializer.createModuleIdFactory(); this._bundler = new IncrementalBundler(config, { hasReducedPerformance: options && options.hasReducedPerformance, watch: options ? options.watch : undefined, }); this._nextBundleBuildNumber = 1; } async end() { if (!this._isEnded) { await this._bundler.end(); this._isEnded = true; } } getBundler() { return this._bundler; } getCreateModuleId() { return this._createModuleId; } async build(options) { const { entryFile, graphOptions, onProgress, resolverOptions, serializerOptions, transformOptions, } = splitBundleOptions(options); const { prepend, graph } = await this._bundler.buildGraph( entryFile, transformOptions, resolverOptions, { onProgress, shallow: graphOptions.shallow, lazy: graphOptions.lazy, } ); const entryPoint = this._getEntryPointAbsolutePath(entryFile); const bundleOptions = { asyncRequireModulePath: await this._resolveRelativePath( this._config.transformer.asyncRequireModulePath, { relativeTo: "project", resolverOptions, transformOptions, } ), processModuleFilter: this._config.serializer.processModuleFilter, createModuleId: this._createModuleId, getRunModuleStatement: this._config.serializer.getRunModuleStatement, dev: transformOptions.dev, includeAsyncPaths: graphOptions.lazy, projectRoot: this._config.projectRoot, modulesOnly: serializerOptions.modulesOnly, runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule( path.relative(this._config.projectRoot, entryPoint) ), runModule: serializerOptions.runModule, sourceMapUrl: serializerOptions.sourceMapUrl, sourceUrl: serializerOptions.sourceUrl, inlineSourceMap: serializerOptions.inlineSourceMap, serverRoot: this._config.server.unstable_serverRoot ?? this._config.projectRoot, shouldAddToIgnoreList: (module) => this._shouldAddModuleToIgnoreList(module), getSourceUrl: (module) => this._getModuleSourceUrl(module, serializerOptions.sourcePaths), }; let bundleCode = null; let bundleMap = null; if (this._config.serializer.customSerializer) { const bundle = await this._config.serializer.customSerializer( entryPoint, prepend, graph, bundleOptions ); if (typeof bundle === "string") { bundleCode = bundle; } else { bundleCode = bundle.code; bundleMap = bundle.map; } } else { bundleCode = bundleToString( baseJSBundle(entryPoint, prepend, graph, bundleOptions) ).code; } if (!bundleMap) { bundleMap = await sourceMapStringNonBlocking( [...prepend, ...this._getSortedModules(graph)], { excludeSource: serializerOptions.excludeSource, processModuleFilter: this._config.serializer.processModuleFilter, shouldAddToIgnoreList: bundleOptions.shouldAddToIgnoreList, getSourceUrl: (module) => this._getModuleSourceUrl(module, serializerOptions.sourcePaths), } ); } return { code: bundleCode, map: bundleMap, }; } async getRamBundleInfo(options) { const { entryFile, graphOptions, onProgress, resolverOptions, serializerOptions, transformOptions, } = splitBundleOptions(options); const { prepend, graph } = await this._bundler.buildGraph( entryFile, transformOptions, resolverOptions, { onProgress, shallow: graphOptions.shallow, lazy: graphOptions.lazy, } ); const entryPoint = this._getEntryPointAbsolutePath(entryFile); return await getRamBundleInfo(entryPoint, prepend, graph, { asyncRequireModulePath: await this._resolveRelativePath( this._config.transformer.asyncRequireModulePath, { relativeTo: "project", resolverOptions, transformOptions, } ), processModuleFilter: this._config.serializer.processModuleFilter, createModuleId: this._createModuleId, dev: transformOptions.dev, excludeSource: serializerOptions.excludeSource, getRunModuleStatement: this._config.serializer.getRunModuleStatement, getTransformOptions: this._config.transformer.getTransformOptions, includeAsyncPaths: graphOptions.lazy, platform: transformOptions.platform, projectRoot: this._config.projectRoot, modulesOnly: serializerOptions.modulesOnly, runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule( path.relative(this._config.projectRoot, entryPoint) ), runModule: serializerOptions.runModule, sourceMapUrl: serializerOptions.sourceMapUrl, sourceUrl: serializerOptions.sourceUrl, inlineSourceMap: serializerOptions.inlineSourceMap, serverRoot: this._config.server.unstable_serverRoot ?? this._config.projectRoot, shouldAddToIgnoreList: (module) => this._shouldAddModuleToIgnoreList(module), getSourceUrl: (module) => this._getModuleSourceUrl(module, serializerOptions.sourcePaths), }); } async getAssets(options) { const { entryFile, onProgress, resolverOptions, transformOptions } = splitBundleOptions(options); const dependencies = await this._bundler.getDependencies( [entryFile], transformOptions, resolverOptions, { onProgress, shallow: false, lazy: false, } ); return await getAssets(dependencies, { processModuleFilter: this._config.serializer.processModuleFilter, assetPlugins: this._config.transformer.assetPlugins, platform: transformOptions.platform, projectRoot: this._getServerRootDir(), publicPath: this._config.transformer.publicPath, }); } async getOrderedDependencyPaths(options) { const { entryFile, onProgress, resolverOptions, transformOptions } = splitBundleOptions({ ...Server.DEFAULT_BUNDLE_OPTIONS, ...options, bundleType: "bundle", }); const { prepend, graph } = await this._bundler.buildGraph( entryFile, transformOptions, resolverOptions, { onProgress, shallow: false, lazy: false, } ); const platform = transformOptions.platform || parsePlatformFilePath(entryFile, this._platforms).platform; return await getAllFiles(prepend, graph, { platform, processModuleFilter: this._config.serializer.processModuleFilter, }); } _rangeRequestMiddleware(req, res, data, assetPath) { 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}`, }); return data.slice(dataStart, dataEnd + 1); } res.setHeader("Content-Length", String(Buffer.byteLength(data))); return data; } async _processSingleAssetRequest(req, res) { const urlObj = url.parse(decodeURI(req.url), true); let [, assetPath] = (urlObj && urlObj.pathname && urlObj.pathname.match(/^\/assets\/(.+)$/)) || []; if (!assetPath && urlObj && urlObj.query && urlObj.query.unstable_path) { const [, actualPath, secondaryQuery] = nullthrows( urlObj.query.unstable_path.match(/^([^?]*)\??(.*)$/) ); if (secondaryQuery) { Object.assign(urlObj.query, querystring.parse(secondaryQuery)); } assetPath = actualPath; } if (!assetPath) { throw new Error("Could not extract asset path from URL"); } const processingAssetRequestLogEntry = log( createActionStartEntry({ action_name: "Processing asset request", asset: assetPath[1], }) ); try { const data = await getAsset( assetPath, this._config.projectRoot, this._config.watchFolders, urlObj.query.platform, this._config.resolver.assetExts ); if (process.env.REACT_NATIVE_ENABLE_ASSET_CACHING === true) { res.setHeader("Cache-Control", "max-age=31536000"); } res.setHeader("Content-Type", mime.lookup(path.basename(assetPath))); 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, res, next) => { this._processRequest(req, res, next).catch(next); }; _parseOptions(url) { return parseOptionsFromUrl(url, new Set(this._config.resolver.platforms)); } _rewriteAndNormalizeUrl(requestUrl) { return jscSafeUrl.toNormalUrl( this._config.server.rewriteRequestUrl(jscSafeUrl.toNormalUrl(requestUrl)) ); } async _processRequest(req, res, next) { const originalUrl = req.url; req.url = this._rewriteAndNormalizeUrl(req.url); const urlObj = url.parse(decodeURI(req.url), true); const { host } = req.headers; debug( `Handling request: ${host ? "http://" + host : ""}${req.url}` + (originalUrl !== req.url ? ` (rewritten from ${originalUrl})` : "") ); const formattedUrl = url.format({ ...urlObj, host, protocol: "http", }); const pathname = urlObj.pathname || ""; const buildNumber = this.getNewBuildNumber(); if (pathname.endsWith(".bundle")) { const options = this._parseOptions(formattedUrl); await this._processBundleRequest(req, res, options, { buildNumber, bundlePerfLogger: this._config.unstable_perfLoggerFactory?.("BUNDLING_REQUEST", { key: buildNumber, }) ?? noopLogger, }); if (this._serverOptions && this._serverOptions.onBundleBuilt) { this._serverOptions.onBundleBuilt(pathname); } } else if (pathname.endsWith(".map")) { res.setHeader("Access-Control-Allow-Origin", "devtools://devtools"); await this._processSourceMapRequest( req, res, this._parseOptions(formattedUrl), { buildNumber, bundlePerfLogger: noopLogger, } ); } else if (pathname.endsWith(".assets")) { await this._processAssetsRequest( req, res, this._parseOptions(formattedUrl), { buildNumber, bundlePerfLogger: noopLogger, } ); } else if (pathname.startsWith("/assets/") || pathname === "/assets") { await this._processSingleAssetRequest(req, res); } else if (pathname === "/symbolicate") { await this._symbolicate(req, res); } else { let handled = false; for (const [pathnamePrefix, normalizedRootDir] of this ._sourceRequestRoutingMap) { if (pathname.startsWith(pathnamePrefix)) { const relativePathname = pathname.substr(pathnamePrefix.length); await this._processSourceRequest( relativePathname, normalizedRootDir, res ); handled = true; break; } } if (!handled) { next(); } } } async _processSourceRequest(relativePathname, rootDir, res) { if ( !this._allowedSuffixesForSourceRequests.some((suffix) => relativePathname.endsWith(suffix) ) ) { res.writeHead(404); res.end(); return; } const depGraph = await this._bundler.getBundler().getDependencyGraph(); const filePath = path.join(rootDir, relativePathname); try { depGraph.getSha1(filePath); } catch { res.writeHead(404); res.end(); return; } const mimeType = mime.lookup(path.basename(relativePathname)); res.setHeader("Content-Type", mimeType); const stream = fs.createReadStream(filePath); stream.pipe(res); stream.on("error", (error) => { if (error.code === "ENOENT") { res.writeHead(404); res.end(); } else { res.writeHead(500); res.end(); } }); } _createRequestProcessor({ createStartEntry, createEndEntry, build, delete: deleteFn, finish, }) { return async function requestProcessor( req, res, bundleOptions, buildContext ) { const requestStartTimestamp = performance.timeOrigin + performance.now(); const { buildNumber } = buildContext; const { entryFile, graphOptions, resolverOptions, serializerOptions, transformOptions, } = splitBundleOptions(bundleOptions); let resolvedEntryFilePath; try { resolvedEntryFilePath = await this._resolveRelativePath(entryFile, { relativeTo: "server", resolverOptions, transformOptions, }); } catch (error) { const formattedError = formatBundlingError(error); const status = error instanceof UnableToResolveError ? 404 : 500; res.writeHead(status, { "Content-Type": "application/json; charset=UTF-8", }); res.end(JSON.stringify(formattedError)); return; } const graphId = getGraphId(resolvedEntryFilePath, transformOptions, { unstable_allowRequireContext: this._config.transformer.unstable_allowRequireContext, resolverOptions, shallow: graphOptions.shallow, lazy: graphOptions.lazy, }); if (deleteFn && req.method === "DELETE") { const deleteContext = { graphId, req, res, }; try { await deleteFn(deleteContext); } catch (error) { const formattedError = formatBundlingError(error); const status = error instanceof ResourceNotFoundError ? 404 : 500; res.writeHead(status, { "Content-Type": "application/json; charset=UTF-8", }); res.end(JSON.stringify(formattedError)); } return; } const mres = MultipartResponse.wrapIfSupported(req, res); let onProgress = null; let lastProgress = -1; if (this._config.reporter) { onProgress = (transformedFileCount, totalFileCount) => { const currentProgress = parseInt( (transformedFileCount / totalFileCount) * 100, 10 ); if (currentProgress > lastProgress || totalFileCount < 10) { if (mres instanceof MultipartResponse) { mres.writeChunk( { "Content-Type": "application/json", }, JSON.stringify({ done: transformedFileCount, total: totalFileCount, }) ); } if (res.socket != null && res.socket.uncork != null) { res.socket.uncork(); } lastProgress = currentProgress; } this._reporter.update({ buildID: getBuildID(buildNumber), type: "bundle_transform_progressed", transformedFileCount, totalFileCount, }); }; } this._reporter.update({ buildID: getBuildID(buildNumber), bundleDetails: { bundleType: bundleOptions.bundleType, customResolverOptions: bundleOptions.customResolverOptions, customTransformOptions: bundleOptions.customTransformOptions, dev: transformOptions.dev, entryFile: resolvedEntryFilePath, minify: transformOptions.minify, platform: transformOptions.platform, }, isPrefetch: req.method === "HEAD", type: "bundle_build_started", }); const startContext = { buildNumber, bundleOptions, entryFile: resolvedEntryFilePath, graphId, graphOptions, mres, onProgress, req, resolverOptions, serializerOptions, transformOptions, bundlePerfLogger: buildContext.bundlePerfLogger, requestStartTimestamp, }; 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({ buildID: getBuildID(buildNumber), type: "bundle_build_failed", bundleOptions, }); 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: getBuildID(buildNumber), stack: formattedError.message, }); debug("Bundling error", error); buildContext.bundlePerfLogger.end("FAIL"); return; } const endContext = { ...startContext, result, }; finish(endContext); this._reporter.update({ buildID: getBuildID(buildNumber), type: "bundle_build_done", }); log( createActionEndEntry({ ...logEntry, ...createEndEntry(endContext), }) ); }; } _processBundleRequest = this._createRequestProcessor({ createStartEntry(context) { return { action_name: "Requesting bundle", bundle_url: context.req.url, entry_point: context.entryFile, bundler: "delta", build_id: getBuildID(context.buildNumber), bundle_options: context.bundleOptions, bundle_hash: context.graphId, user_agent: context.req.headers["user-agent"] ?? "unknown", }; }, createEndEntry(context) { return { outdated_modules: context.result.numModifiedFiles, }; }, build: async ({ entryFile, graphId, graphOptions, onProgress, resolverOptions, serializerOptions, transformOptions, bundlePerfLogger, requestStartTimestamp, }) => { bundlePerfLogger.start({ timestamp: requestStartTimestamp, }); bundlePerfLogger.annotate({ string: { bundle_url: entryFile, }, }); const revPromise = this._bundler.getRevisionByGraphId(graphId); bundlePerfLogger.point("resolvingAndTransformingDependencies_start"); bundlePerfLogger.annotate({ bool: { initial_build: revPromise == null, }, }); const { delta, revision } = await (revPromise != null ? this._bundler.updateGraph(await revPromise, false) : this._bundler.initializeGraph( entryFile, transformOptions, resolverOptions, { onProgress, shallow: graphOptions.shallow, lazy: graphOptions.lazy, } )); bundlePerfLogger.annotate({ int: { graph_node_count: revision.graph.dependencies.size, }, }); bundlePerfLogger.point("resolvingAndTransformingDependencies_end"); bundlePerfLogger.point("serializingBundle_start"); const serializer = this._config.serializer.customSerializer || ((entryPoint, preModules, graph, options) => bundleToString(baseJSBundle(entryPoint, preModules, graph, options)) .code); const bundle = await serializer( entryFile, revision.prepend, revision.graph, { asyncRequireModulePath: await this._resolveRelativePath( this._config.transformer.asyncRequireModulePath, { relativeTo: "project", resolverOptions, transformOptions, } ), processModuleFilter: this._config.serializer.processModuleFilter, createModuleId: this._createModuleId, getRunModuleStatement: this._config.serializer.getRunModuleStatement, includeAsyncPaths: graphOptions.lazy, dev: transformOptions.dev, projectRoot: this._config.projectRoot, modulesOnly: serializerOptions.modulesOnly, runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule( path.relative(this._config.projectRoot, entryFile) ), runModule: serializerOptions.runModule, sourceMapUrl: serializerOptions.sourceMapUrl, sourceUrl: serializerOptions.sourceUrl, inlineSourceMap: serializerOptions.inlineSourceMap, serverRoot: this._config.server.unstable_serverRoot ?? this._config.projectRoot, shouldAddToIgnoreList: (module) => this._shouldAddModuleToIgnoreList(module), getSourceUrl: (module) => this._getModuleSourceUrl(module, serializerOptions.sourcePaths), } ); bundlePerfLogger.point("serializingBundle_end"); const bundleCode = typeof bundle === "string" ? bundle : bundle.code; 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: bundleCode, }; }, finish({ req, mres, serializerOptions, result, bundlePerfLogger }) { bundlePerfLogger.annotate({ int: { bundle_length: result.bundle.length, bundle_byte_length: Buffer.byteLength(result.bundle), }, }); mres.once("error", () => { bundlePerfLogger.end("FAIL"); }); mres.once("finish", () => { bundlePerfLogger.end("SUCCESS"); }); if ( req.headers["if-modified-since"] === result.lastModifiedDate.toUTCString() ) { bundlePerfLogger.annotate({ string: { http_status: "304", }, }); debug("Responding with 304"); mres.writeHead(304); mres.end(); } else { bundlePerfLogger.annotate({ string: { http_status: "200", }, }); mres.setHeader( FILES_CHANGED_COUNT_HEADER, String(result.numModifiedFiles) ); mres.setHeader(DELTA_ID_HEADER, String(result.nextRevId)); if (serializerOptions?.sourceUrl != null) { mres.setHeader("Content-Location", serializerOptions.sourceUrl); } mres.setHeader("Content-Type", "application/javascript; charset=UTF-8"); mres.setHeader("Last-Modified", result.lastModifiedDate.toUTCString()); mres.setHeader( "Content-Length", String(Buffer.byteLength(result.bundle)) ); mres.end(result.bundle); } }, delete: async ({ graphId, res }) => { await this._bundler.endGraph(graphId); res.statusCode = 204; res.end(); }, }); _getSortedModules(graph) { const modules = [...graph.dependencies.values()]; for (const module of modules) { this._createModuleId(module.path); } return modules.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, graphId, graphOptions, onProgress, resolverOptions, serializerOptions, transformOptions, }) => { let revision; const revPromise = this._bundler.getRevisionByGraphId(graphId); if (revPromise == null) { ({ revision } = await this._bundler.initializeGraph( entryFile, transformOptions, resolverOptions, { onProgress, shallow: graphOptions.shallow, lazy: graphOptions.lazy, } )); } else { ({ revision } = await this._bundler.updateGraph( await revPromise, false )); } let { prepend, graph } = revision; if (serializerOptions.modulesOnly) { prepend = []; } return await sourceMapStringNonBlocking( [...prepend, ...this._getSortedModules(graph)], { excludeSource: serializerOptions.excludeSource, processModuleFilter: this._config.serializer.processModuleFilter, shouldAddToIgnoreList: (module) => this._shouldAddModuleToIgnoreList(module), getSourceUrl: (module) => this._getModuleSourceUrl(module, serializerOptions.sourcePaths), } ); }, finish({ mres, result }) { mres.setHeader("Content-Type", "application/json"); mres.end(result.toString()); }, }); _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, onProgress, resolverOptions, transformOptions, }) => { const dependencies = await this._bundler.getDependencies( [entryFile], transformOptions, resolverOptions, { onProgress, shallow: false, lazy: false, } ); return await getAssets(dependencies, { 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)); }, }); async _symbolicate(req, res) { const getCodeFrame = (urls, symbolicatedStack) => { for (let i = 0; i < symbolicatedStack.length; i++) { const { collapse, column, file, lineNumber } = symbolicatedStack[i]; if ( collapse || lineNumber == null || (file != null && urls.has(file)) ) { continue; } const fileAbsolute = path.resolve(this._config.projectRoot, file ?? ""); try { return { content: codeFrameColumns( fs.readFileSync(fileAbsolute, "utf8"), { start: { column: column + 1, line: lineNumber, }, }, { forceColor: true, } ), location: { row: lineNumber, column, }, fileName: file, }; } catch (error) { console.error(error); } } return null; }; try { const symbolicatingLogEntry = log( createActionStartEntry("Symbolicating") ); debug("Start symbolication"); const body = await req.rawBody; const parsedBody = JSON.parse(body); const rewriteAndNormalizeStackFrame = (frame, lineNumber) => { invariant( frame != null && typeof frame === "object", "Bad stack frame at line %d, expected object, received: %s", lineNumber, typeof frame ); const frameFile = frame.file; if (typeof frameFile === "string" && frameFile.includes("://")) { return { ...frame, file: this._rewriteAndNormalizeUrl(frameFile), }; } return frame; }; const stack = parsedBody.stack.map(rewriteAndNormalizeStackFrame); const urls = new Set(); stack.forEach((frame) => { const sourceUrl = frame.file; if ( sourceUrl != null && !urls.has(sourceUrl) && !sourceUrl.endsWith("/debuggerWorker.js") && sourceUrl.startsWith("http") ) { urls.add(sourceUrl); } }); debug("Getting source maps for symbolication"); const sourceMaps = await Promise.all( Array.from(urls.values()).map((normalizedUrl) => this._explodedSourceMapForBundleOptions( this._parseOptions(normalizedUrl) ) ) ); debug("Performing fast symbolication"); const symbolicatedStack = await symbolicate( stack, zip(urls.values(), sourceMaps), this._config, parsedBody.extraData ?? {} ); debug("Symbolication done"); res.end( JSON.stringify({ codeFrame: getCodeFrame(urls, symbolicatedStack), stack: symbolicatedStack, }) ); process.nextTick(() => { log(createActionEndEntry(symbolicatingLogEntry)); }); } catch (error) { console.error(error.stack || error); res.statusCode = 500; res.end( JSON.stringify({ error: error.message, }) ); } } async _explodedSourceMapForBundleOptions(bundleOptions) { const { entryFile, graphOptions, onProgress, resolverOptions, serializerOptions, transformOptions, } = splitBundleOptions(bundleOptions); const resolvedEntryFilePath = await this._resolveRelativePath(entryFile, { relativeTo: "server", resolverOptions, transformOptions, }); const graphId = getGraphId(resolvedEntryFilePath, transformOptions, { unstable_allowRequireContext: this._config.transformer.unstable_allowRequireContext, resolverOptions, shallow: graphOptions.shallow, lazy: graphOptions.lazy, }); let revision; const revPromise = this._bundler.getRevisionByGraphId(graphId); if (revPromise == null) { ({ revision } = await this._bundler.initializeGraph( resolvedEntryFilePath, transformOptions, resolverOptions, { onProgress, shallow: graphOptions.shallow, lazy: graphOptions.lazy, } )); } else { ({ revision } = await this._bundler.updateGraph(await revPromise, false)); } let { prepend, graph } = revision; if (serializerOptions.modulesOnly) { prepend = []; } return getExplodedSourceMap( [...prepend, ...this._getSortedModules(graph)], { processModuleFilter: this._config.serializer.processModuleFilter, } ); } async _resolveRelativePath( filePath, { relativeTo, resolverOptions, transformOptions } ) { const resolutionFn = await transformHelpers.getResolveDependencyFn( this._bundler.getBundler(), transformOptions.platform, resolverOptions ); const rootDir = relativeTo === "server" ? this._getServerRootDir() : this._config.projectRoot; return resolutionFn(`${rootDir}/.`, { name: filePath, data: { key: filePath, locs: [], asyncType: null, }, }).filePath; } getNewBuildNumber() { return this._nextBundleBuildNumber++; } getPlatforms() { return this._config.resolver.platforms; } getWatchFolders() { return this._config.watchFolders; } static DEFAULT_GRAPH_OPTIONS = { customResolverOptions: Object.create(null), customTransformOptions: Object.create(null), dev: true, hot: false, minify: false, unstable_transformProfile: "default", }; static DEFAULT_BUNDLE_OPTIONS = { ...Server.DEFAULT_GRAPH_OPTIONS, excludeSource: false, inlineSourceMap: false, lazy: false, modulesOnly: false, onProgress: null, runModule: true, shallow: false, sourceMapUrl: null, sourceUrl: null, sourcePaths: _types.SourcePathsMode.Absolute, }; _getServerRootDir() { return this._config.server.unstable_serverRoot ?? this._config.projectRoot; } _getEntryPointAbsolutePath(entryFile) { return path.resolve(this._getServerRootDir(), entryFile); } async ready() { await this._bundler.ready(); } _shouldAddModuleToIgnoreList(module) { return ( module.path === "__prelude__" || module.path.includes("?ctx=") || this._config.serializer.isThirdPartyModule(module) ); } _getModuleSourceUrl(module, mode) { switch (mode) { case _types.SourcePathsMode.ServerUrl: for (const [pathnamePrefix, normalizedRootDir] of this ._sourceRequestRoutingMap) { if (module.path.startsWith(normalizedRootDir + path.sep)) { const relativePath = module.path.slice( normalizedRootDir.length + 1 ); const relativePathPosix = relativePath.split(path.sep).join("/"); return pathnamePrefix + encodeURI(relativePathPosix); } } const modulePathPosix = module.path.split(path.sep).join("/"); return modulePathPosix.startsWith("/") ? encodeURI(modulePathPosix) : "/" + encodeURI(modulePathPosix); case _types.SourcePathsMode.Absolute: return module.path; } } } function* zip(xs, ys) { const ysIter = ys[Symbol.iterator](); for (const x of xs) { const y = ysIter.next(); if (y.done) { return; } yield [x, y.value]; } } function getBuildID(buildNumber) { return buildNumber.toString(36); } module.exports = Server;