From 5f13dc29afe6e150d12e46d33062cac24185e047 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 21 Sep 2022 09:15:53 +0200 Subject: [PATCH 1/7] add external handling for sc_server --- packages/next/build/webpack-config.ts | 103 ++++++++++++++---- .../build/webpack/loaders/next-app-loader.ts | 1 + .../plugins/flight-client-entry-plugin.ts | 4 +- packages/next/server/app-render.tsx | 34 +++--- test/e2e/app-dir/app/app/style.css | 2 +- 5 files changed, 101 insertions(+), 43 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 6aaa9ceeccae6..1e97ed653ccb8 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -977,6 +977,7 @@ export default async function getBaseWebpackConfig( context: string, request: string, dependencyType: string, + layer: string | null, getResolve: ( options: any ) => ( @@ -1001,14 +1002,49 @@ export default async function getBaseWebpackConfig( return `commonjs next/dist/lib/import-next-warning` } + const resolveWithReactServerCondition = + layer === WEBPACK_LAYERS.server + ? getResolve({ + alias: process.env.__NEXT_REACT_CHANNEL + ? { + react: `react-${process.env.__NEXT_REACT_CHANNEL}`, + 'react/package.json': `react-${process.env.__NEXT_REACT_CHANNEL}/package.json`, + 'react/jsx-runtime': `react-${process.env.__NEXT_REACT_CHANNEL}/jsx-runtime`, + 'react/jsx-dev-runtime': `react-${process.env.__NEXT_REACT_CHANNEL}/jsx-dev-runtime`, + 'react-dom': `react-dom-${process.env.__NEXT_REACT_CHANNEL}`, + 'react-dom/package.json': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/package.json`, + 'react-dom/server': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/server`, + 'react-dom/server.browser': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/server.browser`, + 'react-dom/client': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/client`, + } + : {}, + conditionNames: ['react-server'], + }) + : null + + // Special internal modules that should be bundled for Server Components. + if (layer === WEBPACK_LAYERS.server) { + if (request === 'react') { + const [resolved] = await resolveWithReactServerCondition!( + context, + request + ) + return resolved + } + if ( + request === + 'next/dist/compiled/react-server-dom-webpack/writer.browser.server' + ) { + return + } + } + // Relative requires don't need custom resolution, because they // are relative to requests we've already resolved here. // Absolute requires (require('/foo')) are extremely uncommon, but // also have no need for customization as they're already resolved. if (!isLocal) { - // styled-jsx is also marked as externals here to avoid being - // bundled in client components for RSC. - if (/^(?:next$|styled-jsx$|react(?:$|\/))/.test(request)) { + if (/^(?:next$|react(?:$|\/))/.test(request)) { return `commonjs ${request}` } @@ -1122,9 +1158,17 @@ export default async function getBaseWebpackConfig( return } - // Anything else that is standard JavaScript within `node_modules` - // can be externalized. if (/node_modules[/\\].*\.[mc]?js$/.test(res)) { + if (layer === WEBPACK_LAYERS.server) { + const [resolved] = await resolveWithReactServerCondition!( + context, + request + ) + return resolved + } + + // Anything else that is standard JavaScript within `node_modules` + // can be externalized. return `${externalType} ${request}` } @@ -1176,11 +1220,17 @@ export default async function getBaseWebpackConfig( context, request, dependencyType, + contextInfo, getResolve, }: { context: string request: string dependencyType: string + contextInfo: { + issuer: string + issuerLayer: string | null + compiler: string + } getResolve: ( options: any ) => ( @@ -1193,24 +1243,31 @@ export default async function getBaseWebpackConfig( ) => void ) => void }) => - handleExternals(context, request, dependencyType, (options) => { - const resolveFunction = getResolve(options) - return (resolveContext: string, requestToResolve: string) => - new Promise((resolve, reject) => { - resolveFunction( - resolveContext, - requestToResolve, - (err, result, resolveData) => { - if (err) return reject(err) - if (!result) return resolve([null, false]) - const isEsm = /\.js$/i.test(result) - ? resolveData?.descriptionFileData?.type === 'module' - : /\.mjs$/i.test(result) - resolve([result, isEsm]) - } - ) - }) - }), + handleExternals( + context, + request, + dependencyType, + contextInfo.issuerLayer, + (options) => { + const resolveFunction = getResolve(options) + return (resolveContext: string, requestToResolve: string) => + new Promise((resolve, reject) => { + resolveFunction( + resolveContext, + requestToResolve, + (err, result, resolveData) => { + if (err) return reject(err) + if (!result) return resolve([null, false]) + const isEsm = /\.js$/i.test(result) + ? resolveData?.descriptionFileData?.type === + 'module' + : /\.mjs$/i.test(result) + resolve([result, isEsm]) + } + ) + }) + } + ), ] : [ // When the 'serverless' target is used all node_modules will be compiled into the output bundles diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index 3153b835c7db5..6efe970a9a862 100644 --- a/packages/next/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/build/webpack/loaders/next-app-loader.ts @@ -191,6 +191,7 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ : 'null' } + export const renderToReadableStream = require('next/dist/compiled/react-server-dom-webpack/writer.browser.server').renderToReadableStream export const __next_app_webpack_require__ = __webpack_require__ ` 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 c4cb536d04afb..e6ae570200318 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -59,11 +59,11 @@ export class FlightClientEntryPlugin { ) compiler.hooks.finishMake.tapPromise(PLUGIN_NAME, (compilation) => { - return this.createClientEndpoints(compiler, compilation) + return this.createClientEntries(compiler, compilation) }) } - async createClientEndpoints(compiler: any, compilation: any) { + async createClientEntries(compiler: any, compilation: any) { const promises: Array< ReturnType > = [] diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 4e4112a428ab7..b393af9d2e72e 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -1,12 +1,14 @@ import type { IncomingMessage, ServerResponse } from 'http' import type { LoadComponentsReturnType } from './load-components' import type { ServerRuntime } from '../types' +import type { ComponentType, ReactNode } from 'react' // TODO-APP: change to React.use once it becomes stable +// @ts-ignore import React, { experimental_use as use } from 'react' + import { ParsedUrlQuery, stringify as stringifyQuery } from 'querystring' import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack' -import { renderToReadableStream } from 'next/dist/compiled/react-server-dom-webpack/writer.browser.server' import { NextParsedUrlQuery } from './request-meta' import RenderResult from './render-result' import { @@ -199,8 +201,9 @@ function useFlightResponse( * This is only used for renderToHTML, the Flight response does not need additional wrappers. */ function createServerComponentRenderer( - ComponentToRender: React.ComponentType, + ComponentToRender: ComponentType, ComponentMod: { + renderToReadableStream: any __next_app_webpack_require__?: any __next_rsc__?: { __webpack_require__?: any @@ -236,7 +239,7 @@ function createServerComponentRenderer( let RSCStream: ReadableStream const createRSCStream = () => { if (!RSCStream) { - RSCStream = renderToReadableStream( + RSCStream = ComponentMod.renderToReadableStream( , serverComponentManifest, { @@ -340,7 +343,7 @@ export type FlightDataPath = ...FlightSegmentPath, /* segment of the rendered slice: */ Segment, /* treePatch */ FlightRouterState, - /* subTreeData: */ React.ReactNode | null // Can be null during prefetch if there's no loading component + /* subTreeData: */ ReactNode | null // Can be null during prefetch if there's no loading component ] /** @@ -355,7 +358,7 @@ export type ChildProp = { /** * Null indicates that the tree is partial */ - current: React.ReactNode | null + current: ReactNode | null segment: Segment } @@ -514,7 +517,7 @@ export async function renderToHTMLOrFlight( // Empty so that the client-side router will do a full page navigation. const flightData: FlightData = pathname + (search ? `?${search}` : '') return new FlightRenderResult( - renderToReadableStream(flightData, serverComponentManifest, { + ComponentMod.renderToReadableStream(flightData, serverComponentManifest, { onError, }).pipeThrough(createBufferedTransformStream()) ) @@ -685,7 +688,7 @@ export async function renderToHTMLOrFlight( parentParams: { [key: string]: any } rootLayoutIncluded?: boolean firstItem?: boolean - }): Promise<{ Component: React.ComponentType }> => { + }): Promise<{ Component: ComponentType }> => { // TODO-APP: enable stylesheet per layout/page const stylesheets: string[] = layoutOrPagePath ? getCssInlinedLinkTags( @@ -761,7 +764,7 @@ export async function renderToHTMLOrFlight( // This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down const parallelRouteMap = await Promise.all( Object.keys(parallelRoutes).map( - async (parallelRouteKey): Promise<[string, React.ReactNode]> => { + async (parallelRouteKey): Promise<[string, ReactNode]> => { const currentSegmentPath: FlightSegmentPath = firstItem ? [parallelRouteKey] : [actualSegment, parallelRouteKey] @@ -843,7 +846,7 @@ export async function renderToHTMLOrFlight( list[parallelRouteKey] = Comp return list }, - {} as { [key: string]: React.ReactNode } + {} as { [key: string]: ReactNode } ) // When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component @@ -1003,7 +1006,7 @@ export async function renderToHTMLOrFlight( ).slice(1), ] - const readable = renderToReadableStream( + const readable = ComponentMod.renderToReadableStream( flightData, serverComponentManifest, { @@ -1084,16 +1087,13 @@ export async function renderToHTMLOrFlight( nonce ) - const flushEffectsCallbacks: Set<() => React.ReactNode> = new Set() + const flushEffectsCallbacks: Set<() => ReactNode> = new Set() function FlushEffects({ children }: { children: JSX.Element }) { // Reset flushEffectsHandler on each render flushEffectsCallbacks.clear() - const addFlushEffects = React.useCallback( - (handler: () => React.ReactNode) => { - flushEffectsCallbacks.add(handler) - }, - [] - ) + const addFlushEffects = React.useCallback((handler: () => ReactNode) => { + flushEffectsCallbacks.add(handler) + }, []) return ( diff --git a/test/e2e/app-dir/app/app/style.css b/test/e2e/app-dir/app/app/style.css index 0b8fbd008481a..3a94c07339f95 100644 --- a/test/e2e/app-dir/app/app/style.css +++ b/test/e2e/app-dir/app/app/style.css @@ -1,3 +1,3 @@ body { - font-size: xx-large; + font-size: large; } From 17eb76e92732515c35dc274b0a43bf3ca30b3a94 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 21 Sep 2022 09:47:33 +0200 Subject: [PATCH 2/7] add test cases --- test/e2e/app-dir/rsc-basic.test.ts | 22 ++++++++++++++++++- .../react-server/3rd-party-package/client.js | 15 +++++++++++++ .../react-server/3rd-party-package/page.js | 16 ++++++++++++++ .../app/react-server/client-detector.js | 3 +++ .../rsc-basic/app/react-server/detector.js | 5 +++++ .../rsc-basic/app/react-server/page.js | 13 +++++++++++ .../conditional-exports/index.js | 1 + .../conditional-exports/index.server.js | 1 + .../conditional-exports/package.json | 15 +++++++++++++ .../conditional-exports/subpath.js | 1 + .../conditional-exports/subpath.server.js | 1 + 11 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/client.js create mode 100644 test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/page.js create mode 100644 test/e2e/app-dir/rsc-basic/app/react-server/client-detector.js create mode 100644 test/e2e/app-dir/rsc-basic/app/react-server/detector.js create mode 100644 test/e2e/app-dir/rsc-basic/app/react-server/page.js create mode 100644 test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.js create mode 100644 test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.server.js create mode 100644 test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/package.json create mode 100644 test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.js create mode 100644 test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.server.js diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index 4d5196ffcad32..d0b4ee82465a5 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -44,7 +44,7 @@ describe('app dir - react server components', () => { }, packageJson: { scripts: { - setup: `cp -r ./node_modules_bak/non-isomorphic-text ./node_modules; cp -r ./node_modules_bak/random-module-instance ./node_modules`, + setup: `cp -r ./node_modules_bak/* ./node_modules`, build: 'yarn setup && next build', dev: 'yarn setup && next dev', start: 'next start', @@ -387,6 +387,26 @@ describe('app dir - react server components', () => { ) }) + it('should resolve the subset react in server components based on the react-server condition', async () => { + await fetchViaHTTP(next.url, '/react-server').then(async (response) => { + const result = await resolveStreamResponse(response) + expect(result).toContain('Server: subset') + expect(result).toContain('Client: full') + }) + }) + + it('should resolve 3rd party package exports based on the react-server condition', async () => { + await fetchViaHTTP(next.url, '/react-server/3rd-party-package').then( + async (response) => { + const result = await resolveStreamResponse(response) + expect(result).toContain('Server: index.react-server') + expect(result).toContain('Server subpath: subpath.react-server') + expect(result).toContain('Client: index.default') + expect(result).toContain('Client subpath: subpath.default') + } + ) + }) + if (!isNextDev) { it('should generate edge SSR manifests for Node.js', async () => { const distServerDir = path.join(distDir, 'server') diff --git a/test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/client.js b/test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/client.js new file mode 100644 index 0000000000000..ae07e30fa6ce8 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/client.js @@ -0,0 +1,15 @@ +'client' + +import v from 'conditional-exports' +import v1 from 'conditional-exports/subpath' + +export default function Client() { + return ( + <> + {`Client: ${v}`} +
+ {`Client subpath: ${v1}`} +
+ + ) +} diff --git a/test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/page.js b/test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/page.js new file mode 100644 index 0000000000000..33141e12f7685 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/app/react-server/3rd-party-package/page.js @@ -0,0 +1,16 @@ +import v from 'conditional-exports' +import v1 from 'conditional-exports/subpath' + +import Client from './client' + +export default function Page() { + return ( +
+ {`Server: ${v}`} +
+ {`Server subpath: ${v1}`} +
+ +
+ ) +} diff --git a/test/e2e/app-dir/rsc-basic/app/react-server/client-detector.js b/test/e2e/app-dir/rsc-basic/app/react-server/client-detector.js new file mode 100644 index 0000000000000..47c07de636fbc --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/app/react-server/client-detector.js @@ -0,0 +1,3 @@ +'client' + +export { default } from './detector' diff --git a/test/e2e/app-dir/rsc-basic/app/react-server/detector.js b/test/e2e/app-dir/rsc-basic/app/react-server/detector.js new file mode 100644 index 0000000000000..63cd22b647d54 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/app/react-server/detector.js @@ -0,0 +1,5 @@ +import React from 'react' + +export default function Detector() { + return 'useState' in React ? 'full' : 'subset' +} diff --git a/test/e2e/app-dir/rsc-basic/app/react-server/page.js b/test/e2e/app-dir/rsc-basic/app/react-server/page.js new file mode 100644 index 0000000000000..7f006ada27686 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/app/react-server/page.js @@ -0,0 +1,13 @@ +import Detector from './detector' +import ClientDetector from './client-detector' + +export default function Page() { + return ( +
+ Server: +
+ Client: +
+
+ ) +} diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.js b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.js new file mode 100644 index 0000000000000..3a7bb344a967a --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.js @@ -0,0 +1 @@ +module.exports = 'index.default' diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.server.js b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.server.js new file mode 100644 index 0000000000000..5618abfbec839 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/index.server.js @@ -0,0 +1 @@ +module.exports = 'index.react-server' diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/package.json b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/package.json new file mode 100644 index 0000000000000..addc598c8fcfd --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/package.json @@ -0,0 +1,15 @@ +{ + "name": "conditional-exports", + "main": "index.js", + "exports": { + ".": { + "react-server": "./index.server.js", + "default": "./index.js" + }, + "./subpath": { + "react-server": "./subpath.server.js", + "default": "./subpath.js" + }, + "./package.json": "./package.json" + } +} diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.js b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.js new file mode 100644 index 0000000000000..11d27846911c8 --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.js @@ -0,0 +1 @@ +module.exports = 'subpath.default' diff --git a/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.server.js b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.server.js new file mode 100644 index 0000000000000..11acc618fbd0f --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/node_modules_bak/conditional-exports/subpath.server.js @@ -0,0 +1 @@ +module.exports = 'subpath.react-server' From d9abab1239d81d02dd9498caa79989f3000023ec Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 21 Sep 2022 11:18:10 +0200 Subject: [PATCH 3/7] fix alias --- packages/next/build/webpack-config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 1e97ed653ccb8..09b56c15a7408 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1017,14 +1017,14 @@ export default async function getBaseWebpackConfig( 'react-dom/server.browser': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/server.browser`, 'react-dom/client': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/client`, } - : {}, + : false, conditionNames: ['react-server'], }) : null - // Special internal modules that should be bundled for Server Components. + // Special internal modules that must be bundled for Server Components. if (layer === WEBPACK_LAYERS.server) { - if (request === 'react') { + if (!isLocal && /^react(?:$|\/)/.test(request)) { const [resolved] = await resolveWithReactServerCondition!( context, request From e64e68411ced8f4e2036631db564e4b38e3ba742 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 21 Sep 2022 13:54:35 +0200 Subject: [PATCH 4/7] reorganize react server imports --- packages/next/build/webpack-config.ts | 15 +++++--- .../build/webpack/loaders/next-app-loader.ts | 2 + .../client/components/hooks-server-context.ts | 7 ++++ .../next/client/components/hooks-server.ts | 19 +--------- .../static-generation-async-storage.ts | 18 +++++++++ packages/next/export/worker.ts | 4 +- packages/next/server/app-render.tsx | 37 ++++++++----------- .../app/app/hooks/use-pathname/server/page.js | 4 +- .../app/app/hooks/use-router/server/page.js | 4 +- .../hooks/use-search-params/server/page.js | 4 +- 10 files changed, 60 insertions(+), 54 deletions(-) create mode 100644 packages/next/client/components/static-generation-async-storage.ts diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 09b56c15a7408..e7994678cff98 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1160,11 +1160,16 @@ export default async function getBaseWebpackConfig( if (/node_modules[/\\].*\.[mc]?js$/.test(res)) { if (layer === WEBPACK_LAYERS.server) { - const [resolved] = await resolveWithReactServerCondition!( - context, - request - ) - return resolved + try { + const [resolved] = await resolveWithReactServerCondition!( + context, + request + ) + return resolved + } catch (err) { + // The `react-server` condition is not matched, fallback. + return + } } // Anything else that is standard JavaScript within `node_modules` diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index 6efe970a9a862..c3adb305a4e5f 100644 --- a/packages/next/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/build/webpack/loaders/next-app-loader.ts @@ -191,6 +191,8 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ : 'null' } + export const serverHooks = require('next/dist/client/components/hooks-server-context.js') + export const renderToReadableStream = require('next/dist/compiled/react-server-dom-webpack/writer.browser.server').renderToReadableStream export const __next_app_webpack_require__ = __webpack_require__ ` diff --git a/packages/next/client/components/hooks-server-context.ts b/packages/next/client/components/hooks-server-context.ts index 81d288381be0e..fffcbcb89c22c 100644 --- a/packages/next/client/components/hooks-server-context.ts +++ b/packages/next/client/components/hooks-server-context.ts @@ -1,6 +1,13 @@ // @ts-expect-error createServerContext exists on experimental channel import { createServerContext } from 'react' +// createServerContext exists in react@experimental + react-dom@experimental +if (typeof createServerContext === 'undefined') { + throw new Error( + '"app" directory requires React.createServerContext which is not available in the version of React you are using. Please update to react@experimental and react-dom@experimental.' + ) +} + export class DynamicServerError extends Error { constructor(type: string) { super(`Dynamic server usage: ${type}`) diff --git a/packages/next/client/components/hooks-server.ts b/packages/next/client/components/hooks-server.ts index 7a47daa19e54c..5d4faa258a31e 100644 --- a/packages/next/client/components/hooks-server.ts +++ b/packages/next/client/components/hooks-server.ts @@ -1,4 +1,3 @@ -import type { AsyncLocalStorage } from 'async_hooks' import { useContext } from 'react' import { HeadersContext, @@ -6,23 +5,7 @@ import { CookiesContext, DynamicServerError, } from './hooks-server-context' - -export interface StaticGenerationStore { - inUse?: boolean - pathname?: string - revalidate?: number - fetchRevalidate?: number - isStaticGeneration?: boolean -} - -export let staticGenerationAsyncStorage: - | AsyncLocalStorage - | StaticGenerationStore = {} - -if (process.env.NEXT_RUNTIME !== 'edge' && typeof window === 'undefined') { - staticGenerationAsyncStorage = - new (require('async_hooks').AsyncLocalStorage)() -} +import { staticGenerationAsyncStorage } from './static-generation-async-storage' function useStaticGenerationBailout(reason: string) { const staticGenerationStore = diff --git a/packages/next/client/components/static-generation-async-storage.ts b/packages/next/client/components/static-generation-async-storage.ts new file mode 100644 index 0000000000000..f3abd89db0413 --- /dev/null +++ b/packages/next/client/components/static-generation-async-storage.ts @@ -0,0 +1,18 @@ +import type { AsyncLocalStorage } from 'async_hooks' + +export interface StaticGenerationStore { + inUse?: boolean + pathname?: string + revalidate?: number + fetchRevalidate?: number + isStaticGeneration?: boolean +} + +export let staticGenerationAsyncStorage: + | AsyncLocalStorage + | StaticGenerationStore = {} + +if (process.env.NEXT_RUNTIME !== 'edge' && typeof window === 'undefined') { + staticGenerationAsyncStorage = + new (require('async_hooks').AsyncLocalStorage)() +} diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index f7d1cb230cea2..e27ab26c863a6 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -388,9 +388,7 @@ export default async function exportPage({ // and bail when dynamic dependencies are detected // only fully static paths are fully generated here if (isAppDir) { - const { - DynamicServerError, - } = require('../client/components/hooks-server-context') + const { DynamicServerError } = components.ComponentMod.serverHooks const { renderToHTMLOrFlight } = require('../server/app-render') as typeof import('../server/app-render') diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index aad048fb8824b..88acf10c5d02f 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -104,15 +104,15 @@ let isFetchPatched = false // we patch fetch to collect cache information used for // determining if a page is static or not -function patchFetch() { +function patchFetch(ComponentMod: any) { if (isFetchPatched) return isFetchPatched = true const { DynamicServerError } = - require('../client/components/hooks-server-context') as typeof import('../client/components/hooks-server-context') + ComponentMod.serverHooks as typeof import('../client/components/hooks-server-context') const { staticGenerationAsyncStorage } = - require('../client/components/hooks-server') as typeof import('../client/components/hooks-server') + require('../client/components/static-generation-async-storage') as typeof import('../client/components/static-generation-async-storage') const origFetch = (global as any).fetch @@ -504,10 +504,19 @@ export async function renderToHTMLOrFlight( isPagesDir: boolean, isStaticGeneration: boolean = false ): Promise { - patchFetch() + const { + buildManifest, + subresourceIntegrityManifest, + serverComponentManifest, + serverCSSManifest = {}, + supportsDynamicHTML, + ComponentMod, + } = renderOpts + + patchFetch(ComponentMod) const { staticGenerationAsyncStorage } = - require('../client/components/hooks-server') as typeof import('../client/components/hooks-server') + require('../client/components/static-generation-async-storage') as typeof import('../client/components/static-generation-async-storage') if ( !('getStore' in staticGenerationAsyncStorage) && @@ -526,27 +535,11 @@ export async function renderToHTMLOrFlight( : staticGenerationAsyncStorage const { CONTEXT_NAMES } = - require('../client/components/hooks-server-context') as typeof import('../client/components/hooks-server-context') - - // @ts-expect-error createServerContext exists in react@experimental + react-dom@experimental - if (typeof React.createServerContext === 'undefined') { - throw new Error( - '"app" directory requires React.createServerContext which is not available in the version of React you are using. Please update to react@experimental and react-dom@experimental.' - ) - } + ComponentMod.serverHooks as typeof import('../client/components/hooks-server-context') // don't modify original query object query = Object.assign({}, query) - const { - buildManifest, - subresourceIntegrityManifest, - serverComponentManifest, - serverCSSManifest = {}, - supportsDynamicHTML, - ComponentMod, - } = renderOpts - const isFlight = query.__flight__ !== undefined const isPrefetch = query.__flight_prefetch__ !== undefined diff --git a/test/e2e/app-dir/app/app/hooks/use-pathname/server/page.js b/test/e2e/app-dir/app/app/hooks/use-pathname/server/page.js index 9c4c9543406a6..bb5ab57464994 100644 --- a/test/e2e/app-dir/app/app/hooks/use-pathname/server/page.js +++ b/test/e2e/app-dir/app/app/hooks/use-pathname/server/page.js @@ -1,8 +1,8 @@ -import { usePathname } from 'next/dist/client/components/hooks-client' +// import { usePathname } from 'next/dist/client/components/hooks-client' export default function Page() { // This should throw an error. - usePathname() + // usePathname() return null } diff --git a/test/e2e/app-dir/app/app/hooks/use-router/server/page.js b/test/e2e/app-dir/app/app/hooks/use-router/server/page.js index ca3f10a333cbb..18a79712d065f 100644 --- a/test/e2e/app-dir/app/app/hooks/use-router/server/page.js +++ b/test/e2e/app-dir/app/app/hooks/use-router/server/page.js @@ -1,8 +1,8 @@ -import { useRouter } from 'next/dist/client/components/hooks-client' +// import { useRouter } from 'next/dist/client/components/hooks-client' export default function Page() { // This should throw an error. - useRouter() + // useRouter() return null } diff --git a/test/e2e/app-dir/app/app/hooks/use-search-params/server/page.js b/test/e2e/app-dir/app/app/hooks/use-search-params/server/page.js index 3468f385f6a66..65c6fe8d91162 100644 --- a/test/e2e/app-dir/app/app/hooks/use-search-params/server/page.js +++ b/test/e2e/app-dir/app/app/hooks/use-search-params/server/page.js @@ -1,8 +1,8 @@ -import { useSearchParams } from 'next/dist/client/components/hooks-client' +// import { useSearchParams } from 'next/dist/client/components/hooks-client' export default function Page() { // This should throw an error. - useSearchParams() + // useSearchParams() return null } From 1bf7b68f00bab903f3b2f68ba89dc21d9af3317e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 21 Sep 2022 16:59:03 +0200 Subject: [PATCH 5/7] skip the old router test --- .../next-swc/crates/core/src/react_server_components.rs | 8 +++++++- test/e2e/app-dir/index.test.ts | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/next-swc/crates/core/src/react_server_components.rs b/packages/next-swc/crates/core/src/react_server_components.rs index af5e959f2c6e1..669f2f1f754b8 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -395,8 +395,14 @@ pub fn server_components( JsWord::from("client-only"), JsWord::from("react-dom/client"), JsWord::from("react-dom/server"), + // TODO-APP: JsWord::from("next/router"), + // TODO-APP: Rule out client hooks. + ], + invalid_client_imports: vec![ + JsWord::from("server-only"), + // TODO-APP: Rule out server hooks such as `useCookies`, `useHeaders`, + // `usePreviewData`. ], - invalid_client_imports: vec![JsWord::from("server-only")], invalid_server_react_dom_apis: vec![ JsWord::from("findDOMNode"), JsWord::from("flushSync"), diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 9dc1ef82d10b5..e92f41c467194 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -765,7 +765,8 @@ describe('app dir', () => { }) describe('next/router', () => { - it('should always return null when accessed from /app', async () => { + // `useRouter` should not be accessible in server components. + it.skip('should always return null when accessed from /app', async () => { const browser = await webdriver(next.url, '/old-router') try { From 72a19960c828f018141249d75e17ecd0f8f5a724 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 21 Sep 2022 18:54:37 +0200 Subject: [PATCH 6/7] always alias react and react-dom based on require.resolved results --- packages/next/build/webpack-config.ts | 35 +++++++++++++-------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index e7994678cff98..b8d62b062586a 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -831,6 +831,14 @@ export default async function getBaseWebpackConfig( [COMPILER_NAMES.edgeServer]: ['browser', 'module', 'main'], } + const reactAliases = { + react: reactDir, + 'react-dom$': reactDomDir, + 'react-dom/server$': `${reactDomDir}/server`, + 'react-dom/server.browser$': `${reactDomDir}/server.browser`, + 'react-dom/client$': `${reactDomDir}/client`, + } + const resolveConfig = { // Disable .mjs for node_modules bundling extensions: isNodeServer @@ -843,11 +851,8 @@ export default async function getBaseWebpackConfig( alias: { next: NEXT_PROJECT_ROOT, - react: `${reactDir}`, - 'react-dom$': `${reactDomDir}`, - 'react-dom/server$': `${reactDomDir}/server`, - 'react-dom/server.browser$': `${reactDomDir}/server.browser`, - 'react-dom/client$': `${reactDomDir}/client`, + ...reactAliases, + 'styled-jsx/style$': require.resolve(`styled-jsx/style`), 'styled-jsx$': require.resolve(`styled-jsx`), @@ -1005,19 +1010,13 @@ export default async function getBaseWebpackConfig( const resolveWithReactServerCondition = layer === WEBPACK_LAYERS.server ? getResolve({ - alias: process.env.__NEXT_REACT_CHANNEL - ? { - react: `react-${process.env.__NEXT_REACT_CHANNEL}`, - 'react/package.json': `react-${process.env.__NEXT_REACT_CHANNEL}/package.json`, - 'react/jsx-runtime': `react-${process.env.__NEXT_REACT_CHANNEL}/jsx-runtime`, - 'react/jsx-dev-runtime': `react-${process.env.__NEXT_REACT_CHANNEL}/jsx-dev-runtime`, - 'react-dom': `react-dom-${process.env.__NEXT_REACT_CHANNEL}`, - 'react-dom/package.json': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/package.json`, - 'react-dom/server': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/server`, - 'react-dom/server.browser': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/server.browser`, - 'react-dom/client': `react-dom-${process.env.__NEXT_REACT_CHANNEL}/client`, - } - : false, + alias: { + ...reactAliases, + 'react/package.json': `${reactDir}/package.json`, + 'react/jsx-runtime': `${reactDir}/jsx-runtime`, + 'react/jsx-dev-runtime': `${reactDir}/jsx-dev-runtime`, + 'react-dom/package.json': `${reactDomDir}/package.json`, + }, conditionNames: ['react-server'], }) : null From a096b8931bad18fc49cccf3622d53fc078d519a3 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 21 Sep 2022 19:29:07 +0200 Subject: [PATCH 7/7] fix alias logic --- packages/next/build/webpack-config.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index b8d62b062586a..48d71e8f4b1ee 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1010,13 +1010,17 @@ export default async function getBaseWebpackConfig( const resolveWithReactServerCondition = layer === WEBPACK_LAYERS.server ? getResolve({ - alias: { - ...reactAliases, - 'react/package.json': `${reactDir}/package.json`, - 'react/jsx-runtime': `${reactDir}/jsx-runtime`, - 'react/jsx-dev-runtime': `${reactDir}/jsx-dev-runtime`, - 'react-dom/package.json': `${reactDomDir}/package.json`, - }, + // If React is aliased to another channel during Next.js' local development, + // we need to provide that alias to webpack's resolver. + alias: process.env.__NEXT_REACT_CHANNEL + ? { + ...reactAliases, + 'react/package.json': `${reactDir}/package.json`, + 'react/jsx-runtime': `${reactDir}/jsx-runtime`, + 'react/jsx-dev-runtime': `${reactDir}/jsx-dev-runtime`, + 'react-dom/package.json': `${reactDomDir}/package.json`, + } + : false, conditionNames: ['react-server'], }) : null