Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add template and error file types #39808

Merged
merged 27 commits into from Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
206798c
Add template and error file types
timneutkens Aug 22, 2022
f68d056
Add fallback for template
timneutkens Aug 22, 2022
44fed5b
Remove log
timneutkens Aug 22, 2022
85d0db2
Handle error boundary
timneutkens Aug 22, 2022
cdfb688
Add comments to error components
timneutkens Aug 22, 2022
2c859cf
Move error thrown in rendering to client-side rendering to match Susp…
timneutkens Aug 22, 2022
7403eb1
Render error document instead of hydrating to avoid hydration errors.
timneutkens Aug 22, 2022
8427936
Remove throw
timneutkens Aug 22, 2022
bddbc44
Remove ts-ignore for startTransition as it's no longer used here.
timneutkens Aug 23, 2022
ce6f67c
Invert handling of template.js
timneutkens Aug 23, 2022
1a1a7ba
Fix type
timneutkens Aug 23, 2022
99775c3
Merge branch 'canary' into add/template-error-loading
timneutkens Aug 23, 2022
33c4d2c
Merge branch 'canary' of github.com:vercel/next.js into add/template-…
timneutkens Sep 7, 2022
74443d6
Apply changes again
timneutkens Sep 7, 2022
ca7a111
Fix type error
timneutkens Sep 7, 2022
3492a2a
Use new segment path
timneutkens Sep 7, 2022
430b5bc
Add test for template.client.js
timneutkens Sep 7, 2022
eeba0f3
Add test for server component template.js
timneutkens Sep 8, 2022
65c1176
Add test for error.js
timneutkens Sep 8, 2022
c8cbe91
Fix broken path
timneutkens Sep 8, 2022
20a6b5b
Skip error component tests in development
timneutkens Sep 8, 2022
aacb1df
Add test for hydrating ssr error
timneutkens Sep 8, 2022
a9be299
Update packages/next/build/webpack/loaders/next-app-loader.ts
timneutkens Sep 8, 2022
43a196f
Merge branch 'canary' of github.com:vercel/next.js into add/template-…
timneutkens Sep 9, 2022
68bb2c8
Add sri handling for error fallback page
timneutkens Sep 9, 2022
bba458d
Merge branch 'add/template-error-loading' of github.com:timneutkens/n…
timneutkens Sep 9, 2022
033bcb6
Add nonce to error fallback
timneutkens Sep 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 41 additions & 21 deletions packages/next/build/webpack/loaders/next-app-loader.ts
@@ -1,7 +1,24 @@
import type webpack from 'webpack'
import { ValueOf } from '../../../shared/lib/constants'
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
import { NODE_RESOLVE_OPTIONS } from '../../webpack-config'
import { getModuleBuildInfo } from './get-module-build-info'

export const FILE_TYPES = {
layout: 'layout',
template: 'template',
error: 'error',
loading: 'loading',
} as const

// TODO-APP: check if this can be narrowed.
type ComponentModule = () => any
export type ComponentsType = {
readonly [componentKey in ValueOf<typeof FILE_TYPES>]?: ComponentModule
} & {
readonly layoutOrPagePath?: string
readonly page?: ComponentModule
}

