From e1da07e75a7b8cf96d4d1d6f8c2b6aa3b81f9a4a Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 1 Oct 2024 02:15:18 +0200 Subject: [PATCH] Externalize node binary modules for app router (#70646) backport #70330 Fixes #69912 --------- Co-authored-by: JJ Kasper --- packages/next/src/build/webpack-config.ts | 13 +++++++++ .../next-error-browser-binary-loader.ts | 11 +++++++ .../loaders/next-server-binary-loader.ts | 14 +++++++++ .../app/layout.tsx | 8 +++++ .../app/page.tsx | 7 +++++ ...ernalize-node-binary-browser-error.test.ts | 29 +++++++++++++++++++ .../foo-browser-import-binary/binary.node | 1 + .../foo-browser-import-binary/index.js | 10 +++++++ .../foo-browser-import-binary/package.json | 4 +++ .../externalize-node-binary/app/layout.tsx | 8 +++++ .../externalize-node-binary/app/page.tsx | 5 ++++ .../externalize-node-binary.test.ts | 15 ++++++++++ .../node_modules/foo/binary.node | 1 + .../node_modules/foo/index.js | 9 ++++++ .../node_modules/foo/package.json | 4 +++ 15 files changed, 139 insertions(+) create mode 100644 packages/next/src/build/webpack/loaders/next-error-browser-binary-loader.ts create mode 100644 packages/next/src/build/webpack/loaders/next-server-binary-loader.ts create mode 100644 test/development/app-dir/externalize-node-binary-browser-error/app/layout.tsx create mode 100644 test/development/app-dir/externalize-node-binary-browser-error/app/page.tsx create mode 100644 test/development/app-dir/externalize-node-binary-browser-error/externalize-node-binary-browser-error.test.ts create mode 100644 test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/binary.node create mode 100644 test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/index.js create mode 100644 test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/package.json create mode 100644 test/e2e/app-dir/externalize-node-binary/app/layout.tsx create mode 100644 test/e2e/app-dir/externalize-node-binary/app/page.tsx create mode 100644 test/e2e/app-dir/externalize-node-binary/externalize-node-binary.test.ts create mode 100644 test/e2e/app-dir/externalize-node-binary/node_modules/foo/binary.node create mode 100644 test/e2e/app-dir/externalize-node-binary/node_modules/foo/index.js create mode 100644 test/e2e/app-dir/externalize-node-binary/node_modules/foo/package.json diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index a4711bc4b9a07..6e84ea45b5fba 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1192,6 +1192,8 @@ export default async function getBaseWebpackConfig( 'next-metadata-route-loader', 'modularize-import-loader', 'next-barrel-loader', + 'next-server-binary-loader', + 'next-error-browser-binary-loader', ].reduce((alias, loader) => { // using multiple aliases to replace `resolveLoader.modules` alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) @@ -1276,6 +1278,17 @@ export default async function getBaseWebpackConfig( or: WEBPACK_LAYERS.GROUP.nonClientServerTarget, }, }, + { + test: /[\\/].*?\.node$/, + loader: isNodeServer + ? 'next-server-binary-loader' + : 'next-error-browser-binary-loader', + // On server side bundling, only apply to app router, do not apply to pages router; + // On client side or edge runtime bundling, always error. + ...(isNodeServer && { + issuerLayer: isWebpackAppLayer, + }), + }, ...(hasAppDir ? [ { diff --git a/packages/next/src/build/webpack/loaders/next-error-browser-binary-loader.ts b/packages/next/src/build/webpack/loaders/next-error-browser-binary-loader.ts new file mode 100644 index 0000000000000..36958fa050fa2 --- /dev/null +++ b/packages/next/src/build/webpack/loaders/next-error-browser-binary-loader.ts @@ -0,0 +1,11 @@ +import type { webpack } from 'next/dist/compiled/webpack/webpack' + +export default function nextErrorBrowserBinaryLoader( + this: webpack.LoaderContext +) { + const { resourcePath, rootContext } = this + const relativePath = resourcePath.slice(rootContext.length + 1) + throw new Error( + `Node.js binary module ./${relativePath} is not supported in the browser. Please only use the module on server side` + ) +} diff --git a/packages/next/src/build/webpack/loaders/next-server-binary-loader.ts b/packages/next/src/build/webpack/loaders/next-server-binary-loader.ts new file mode 100644 index 0000000000000..6525734216728 --- /dev/null +++ b/packages/next/src/build/webpack/loaders/next-server-binary-loader.ts @@ -0,0 +1,14 @@ +import type { webpack } from 'next/dist/compiled/webpack/webpack' +import path from 'path' + +export default function nextErrorBrowserBinaryLoader( + this: webpack.LoaderContext +) { + let relativePath = path.relative(this.rootContext, this.resourcePath) + if (!relativePath.startsWith('.')) { + relativePath = './' + relativePath + } + return `module.exports = __non_webpack_require__(${JSON.stringify( + relativePath + )})` +} diff --git a/test/development/app-dir/externalize-node-binary-browser-error/app/layout.tsx b/test/development/app-dir/externalize-node-binary-browser-error/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/development/app-dir/externalize-node-binary-browser-error/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/externalize-node-binary-browser-error/app/page.tsx b/test/development/app-dir/externalize-node-binary-browser-error/app/page.tsx new file mode 100644 index 0000000000000..c0acd8c09d702 --- /dev/null +++ b/test/development/app-dir/externalize-node-binary-browser-error/app/page.tsx @@ -0,0 +1,7 @@ +'use client' + +import { foo } from 'foo-browser-import-binary' + +export default function Page() { + return

{foo()}

+} diff --git a/test/development/app-dir/externalize-node-binary-browser-error/externalize-node-binary-browser-error.test.ts b/test/development/app-dir/externalize-node-binary-browser-error/externalize-node-binary-browser-error.test.ts new file mode 100644 index 0000000000000..2b6ef114bb561 --- /dev/null +++ b/test/development/app-dir/externalize-node-binary-browser-error/externalize-node-binary-browser-error.test.ts @@ -0,0 +1,29 @@ +import { nextTestSetup } from 'e2e-utils' +import { + hasRedbox, + getRedboxDescription, + getRedboxSource, +} from 'next-test-utils' +;(process.env.TURBOPACK ? describe.skip : describe)( + 'externalize-node-binary-browser-error', + () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should error when import node binary on browser side', async () => { + const browser = await next.browser('/') + await hasRedbox(browser) + const redbox = { + description: await getRedboxDescription(browser), + source: await getRedboxSource(browser), + } + + expect(redbox.description).toBe('Failed to compile') + expect(redbox.source).toMatchInlineSnapshot(` + "./node_modules/foo-browser-import-binary/binary.node + Error: Node.js binary module ./node_modules/foo-browser-import-binary/binary.node is not supported in the browser. Please only use the module on server side" + `) + }) + } +) diff --git a/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/binary.node b/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/binary.node new file mode 100644 index 0000000000000..c2cdd965f8891 --- /dev/null +++ b/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/binary.node @@ -0,0 +1 @@ +�This-leading-char-will-trigger-Module parse failed: Unexpected character diff --git a/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/index.js b/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/index.js new file mode 100644 index 0000000000000..a91a65011ee59 --- /dev/null +++ b/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/index.js @@ -0,0 +1,10 @@ +exports.foo = function () { + return 'I am foo' +} + +exports.bar = function () { + if (typeof window !== 'undefined') { + // This will be bundled on server side + require('./binary.node') + } +} diff --git a/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/package.json b/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/package.json new file mode 100644 index 0000000000000..ad10d48c9da2e --- /dev/null +++ b/test/development/app-dir/externalize-node-binary-browser-error/node_modules/foo-browser-import-binary/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo-browser-import-binary", + "exports": "./index.js" +} diff --git a/test/e2e/app-dir/externalize-node-binary/app/layout.tsx b/test/e2e/app-dir/externalize-node-binary/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/externalize-node-binary/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/externalize-node-binary/app/page.tsx b/test/e2e/app-dir/externalize-node-binary/app/page.tsx new file mode 100644 index 0000000000000..bb7deb69423df --- /dev/null +++ b/test/e2e/app-dir/externalize-node-binary/app/page.tsx @@ -0,0 +1,5 @@ +import { foo } from 'foo' + +export default function Page() { + return

{foo()}

+} diff --git a/test/e2e/app-dir/externalize-node-binary/externalize-node-binary.test.ts b/test/e2e/app-dir/externalize-node-binary/externalize-node-binary.test.ts new file mode 100644 index 0000000000000..bf5090cb24ee7 --- /dev/null +++ b/test/e2e/app-dir/externalize-node-binary/externalize-node-binary.test.ts @@ -0,0 +1,15 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('externalize-node-binary', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render correctly when node_modules require node binary module', async () => { + const { status } = await next.fetch('/') + expect(status).toBe(200) + + const browser = await next.browser('/') + expect(await browser.elementByCss('p').text()).toBe('I am foo') + }) +}) diff --git a/test/e2e/app-dir/externalize-node-binary/node_modules/foo/binary.node b/test/e2e/app-dir/externalize-node-binary/node_modules/foo/binary.node new file mode 100644 index 0000000000000..c2cdd965f8891 --- /dev/null +++ b/test/e2e/app-dir/externalize-node-binary/node_modules/foo/binary.node @@ -0,0 +1 @@ +�This-leading-char-will-trigger-Module parse failed: Unexpected character diff --git a/test/e2e/app-dir/externalize-node-binary/node_modules/foo/index.js b/test/e2e/app-dir/externalize-node-binary/node_modules/foo/index.js new file mode 100644 index 0000000000000..e800b36b2de7d --- /dev/null +++ b/test/e2e/app-dir/externalize-node-binary/node_modules/foo/index.js @@ -0,0 +1,9 @@ +exports.foo = function () { + return 'I am foo' +} + +exports.bar = function () { + if (typeof window === 'undefined') { + require('./binary.node') + } +} diff --git a/test/e2e/app-dir/externalize-node-binary/node_modules/foo/package.json b/test/e2e/app-dir/externalize-node-binary/node_modules/foo/package.json new file mode 100644 index 0000000000000..61858e0632858 --- /dev/null +++ b/test/e2e/app-dir/externalize-node-binary/node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "exports": "./index.js" +}