From 843d58425b65fc00cfc1b416128fdd3343a3b083 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 10 Aug 2020 03:26:21 +0200 Subject: [PATCH] Add browser polyfils for Node.js modules (webpack 5 backwards compat) (#16022) This adds the following Node.js core polyfills only when the import is used: - `path` - `stream` - `vm` - `crypto` - `buffer` Fixes #15948 We'll have a separate issue about adding warnings for the usage of these modules in the browser, some polyfills like crypto are quite heavy and generally not needed for most applications (included accidentally through node_modules). --- packages/next/build/webpack-config.ts | 11 ++++- packages/next/package.json | 4 ++ .../basic/pages/node-browser-polyfills.js | 48 +++++++++++++++++++ test/integration/basic/test/index.test.js | 19 ++++++++ .../pages/node-browser-polyfills.js | 48 +++++++++++++++++++ .../integration/production/test/index.test.js | 18 +++++++ yarn.lock | 48 ++++++++++++++++--- 7 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 test/integration/basic/pages/node-browser-polyfills.js create mode 100644 test/integration/production/pages/node-browser-polyfills.js diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 4f15fd19d4157..6355ca150028e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -356,7 +356,16 @@ export default async function getBaseWebpackConfig( 'next/router': 'next/dist/client/router.js', 'next/config': 'next/dist/next-server/lib/runtime-config.js', 'next/dynamic': 'next/dist/next-server/lib/dynamic.js', - next: NEXT_PROJECT_ROOT, + ...(isServer + ? {} + : { + stream: 'stream-browserify', + path: 'path-browserify', + crypto: 'crypto-browserify', + buffer: 'buffer', + vm: 'vm-browserify', + next: NEXT_PROJECT_ROOT, + }), [PAGES_DIR_ALIAS]: pagesDir, [DOT_NEXT_ALIAS]: distDir, ...getOptimizedAliases(isServer), diff --git a/packages/next/package.json b/packages/next/package.json index 7839cd34c57a1..d2b61929ca23c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -87,6 +87,7 @@ "buffer": "5.6.0", "cacache": "13.0.1", "chokidar": "2.1.8", + "crypto-browserify": "3.12.0", "css-loader": "3.5.3", "cssnano-simple": "1.0.6", "find-cache-dir": "3.3.1", @@ -97,6 +98,7 @@ "native-url": "0.3.4", "neo-async": "2.6.1", "node-html-parser": "^1.2.19", + "path-browserify": "1.0.1", "pnp-webpack-plugin": "1.6.4", "postcss": "7.0.32", "process": "0.11.10", @@ -107,9 +109,11 @@ "resolve-url-loader": "3.1.1", "sass-loader": "8.0.2", "schema-utils": "2.6.6", + "stream-browserify": "3.0.0", "style-loader": "1.2.1", "styled-jsx": "3.3.0", "use-subscription": "1.4.1", + "vm-browserify": "1.1.2", "watchpack": "2.0.0-beta.13", "web-vitals": "0.2.1", "webpack": "4.44.1", diff --git a/test/integration/basic/pages/node-browser-polyfills.js b/test/integration/basic/pages/node-browser-polyfills.js new file mode 100644 index 0000000000000..d5776c88a366b --- /dev/null +++ b/test/integration/basic/pages/node-browser-polyfills.js @@ -0,0 +1,48 @@ +import { Writable } from 'stream' +import path from 'path' +import crypto from 'crypto' +import { Buffer } from 'buffer' +import vm from 'vm' +import { useEffect, useState } from 'react' + +export default function NodeBrowserPolyfillPage() { + const [state, setState] = useState({}) + useEffect(() => { + let closedStream = false + + const writable = new Writable({ + write(_chunk, _encoding, callback) { + callback() + }, + }) + + writable.on('finish', () => { + closedStream = true + }) + + writable.end() + + setState({ + path: path.join('/hello/world', 'test.txt'), + hash: crypto.createHash('sha256').update('hello world').digest('hex'), + buffer: Buffer.from('hello world').toString('utf8'), + vm: vm.runInNewContext('a + 5', { a: 100 }), + stream: closedStream, + }) + }, []) + + useEffect(() => { + if (state.vm) { + window.didRender = true + } + }, [state]) + + return ( + <> +
+ + ) +} diff --git a/test/integration/basic/test/index.test.js b/test/integration/basic/test/index.test.js index e94c48e373f69..af8997f0d7f6c 100644 --- a/test/integration/basic/test/index.test.js +++ b/test/integration/basic/test/index.test.js @@ -2,6 +2,7 @@ import { join } from 'path' import { renderViaHTTP, findPort, launchApp, killApp } from 'next-test-utils' +import webdriver from 'next-webdriver' // test suits import hmr from './hmr' @@ -33,6 +34,24 @@ describe('Basic Features', () => { }) afterAll(() => killApp(context.server)) + it('should polyfill Node.js modules', async () => { + const browser = await webdriver(context.appPort, '/node-browser-polyfills') + await browser.waitForCondition('window.didRender') + + const data = await browser + .waitForElementByCss('#node-browser-polyfills') + .text() + const parsedData = JSON.parse(data) + + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + }) + dynamic(context, (p, q) => renderViaHTTP(context.appPort, p, q)) hmr(context, (p, q) => renderViaHTTP(context.appPort, p, q)) errorRecovery(context, (p, q) => renderViaHTTP(context.appPort, p, q)) diff --git a/test/integration/production/pages/node-browser-polyfills.js b/test/integration/production/pages/node-browser-polyfills.js new file mode 100644 index 0000000000000..d5776c88a366b --- /dev/null +++ b/test/integration/production/pages/node-browser-polyfills.js @@ -0,0 +1,48 @@ +import { Writable } from 'stream' +import path from 'path' +import crypto from 'crypto' +import { Buffer } from 'buffer' +import vm from 'vm' +import { useEffect, useState } from 'react' + +export default function NodeBrowserPolyfillPage() { + const [state, setState] = useState({}) + useEffect(() => { + let closedStream = false + + const writable = new Writable({ + write(_chunk, _encoding, callback) { + callback() + }, + }) + + writable.on('finish', () => { + closedStream = true + }) + + writable.end() + + setState({ + path: path.join('/hello/world', 'test.txt'), + hash: crypto.createHash('sha256').update('hello world').digest('hex'), + buffer: Buffer.from('hello world').toString('utf8'), + vm: vm.runInNewContext('a + 5', { a: 100 }), + stream: closedStream, + }) + }, []) + + useEffect(() => { + if (state.vm) { + window.didRender = true + } + }, [state]) + + return ( + <> +
+ + ) +} diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 7201d9f0bb7b6..9868a02015de1 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -70,6 +70,24 @@ describe('Production Usage', () => { }) } + it('should polyfill Node.js modules', async () => { + const browser = await webdriver(appPort, '/node-browser-polyfills') + await browser.waitForCondition('window.didRender') + + const data = await browser + .waitForElementByCss('#node-browser-polyfills') + .text() + const parsedData = JSON.parse(data) + + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + }) + it('should allow etag header support', async () => { const url = `http://localhost:${appPort}/` const etag = (await fetch(url)).headers.get('ETag') diff --git a/yarn.lock b/yarn.lock index 3b5f638c3eb1d..d366f5c7016fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4121,7 +4121,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.13.0, browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6, browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.13.0, browserslist@^4.3.6, browserslist@^4.6.4, browserslist@^4.8.3, browserslist@^4.8.5: +browserslist@4.13.0, browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.13.0, browserslist@^4.3.6, browserslist@^4.6.4, browserslist@^4.8.3, browserslist@^4.8.5: version "4.13.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== @@ -4131,6 +4131,14 @@ browserslist@4.13.0, browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7 escalade "^3.0.1" node-releases "^1.1.58" +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + integrity sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk= + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + browserstack-local@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/browserstack-local/-/browserstack-local-1.4.0.tgz#d979cac056f57b9af159b3bcd7fdc09b4354537c" @@ -4460,11 +4468,21 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634: version "1.0.30001023" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001023.tgz#f856f71af16a5a44e81f1fcefc1673912a43da72" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001019, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001093: +caniuse-db@^1.0.30000639: + version "1.0.30001112" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001112.tgz#89cb249402af2ebf2c23a5585d1bd78cb5e38732" + integrity sha512-Xrn3lVEIsvAAUmFVHKKComfyCRM59NfuV3EwCXqs2XLgOxAqYgrfEs0vDk+Dk7KctlAMh6CH/CSd1srJt503ZA== + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001019, caniuse-lite@^1.0.30001020: version "1.0.30001066" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz#0a8a58a10108f2b9bf38e7b65c237b12fd9c5f04" integrity sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw== +caniuse-lite@^1.0.30001093: + version "1.0.30001112" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001112.tgz#0fffc3b934ff56ff0548c37bc9dad7d882bcf672" + integrity sha512-J05RTQlqsatidif/38aN3PGULCLrg8OYQOlJUKbeYVzC2mGZkZLIztwRlB3MtrfLmawUmjFlNJvy/uhwniIe1Q== + capitalize@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capitalize/-/capitalize-1.0.0.tgz#dc802c580aee101929020d2ca14b4ca8a0ae44be" @@ -5376,7 +5394,7 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@^3.11.0: +crypto-browserify@3.12.0, crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== @@ -6190,6 +6208,11 @@ ejs@^2.6.1: version "2.7.4" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" +electron-to-chromium@^1.2.7: + version "1.3.526" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.526.tgz#0e004899edf75afc172cce1b8189aac5dca646aa" + integrity sha512-HiroW5ZbGwgT8kCnoEO8qnGjoTPzJxduvV/Vv/wH63eo2N6Zj3xT5fmmaSPAPUM05iN9/5fIEkIg3owTtV6QZg== + electron-to-chromium@^1.3.488: version "1.3.501" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.501.tgz#faa17a2cb0105ee30d5e1ca87eae7d8e85dd3175" @@ -8108,7 +8131,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" @@ -11695,6 +11718,11 @@ path-browserify@0.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== +path-browserify@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5" @@ -13337,7 +13365,7 @@ read@1, read@~1.0.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -14717,6 +14745,14 @@ stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" +stream-browserify@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" + integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== + dependencies: + inherits "~2.0.4" + readable-stream "^3.5.0" + stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -15970,7 +16006,7 @@ vlq@^0.2.1, vlq@^0.2.2: version "0.2.3" resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" -vm-browserify@^1.0.1: +vm-browserify@1.1.2, vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==