async function createTreeCodeFromPath({
pagePath,
resolve,
Expand Down Expand Up @@ -39,7 +56,7 @@ async function createTreeCodeFromPath({
const matchedPagePath = `${appDirPrefix}${parallelSegmentPath}`
const resolvedPagePath = await resolve(matchedPagePath)
// Use '' for segment as it's the page. There can't be a segment called '' so this is the safest way to add it.
props[parallelKey] = `['', {}, {filePath: ${JSON.stringify(
props[parallelKey] = `['', {}, {layoutOrPagePath: ${JSON.stringify(
resolvedPagePath
)}, page: () => require(${JSON.stringify(resolvedPagePath)})}]`
continue
Expand All @@ -50,31 +67,33 @@ async function createTreeCodeFromPath({
parallelSegment,
])

// For segmentPath === '' avoid double `/`
const layoutPath = `${appDirPrefix}${parallelSegmentPath}/layout`
// For segmentPath === '' avoid double `/`
const loadingPath = `${appDirPrefix}${parallelSegmentPath}/loading`

const resolvedLayoutPath = await resolve(layoutPath)
const resolvedLoadingPath = await resolve(loadingPath)
// `page` is not included here as it's added above.
const filePaths = await Promise.all(
Object.values(FILE_TYPES).map(async (file) => {
return [
file,
await resolve(`${appDirPrefix}${parallelSegmentPath}/${file}`),
] as const
})
)

props[parallelKey] = `[
'${parallelSegment}',
${subtree},
{
filePath: ${JSON.stringify(resolvedLayoutPath)},
${
resolvedLayoutPath
? `layout: () => require(${JSON.stringify(resolvedLayoutPath)}),`
: ''
}
${
resolvedLoadingPath
? `loading: () => require(${JSON.stringify(
resolvedLoadingPath
)}),`
: ''
}
${filePaths
.filter(([, filePath]) => filePath !== undefined)
.map(([file, filePath]) => {
if (filePath === undefined) {
return ''
}
return `${
file === FILE_TYPES.layout
? `layoutOrPagePath: '${filePath}',`
: ''
}${file}: () => require(${JSON.stringify(filePath)}),`
})
.join('\n')}
}
]`
}
Expand Down Expand Up @@ -164,6 +183,7 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{

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
export const RenderFromTemplateContext = require('next/dist/client/components/render-from-template-context.client.js').default
export const HotReloader = ${
// Disable HotReloader component in production
this.mode === 'development'
Expand Down
28 changes: 10 additions & 18 deletions packages/next/client/app-index.tsx
Expand Up @@ -2,7 +2,6 @@
import '../build/polyfills/polyfill-module'
// @ts-ignore react-dom/client exists when using React 18
import ReactDOMClient from 'react-dom/client'
// @ts-ignore startTransition exists when using React 18
import React from 'react'
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'

Expand Down Expand Up @@ -44,21 +43,6 @@ export const version = process.env.__NEXT_VERSION

const appElement: HTMLElement | Document | null = document

let reactRoot: any = null

function renderReactElement(
domEl: HTMLElement | Document,
fn: () => JSX.Element
): void {
const reactEl = fn()
if (!reactRoot) {
// Unlike with createRoot, you don't need a separate root.render() call here
reactRoot = (ReactDOMClient as any).hydrateRoot(domEl, reactEl)
} else {
reactRoot.render(reactEl)
}
}

const getCacheKey = () => {
const { pathname, search } = location
return pathname + search
Expand Down Expand Up @@ -182,11 +166,19 @@ function RSCComponent(props: any) {
}

export function hydrate() {
renderReactElement(appElement!, () => (
const reactEl = (
<React.StrictMode>
<Root>
<RSCComponent />
</Root>
</React.StrictMode>
))
)

const isError = document.documentElement.id === '__next_error__'
const reactRoot = isError
? (ReactDOMClient as any).createRoot(appElement)
: (ReactDOMClient as any).hydrateRoot(appElement, reactEl)
if (isError) {
reactRoot.render(reactEl)
}
}
116 changes: 98 additions & 18 deletions packages/next/client/components/layout-router.client.tsx
Expand Up @@ -12,6 +12,7 @@ import type {
import {
LayoutRouterContext,
GlobalLayoutRouterContext,
TemplateContext,
} from '../../shared/lib/app-router-context'
import { fetchServerResponse } from './app-router.client'
// import { matchSegment } from './match-segments'
Expand Down Expand Up @@ -312,14 +313,73 @@ function LoadingBoundary({
}: {
children: React.ReactNode
loading?: React.ReactNode
}) {
}): JSX.Element {
if (loading) {
return <React.Suspense fallback={loading}>{children}</React.Suspense>
}

return <>{children}</>
}

type ErrorComponent = React.ComponentType<{ error: Error; reset: () => void }>
interface ErrorBoundaryProps {
errorComponent: ErrorComponent
}

/**
* Handles errors through `getDerivedStateFromError`.
* Renders the provided error component and provides a way to `reset` the error boundary state.
*/
class ErrorBoundaryHandler extends React.Component<
ErrorBoundaryProps,
{ error: Error | null }
> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: null }
}

static getDerivedStateFromError(error: Error) {
return { error }
}

reset = () => {
this.setState({ error: null })
}

render() {
if (this.state.error) {
return (
<this.props.errorComponent
error={this.state.error}
reset={this.reset}
/>
)
}

return this.props.children
}
}

/**
* Renders error boundary with the provided "errorComponent" property as the fallback.
* If no "errorComponent" property is provided it renders the children without an error boundary.
*/
function ErrorBoundary({
errorComponent,
children,
}: ErrorBoundaryProps & { children: React.ReactNode }): JSX.Element {
if (errorComponent) {
return (
<ErrorBoundaryHandler errorComponent={errorComponent}>
{children}
</ErrorBoundaryHandler>
)
}

return <>{children}</>
}

/**
* OuterLayoutRouter handles the current segment as well as <Offscreen> rendering of other segments.
* It can be rendered next to each other with a different `parallelRouterKey`, allowing for Parallel routes.
Expand All @@ -328,12 +388,16 @@ export default function OuterLayoutRouter({
parallelRouterKey,
segmentPath,
childProp,
error,
loading,
template,
rootLayoutIncluded,
}: {
parallelRouterKey: string
segmentPath: FlightSegmentPath
childProp: ChildProp
error: ErrorComponent
template: React.ReactNode
loading: React.ReactNode | undefined
rootLayoutIncluded: boolean
}) {
Expand Down Expand Up @@ -371,23 +435,39 @@ export default function OuterLayoutRouter({
<>
{preservedSegments.map((preservedSegment) => {
return (
// Loading boundary is render for each segment to ensure they have their own loading state.
// The loading boundary is passed to the router during rendering to ensure it can be immediately rendered when suspending on a Flight fetch.
<LoadingBoundary loading={loading} key={preservedSegment}>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment ? childProp : null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
rootLayoutIncluded={rootLayoutIncluded}
/>
</LoadingBoundary>
/*
- Error boundary
- Only renders error boundary if error component is provided.
- Rendered for each segment to ensure they have their own error state.
- Loading boundary
- Only renders suspense boundary if loading components is provided.
- Rendered for each segment to ensure they have their own loading state.
- Passed to the router during rendering to ensure it can be immediately rendered when suspending on a Flight fetch.
*/
<TemplateContext.Provider
key={preservedSegment}
value={
<ErrorBoundary errorComponent={error}>
<LoadingBoundary loading={loading}>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment ? childProp : null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
rootLayoutIncluded={rootLayoutIncluded}
/>
</LoadingBoundary>
</ErrorBoundary>
}
>
{template}
</TemplateContext.Provider>
)
})}
</>
Expand Down
@@ -0,0 +1,7 @@
import React, { useContext } from 'react'
import { TemplateContext } from '../../shared/lib/app-router-context'

export default function RenderFromTemplateContext(): JSX.Element {
const children = useContext(TemplateContext)
return <>{children}</>
}