From 450d747860bd8af1471d6528bb92ad707e111242 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 13 Sep 2022 17:00:55 +0200 Subject: [PATCH] change rsc detection --- .../build/analysis/get-page-static-info.ts | 35 ++++++++++++- packages/next/build/entries.ts | 16 +++--- packages/next/build/index.ts | 24 ++++----- packages/next/build/utils.ts | 17 +----- packages/next/build/webpack-config.ts | 4 -- .../loaders/next-flight-loader/index.ts | 52 +++++++------------ .../plugins/flight-client-entry-plugin.ts | 3 +- .../webpack/plugins/flight-manifest-plugin.ts | 4 +- packages/next/server/dev/hot-reloader.ts | 12 ++--- .../server/dev/on-demand-entry-handler.ts | 6 +-- packages/next/shared/lib/constants.ts | 5 +- test/e2e/app-dir/convention/app/css/page.js | 2 +- 12 files changed, 86 insertions(+), 94 deletions(-) diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 37d95e9bdcdd..46dc9b2ee640 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -12,6 +12,7 @@ import * as Log from '../output/log' import { SERVER_RUNTIME } from '../../lib/constants' import { ServerRuntime } from 'next/types' import { checkCustomRoutes } from '../../lib/load-custom-routes' +import { RSC_MODULE_TYPES } from '../../shared/lib/constants' export interface MiddlewareConfig { matchers: MiddlewareMatcher[] @@ -27,9 +28,34 @@ export interface PageStaticInfo { runtime?: ServerRuntime ssg?: boolean ssr?: boolean + rsc?: RSCModuleType middleware?: Partial } +export type RSCModuleType = 'server' | 'client' +export function getRSCModuleType(swcAST: any): RSCModuleType { + const { body } = swcAST + // TODO-APP: optimize the directive detection + // Assume there're only "use strict" and "client" directives at top, + // so pick the 2 nodes + const firstTwoNodes = body.slice(0, 2) + + let rscType: RSCModuleType = 'server' + for (const node of firstTwoNodes) { + if ( + node.type === 'ExpressionStatement' && + node.expression.type === 'StringLiteral' + ) { + if (node.expression.value === RSC_MODULE_TYPES.client) { + // Detect client entry + rscType = 'client' + break + } + } + } + return rscType +} + /** * Receives a parsed AST from SWC and checks if it belongs to a module that * requires a runtime to be specified. Those are: @@ -226,6 +252,7 @@ export async function getPageStaticInfo(params: { if (/runtime|getStaticProps|getServerSideProps|matcher/.test(fileContent)) { const swcAST = await parseModule(pageFilePath, fileContent) const { ssg, ssr } = checkExports(swcAST) + const rsc = getRSCModuleType(swcAST) // default / failsafe value for config let config: any = {} @@ -273,10 +300,16 @@ export async function getPageStaticInfo(params: { return { ssr, ssg, + rsc, ...(middlewareConfig && { middleware: middlewareConfig }), ...(runtime && { runtime }), } } - return { ssr: false, ssg: false, runtime: nextConfig.experimental?.runtime } + return { + ssr: false, + ssg: false, + rsc: RSC_MODULE_TYPES.server, + runtime: nextConfig.experimental?.runtime, + } } diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index f58536b2eb7f..a2c569b6553e 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -37,14 +37,12 @@ import { warn } from './output/log' import { isMiddlewareFile, isMiddlewareFilename, - isServerComponentPage, NestedMiddlewareError, MiddlewareInServerlessTargetError, } from './utils' import { getPageStaticInfo } from './analysis/get-page-static-info' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' -import { serverComponentRegex } from './webpack/loaders/utils' import { ServerRuntime } from '../types' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { encodeMatchers } from './webpack/loaders/next-middleware-loader' @@ -204,10 +202,7 @@ export function getEdgeServerEntry(opts: { absolutePagePath: opts.absolutePagePath, buildId: opts.buildId, dev: opts.isDev, - isServerComponent: isServerComponentPage( - opts.config, - opts.absolutePagePath - ), + isServerComponent: opts.isServerComponent, page: opts.page, stringifiedConfig: JSON.stringify(opts.config), pagesType: opts.pagesType, @@ -414,9 +409,10 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { nestedMiddleware.push(page) } - const isServerComponent = serverComponentRegex.test(absolutePagePath) - const isInsideAppDir = appDir && absolutePagePath.startsWith(appDir) - + const isInsideAppDir = + !!appDir && + (absolutePagePath.startsWith(APP_DIR_ALIAS) || + absolutePagePath.startsWith(appDir)) const staticInfo = await getPageStaticInfo({ nextConfig: config, pageFilePath, @@ -424,6 +420,8 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { page, }) + const isServerComponent = isInsideAppDir && staticInfo.rsc === 'server' + if (isMiddlewareFile(page)) { middlewareMatchers = staticInfo.middleware?.matchers ?? [ { regexp: '.*' }, diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 32786ae48f07..21987484189b 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -96,7 +96,6 @@ import { printTreeView, copyTracedFiles, isReservedPage, - isServerComponentPage, } from './utils' import getBaseWebpackConfig from './webpack-config' import { PagesManifest } from './webpack/plugins/pages-manifest-plugin' @@ -1267,21 +1266,16 @@ export default async function build( ) : appPaths?.find((p) => p.startsWith(actualPage + '/page.')) - const pageRuntime = + const staticInfo = pagesDir && pageType === 'pages' && pagePath - ? ( - await getPageStaticInfo({ - pageFilePath: join(pagesDir, pagePath), - nextConfig: config, - }) - ).runtime - : undefined - - if (hasServerComponents && pagePath) { - if (isServerComponentPage(config, pagePath)) { - isServerComponent = true - } - } + ? await getPageStaticInfo({ + pageFilePath: join(pagesDir, pagePath), + nextConfig: config, + }) + : {} + const pageRuntime = staticInfo.runtime + isServerComponent = + pageType === 'app' && staticInfo.rsc === 'server' if ( // Only calculate page static information if the page is not an diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 5e04a963c108..957052b12790 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1327,6 +1327,7 @@ export function detectConflictingPaths( } } +// TODO-APP: drop withoutRSCExtensions to use original page extensions /** * With RSC we automatically add .server and .client to page extensions. This * function allows to remove them for cases where we just need to strip out @@ -1338,22 +1339,6 @@ export function withoutRSCExtensions(pageExtensions: string[]): string[] { ) } -export function isServerComponentPage( - nextConfig: NextConfigComplete, - filePath: string -): boolean { - if (!nextConfig.experimental.serverComponents) { - return false - } - - const rawPageExtensions = withoutRSCExtensions( - nextConfig.pageExtensions || [] - ) - return rawPageExtensions.some((ext) => { - return filePath.endsWith(`.server.${ext}`) - }) -} - export async function copyTracedFiles( dir: string, distDir: string, diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index c1f163bc11a0..025f72a943a2 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -685,10 +685,6 @@ export default async function getBaseWebpackConfig( ? withoutRSCExtensions(config.pageExtensions) : config.pageExtensions - const serverComponentsRegex = new RegExp( - `\\.server\\.(${rawPageExtensions.join('|')})$` - ) - const babelIncludeRegexes: RegExp[] = [ /next[\\/]dist[\\/]shared[\\/]lib/, /next[\\/]dist[\\/]client/, diff --git a/packages/next/build/webpack/loaders/next-flight-loader/index.ts b/packages/next/build/webpack/loaders/next-flight-loader/index.ts index 3fee533cccc7..caaae46d4429 100644 --- a/packages/next/build/webpack/loaders/next-flight-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-flight-loader/index.ts @@ -1,6 +1,9 @@ import path from 'path' -import { RSC_CLIENT_ENTRY } from '../../../../shared/lib/constants' -import { checkExports } from '../../../analysis/get-page-static-info' +import { RSC_MODULE_TYPES } from '../../../../shared/lib/constants' +import { + checkExports, + getRSCModuleType, +} from '../../../analysis/get-page-static-info' import { parse } from '../../../swc' function transformClient(resourcePath: string): string { @@ -31,50 +34,35 @@ const isPageOrLayoutFile = (filePath: string) => { export default async function transformSource( this: any, - source: string -): Promise { + source: string, + sourceMap: any +) { if (typeof source !== 'string') { throw new Error('Expected source to have been transformed to a string.') } const { resourcePath } = this + const callback = this.async() const buildInfo = (this as any)._module.buildInfo const swcAST = await parse(source, { filename: resourcePath, isModule: 'unknown', }) - const isModule = swcAST.type === 'Module' - const { body } = swcAST - // TODO-APP: optimize the directive detection - // Assume there're only "use strict" and "-entry" directives at top, - // so pick the 2 nodes - const firstTwoNodes = body.slice(0, 2) - const appDir = path.join(this.rootContext, 'app') - const isUnderAppDir = containsPath(appDir, this.resourcePath) + const rscType = getRSCModuleType(swcAST) + + // Assign the RSC meta information to buildInfo. + buildInfo.rsc = { type: rscType } + + const isModule = swcAST.type === 'Module' const createError = (name: string) => new Error( `${name} is not supported in client components.\nFrom: ${this.resourcePath}` ) - + const appDir = path.join(this.rootContext, 'app') + const isUnderAppDir = containsPath(appDir, this.resourcePath) const isResourcePageOrLayoutFile = isPageOrLayoutFile(this.resourcePath) - - // Assign the RSC meta information to buildInfo. - buildInfo.rsc = {} - for (const node of firstTwoNodes) { - if ( - node.type === 'ExpressionStatement' && - node.expression.type === 'StringLiteral' - ) { - if (node.expression.value === RSC_CLIENT_ENTRY) { - // Detect client entry - buildInfo.rsc.type = RSC_CLIENT_ENTRY - break - } - } - } - // If client entry has any gSSP/gSP data fetching methods, error function errorForInvalidDataFetching(onError: (error: any) => void) { if (isUnderAppDir && isResourcePageOrLayoutFile) { @@ -88,12 +76,12 @@ export default async function transformSource( } } - if (buildInfo.rsc.type === RSC_CLIENT_ENTRY) { + if (buildInfo.rsc.type === RSC_MODULE_TYPES.client) { errorForInvalidDataFetching(this.emitError) const code = transformClient(this.resourcePath) - return code + return callback(null, code, sourceMap) } const code = transformServer(source, isModule) - return code + return callback(null, code, sourceMap) } diff --git a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts index 0afde7d658c9..4174494c6890 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -15,6 +15,7 @@ import { APP_DIR_ALIAS } from '../../../lib/constants' import { COMPILER_NAMES, FLIGHT_SERVER_CSS_MANIFEST, + RSC_MODULE_TYPES, } from '../../../shared/lib/constants' import { FlightCSSManifest } from './flight-manifest-plugin' @@ -240,7 +241,7 @@ export class FlightClientEntryPlugin { const isCSS = regexCSS.test(modRequest) const rscModuleType = mod.buildInfo.rsc?.type - const isClientComponent = rscModuleType === 'client-entry' + const isClientComponent = rscModuleType === RSC_MODULE_TYPES.client if (isCSS) { serverCSSImports[layoutOrPageRequest] = diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 9f49a7977588..39126974a04b 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -8,7 +8,7 @@ import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { FLIGHT_MANIFEST, - RSC_CLIENT_ENTRY, + RSC_MODULE_TYPES, } from '../../../shared/lib/constants' import { relative } from 'path' @@ -179,7 +179,7 @@ export class FlightManifestPlugin { // That way we know by the type of dep whether to include. // It also resolves conflicts when the same module is in multiple chunks. const rscType = mod.buildInfo.rsc?.type - if (rscType !== RSC_CLIENT_ENTRY) return + if (rscType !== RSC_MODULE_TYPES.client) return if (/\/(page|layout)\.(ts|js)x?$/.test(resource)) { entryFilepath = resource diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 0130ed5a93ff..9ea2e27fa72c 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -46,7 +46,6 @@ import { getProperError } from '../../lib/is-error' import ws from 'next/dist/compiled/ws' import { promises as fs } from 'fs' import { getPageStaticInfo } from '../../build/analysis/get-page-static-info' -import { serverComponentRegex } from '../../build/webpack/loaders/utils' import { UnwrapPromise } from '../../lib/coalesced-function' function diff(a: Set, b: Set) { @@ -598,10 +597,7 @@ export default class HotReloader { } } - const isServerComponent = isEntry - ? serverComponentRegex.test(entryData.absolutePagePath) - : false - + const isAppPath = !!this.appDir && bundlePath.startsWith('app/') const staticInfo = isEntry ? await getPageStaticInfo({ pageFilePath: entryData.absolutePagePath, @@ -609,6 +605,7 @@ export default class HotReloader { isDev: true, }) : {} + const isServerComponent = isAppPath && staticInfo.rsc === 'server' await runDependingOnPageType({ page, @@ -616,9 +613,8 @@ export default class HotReloader { onEdgeServer: () => { // TODO-APP: verify if child entry should support. if (!isEdgeServerCompilation || !isEntry) return - const isApp = this.appDir && bundlePath.startsWith('app/') const appDirLoader = - isApp && this.appDir + isAppPath && this.appDir ? getAppEntry({ name: bundlePath, appPaths: entryData.appPaths, @@ -648,7 +644,7 @@ export default class HotReloader { pages: this.pagesMapping, isServerComponent, appDirLoader, - pagesType: isApp ? 'app' : undefined, + pagesType: isAppPath ? 'app' : undefined, }), appDir: this.config.experimental.appDir, }) diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 4784fcf4489f..8f8d910b8875 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -12,7 +12,6 @@ import { ensureLeadingSlash } from '../../shared/lib/page-path/ensure-leading-sl import { removePagePathTail } from '../../shared/lib/page-path/remove-page-path-tail' import { reportTrigger } from '../../build/output' import getRouteFromEntrypoint from '../get-route-from-entrypoint' -import { serverComponentRegex } from '../../build/webpack/loaders/utils' import { getPageStaticInfo } from '../../build/analysis/get-page-static-info' import { isMiddlewareFile, isMiddlewareFilename } from '../../build/utils' import { PageNotFoundError } from '../../shared/lib/utils' @@ -578,9 +577,6 @@ export function onDemandEntryHandler({ appDir ) - const isServerComponent = serverComponentRegex.test( - pagePathData.absolutePagePath - ) const isInsideAppDir = appDir && pagePathData.absolutePagePath.startsWith(appDir) @@ -637,6 +633,8 @@ export function onDemandEntryHandler({ const added = new Map>() + const isServerComponent = staticInfo.rsc === 'server' + await runDependingOnPageType({ page: pagePathData.page, pageRuntime: staticInfo.runtime, diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index b9798bed11de..fa75daf7d894 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -95,7 +95,10 @@ export const OPTIMIZED_FONT_PROVIDERS = [ export const STATIC_STATUS_PAGES = ['/500'] export const TRACE_OUTPUT_VERSION = 1 -export const RSC_CLIENT_ENTRY = 'client' +export const RSC_MODULE_TYPES = { + client: 'client', + server: 'server', +} as const // comparing // https://nextjs.org/docs/api-reference/edge-runtime diff --git a/test/e2e/app-dir/convention/app/css/page.js b/test/e2e/app-dir/convention/app/css/page.js index f35433e92c25..32c127cb7667 100644 --- a/test/e2e/app-dir/convention/app/css/page.js +++ b/test/e2e/app-dir/convention/app/css/page.js @@ -1,4 +1,4 @@ -import styles from './styles.css' +import styles from './styles.module.css' import { useState } from 'react' export default function page() {