@expo/cli
Version:
512 lines (511 loc) • 23.2 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
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
createServerComponentsMiddleware: function() {
return createServerComponentsMiddleware;
},
fileURLToFilePath: function() {
return fileURLToFilePath;
}
});
function _paths() {
const data = require("@expo/config/paths");
_paths = function() {
return data;
};
return data;
}
function _rsc() {
const data = require("@expo/server/build/middleware/rsc");
_rsc = function() {
return data;
};
return data;
}
function _assert() {
const data = /*#__PURE__*/ _interop_require_default(require("assert"));
_assert = function() {
return data;
};
return data;
}
function _path() {
const data = /*#__PURE__*/ _interop_require_default(require("path"));
_path = function() {
return data;
};
return data;
}
function _url() {
const data = /*#__PURE__*/ _interop_require_default(require("url"));
_url = function() {
return data;
};
return data;
}
const _metroErrorInterface = require("./metroErrorInterface");
const _xcodeCompilerLogger = require("../../../export/embed/xcodeCompilerLogger");
const _ansi = require("../../../utils/ansi");
const _filePath = require("../../../utils/filePath");
const _fn = require("../../../utils/fn");
const _ip = require("../../../utils/ip");
const _stream = require("../../../utils/stream");
const _createBuiltinAPIRequestHandler = require("../middleware/createBuiltinAPIRequestHandler");
const _metroOptions = require("../middleware/metroOptions");
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
const debug = require('debug')('expo:rsc');
const getMetroServerRootMemo = (0, _fn.memoize)(_paths().getMetroServerRoot);
function createServerComponentsMiddleware(projectRoot, { rscPath, instanceMetroOptions, ssrLoadModule, ssrLoadModuleArtifacts, useClientRouter, createModuleId, routerOptions }) {
const routerModule = useClientRouter ? 'expo-router/build/rsc/router/noopRouter' : 'expo-router/build/rsc/router/expo-definedRouter';
const rscMiddleware = (0, _rsc().getRscMiddleware)({
config: {},
// Disabled in development
baseUrl: '',
rscPath,
onError: console.error,
renderRsc: async (args)=>{
// In development we should add simulated versions of common production headers.
if (args.headers['x-real-ip'] == null) {
args.headers['x-real-ip'] = (0, _ip.getIpAddress)();
}
if (args.headers['x-forwarded-for'] == null) {
args.headers['x-forwarded-for'] = args.headers['x-real-ip'];
}
if (args.headers['x-forwarded-proto'] == null) {
args.headers['x-forwarded-proto'] = 'http';
}
// Dev server-only implementation.
try {
return await renderRscToReadableStream({
...args,
headers: new Headers(args.headers),
body: args.body,
routerOptions
});
} catch (error) {
// If you get a codeFrame error during SSR like when using a Class component in React Server Components, then this
// will throw with:
// {
// rawObject: {
// type: 'TransformError',
// lineNumber: 0,
// errors: [ [Object] ],
// name: 'SyntaxError',
// message: '...',
// }
// }
// TODO: Revisit all error handling now that we do direct metro bundling...
await (0, _metroErrorInterface.logMetroError)(projectRoot, {
error
});
if (error[_metroErrorInterface.IS_METRO_BUNDLE_ERROR_SYMBOL]) {
throw new Response(JSON.stringify(error), {
status: (0, _xcodeCompilerLogger.isPossiblyUnableToResolveError)(error) ? 404 : 500,
headers: {
'Content-Type': 'application/json'
}
});
}
const sanitizedServerMessage = (0, _ansi.stripAnsi)(error.message) ?? error.message;
throw new Response(sanitizedServerMessage, {
status: 500,
headers: {
'Content-Type': 'text/plain'
}
});
}
}
});
let rscPathPrefix = rscPath;
if (rscPathPrefix !== '/') {
rscPathPrefix += '/';
}
async function exportServerActionsAsync({ platform, entryPoints, domRoot }, files) {
const uniqueEntryPoints = [
...new Set(entryPoints)
];
// TODO: Support multiple entry points in a single split server bundle...
const manifest = {};
const nestedClientBoundaries = [];
const nestedServerBoundaries = [];
const processedEntryPoints = new Set();
async function processEntryPoint(entryPoint) {
var _contents_artifacts_filter__metadata_reactClientReferences, _contents_artifacts_filter__metadata_reactServerReferences;
processedEntryPoints.add(entryPoint);
const contents = await ssrLoadModuleArtifacts(entryPoint, {
environment: 'react-server',
platform,
// Ignore the metro runtime to avoid overwriting the original in the API route.
modulesOnly: true,
// Required
runModule: true,
// Required to ensure assets load as client boundaries.
domRoot
});
const reactClientReferences = (_contents_artifacts_filter__metadata_reactClientReferences = contents.artifacts.filter((a)=>a.type === 'js')[0].metadata.reactClientReferences) == null ? void 0 : _contents_artifacts_filter__metadata_reactClientReferences.map((ref)=>fileURLToFilePath(ref));
if (reactClientReferences) {
nestedClientBoundaries.push(...reactClientReferences);
}
const reactServerReferences = (_contents_artifacts_filter__metadata_reactServerReferences = contents.artifacts.filter((a)=>a.type === 'js')[0].metadata.reactServerReferences) == null ? void 0 : _contents_artifacts_filter__metadata_reactServerReferences.map((ref)=>fileURLToFilePath(ref));
if (reactServerReferences) {
nestedServerBoundaries.push(...reactServerReferences);
}
// Naive check to ensure the module runtime is not included in the server action bundle.
if (contents.src.includes('The experimental Metro feature')) {
throw new Error('Internal error: module runtime should not be included in server action bundles: ' + entryPoint);
}
const relativeName = createModuleId(entryPoint, {
platform,
environment: 'react-server'
});
const safeName = _path().default.basename(contents.artifacts.find((a)=>a.type === 'js').filename);
const outputName = `_expo/rsc/${platform}/${safeName}`;
// While we're here, export the router for the server to dynamically render RSC.
files.set(outputName, {
targetDomain: 'server',
contents: wrapBundle(contents.src)
});
// Match babel plugin.
const publicModuleId = './' + (0, _filePath.toPosixPath)(_path().default.relative(projectRoot, entryPoint));
// Import relative to `dist/server/_expo/rsc/web/router.js`
manifest[publicModuleId] = [
String(relativeName),
outputName
];
}
async function processEntryPoints(entryPoints, recursions = 0) {
// Arbitrary recursion limit to prevent infinite loops.
if (recursions > 10) {
throw new Error('Recursion limit exceeded while processing server boundaries');
}
for (const entryPoint of entryPoints){
await processEntryPoint(entryPoint);
}
// When a server action has other server actions inside of it, we need to process those as well to ensure all entry points are in the manifest and accounted for.
let uniqueNestedServerBoundaries = [
...new Set(nestedServerBoundaries)
];
// Filter out values that have already been processed.
uniqueNestedServerBoundaries = uniqueNestedServerBoundaries.filter((value)=>!processedEntryPoints.has(value));
if (uniqueNestedServerBoundaries.length) {
debug('bundling nested server action boundaries', uniqueNestedServerBoundaries);
return processEntryPoints(uniqueNestedServerBoundaries, recursions + 1);
}
}
await processEntryPoints(uniqueEntryPoints);
// Save the SSR manifest so we can perform more replacements in the server renderer and with server actions.
files.set(`_expo/rsc/${platform}/action-manifest.js`, {
targetDomain: 'server',
contents: 'module.exports = ' + JSON.stringify(manifest)
});
return {
manifest,
clientBoundaries: nestedClientBoundaries
};
}
async function getExpoRouterClientReferencesAsync({ platform, domRoot }, files) {
var _contents_artifacts_filter__metadata_reactServerReferences, _contents_artifacts_filter__metadata_reactClientReferences;
const contents = await ssrLoadModuleArtifacts(routerModule, {
environment: 'react-server',
platform,
modulesOnly: true,
domRoot
});
// Extract the global CSS modules that are imported from the router.
// These will be injected in the head of the HTML document for the website.
const cssModules = contents.artifacts.filter((a)=>a.type.startsWith('css'));
const reactServerReferences = (_contents_artifacts_filter__metadata_reactServerReferences = contents.artifacts.filter((a)=>a.type === 'js')[0].metadata.reactServerReferences) == null ? void 0 : _contents_artifacts_filter__metadata_reactServerReferences.map((ref)=>fileURLToFilePath(ref));
if (!reactServerReferences) {
throw new Error('Static server action references were not returned from the Metro SSR bundle for definedRouter');
}
debug('React client boundaries:', reactServerReferences);
const reactClientReferences = (_contents_artifacts_filter__metadata_reactClientReferences = contents.artifacts.filter((a)=>a.type === 'js')[0].metadata.reactClientReferences) == null ? void 0 : _contents_artifacts_filter__metadata_reactClientReferences.map((ref)=>fileURLToFilePath(ref));
if (!reactClientReferences) {
throw new Error('Static client references were not returned from the Metro SSR bundle for definedRouter');
}
debug('React client boundaries:', reactClientReferences);
// While we're here, export the router for the server to dynamically render RSC.
files.set(`_expo/rsc/${platform}/router.js`, {
targetDomain: 'server',
contents: wrapBundle(contents.src)
});
return {
reactClientReferences,
reactServerReferences,
cssModules
};
}
const routerCache = new Map();
async function getExpoRouterRscEntriesGetterAsync({ platform, routerOptions }) {
await ensureMemo();
// We can only cache this if we're using the client router since it doesn't change or use HMR
if (routerCache.has(platform) && useClientRouter) {
return routerCache.get(platform);
}
const router = await ssrLoadModule(routerModule, {
environment: 'react-server',
modulesOnly: true,
platform
}, {
hot: !useClientRouter
});
const entries = router.default({
redirects: routerOptions == null ? void 0 : routerOptions.redirects,
rewrites: routerOptions == null ? void 0 : routerOptions.rewrites
});
routerCache.set(platform, entries);
return entries;
}
function getResolveClientEntry(context) {
const serverRoot = getMetroServerRootMemo(projectRoot);
const { mode, minify = false, isExporting, baseUrl, routerRoot, asyncRoutes, preserveEnvVars, reactCompiler, lazy } = instanceMetroOptions;
(0, _assert().default)(isExporting != null && baseUrl != null && mode != null && routerRoot != null && asyncRoutes != null, `The server must be started. (isExporting: ${isExporting}, baseUrl: ${baseUrl}, mode: ${mode}, routerRoot: ${routerRoot}, asyncRoutes: ${asyncRoutes})`);
return (file, isServer)=>{
const filePath = _path().default.join(projectRoot, file.startsWith('file://') ? fileURLToFilePath(file) : file);
if (isExporting) {
(0, _assert().default)(context.ssrManifest, 'SSR manifest must exist when exporting');
const relativeFilePath = (0, _filePath.toPosixPath)(_path().default.relative(serverRoot, filePath));
(0, _assert().default)(context.ssrManifest.has(relativeFilePath), `SSR manifest is missing client boundary "${relativeFilePath}"`);
const chunk = context.ssrManifest.get(relativeFilePath);
return {
id: String(createModuleId(filePath, {
platform: context.platform,
environment: 'client'
})),
chunks: chunk != null ? [
chunk
] : []
};
}
const environment = isServer ? 'react-server' : 'client';
const searchParams = (0, _metroOptions.createBundleUrlSearchParams)({
mainModuleName: '',
platform: context.platform,
mode,
minify,
lazy,
preserveEnvVars,
asyncRoutes,
baseUrl,
routerRoot,
isExporting,
reactCompiler: !!reactCompiler,
engine: context.engine ?? undefined,
bytecode: false,
clientBoundaries: [],
inlineSourceMap: false,
environment,
modulesOnly: true,
runModule: false
});
searchParams.set('resolver.clientboundary', String(true));
const clientReferenceUrl = new URL('http://a');
// TICKLE: Handshake 1
searchParams.set('xRSC', '1');
clientReferenceUrl.search = searchParams.toString();
const relativeFilePath = _path().default.relative(serverRoot, filePath);
clientReferenceUrl.pathname = relativeFilePath;
// Ensure url.pathname ends with '.bundle'
if (!clientReferenceUrl.pathname.endsWith('.bundle')) {
clientReferenceUrl.pathname += '.bundle';
}
// Return relative URLs to help Android fetch from wherever it was loaded from since it doesn't support localhost.
const chunkName = clientReferenceUrl.pathname + clientReferenceUrl.search;
return {
id: String(createModuleId(filePath, {
platform: context.platform,
environment
})),
chunks: [
chunkName
]
};
};
}
const rscRendererCache = new Map();
let ensurePromise = null;
async function ensureSSRReady() {
// TODO: Extract CSS Modules / Assets from the bundler process
const runtime = await ssrLoadModule('metro-runtime/src/modules/empty-module.js', {
environment: 'react-server',
platform: 'web'
});
return runtime;
}
const ensureMemo = ()=>{
ensurePromise ??= ensureSSRReady();
return ensurePromise;
};
async function getRscRendererAsync(platform) {
await ensureMemo();
// NOTE(EvanBacon): We memoize this now that there's a persistent server storage cache for Server Actions.
if (rscRendererCache.has(platform)) {
return rscRendererCache.get(platform);
}
// TODO: Extract CSS Modules / Assets from the bundler process
const renderer = await ssrLoadModule('expo-router/build/rsc/rsc-renderer', {
environment: 'react-server',
platform
});
rscRendererCache.set(platform, renderer);
return renderer;
}
const rscRenderContext = new Map();
function getRscRenderContext(platform) {
// NOTE(EvanBacon): We memoize this now that there's a persistent server storage cache for Server Actions.
if (rscRenderContext.has(platform)) {
return rscRenderContext.get(platform);
}
const context = {};
rscRenderContext.set(platform, context);
return context;
}
async function renderRscToReadableStream({ input, headers, method, platform, body, engine, contentType, ssrManifest, decodedBody, routerOptions }, isExporting = instanceMetroOptions.isExporting) {
(0, _assert().default)(isExporting != null, 'The server must be started before calling renderRscToReadableStream.');
if (method === 'POST') {
(0, _assert().default)(body, 'Server request must be provided when method is POST (server actions)');
}
const context = getRscRenderContext(platform);
context['__expo_requestHeaders'] = headers;
const { renderRsc } = await getRscRendererAsync(platform);
return renderRsc({
body,
decodedBody,
context,
config: {},
input,
contentType
}, {
isExporting,
entries: await getExpoRouterRscEntriesGetterAsync({
platform,
routerOptions
}),
resolveClientEntry: getResolveClientEntry({
platform,
engine,
ssrManifest
}),
async loadServerModuleRsc (urlFragment) {
const serverRoot = getMetroServerRootMemo(projectRoot);
debug('[SSR] loadServerModuleRsc:', urlFragment);
const options = (0, _metroOptions.getMetroOptionsFromUrl)(urlFragment);
return ssrLoadModule(_path().default.join(serverRoot, options.mainModuleName), options, {
hot: true
});
}
});
}
return {
// Get the static client boundaries (no dead code elimination allowed) for the production export.
getExpoRouterClientReferencesAsync,
exportServerActionsAsync,
async exportRoutesAsync ({ platform, ssrManifest, routerOptions }, files) {
// TODO: When we add web SSR support, we need to extract CSS Modules / Assets from the bundler process to prevent FLOUC.
const { getBuildConfig } = (await getExpoRouterRscEntriesGetterAsync({
platform,
routerOptions
})).default;
// Get all the routes to render.
const buildConfig = await getBuildConfig(async ()=>// TODO: Rework prefetching code to use Metro runtime.
[]);
await Promise.all(Array.from(buildConfig).map(async ({ entries })=>{
for (const { input, isStatic } of entries || []){
if (!isStatic) {
debug('Skipping static export for route', {
input
});
continue;
}
const destRscFile = _path().default.join('_flight', platform, encodeInput(input));
const pipe = await renderRscToReadableStream({
input,
method: 'GET',
platform,
headers: new Headers(),
ssrManifest,
routerOptions
}, true);
const rsc = await (0, _stream.streamToStringAsync)(pipe);
debug('RSC Payload', {
platform,
input,
rsc
});
files.set(destRscFile, {
contents: rsc,
targetDomain: 'client',
rscId: input
});
}
}));
},
middleware: (0, _createBuiltinAPIRequestHandler.createBuiltinAPIRequestHandler)(// Match `/_flight/[platform]/[...path]`
(req)=>{
return getFullUrl(req.url).pathname.startsWith(rscPathPrefix);
}, rscMiddleware),
onReloadRscEvent: (platform)=>{
// NOTE: We cannot clear the renderer context because it would break the mounted context state.
rscRendererCache.delete(platform);
routerCache.delete(platform);
}
};
}
const getFullUrl = (url)=>{
try {
return new URL(url);
} catch {
return new URL(url, 'http://localhost:0');
}
};
const fileURLToFilePath = (fileURL)=>{
try {
return _url().default.fileURLToPath(fileURL);
} catch (error) {
if (error instanceof TypeError) {
throw Error(`Invalid URL: ${fileURL}`, {
cause: error
});
}
throw error;
}
};
const encodeInput = (input)=>{
if (input === '') {
return 'index.txt';
}
if (input === 'index') {
throw new Error('Input should not be `index`');
}
if (input.startsWith('/')) {
throw new Error('Input should not start with `/`');
}
if (input.endsWith('/')) {
throw new Error('Input should not end with `/`');
}
return input + '.txt';
};
function wrapBundle(str) {
// Skip the metro runtime so debugging is a bit easier.
// Replace the __r() call with an export statement.
// Use gm to apply to the last require line. This is needed when the bundle has side-effects.
return str.replace(/^(__r\(.*\);)$/gm, 'module.exports = $1');
}
//# sourceMappingURL=createServerComponentsMiddleware.js.map