From 32ffff8a41742d116c988027c47aec2f2270992a Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 23 May 2022 21:02:38 +0200 Subject: [PATCH 01/10] dynamically load chunks --- packages/next/build/index.ts | 2 +- packages/next/build/webpack-config.ts | 18 +- .../next-flight-client-entry-loader.ts | 1 + .../webpack/plugins/client-entry-plugin.ts | 192 ++++++++++++++++++ .../webpack/plugins/flight-manifest-plugin.ts | 189 +---------------- packages/next/client/views-next.js | 8 + 6 files changed, 222 insertions(+), 188 deletions(-) create mode 100644 packages/next/build/webpack/plugins/client-entry-plugin.ts diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 22d624b8691a4..61c5ef3e3733e 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -108,7 +108,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' import { recursiveCopy } from '../lib/recursive-copy' import { recursiveReadDir } from '../lib/recursive-readdir' import { lockfilePatchPromise, teardownTraceSubscriber } from './swc' -import { injectedClientEntries } from './webpack/plugins/flight-manifest-plugin' +import { injectedClientEntries } from './webpack/plugins/client-entry-plugin' import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex' import { flatReaddir } from '../lib/flat-readdir' diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 8a3e6331955ee..28e0e30217fda 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -46,6 +46,7 @@ import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin import { regexLikeCss } from './webpack/config/blocks/css' import { CopyFilePlugin } from './webpack/plugins/copy-file-plugin' import { FlightManifestPlugin } from './webpack/plugins/flight-manifest-plugin' +import { ClientEntryPlugin } from './webpack/plugins/client-entry-plugin' import { Feature, SWC_TARGET_TRIPLE, @@ -1182,7 +1183,7 @@ export default async function getBaseWebpackConfig( ? `[name].js` : `../[name].js` : `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${ - dev ? '' : '-[contenthash]' + dev ? '' : viewsDir ? '' : '-[contenthash]' }.js`, library: isClient || isEdgeServer ? '_N_E' : undefined, libraryTarget: isClient || isEdgeServer ? 'assign' : 'commonjs2', @@ -1641,12 +1642,15 @@ export default async function getBaseWebpackConfig( }, }), hasServerComponents && - !isClient && - new FlightManifestPlugin({ - dev, - pageExtensions: rawPageExtensions, - isEdgeServer, - }), + (isClient + ? new FlightManifestPlugin({ + dev, + pageExtensions: rawPageExtensions, + }) + : new ClientEntryPlugin({ + dev, + isEdgeServer, + })), !dev && isClient && new TelemetryPlugin( diff --git a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts index 90bfbc06d2c4f..0c70d0c042888 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -11,6 +11,7 @@ export default async function transformSource(this: any): Promise { ) .join(';') + ` + export const __next_rsc__ = { server: false, __webpack_require__ diff --git a/packages/next/build/webpack/plugins/client-entry-plugin.ts b/packages/next/build/webpack/plugins/client-entry-plugin.ts new file mode 100644 index 0000000000000..2cff965cc9549 --- /dev/null +++ b/packages/next/build/webpack/plugins/client-entry-plugin.ts @@ -0,0 +1,192 @@ +import { stringify } from 'querystring' +import { webpack } from 'next/dist/compiled/webpack/webpack' +import { + EDGE_RUNTIME_WEBPACK, + NEXT_CLIENT_SSR_ENTRY_SUFFIX, +} from '../../../shared/lib/constants' +import { clientComponentRegex } from '../loaders/utils' +import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' +import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path' +import { + getInvalidator, + entries, +} from '../../../server/dev/on-demand-entry-handler' +import { getPageStaticInfo } from '../../analysis/get-page-static-info' + +type Options = { + dev: boolean + isEdgeServer: boolean +} + +const PLUGIN_NAME = 'ClientEntryPlugin' + +export const injectedClientEntries = new Map() + +export class ClientEntryPlugin { + dev: boolean = false + isEdgeServer: boolean + + constructor(options: Options) { + if (typeof options.dev === 'boolean') { + this.dev = options.dev + } + this.isEdgeServer = options.isEdgeServer + } + + apply(compiler: any) { + compiler.hooks.compilation.tap( + PLUGIN_NAME, + (compilation: any, { normalModuleFactory }: any) => { + compilation.dependencyFactories.set( + (webpack as any).dependencies.ModuleDependency, + normalModuleFactory + ) + compilation.dependencyTemplates.set( + (webpack as any).dependencies.ModuleDependency, + new (webpack as any).dependencies.NullDependency.Template() + ) + } + ) + + // Only for webpack 5 + compiler.hooks.finishMake.tapAsync( + PLUGIN_NAME, + async (compilation: any, callback: any) => { + this.createClientEndpoints(compilation, callback) + } + ) + } + + async createClientEndpoints(compilation: any, callback: () => void) { + const context = (this as any).context + const promises: any = [] + + // For each SC server compilation entry, we need to create its corresponding + // client component entry. + for (const [name, entry] of compilation.entries.entries()) { + // Check if the page entry is a server component or not. + const entryDependency = entry.dependencies?.[0] + const request = entryDependency?.request + + if (request && entry.options?.layer === 'sc_server') { + const visited = new Set() + const clientComponentImports: string[] = [] + + function filterClientComponents(dependency: any) { + const module = compilation.moduleGraph.getResolvedModule(dependency) + if (!module) return + + if (visited.has(module.userRequest)) return + visited.add(module.userRequest) + + if (clientComponentRegex.test(module.userRequest)) { + clientComponentImports.push(module.userRequest) + } + + compilation.moduleGraph + .getOutgoingConnections(module) + .forEach((connection: any) => { + filterClientComponents(connection.dependency) + }) + } + + // Traverse the module graph to find all client components. + filterClientComponents(entryDependency) + + const entryModule = + compilation.moduleGraph.getResolvedModule(entryDependency) + const routeInfo = entryModule.buildInfo.route || { + page: denormalizePagePath(name.replace(/^pages/, '')), + absolutePagePath: entryModule.resource, + } + + // Parse gSSP and gSP exports from the page source. + const pageStaticInfo = this.isEdgeServer + ? {} + : await getPageStaticInfo({ + pageFilePath: routeInfo.absolutePagePath, + nextConfig: {}, + isDev: this.dev, + }) + + const clientLoader = `next-flight-client-entry-loader?${stringify({ + modules: clientComponentImports, + runtime: this.isEdgeServer ? 'edge' : 'nodejs', + ssr: pageStaticInfo.ssr, + // Adding name here to make the entry key unique. + name, + })}!` + + const bundlePath = 'pages' + normalizePagePath(routeInfo.page) + + // Inject the entry to the client compiler. + if (this.dev) { + const pageKey = 'client' + routeInfo.page + if (!entries[pageKey]) { + entries[pageKey] = { + bundlePath, + absolutePagePath: routeInfo.absolutePagePath, + clientLoader, + dispose: false, + lastActiveTime: Date.now(), + } as any + const invalidator = getInvalidator() + if (invalidator) { + invalidator.invalidate() + } + } + } else { + injectedClientEntries.set( + bundlePath, + `next-client-pages-loader?${stringify({ + isServerComponent: true, + page: denormalizePagePath(bundlePath.replace(/^pages/, '')), + absolutePagePath: clientLoader, + })}!` + clientLoader + ) + } + + // Inject the entry to the server compiler. + const clientComponentEntryDep = ( + webpack as any + ).EntryPlugin.createDependency( + clientLoader, + name + NEXT_CLIENT_SSR_ENTRY_SUFFIX + ) + promises.push( + new Promise((res, rej) => { + compilation.addEntry( + context, + clientComponentEntryDep, + this.isEdgeServer + ? { + name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, + library: { + name: ['self._CLIENT_ENTRY'], + type: 'assign', + }, + runtime: EDGE_RUNTIME_WEBPACK, + asyncChunks: false, + } + : { + name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, + runtime: 'webpack-runtime', + }, + (err: any) => { + if (err) { + rej(err) + } else { + res() + } + } + ) + }) + ) + } + } + + Promise.all(promises) + .then(() => callback()) + .catch(callback) + } +} diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index f0362f7c1b1a4..247d8cf6dd695 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -5,21 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import { stringify } from 'querystring' +import type webpack5 from 'webpack5' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' -import { - MIDDLEWARE_FLIGHT_MANIFEST, - EDGE_RUNTIME_WEBPACK, - NEXT_CLIENT_SSR_ENTRY_SUFFIX, -} from '../../../shared/lib/constants' +import { MIDDLEWARE_FLIGHT_MANIFEST } from '../../../shared/lib/constants' import { clientComponentRegex } from '../loaders/utils' -import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' -import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path' -import { - getInvalidator, - entries, -} from '../../../server/dev/on-demand-entry-handler' -import { getPageStaticInfo } from '../../analysis/get-page-static-info' // This is the module that will be used to anchor all client references to. // I.e. it will have all the client files as async deps from this point on. @@ -31,27 +20,19 @@ import { getPageStaticInfo } from '../../analysis/get-page-static-info' type Options = { dev: boolean pageExtensions: string[] - isEdgeServer: boolean } const PLUGIN_NAME = 'FlightManifestPlugin' -let edgeFlightManifest = {} -let nodeFlightManifest = {} - -export const injectedClientEntries = new Map() - export class FlightManifestPlugin { dev: boolean = false pageExtensions: string[] - isEdgeServer: boolean constructor(options: Options) { if (typeof options.dev === 'boolean') { this.dev = options.dev } this.pageExtensions = options.pageExtensions - this.isEdgeServer = options.isEdgeServer } apply(compiler: any) { @@ -69,14 +50,6 @@ export class FlightManifestPlugin { } ) - // Only for webpack 5 - compiler.hooks.finishMake.tapAsync( - PLUGIN_NAME, - async (compilation: any, callback: any) => { - this.createClientEndpoints(compilation, callback) - } - ) - compiler.hooks.make.tap(PLUGIN_NAME, (compilation: any) => { compilation.hooks.processAssets.tap( { @@ -89,143 +62,10 @@ export class FlightManifestPlugin { }) } - async createClientEndpoints(compilation: any, callback: () => void) { - const context = (this as any).context - const promises: any = [] - - // For each SC server compilation entry, we need to create its corresponding - // client component entry. - for (const [name, entry] of compilation.entries.entries()) { - // Check if the page entry is a server component or not. - const entryDependency = entry.dependencies?.[0] - const request = entryDependency?.request - - if (request && entry.options?.layer === 'sc_server') { - const visited = new Set() - const clientComponentImports: string[] = [] - - function filterClientComponents(dependency: any) { - const module = compilation.moduleGraph.getResolvedModule(dependency) - if (!module) return - - if (visited.has(module.userRequest)) return - visited.add(module.userRequest) - - if (clientComponentRegex.test(module.userRequest)) { - clientComponentImports.push(module.userRequest) - } - - compilation.moduleGraph - .getOutgoingConnections(module) - .forEach((connection: any) => { - filterClientComponents(connection.dependency) - }) - } - - // Traverse the module graph to find all client components. - filterClientComponents(entryDependency) - - const entryModule = - compilation.moduleGraph.getResolvedModule(entryDependency) - const routeInfo = entryModule.buildInfo.route || { - page: denormalizePagePath(name.replace(/^pages/, '')), - absolutePagePath: entryModule.resource, - } - - // Parse gSSP and gSP exports from the page source. - const pageStaticInfo = this.isEdgeServer - ? {} - : await getPageStaticInfo({ - pageFilePath: routeInfo.absolutePagePath, - nextConfig: {}, - isDev: this.dev, - }) - - const clientLoader = `next-flight-client-entry-loader?${stringify({ - modules: clientComponentImports, - runtime: this.isEdgeServer ? 'edge' : 'nodejs', - ssr: pageStaticInfo.ssr, - // Adding name here to make the entry key unique. - name, - })}!` - - const bundlePath = 'pages' + normalizePagePath(routeInfo.page) - - // Inject the entry to the client compiler. - if (this.dev) { - const pageKey = 'client' + routeInfo.page - if (!entries[pageKey]) { - entries[pageKey] = { - bundlePath, - absolutePagePath: routeInfo.absolutePagePath, - clientLoader, - dispose: false, - lastActiveTime: Date.now(), - } as any - const invalidator = getInvalidator() - if (invalidator) { - invalidator.invalidate() - } - } - } else { - injectedClientEntries.set( - bundlePath, - `next-client-pages-loader?${stringify({ - isServerComponent: true, - page: denormalizePagePath(bundlePath.replace(/^pages/, '')), - absolutePagePath: clientLoader, - })}!` + clientLoader - ) - } - - // Inject the entry to the server compiler. - const clientComponentEntryDep = ( - webpack as any - ).EntryPlugin.createDependency( - clientLoader, - name + NEXT_CLIENT_SSR_ENTRY_SUFFIX - ) - promises.push( - new Promise((res, rej) => { - compilation.addEntry( - context, - clientComponentEntryDep, - this.isEdgeServer - ? { - name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, - library: { - name: ['self._CLIENT_ENTRY'], - type: 'assign', - }, - runtime: EDGE_RUNTIME_WEBPACK, - asyncChunks: false, - } - : { - name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, - runtime: 'webpack-runtime', - }, - (err: any) => { - if (err) { - rej(err) - } else { - res() - } - } - ) - }) - ) - } - } - - Promise.all(promises) - .then(() => callback()) - .catch(callback) - } - createAsset(assets: any, compilation: any) { const manifest: any = {} compilation.chunkGroups.forEach((chunkGroup: any) => { - function recordModule(id: string, _chunk: any, mod: any) { + function recordModule(chunk: any, id: string, mod: any) { const resource = mod.resource // TODO: Hook into deps instead of the target module. @@ -258,7 +98,7 @@ export class FlightManifestPlugin { moduleExports[name] = { id: id.replace(/^\(sc_server\)\//, ''), name, - chunks: [], + chunks: chunk.ids, } } }) @@ -277,8 +117,10 @@ export class FlightManifestPlugin { modId = modId.split('?')[0] // Remove the loader prefix. modId = modId.split('next-flight-client-loader.js!')[1] || modId + modId = modId.replace(/^\(sc_server\)\//, '') + + recordModule(chunk, modId, mod) - recordModule(modId, chunk, mod) // If this is a concatenation, register each child to the parent ID. if (mod.modules) { mod.modules.forEach((concatenatedMod: any) => { @@ -289,21 +131,8 @@ export class FlightManifestPlugin { }) }) - // With switchable runtime, we need to emit the manifest files for both - // runtimes. - if (this.isEdgeServer) { - edgeFlightManifest = manifest - } else { - nodeFlightManifest = manifest - } - const mergedManifest = { - ...nodeFlightManifest, - ...edgeFlightManifest, - } - const file = - (!this.dev && !this.isEdgeServer ? '../' : '') + - MIDDLEWARE_FLIGHT_MANIFEST - const json = JSON.stringify(mergedManifest) + const file = 'server/' + MIDDLEWARE_FLIGHT_MANIFEST + const json = JSON.stringify(manifest) assets[file + '.js'] = new sources.RawSource('self.__RSC_MANIFEST=' + json) assets[file + '.json'] = new sources.RawSource(json) diff --git a/packages/next/client/views-next.js b/packages/next/client/views-next.js index c41d59c227f2f..1b8a787d26883 100644 --- a/packages/next/client/views-next.js +++ b/packages/next/client/views-next.js @@ -5,4 +5,12 @@ window.next = { root: true, } +// Override chunk URL mapping in the webpack runtime +// https://github.com/webpack/webpack/blob/2738eebc7880835d88c727d364ad37f3ec557593/lib/RuntimeGlobals.js#L204 +// @ts-ignore +const getChunkScriptFilename = __webpack_require__.u +__webpack_require__.u = (chunkId) => { + return getChunkScriptFilename(chunkId) || `static/chunks/${chunkId}.js` +} + hydrate() From 102ac4364fcb105a586373f1ab0283475f391f17 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 23 May 2022 21:09:31 +0200 Subject: [PATCH 02/10] enable test --- test/e2e/views-dir/index.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/e2e/views-dir/index.test.ts b/test/e2e/views-dir/index.test.ts index bb33ccb8fd36f..cc3a891aeb46a 100644 --- a/test/e2e/views-dir/index.test.ts +++ b/test/e2e/views-dir/index.test.ts @@ -260,12 +260,11 @@ describe('views dir', () => { expect($('p').text()).toBe('hello from root/client-nested') }) - // TODO: Implement hydration - it.skip('should include it client-side', async () => { + it('should include it client-side', async () => { const browser = await webdriver(next.url, '/client-nested') // After hydration count should be 1 expect(await browser.elementByCss('h1').text()).toBe( - 'Client Nested. Count: 0' + 'Client Nested. Count: 1' ) // After hydration count should be 1 expect(await browser.elementByCss('h1').text()).toBe( From 37267fc7aa5b05623f4633826ec9bfdf592815f5 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 23 May 2022 21:12:02 +0200 Subject: [PATCH 03/10] fix params --- packages/next/build/webpack/plugins/flight-manifest-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 247d8cf6dd695..7fc57e633c904 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -124,7 +124,7 @@ export class FlightManifestPlugin { // If this is a concatenation, register each child to the parent ID. if (mod.modules) { mod.modules.forEach((concatenatedMod: any) => { - recordModule(modId, chunk, concatenatedMod) + recordModule(chunk, modId, concatenatedMod) }) } } From 8a707496e1b2bed8803c9877b6f8209e688c2075 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 23 May 2022 21:28:15 +0200 Subject: [PATCH 04/10] fix --- .../next/build/webpack/plugins/flight-manifest-plugin.ts | 1 - packages/next/client/views-next.js | 5 ++++- test/e2e/views-dir/index.test.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 7fc57e633c904..2c47526e0ca9f 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import type webpack5 from 'webpack5' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { MIDDLEWARE_FLIGHT_MANIFEST } from '../../../shared/lib/constants' import { clientComponentRegex } from '../loaders/utils' diff --git a/packages/next/client/views-next.js b/packages/next/client/views-next.js index 1b8a787d26883..515afe410257e 100644 --- a/packages/next/client/views-next.js +++ b/packages/next/client/views-next.js @@ -7,8 +7,11 @@ window.next = { // Override chunk URL mapping in the webpack runtime // https://github.com/webpack/webpack/blob/2738eebc7880835d88c727d364ad37f3ec557593/lib/RuntimeGlobals.js#L204 -// @ts-ignore + +// eslint-disable-next-line no-undef const getChunkScriptFilename = __webpack_require__.u + +// eslint-disable-next-line no-undef __webpack_require__.u = (chunkId) => { return getChunkScriptFilename(chunkId) || `static/chunks/${chunkId}.js` } diff --git a/test/e2e/views-dir/index.test.ts b/test/e2e/views-dir/index.test.ts index cc3a891aeb46a..a67be2a0e3e99 100644 --- a/test/e2e/views-dir/index.test.ts +++ b/test/e2e/views-dir/index.test.ts @@ -267,7 +267,7 @@ describe('views dir', () => { 'Client Nested. Count: 1' ) // After hydration count should be 1 - expect(await browser.elementByCss('h1').text()).toBe( + expect(await browser.elementByCss('p').text()).toBe( 'hello from root/client-nested' ) }) From 2409a059b431d74d21fb0fd836f41456f5936c10 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 24 May 2022 00:37:26 +0200 Subject: [PATCH 05/10] add hydrated check --- .../app/views/client-component-route/page.client.js | 1 + test/e2e/views-dir/app/views/client-nested/layout.client.js | 1 + test/e2e/views-dir/index.test.ts | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e/views-dir/app/views/client-component-route/page.client.js b/test/e2e/views-dir/app/views/client-component-route/page.client.js index 64b3f98c1f12b..bdfa450686921 100644 --- a/test/e2e/views-dir/app/views/client-component-route/page.client.js +++ b/test/e2e/views-dir/app/views/client-component-route/page.client.js @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react' export default function ClientComponentRoute() { const [count, setCount] = useState(0) useEffect(() => { + window.hydrated = true setCount(1) }, [count]) return ( diff --git a/test/e2e/views-dir/app/views/client-nested/layout.client.js b/test/e2e/views-dir/app/views/client-nested/layout.client.js index 6f835e03f4c60..2f5a8be77f7d7 100644 --- a/test/e2e/views-dir/app/views/client-nested/layout.client.js +++ b/test/e2e/views-dir/app/views/client-nested/layout.client.js @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react' export default function ClientNestedLayout({ children }) { const [count, setCount] = useState(0) useEffect(() => { + window.hydrated = true setCount(1) }, []) return ( diff --git a/test/e2e/views-dir/index.test.ts b/test/e2e/views-dir/index.test.ts index a67be2a0e3e99..aceb12e90c419 100644 --- a/test/e2e/views-dir/index.test.ts +++ b/test/e2e/views-dir/index.test.ts @@ -240,9 +240,9 @@ describe('views dir', () => { ) }) - // TODO: Implement hydration - it.skip('should serve client-side', async () => { + it('should serve client-side', async () => { const browser = await webdriver(next.url, '/client-component-route') + await browser.waitForCondition('window.hydrated') // After hydration count should be 1 expect(await browser.elementByCss('p').text()).toBe( 'hello from root/client-component-route. count: 1' @@ -262,6 +262,7 @@ describe('views dir', () => { it('should include it client-side', async () => { const browser = await webdriver(next.url, '/client-nested') + await browser.waitForCondition('window.hydrated') // After hydration count should be 1 expect(await browser.elementByCss('h1').text()).toBe( 'Client Nested. Count: 1' From 840a43ab0275a564f7f57ffac18487aeff92bb39 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 24 May 2022 01:18:39 +0200 Subject: [PATCH 06/10] tweak tests --- .../app/views/client-component-route/page.client.js | 2 +- .../app/views/client-nested/layout.client.js | 1 - test/e2e/views-dir/index.test.ts | 13 +++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/e2e/views-dir/app/views/client-component-route/page.client.js b/test/e2e/views-dir/app/views/client-component-route/page.client.js index bdfa450686921..2d1870bdc396d 100644 --- a/test/e2e/views-dir/app/views/client-component-route/page.client.js +++ b/test/e2e/views-dir/app/views/client-component-route/page.client.js @@ -1,8 +1,8 @@ import { useState, useEffect } from 'react' + export default function ClientComponentRoute() { const [count, setCount] = useState(0) useEffect(() => { - window.hydrated = true setCount(1) }, [count]) return ( diff --git a/test/e2e/views-dir/app/views/client-nested/layout.client.js b/test/e2e/views-dir/app/views/client-nested/layout.client.js index 2f5a8be77f7d7..6f835e03f4c60 100644 --- a/test/e2e/views-dir/app/views/client-nested/layout.client.js +++ b/test/e2e/views-dir/app/views/client-nested/layout.client.js @@ -3,7 +3,6 @@ import { useState, useEffect } from 'react' export default function ClientNestedLayout({ children }) { const [count, setCount] = useState(0) useEffect(() => { - window.hydrated = true setCount(1) }, []) return ( diff --git a/test/e2e/views-dir/index.test.ts b/test/e2e/views-dir/index.test.ts index aceb12e90c419..4717fb370ed35 100644 --- a/test/e2e/views-dir/index.test.ts +++ b/test/e2e/views-dir/index.test.ts @@ -242,10 +242,10 @@ describe('views dir', () => { it('should serve client-side', async () => { const browser = await webdriver(next.url, '/client-component-route') - await browser.waitForCondition('window.hydrated') + // After hydration count should be 1 - expect(await browser.elementByCss('p').text()).toBe( - 'hello from root/client-component-route. count: 1' + await browser.waitForCondition( + 'document.querySelector("p").textContent === "hello from root/client-component-route. count: 1"' ) }) }) @@ -262,11 +262,12 @@ describe('views dir', () => { it('should include it client-side', async () => { const browser = await webdriver(next.url, '/client-nested') - await browser.waitForCondition('window.hydrated') + // After hydration count should be 1 - expect(await browser.elementByCss('h1').text()).toBe( - 'Client Nested. Count: 1' + await browser.waitForCondition( + 'document.querySelector("h1").textContent === "Client Nested. Count: 1"' ) + // After hydration count should be 1 expect(await browser.elementByCss('p').text()).toBe( 'hello from root/client-nested' From 3bbdb7e581acf634531b8e4bb38d21cdeb17e98e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 24 May 2022 02:26:10 +0200 Subject: [PATCH 07/10] change back tests --- test/e2e/views-dir/index.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/views-dir/index.test.ts b/test/e2e/views-dir/index.test.ts index 4717fb370ed35..308c8a5e16393 100644 --- a/test/e2e/views-dir/index.test.ts +++ b/test/e2e/views-dir/index.test.ts @@ -244,8 +244,8 @@ describe('views dir', () => { const browser = await webdriver(next.url, '/client-component-route') // After hydration count should be 1 - await browser.waitForCondition( - 'document.querySelector("p").textContent === "hello from root/client-component-route. count: 1"' + expect(await browser.elementByCss('p').text()).toBe( + 'hello from root/client-component-route. count: 1' ) }) }) @@ -264,8 +264,8 @@ describe('views dir', () => { const browser = await webdriver(next.url, '/client-nested') // After hydration count should be 1 - await browser.waitForCondition( - 'document.querySelector("h1").textContent === "Client Nested. Count: 1"' + expect(await browser.elementByCss('h1').text()).toBe( + 'Client Nested. Count: 1' ) // After hydration count should be 1 From d29dca63c2c17ba81b9cebbac376b8e8c4450082 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 24 May 2022 02:40:27 +0200 Subject: [PATCH 08/10] bug fix --- packages/next/build/webpack-config.ts | 1 + .../build/webpack/loaders/next-flight-client-entry-loader.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 28e0e30217fda..51e1336f82bca 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1017,6 +1017,7 @@ export default async function getBaseWebpackConfig( ? { // We have to use the names here instead of hashes to ensure the consistency between compilers. moduleIds: 'named', + chunkIds: 'named', } : {}), splitChunks: ((): webpack.Options.SplitChunksOptions | false => { diff --git a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts index 0c70d0c042888..90bfbc06d2c4f 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -11,7 +11,6 @@ export default async function transformSource(this: any): Promise { ) .join(';') + ` - export const __next_rsc__ = { server: false, __webpack_require__ From f7f7d3033baeea1ba8370bf6b72cb3a247c2f7f2 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 24 May 2022 14:48:01 +0200 Subject: [PATCH 09/10] add missing __webpack_chunk_load__ --- packages/next/server/render.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index f0ca6037fce78..9f5f8ae848463 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -486,6 +486,8 @@ export async function renderToHTML( // @ts-ignore globalThis.__webpack_require__ = ComponentMod.__next_rsc__.__webpack_require__ + // @ts-ignore + globalThis.__webpack_chunk_load__ = () => Promise.resolve() Component = createServerComponentRenderer(Component, { cachePrefix: pathname + (search ? `?${search}` : ''), From 56afc19c15ce8c8981d09c8738b4d5bb8c427453 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 24 May 2022 15:33:19 +0200 Subject: [PATCH 10/10] only include chunks for viewsDir --- packages/next/build/webpack-config.ts | 1 + .../next/build/webpack/plugins/flight-manifest-plugin.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 51e1336f82bca..f78bb1bd3586e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1646,6 +1646,7 @@ export default async function getBaseWebpackConfig( (isClient ? new FlightManifestPlugin({ dev, + viewsDir: !!config.experimental.viewsDir, pageExtensions: rawPageExtensions, }) : new ClientEntryPlugin({ diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 2c47526e0ca9f..e0b20b1a244d6 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -18,6 +18,7 @@ import { clientComponentRegex } from '../loaders/utils' type Options = { dev: boolean + viewsDir: boolean pageExtensions: string[] } @@ -26,11 +27,13 @@ const PLUGIN_NAME = 'FlightManifestPlugin' export class FlightManifestPlugin { dev: boolean = false pageExtensions: string[] + viewsDir: boolean = false constructor(options: Options) { if (typeof options.dev === 'boolean') { this.dev = options.dev } + this.viewsDir = options.viewsDir this.pageExtensions = options.pageExtensions } @@ -63,6 +66,8 @@ export class FlightManifestPlugin { createAsset(assets: any, compilation: any) { const manifest: any = {} + const viewsDir = this.viewsDir + compilation.chunkGroups.forEach((chunkGroup: any) => { function recordModule(chunk: any, id: string, mod: any) { const resource = mod.resource @@ -97,7 +102,7 @@ export class FlightManifestPlugin { moduleExports[name] = { id: id.replace(/^\(sc_server\)\//, ''), name, - chunks: chunk.ids, + chunks: viewsDir ? chunk.ids : [], } } })