From a31b3b194382707e411ac837721ba38ef2417d7a Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 31 Aug 2022 14:34:22 +0200 Subject: [PATCH 01/13] Bypass empty pages folder for layouts --- packages/next/build/entries.ts | 6 +- packages/next/build/index.ts | 35 ++++++---- packages/next/build/utils.ts | 5 +- packages/next/build/webpack-config.ts | 10 +-- packages/next/build/webpack/config/index.ts | 2 +- packages/next/build/webpack/config/utils.ts | 2 +- .../webpack/plugins/react-loadable-plugin.ts | 11 ++- packages/next/cli/next-lint.ts | 23 +++---- packages/next/lib/eslint/runLintCheck.ts | 67 +++++++++++++------ packages/next/lib/find-pages-dir.ts | 22 +++--- packages/next/lib/verifyAndLint.ts | 11 ++- packages/next/server/dev/hot-reloader.ts | 52 +++++++------- packages/next/server/dev/next-dev-server.ts | 37 +++++++--- .../server/dev/on-demand-entry-handler.ts | 10 +-- test/e2e/app-dir/app-rendering/pages/.gitkeep | 0 test/e2e/app-dir/rsc-basic.test.ts | 4 +- .../non-isomorphic-text/browser.js | 0 .../non-isomorphic-text/index.mjs | 0 .../non-isomorphic-text/package.json | 0 .../random-module-instance/index.js | 0 .../random-module-instance/package.json | 0 test/e2e/app-dir/rsc-basic/pages/.gitkeep | 0 22 files changed, 185 insertions(+), 112 deletions(-) delete mode 100644 test/e2e/app-dir/app-rendering/pages/.gitkeep rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/non-isomorphic-text/browser.js (100%) rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/non-isomorphic-text/index.mjs (100%) rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/non-isomorphic-text/package.json (100%) rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/random-module-instance/index.js (100%) rename test/e2e/app-dir/rsc-basic/{node_modules_bak => node_modules}/random-module-instance/package.json (100%) delete mode 100644 test/e2e/app-dir/rsc-basic/pages/.gitkeep diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 65b62563154..8b1b88db38c 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -149,7 +149,7 @@ interface CreateEntrypointsParams { envFiles: LoadedEnvFiles isDev?: boolean pages: { [page: string]: string } - pagesDir: string + pagesDir?: string previewMode: __ApiPreviewProps rootDir: string rootPaths?: Record @@ -366,7 +366,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { // Handle paths that have aliases const pageFilePath = (() => { - if (absolutePagePath.startsWith(PAGES_DIR_ALIAS)) { + if (pagesDir && absolutePagePath.startsWith(PAGES_DIR_ALIAS)) { return absolutePagePath.replace(PAGES_DIR_ALIAS, pagesDir) } @@ -490,7 +490,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { await Promise.all(Object.keys(pages).map(getEntryHandler(pages, 'pages'))) if (nestedMiddleware.length > 0) { - throw new NestedMiddlewareError(nestedMiddleware, rootDir, pagesDir) + throw new NestedMiddlewareError(nestedMiddleware, rootDir, pagesDir!) } return { diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index f9097ff1647..ea5fa1a21e2 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -314,7 +314,9 @@ export default async function build( eventCliSession(dir, config, { webpackVersion: 5, cliCommand: 'build', - isSrcDir: path.relative(dir, pagesDir!).startsWith('src'), + isSrcDir: pagesDir + ? path.relative(dir, pagesDir).startsWith('src') + : false, hasNowJson: !!(await findUp('now.json', { cwd: dir })), isCustomServer: null, }) @@ -395,7 +397,8 @@ export default async function build( config.eslint?.dirs, config.experimental.cpus, config.experimental.workerThreads, - telemetry + telemetry, + !!config.experimental.appDir ) }), ]) @@ -436,14 +439,16 @@ export default async function build( const isLikeServerless = isTargetLikeServerless(target) - const pagesPaths = await nextBuildSpan - .traceChild('collect-pages') - .traceAsyncFn(() => - recursiveReadDir( - pagesDir, - new RegExp(`\\.(?:${config.pageExtensions.join('|')})$`) - ) - ) + const pagesPaths = pagesDir + ? await nextBuildSpan + .traceChild('collect-pages') + .traceAsyncFn(() => + recursiveReadDir( + pagesDir, + new RegExp(`\\.(?:${config.pageExtensions.join('|')})$`) + ) + ) + : [] let appPaths: string[] | undefined @@ -462,9 +467,11 @@ export default async function build( `^${MIDDLEWARE_FILENAME}\\.(?:${config.pageExtensions.join('|')})$` ) - const rootPaths = ( - await flatReaddir(join(pagesDir, '..'), middlewareDetectionRegExp) - ).map((absoluteFile) => absoluteFile.replace(dir, '')) + const rootPaths = pagesDir + ? ( + await flatReaddir(join(pagesDir, '..'), middlewareDetectionRegExp) + ).map((absoluteFile) => absoluteFile.replace(dir, '')) + : [] // needed for static exporting since we want to replace with HTML // files @@ -1256,7 +1263,7 @@ export default async function build( : appPaths?.find((p) => p.startsWith(actualPage + '/page.')) const pageRuntime = - pageType === 'pages' && pagePath + pagesDir && pageType === 'pages' && pagePath ? ( await getPageStaticInfo({ pageFilePath: join(pagesDir, pagePath), diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index e5cdc0f9392..87d29f5b1ba 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -305,7 +305,7 @@ export async function printTreeView( }: { distPath: string buildId: string - pagesDir: string + pagesDir?: string pageExtensions: string[] buildManifest: BuildManifest appBuildManifest?: AppBuildManifest @@ -345,7 +345,8 @@ export async function printTreeView( .replace(/(?:^|[.-])([0-9a-z]{6})[0-9a-z]{14}(?=\.)/, '.$1') // Check if we have a custom app. - const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions) + const hasCustomApp = + pagesDir && (await findPageFile(pagesDir, '/_app', pageExtensions)) const filterAndSortList = (list: ReadonlyArray) => list diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index d47806b40c2..6cf37a92c2f 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -518,7 +518,7 @@ export default async function getBaseWebpackConfig( entrypoints: webpack.EntryObject hasReactRoot: boolean isDevFallback?: boolean - pagesDir: string + pagesDir?: string reactProductionProfiling?: boolean rewrites: CustomRoutes['rewrites'] runWebpackSpan: Span @@ -792,7 +792,7 @@ export default async function getBaseWebpackConfig( const customDocumentAliases: { [key: string]: string[] } = {} const customRootAliases: { [key: string]: string[] } = {} - if (dev) { + if (dev && pagesDir) { customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [ ...rawPageExtensions.reduce((prev, ext) => { prev.push(path.join(pagesDir, `_app.${ext}`)) @@ -850,7 +850,7 @@ export default async function getBaseWebpackConfig( ...customDocumentAliases, ...customRootAliases, - [PAGES_DIR_ALIAS]: pagesDir, + ...(pagesDir ? { [PAGES_DIR_ALIAS]: pagesDir } : {}), ...(appDir ? { [APP_DIR_ALIAS]: appDir, @@ -2051,7 +2051,9 @@ export default async function getBaseWebpackConfig( webpackConfig = await buildConfiguration(webpackConfig, { supportedBrowsers, rootDirectory: dir, - customAppFile: new RegExp(escapeStringRegexp(path.join(pagesDir, `_app`))), + customAppFile: pagesDir + ? new RegExp(escapeStringRegexp(path.join(pagesDir, `_app`))) + : undefined, isDevelopment: dev, isServer: isNodeServer || isEdgeServer, isEdgeRuntime: isEdgeServer, diff --git a/packages/next/build/webpack/config/index.ts b/packages/next/build/webpack/config/index.ts index 8da928de600..201403bae8c 100644 --- a/packages/next/build/webpack/config/index.ts +++ b/packages/next/build/webpack/config/index.ts @@ -25,7 +25,7 @@ export async function build( }: { supportedBrowsers: string[] | undefined rootDirectory: string - customAppFile: RegExp + customAppFile: RegExp | undefined isDevelopment: boolean isServer: boolean isEdgeRuntime: boolean diff --git a/packages/next/build/webpack/config/utils.ts b/packages/next/build/webpack/config/utils.ts index d2cdbb1277a..b8355ad52ef 100644 --- a/packages/next/build/webpack/config/utils.ts +++ b/packages/next/build/webpack/config/utils.ts @@ -4,7 +4,7 @@ import type { NextConfigComplete } from '../../../server/config-shared' export type ConfigurationContext = { supportedBrowsers: string[] | undefined rootDirectory: string - customAppFile: RegExp + customAppFile: RegExp | undefined isDevelopment: boolean isProduction: boolean diff --git a/packages/next/build/webpack/plugins/react-loadable-plugin.ts b/packages/next/build/webpack/plugins/react-loadable-plugin.ts index aacb383b5dd..2677a6c9465 100644 --- a/packages/next/build/webpack/plugins/react-loadable-plugin.ts +++ b/packages/next/build/webpack/plugins/react-loadable-plugin.ts @@ -53,9 +53,14 @@ function getChunkGroupFromBlock( function buildManifest( _compiler: webpack.Compiler, compilation: webpack.Compilation, - pagesDir: string, + pagesDir: string | undefined, dev: boolean ) { + // If there's no pagesDir, output an empty manifest + if (!pagesDir) { + return {} + } + let manifest: { [k: string]: { id: string | number; files: string[] } } = {} // This is allowed: @@ -145,13 +150,13 @@ function buildManifest( export class ReactLoadablePlugin { private filename: string - private pagesDir: string + private pagesDir?: string private runtimeAsset?: string private dev: boolean constructor(opts: { filename: string - pagesDir: string + pagesDir?: string runtimeAsset?: string dev: boolean }) { diff --git a/packages/next/cli/next-lint.ts b/packages/next/cli/next-lint.ts index c25b340e238..ca0d1e5246a 100755 --- a/packages/next/cli/next-lint.ts +++ b/packages/next/cli/next-lint.ts @@ -92,11 +92,11 @@ const nextLint: cliCommand = async (argv) => { printAndExit( ` Description - Run ESLint on every file in specified directories. + Run ESLint on every file in specified directories. If not configured, ESLint will be set up for the first time. Usage - $ next lint [options] + $ next lint [options] represents the directory of the Next.js application. If no directory is provided, the current directory will be used. @@ -127,7 +127,7 @@ const nextLint: cliCommand = async (argv) => { Handling warnings: --quiet Report errors only - default: false --max-warnings Int Number of warnings to trigger nonzero exit code - default: -1 - + Output: -o, --output-file path::String Specify file to write report to -f, --format String Use a specific output format - default: Next.js custom formatter @@ -140,7 +140,7 @@ const nextLint: cliCommand = async (argv) => { --no-cache Disable caching --cache-location path::String Path to the cache file or directory - default: .eslintcache --cache-strategy String Strategy to use for detecting changed files in the cache, either metadata or content - default: metadata - + Miscellaneous: --error-on-unmatched-pattern Show errors when any file patterns are unmatched - default: false `, @@ -179,17 +179,16 @@ const nextLint: cliCommand = async (argv) => { const distDir = join(baseDir, nextConfig.distDir) const defaultCacheLocation = join(distDir, 'cache', 'eslint/') - runLintCheck( - baseDir, - pathsToLint, - false, - eslintOptions(args, defaultCacheLocation), - reportErrorsOnly, + runLintCheck(baseDir, pathsToLint, { + lintDuringBuild: false, + eslintOptions: eslintOptions(args, defaultCacheLocation), + reportErrorsOnly: reportErrorsOnly, maxWarnings, formatter, outputFile, - strict - ) + strict, + hasAppDir: !!nextConfig.experimental.appDir, + }) .then(async (lintResults) => { const lintOutput = typeof lintResults === 'string' ? lintResults : lintResults?.output diff --git a/packages/next/lib/eslint/runLintCheck.ts b/packages/next/lib/eslint/runLintCheck.ts index 8f349ac2ed5..9799f673307 100644 --- a/packages/next/lib/eslint/runLintCheck.ts +++ b/packages/next/lib/eslint/runLintCheck.ts @@ -83,12 +83,22 @@ async function lint( lintDirs: string[], eslintrcFile: string | null, pkgJsonPath: string | null, - lintDuringBuild: boolean = false, - eslintOptions: any = null, - reportErrorsOnly: boolean = false, - maxWarnings: number = -1, - formatter: string | null = null, - outputFile: string | null = null + hasAppDir: boolean, + { + lintDuringBuild = false, + eslintOptions = null, + reportErrorsOnly = false, + maxWarnings = -1, + formatter = null, + outputFile = null, + }: { + lintDuringBuild: boolean + eslintOptions: any + reportErrorsOnly: boolean + maxWarnings: number + formatter: string | null + outputFile: string | null + } ): Promise< | string | null @@ -176,8 +186,7 @@ async function lint( } } - // TODO: should we apply these rules to "root" dir as well? - const pagesDir = findPagesDir(baseDir).pages + const pagesDir = findPagesDir(baseDir, hasAppDir).pages if (nextEslintPluginIsEnabled) { let updatedPagesDir = false @@ -268,14 +277,27 @@ async function lint( export async function runLintCheck( baseDir: string, lintDirs: string[], - lintDuringBuild: boolean = false, - eslintOptions: any = null, - reportErrorsOnly: boolean = false, - maxWarnings: number = -1, - formatter: string | null = null, - outputFile: string | null = null, - strict: boolean = false + opts: { + lintDuringBuild?: boolean + eslintOptions?: any + reportErrorsOnly?: boolean + maxWarnings?: number + formatter?: string | null + outputFile?: string | null + strict?: boolean + hasAppDir: boolean + } ): ReturnType { + const { + lintDuringBuild = false, + eslintOptions = null, + reportErrorsOnly = false, + maxWarnings = -1, + formatter = null, + outputFile = null, + strict = false, + hasAppDir, + } = opts try { // Find user's .eslintrc file // See: https://eslint.org/docs/user-guide/configuring/configuration-files#configuration-file-formats @@ -313,12 +335,15 @@ export async function runLintCheck( lintDirs, eslintrcFile, pkgJsonPath, - lintDuringBuild, - eslintOptions, - reportErrorsOnly, - maxWarnings, - formatter, - outputFile + hasAppDir, + { + lintDuringBuild, + eslintOptions, + reportErrorsOnly, + maxWarnings, + formatter, + outputFile, + } ) } else { // Display warning if no ESLint configuration is present inside diff --git a/packages/next/lib/find-pages-dir.ts b/packages/next/lib/find-pages-dir.ts index 61592f9e4f9..c8421267312 100644 --- a/packages/next/lib/find-pages-dir.ts +++ b/packages/next/lib/find-pages-dir.ts @@ -24,23 +24,29 @@ function findDir(dir: string, name: 'pages' | 'app'): string | null { export function findPagesDir( dir: string, appDirEnabled?: boolean -): { pages: string; appDir?: string } { - const pagesDir = findDir(dir, 'pages') +): { pages: string | undefined; appDir: string | undefined } { + const pagesDir = findDir(dir, 'pages') || undefined let appDir: undefined | string if (appDirEnabled) { appDir = findDir(dir, 'app') || undefined + if (appDirEnabled == null && pagesDir == null) { + throw new Error( + "> Couldn't find any `pages` or `app` directory. Please create one under the project root" + ) + } } - // TODO: allow "root" dir without pages dir - if (pagesDir === null) { - throw new Error( - "> Couldn't find a `pages` directory. Please create one under the project root" - ) + if (!appDirEnabled) { + if (pagesDir == null) { + throw new Error( + "> Couldn't find a `pages` directory. Please create one under the project root" + ) + } } return { pages: pagesDir, - appDir, + appDir: appDir, } } diff --git a/packages/next/lib/verifyAndLint.ts b/packages/next/lib/verifyAndLint.ts index 76f384db10d..b5e47d423f8 100644 --- a/packages/next/lib/verifyAndLint.ts +++ b/packages/next/lib/verifyAndLint.ts @@ -14,7 +14,8 @@ export async function verifyAndLint( configLintDirs: string[] | undefined, numWorkers: number | undefined, enableWorkerThreads: boolean | undefined, - telemetry: Telemetry + telemetry: Telemetry, + hasAppDir: boolean ): Promise { try { const lintWorkers = new Worker(require.resolve('./eslint/runLintCheck'), { @@ -38,8 +39,12 @@ export async function verifyAndLint( [] ) - const lintResults = await lintWorkers.runLintCheck(dir, lintDirs, true, { - cacheLocation, + const lintResults = await lintWorkers.runLintCheck(dir, lintDirs, { + hasAppDir, + lintDuringBuild: true, + eslintOptions: { + cacheLocation, + }, }) const lintOutput = typeof lintResults === 'string' ? lintResults : lintResults?.output diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 76aa4510599..5dd7ba50224 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -159,7 +159,7 @@ export default class HotReloader { private dir: string private buildId: string private interceptors: any[] - private pagesDir: string + private pagesDir?: string private distDir: string private webpackHotMiddleware?: WebpackHotMiddleware private config: NextConfigComplete @@ -197,7 +197,7 @@ export default class HotReloader { appDir, }: { config: NextConfigComplete - pagesDir: string + pagesDir?: string distDir: string buildId: string previewProps: __ApiPreviewProps @@ -405,28 +405,30 @@ export default class HotReloader { : this.config.pageExtensions return webpackConfigSpan.traceAsyncFn(async () => { - const pagePaths = await webpackConfigSpan - .traceChild('get-page-paths') - .traceAsyncFn(() => - Promise.all([ - findPageFile(this.pagesDir, '/_app', rawPageExtensions), - findPageFile(this.pagesDir, '/_document', rawPageExtensions), - ]) - ) + const pagePaths = !this.pagesDir + ? ([null, null] as [null, null]) + : await webpackConfigSpan + .traceChild('get-page-paths') + .traceAsyncFn(() => + Promise.all([ + findPageFile(this.pagesDir!, '/_app', rawPageExtensions), + findPageFile(this.pagesDir!, '/_document', rawPageExtensions), + ]) + ) - this.pagesMapping = webpackConfigSpan - .traceChild('create-pages-mapping') - .traceFn(() => - createPagesMapping({ - hasServerComponents: this.hasServerComponents, - isDev: true, - pageExtensions: this.config.pageExtensions, - pagesType: 'pages', - pagePaths: pagePaths.filter( - (i): i is string => typeof i === 'string' - ), - }) - ) + this.pagesMapping = !this.pagesDir + ? {} + : webpackConfigSpan.traceChild('create-pages-mapping').traceFn(() => + createPagesMapping({ + hasServerComponents: this.hasServerComponents, + isDev: true, + pageExtensions: this.config.pageExtensions, + pagesType: 'pages', + pagePaths: pagePaths.filter( + (i: string | null): i is string => typeof i === 'string' + ), + }) + ) const entrypoints = await webpackConfigSpan .traceChild('create-entrypoints') @@ -848,6 +850,10 @@ export default class HotReloader { this.serverError = null this.serverStats = stats + if (!this.pagesDir) { + return + } + const { compilation } = stats // We only watch `_document` for changes on the server compilation diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 0452eea5b6a..589ad20eb14 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -102,7 +102,7 @@ export default class DevServer extends Server { private isCustomServer: boolean protected sortedRoutes?: string[] private addedUpgradeListener = false - private pagesDir: string + private pagesDir?: string private appDir?: string private actualMiddlewareFile?: string private middleware?: MiddlewareRoutingItem @@ -259,6 +259,13 @@ export default class DevServer extends Server { let resolved = false return new Promise(async (resolve, reject) => { + if (!this.pagesDir) { + if (!resolved) { + resolve() + resolved = true + } + return + } // Watchpack doesn't emit an event for an empty directory fs.readdir(this.pagesDir, (_, files) => { if (files?.length) { @@ -380,7 +387,7 @@ export default class DevServer extends Server { } let pageName = absolutePathToPage(fileName, { - pagesDir: isAppPath ? this.appDir! : this.pagesDir, + pagesDir: isAppPath ? this.appDir! : this.pagesDir!, extensions: this.nextConfig.pageExtensions, keepIndex: isAppPath, }) @@ -529,8 +536,11 @@ export default class DevServer extends Server { if (nestedMiddleware.length > 0) { Log.error( - new NestedMiddlewareError(nestedMiddleware, this.dir, this.pagesDir) - .message + new NestedMiddlewareError( + nestedMiddleware, + this.dir, + this.pagesDir! + ).message ) nestedMiddleware = [] } @@ -679,7 +689,9 @@ export default class DevServer extends Server { eventCliSession(this.distDir, this.nextConfig, { webpackVersion: 5, cliCommand: 'dev', - isSrcDir: relative(this.dir, this.pagesDir).startsWith('src'), + isSrcDir: this.pagesDir + ? relative(this.dir, this.pagesDir).startsWith('src') + : false, hasNowJson: !!(await findUp('now.json', { cwd: this.dir })), isCustomServer: this.isCustomServer, }) @@ -735,12 +747,15 @@ export default class DevServer extends Server { if (pageFile) return true } - const pageFile = await findPageFile( - this.pagesDir, - normalizedPath, - this.nextConfig.pageExtensions - ) - return !!pageFile + if (this.pagesDir) { + const pageFile = await findPageFile( + this.pagesDir, + normalizedPath, + this.nextConfig.pageExtensions + ) + return !!pageFile + } + return false } protected async _beforeCatchAllRender( diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 09570760812..7132b07f223 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -276,9 +276,9 @@ function tryToNormalizePagePath(page: string) { */ async function findPagePathData( rootDir: string, - pagesDir: string, page: string, extensions: string[], + pagesDir?: string, appDir?: string ) { const normalizedPagePath = tryToNormalizePagePath(page) @@ -323,11 +323,11 @@ async function findPagePathData( } } - if (!pagePath) { + if (!pagePath && pagesDir) { pagePath = await findPageFile(pagesDir, normalizedPagePath, extensions) } - if (pagePath !== null) { + if (pagePath !== null && pagesDir) { const pageUrl = ensureLeadingSlash( removePagePathTail(normalizePathSep(pagePath), { extensions, @@ -365,7 +365,7 @@ export function onDemandEntryHandler({ multiCompiler: webpack.MultiCompiler nextConfig: NextConfigComplete pagesBufferLength: number - pagesDir: string + pagesDir?: string rootDir: string appDir?: string }) { @@ -545,9 +545,9 @@ export function onDemandEntryHandler({ try { const pagePathData = await findPagePathData( rootDir, - pagesDir, page, nextConfig.pageExtensions, + pagesDir, appDir ) diff --git a/test/e2e/app-dir/app-rendering/pages/.gitkeep b/test/e2e/app-dir/app-rendering/pages/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index 09dd011d522..a2b3a80cf09 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -38,7 +38,7 @@ describe('app dir - react server components', () => { const appDir = path.join(__dirname, './rsc-basic') next = await createNext({ files: { - node_modules_bak: new FileRef(path.join(appDir, 'node_modules_bak')), + node_modules: new FileRef(path.join(appDir, 'node_modules')), pages: new FileRef(path.join(appDir, 'pages')), public: new FileRef(path.join(appDir, 'public')), components: new FileRef(path.join(appDir, 'components')), @@ -52,12 +52,14 @@ describe('app dir - react server components', () => { }, packageJson: { scripts: { + preinstall: 'mv node_modules node_modules_bak', setup: `cp -r ./node_modules_bak/non-isomorphic-text ./node_modules; cp -r ./node_modules_bak/random-module-instance ./node_modules`, build: 'yarn setup && next build', dev: 'yarn setup && next dev', start: 'next start', }, }, + installCommand: 'yarn', startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start', buildCommand: 'yarn build', }) diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/browser.js b/test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/browser.js similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/browser.js rename to test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/browser.js diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/index.mjs b/test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/index.mjs similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/index.mjs rename to test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/index.mjs diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/package.json b/test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/package.json similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/package.json rename to test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/package.json diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/index.js b/test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/index.js similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/index.js rename to test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/index.js diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/package.json b/test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/package.json similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/package.json rename to test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/package.json diff --git a/test/e2e/app-dir/rsc-basic/pages/.gitkeep b/test/e2e/app-dir/rsc-basic/pages/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 From 250f98a42d0ed14c40e237eaae180843735c4279 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 31 Aug 2022 14:49:39 +0200 Subject: [PATCH 02/13] fix test --- test/e2e/app-dir/rendering.test.ts | 1 - test/e2e/app-dir/rsc-basic.test.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/e2e/app-dir/rendering.test.ts b/test/e2e/app-dir/rendering.test.ts index 10ec1b72df4..69f1f0dd385 100644 --- a/test/e2e/app-dir/rendering.test.ts +++ b/test/e2e/app-dir/rendering.test.ts @@ -22,7 +22,6 @@ describe('app dir rendering', () => { next = await createNext({ files: { app: new FileRef(path.join(__dirname, 'app-rendering/app')), - pages: new FileRef(path.join(__dirname, 'app-rendering/pages')), 'next.config.js': new FileRef( path.join(__dirname, 'app-rendering/next.config.js') ), diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index a2b3a80cf09..de77941c10b 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -39,7 +39,6 @@ describe('app dir - react server components', () => { next = await createNext({ files: { node_modules: new FileRef(path.join(appDir, 'node_modules')), - pages: new FileRef(path.join(appDir, 'pages')), public: new FileRef(path.join(appDir, 'public')), components: new FileRef(path.join(appDir, 'components')), app: new FileRef(path.join(appDir, 'app')), @@ -169,7 +168,7 @@ describe('app dir - react server components', () => { // expect(modFromClient[1]).not.toBe(modFromServer[1]) }) - it('should be able to navigate between rsc pages', async () => { + it('should be able to navigate between rsc routes', async () => { const browser = await webdriver(next.url, '/root') await browser.waitForElementByCss('#goto-next-link').click() From f988c22f73f1a11dae128683b55983876b731ed2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 31 Aug 2022 15:15:52 +0200 Subject: [PATCH 03/13] revert node_modules_bak --- test/e2e/app-dir/rsc-basic.test.ts | 3 +-- .../non-isomorphic-text/browser.js | 0 .../non-isomorphic-text/index.mjs | 0 .../non-isomorphic-text/package.json | 0 .../random-module-instance/index.js | 0 .../random-module-instance/package.json | 0 6 files changed, 1 insertion(+), 2 deletions(-) rename test/e2e/app-dir/rsc-basic/{node_modules => node_modules_bak}/non-isomorphic-text/browser.js (100%) rename test/e2e/app-dir/rsc-basic/{node_modules => node_modules_bak}/non-isomorphic-text/index.mjs (100%) rename test/e2e/app-dir/rsc-basic/{node_modules => node_modules_bak}/non-isomorphic-text/package.json (100%) rename test/e2e/app-dir/rsc-basic/{node_modules => node_modules_bak}/random-module-instance/index.js (100%) rename test/e2e/app-dir/rsc-basic/{node_modules => node_modules_bak}/random-module-instance/package.json (100%) diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index de77941c10b..0f91f92f807 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -38,7 +38,7 @@ describe('app dir - react server components', () => { const appDir = path.join(__dirname, './rsc-basic') next = await createNext({ files: { - node_modules: new FileRef(path.join(appDir, 'node_modules')), + node_modules_bak: new FileRef(path.join(appDir, 'node_modules_bak')), public: new FileRef(path.join(appDir, 'public')), components: new FileRef(path.join(appDir, 'components')), app: new FileRef(path.join(appDir, 'app')), @@ -51,7 +51,6 @@ describe('app dir - react server components', () => { }, packageJson: { scripts: { - preinstall: 'mv node_modules node_modules_bak', setup: `cp -r ./node_modules_bak/non-isomorphic-text ./node_modules; cp -r ./node_modules_bak/random-module-instance ./node_modules`, build: 'yarn setup && next build', dev: 'yarn setup && next dev', diff --git a/test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/browser.js b/test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/browser.js similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/browser.js rename to test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/browser.js diff --git a/test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/index.mjs b/test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/index.mjs similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/index.mjs rename to test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/index.mjs diff --git a/test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/package.json b/test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/package.json similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules/non-isomorphic-text/package.json rename to test/e2e/app-dir/rsc-basic/node_modules_bak/non-isomorphic-text/package.json diff --git a/test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/index.js b/test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/index.js similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/index.js rename to test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/index.js diff --git a/test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/package.json b/test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/package.json similarity index 100% rename from test/e2e/app-dir/rsc-basic/node_modules/random-module-instance/package.json rename to test/e2e/app-dir/rsc-basic/node_modules_bak/random-module-instance/package.json From db55c6f8143b4291681bbd2b841ccaee81fa65b8 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 31 Aug 2022 18:57:26 +0200 Subject: [PATCH 04/13] Fix app dir index route --- packages/next/build/entries.ts | 9 +-- packages/next/build/utils.ts | 26 +++++-- packages/next/build/webpack-config.ts | 32 +++++---- packages/next/export/worker.ts | 7 +- packages/next/server/base-server.ts | 14 ++-- packages/next/server/dev/hot-reloader.ts | 18 +++-- packages/next/server/dev/next-dev-server.ts | 72 ++++++++++--------- .../server/dev/on-demand-entry-handler.ts | 31 ++++++-- .../next/server/dev/static-paths-worker.ts | 8 ++- packages/next/server/lib/find-page-file.ts | 5 +- packages/next/server/load-components.ts | 35 ++++----- packages/next/server/next-server.ts | 41 +++++++---- packages/next/server/web-server.ts | 5 +- .../shared/lib/page-path/get-page-paths.ts | 16 +++-- test/unit/find-page-file.test.ts | 21 +++++- 15 files changed, 218 insertions(+), 122 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 8b1b88db38c..2d558ae91c1 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -366,7 +366,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { // Handle paths that have aliases const pageFilePath = (() => { - if (pagesDir && absolutePagePath.startsWith(PAGES_DIR_ALIAS)) { + if (absolutePagePath.startsWith(PAGES_DIR_ALIAS) && pagesDir) { return absolutePagePath.replace(PAGES_DIR_ALIAS, pagesDir) } @@ -444,12 +444,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { }) } } else { - server[serverBundlePath] = isServerComponent - ? { - import: mappings[page], - layer: WEBPACK_LAYERS.server, - } - : [mappings[page]] + server[serverBundlePath] = [mappings[page]] } }, onEdgeServer: () => { diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 87d29f5b1ba..fa114b46ef0 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -346,7 +346,7 @@ export async function printTreeView( // Check if we have a custom app. const hasCustomApp = - pagesDir && (await findPageFile(pagesDir, '/_app', pageExtensions)) + pagesDir && (await findPageFile(pagesDir, '/_app', pageExtensions, false)) const filterAndSortList = (list: ReadonlyArray) => list @@ -1082,7 +1082,13 @@ export async function isPageStatic({ getStaticProps: mod.getStaticProps, } } else { - componentsResult = await loadComponents(distDir, page, serverless) + componentsResult = await loadComponents( + distDir, + page, + serverless, + false, + false + ) } const Comp = componentsResult.Component @@ -1208,7 +1214,13 @@ export async function hasCustomGetInitialProps( ): Promise { require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig) - const components = await loadComponents(distDir, page, isLikeServerless) + const components = await loadComponents( + distDir, + page, + isLikeServerless, + false, + false + ) let mod = components.ComponentMod if (checkingApp) { @@ -1227,7 +1239,13 @@ export async function getNamedExports( runtimeEnvConfig: any ): Promise> { require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig) - const components = await loadComponents(distDir, page, isLikeServerless) + const components = await loadComponents( + distDir, + page, + isLikeServerless, + false, + false + ) let mod = components.ComponentMod return Object.keys(mod) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 6cf37a92c2f..02f1012f749 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -792,26 +792,32 @@ export default async function getBaseWebpackConfig( const customDocumentAliases: { [key: string]: string[] } = {} const customRootAliases: { [key: string]: string[] } = {} - if (dev && pagesDir) { + if (dev) { customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [ - ...rawPageExtensions.reduce((prev, ext) => { - prev.push(path.join(pagesDir, `_app.${ext}`)) - return prev - }, [] as string[]), + ...(pagesDir + ? rawPageExtensions.reduce((prev, ext) => { + prev.push(path.join(pagesDir, `_app.${ext}`)) + return prev + }, [] as string[]) + : []), 'next/dist/pages/_app.js', ] customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [ - ...rawPageExtensions.reduce((prev, ext) => { - prev.push(path.join(pagesDir, `_error.${ext}`)) - return prev - }, [] as string[]), + ...(pagesDir + ? rawPageExtensions.reduce((prev, ext) => { + prev.push(path.join(pagesDir, `_error.${ext}`)) + return prev + }, [] as string[]) + : []), 'next/dist/pages/_error.js', ] customDocumentAliases[`${PAGES_DIR_ALIAS}/_document`] = [ - ...rawPageExtensions.reduce((prev, ext) => { - prev.push(path.join(pagesDir, `_document.${ext}`)) - return prev - }, [] as string[]), + ...(pagesDir + ? rawPageExtensions.reduce((prev, ext) => { + prev.push(path.join(pagesDir, `_document.${ext}`)) + return prev + }, [] as string[]) + : []), `next/dist/pages/_document.js`, ] } diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index 298b87cb5fc..bec5097dd79 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -292,8 +292,8 @@ export default async function exportPage({ distDir, page, serverless, - serverComponents, - appDir + !!serverComponents, + !!appDir ) const ampState = { ampFirst: pageConfig?.amp === true, @@ -359,7 +359,8 @@ export default async function exportPage({ distDir, page, serverless, - serverComponents + !!serverComponents, + !!appDir ) const ampState = { ampFirst: components.pageConfig?.amp === true, diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index c162054dc1e..b9ccdd4fe5b 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -241,9 +241,9 @@ export default abstract class Server { protected abstract getFilesystemPaths(): Set protected abstract findPageComponents( pathname: string, - query?: NextParsedUrlQuery, - params?: Params, - isAppDir?: boolean + query: NextParsedUrlQuery, + params: Params, + isAppDir: boolean ): Promise protected abstract getFontManifest(): FontManifest | undefined protected abstract getPrerenderManifest(): PrerenderManifest @@ -1532,7 +1532,7 @@ export default abstract class Server { const result = await this.findPageComponents( page, query, - ctx.renderOpts.params, + ctx.renderOpts.params || {}, typeof appPath === 'string' ) if (result) { @@ -1737,7 +1737,7 @@ export default abstract class Server { // use static 404 page if available and is 404 response if (is404 && (await this.hasPage('/404'))) { - result = await this.findPageComponents('/404', query) + result = await this.findPageComponents('/404', query, {}, false) using404Page = result !== null } let statusPage = `/${res.statusCode}` @@ -1750,12 +1750,12 @@ export default abstract class Server { // skip ensuring /500 in dev mode as it isn't used and the // dev overlay is used instead if (statusPage !== '/500' || !this.renderOpts.dev) { - result = await this.findPageComponents(statusPage, query) + result = await this.findPageComponents(statusPage, query, {}, false) } } if (!result) { - result = await this.findPageComponents('/_error', query) + result = await this.findPageComponents('/_error', query, {}, false) statusPage = '/_error' } diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 5dd7ba50224..d1eca5c8cda 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -272,7 +272,7 @@ export default class HotReloader { if (page === '/_error' || BLOCKED_PAGES.indexOf(page) === -1) { try { - await this.ensurePage(page, true) + await this.ensurePage(page, false, true) } catch (error) { await renderScriptError(pageBundleRes, getProperError(error)) return { finished: true } @@ -406,13 +406,18 @@ export default class HotReloader { return webpackConfigSpan.traceAsyncFn(async () => { const pagePaths = !this.pagesDir - ? ([null, null] as [null, null]) + ? ([] as (string | null)[]) : await webpackConfigSpan .traceChild('get-page-paths') .traceAsyncFn(() => Promise.all([ - findPageFile(this.pagesDir!, '/_app', rawPageExtensions), - findPageFile(this.pagesDir!, '/_document', rawPageExtensions), + findPageFile(this.pagesDir!, '/_app', rawPageExtensions, false), + findPageFile( + this.pagesDir!, + '/_document', + rawPageExtensions, + false + ), ]) ) @@ -1068,7 +1073,8 @@ export default class HotReloader { public async ensurePage( page: string, - clientOnly: boolean = false + isAppDir: boolean, + clientOnly: boolean ): Promise { // Make sure we don't re-build or dispose prebuilt pages if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) { @@ -1080,6 +1086,6 @@ export default class HotReloader { if (error) { return Promise.reject(error) } - return this.onDemandEntries?.ensurePage(page, clientOnly) as any + return this.onDemandEntries?.ensurePage(page, isAppDir, clientOnly) as any } } diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 589ad20eb14..e8a513cd4c1 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -259,35 +259,32 @@ export default class DevServer extends Server { let resolved = false return new Promise(async (resolve, reject) => { - if (!this.pagesDir) { - if (!resolved) { - resolve() - resolved = true - } - return - } - // Watchpack doesn't emit an event for an empty directory - fs.readdir(this.pagesDir, (_, files) => { - if (files?.length) { - return - } + if (this.pagesDir) { + // Watchpack doesn't emit an event for an empty directory + fs.readdir(this.pagesDir, (_, files) => { + if (files?.length) { + return + } - if (!resolved) { - resolve() - resolved = true - } - }) + if (!resolved) { + resolve() + resolved = true + } + }) + } const wp = (this.webpackWatcher = new Watchpack({ ignored: /([/\\]node_modules[/\\]|[/\\]\.next[/\\]|[/\\]\.git[/\\])/, })) - const pages = [this.pagesDir] + const pages = this.pagesDir ? [this.pagesDir] : [] const app = this.appDir ? [this.appDir] : [] const directories = [...pages, ...app] - const files = getPossibleMiddlewareFilenames( - pathJoin(this.pagesDir, '..'), - this.nextConfig.pageExtensions - ) + const files = this.pagesDir + ? getPossibleMiddlewareFilenames( + pathJoin(this.pagesDir, '..'), + this.nextConfig.pageExtensions + ) + : [] let nestedMiddleware: string[] = [] const envFiles = [ @@ -628,7 +625,7 @@ export default class DevServer extends Server { this.verifyingTypeScript = true const verifyResult = await verifyTypeScriptSetup({ dir: this.dir, - intentDirs: [this.pagesDir!, this.appDir].filter(Boolean) as string[], + intentDirs: [this.pagesDir, this.appDir].filter(Boolean) as string[], typeCheckPreflight: false, tsconfigPath: this.nextConfig.typescript.tsconfigPath, disableStaticImages: this.nextConfig.images.disableStaticImages, @@ -733,7 +730,8 @@ export default class DevServer extends Server { return findPageFile( this.dir, normalizedPath, - this.nextConfig.pageExtensions + this.nextConfig.pageExtensions, + false ).then(Boolean) } @@ -742,7 +740,8 @@ export default class DevServer extends Server { const pageFile = await findPageFile( this.appDir, normalizedPath, - this.nextConfig.pageExtensions + this.nextConfig.pageExtensions, + true ) if (pageFile) return true } @@ -751,7 +750,8 @@ export default class DevServer extends Server { const pageFile = await findPageFile( this.pagesDir, normalizedPath, - this.nextConfig.pageExtensions + this.nextConfig.pageExtensions, + false ) return !!pageFile } @@ -1116,11 +1116,15 @@ export default class DevServer extends Server { } protected async ensureMiddleware() { - return this.hotReloader!.ensurePage(this.actualMiddlewareFile!) + return this.hotReloader!.ensurePage( + this.actualMiddlewareFile!, + false, + false + ) } protected async ensureEdgeFunction(pathname: string) { - return this.hotReloader!.ensurePage(pathname) + return this.hotReloader!.ensurePage(pathname, false, false) } generateRoutes() { @@ -1286,14 +1290,14 @@ export default class DevServer extends Server { } protected async ensureApiPage(pathname: string): Promise { - return this.hotReloader!.ensurePage(pathname) + return this.hotReloader!.ensurePage(pathname, false, false) } protected async findPageComponents( pathname: string, - query: ParsedUrlQuery = {}, - params: Params | null = null, - isAppDir: boolean = false + query: ParsedUrlQuery, + params: Params, + isAppDir: boolean ): Promise { await this.devReady const compilationErr = await this.getCompilationError(pathname) @@ -1302,7 +1306,7 @@ export default class DevServer extends Server { throw new WrappedBuildError(compilationErr) } try { - await this.hotReloader!.ensurePage(pathname) + await this.hotReloader!.ensurePage(pathname, isAppDir, false) const serverComponents = this.nextConfig.experimental.serverComponents @@ -1326,7 +1330,7 @@ export default class DevServer extends Server { await this.hotReloader!.buildFallbackError() // Build the error page to ensure the fallback is built too. // TODO: See if this can be moved into hotReloader or removed. - await this.hotReloader!.ensurePage('/_error') + await this.hotReloader!.ensurePage('/_error', false, false) return await loadDefaultErrorComponents(this.distDir) } diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 7132b07f223..bb76b535095 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -22,6 +22,7 @@ import { COMPILER_INDEXES, COMPILER_NAMES, } from '../../shared/lib/constants' +import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' const debug = origDebug('next:on-demand-entry-handler') @@ -255,9 +256,9 @@ function disposeInactiveEntries(maxInactiveAge: number) { }) } -function tryToNormalizePagePath(page: string) { +function tryToNormalizePagePath(page: string, isAppDir: boolean) { try { - return normalizePagePath(page) + return isAppDir ? normalizeAppPath(page) : normalizePagePath(page) } catch (err) { console.error(err) throw new PageNotFoundError(page) @@ -278,14 +279,20 @@ async function findPagePathData( rootDir: string, page: string, extensions: string[], + isAppDir: boolean, pagesDir?: string, appDir?: string ) { - const normalizedPagePath = tryToNormalizePagePath(page) + const normalizedPagePath = tryToNormalizePagePath(page, isAppDir) let pagePath: string | null = null if (isMiddlewareFile(normalizedPagePath)) { - pagePath = await findPageFile(rootDir, normalizedPagePath, extensions) + pagePath = await findPageFile( + rootDir, + normalizedPagePath, + extensions, + false + ) if (!pagePath) { throw new PageNotFoundError(normalizedPagePath) @@ -306,7 +313,7 @@ async function findPagePathData( // Check appDir first falling back to pagesDir if (appDir) { - pagePath = await findPageFile(appDir, normalizedPagePath, extensions) + pagePath = await findPageFile(appDir, normalizedPagePath, extensions, true) if (pagePath) { const pageUrl = ensureLeadingSlash( removePagePathTail(normalizePathSep(pagePath), { @@ -324,7 +331,12 @@ async function findPagePathData( } if (!pagePath && pagesDir) { - pagePath = await findPageFile(pagesDir, normalizedPagePath, extensions) + pagePath = await findPageFile( + pagesDir, + normalizedPagePath, + extensions, + false + ) } if (pagePath !== null && pagesDir) { @@ -534,7 +546,11 @@ export function onDemandEntryHandler({ } return { - async ensurePage(page: string, clientOnly: boolean): Promise { + async ensurePage( + page: string, + isAppDir: boolean, + clientOnly: boolean + ): Promise { const stalledTime = 60 const stalledEnsureTimeout = setTimeout(() => { debug( @@ -547,6 +563,7 @@ export function onDemandEntryHandler({ rootDir, page, nextConfig.pageExtensions, + isAppDir, pagesDir, appDir ) diff --git a/packages/next/server/dev/static-paths-worker.ts b/packages/next/server/dev/static-paths-worker.ts index 240178e2596..185948d7f2d 100644 --- a/packages/next/server/dev/static-paths-worker.ts +++ b/packages/next/server/dev/static-paths-worker.ts @@ -38,7 +38,13 @@ export async function loadStaticPaths( require('../../shared/lib/runtime-config').setConfig(config) setHttpAgentOptions(httpAgentOptions) - const components = await loadComponents(distDir, pathname, serverless) + const components = await loadComponents( + distDir, + pathname, + serverless, + false, + false + ) if (!components.getStaticPaths) { // we shouldn't get to this point since the worker should diff --git a/packages/next/server/lib/find-page-file.ts b/packages/next/server/lib/find-page-file.ts index eab4d18f72c..30b9a32013b 100644 --- a/packages/next/server/lib/find-page-file.ts +++ b/packages/next/server/lib/find-page-file.ts @@ -29,9 +29,10 @@ async function isTrueCasePagePath(pagePath: string, pagesDir: string) { export async function findPageFile( pagesDir: string, normalizedPagePath: string, - pageExtensions: string[] + pageExtensions: string[], + isAppDir: boolean ): Promise { - const pagePaths = getPagePaths(normalizedPagePath, pageExtensions) + const pagePaths = getPagePaths(normalizedPagePath, pageExtensions, isAppDir) const [existingPath, ...others] = ( await Promise.all( pagePaths.map(async (path) => diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index e54139ce051..cf8bddc9f3e 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -63,8 +63,8 @@ export async function loadComponents( distDir: string, pathname: string, serverless: boolean, - hasServerComponents?: boolean, - appDirEnabled?: boolean + hasServerComponents: boolean, + isAppDirRoute: boolean ): Promise { if (serverless) { const ComponentMod = await requirePage(pathname, distDir, serverless) @@ -99,17 +99,21 @@ export async function loadComponents( } as LoadComponentsReturnType } - const [DocumentMod, AppMod, ComponentMod] = await Promise.all([ - Promise.resolve().then(() => - requirePage('/_document', distDir, serverless, appDirEnabled) - ), - Promise.resolve().then(() => - requirePage('/_app', distDir, serverless, appDirEnabled) - ), - Promise.resolve().then(() => - requirePage(pathname, distDir, serverless, appDirEnabled) - ), - ]) + let DocumentMod = {} + let AppMod = {} + if (!isAppDirRoute) { + ;[DocumentMod, AppMod] = await Promise.all([ + Promise.resolve().then(() => + requirePage('/_document', distDir, serverless, false) + ), + Promise.resolve().then(() => + requirePage('/_app', distDir, serverless, false) + ), + ]) + } + const ComponentMod = await Promise.resolve().then(() => + requirePage(pathname, distDir, serverless, isAppDirRoute) + ) const [buildManifest, reactLoadableManifest, serverComponentManifest] = await Promise.all([ @@ -127,15 +131,14 @@ export async function loadComponents( const { getServerSideProps, getStaticProps, getStaticPaths } = ComponentMod let isAppPath = false - - if (appDirEnabled) { + if (isAppDirRoute) { const pagePath = getPagePath( pathname, distDir, serverless, false, undefined, - appDirEnabled + isAppDirRoute ) isAppPath = !!pagePath?.match(/server[/\\]app[/\\]/) } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 48f19bf2e47..055c1c476dd 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -27,7 +27,7 @@ import type { NextConfig } from './config-shared' import type { DynamicRoutes, PageChecker } from './router' import fs from 'fs' -import { join, relative, resolve, sep } from 'path' +import path, { join, relative, resolve, sep } from 'path' import { IncomingMessage, ServerResponse } from 'http' import { addRequestMeta, getRequestMeta } from './request-meta' import { isDynamicRoute } from '../shared/lib/router/utils' @@ -101,6 +101,7 @@ import ResponseCache from './response-cache' import { IncrementalCache } from './lib/incremental-cache' import { interpolateDynamicPath } from '../build/webpack/loaders/next-serverless-loader/utils' import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex' +import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' if (shouldUseReactRoot) { ;(process.env as any).__NEXT_REACT_ROOT = 'true' @@ -247,12 +248,20 @@ export default class NextNodeServer extends BaseServer { if (!options.dev) { // pre-warm _document and _app as these will be // needed for most requests - loadComponents(this.distDir, '/_document', this._isLikeServerless).catch( - () => {} - ) - loadComponents(this.distDir, '/_app', this._isLikeServerless).catch( - () => {} - ) + loadComponents( + this.distDir, + '/_document', + this._isLikeServerless, + false, + false + ).catch(() => {}) + loadComponents( + this.distDir, + '/_app', + this._isLikeServerless, + false, + false + ).catch(() => {}) } } @@ -906,13 +915,17 @@ export default class NextNodeServer extends BaseServer { protected async findPageComponents( pathname: string, - query: NextParsedUrlQuery = {}, - params: Params | null = null, - isAppDir: boolean = false + query: NextParsedUrlQuery, + params: Params, + isAppDir: boolean ): Promise { let paths = [ // try serving a static AMP version first - query.amp ? normalizePagePath(pathname) + '.amp' : null, + query.amp + ? (isAppDir + ? normalizeAppPath(pathname) + : normalizePagePath(pathname)) + '.amp' + : null, pathname, ].filter(Boolean) @@ -931,8 +944,8 @@ export default class NextNodeServer extends BaseServer { this.distDir, pagePath!, !this.renderOpts.dev && this._isLikeServerless, - this.renderOpts.serverComponents, - this.nextConfig.experimental.appDir + !!this.renderOpts.serverComponents, + isAppDir ) if ( @@ -966,6 +979,8 @@ export default class NextNodeServer extends BaseServer { // in the pages-manifest if (!(err instanceof PageNotFoundError)) { throw err + } else if (this.renderOpts.dev) { + console.error(err) } } } diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index 7afff576aea..e3ad0ce87d9 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -406,8 +406,9 @@ export default class NextWebServer extends BaseServer { } protected async findPageComponents( pathname: string, - query?: NextParsedUrlQuery, - params?: Params | null + query: NextParsedUrlQuery, + params: Params | null, + _isAppDir: boolean ) { const result = await this.serverOptions.webServerConfig.loadComponent( pathname diff --git a/packages/next/shared/lib/page-path/get-page-paths.ts b/packages/next/shared/lib/page-path/get-page-paths.ts index 4d4906e3d86..af08b55b9b3 100644 --- a/packages/next/shared/lib/page-path/get-page-paths.ts +++ b/packages/next/shared/lib/page-path/get-page-paths.ts @@ -10,14 +10,22 @@ import { join } from '../isomorphic/path' * @param normalizedPagePath Normalized page path (it will denormalize). * @param extensions Allowed extensions. */ -export function getPagePaths(normalizedPagePath: string, extensions: string[]) { +export function getPagePaths( + normalizedPagePath: string, + extensions: string[], + isAppDir: boolean +) { const page = denormalizePagePath(normalizedPagePath) return flatten( extensions.map((extension) => { - return !normalizedPagePath.endsWith('/index') - ? [`${page}.${extension}`, join(page, `index.${extension}`)] - : [join(page, `index.${extension}`)] + const appPage = join(page, `page.${extension}`) + const folderIndexPage = join(page, `index.${extension}`) + + if (!normalizedPagePath.endsWith('/index')) { + return isAppDir ? [appPage] : [`${page}.${extension}`, folderIndexPage] + } + return [isAppDir ? appPage : folderIndexPage] }) ) } diff --git a/test/unit/find-page-file.test.ts b/test/unit/find-page-file.test.ts index 87c97d0319f..b59853a4101 100644 --- a/test/unit/find-page-file.test.ts +++ b/test/unit/find-page-file.test.ts @@ -13,19 +13,34 @@ const dirWithPages = join(resolveDataDir, 'readdir', 'pages') describe('findPageFile', () => { it('should work', async () => { const pagePath = normalizePagePath('/nav/about') - const result = await findPageFile(dirWithPages, pagePath, ['jsx', 'js']) + const result = await findPageFile( + dirWithPages, + pagePath, + ['jsx', 'js'], + false + ) expect(result).toMatch(/^[\\/]nav[\\/]about\.js/) }) it('should work with nested index.js', async () => { const pagePath = normalizePagePath('/nested') - const result = await findPageFile(dirWithPages, pagePath, ['jsx', 'js']) + const result = await findPageFile( + dirWithPages, + pagePath, + ['jsx', 'js'], + false + ) expect(result).toMatch(/^[\\/]nested[\\/]index\.js/) }) it('should prefer prefered.js before preferred/index.js', async () => { const pagePath = normalizePagePath('/prefered') - const result = await findPageFile(dirWithPages, pagePath, ['jsx', 'js']) + const result = await findPageFile( + dirWithPages, + pagePath, + ['jsx', 'js'], + false + ) expect(result).toMatch(/^[\\/]prefered\.js/) }) }) From 86ddadab134c94c112ef971381a7572991174261 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 31 Aug 2022 23:32:27 +0200 Subject: [PATCH 05/13] fix _error static generation --- packages/next/build/index.ts | 1 + packages/next/export/index.ts | 2 ++ packages/next/export/worker.ts | 9 +++++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index ea5fa1a21e2..d311eb80367 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -1920,6 +1920,7 @@ export default async function build( await staticWorkers.end() } : undefined, + appPaths, } const exportConfig: any = { ...config, diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index da285419951..323c769066f 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -133,6 +133,7 @@ interface ExportOptions { statusMessage?: string exportPageWorker?: typeof import('./worker').default endWorker?: () => Promise + appPaths?: string[] } export default async function exportApp( @@ -594,6 +595,7 @@ export default async function exportApp( parentSpanId: pageExportSpan.id, httpAgentOptions: nextConfig.httpAgentOptions, serverComponents: nextConfig.experimental.serverComponents, + appPaths: options.appPaths || [], }) for (const validation of result.ampValidations || []) { diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index bec5097dd79..c785613eaef 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -66,6 +66,7 @@ interface ExportPageInput { httpAgentOptions: NextConfigComplete['httpAgentOptions'] serverComponents?: boolean appDir?: boolean + appPaths: string[] } interface ExportPageResults { @@ -106,6 +107,7 @@ export default async function exportPage({ distDir, outDir, appDir, + appPaths, pagesDataDir, renderOpts, buildExport, @@ -273,6 +275,9 @@ export default async function exportPage({ return !buildExport && getStaticProps && !isDynamicRoute(path) } + const isAppPath = appPaths.some((appPath: string) => + appPath.startsWith(page + '.page') + ) if (serverless) { const curUrl = url.parse(req.url!, true) req.url = url.format({ @@ -293,7 +298,7 @@ export default async function exportPage({ page, serverless, !!serverComponents, - !!appDir + isAppPath ) const ampState = { ampFirst: pageConfig?.amp === true, @@ -360,7 +365,7 @@ export default async function exportPage({ page, serverless, !!serverComponents, - !!appDir + isAppPath ) const ampState = { ampFirst: components.pageConfig?.amp === true, From 32c24a74b6c48bfbfe35fefbdc10a4f27663e450 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 31 Aug 2022 23:46:17 +0200 Subject: [PATCH 06/13] fix lint --- packages/next/export/index.ts | 1 - packages/next/export/worker.ts | 3 --- packages/next/server/next-server.ts | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 323c769066f..e5c56a41e6a 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -583,7 +583,6 @@ export default async function exportApp( outDir, pagesDataDir, renderOpts, - appDir: nextConfig.experimental.appDir, serverRuntimeConfig, subFolders, buildExport: options.buildExport, diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index c785613eaef..51655364d66 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -65,7 +65,6 @@ interface ExportPageInput { parentSpanId: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] serverComponents?: boolean - appDir?: boolean appPaths: string[] } @@ -92,7 +91,6 @@ interface RenderOpts { defaultLocale?: string domainLocales?: DomainLocale[] trailingSlash?: boolean - appDir?: boolean } type ComponentModule = ComponentType<{}> & { @@ -106,7 +104,6 @@ export default async function exportPage({ pathMap, distDir, outDir, - appDir, appPaths, pagesDataDir, renderOpts, diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 055c1c476dd..2e3ea0283cb 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -27,7 +27,7 @@ import type { NextConfig } from './config-shared' import type { DynamicRoutes, PageChecker } from './router' import fs from 'fs' -import path, { join, relative, resolve, sep } from 'path' +import { join, relative, resolve, sep } from 'path' import { IncomingMessage, ServerResponse } from 'http' import { addRequestMeta, getRequestMeta } from './request-meta' import { isDynamicRoute } from '../shared/lib/router/utils' From 8174b9b90f5245e9f8732328264373ec7fcac010 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 1 Sep 2022 02:05:19 +0200 Subject: [PATCH 07/13] render proper pathname for edge route --- packages/next/server/base-server.ts | 2 +- packages/next/server/dev/next-dev-server.ts | 5 ++-- packages/next/server/next-server.ts | 26 ++++++++++++--------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index b9ccdd4fe5b..1413b3146b7 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1522,9 +1522,9 @@ export default abstract class Server { bubbleNoFallback: boolean ) { const { query, pathname } = ctx - const appPath = this.getOriginalAppPath(pathname) let page = pathname + const appPath = this.getOriginalAppPath(pathname) if (typeof appPath === 'string') { page = appPath } diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index e8a513cd4c1..33033d5f7ea 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -901,6 +901,7 @@ export default class DevServer extends Server { query: ParsedUrlQuery params: Params | undefined page: string + isAppDir: boolean }) { try { return super.runEdgeFunction({ @@ -1123,8 +1124,8 @@ export default class DevServer extends Server { ) } - protected async ensureEdgeFunction(pathname: string) { - return this.hotReloader!.ensurePage(pathname, false, false) + protected async ensureEdgeFunction(pathname: string, isAppDir: boolean) { + return this.hotReloader!.ensurePage(pathname, isAppDir, false) } generateRoutes() { diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 2e3ea0283cb..ebf0d36c649 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -759,6 +759,7 @@ export default class NextNodeServer extends BaseServer { query, params, page, + isAppDir: false, }) if (handledAsEdgeFunction) { @@ -888,10 +889,11 @@ export default class NextNodeServer extends BaseServer { ctx: RequestContext, bubbleNoFallback: boolean ) { - const appPath = this.getOriginalAppPath(ctx.pathname) + const appPath = this.getOriginalAppPath(ctx.pathname) || undefined let page = ctx.pathname - if (typeof appPath === 'string') { + const isAppDir = typeof appPath === 'string' + if (isAppDir) { page = appPath } @@ -904,7 +906,9 @@ export default class NextNodeServer extends BaseServer { res: ctx.res, query: ctx.query, params: ctx.renderOpts.params, - page, + page: ctx.pathname, + appPath, + isAppDir, }) return null } @@ -1666,7 +1670,7 @@ export default class NextNodeServer extends BaseServer { * so that we can run it. */ protected async ensureMiddleware() {} - protected async ensureEdgeFunction(_pathname: string) {} + protected async ensureEdgeFunction(_pathname: string, _isAppDir: boolean) {} /** * This method gets all middleware matchers and execute them when the request @@ -2014,18 +2018,17 @@ export default class NextNodeServer extends BaseServer { query: ParsedUrlQuery params: Params | undefined page: string + isAppDir: boolean + appPath?: string onWarning?: (warning: Error) => void }): Promise { let middlewareInfo: ReturnType | undefined - let appPath = this.getOriginalAppPath(params.page) - - if (typeof appPath === 'string') { - params.page = appPath - } - await this.ensureEdgeFunction(params.page) + // If it's edge app route, use appPath to find the edge SSR page + const page = params.isAppDir ? params.appPath! : params.page + await this.ensureEdgeFunction(page, params.isAppDir) middlewareInfo = this.getEdgeFunctionInfo({ - page: params.page, + page: page, middleware: false, }) @@ -2039,6 +2042,7 @@ export default class NextNodeServer extends BaseServer { Object.assign({}, getRequestMeta(params.req, '__NEXT_INIT_QUERY') || {}) ).toString() const locale = params.query.__nextLocale + // Use original pathname (without `/page`) instead of appPath for url let normalizedPathname = params.page if (isDataReq) { From 2046edbde82df92270b3a5d91caed1611ab837c8 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 1 Sep 2022 14:28:38 +0200 Subject: [PATCH 08/13] sahred normailze path logic --- packages/next/lib/find-pages-dir.ts | 2 +- packages/next/server/dev/hot-reloader.ts | 10 +++------- packages/next/server/dev/next-dev-server.ts | 16 ++++++---------- .../next/server/dev/on-demand-entry-handler.ts | 15 +++++---------- packages/next/server/lib/find-page-file.ts | 7 ++++--- packages/next/server/next-server.ts | 6 ++---- .../next/shared/lib/page-path/get-page-paths.ts | 5 ++++- 7 files changed, 25 insertions(+), 36 deletions(-) diff --git a/packages/next/lib/find-pages-dir.ts b/packages/next/lib/find-pages-dir.ts index c8421267312..2edec19c3f9 100644 --- a/packages/next/lib/find-pages-dir.ts +++ b/packages/next/lib/find-pages-dir.ts @@ -47,6 +47,6 @@ export function findPagesDir( return { pages: pagesDir, - appDir: appDir, + appDir, } } diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index d1eca5c8cda..68e05821c8e 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -272,7 +272,7 @@ export default class HotReloader { if (page === '/_error' || BLOCKED_PAGES.indexOf(page) === -1) { try { - await this.ensurePage(page, false, true) + await this.ensurePage(page, true) } catch (error) { await renderScriptError(pageBundleRes, getProperError(error)) return { finished: true } @@ -1071,11 +1071,7 @@ export default class HotReloader { ) } - public async ensurePage( - page: string, - isAppDir: boolean, - clientOnly: boolean - ): Promise { + public async ensurePage(page: string, clientOnly: boolean): Promise { // Make sure we don't re-build or dispose prebuilt pages if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) { return @@ -1086,6 +1082,6 @@ export default class HotReloader { if (error) { return Promise.reject(error) } - return this.onDemandEntries?.ensurePage(page, isAppDir, clientOnly) as any + return this.onDemandEntries?.ensurePage(page, clientOnly) as any } } diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 33033d5f7ea..527613eb944 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -1117,15 +1117,11 @@ export default class DevServer extends Server { } protected async ensureMiddleware() { - return this.hotReloader!.ensurePage( - this.actualMiddlewareFile!, - false, - false - ) + return this.hotReloader!.ensurePage(this.actualMiddlewareFile!, false) } - protected async ensureEdgeFunction(pathname: string, isAppDir: boolean) { - return this.hotReloader!.ensurePage(pathname, isAppDir, false) + protected async ensureEdgeFunction(pathname: string) { + return this.hotReloader!.ensurePage(pathname, false) } generateRoutes() { @@ -1291,7 +1287,7 @@ export default class DevServer extends Server { } protected async ensureApiPage(pathname: string): Promise { - return this.hotReloader!.ensurePage(pathname, false, false) + return this.hotReloader!.ensurePage(pathname, false) } protected async findPageComponents( @@ -1307,7 +1303,7 @@ export default class DevServer extends Server { throw new WrappedBuildError(compilationErr) } try { - await this.hotReloader!.ensurePage(pathname, isAppDir, false) + await this.hotReloader!.ensurePage(pathname, false) const serverComponents = this.nextConfig.experimental.serverComponents @@ -1331,7 +1327,7 @@ export default class DevServer extends Server { await this.hotReloader!.buildFallbackError() // Build the error page to ensure the fallback is built too. // TODO: See if this can be moved into hotReloader or removed. - await this.hotReloader!.ensurePage('/_error', false, false) + await this.hotReloader!.ensurePage('/_error', false) return await loadDefaultErrorComponents(this.distDir) } diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index bb76b535095..88c897b0ca6 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -256,9 +256,10 @@ function disposeInactiveEntries(maxInactiveAge: number) { }) } -function tryToNormalizePagePath(page: string, isAppDir: boolean) { +// Normalize both app paths and page paths +function tryToNormalizePagePath(page: string) { try { - return isAppDir ? normalizeAppPath(page) : normalizePagePath(page) + return normalizePagePath(page) } catch (err) { console.error(err) throw new PageNotFoundError(page) @@ -279,11 +280,10 @@ async function findPagePathData( rootDir: string, page: string, extensions: string[], - isAppDir: boolean, pagesDir?: string, appDir?: string ) { - const normalizedPagePath = tryToNormalizePagePath(page, isAppDir) + const normalizedPagePath = tryToNormalizePagePath(page) let pagePath: string | null = null if (isMiddlewareFile(normalizedPagePath)) { @@ -546,11 +546,7 @@ export function onDemandEntryHandler({ } return { - async ensurePage( - page: string, - isAppDir: boolean, - clientOnly: boolean - ): Promise { + async ensurePage(page: string, clientOnly: boolean): Promise { const stalledTime = 60 const stalledEnsureTimeout = setTimeout(() => { debug( @@ -563,7 +559,6 @@ export function onDemandEntryHandler({ rootDir, page, nextConfig.pageExtensions, - isAppDir, pagesDir, appDir ) diff --git a/packages/next/server/lib/find-page-file.ts b/packages/next/server/lib/find-page-file.ts index 30b9a32013b..53d2a86dd35 100644 --- a/packages/next/server/lib/find-page-file.ts +++ b/packages/next/server/lib/find-page-file.ts @@ -35,9 +35,10 @@ export async function findPageFile( const pagePaths = getPagePaths(normalizedPagePath, pageExtensions, isAppDir) const [existingPath, ...others] = ( await Promise.all( - pagePaths.map(async (path) => - (await fileExists(join(pagesDir, path))) ? path : null - ) + pagePaths.map(async (path) => { + const filePath = join(pagesDir, path) + return (await fileExists(filePath)) ? path : null + }) ) ).filter(nonNullable) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index ebf0d36c649..389c7d8943a 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -983,8 +983,6 @@ export default class NextNodeServer extends BaseServer { // in the pages-manifest if (!(err instanceof PageNotFoundError)) { throw err - } else if (this.renderOpts.dev) { - console.error(err) } } } @@ -1670,7 +1668,7 @@ export default class NextNodeServer extends BaseServer { * so that we can run it. */ protected async ensureMiddleware() {} - protected async ensureEdgeFunction(_pathname: string, _isAppDir: boolean) {} + protected async ensureEdgeFunction(_pathname: string) {} /** * This method gets all middleware matchers and execute them when the request @@ -2026,7 +2024,7 @@ export default class NextNodeServer extends BaseServer { // If it's edge app route, use appPath to find the edge SSR page const page = params.isAppDir ? params.appPath! : params.page - await this.ensureEdgeFunction(page, params.isAppDir) + await this.ensureEdgeFunction(page) middlewareInfo = this.getEdgeFunctionInfo({ page: page, middleware: false, diff --git a/packages/next/shared/lib/page-path/get-page-paths.ts b/packages/next/shared/lib/page-path/get-page-paths.ts index af08b55b9b3..3ea3d9bd8e7 100644 --- a/packages/next/shared/lib/page-path/get-page-paths.ts +++ b/packages/next/shared/lib/page-path/get-page-paths.ts @@ -7,6 +7,9 @@ import { join } from '../isomorphic/path' * allowed extensions. This can be used to check which one of the files exists * and to debug inspected locations. * + * For pages, map `/route` to [`/route.[ext]`, `/route/index.[ext]`] + * For app paths, map `/route/page` to [`/route/page.[ext]`] + * * @param normalizedPagePath Normalized page path (it will denormalize). * @param extensions Allowed extensions. */ @@ -19,7 +22,7 @@ export function getPagePaths( return flatten( extensions.map((extension) => { - const appPage = join(page, `page.${extension}`) + const appPage = `${page}.${extension}` const folderIndexPage = join(page, `index.${extension}`) if (!normalizedPagePath.endsWith('/index')) { From 32cf0ccd1d47a441e8320d828aa5d0a6974df636 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 1 Sep 2022 14:38:38 +0200 Subject: [PATCH 09/13] rename to isAppPath --- packages/next/server/base-server.ts | 2 +- packages/next/server/dev/next-dev-server.ts | 6 +++--- .../server/dev/on-demand-entry-handler.ts | 1 - packages/next/server/load-components.ts | 11 +++++----- packages/next/server/next-server.ts | 20 +++++++++---------- packages/next/server/web-server.ts | 2 +- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 1413b3146b7..29b732fcd3a 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -243,7 +243,7 @@ export default abstract class Server { pathname: string, query: NextParsedUrlQuery, params: Params, - isAppDir: boolean + isAppPath: boolean ): Promise protected abstract getFontManifest(): FontManifest | undefined protected abstract getPrerenderManifest(): PrerenderManifest diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 527613eb944..7a96c47dbef 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -901,7 +901,7 @@ export default class DevServer extends Server { query: ParsedUrlQuery params: Params | undefined page: string - isAppDir: boolean + isAppPath: boolean }) { try { return super.runEdgeFunction({ @@ -1294,7 +1294,7 @@ export default class DevServer extends Server { pathname: string, query: ParsedUrlQuery, params: Params, - isAppDir: boolean + isAppPath: boolean ): Promise { await this.devReady const compilationErr = await this.getCompilationError(pathname) @@ -1314,7 +1314,7 @@ export default class DevServer extends Server { this.serverCSSManifest = super.getServerCSSManifest() } - return super.findPageComponents(pathname, query, params, isAppDir) + return super.findPageComponents(pathname, query, params, isAppPath) } catch (err) { if ((err as any).code !== 'ENOENT') { throw err diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 88c897b0ca6..c3ce13b67ac 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -22,7 +22,6 @@ import { COMPILER_INDEXES, COMPILER_NAMES, } from '../../shared/lib/constants' -import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' const debug = origDebug('next:on-demand-entry-handler') diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index cf8bddc9f3e..6535bb2b4c3 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -64,7 +64,7 @@ export async function loadComponents( pathname: string, serverless: boolean, hasServerComponents: boolean, - isAppDirRoute: boolean + isAppPath: boolean ): Promise { if (serverless) { const ComponentMod = await requirePage(pathname, distDir, serverless) @@ -101,7 +101,7 @@ export async function loadComponents( let DocumentMod = {} let AppMod = {} - if (!isAppDirRoute) { + if (!isAppPath) { ;[DocumentMod, AppMod] = await Promise.all([ Promise.resolve().then(() => requirePage('/_document', distDir, serverless, false) @@ -112,7 +112,7 @@ export async function loadComponents( ]) } const ComponentMod = await Promise.resolve().then(() => - requirePage(pathname, distDir, serverless, isAppDirRoute) + requirePage(pathname, distDir, serverless, isAppPath) ) const [buildManifest, reactLoadableManifest, serverComponentManifest] = @@ -130,15 +130,14 @@ export async function loadComponents( const { getServerSideProps, getStaticProps, getStaticPaths } = ComponentMod - let isAppPath = false - if (isAppDirRoute) { + if (isAppPath) { const pagePath = getPagePath( pathname, distDir, serverless, false, undefined, - isAppDirRoute + isAppPath ) isAppPath = !!pagePath?.match(/server[/\\]app[/\\]/) } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 389c7d8943a..5e39ac360fc 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -759,7 +759,7 @@ export default class NextNodeServer extends BaseServer { query, params, page, - isAppDir: false, + isAppPath: false, }) if (handledAsEdgeFunction) { @@ -892,8 +892,8 @@ export default class NextNodeServer extends BaseServer { const appPath = this.getOriginalAppPath(ctx.pathname) || undefined let page = ctx.pathname - const isAppDir = typeof appPath === 'string' - if (isAppDir) { + const isAppPath = typeof appPath === 'string' + if (isAppPath) { page = appPath } @@ -908,7 +908,7 @@ export default class NextNodeServer extends BaseServer { params: ctx.renderOpts.params, page: ctx.pathname, appPath, - isAppDir, + isAppPath: isAppPath, }) return null } @@ -921,12 +921,12 @@ export default class NextNodeServer extends BaseServer { pathname: string, query: NextParsedUrlQuery, params: Params, - isAppDir: boolean + isAppPath: boolean ): Promise { let paths = [ // try serving a static AMP version first query.amp - ? (isAppDir + ? (isAppPath ? normalizeAppPath(pathname) : normalizePagePath(pathname)) + '.amp' : null, @@ -949,7 +949,7 @@ export default class NextNodeServer extends BaseServer { pagePath!, !this.renderOpts.dev && this._isLikeServerless, !!this.renderOpts.serverComponents, - isAppDir + isAppPath ) if ( @@ -975,7 +975,7 @@ export default class NextNodeServer extends BaseServer { } as NextParsedUrlQuery) : query), // For appDir params is excluded. - ...((isAppDir ? {} : params) || {}), + ...((isAppPath ? {} : params) || {}), }, } } catch (err) { @@ -2016,14 +2016,14 @@ export default class NextNodeServer extends BaseServer { query: ParsedUrlQuery params: Params | undefined page: string - isAppDir: boolean + isAppPath: boolean appPath?: string onWarning?: (warning: Error) => void }): Promise { let middlewareInfo: ReturnType | undefined // If it's edge app route, use appPath to find the edge SSR page - const page = params.isAppDir ? params.appPath! : params.page + const page = params.isAppPath ? params.appPath! : params.page await this.ensureEdgeFunction(page) middlewareInfo = this.getEdgeFunctionInfo({ page: page, diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index e3ad0ce87d9..0b966acb9b8 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -408,7 +408,7 @@ export default class NextWebServer extends BaseServer { pathname: string, query: NextParsedUrlQuery, params: Params | null, - _isAppDir: boolean + _isAppPath: boolean ) { const result = await this.serverOptions.webServerConfig.loadComponent( pathname From 177fdab747916cea7947609c798bb37b8c1e2888 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 1 Sep 2022 14:41:18 +0200 Subject: [PATCH 10/13] remove duplicate checking --- packages/next/server/load-components.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 6535bb2b4c3..38f0ff527ab 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -15,7 +15,7 @@ import { FLIGHT_MANIFEST, } from '../shared/lib/constants' import { join } from 'path' -import { requirePage, getPagePath } from './require' +import { requirePage } from './require' import { BuildManifest } from './get-page-files' import { interopDefault } from '../lib/interop-default' @@ -38,7 +38,7 @@ export type LoadComponentsReturnType = { getStaticPaths?: GetStaticPaths getServerSideProps?: GetServerSideProps ComponentMod: any - isAppPath?: boolean + isAppPath: boolean } export async function loadDefaultErrorComponents(distDir: string) { @@ -130,18 +130,6 @@ export async function loadComponents( const { getServerSideProps, getStaticProps, getStaticPaths } = ComponentMod - if (isAppPath) { - const pagePath = getPagePath( - pathname, - distDir, - serverless, - false, - undefined, - isAppPath - ) - isAppPath = !!pagePath?.match(/server[/\\]app[/\\]/) - } - return { App, Document, From d012198e004eec65cf802e8cadeec4aca557e5a3 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 1 Sep 2022 15:03:55 +0200 Subject: [PATCH 11/13] tweak typing --- packages/next/server/load-components.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 38f0ff527ab..5cbf543ccc4 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -38,7 +38,7 @@ export type LoadComponentsReturnType = { getStaticPaths?: GetStaticPaths getServerSideProps?: GetServerSideProps ComponentMod: any - isAppPath: boolean + isAppPath?: boolean } export async function loadDefaultErrorComponents(distDir: string) { From 4fee739e10b53101acfded2c3c7c3cd3a15b589e Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 2 Sep 2022 14:05:02 +0200 Subject: [PATCH 12/13] fix 404 route in dev --- packages/next/build/entries.ts | 4 +++- packages/next/build/index.ts | 3 +++ packages/next/server/dev/hot-reloader.ts | 27 ++++++++++++------------ test/e2e/app-dir/rsc-basic.test.ts | 8 +++++++ 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 2d558ae91c1..aed59e8e069 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -69,12 +69,14 @@ export function createPagesMapping({ pageExtensions, pagePaths, pagesType, + pagesDir, }: { hasServerComponents: boolean isDev: boolean pageExtensions: string[] pagePaths: string[] pagesType: 'pages' | 'root' | 'app' + pagesDir: string | undefined }): { [page: string]: string } { const previousPages: { [key: string]: string } = {} const pages = pagePaths.reduce<{ [key: string]: string }>( @@ -133,7 +135,7 @@ export function createPagesMapping({ // In development we always alias these to allow Webpack to fallback to // the correct source file so that HMR can work properly when a file is // added or removed. - const root = isDev ? PAGES_DIR_ALIAS : 'next/dist/pages' + const root = isDev && pagesDir ? PAGES_DIR_ALIAS : 'next/dist/pages' return { '/_app': `${root}/_app`, diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index d311eb80367..7e3fedd224e 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -494,6 +494,7 @@ export default async function build( pageExtensions: config.pageExtensions, pagesType: 'pages', pagePaths: pagesPaths, + pagesDir, }) ) @@ -509,6 +510,7 @@ export default async function build( isDev: false, pagesType: 'app', pageExtensions: config.pageExtensions, + pagesDir: pagesDir, }) ) } @@ -521,6 +523,7 @@ export default async function build( pageExtensions: config.pageExtensions, pagePaths: rootPaths, pagesType: 'root', + pagesDir: pagesDir, }) } diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 68e05821c8e..921706767ce 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -421,19 +421,20 @@ export default class HotReloader { ]) ) - this.pagesMapping = !this.pagesDir - ? {} - : webpackConfigSpan.traceChild('create-pages-mapping').traceFn(() => - createPagesMapping({ - hasServerComponents: this.hasServerComponents, - isDev: true, - pageExtensions: this.config.pageExtensions, - pagesType: 'pages', - pagePaths: pagePaths.filter( - (i: string | null): i is string => typeof i === 'string' - ), - }) - ) + this.pagesMapping = webpackConfigSpan + .traceChild('create-pages-mapping') + .traceFn(() => + createPagesMapping({ + hasServerComponents: this.hasServerComponents, + isDev: true, + pageExtensions: this.config.pageExtensions, + pagesType: 'pages', + pagePaths: pagePaths.filter( + (i: string | null): i is string => typeof i === 'string' + ), + pagesDir: this.pagesDir, + }) + ) const entrypoints = await webpackConfigSpan .traceChild('create-entrypoints') diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index 0f91f92f807..4b9b3471992 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -262,6 +262,14 @@ describe('app dir - react server components', () => { expect(manipulated).toBe(undefined) }) + it('should render built-in 404 page for missing route if pagesDir is not presented', async () => { + const res = await fetchViaHTTP(next.url, '/does-not-exist') + + expect(res.status).toBe(404) + const html = await res.text() + expect(html).toContain('This page could not be found') + }) + it('should suspense next/image in server components', async () => { const imageHTML = await renderViaHTTP(next.url, '/next-api/image') const imageTag = getNodeBySelector(imageHTML, '#myimg') From 1034a5b004630839b9f4863c1ea5d25c8fbab136 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 3 Sep 2022 01:02:56 +0200 Subject: [PATCH 13/13] update checking for isSrcDir --- packages/next/build/index.ts | 6 +++--- packages/next/server/dev/next-dev-server.ts | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 7e3fedd224e..5f107284eb6 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -314,9 +314,9 @@ export default async function build( eventCliSession(dir, config, { webpackVersion: 5, cliCommand: 'build', - isSrcDir: pagesDir - ? path.relative(dir, pagesDir).startsWith('src') - : false, + isSrcDir: + (!!pagesDir && path.relative(dir, pagesDir).startsWith('src')) || + (!!appDir && path.relative(dir, appDir).startsWith('src')), hasNowJson: !!(await findUp('now.json', { cwd: dir })), isCustomServer: null, }) diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 7a96c47dbef..78f4656d853 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -686,9 +686,10 @@ export default class DevServer extends Server { eventCliSession(this.distDir, this.nextConfig, { webpackVersion: 5, cliCommand: 'dev', - isSrcDir: this.pagesDir - ? relative(this.dir, this.pagesDir).startsWith('src') - : false, + isSrcDir: + (!!this.pagesDir && + relative(this.dir, this.pagesDir).startsWith('src')) || + (!!this.appDir && relative(this.dir, this.appDir).startsWith('src')), hasNowJson: !!(await findUp('now.json', { cwd: this.dir })), isCustomServer: this.isCustomServer, })