Skip to content

Commit

Permalink
handle default
Browse files Browse the repository at this point in the history
fix test fixture

improve test

handle no matched boundary case

test: reorganize the default test

update metadata error convention (#72834)

add element validation and remove unused boundaries

use flag

pass down flag everywhere
  • Loading branch information
huozhi committed Nov 21, 2024
1 parent 4ed3837 commit cab2e9d
Show file tree
Hide file tree
Showing 52 changed files with 602 additions and 102 deletions.
5 changes: 5 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,9 @@ export default async function build(
)

const isAppDynamicIOEnabled = Boolean(config.experimental.dynamicIO)
const isNavigationEnabled = Boolean(
config.experimental.navigationDeniedApi
)
const isAppPPREnabled = checkIsAppPPREnabled(config.experimental.ppr)

const routesManifestPath = path.join(distDir, ROUTES_MANIFEST)
Expand Down Expand Up @@ -1989,6 +1992,7 @@ export default async function build(
configFileName,
runtimeEnvConfig,
dynamicIO: isAppDynamicIOEnabled,
navigationDeniedApi: isNavigationEnabled,
httpAgentOptions: config.httpAgentOptions,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
Expand Down Expand Up @@ -2212,6 +2216,7 @@ export default async function build(
edgeInfo,
pageType,
dynamicIO: isAppDynamicIOEnabled,
navigationDeniedApi: isNavigationEnabled,
cacheHandler: config.cacheHandler,
cacheHandlers: config.experimental.cacheHandlers,
isrFlushToDisk: ciEnvironment.hasNextSupport
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@ export async function buildAppStaticPaths({
page,
distDir,
dynamicIO,
navigationDeniedApi,
configFileName,
segments,
isrFlushToDisk,
Expand All @@ -1230,6 +1231,7 @@ export async function buildAppStaticPaths({
dir: string
page: string
dynamicIO: boolean
navigationDeniedApi: boolean
configFileName: string
segments: AppSegment[]
distDir: string
Expand Down Expand Up @@ -1312,6 +1314,7 @@ export async function buildAppStaticPaths({
experimental: {
after: false,
dynamicIO,
navigationDeniedApi,
},
buildId,
},
Expand Down Expand Up @@ -1487,6 +1490,7 @@ export async function isPageStatic({
edgeInfo,
pageType,
dynamicIO,
navigationDeniedApi,
originalAppPath,
isrFlushToDisk,
maxMemoryCacheSize,
Expand All @@ -1501,6 +1505,7 @@ export async function isPageStatic({
page: string
distDir: string
dynamicIO: boolean
navigationDeniedApi: boolean
configFileName: string
runtimeEnvConfig: any
httpAgentOptions: NextConfigComplete['httpAgentOptions']
Expand Down Expand Up @@ -1642,6 +1647,7 @@ export async function isPageStatic({
dir,
page,
dynamicIO,
navigationDeniedApi,
configFileName,
segments,
distDir,
Expand Down
52 changes: 37 additions & 15 deletions packages/next/src/build/webpack/loaders/next-app-loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,30 @@ export type AppLoaderOptions = {
}
type AppLoader = webpack.LoaderDefinitionFunction<AppLoaderOptions>

const HTTP_ACCESS_FALLBACKS = {
'not-found': 'not-found',
forbidden: 'forbidden',
unauthorized: 'unauthorized',
} as const
const defaultHTTPAccessFallbackPaths = {
'not-found': 'next/dist/client/components/not-found-error',
forbidden: 'next/dist/client/components/forbidden-error',
unauthorized: 'next/dist/client/components/unauthorized-error',
} as const

const FILE_TYPES = {
layout: 'layout',
template: 'template',
error: 'error',
loading: 'loading',
'not-found': 'not-found',
forbidden: 'forbidden',
unauthorized: 'unauthorized',
'global-error': 'global-error',
...HTTP_ACCESS_FALLBACKS,
} as const

const GLOBAL_ERROR_FILE_TYPE = 'global-error'
const PAGE_SEGMENT = 'page$'
const PARALLEL_CHILDREN_SEGMENT = 'children$'

const defaultNotFoundPath = 'next/dist/client/components/not-found-error'
const defaultGlobalErrorPath = 'next/dist/client/components/error-boundary'
const defaultLayoutPath = 'next/dist/client/components/default-layout'

Expand Down Expand Up @@ -144,9 +152,6 @@ async function createTreeCodeFromPath(

const isDefaultNotFound = isAppBuiltinNotFoundPage(pagePath)
const appDirPrefix = isDefaultNotFound ? APP_DIR_ALIAS : splittedPath[0]
const hasRootNotFound = await resolver(
`${appDirPrefix}/${FILE_TYPES['not-found']}`
)
const pages: string[] = []

let rootLayout: string | undefined
Expand Down Expand Up @@ -304,18 +309,35 @@ async function createTreeCodeFromPath(
return false
}) as [ValueOf<typeof FILE_TYPES>, string][]

// Add default not found error as root not found if not present
const hasNotFoundFile = definedFilePaths.some(
([type]) => type === 'not-found'
// Add default access fallback as root fallback if not present
const existedConventionNames = new Set(
definedFilePaths.map(([type]) => type)
)
// If the first layer is a group route, we treat it as root layer
const isFirstLayerGroupRoute =
segments.length === 1 &&
subSegmentPath.filter((seg) => isGroupSegment(seg)).length === 1
if ((isRootLayer || isFirstLayerGroupRoute) && !hasNotFoundFile) {
// If you already have a root not found, don't insert default not-found to group routes root
if (!(hasRootNotFound && isFirstLayerGroupRoute)) {
definedFilePaths.push(['not-found', defaultNotFoundPath])

if (isRootLayer || isFirstLayerGroupRoute) {
const accessFallbackTypes = Object.keys(
defaultHTTPAccessFallbackPaths
) as (keyof typeof defaultHTTPAccessFallbackPaths)[]
for (const type of accessFallbackTypes) {
const hasRootFallbackFile = await resolver(
`${appDirPrefix}/${FILE_TYPES[type]}`
)
const hasLayerFallbackFile = existedConventionNames.has(type)

// If you already have a root access error fallback, don't insert default access error boundary to group routes root
if (
// Is treated as root layout and without boundary
!(hasRootFallbackFile && isFirstLayerGroupRoute) &&
// Does not have a fallback boundary file
!hasLayerFallbackFile
) {
const defaultFallbackPath = defaultHTTPAccessFallbackPaths[type]
definedFilePaths.push([type, defaultFallbackPath])
}
}
}

Expand Down Expand Up @@ -360,7 +382,7 @@ async function createTreeCodeFromPath(
if (isNotFoundRoute && normalizedParallelKey === 'children') {
const notFoundPath =
definedFilePaths.find(([type]) => type === 'not-found')?.[1] ??
defaultNotFoundPath
defaultHTTPAccessFallbackPaths['not-found']

const varName = `notFound${nestedCollectedDeclarations.length}`
nestedCollectedDeclarations.push([varName, notFoundPath])
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/build/webpack/plugins/define-env-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export function getDefineEnv({
// Internal only so untyped to avoid discovery
(config.experimental as any).internal_disableSyncDynamicAPIWarnings ??
false,
'process.env.__NEXT_EXPERIMENTAL_NAVIGATION_API':
!!config.experimental.navigationDeniedApi,
...(isNodeOrEdgeCompilation
? {
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/components/forbidden-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function Forbidden() {
return (
<HTTPAccessErrorFallback
status={403}
message="This page could not be accessed"
message="This page could not be accessed."
/>
)
}
8 changes: 2 additions & 6 deletions packages/next/src/client/components/forbidden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,9 @@ import {
* Read more: [Next.js Docs: `forbidden`](https://nextjs.org/docs/app/api-reference/functions/not-found)
*/
export function forbidden(): never {
if (
!process.env.__NEXT_VERSION?.includes('canary') &&
!process.env.__NEXT_TEST_MODE &&
!process.env.NEXT_PRIVATE_SKIP_CANARY_CHECK
) {
if (!process.env.__NEXT_EXPERIMENTAL_NAVIGATION_API) {
throw new Error(
`\`forbidden()\` is experimental and only allowed to be used in canary builds.`
`\`forbidden()\` is experimental and only allowed to be enabled when \`experimental.navigationDeniedApi\` is enabled.`
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,30 +110,35 @@ class HTTPAccessFallbackErrorBoundary extends React.Component<
}

render() {
const { notFound, forbidden, unauthorized } = this.props
const { notFound, forbidden, unauthorized, children } = this.props
const { triggeredStatus } = this.state
if (triggeredStatus) {
const isNotFound =
triggeredStatus === HTTPAccessErrorStatus.NOT_FOUND && notFound
const isForbidden =
triggeredStatus === HTTPAccessErrorStatus.FORBIDDEN && forbidden
const isUnauthorized =
triggeredStatus === HTTPAccessErrorStatus.UNAUTHORIZED && unauthorized

// If there's no matched boundary in this layer, keep throwing the error by rendering the children
if (!(isNotFound || isForbidden || isUnauthorized)) {
return children
}

return (
<>
<meta name="robots" content="noindex" />
{process.env.NODE_ENV === 'development' && (
<meta name="next-error" content="not-found" />
)}
{triggeredStatus === HTTPAccessErrorStatus.NOT_FOUND && notFound
? notFound
: null}
{triggeredStatus === HTTPAccessErrorStatus.FORBIDDEN && forbidden
? forbidden
: null}
{triggeredStatus === HTTPAccessErrorStatus.UNAUTHORIZED &&
unauthorized
? unauthorized
: null}
{isNotFound ? notFound : null}
{isForbidden ? forbidden : null}
{isUnauthorized ? unauthorized : null}
</>
)
}

return this.props.children
return children
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ export function getAccessFallbackHTTPStatus(

export function getAccessFallbackErrorTypeByStatus(
status: number
): 'not-found' | undefined {
// TODO: support 403 and 401
): 'not-found' | 'forbidden' | 'unauthorized' | undefined {
switch (status) {
case 401:
return 'unauthorized'
case 403:
return 'forbidden'
case 404:
return 'not-found'
default:
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/components/unauthorized-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function Unauthorized() {
return (
<HTTPAccessErrorFallback
status={401}
message="You're not authorized to access this page"
message="You're not authorized to access this page."
/>
)
}
8 changes: 2 additions & 6 deletions packages/next/src/client/components/unauthorized.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,9 @@ import {
* Read more: [Next.js Docs: `unauthorized`](https://nextjs.org/docs/app/api-reference/functions/not-found)
*/
export function unauthorized(): never {
if (
!process.env.__NEXT_VERSION?.includes('canary') &&
!process.env.__NEXT_TEST_MODE &&
!process.env.NEXT_PRIVATE_SKIP_CANARY_CHECK
) {
if (!process.env.__NEXT_EXPERIMENTAL_NAVIGATION_API) {
throw new Error(
`\`unauthorized()\` is experimental and only allowed to be used in canary builds.`
`\`unauthorized()\` is experimental and only allowed to be used when \`experimental.navigationDeniedApi\` is enabled.`
)
}

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ async function exportAppImpl(
after: nextConfig.experimental.after ?? false,
dynamicIO: nextConfig.experimental.dynamicIO ?? false,
inlineCss: nextConfig.experimental.inlineCss ?? false,
navigationDeniedApi: !!nextConfig.experimental.navigationDeniedApi,
},
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,
}
Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/export/routes/app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export async function exportAppRoute(
},
htmlFilepath: string,
fileWriter: FileWriter,
experimental: Required<Pick<ExperimentalConfig, 'after' | 'dynamicIO'>>,
experimental: Required<
Pick<ExperimentalConfig, 'after' | 'dynamicIO' | 'navigationDeniedApi'>
>,
buildId: string
): Promise<ExportRouteResult> {
// Ensure that the URL is absolute.
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ type ViewportResolver = (
parent: ResolvingViewport
) => Viewport | Promise<Viewport>

export type MetadataErrorType = 'not-found'
export type MetadataErrorType = 'not-found' | 'forbidden' | 'unauthorized'

export type MetadataItems = [
Metadata | MetadataResolver | null,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ async function getRSCPayload(
getMetadataReady,
missingSlots,
preloadCallbacks,
navigationDeniedApi: ctx.renderOpts.experimental.navigationDeniedApi,
})

// When the `vary` response header is present with `Next-URL`, that means there's a chance
Expand Down
Loading

0 comments on commit cab2e9d

Please sign in to comment.