UNPKG

next

Version:

The React Framework

643 lines (642 loc) • 30.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderToHTMLOrFlight = renderToHTMLOrFlight; var _react = _interopRequireDefault(require("react")); var _querystring = require("querystring"); var _reactServerDomWebpack = require("next/dist/compiled/react-server-dom-webpack"); var _writerBrowserServer = require("next/dist/compiled/react-server-dom-webpack/writer.browser.server"); var _renderResult = _interopRequireDefault(require("./render-result")); var _nodeWebStreamsHelper = require("./node-web-streams-helper"); var _utils = require("../shared/lib/router/utils"); var _node = require("./api-utils/node"); var _htmlescape = require("./htmlescape"); var _utils1 = require("./utils"); var _matchSegments = require("../client/components/match-segments"); var _hooksClient = require("../client/components/hooks-client"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // this needs to be required lazily so that `next-server` can set // the env before we require const ReactDOMServer = _utils1.shouldUseReactRoot ? require("react-dom/server.browser") : require("react-dom/server"); /** * Interop between "export default" and "module.exports". */ function interopDefault(mod) { return mod.default || mod; } const rscCache = new Map(); var // Shadowing check does not work with TypeScript enums // eslint-disable-next-line no-shadow RecordStatus; (function(RecordStatus) { RecordStatus[RecordStatus["Pending"] = 0] = "Pending"; RecordStatus[RecordStatus["Resolved"] = 1] = "Resolved"; RecordStatus[RecordStatus["Rejected"] = 2] = "Rejected"; })(RecordStatus || (RecordStatus = {})); /** * Create data fetching record for Promise. */ function createRecordFromThenable(thenable) { const record = { status: 0, value: thenable }; thenable.then(function(value) { if (record.status === 0) { const resolvedRecord = record; resolvedRecord.status = 1; resolvedRecord.value = value; } }, function(err) { if (record.status === 0) { const rejectedRecord = record; rejectedRecord.status = 2; rejectedRecord.value = err; } }); return record; } /** * Read record value or throw Promise if it's not resolved yet. */ function readRecordValue(record) { if (record.status === 1) { return record.value; } else { throw record.value; } } /** * Preload data fetching record before it is called during React rendering. * If the record is already in the cache returns that record. */ function preloadDataFetchingRecord(map, key, fetcher) { let record = map.get(key); if (!record) { const thenable = fetcher(); record = createRecordFromThenable(thenable); map.set(key, record); } return record; } /** * Render Flight stream. * This is only used for renderToHTML, the Flight response does not need additional wrappers. */ function useFlightResponse(writable, cachePrefix, req, serverComponentManifest) { const id = cachePrefix + "," + _react.default.useId(); let entry = rscCache.get(id); if (!entry) { const [renderStream, forwardStream] = (0, _nodeWebStreamsHelper).readableStreamTee(req); entry = (0, _reactServerDomWebpack).createFromReadableStream(renderStream, { moduleMap: serverComponentManifest.__ssr_module_mapping__ }); rscCache.set(id, entry); let bootstrapped = false; // We only attach CSS chunks to the inlined data. const forwardReader = forwardStream.getReader(); const writer = writable.getWriter(); function process() { forwardReader.read().then(({ done , value })=>{ if (!bootstrapped) { bootstrapped = true; writer.write((0, _nodeWebStreamsHelper).encodeText(`<script>(self.__next_s=self.__next_s||[]).push(${(0, _htmlescape).htmlEscapeJsonString(JSON.stringify([ 0, id ]))})</script>`)); } if (done) { rscCache.delete(id); writer.close(); } else { const responsePartial = (0, _nodeWebStreamsHelper).decodeText(value); const scripts = `<script>(self.__next_s=self.__next_s||[]).push(${(0, _htmlescape).htmlEscapeJsonString(JSON.stringify([ 1, id, responsePartial ]))})</script>`; writer.write((0, _nodeWebStreamsHelper).encodeText(scripts)); process(); } }); } process(); } return entry; } /** * Create a component that renders the Flight stream. * This is only used for renderToHTML, the Flight response does not need additional wrappers. */ function createServerComponentRenderer(ComponentToRender, ComponentMod, { cachePrefix , transformStream , serverComponentManifest , serverContexts }) { // We need to expose the `__webpack_require__` API globally for // react-server-dom-webpack. This is a hack until we find a better way. if (ComponentMod.__next_app_webpack_require__ || ComponentMod.__next_rsc__) { var ref; // @ts-ignore globalThis.__next_require__ = ComponentMod.__next_app_webpack_require__ || ((ref = ComponentMod.__next_rsc__) == null ? void 0 : ref.__webpack_require__); // @ts-ignore globalThis.__next_chunk_load__ = ()=>Promise.resolve(); } let RSCStream; const createRSCStream = ()=>{ if (!RSCStream) { RSCStream = (0, _writerBrowserServer).renderToReadableStream(/*#__PURE__*/ _react.default.createElement(ComponentToRender, null), serverComponentManifest, { context: serverContexts }); } return RSCStream; }; const writable = transformStream.writable; return function ServerComponentWrapper() { const reqStream = createRSCStream(); const response = useFlightResponse(writable, cachePrefix, reqStream, serverComponentManifest); return response.readRoot(); }; } /** * Shorten the dynamic param in order to make it smaller when transmitted to the browser. */ function getShortDynamicParamType(type) { switch(type){ case "catchall": return "c"; case "optional-catchall": return "oc"; case "dynamic": return "d"; default: throw new Error("Unknown dynamic param type"); } } /** * Parse dynamic route segment to type of parameter */ function getSegmentParam(segment) { if (segment.startsWith("[[...") && segment.endsWith("]]")) { return { type: "optional-catchall", param: segment.slice(5, -2) }; } if (segment.startsWith("[...") && segment.endsWith("]")) { return { type: "catchall", param: segment.slice(4, -1) }; } if (segment.startsWith("[") && segment.endsWith("]")) { return { type: "dynamic", param: segment.slice(1, -1) }; } return null; } /** * Get inline <link> tags based on __next_rsc_css__ manifest. Only used when rendering to HTML. */ function getCssInlinedLinkTags(ComponentMod, serverComponentManifest) { var ref; const importedServerCSSFiles = ((ref = ComponentMod.__client__) == null ? void 0 : ref.__next_rsc_css__) || []; return Array.from(new Set(importedServerCSSFiles.map((css)=>css.endsWith(".css") ? serverComponentManifest[css].default.chunks : []).flat())); } async function renderToHTMLOrFlight(req, res, pathname, query, renderOpts, isPagesDir) { // @ts-expect-error createServerContext exists in react@experimental + react-dom@experimental if (typeof _react.default.createServerContext === "undefined") { throw new Error('"app" directory requires React.createServerContext which is not available in the version of React you are using. Please update to react@experimental and react-dom@experimental.'); } // don't modify original query object query = Object.assign({}, query); const { buildManifest , serverComponentManifest , supportsDynamicHTML , ComponentMod , } = renderOpts; const isFlight = query.__flight__ !== undefined; // Handle client-side navigation to pages directory if (isFlight && isPagesDir) { (0, _utils1).stripInternalQueries(query); const search = (0, _querystring).stringify(query); // Empty so that the client-side router will do a full page navigation. const flightData = pathname + (search ? `?${search}` : ""); return new _renderResult.default((0, _writerBrowserServer).renderToReadableStream(flightData, serverComponentManifest).pipeThrough((0, _nodeWebStreamsHelper).createBufferedTransformStream())); } // TODO-APP: verify the tree is valid // TODO-APP: verify query param is single value (not an array) // TODO-APP: verify tree can't grow out of control /** * Router state provided from the client-side router. Used to handle rendering from the common layout down. */ const providedFlightRouterState = isFlight ? query.__flight_router_state_tree__ ? JSON.parse(query.__flight_router_state_tree__) : {} : undefined; (0, _utils1).stripInternalQueries(query); const pageIsDynamic = (0, _utils).isDynamicRoute(pathname); const LayoutRouter = ComponentMod.LayoutRouter; const HotReloader = ComponentMod.HotReloader; const headers = req.headers; // TODO-APP: fix type of req // @ts-expect-error const cookies = req.cookies; /** * The tree created in next-app-loader that holds component segments and modules */ const loaderTree = ComponentMod.tree; // Reads of this are cached on the `req` object, so this should resolve // instantly. There's no need to pass this data down from a previous // invoke, where we'd have to consider server & serverless. const previewData = (0, _node).tryGetPreviewData(req, res, renderOpts.previewProps); const isPreview = previewData !== false; /** * Server Context is specifically only available in Server Components. * It has to hold values that can't change while rendering from the common layout down. * An example of this would be that `headers` are available but `searchParams` are not because that'd mean we have to render from the root layout down on all requests. */ const serverContexts = [ [ "WORKAROUND", null ], [ "HeadersContext", headers ], [ "CookiesContext", cookies ], [ "PreviewDataContext", previewData ], ]; /** * Used to keep track of in-flight / resolved data fetching Promises. */ const dataCache = new Map(); /** * Dynamic parameters. E.g. when you visit `/dashboard/vercel` which is rendered by `/dashboard/[slug]` the value will be {"slug": "vercel"}. */ const pathParams = renderOpts.params; /** * Parse the dynamic segment and return the associated value. */ const getDynamicParamFromSegment = (// [slug] / [[slug]] / [...slug] segment)=>{ const segmentParam = getSegmentParam(segment); if (!segmentParam) { return null; } const key = segmentParam.param; const value = pathParams[key]; if (!value) { // Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard` if (segmentParam.type === "optional-catchall") { const type = getShortDynamicParamType(segmentParam.type); return { param: key, value: null, type: type, // This value always has to be a string. treeSegment: [ key, "", type ] }; } return null; } const type = getShortDynamicParamType(segmentParam.type); return { param: key, // The value that is passed to user code. value: value, // The value that is rendered in the router tree. treeSegment: [ key, Array.isArray(value) ? value.join("/") : value, type ], type: type }; }; const createFlightRouterStateFromLoaderTree = ([segment, parallelRoutes, { loading }])=>{ const hasLoading = Boolean(loading); const dynamicParam = getDynamicParamFromSegment(segment); const segmentTree = [ dynamicParam ? dynamicParam.treeSegment : segment, {}, ]; if (parallelRoutes) { segmentTree[1] = Object.keys(parallelRoutes).reduce((existingValue, currentValue)=>{ existingValue[currentValue] = createFlightRouterStateFromLoaderTree(parallelRoutes[currentValue]); return existingValue; }, {}); } if (hasLoading) { segmentTree[4] = "loading"; } return segmentTree; }; /** * Use the provided loader tree to create the React Component tree. */ const createComponentTree = async ({ createSegmentPath , loaderTree: [segment, parallelRoutes, { layout , loading , page }] , parentParams , firstItem , rootLayoutIncluded })=>{ const Loading = loading ? await interopDefault(loading()) : undefined; const isLayout = typeof layout !== "undefined"; const isPage = typeof page !== "undefined"; const layoutOrPageMod = isLayout ? await layout() : isPage ? await page() : undefined; /** * Checks if the current segment is a root layout. */ const rootLayoutAtThisLevel = isLayout && !rootLayoutIncluded; /** * Checks if the current segment or any level above it has a root layout. */ const rootLayoutIncludedAtThisLevelOrAbove = rootLayoutIncluded || rootLayoutAtThisLevel; /** * Check if the current layout/page is a client component */ const isClientComponentModule = layoutOrPageMod && !layoutOrPageMod.hasOwnProperty("__next_rsc__"); // Only server components can have getServerSideProps / getStaticProps // TODO-APP: friendly error with correct stacktrace. Potentially this can be part of the compiler instead. if (isClientComponentModule) { if (layoutOrPageMod.getServerSideProps) { throw new Error("getServerSideProps is not supported on Client Components"); } if (layoutOrPageMod.getStaticProps) { throw new Error("getStaticProps is not supported on Client Components"); } } /** * The React Component to render. */ const Component = layoutOrPageMod ? interopDefault(layoutOrPageMod) : undefined; // Handle dynamic segment params. const segmentParam = getDynamicParamFromSegment(segment); /** * Create object holding the parent params and current params, this is passed to getServerSideProps and getStaticProps. */ const currentParams = // Handle null case where dynamic param is optional segmentParam && segmentParam.value !== null ? { ...parentParams, [segmentParam.param]: segmentParam.value } : parentParams; // Resolve the segment param const actualSegment = segmentParam ? segmentParam.treeSegment : segment; // This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down const parallelRouteMap = await Promise.all(Object.keys(parallelRoutes).map(async (parallelRouteKey)=>{ const currentSegmentPath = firstItem ? [ parallelRouteKey ] : [ actualSegment, parallelRouteKey ]; // Create the child component const { Component: ChildComponent } = await createComponentTree({ createSegmentPath: (child)=>{ return createSegmentPath([ ...currentSegmentPath, ...child ]); }, loaderTree: parallelRoutes[parallelRouteKey], parentParams: currentParams, rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove }); const childSegment = parallelRoutes[parallelRouteKey][0]; const childSegmentParam = getDynamicParamFromSegment(childSegment); const childProp = { current: /*#__PURE__*/ _react.default.createElement(ChildComponent, null), segment: childSegmentParam ? childSegmentParam.treeSegment : childSegment }; // This is turned back into an object below. return [ parallelRouteKey, /*#__PURE__*/ _react.default.createElement(LayoutRouter, { parallelRouterKey: parallelRouteKey, segmentPath: createSegmentPath(currentSegmentPath), loading: Loading ? /*#__PURE__*/ _react.default.createElement(Loading, null) : undefined, childProp: childProp, rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove }), ]; })); // Convert the parallel route map into an object after all promises have been resolved. const parallelRouteComponents = parallelRouteMap.reduce((list, [parallelRouteKey, Comp])=>{ list[parallelRouteKey] = Comp; return list; }, {}); // When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component if (!Component) { return { Component: ()=>/*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, parallelRouteComponents.children) }; } const segmentPath = createSegmentPath([ actualSegment ]); const dataCacheKey = JSON.stringify(segmentPath); let fetcher = null; // TODO-APP: pass a shared cache from previous getStaticProps/getServerSideProps calls? if (layoutOrPageMod.getServerSideProps) { // TODO-APP: recommendation for i18n // locales: (renderOpts as any).locales, // always the same // locale: (renderOpts as any).locale, // /nl/something -> nl // defaultLocale: (renderOpts as any).defaultLocale, // changes based on domain const getServerSidePropsContext = { headers, cookies, layoutSegments: segmentPath, // TODO-APP: change pathname to actual pathname, it holds the dynamic parameter currently ...isPage ? { searchParams: query, pathname } : {}, ...pageIsDynamic ? { params: currentParams } : undefined, ...isPreview ? { preview: true, previewData: previewData } : undefined }; fetcher = ()=>Promise.resolve(layoutOrPageMod.getServerSideProps(getServerSidePropsContext)); } // TODO-APP: implement layout specific caching for getStaticProps if (layoutOrPageMod.getStaticProps) { const getStaticPropsContext = { layoutSegments: segmentPath, ...isPage ? { pathname } : {}, ...pageIsDynamic ? { params: currentParams } : undefined, ...isPreview ? { preview: true, previewData: previewData } : undefined }; fetcher = ()=>Promise.resolve(layoutOrPageMod.getStaticProps(getStaticPropsContext)); } if (fetcher) { // Kick off data fetching before rendering, this ensures there is no waterfall for layouts as // all data fetching required to render the page is kicked off simultaneously preloadDataFetchingRecord(dataCache, dataCacheKey, fetcher); } return { Component: ()=>{ let props; // The data fetching was kicked off before rendering (see above) // if the data was not resolved yet the layout rendering will be suspended if (fetcher) { const record = preloadDataFetchingRecord(dataCache, dataCacheKey, fetcher); // Result of calling getStaticProps or getServerSideProps. If promise is not resolve yet it will suspend. const recordValue = readRecordValue(record); if (props) { props = Object.assign({}, props, recordValue.props); } else { props = recordValue.props; } } return /*#__PURE__*/ _react.default.createElement(Component, Object.assign({}, props, parallelRouteComponents, { // TODO-APP: params and query have to be blocked parallel route names. Might have to add a reserved name list. // Params are always the current params that apply to the layout // If you have a `/dashboard/[team]/layout.js` it will provide `team` as a param but not anything further down. params: currentParams }, isPage ? { searchParams: query } : {})); } }; }; // Handle Flight render request. This is only used when client-side navigating. E.g. when you `router.push('/dashboard')` or `router.reload()`. if (isFlight) { // TODO-APP: throw on invalid flightRouterState /** * Use router state to decide at what common layout to render the page. * This can either be the common layout between two pages or a specific place to start rendering from using the "refetch" marker in the tree. */ const walkTreeWithFlightRouterState = async (loaderTreeToFilter, parentParams, flightRouterState, parentRendered)=>{ const [segment, parallelRoutes] = loaderTreeToFilter; const parallelRoutesKeys = Object.keys(parallelRoutes); // Because this function walks to a deeper point in the tree to start rendering we have to track the dynamic parameters up to the point where rendering starts // That way even when rendering the subtree getServerSideProps/getStaticProps get the right parameters. const segmentParam = getDynamicParamFromSegment(segment); const currentParams = // Handle null case where dynamic param is optional segmentParam && segmentParam.value !== null ? { ...parentParams, [segmentParam.param]: segmentParam.value } : parentParams; const actualSegment = segmentParam ? segmentParam.treeSegment : segment; /** * Decide if the current segment is where rendering has to start. */ const renderComponentsOnThisLevel = // No further router state available !flightRouterState || // Segment in router state does not match current segment !(0, _matchSegments).matchSegment(actualSegment, flightRouterState[0]) || // Last item in the tree parallelRoutesKeys.length === 0 || // Explicit refresh flightRouterState[3] === "refetch"; if (!parentRendered && renderComponentsOnThisLevel) { return [ actualSegment, // Create router state using the slice of the loaderTree createFlightRouterStateFromLoaderTree(loaderTreeToFilter), // Create component tree using the slice of the loaderTree /*#__PURE__*/ _react.default.createElement((await createComponentTree(// This ensures flightRouterPath is valid and filters down the tree { createSegmentPath: (child)=>child, loaderTree: loaderTreeToFilter, parentParams: currentParams, firstItem: true })).Component), ]; } // Walk through all parallel routes. for (const parallelRouteKey of parallelRoutesKeys){ const parallelRoute = parallelRoutes[parallelRouteKey]; const path = await walkTreeWithFlightRouterState(parallelRoute, currentParams, flightRouterState && flightRouterState[1][parallelRouteKey], parentRendered || renderComponentsOnThisLevel); if (typeof path[path.length - 1] !== "string") { return [ actualSegment, parallelRouteKey, ...path ]; } } return [ actualSegment ]; }; // Flight data that is going to be passed to the browser. // Currently a single item array but in the future multiple patches might be combined in a single request. const flightData = [ // TODO-APP: change walk to output without '' (await walkTreeWithFlightRouterState(loaderTree, {}, providedFlightRouterState)).slice(1), ]; return new _renderResult.default((0, _writerBrowserServer).renderToReadableStream(flightData, serverComponentManifest, { context: serverContexts }).pipeThrough((0, _nodeWebStreamsHelper).createBufferedTransformStream())); } // Below this line is handling for rendering to HTML. // Create full component tree from root to leaf. const { Component: ComponentTree } = await createComponentTree({ createSegmentPath: (child)=>child, loaderTree: loaderTree, parentParams: {}, firstItem: true }); // AppRouter is provided by next-app-loader const AppRouter = ComponentMod.AppRouter; let serverComponentsInlinedTransformStream = new TransformStream(); // TODO-APP: validate req.url as it gets passed to render. const initialCanonicalUrl = req.url; const initialStylesheets = getCssInlinedLinkTags(ComponentMod, serverComponentManifest); /** * A new React Component that renders the provided React Component * using Flight which can then be rendered to HTML. */ const ServerComponentsRenderer = createServerComponentRenderer(()=>{ const initialTree = createFlightRouterStateFromLoaderTree(loaderTree); return /*#__PURE__*/ _react.default.createElement(AppRouter, { hotReloader: HotReloader && /*#__PURE__*/ _react.default.createElement(HotReloader, { assetPrefix: renderOpts.assetPrefix || "" }), initialCanonicalUrl: initialCanonicalUrl, initialTree: initialTree, initialStylesheets: initialStylesheets }, /*#__PURE__*/ _react.default.createElement(ComponentTree, null)); }, ComponentMod, { cachePrefix: initialCanonicalUrl, transformStream: serverComponentsInlinedTransformStream, serverComponentManifest, serverContexts }); let flushEffectsHandler = null; function FlushEffects({ children }) { // Reset flushEffectsHandler on each render flushEffectsHandler = null; const setFlushEffectsHandler = _react.default.useCallback((handler)=>{ if (flushEffectsHandler) throw new Error("The `useFlushEffects` hook cannot be used more than once."); flushEffectsHandler = handler; }, []); return /*#__PURE__*/ _react.default.createElement(_hooksClient.FlushEffectsContext.Provider, { value: setFlushEffectsHandler }, children); } /** * Rules of Static & Dynamic HTML: * * 1.) We must generate static HTML unless the caller explicitly opts * in to dynamic HTML support. * * 2.) If dynamic HTML support is requested, we must honor that request * or throw an error. It is the sole responsibility of the caller to * ensure they aren't e.g. requesting dynamic HTML for an AMP page. * * These rules help ensure that other existing features like request caching, * coalescing, and ISR continue working as intended. */ const generateStaticHTML = supportsDynamicHTML !== true; const bodyResult = async ()=>{ const content = /*#__PURE__*/ _react.default.createElement(FlushEffects, null, /*#__PURE__*/ _react.default.createElement(ServerComponentsRenderer, null)); const flushEffectHandler = ()=>{ const flushed = ReactDOMServer.renderToString(/*#__PURE__*/ _react.default.createElement(_react.default.Fragment, null, flushEffectsHandler && flushEffectsHandler())); return flushed; }; const renderStream = await (0, _nodeWebStreamsHelper).renderToInitialStream({ ReactDOMServer, element: content, streamOptions: { // Include hydration scripts in the HTML bootstrapScripts: buildManifest.rootMainFiles.map((src)=>`${renderOpts.assetPrefix || ""}/_next/` + src) } }); return await (0, _nodeWebStreamsHelper).continueFromInitialStream(renderStream, { dataStream: serverComponentsInlinedTransformStream == null ? void 0 : serverComponentsInlinedTransformStream.readable, generateStaticHTML: generateStaticHTML, flushEffectHandler, flushEffectsToHead: true, initialStylesheets }); }; return new _renderResult.default(await bodyResult()); } //# sourceMappingURL=app-render.js.map