From 11deaaa82bda301254167967903a384ef6c4c51f Mon Sep 17 00:00:00 2001 From: Jimmy Lai Date: Tue, 27 Sep 2022 01:56:16 +0200 Subject: [PATCH] edge-ssr: bundle next/dist as ESM for better tree-shaking (#40251) # Context Edge SSR'd routes cold boot performances are proportional to the executed code size. In order to improve it, we are trying to optimize for the bundle size of a packed Edge SSR route. This PR adds ESM compilation targets for all Next.js dist packages and use them to bundle Edge SSR'd route. This allows us to leverage the better tree shaking/DCE for ESM modules in webpack in order to decrease the overall bundle size. This PR also enables minifying Edge SSR routes. Since we don't control which minifier might be used later (if any), it's best if we provide an already optimised bundle. image This is a 10ms cold boot win per my benchmarking script, which I'll put in a subsequent PR. Not done yet: - ~~swap exported requires in `next/link` (and others) etc to point them to the esm modules version~~ ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) Co-authored-by: JJ Kasper Co-authored-by: Shu Ding --- packages/next/amp.js | 5 +- packages/next/app.js | 5 +- packages/next/build/entries.ts | 3 + packages/next/build/webpack-config.ts | 24 ++-- .../build/webpack/loaders/next-app-loader.ts | 18 +-- .../loaders/next-edge-ssr-loader/index.ts | 35 ++++-- .../loaders/next-edge-ssr-loader/render.ts | 1 + .../plugins/flight-client-entry-plugin.ts | 16 ++- .../webpack/plugins/flight-manifest-plugin.ts | 20 ++++ .../build/webpack/plugins/telemetry-plugin.ts | 14 ++- packages/next/client.js | 5 +- packages/next/config.js | 5 +- packages/next/constants.js | 5 +- packages/next/document.js | 5 +- packages/next/dynamic.js | 5 +- packages/next/error.js | 5 +- packages/next/head.js | 5 +- packages/next/image.js | 5 +- packages/next/link.js | 5 +- packages/next/pages/_app.tsx | 6 +- packages/next/router.js | 5 +- packages/next/script.js | 5 +- packages/next/server/app-render.tsx | 11 +- packages/next/server/dev/hot-reloader.ts | 2 + packages/next/taskfile-swc.js | 7 +- packages/next/taskfile.js | 106 ++++++++++++++++++ 26 files changed, 273 insertions(+), 55 deletions(-) diff --git a/packages/next/amp.js b/packages/next/amp.js index 9c3c257022d4b..882ccb799815c 100644 --- a/packages/next/amp.js +++ b/packages/next/amp.js @@ -1 +1,4 @@ -module.exports = require('./dist/shared/lib/amp') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/shared/lib/amp') + : require('./dist/shared/lib/amp') diff --git a/packages/next/app.js b/packages/next/app.js index 5437bb67ccd1b..b51ec15930da2 100644 --- a/packages/next/app.js +++ b/packages/next/app.js @@ -1 +1,4 @@ -module.exports = require('./dist/pages/_app') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/pages/_app') + : require('./dist/pages/_app') diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 6b0a6e0a39025..2419462029a6c 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -219,6 +219,7 @@ export function getAppEntry(opts: { appDir: string appPaths: string[] | null pageExtensions: string[] + nextRuntime: string }) { return { import: `next-app-loader?${stringify(opts)}!`, @@ -455,6 +456,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { appDir, appPaths: matchedAppPaths, pageExtensions, + nextRuntime: 'nodejs', }) } else if (isTargetLikeServerless(target)) { if (page !== '/_app' && page !== '/_document') { @@ -479,6 +481,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { appDir: appDir!, appPaths: matchedAppPaths, pageExtensions, + nextRuntime: 'edge', }).import } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index b9f94157fdde1..3c433b7c4b180 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -139,11 +139,9 @@ export function getDefineEnv({ }), // TODO: enforce `NODE_ENV` on `process.env`, and add a test: 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), - ...((isNodeServer || isEdgeServer) && { - 'process.env.NEXT_RUNTIME': JSON.stringify( - isEdgeServer ? 'edge' : 'nodejs' - ), - }), + 'process.env.NEXT_RUNTIME': JSON.stringify( + isEdgeServer ? 'edge' : isNodeServer ? 'nodejs' : undefined + ), 'process.env.__NEXT_MIDDLEWARE_MATCHERS': JSON.stringify( middlewareMatchers || [] ), @@ -685,9 +683,9 @@ export default async function getBaseWebpackConfig( const pageExtensions = config.pageExtensions const babelIncludeRegexes: RegExp[] = [ - /next[\\/]dist[\\/]shared[\\/]lib/, - /next[\\/]dist[\\/]client/, - /next[\\/]dist[\\/]pages/, + /next[\\/]dist[\\/](esm[\\/])?shared[\\/]lib/, + /next[\\/]dist[\\/](esm[\\/])?client/, + /next[\\/]dist[\\/](esm[\\/])?pages/, /[\\/](strip-ansi|ansi-regex)[\\/]/, /styled-jsx[\\/]/, ] @@ -796,7 +794,7 @@ export default async function getBaseWebpackConfig( return prev }, [] as string[]) : []), - 'next/dist/pages/_app.js', + isEdgeServer ? 'next/dist/esm/pages/_app.js' : 'next/dist/pages/_app.js', ] customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [ ...(pagesDir @@ -805,7 +803,9 @@ export default async function getBaseWebpackConfig( return prev }, [] as string[]) : []), - 'next/dist/pages/_error.js', + isEdgeServer + ? 'next/dist/esm/pages/_error.js' + : 'next/dist/pages/_error.js', ] customDocumentAliases[`${PAGES_DIR_ALIAS}/_document`] = [ ...(pagesDir @@ -814,7 +814,9 @@ export default async function getBaseWebpackConfig( return prev }, [] as string[]) : []), - `next/dist/pages/_document.js`, + isEdgeServer + ? `next/dist/esm/pages/_document.js` + : `next/dist/pages/_document.js`, ] } diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index 7dbbf75ebd02c..a5000649f4b73 100644 --- a/packages/next/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/build/webpack/loaders/next-app-loader.ts @@ -120,8 +120,9 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ appDir: string appPaths: string[] | null pageExtensions: string[] + nextRuntime: string }> = async function nextAppLoader() { - const { name, appDir, appPaths, pagePath, pageExtensions } = + const { name, appDir, appPaths, pagePath, pageExtensions, nextRuntime } = this.getOptions() || {} const buildInfo = getModuleBuildInfo((this as any)._module) @@ -179,23 +180,24 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{ resolveParallelSegments, }) + const rootDistFolder = nextRuntime === 'edge' ? 'next/dist/esm' : 'next/dist' const result = ` export ${treeCode} - export const AppRouter = require('next/dist/client/components/app-router.client.js').default - export const LayoutRouter = require('next/dist/client/components/layout-router.client.js').default - export const RenderFromTemplateContext = require('next/dist/client/components/render-from-template-context.client.js').default + export const AppRouter = require('${rootDistFolder}/client/components/app-router.client.js').default + export const LayoutRouter = require('${rootDistFolder}/client/components/layout-router.client.js').default + export const RenderFromTemplateContext = require('${rootDistFolder}/client/components/render-from-template-context.client.js').default export const HotReloader = ${ // Disable HotReloader component in production this.mode === 'development' - ? `require('next/dist/client/components/hot-reloader.client.js').default` + ? `require('${rootDistFolder}/client/components/hot-reloader.client.js').default` : 'null' } - export const staticGenerationAsyncStorage = require('next/dist/client/components/static-generation-async-storage.js').staticGenerationAsyncStorage - export const requestAsyncStorage = require('next/dist/client/components/request-async-storage.js').requestAsyncStorage + export const staticGenerationAsyncStorage = require('${rootDistFolder}/client/components/static-generation-async-storage.js').staticGenerationAsyncStorage + export const requestAsyncStorage = require('${rootDistFolder}/client/components/request-async-storage.js').requestAsyncStorage - export const serverHooks = require('next/dist/client/components/hooks-server-context.js') + export const serverHooks = require('${rootDistFolder}/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/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts index a04f5c9979b9c..19d649f50878d 100644 --- a/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts @@ -18,6 +18,18 @@ export type EdgeSSRLoaderQuery = { hasFontLoaders: boolean } +/* +For pages SSR'd at the edge, we bundle them with the ESM version of Next in order to +benefit from the better tree-shaking and thus, smaller bundle sizes. + +The absolute paths for _app, _error and _document, used in this loader, link to the regular CJS modules. +They are generated in `createPagesMapping` where we don't have access to `isEdgeRuntime`, +so we have to do it here. It's not that bad because it keeps all references to ESM modules magic in this place. +*/ +function swapDistFolderWithEsmDistFolder(path: string) { + return path.replace('next/dist/pages', 'next/dist/esm/pages') +} + export default async function edgeSSRLoader(this: any) { const { dev, @@ -54,9 +66,18 @@ export default async function edgeSSRLoader(this: any) { } const stringifiedPagePath = stringifyRequest(this, absolutePagePath) - const stringifiedAppPath = stringifyRequest(this, absoluteAppPath) - const stringifiedErrorPath = stringifyRequest(this, absoluteErrorPath) - const stringifiedDocumentPath = stringifyRequest(this, absoluteDocumentPath) + const stringifiedAppPath = stringifyRequest( + this, + swapDistFolderWithEsmDistFolder(absoluteAppPath) + ) + const stringifiedErrorPath = stringifyRequest( + this, + swapDistFolderWithEsmDistFolder(absoluteErrorPath) + ) + const stringifiedDocumentPath = stringifyRequest( + this, + swapDistFolderWithEsmDistFolder(absoluteDocumentPath) + ) const stringified500Path = absolute500Path ? stringifyRequest(this, absolute500Path) : null @@ -67,8 +88,8 @@ export default async function edgeSSRLoader(this: any) { )}` const transformed = ` - import { adapter, enhanceGlobals } from 'next/dist/server/web/adapter' - import { getRender } from 'next/dist/build/webpack/loaders/next-edge-ssr-loader/render' + import { adapter, enhanceGlobals } from 'next/dist/esm/server/web/adapter' + import { getRender } from 'next/dist/esm/build/webpack/loaders/next-edge-ssr-loader/render' enhanceGlobals() @@ -77,7 +98,7 @@ export default async function edgeSSRLoader(this: any) { isAppDir ? ` const Document = null - const appRenderToHTML = require('next/dist/server/app-render').renderToHTMLOrFlight + const appRenderToHTML = require('next/dist/esm/server/app-render').renderToHTMLOrFlight const pagesRenderToHTML = null const pageMod = require(${JSON.stringify(pageModPath)}) const appMod = null @@ -87,7 +108,7 @@ export default async function edgeSSRLoader(this: any) { : ` const Document = require(${stringifiedDocumentPath}).default const appRenderToHTML = null - const pagesRenderToHTML = require('next/dist/server/render').renderToHTML + const pagesRenderToHTML = require('next/dist/esm/server/render').renderToHTML const pageMod = require(${stringifiedPagePath}) const appMod = require(${stringifiedAppPath}) const errorMod = require(${stringifiedErrorPath}) diff --git a/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts index 23c175a5a77cd..6210f458c7768 100644 --- a/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -1,4 +1,5 @@ import type { NextConfig } from '../../../../server/config-shared' + import type { DocumentType, AppType } from '../../../../shared/lib/utils' import type { BuildManifest } from '../../../../server/get-page-files' import type { ReactLoadableManifest } from '../../../../server/load-components' 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 d39dce912f92c..22834c6ba086d 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -299,9 +299,19 @@ export class FlightClientEntryPlugin { modules: clientComponentImports, server: false, } - const clientLoader = `next-flight-client-entry-loader?${stringify( - loaderOptions - )}!` + + // For the client entry, we always use the CJS build of Next.js. If the + // server is using the ESM build (when using the Edge runtime), we need to + // replace them. + const clientLoader = `next-flight-client-entry-loader?${stringify({ + modules: this.isEdgeServer + ? clientComponentImports.map((importPath) => + importPath.replace('next/dist/esm/', 'next/dist/') + ) + : clientComponentImports, + server: false, + })}!` + const clientSSRLoader = `next-flight-client-entry-loader?${stringify({ ...loaderOptions, server: true, diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 26567ed8f5a64..f2454b32bf236 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -55,6 +55,9 @@ export type FlightManifest = { __ssr_module_mapping__: { [moduleId: string]: ManifestNode } + __edge_ssr_module_mapping__: { + [moduleId: string]: ManifestNode + } } & { [modulePath: string]: ManifestNode } @@ -138,6 +141,7 @@ export class FlightManifestPlugin { ) { const manifest: FlightManifest = { __ssr_module_mapping__: {}, + __edge_ssr_module_mapping__: {}, } const dev = this.dev @@ -188,6 +192,7 @@ export class FlightManifestPlugin { const moduleExports = manifest[resource] || {} const moduleIdMapping = manifest.__ssr_module_mapping__ + const edgeModuleIdMapping = manifest.__edge_ssr_module_mapping__ // Note that this isn't that reliable as webpack is still possible to assign // additional queries to make sure there's no conflict even using the `named` @@ -296,10 +301,25 @@ export class FlightManifestPlugin { ...moduleExports[name], id: ssrNamedModuleId, } + + edgeModuleIdMapping[id] = edgeModuleIdMapping[id] || {} + edgeModuleIdMapping[id][name] = { + ...moduleExports[name], + id: ssrNamedModuleId.replace(/\/next\/dist\//, '/next/dist/esm/'), + } }) manifest[resource] = moduleExports + + // The client compiler will always use the CJS Next.js build, so here we + // also add the mapping for the ESM build (Edge runtime) to consume. + if (/\/next\/dist\//.test(resource)) { + manifest[resource.replace(/\/next\/dist\//, '/next/dist/esm/')] = + moduleExports + } + manifest.__ssr_module_mapping__ = moduleIdMapping + manifest.__edge_ssr_module_mapping__ = edgeModuleIdMapping } chunkGroup.chunks.forEach((chunk: webpack.Chunk) => { diff --git a/packages/next/build/webpack/plugins/telemetry-plugin.ts b/packages/next/build/webpack/plugins/telemetry-plugin.ts index 2580ba8a411d6..d81e7e64cf7b5 100644 --- a/packages/next/build/webpack/plugins/telemetry-plugin.ts +++ b/packages/next/build/webpack/plugins/telemetry-plugin.ts @@ -110,11 +110,15 @@ function findFeatureInModule(module: Module): Feature | undefined { * dependency. */ function findUniqueOriginModulesInConnections( - connections: Connection[] + connections: Connection[], + originModule: Module ): Set { const originModules = new Set() for (const connection of connections) { - if (!originModules.has(connection.originModule)) { + if ( + !originModules.has(connection.originModule) && + connection.originModule !== originModule + ) { originModules.add(connection.originModule) } } @@ -161,8 +165,10 @@ export class TelemetryPlugin implements webpack.WebpackPluginInstance { const connections = ( compilation as any ).moduleGraph.getIncomingConnections(module) - const originModules = - findUniqueOriginModulesInConnections(connections) + const originModules = findUniqueOriginModulesInConnections( + connections, + module + ) this.usageTracker.get(feature)!.invocationCount = originModules.size } diff --git a/packages/next/client.js b/packages/next/client.js index ff71a4ae0c8f1..3f049f0bd0420 100644 --- a/packages/next/client.js +++ b/packages/next/client.js @@ -1 +1,4 @@ -module.exports = require('./dist/client/index') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/client/index') + : require('./dist/client/index') diff --git a/packages/next/config.js b/packages/next/config.js index 2da980d8b0065..bc591fa891565 100644 --- a/packages/next/config.js +++ b/packages/next/config.js @@ -1 +1,4 @@ -module.exports = require('./dist/shared/lib/runtime-config') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/shared/lib/runtime-config') + : require('./dist/shared/lib/runtime-config') diff --git a/packages/next/constants.js b/packages/next/constants.js index 6e690109937c5..cea2032c0f568 100644 --- a/packages/next/constants.js +++ b/packages/next/constants.js @@ -1 +1,4 @@ -module.exports = require('./dist/shared/lib/constants') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/shared/lib/constants') + : require('./dist/shared/lib/constants') diff --git a/packages/next/document.js b/packages/next/document.js index 5741035f38add..22296a77bb16a 100644 --- a/packages/next/document.js +++ b/packages/next/document.js @@ -1 +1,4 @@ -module.exports = require('./dist/pages/_document') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/pages/_document') + : require('./dist/pages/_document') diff --git a/packages/next/dynamic.js b/packages/next/dynamic.js index e2956e5f40337..e9848e7f0dc1e 100644 --- a/packages/next/dynamic.js +++ b/packages/next/dynamic.js @@ -1 +1,4 @@ -module.exports = require('./dist/shared/lib/dynamic') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/shared/lib/dynamic') + : require('./dist/shared/lib/dynamic') diff --git a/packages/next/error.js b/packages/next/error.js index 899cd04662710..653e9316b7799 100644 --- a/packages/next/error.js +++ b/packages/next/error.js @@ -1 +1,4 @@ -module.exports = require('./dist/pages/_error') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/pages/_error') + : require('./dist/pages/_error') diff --git a/packages/next/head.js b/packages/next/head.js index 71758fdbeaa5c..69e6800ae14ca 100644 --- a/packages/next/head.js +++ b/packages/next/head.js @@ -1 +1,4 @@ -module.exports = require('./dist/shared/lib/head') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/shared/lib/head') + : require('./dist/shared/lib/head') diff --git a/packages/next/image.js b/packages/next/image.js index a1de5ad69891a..eae3690cd153a 100644 --- a/packages/next/image.js +++ b/packages/next/image.js @@ -1 +1,4 @@ -module.exports = require('./dist/client/image') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/client/image') + : require('./dist/client/image') diff --git a/packages/next/link.js b/packages/next/link.js index a37307be2e738..d3c35c1ca5731 100644 --- a/packages/next/link.js +++ b/packages/next/link.js @@ -1 +1,4 @@ -module.exports = require('./dist/client/link') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/client/link') + : require('./dist/client/link') diff --git a/packages/next/pages/_app.tsx b/packages/next/pages/_app.tsx index c646d0842fea0..4d83e27592982 100644 --- a/packages/next/pages/_app.tsx +++ b/packages/next/pages/_app.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { - loadGetInitialProps, + +import type { AppContextType, AppInitialProps, AppPropsType, @@ -9,6 +9,8 @@ import { } from '../shared/lib/utils' import type { Router } from '../client/router' +import { loadGetInitialProps } from '../shared/lib/utils' + export { AppInitialProps, AppType } export { NextWebVitalsMetric } diff --git a/packages/next/router.js b/packages/next/router.js index afdc8f9ad2eb6..9b8d7bbbe2183 100644 --- a/packages/next/router.js +++ b/packages/next/router.js @@ -1 +1,4 @@ -module.exports = require('./dist/client/router') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/client/router') + : require('./dist/client/router') diff --git a/packages/next/script.js b/packages/next/script.js index 4e0f885ac60a3..6d257fa56f157 100644 --- a/packages/next/script.js +++ b/packages/next/script.js @@ -1 +1,4 @@ -module.exports = require('./dist/client/script') +module.exports = + process.env.NEXT_RUNTIME === 'edge' + ? require('./dist/esm/client/script') + : require('./dist/client/script') diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index dc79969c9684d..3d1120d96590c 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -258,7 +258,10 @@ function useFlightResponse( const [renderStream, forwardStream] = readableStreamTee(req) const res = createFromReadableStream(renderStream, { - moduleMap: serverComponentManifest.__ssr_module_mapping__, + moduleMap: + process.env.NEXT_RUNTIME === 'edge' + ? serverComponentManifest.__edge_ssr_module_mapping__ + : serverComponentManifest.__ssr_module_mapping__, }) flightResponseRef.current = res @@ -270,7 +273,7 @@ function useFlightResponse( ? `` writer.write(encodeText(scripts)) - process() + read() } }) } - process() + read() return res } diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index cdf1c7c00904a..6a92fe1a1ddf7 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -626,6 +626,7 @@ export default class HotReloader { ), appDir: this.appDir!, pageExtensions: this.config.pageExtensions, + nextRuntime: 'edge', }).import : undefined @@ -705,6 +706,7 @@ export default class HotReloader { ), appDir: this.appDir!, pageExtensions: this.config.pageExtensions, + nextRuntime: 'nodejs', }) : relativeRequest, appDir: this.config.experimental.appDir, diff --git a/packages/next/taskfile-swc.js b/packages/next/taskfile-swc.js index bf61332c316a8..ea4bcf40ddec2 100644 --- a/packages/next/taskfile-swc.js +++ b/packages/next/taskfile-swc.js @@ -18,6 +18,7 @@ module.exports = function (task) { stripExtension, keepImportAssertions = false, interopClientDefaultExport = false, + esm = false, } = {} ) { // Don't compile .d.ts @@ -28,7 +29,7 @@ module.exports = function (task) { /** @type {import('@swc/core').Options} */ const swcClientOptions = { module: { - type: 'commonjs', + type: esm ? 'es6' : 'commonjs', ignoreDynamic: true, }, jsc: { @@ -59,7 +60,7 @@ module.exports = function (task) { /** @type {import('@swc/core').Options} */ const swcServerOptions = { module: { - type: 'commonjs', + type: esm ? 'es6' : 'commonjs', ignoreDynamic: true, }, env: { @@ -126,7 +127,7 @@ module.exports = function (task) { } if (output.map) { - if (interopClientDefaultExport) { + if (interopClientDefaultExport && !esm) { output.code += ` if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') { Object.defineProperty(exports.default, '__esModule', { value: true }); diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 51c672b32a287..b91bf3be9ffa6 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1935,16 +1935,23 @@ export async function compile(task, opts) { 'cli', 'bin', 'server', + 'server_esm', 'nextbuild', 'nextbuildjest', 'nextbuildstatic', + 'nextbuild_esm', 'pages', + 'pages_esm', 'lib', + 'lib_esm', 'client', + 'client_esm', 'telemetry', 'trace', 'shared', + 'shared_esm', 'shared_re_exported', + 'shared_re_exported_esm', 'server_wasm', // we compile this each time so that fresh runtime data is pulled // before each publish @@ -1979,6 +1986,14 @@ export async function lib(task, opts) { notify('Compiled lib files') } +export async function lib_esm(task, opts) { + await task + .source(opts.src || 'lib/**/*.+(js|ts|tsx)') + .swc('server', { dev: opts.dev, esm: true }) + .target('dist/esm/lib') + notify('Compiled lib files') +} + export async function server(task, opts) { await task .source(opts.src || 'server/**/*.+(js|ts|tsx)') @@ -1993,6 +2008,14 @@ export async function server(task, opts) { notify('Compiled server files') } +export async function server_esm(task, opts) { + await task + .source(opts.src || 'server/**/*.+(js|ts|tsx)') + .swc('server', { dev: opts.dev, esm: true }) + .target('dist/esm/server') + notify('Compiled server files to ESM') +} + export async function nextbuild(task, opts) { await task .source(opts.src || 'build/**/*.+(js|ts|tsx)', { @@ -2003,6 +2026,16 @@ export async function nextbuild(task, opts) { notify('Compiled build files') } +export async function nextbuild_esm(task, opts) { + await task + .source(opts.src || 'build/**/*.+(js|ts|tsx)', { + ignore: ['**/fixture/**', '**/tests/**', '**/jest/**'], + }) + .swc('server', { dev: opts.dev, esm: true }) + .target('dist/esm/build') + notify('Compiled build files to ESM') +} + export async function nextbuildjest(task, opts) { await task .source(opts.src || 'build/jest/**/*.+(js|ts|tsx)', { @@ -2021,6 +2054,14 @@ export async function client(task, opts) { notify('Compiled client files') } +export async function client_esm(task, opts) { + await task + .source(opts.src || 'client/**/*.+(js|ts|tsx)') + .swc('client', { dev: opts.dev, esm: true }) + .target('dist/esm/client') + notify('Compiled client files to ESM') +} + // export is a reserved keyword for functions export async function nextbuildstatic(task, opts) { await task @@ -2051,10 +2092,38 @@ export async function pages_document(task, opts) { .target('dist/pages') } +export async function pages_app_esm(task, opts) { + await task + .source('pages/_app.tsx') + .swc('client', { dev: opts.dev, keepImportAssertions: true, esm: true }) + .target('dist/esm/pages') +} + +export async function pages_error_esm(task, opts) { + await task + .source('pages/_error.tsx') + .swc('client', { dev: opts.dev, keepImportAssertions: true, esm: true }) + .target('dist/esm/pages') +} + +export async function pages_document_esm(task, opts) { + await task + .source('pages/_document.tsx') + .swc('server', { dev: opts.dev, keepImportAssertions: true, esm: true }) + .target('dist/esm/pages') +} + export async function pages(task, opts) { await task.parallel(['pages_app', 'pages_error', 'pages_document'], opts) } +export async function pages_esm(task, opts) { + await task.parallel( + ['pages_app_esm', 'pages_error_esm', 'pages_document_esm'], + opts + ) +} + export async function telemetry(task, opts) { await task .source(opts.src || 'telemetry/**/*.+(js|ts|tsx)') @@ -2082,11 +2151,15 @@ export default async function (task) { await task.watch('bin/*', 'bin', opts) await task.watch('pages/**/*.+(js|ts|tsx)', 'pages', opts) await task.watch('server/**/*.+(js|ts|tsx)', 'server', opts) + await task.watch('server/**/*.+(js|ts|tsx)', 'server_esm', opts) await task.watch('build/**/*.+(js|ts|tsx)', 'nextbuild', opts) + await task.watch('build/**/*.+(js|ts|tsx)', 'nextbuild_esm', opts) await task.watch('build/jest/**/*.+(js|ts|tsx)', 'nextbuildjest', opts) await task.watch('export/**/*.+(js|ts|tsx)', 'nextbuildstatic', opts) await task.watch('client/**/*.+(js|ts|tsx)', 'client', opts) + await task.watch('client/**/*.+(js|ts|tsx)', 'client_esm', opts) await task.watch('lib/**/*.+(js|ts|tsx)', 'lib', opts) + await task.watch('lib/**/*.+(js|ts|tsx)', 'lib_esm', opts) await task.watch('cli/**/*.+(js|ts|tsx)', 'cli', opts) await task.watch('telemetry/**/*.+(js|ts|tsx)', 'telemetry', opts) await task.watch('trace/**/*.+(js|ts|tsx)', 'trace', opts) @@ -2100,6 +2173,16 @@ export default async function (task) { 'shared', opts ) + await task.watch( + 'shared/**/!(amp|config|constants|dynamic|head).+(js|ts|tsx)', + 'shared_esm', + opts + ) + await task.watch( + 'shared/lib/{amp,config,constants,dynamic,head}.+(js|ts|tsx)', + 'shared_re_exported_esm', + opts + ) await task.watch('server/**/*.+(wasm)', 'server_wasm', opts) await task.watch( '../react-dev-overlay/dist/**/*.js', @@ -2119,6 +2202,16 @@ export async function shared(task, opts) { notify('Compiled shared files') } +export async function shared_esm(task, opts) { + await task + .source( + opts.src || 'shared/**/!(amp|config|constants|dynamic|head).+(js|ts|tsx)' + ) + .swc('client', { dev: opts.dev, esm: true }) + .target('dist/esm/shared') + notify('Compiled shared files to ESM') +} + export async function shared_re_exported(task, opts) { await task .source( @@ -2130,6 +2223,19 @@ export async function shared_re_exported(task, opts) { notify('Compiled shared re-exported files') } +export async function shared_re_exported_esm(task, opts) { + await task + .source( + opts.src || 'shared/**/{amp,config,constants,dynamic,head}.+(js|ts|tsx)' + ) + .swc('client', { + dev: opts.dev, + esm: true, + }) + .target('dist/esm/shared') + notify('Compiled shared re-exported files as ESM') +} + export async function server_wasm(task, opts) { await task.source(opts.src || 'server/**/*.+(wasm)').target('dist/server') notify('Moved server wasm files')