UNPKG

@remix-run/server-runtime

Version:
291 lines (279 loc) • 10.8 kB
/** * @remix-run/server-runtime v2.16.8 * * Copyright (c) Remix Software Inc. * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var router = require('@remix-run/router'); var turboStream = require('turbo-stream'); var errors = require('./errors.js'); var headers = require('./headers.js'); var mode = require('./mode.js'); var responses = require('./responses.js'); const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect"); // We can't use a 3xx status or else the `fetch()` would follow the redirect. // We need to communicate the redirect back as data so we can act on it in the // client side router. We use a 202 to avoid any automatic caching we might // get from a 200 since a "temporary" redirect should not be cached. This lets // the user control cache behavior via Cache-Control const SINGLE_FETCH_REDIRECT_STATUS = 202; function getSingleFetchDataStrategy({ isActionDataRequest, loadRouteIds } = {}) { return async ({ request, matches }) => { // Don't call loaders on action data requests if (isActionDataRequest && request.method === "GET") { return {}; } // Only run opt-in loaders when fine-grained revalidation is enabled let matchesToLoad = loadRouteIds ? matches.filter(m => loadRouteIds.includes(m.route.id)) : matches; let results = await Promise.all(matchesToLoad.map(match => match.resolve())); return results.reduce((acc, result, i) => Object.assign(acc, { [matchesToLoad[i].route.id]: result }), {}); }; } async function singleFetchAction(build, serverMode, staticHandler, request, handlerUrl, loadContext, handleError) { try { let handlerRequest = new Request(handlerUrl, { method: request.method, body: request.body, headers: request.headers, signal: request.signal, ...(request.body ? { duplex: "half" } : undefined) }); let result = await staticHandler.query(handlerRequest, { requestContext: loadContext, skipLoaderErrorBubbling: true, dataStrategy: getSingleFetchDataStrategy({ isActionDataRequest: true }) }); // Unlike `handleDataRequest`, when singleFetch is enabled, query does // let non-Response return values through if (responses.isResponse(result)) { return { result: getSingleFetchRedirect(result.status, result.headers, build.basename), headers: result.headers, status: SINGLE_FETCH_REDIRECT_STATUS }; } let context = result; let headers$1 = headers.getDocumentHeaders(build, context); if (responses.isRedirectStatusCode(context.statusCode) && headers$1.has("Location")) { return { result: getSingleFetchRedirect(context.statusCode, headers$1, build.basename), headers: headers$1, status: SINGLE_FETCH_REDIRECT_STATUS }; } // Sanitize errors outside of development environments if (context.errors) { Object.values(context.errors).forEach(err => { // @ts-expect-error This is "private" from users but intended for internal use if (!router.isRouteErrorResponse(err) || err.error) { handleError(err); } }); context.errors = errors.sanitizeErrors(context.errors, serverMode); } let singleFetchResult; if (context.errors) { singleFetchResult = { error: Object.values(context.errors)[0] }; } else { singleFetchResult = { data: Object.values(context.actionData || {})[0] }; } return { result: singleFetchResult, headers: headers$1, status: context.statusCode }; } catch (error) { handleError(error); // These should only be internal remix errors, no need to deal with responseStubs return { result: { error }, headers: new Headers(), status: 500 }; } } async function singleFetchLoaders(build, serverMode, staticHandler, request, handlerUrl, loadContext, handleError) { try { var _URL$searchParams$get; let handlerRequest = new Request(handlerUrl, { headers: request.headers, signal: request.signal }); let loadRouteIds = ((_URL$searchParams$get = new URL(request.url).searchParams.get("_routes")) === null || _URL$searchParams$get === void 0 ? void 0 : _URL$searchParams$get.split(",")) || undefined; let result = await staticHandler.query(handlerRequest, { requestContext: loadContext, skipLoaderErrorBubbling: true, dataStrategy: getSingleFetchDataStrategy({ loadRouteIds }) }); if (responses.isResponse(result)) { return { result: { [SingleFetchRedirectSymbol]: getSingleFetchRedirect(result.status, result.headers, build.basename) }, headers: result.headers, status: SINGLE_FETCH_REDIRECT_STATUS }; } let context = result; let headers$1 = headers.getDocumentHeaders(build, context); if (responses.isRedirectStatusCode(context.statusCode) && headers$1.has("Location")) { return { result: { [SingleFetchRedirectSymbol]: getSingleFetchRedirect(context.statusCode, headers$1, build.basename) }, headers: headers$1, status: SINGLE_FETCH_REDIRECT_STATUS }; } // Sanitize errors outside of development environments if (context.errors) { Object.values(context.errors).forEach(err => { // @ts-expect-error This is "private" from users but intended for internal use if (!router.isRouteErrorResponse(err) || err.error) { handleError(err); } }); context.errors = errors.sanitizeErrors(context.errors, serverMode); } // Aggregate results based on the matches we intended to load since we get // `null` values back in `context.loaderData` for routes we didn't load let results = {}; let loadedMatches = loadRouteIds ? context.matches.filter(m => m.route.loader && loadRouteIds.includes(m.route.id)) : context.matches; loadedMatches.forEach(m => { var _context$loaderData, _context$errors; let data = (_context$loaderData = context.loaderData) === null || _context$loaderData === void 0 ? void 0 : _context$loaderData[m.route.id]; let error = (_context$errors = context.errors) === null || _context$errors === void 0 ? void 0 : _context$errors[m.route.id]; if (error !== undefined) { results[m.route.id] = { error }; } else if (data !== undefined) { results[m.route.id] = { data }; } }); return { result: results, headers: headers$1, status: context.statusCode }; } catch (error) { handleError(error); // These should only be internal remix errors, no need to deal with responseStubs return { result: { root: { error } }, headers: new Headers(), status: 500 }; } } function getSingleFetchRedirect(status, headers, basename) { let redirect = headers.get("Location"); if (basename) { redirect = router.stripBasename(redirect, basename) || redirect; } return { redirect, status, revalidate: // Technically X-Remix-Revalidate isn't needed here - that was an implementation // detail of ?_data requests as our way to tell the front end to revalidate when // we didn't have a response body to include that information in. // With single fetch, we tell the front end via this revalidate boolean field. // However, we're respecting it for now because it may be something folks have // used in their own responses // TODO(v3): Consider removing or making this official public API headers.has("X-Remix-Revalidate") || headers.has("Set-Cookie"), reload: headers.has("X-Remix-Reload-Document"), replace: headers.has("X-Remix-Replace") }; } // Note: If you change this function please change the corresponding // decodeViaTurboStream function in server-runtime function encodeViaTurboStream(data, requestSignal, streamTimeout, serverMode) { let controller = new AbortController(); // How long are we willing to wait for all of the promises in `data` to resolve // before timing out? We default this to 50ms shorter than the default value for // `ABORT_DELAY` in our built-in `entry.server.tsx` so that once we reject we // have time to flush the rejections down through React's rendering stream before ` // we call abort() on that. If the user provides their own it's up to them to // decouple the aborting of the stream from the aborting of React's renderToPipeableStream let timeoutId = setTimeout(() => controller.abort(new Error("Server Timeout")), typeof streamTimeout === "number" ? streamTimeout : 4950); requestSignal.addEventListener("abort", () => clearTimeout(timeoutId)); return turboStream.encode(data, { signal: controller.signal, plugins: [value => { // Even though we sanitized errors on context.errors prior to responding, // we still need to handle this for any deferred data that rejects with an // Error - as those will not be sanitized yet if (value instanceof Error) { let { name, message, stack } = serverMode === mode.ServerMode.Production ? errors.sanitizeError(value, serverMode) : value; return ["SanitizedError", name, message, stack]; } if (value instanceof router.UNSAFE_ErrorResponseImpl) { let { data, status, statusText } = value; return ["ErrorResponse", data, status, statusText]; } if (value && typeof value === "object" && SingleFetchRedirectSymbol in value) { return ["SingleFetchRedirect", value[SingleFetchRedirectSymbol]]; } }], postPlugins: [value => { if (!value) return; if (typeof value !== "object") return; return ["SingleFetchClassInstance", Object.fromEntries(Object.entries(value))]; }, () => ["SingleFetchFallback"]] }); } function data(value, init) { return router.data(value, init); } // prettier-ignore // eslint-disable-next-line exports.SINGLE_FETCH_REDIRECT_STATUS = SINGLE_FETCH_REDIRECT_STATUS; exports.SingleFetchRedirectSymbol = SingleFetchRedirectSymbol; exports.data = data; exports.encodeViaTurboStream = encodeViaTurboStream; exports.getSingleFetchDataStrategy = getSingleFetchDataStrategy; exports.getSingleFetchRedirect = getSingleFetchRedirect; exports.singleFetchAction = singleFetchAction; exports.singleFetchLoaders = singleFetchLoaders;