@expo/cli
Version:
961 lines • 70.4 kB
JavaScript
/**
* Copyright © 2022 650 Industries.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "MetroBundlerDevServer", {
enumerable: true,
get: function() {
return MetroBundlerDevServer;
}
});
function _config() {
const data = require("@expo/config");
_config = function() {
return data;
};
return data;
}
function _paths() {
const data = require("@expo/config/paths");
_paths = function() {
return data;
};
return data;
}
function _env() {
const data = /*#__PURE__*/ _interop_require_wildcard(require("@expo/env"));
_env = function() {
return data;
};
return data;
}
function _assert() {
const data = /*#__PURE__*/ _interop_require_default(require("assert"));
_assert = function() {
return data;
};
return data;
}
function _chalk() {
const data = /*#__PURE__*/ _interop_require_default(require("chalk"));
_chalk = function() {
return data;
};
return data;
}
function _baseJSBundle() {
const data = /*#__PURE__*/ _interop_require_default(require("metro/src/DeltaBundler/Serializers/baseJSBundle"));
_baseJSBundle = function() {
return data;
};
return data;
}
function _sourceMapGenerator() {
const data = require("metro/src/DeltaBundler/Serializers/sourceMapGenerator");
_sourceMapGenerator = function() {
return data;
};
return data;
}
function _bundleToString() {
const data = /*#__PURE__*/ _interop_require_default(require("metro/src/lib/bundleToString"));
_bundleToString = function() {
return data;
};
return data;
}
function _getGraphId() {
const data = /*#__PURE__*/ _interop_require_default(require("metro/src/lib/getGraphId"));
_getGraphId = function() {
return data;
};
return data;
}
function _path() {
const data = /*#__PURE__*/ _interop_require_default(require("path"));
_path = function() {
return data;
};
return data;
}
function _resolvefrom() {
const data = /*#__PURE__*/ _interop_require_default(require("resolve-from"));
_resolvefrom = function() {
return data;
};
return data;
}
const _createServerComponentsMiddleware = require("./createServerComponentsMiddleware");
const _createServerRouteMiddleware = require("./createServerRouteMiddleware");
const _fetchRouterManifest = require("./fetchRouterManifest");
const _instantiateMetro = require("./instantiateMetro");
const _metroErrorInterface = require("./metroErrorInterface");
const _metroPrivateServer = require("./metroPrivateServer");
const _metroWatchTypeScriptFiles = require("./metroWatchTypeScriptFiles");
const _router = require("./router");
const _serializeHtml = require("./serializeHtml");
const _waitForMetroToObserveTypeScriptFile = require("./waitForMetroToObserveTypeScriptFile");
const _log = require("../../../log");
const _env1 = require("../../../utils/env");
const _errors = require("../../../utils/errors");
const _filePath = require("../../../utils/filePath");
const _port = require("../../../utils/port");
const _BundlerDevServer = require("../BundlerDevServer");
const _getStaticRenderFunctions = require("../getStaticRenderFunctions");
const _ContextModuleSourceMapsMiddleware = require("../middleware/ContextModuleSourceMapsMiddleware");
const _CreateFileMiddleware = require("../middleware/CreateFileMiddleware");
const _DevToolsPluginMiddleware = require("../middleware/DevToolsPluginMiddleware");
const _DomComponentsMiddleware = require("../middleware/DomComponentsMiddleware");
const _FaviconMiddleware = require("../middleware/FaviconMiddleware");
const _HistoryFallbackMiddleware = require("../middleware/HistoryFallbackMiddleware");
const _InterstitialPageMiddleware = require("../middleware/InterstitialPageMiddleware");
const _ManifestMiddleware = require("../middleware/ManifestMiddleware");
const _RuntimeRedirectMiddleware = require("../middleware/RuntimeRedirectMiddleware");
const _ServeStaticMiddleware = require("../middleware/ServeStaticMiddleware");
const _metroOptions = require("../middleware/metroOptions");
const _mutations = require("../middleware/mutations");
const _startTypescriptTypeGeneration = require("../type-generation/startTypescriptTypeGeneration");
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function(nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
return {
default: obj
};
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {
__proto__: null
};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for(var key in obj){
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
const debug = require('debug')('expo:start:server:metro');
/** Default port to use for apps running in Expo Go. */ const EXPO_GO_METRO_PORT = 8081;
/** Default port to use for apps that run in standard React Native projects or Expo Dev Clients. */ const DEV_CLIENT_METRO_PORT = 8081;
class MetroBundlerDevServer extends _BundlerDevServer.BundlerDevServer {
get name() {
return 'metro';
}
async resolvePortAsync(options = {}) {
const port = // If the manually defined port is busy then an error should be thrown...
options.port ?? // Otherwise use the default port based on the runtime target.
(options.devClient ? Number(process.env.RCT_METRO_PORT) || DEV_CLIENT_METRO_PORT : await (0, _port.getFreePortAsync)(EXPO_GO_METRO_PORT));
return port;
}
async exportExpoRouterApiRoutesAsync({ includeSourceMaps, outputDir, prerenderManifest, platform }) {
const { routerRoot } = this.instanceMetroOptions;
(0, _assert().default)(routerRoot != null, 'The server must be started before calling exportExpoRouterApiRoutesAsync.');
const appDir = _path().default.join(this.projectRoot, routerRoot);
const manifest = await this.getExpoRouterRoutesManifestAsync({
appDir
});
const files = new Map();
// Inject RSC middleware.
const rscPath = '/_flight/[...rsc]';
if (this.isReactServerComponentsEnabled && // If the RSC route is not already in the manifest, add it.
!manifest.apiRoutes.find((route)=>route.page.startsWith('/_flight/'))) {
debug('Adding RSC route to the manifest:', rscPath);
// NOTE: This might need to be sorted to the correct spot in the future.
manifest.apiRoutes.push({
file: (0, _resolvefrom().default)(this.projectRoot, '@expo/cli/static/template/[...rsc]+api.ts'),
page: rscPath,
namedRegex: '^/_flight(?:/(?<rsc>.+?))?(?:/)?$',
routeKeys: {
rsc: 'rsc'
}
});
}
for (const route of manifest.apiRoutes){
const filepath = _path().default.isAbsolute(route.file) ? route.file : _path().default.join(appDir, route.file);
const contents = await this.bundleApiRoute(filepath, {
platform
});
const artifactFilename = route.page === rscPath ? (0, _metroOptions.convertPathToModuleSpecifier)(_path().default.join(outputDir, '.' + rscPath + '.js')) : (0, _metroOptions.convertPathToModuleSpecifier)(_path().default.join(outputDir, _path().default.relative(appDir, filepath.replace(/\.[tj]sx?$/, '.js'))));
if (contents) {
let src = contents.src;
if (includeSourceMaps && contents.map) {
// TODO(kitten): Merge the source map transformer in the future
// https://github.com/expo/expo/blob/0dffdb15/packages/%40expo/metro-config/src/serializer/serializeChunks.ts#L422-L439
// Alternatively, check whether `sourcesRoot` helps here
const artifactBasename = encodeURIComponent(_path().default.basename(artifactFilename) + '.map');
src = src.replace(/\/\/# sourceMappingURL=.*/g, `//# sourceMappingURL=${artifactBasename}`);
const parsedMap = typeof contents.map === 'string' ? JSON.parse(contents.map) : contents.map;
files.set(artifactFilename + '.map', {
contents: JSON.stringify({
version: parsedMap.version,
sources: parsedMap.sources.map((source)=>{
source = typeof source === 'string' && source.startsWith(this.projectRoot) ? _path().default.relative(this.projectRoot, source) : source;
return (0, _metroOptions.convertPathToModuleSpecifier)(source);
}),
sourcesContent: new Array(parsedMap.sources.length).fill(null),
names: parsedMap.names,
mappings: parsedMap.mappings
}),
apiRouteId: route.page,
targetDomain: 'server'
});
}
files.set(artifactFilename, {
contents: src,
apiRouteId: route.page,
targetDomain: 'server'
});
}
// Remap the manifest files to represent the output files.
route.file = artifactFilename;
}
return {
manifest: {
...manifest,
htmlRoutes: prerenderManifest.htmlRoutes
},
files
};
}
async getExpoRouterRoutesManifestAsync({ appDir }) {
var _exp_extra;
// getBuiltTimeServerManifest
const { exp } = (0, _config().getConfig)(this.projectRoot);
const manifest = await (0, _fetchRouterManifest.fetchManifest)(this.projectRoot, {
...(_exp_extra = exp.extra) == null ? void 0 : _exp_extra.router,
preserveRedirectAndRewrites: true,
asJson: true,
appDir
});
if (!manifest) {
throw new _errors.CommandError('EXPO_ROUTER_SERVER_MANIFEST', 'Unexpected error: server manifest could not be fetched.');
}
return manifest;
}
async getServerManifestAsync() {
var _exp_extra, _exp_extra1;
const { exp } = (0, _config().getConfig)(this.projectRoot);
// NOTE: This could probably be folded back into `renderStaticContent` when expo-asset and font support RSC.
const { getBuildTimeServerManifestAsync, getManifest } = await this.ssrLoadModule('expo-router/build/static/getServerManifest.js', {
// Only use react-server environment when the routes are using react-server rendering by default.
environment: this.isReactServerRoutesEnabled ? 'react-server' : 'node'
});
return {
serverManifest: await getBuildTimeServerManifestAsync({
...(_exp_extra = exp.extra) == null ? void 0 : _exp_extra.router
}),
htmlManifest: await getManifest({
...(_exp_extra1 = exp.extra) == null ? void 0 : _exp_extra1.router
})
};
}
async getStaticRenderFunctionAsync() {
var _exp_extra, _exp_extra1;
const url = this.getDevServerUrlOrAssert();
const { getStaticContent, getManifest, getBuildTimeServerManifestAsync } = await this.ssrLoadModule('expo-router/node/render.js', {
// This must always use the legacy rendering resolution (no `react-server`) because it leverages
// the previous React SSG utilities which aren't available in React 19.
environment: 'node'
});
const { exp } = (0, _config().getConfig)(this.projectRoot);
return {
serverManifest: await getBuildTimeServerManifestAsync({
...(_exp_extra = exp.extra) == null ? void 0 : _exp_extra.router
}),
// Get routes from Expo Router.
manifest: await getManifest({
preserveApiRoutes: false,
...(_exp_extra1 = exp.extra) == null ? void 0 : _exp_extra1.router
}),
// Get route generating function
async renderAsync (path) {
return await getStaticContent(new URL(path, url));
}
};
}
async getStaticResourcesAsync({ includeSourceMaps, mainModuleName, clientBoundaries = this.instanceMetroOptions.clientBoundaries ?? [], platform = 'web' } = {}) {
const { mode, minify, isExporting, baseUrl, reactCompiler, routerRoot, asyncRoutes } = this.instanceMetroOptions;
(0, _assert().default)(mode != null && isExporting != null && baseUrl != null && routerRoot != null && reactCompiler != null && asyncRoutes != null, 'The server must be started before calling getStaticResourcesAsync.');
const resolvedMainModuleName = mainModuleName ?? './' + (0, _ManifestMiddleware.resolveMainModuleName)(this.projectRoot, {
platform
});
return await this.metroImportAsArtifactsAsync(resolvedMainModuleName, {
splitChunks: isExporting && !_env1.env.EXPO_NO_BUNDLE_SPLITTING,
platform,
mode,
minify,
environment: 'client',
serializerIncludeMaps: includeSourceMaps,
mainModuleName: resolvedMainModuleName,
lazy: (0, _metroOptions.shouldEnableAsyncImports)(this.projectRoot),
asyncRoutes,
baseUrl,
isExporting,
routerRoot,
clientBoundaries,
reactCompiler,
bytecode: false
});
}
async getStaticPageAsync(pathname) {
const { mode, isExporting, clientBoundaries, baseUrl, reactCompiler, routerRoot, asyncRoutes } = this.instanceMetroOptions;
(0, _assert().default)(mode != null && isExporting != null && baseUrl != null && reactCompiler != null && routerRoot != null && asyncRoutes != null, 'The server must be started before calling getStaticPageAsync.');
const platform = 'web';
const devBundleUrlPathname = (0, _metroOptions.createBundleUrlPath)({
splitChunks: isExporting && !_env1.env.EXPO_NO_BUNDLE_SPLITTING,
platform,
mode,
environment: 'client',
reactCompiler,
mainModuleName: (0, _ManifestMiddleware.resolveMainModuleName)(this.projectRoot, {
platform
}),
lazy: (0, _metroOptions.shouldEnableAsyncImports)(this.projectRoot),
baseUrl,
isExporting,
asyncRoutes,
routerRoot,
clientBoundaries,
bytecode: false
});
const bundleStaticHtml = async ()=>{
const { getStaticContent } = await this.ssrLoadModule('expo-router/node/render.js', {
// This must always use the legacy rendering resolution (no `react-server`) because it leverages
// the previous React SSG utilities which aren't available in React 19.
environment: 'node',
minify: false,
isExporting,
platform
});
const location = new URL(pathname, this.getDevServerUrlOrAssert());
return await getStaticContent(location);
};
const [{ artifacts: resources }, staticHtml] = await Promise.all([
this.getStaticResourcesAsync({
clientBoundaries: []
}),
bundleStaticHtml()
]);
const content = (0, _serializeHtml.serializeHtmlWithAssets)({
isExporting,
resources,
template: staticHtml,
devBundleUrl: devBundleUrlPathname,
baseUrl,
hydrate: _env1.env.EXPO_WEB_DEV_HYDRATE
});
return {
content,
resources
};
}
async metroImportAsArtifactsAsync(filePath, specificOptions = {}) {
const results = await this.ssrLoadModuleContents(filePath, {
serializerOutput: 'static',
...specificOptions
});
// NOTE: This could potentially need more validation in the future.
if (results.artifacts && results.assets) {
return {
artifacts: results.artifacts,
assets: results.assets,
src: results.src,
filename: results.filename,
map: results.map
};
}
throw new _errors.CommandError('Invalid bundler results: ' + results);
}
async metroLoadModuleContents(filePath, specificOptions, extraOptions = {}) {
const { baseUrl } = this.instanceMetroOptions;
(0, _assert().default)(baseUrl != null, 'The server must be started before calling metroLoadModuleContents.');
const opts = {
// TODO: Possibly issues with using an absolute path here...
// mainModuleName: filePath,
lazy: false,
asyncRoutes: false,
inlineSourceMap: false,
engine: 'hermes',
minify: false,
// bytecode: false,
// Bundle in Node.js mode for SSR.
environment: 'node',
// platform: 'web',
// mode: 'development',
//
...this.instanceMetroOptions,
baseUrl,
// routerRoot,
// isExporting,
...specificOptions
};
const expoBundleOptions = (0, _metroOptions.getMetroDirectBundleOptions)(opts);
const resolverOptions = {
customResolverOptions: expoBundleOptions.customResolverOptions ?? {},
dev: expoBundleOptions.dev ?? true
};
const transformOptions = {
dev: expoBundleOptions.dev ?? true,
hot: true,
minify: expoBundleOptions.minify ?? false,
type: 'module',
unstable_transformProfile: extraOptions.unstable_transformProfile ?? expoBundleOptions.unstable_transformProfile ?? 'default',
customTransformOptions: expoBundleOptions.customTransformOptions ?? Object.create(null),
platform: expoBundleOptions.platform ?? 'web',
// @ts-expect-error: `runtimeBytecodeVersion` does not exist in `expoBundleOptions` or `TransformInputOptions`
runtimeBytecodeVersion: expoBundleOptions.runtimeBytecodeVersion
};
const resolvedEntryFilePath = await this.resolveRelativePathAsync(filePath, {
resolverOptions,
transformOptions
});
const filename = (0, _metroOptions.createBundleOsPath)({
...opts,
mainModuleName: resolvedEntryFilePath
});
// https://github.com/facebook/metro/blob/2405f2f6c37a1b641cc379b9c733b1eff0c1c2a1/packages/metro/src/lib/parseOptionsFromUrl.js#L55-L87
const results = await this._bundleDirectAsync(resolvedEntryFilePath, {
graphOptions: {
lazy: expoBundleOptions.lazy ?? false,
shallow: expoBundleOptions.shallow ?? false
},
resolverOptions,
serializerOptions: {
...expoBundleOptions.serializerOptions,
inlineSourceMap: expoBundleOptions.inlineSourceMap ?? false,
modulesOnly: expoBundleOptions.modulesOnly ?? false,
runModule: expoBundleOptions.runModule ?? true,
// @ts-expect-error
sourceUrl: expoBundleOptions.sourceUrl,
// @ts-expect-error
sourceMapUrl: extraOptions.sourceMapUrl ?? expoBundleOptions.sourceMapUrl
},
transformOptions
});
return {
...results,
filename
};
}
async ssrLoadModuleContents(filePath, specificOptions = {}) {
const { baseUrl, routerRoot, isExporting } = this.instanceMetroOptions;
(0, _assert().default)(baseUrl != null && routerRoot != null && isExporting != null, 'The server must be started before calling ssrLoadModuleContents.');
const opts = {
// TODO: Possibly issues with using an absolute path here...
mainModuleName: (0, _metroOptions.convertPathToModuleSpecifier)(filePath),
lazy: false,
asyncRoutes: false,
inlineSourceMap: false,
engine: 'hermes',
minify: false,
bytecode: false,
// Bundle in Node.js mode for SSR unless RSC is enabled.
environment: this.isReactServerComponentsEnabled ? 'react-server' : 'node',
platform: 'web',
mode: 'development',
//
...this.instanceMetroOptions,
// Mostly disable compiler in SSR bundles.
reactCompiler: false,
baseUrl,
routerRoot,
isExporting,
...specificOptions
};
// https://github.com/facebook/metro/blob/2405f2f6c37a1b641cc379b9c733b1eff0c1c2a1/packages/metro/src/lib/parseOptionsFromUrl.js#L55-L87
const { filename, bundle, map, ...rest } = await this.metroLoadModuleContents(filePath, opts);
const scriptContents = wrapBundle(bundle);
if (map) {
debug('Registering SSR source map for:', filename);
_getStaticRenderFunctions.cachedSourceMaps.set(filename, {
url: this.projectRoot,
map
});
} else {
debug('No SSR source map found for:', filename);
}
return {
...rest,
src: scriptContents,
filename,
map
};
}
async nativeExportBundleAsync(exp, options, files, extraOptions = {}) {
if (this.isReactServerComponentsEnabled) {
return this.singlePageReactServerComponentExportAsync(exp, options, files, extraOptions);
}
return this.legacySinglePageExportBundleAsync(options, extraOptions);
}
async singlePageReactServerComponentExportAsync(exp, options, files, extraOptions = {}) {
var _exp_extra;
const getReactServerReferences = (artifacts)=>{
// Get the React server action boundaries from the client bundle.
return unique(artifacts.filter((a)=>a.type === 'js').map((artifact)=>{
var _artifact_metadata_reactServerReferences;
return (_artifact_metadata_reactServerReferences = artifact.metadata.reactServerReferences) == null ? void 0 : _artifact_metadata_reactServerReferences.map((ref)=>(0, _createServerComponentsMiddleware.fileURLToFilePath)(ref));
})// TODO: Segment by module for splitting.
.flat().filter(Boolean));
};
// NOTE(EvanBacon): This will not support any code elimination since it's a static pass.
let { reactClientReferences: clientBoundaries, reactServerReferences: serverActionReferencesInServer, cssModules } = await this.rscRenderer.getExpoRouterClientReferencesAsync({
platform: options.platform,
domRoot: options.domRoot
}, files);
// TODO: The output keys should be in production format or use a lookup manifest.
const processClientBoundaries = async (reactServerReferences)=>{
debug('Evaluated client boundaries:', clientBoundaries);
// Run metro bundler and create the JS bundles/source maps.
const bundle = await this.legacySinglePageExportBundleAsync({
...options,
clientBoundaries
}, extraOptions);
// Get the React server action boundaries from the client bundle.
const newReactServerReferences = getReactServerReferences(bundle.artifacts);
if (!newReactServerReferences) {
// Possible issue with babel plugin / metro-config.
throw new Error('Static server action references were not returned from the Metro client bundle');
}
debug('React server action boundaries from client:', newReactServerReferences);
const allKnownReactServerReferences = unique([
...reactServerReferences,
...newReactServerReferences
]);
// When we export the server actions that were imported from the client, we may need to re-bundle the client with the new client boundaries.
const { clientBoundaries: nestedClientBoundaries } = await this.rscRenderer.exportServerActionsAsync({
platform: options.platform,
domRoot: options.domRoot,
entryPoints: allKnownReactServerReferences
}, files);
// TODO: Check against all modules in the initial client bundles.
const hasUniqueClientBoundaries = nestedClientBoundaries.some((boundary)=>!clientBoundaries.includes(boundary));
if (!hasUniqueClientBoundaries) {
return bundle;
}
debug('Re-bundling client with nested client boundaries:', nestedClientBoundaries);
clientBoundaries = unique(clientBoundaries.concat(nestedClientBoundaries));
// Re-bundle the client with the new client boundaries that only exist in server actions that were imported from the client.
// Run metro bundler and create the JS bundles/source maps.
return processClientBoundaries(allKnownReactServerReferences);
};
const bundle = await processClientBoundaries(serverActionReferencesInServer);
// Inject the global CSS that was imported during the server render.
bundle.artifacts.push(...cssModules);
const serverRoot = (0, _paths().getMetroServerRoot)(this.projectRoot);
// HACK: Maybe this should be done in the serializer.
const clientBoundariesAsOpaqueIds = clientBoundaries.map((boundary)=>// NOTE(cedric): relative module specifiers / IDs should always be POSIX formatted
(0, _filePath.toPosixPath)(_path().default.relative(serverRoot, boundary)));
const moduleIdToSplitBundle = bundle.artifacts.map((artifact)=>{
var _artifact_metadata;
return (artifact == null ? void 0 : (_artifact_metadata = artifact.metadata) == null ? void 0 : _artifact_metadata.paths) && Object.values(artifact.metadata.paths);
}).filter(Boolean).flat().reduce((acc, paths)=>({
...acc,
...paths
}), {});
debug('SSR Manifest:', moduleIdToSplitBundle, clientBoundariesAsOpaqueIds);
const ssrManifest = new Map();
if (Object.keys(moduleIdToSplitBundle).length) {
clientBoundariesAsOpaqueIds.forEach((boundary)=>{
if (boundary in moduleIdToSplitBundle) {
ssrManifest.set(boundary, moduleIdToSplitBundle[boundary]);
} else {
throw new Error(`Could not find boundary "${boundary}" in the SSR manifest. Available: ${Object.keys(moduleIdToSplitBundle).join(', ')}`);
}
});
} else {
// Native apps with bundle splitting disabled.
debug('No split bundles');
clientBoundariesAsOpaqueIds.forEach((boundary)=>{
// @ts-expect-error
ssrManifest.set(boundary, null);
});
}
const routerOptions = (_exp_extra = exp.extra) == null ? void 0 : _exp_extra.router;
// Export the static RSC files
await this.rscRenderer.exportRoutesAsync({
platform: options.platform,
ssrManifest,
routerOptions
}, files);
// Save the SSR manifest so we can perform more replacements in the server renderer and with server actions.
files.set(`_expo/rsc/${options.platform}/ssr-manifest.js`, {
targetDomain: 'server',
contents: 'module.exports = ' + JSON.stringify(// TODO: Add a less leaky version of this across the framework with just [key, value] (module ID, chunk).
Object.fromEntries(Array.from(ssrManifest.entries()).map(([key, value])=>[
// Must match babel plugin.
'./' + (0, _filePath.toPosixPath)(_path().default.relative(this.projectRoot, _path().default.join(serverRoot, key))),
[
key,
value
]
])))
});
return {
...bundle,
files
};
}
async legacySinglePageExportBundleAsync(options, extraOptions = {}) {
const { baseUrl, routerRoot, isExporting } = this.instanceMetroOptions;
(0, _assert().default)(options.mainModuleName != null, 'mainModuleName must be provided in options.');
(0, _assert().default)(baseUrl != null && routerRoot != null && isExporting != null, 'The server must be started before calling legacySinglePageExportBundleAsync.');
const opts = {
...this.instanceMetroOptions,
baseUrl,
routerRoot,
isExporting,
...options,
environment: 'client',
serializerOutput: 'static'
};
// https://github.com/facebook/metro/blob/2405f2f6c37a1b641cc379b9c733b1eff0c1c2a1/packages/metro/src/lib/parseOptionsFromUrl.js#L55-L87
if (!opts.mainModuleName.startsWith('/') && !_path().default.isAbsolute(opts.mainModuleName)) {
opts.mainModuleName = './' + opts.mainModuleName;
}
const output = await this.metroLoadModuleContents(opts.mainModuleName, opts, extraOptions);
return {
artifacts: output.artifacts,
assets: output.assets
};
}
async watchEnvironmentVariables() {
if (!this.instance) {
throw new Error('Cannot observe environment variable changes without a running Metro instance.');
}
if (!this.metro) {
// This can happen when the run command is used and the server is already running in another
// process.
debug('Skipping Environment Variable observation because Metro is not running (headless).');
return;
}
const envFiles = _env().getFiles(process.env.NODE_ENV).map((fileName)=>_path().default.join(this.projectRoot, fileName));
(0, _waitForMetroToObserveTypeScriptFile.observeFileChanges)({
metro: this.metro,
server: this.instance.server
}, envFiles, ()=>{
debug('Reloading environment variables...');
// Force reload the environment variables.
_env().load(this.projectRoot, {
force: true
});
});
}
async startImplementationAsync(options) {
var _exp_experiments, _exp_experiments1, _exp_experiments2, _exp_experiments3, _exp_experiments4, _exp_web, _exp_web1, _exp_experiments5, _exp_extra, _exp_web2, _exp_extra_router, _exp_extra1;
options.port = await this.resolvePortAsync(options);
this.urlCreator = this.getUrlCreator(options);
const config = (0, _config().getConfig)(this.projectRoot, {
skipSDKVersionRequirement: true
});
const { exp } = config;
// NOTE: This will change in the future when it's less experimental, we enable React 19, and turn on more RSC flags by default.
const isReactServerComponentsEnabled = !!((_exp_experiments = exp.experiments) == null ? void 0 : _exp_experiments.reactServerComponentRoutes) || !!((_exp_experiments1 = exp.experiments) == null ? void 0 : _exp_experiments1.reactServerFunctions);
const isReactServerActionsOnlyEnabled = !((_exp_experiments2 = exp.experiments) == null ? void 0 : _exp_experiments2.reactServerComponentRoutes) && !!((_exp_experiments3 = exp.experiments) == null ? void 0 : _exp_experiments3.reactServerFunctions);
this.isReactServerComponentsEnabled = isReactServerComponentsEnabled;
this.isReactServerRoutesEnabled = !!((_exp_experiments4 = exp.experiments) == null ? void 0 : _exp_experiments4.reactServerComponentRoutes);
const useServerRendering = [
'static',
'server'
].includes(((_exp_web = exp.web) == null ? void 0 : _exp_web.output) ?? '');
const hasApiRoutes = isReactServerComponentsEnabled || ((_exp_web1 = exp.web) == null ? void 0 : _exp_web1.output) === 'server';
const baseUrl = (0, _metroOptions.getBaseUrlFromExpoConfig)(exp);
const asyncRoutes = (0, _metroOptions.getAsyncRoutesFromExpoConfig)(exp, options.mode ?? 'development', 'web');
const routerRoot = (0, _router.getRouterDirectoryModuleIdWithManifest)(this.projectRoot, exp);
const reactCompiler = !!((_exp_experiments5 = exp.experiments) == null ? void 0 : _exp_experiments5.reactCompiler);
const appDir = _path().default.join(this.projectRoot, routerRoot);
const mode = options.mode ?? 'development';
const routerOptions = (_exp_extra = exp.extra) == null ? void 0 : _exp_extra.router;
if (isReactServerComponentsEnabled && ((_exp_web2 = exp.web) == null ? void 0 : _exp_web2.output) === 'static') {
throw new _errors.CommandError(`Experimental server component support does not support 'web.output: ${exp.web.output}' yet. Use 'web.output: "server"' during the experimental phase.`);
}
// Error early about the window.location polyfill when React Server Components are enabled.
if (isReactServerComponentsEnabled && (exp == null ? void 0 : (_exp_extra1 = exp.extra) == null ? void 0 : (_exp_extra_router = _exp_extra1.router) == null ? void 0 : _exp_extra_router.origin) === false) {
const configPath = config.dynamicConfigPath ?? config.staticConfigPath ?? '/app.json';
const configFileName = _path().default.basename(configPath);
throw new _errors.CommandError(`The Expo Router "origin" property in the Expo config (${configFileName}) cannot be "false" when React Server Components is enabled. Remove it from the ${configFileName} file and try again.`);
}
const instanceMetroOptions = {
isExporting: !!options.isExporting,
baseUrl,
mode,
routerRoot,
reactCompiler,
minify: options.minify,
asyncRoutes
};
this.instanceMetroOptions = instanceMetroOptions;
const parsedOptions = {
port: options.port,
maxWorkers: options.maxWorkers,
resetCache: options.resetDevServer
};
// Required for symbolication:
process.env.EXPO_DEV_SERVER_ORIGIN = `http://localhost:${options.port}`;
const { metro, hmrServer, server, middleware, messageSocket } = await (0, _instantiateMetro.instantiateMetroAsync)(this, parsedOptions, {
isExporting: !!options.isExporting,
exp
});
if (!options.isExporting) {
const manifestMiddleware = await this.getManifestMiddlewareAsync(options);
// Important that we noop source maps for context modules as soon as possible.
(0, _mutations.prependMiddleware)(middleware, new _ContextModuleSourceMapsMiddleware.ContextModuleSourceMapsMiddleware().getHandler());
// We need the manifest handler to be the first middleware to run so our
// routes take precedence over static files. For example, the manifest is
// served from '/' and if the user has an index.html file in their project
// then the manifest handler will never run, the static middleware will run
// and serve index.html instead of the manifest.
// https://github.com/expo/expo/issues/13114
(0, _mutations.prependMiddleware)(middleware, manifestMiddleware.getHandler());
middleware.use(new _InterstitialPageMiddleware.InterstitialPageMiddleware(this.projectRoot, {
// TODO: Prevent this from becoming stale.
scheme: options.location.scheme ?? null
}).getHandler());
middleware.use(new _DevToolsPluginMiddleware.DevToolsPluginMiddleware(this.projectRoot, this.devToolsPluginManager).getHandler());
const deepLinkMiddleware = new _RuntimeRedirectMiddleware.RuntimeRedirectMiddleware(this.projectRoot, {
getLocation: ({ runtime })=>{
if (runtime === 'custom') {
var _this_urlCreator;
return (_this_urlCreator = this.urlCreator) == null ? void 0 : _this_urlCreator.constructDevClientUrl();
} else {
var _this_urlCreator1;
return (_this_urlCreator1 = this.urlCreator) == null ? void 0 : _this_urlCreator1.constructUrl({
scheme: 'exp'
});
}
}
});
middleware.use(deepLinkMiddleware.getHandler());
const serverRoot = (0, _paths().getMetroServerRoot)(this.projectRoot);
const domComponentRenderer = (0, _DomComponentsMiddleware.createDomComponentsMiddleware)({
metroRoot: serverRoot,
projectRoot: this.projectRoot
}, instanceMetroOptions);
// Add support for DOM components.
// TODO: Maybe put behind a flag for now?
middleware.use(domComponentRenderer);
middleware.use(new _CreateFileMiddleware.CreateFileMiddleware(this.projectRoot).getHandler());
// Append support for redirecting unhandled requests to the index.html page on web.
if (this.isTargetingWeb()) {
// This MUST be after the manifest middleware so it doesn't have a chance to serve the template `public/index.html`.
middleware.use(new _ServeStaticMiddleware.ServeStaticMiddleware(this.projectRoot).getHandler());
// This should come after the static middleware so it doesn't serve the favicon from `public/favicon.ico`.
middleware.use(new _FaviconMiddleware.FaviconMiddleware(this.projectRoot).getHandler());
}
if (useServerRendering || isReactServerComponentsEnabled) {
(0, _waitForMetroToObserveTypeScriptFile.observeAnyFileChanges)({
metro,
server
}, (events)=>{
if (hasApiRoutes) {
// NOTE(EvanBacon): We aren't sure what files the API routes are using so we'll just invalidate
// aggressively to ensure we always have the latest. The only caching we really get here is for
// cases where the user is making subsequent requests to the same API route without changing anything.
// This is useful for testing but pretty suboptimal. Luckily our caching is pretty aggressive so it makes
// up for a lot of the overhead.
this.invalidateApiRouteCache();
} else if (!(0, _router.hasWarnedAboutApiRoutes)()) {
for (const event of events){
var // If the user did not delete a file that matches the Expo Router API Route convention, then we should warn that
// API Routes are not enabled in the project.
_event_metadata;
if (((_event_metadata = event.metadata) == null ? void 0 : _event_metadata.type) !== 'd' && // Ensure the file is in the project's routes directory to prevent false positives in monorepos.
event.filePath.startsWith(appDir) && (0, _router.isApiRouteConvention)(event.filePath)) {
(0, _router.warnInvalidWebOutput)();
}
}
}
});
}
// If React 19 is enabled, then add RSC middleware to the dev server.
if (isReactServerComponentsEnabled) {
this.bindRSCDevModuleInjectionHandler();
const rscMiddleware = (0, _createServerComponentsMiddleware.createServerComponentsMiddleware)(this.projectRoot, {
instanceMetroOptions: this.instanceMetroOptions,
rscPath: '/_flight',
ssrLoadModule: this.ssrLoadModule.bind(this),
ssrLoadModuleArtifacts: this.metroImportAsArtifactsAsync.bind(this),
useClientRouter: isReactServerActionsOnlyEnabled,
createModuleId: metro._createModuleId.bind(metro),
routerOptions
});
this.rscRenderer = rscMiddleware;
middleware.use(rscMiddleware.middleware);
this.onReloadRscEvent = rscMiddleware.onReloadRscEvent;
}
// Append support for redirecting unhandled requests to the index.html page on web.
if (this.isTargetingWeb()) {
if (!useServerRendering) {
// This MUST run last since it's the fallback.
middleware.use(new _HistoryFallbackMiddleware.HistoryFallbackMiddleware(manifestMiddleware.getHandler().internal).getHandler());
} else {
var _config_exp_extra;
middleware.use((0, _createServerRouteMiddleware.createRouteHandlerMiddleware)(this.projectRoot, {
appDir,
routerRoot,
config,
...(_config_exp_extra = config.exp.extra) == null ? void 0 : _config_exp_extra.router,
bundleApiRoute: (functionFilePath)=>this.ssrImportApiRoute(functionFilePath, {
platform: 'web'
}),
getStaticPageAsync: async (pathname)=>{
// TODO: Add server rendering when RSC is enabled.
if (isReactServerComponentsEnabled) {
// NOTE: This is a temporary hack to return the SPA/template index.html in development when RSC is enabled.
// While this technically works, it doesn't provide the correct experience of server rendering the React code to HTML first.
const html = await manifestMiddleware.getSingleHtmlTemplateAsync();
return {
content: html
};
}
// Non-RSC apps will bundle the static HTML for a given pathname and respond with it.
return this.getStaticPageAsync(pathname);
}
}));
}
}
} else {
// If React 19 is enabled, then add RSC middleware to the dev server.
if (isReactServerComponentsEnabled) {
this.bindRSCDevModuleInjectionHandler();
const rscMiddleware = (0, _createServerComponentsMiddleware.createServerComponentsMiddleware)(this.projectRoot, {
instanceMetroOptions: this.instanceMetroOptions,
rscPath: '/_flight',
ssrLoadModule: this.ssrLoadModule.bind(this),
ssrLoadModuleArtifacts: this.metroImportAsArtifactsAsync.bind(this),
useClientRouter: isReactServerActionsOnlyEnabled,
createModuleId: metro._createModuleId.bind(metro),
routerOptions
});
this.rscRenderer = rscMiddleware;
}
}
// Extend the close method to ensure that we clean up the local info.
const originalClose = server.close.bind(server);
server.close = (callback)=>{
return originalClose((err)=>{
this.instance = null;
this.metro = null;
this.hmrServer = null;
this.ssrHmrClients = new Map();
callback == null ? void 0 : callback(err);
});
};
(0, _metroPrivateServer.assertMetroPrivateServer)(metro);
this.metro = metro;
this.hmrServer = hmrServer;
return {
server,
location: {
// The port is the main thing we want to send back.
port: options.port,
// localhost isn't always correct.
host: 'localhost',
// http is the only supported protocol on native.
url: `http://localhost:${options.port}`,
protocol: 'http'
},
middleware,
messageSocket
};
}
async registerSsrHmrAsync(url, onReload) {
if (!this.hmrServer || this.ssrHmrClients.has(url)) {
return;
}
debug('[SSR] Register HMR:', url);
const sendFn = (message)=>{
const data = JSON.parse(String(message));
switch(data.type){
case 'bundle-registered':
case 'update-done':
case 'update-start':
break;
case 'update':
{
const update = data.body;
const { isInitialUpdate, added, modified, deleted } = update;
const hasUpdate = added.length || modified.length || deleted.length;
// NOTE: We throw away the updates and instead simply send a trigger to the client to re-fetch the server route.
if (!isInitialUpdate && hasUpdate) {
// Clear all SSR modules before sending the reload event. This ensures that the next event will rebuild the in-memory state from scratch.
// @ts-expect-error
if (typeof globalThis.__c === 'function') globalThis.__c();
const allModuleIds = new Set([
...added,
...modified
].map((m)=>m.module[0]).concat(deleted));
const platforms = unique(Array.from(allModuleIds).map((moduleId)=>{
var _moduleId_match;
if (typeof moduleId !== 'string') {
return null;
}
// Extract platforms from the module IDs.
return ((_moduleId_match = moduleId.match(/[?&]platform=([\w]+)/)) == null ? void 0 : _moduleId_match[1]) ?? null;
}).filter(Boolean));
onReload(platforms);
}
}
break;
case 'error':
var _data_body;
// GraphNotFound can mean that we have an issue in metroOptions where the URL doesn't match the object props.
_log.Log.error('[SSR] HMR Error: ' + JSON.stringify(data, null, 2));
if (((_data_body = data.body) == null ? void 0 : _data_body.type) === 'GraphNotFoundError') {
var // @ts-expect-error
_this_metro;
_log.Log.error('Available SSR HMR keys:', ((_this_metro = this.metro) == null ? void 0 : _this_metro._bundler._revisionsByGraphId).keys());
}
break;
default:
debug('Unknown HMR message:', data);
break;
}
};
const client = await this.hmrServer.onClientConnect(url, sendFn);
this.ssrHmrClients.set(url, client);
// Opt in...
client.optedIntoHMR = true;
await this.hmrServer._registerEntryPoint(client, url, sendFn);
}
async waitForTypeScriptAsync() {
if (!this.instance) {
throw new Error('Cannot wait for TypeScript without a running server.');
}
return new Promise((resolve)=>{
if (!this.metro) {
// This can happen when the run command is used and the server is already running in another
// process. In this case we can't wait for the TypeScript check to complete because we don't
// have access to the Metro server.
debug('Skipping TypeScr