From a16931f1067fc00014330dc76825143c0ea9d1dc Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 2 Jun 2022 11:11:03 +0200 Subject: [PATCH] Add Suspense boundary to demo --- .../build/webpack/loaders/next-app-loader.ts | 58 ++++++++++++++++--- packages/next/server/app-render.tsx | 21 ++++++- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index a079a9eed0bdb..cb3de9971a00d 100644 --- a/packages/next/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/build/webpack/loaders/next-app-loader.ts @@ -14,14 +14,16 @@ function pathToUrlPath(pathname: string) { return urlPath } -async function resolveLayoutPathsByPage({ +async function resolvePathsByPage({ + name, pagePath, resolve, }: { + name: 'layout' | 'loading' pagePath: string resolve: (pathname: string) => Promise }) { - const layoutPaths = new Map() + const paths = new Map() const parts = pagePath.split('/') const isNewRootLayout = parts[1]?.length > 2 && parts[1]?.startsWith('(') && parts[1]?.endsWith(')') @@ -32,26 +34,30 @@ async function resolveLayoutPathsByPage({ if (!pathWithoutSlashLayout) { continue } - const layoutPath = `${pathWithoutSlashLayout}/layout` + const layoutPath = `${pathWithoutSlashLayout}/${name}` let resolvedLayoutPath = await resolve(layoutPath) let urlPath = pathToUrlPath(pathWithoutSlashLayout) // if we are in a new root app/(root) and a custom root layout was // not provided or a root layout app/layout is not present, we use // a default root layout to provide the html/body tags - const isCustomRootLayout = isNewRootLayout && i === 2 + const isCustomRootLayout = name === 'layout' && isNewRootLayout && i === 2 - if ((isCustomRootLayout || i === 1) && !resolvedLayoutPath) { + if ( + name === 'layout' && + (isCustomRootLayout || i === 1) && + !resolvedLayoutPath + ) { resolvedLayoutPath = await resolve('next/dist/lib/app-layout') } - layoutPaths.set(urlPath, resolvedLayoutPath) + paths.set(urlPath, resolvedLayoutPath) // if we're in a new root layout don't add the top-level app/layout if (isCustomRootLayout) { break } } - return layoutPaths + return paths } const nextAppLoader: webpack.LoaderDefinitionFunction<{ @@ -75,7 +81,39 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ } const resolve = this.getResolve(resolveOptions) - const layoutPaths = await resolveLayoutPathsByPage({ + const loadingPaths = await resolvePathsByPage({ + name: 'loading', + pagePath: pagePath, + resolve: async (pathname) => { + try { + return await resolve(this.rootContext, pathname) + } catch (err: any) { + if (err.message.includes("Can't resolve")) { + return undefined + } + throw err + } + }, + }) + + const loadingComponentsCode = [] + for (const [loadingPath, resolvedLoadingPath] of loadingPaths) { + if (resolvedLoadingPath) { + this.addDependency(resolvedLoadingPath) + // use require so that we can bust the require cache + const codeLine = `'${loadingPath}': () => require('${resolvedLoadingPath}')` + loadingComponentsCode.push(codeLine) + } else { + for (const ext of extensions) { + this.addMissingDependency( + path.join(appDir, loadingPath, `layout${ext}`) + ) + } + } + } + + const layoutPaths = await resolvePathsByPage({ + name: 'layout', pagePath: pagePath, resolve: async (pathname) => { try { @@ -117,6 +155,10 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ ${componentsCode.join(',\n')} }; + export const loadingComponents = { + ${loadingComponentsCode.join(',\n')} + }; + export const AppRouter = require('next/dist/client/components/app-router.client.js').default export const LayoutRouter = require('next/dist/client/components/layout-router.client.js').default diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index fd209cf3bab5a..cc758bdbd3092 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -36,6 +36,10 @@ export type RenderOptsPartial = { export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial +function interopDefault(mod: any) { + return mod.default || mod +} + const rscCache = new Map() // Shadowing check does not work with TypeScript enums @@ -236,7 +240,7 @@ export async function renderToHTML( .sort() .map((path) => { const mod = ComponentMod.components[path]() - mod.Component = mod.default || mod + mod.Component = interopDefault(mod) mod.path = path return mod }) @@ -307,6 +311,9 @@ export async function renderToHTML( } const LayoutRouter = ComponentMod.LayoutRouter + const getLoadingMod = ComponentMod.loadingComponents[layout.path] + const Loading = getLoadingMod ? interopDefault(getLoadingMod()) : null + // eslint-disable-next-line no-loop-func const lastComponent = WrappedComponent WrappedComponent = (props: any) => { @@ -333,16 +340,24 @@ export async function renderToHTML( {}, null ) + + // TODO: add tests for loading.js + const chilrenWithLoading = Loading ? ( + }>{children} + ) : ( + children + ) + // Pages don't need to be wrapped in a router return React.createElement( layout.Component, props, layout.path.endsWith('/page') ? ( - children + chilrenWithLoading ) : ( // TODO: only provide the part of the url that is relevant to the layout (see layout-router.client.tsx) - {children} + {chilrenWithLoading} ) )