From a2accb22999ec12c49a3621f04d6b7d3567f00a3 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 22 Mar 2022 00:03:42 -0500 Subject: [PATCH 01/39] Update trace ignore check to check reasons correctly (#35511) --- .../build/webpack/plugins/next-trace-entrypoints-plugin.ts | 4 +++- test/integration/production/pages/api/index.js | 6 +++--- test/integration/production/test/index.test.js | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts index 2f0e7ababae6..21b14ad40165 100644 --- a/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts +++ b/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts @@ -365,7 +365,9 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance { // static image imports, CSS imports file = nodePath.join(this.tracingRoot, file) const depMod = depModMap.get(file) - const isAsset = reasons.get(file)?.type.includes('asset') + const isAsset = reasons + .get(nodePath.relative(this.tracingRoot, file)) + ?.type.includes('asset') return ( !isAsset && diff --git a/test/integration/production/pages/api/index.js b/test/integration/production/pages/api/index.js index 22e945b9672f..5df383f4a834 100644 --- a/test/integration/production/pages/api/index.js +++ b/test/integration/production/pages/api/index.js @@ -1,12 +1,12 @@ import fs from 'fs' import path from 'path' -import data from '../../static/hello.json' +import css from '../../components/logo/logo.module.css' export default (req, res) => { console.log({ - importedData: data, + importedData: css, fsLoadedData: fs.readFileSync( - path.join(process.cwd(), 'static', 'hello.json'), + path.join(process.cwd(), 'components', 'logo', 'logo.module.css'), 'utf8' ), }) diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 5276dbf5db56..82c91a9acef1 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -225,7 +225,7 @@ describe('Production Usage', () => { }, { page: '/api', - tests: [/webpack-runtime\.js/, /static\/hello\.json/], + tests: [/webpack-runtime\.js/, /\/logo\.module\.css/], notTests: [ /next\/dist\/server\/next\.js/, /next\/dist\/bin/, From 78831c3c8490e2142aedfce34420085f49ba9e73 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 22 Mar 2022 09:00:31 -0400 Subject: [PATCH 02/39] v12.1.1-canary.17 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- 15 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lerna.json b/lerna.json index c1b7c36a832f..b9c10c6aca41 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.1.1-canary.16" + "version": "12.1.1-canary.17" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 04edd9ee5173..7199c8a27ed1 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index a821748d07c6..5d5c053bb88f 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -9,7 +9,7 @@ "directory": "packages/eslint-config-next" }, "dependencies": { - "@next/eslint-plugin-next": "12.1.1-canary.16", + "@next/eslint-plugin-next": "12.1.1-canary.17", "@rushstack/eslint-patch": "1.0.8", "@typescript-eslint/parser": "5.10.1", "eslint-import-resolver-node": "0.3.4", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 13120e31fa47..62ceddaf8f8a 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "description": "ESLint plugin for NextJS.", "main": "lib/index.js", "license": "MIT", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 5b8b6a23abce..0b1b0d037c3c 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 5e89c7f368dd..6af12dfa01a5 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 52d3959f032a..69010149c17d 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 126e9b38c11e..f3b865480071 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 30b3022d3e4a..32bc832788d5 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 99d2905f6bd0..364f2434a25f 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 94de2dc2385b..7ddbeca5bfcb 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 854d63422b4a..5d845564fdb6 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "private": true, "scripts": { "build-native": "napi build --platform --cargo-name next_swc_napi native", diff --git a/packages/next/package.json b/packages/next/package.json index e6ecbc66eb3e..8aad660fb8f2 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -69,7 +69,7 @@ ] }, "dependencies": { - "@next/env": "12.1.1-canary.16", + "@next/env": "12.1.1-canary.17", "caniuse-lite": "^1.0.30001283", "postcss": "8.4.5", "styled-jsx": "5.0.1", @@ -118,11 +118,11 @@ "@hapi/accept": "5.0.2", "@napi-rs/cli": "2.4.4", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "12.1.1-canary.16", - "@next/polyfill-nomodule": "12.1.1-canary.16", - "@next/react-dev-overlay": "12.1.1-canary.16", - "@next/react-refresh-utils": "12.1.1-canary.16", - "@next/swc": "12.1.1-canary.16", + "@next/polyfill-module": "12.1.1-canary.17", + "@next/polyfill-nomodule": "12.1.1-canary.17", + "@next/react-dev-overlay": "12.1.1-canary.17", + "@next/react-refresh-utils": "12.1.1-canary.17", + "@next/swc": "12.1.1-canary.17", "@peculiar/webcrypto": "1.3.1", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index afb5ac544a8a..57db00907726 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 2e1956c8360e..83cbad74a15f 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.1.1-canary.16", + "version": "12.1.1-canary.17", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", From 860c97ccf597eddec9422cab06ab8a7cc328d8bb Mon Sep 17 00:00:00 2001 From: Heyang Zhou Date: Tue, 22 Mar 2022 21:20:57 +0800 Subject: [PATCH 03/39] SWC import modularization plugin (#34969) --- docs/advanced-features/compiler.md | 83 +++++++ examples/modularize-imports/.gitignore | 34 +++ examples/modularize-imports/README.md | 27 ++ .../components/halves/LeftHalf.js | 3 + .../components/halves/RightHalf.js | 3 + .../components/halves/index.js | 5 + examples/modularize-imports/next.config.js | 9 + examples/modularize-imports/package.json | 13 + examples/modularize-imports/pages/index.js | 10 + packages/next-swc/Cargo.lock | 158 +++++++++++- packages/next-swc/crates/core/Cargo.toml | 2 + packages/next-swc/crates/core/src/lib.rs | 8 + .../crates/core/src/modularize_imports.rs | 235 ++++++++++++++++++ .../next-swc/crates/core/tests/fixture.rs | 44 ++++ .../fixture/modularize-imports/regex/input.js | 3 + .../modularize-imports/regex/output.js | 4 + .../modularize-imports/simple/input.js | 2 + .../modularize-imports/simple/output.js | 5 + packages/next-swc/crates/core/tests/full.rs | 1 + packages/next/build/swc/options.js | 1 + packages/next/build/webpack-config.ts | 1 + packages/next/server/config-shared.ts | 8 + 22 files changed, 653 insertions(+), 6 deletions(-) create mode 100644 examples/modularize-imports/.gitignore create mode 100644 examples/modularize-imports/README.md create mode 100644 examples/modularize-imports/components/halves/LeftHalf.js create mode 100644 examples/modularize-imports/components/halves/RightHalf.js create mode 100644 examples/modularize-imports/components/halves/index.js create mode 100644 examples/modularize-imports/next.config.js create mode 100644 examples/modularize-imports/package.json create mode 100644 examples/modularize-imports/pages/index.js create mode 100644 packages/next-swc/crates/core/src/modularize_imports.rs create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js diff --git a/docs/advanced-features/compiler.md b/docs/advanced-features/compiler.md index 16d415a0a11d..35acc704d1ba 100644 --- a/docs/advanced-features/compiler.md +++ b/docs/advanced-features/compiler.md @@ -233,6 +233,89 @@ module.exports = { If you have feedback about `swcMinify`, please share it on the [feedback discussion](https://github.com/vercel/next.js/discussions/30237). +### Modularize Imports + +Allows to modularize imports, similar to [babel-plugin-transform-imports](https://www.npmjs.com/package/babel-plugin-transform-imports). + +Transforms member style imports: + +```js +import { Row, Grid as MyGrid } from 'react-bootstrap' +import { merge } from 'lodash' +``` + +...into default style imports: + +```js +import Row from 'react-bootstrap/lib/Row' +import MyGrid from 'react-bootstrap/lib/Grid' +import merge from 'lodash/merge' +``` + +Config for the above transform: + +```js +// next.config.js +module.exports = { + experimental: { + modularizeImports: { + 'react-bootstrap': { + transform: 'react-bootstrap/lib/{{member}}', + }, + lodash: { + transform: 'lodash/{{member}}', + }, + }, + }, +} +``` + +Advanced transformations: + +- Using regular expressions + +Similar to `babel-plugin-transform-imports`, but the transform is templated with [handlebars](https://docs.rs/handlebars) and regular expressions are in Rust [regex](https://docs.rs/regex/latest/regex/) crate's syntax. + +The config: + +```js +// next.config.js +module.exports = { + experimental: { + modularizeImports: { + 'my-library/?(((\\w*)?/?)*)': { + transform: 'my-library/{{ matches.[1] }}/{{member}}', + }, + }, + }, +} +``` + +Cause this code: + +```js +import { MyModule } from 'my-library' +import { App } from 'my-library/components' +import { Header, Footer } from 'my-library/components/App' +``` + +To become: + +```js +import MyModule from 'my-library/MyModule' +import App from 'my-library/components/App' +import Header from 'my-library/components/App/Header' +import Footer from 'my-library/components/App/Footer' +``` + +- Handlebars templating + +This transform uses [handlebars](https://docs.rs/handlebars) to template the replacement import path in the `transform` field. These variables and helper functions are available: + +1. `matches`: Has type `string[]`. All groups matched by the regular expression. `matches.[0]` is the full match. +2. `member`: Has type `string`. The name of the member import. +3. `lowerCase`, `upperCase`, `camelCase`: Helper functions to convert a string to lower, upper or camel cases. + ## Unsupported Features When your application has a `.babelrc` file, Next.js will automatically fall back to using Babel for transforming individual files. This ensures backwards compatibility with existing applications that leverage custom Babel plugins. diff --git a/examples/modularize-imports/.gitignore b/examples/modularize-imports/.gitignore new file mode 100644 index 000000000000..1437c53f70bc --- /dev/null +++ b/examples/modularize-imports/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/examples/modularize-imports/README.md b/examples/modularize-imports/README.md new file mode 100644 index 000000000000..c80cdb596798 --- /dev/null +++ b/examples/modularize-imports/README.md @@ -0,0 +1,27 @@ +# Modularize Imports Example + +This example shows how to use the `modularizeImports` config option. + +## Preview + +Preview the example live on [StackBlitz](http://stackblitz.com/): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/modularize-imports) + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/modularize-imports&project-name=modularize-imports&repository-name=modularize-imports) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example modularize-imports modularize-imports-app +# or +yarn create next-app --example modularize-imports modularize-imports-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/modularize-imports/components/halves/LeftHalf.js b/examples/modularize-imports/components/halves/LeftHalf.js new file mode 100644 index 000000000000..5f96fe8a0d9a --- /dev/null +++ b/examples/modularize-imports/components/halves/LeftHalf.js @@ -0,0 +1,3 @@ +export default function LeftHalf() { + return Modularize +} diff --git a/examples/modularize-imports/components/halves/RightHalf.js b/examples/modularize-imports/components/halves/RightHalf.js new file mode 100644 index 000000000000..b1b5d5e0d3b4 --- /dev/null +++ b/examples/modularize-imports/components/halves/RightHalf.js @@ -0,0 +1,3 @@ +export default function RightHalf() { + return Imports +} diff --git a/examples/modularize-imports/components/halves/index.js b/examples/modularize-imports/components/halves/index.js new file mode 100644 index 000000000000..b542f3266a39 --- /dev/null +++ b/examples/modularize-imports/components/halves/index.js @@ -0,0 +1,5 @@ +// import LeftHalf from './LeftHalf' +// import RightHalf from './RightHalf' + +// Remove the exports here so that we can verify that `modularize-imports` is working. +// export { LeftHalf, RightHalf }; diff --git a/examples/modularize-imports/next.config.js b/examples/modularize-imports/next.config.js new file mode 100644 index 000000000000..55be9582d2f9 --- /dev/null +++ b/examples/modularize-imports/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + experimental: { + modularizeImports: { + '../components/halves': { + transform: '../components/halves/{{ member }}', + }, + }, + }, +} diff --git a/examples/modularize-imports/package.json b/examples/modularize-imports/package.json new file mode 100644 index 000000000000..f9170ae254fa --- /dev/null +++ b/examples/modularize-imports/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "^17.0.2", + "react-dom": "^17.0.2" + } +} diff --git a/examples/modularize-imports/pages/index.js b/examples/modularize-imports/pages/index.js new file mode 100644 index 000000000000..ac543f049412 --- /dev/null +++ b/examples/modularize-imports/pages/index.js @@ -0,0 +1,10 @@ +import { LeftHalf, RightHalf } from '../components/halves' + +const Index = () => ( +
+ + +
+) + +export default Index diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock index e42eee1bc7c0..68ff4a132b49 100644 --- a/packages/next-swc/Cargo.lock +++ b/packages/next-swc/Cargo.lock @@ -160,13 +160,34 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + [[package]] name = "block-buffer" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "generic-array", + "generic-array 0.14.5", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -199,6 +220,12 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "byteorder" version = "1.4.3" @@ -331,7 +358,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "generic-array", + "generic-array 0.14.5", "typenum", ] @@ -428,13 +455,22 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "block-buffer", + "block-buffer 0.10.2", "crypto-common", ] @@ -462,6 +498,12 @@ dependencies = [ "syn", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "fastrand" version = "1.7.0" @@ -514,6 +556,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.5" @@ -558,6 +609,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "handlebars" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d6a30320f094710245150395bc763ad23128d6a1ebbad7594dc4164b62c56b" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error", + "serde", + "serde_json", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -775,6 +840,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchers" version = "0.1.0" @@ -892,6 +963,7 @@ dependencies = [ "easy-error", "either", "fxhash", + "handlebars", "once_cell", "pathdiff", "radix_fmt", @@ -901,6 +973,7 @@ dependencies = [ "styled_components", "swc", "swc_atoms", + "swc_cached", "swc_common", "swc_css", "swc_ecma_loader", @@ -1012,6 +1085,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "ordered-float" version = "2.10.0" @@ -1097,6 +1176,49 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1 0.8.2", +] + [[package]] name = "petgraph" version = "0.6.0" @@ -1267,6 +1389,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.15" @@ -1565,6 +1693,18 @@ dependencies = [ "serde", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug", +] + [[package]] name = "sha-1" version = "0.10.0" @@ -1573,7 +1713,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.10.3", ] [[package]] @@ -2274,7 +2414,7 @@ dependencies = [ "once_cell", "regex", "serde", - "sha-1", + "sha-1 0.10.0", "string_enum", "swc_atoms", "swc_common", @@ -2297,7 +2437,7 @@ dependencies = [ "hex", "serde", "serde_json", - "sha-1", + "sha-1 0.10.0", "swc_common", "swc_ecma_ast", "swc_ecma_codegen", @@ -2692,6 +2832,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "unicode-bidi" version = "0.3.7" diff --git a/packages/next-swc/crates/core/Cargo.toml b/packages/next-swc/crates/core/Cargo.toml index 09d272b19582..4e5e5be3b190 100644 --- a/packages/next-swc/crates/core/Cargo.toml +++ b/packages/next-swc/crates/core/Cargo.toml @@ -28,7 +28,9 @@ swc_ecma_loader = {version = "0.29.0", features = ["node", "lru"]} swc_ecmascript = {version = "0.132.0", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"]} swc_node_base = "0.5.1" swc_stylis = "0.96.1" +swc_cached = "0.1.1" tracing = {version = "0.1.28", features = ["release_max_level_off"]} +handlebars = "4.2.1" [dev-dependencies] swc_ecma_transforms_testing = "0.69.0" diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index dd13ea2af816..9ca3d33a8ee5 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -49,6 +49,7 @@ mod auto_cjs; pub mod disallow_re_export_all_in_page; pub mod emotion; pub mod hook_optimizer; +pub mod modularize_imports; pub mod next_dynamic; pub mod next_ssg; pub mod page_config; @@ -102,6 +103,9 @@ pub struct TransformOptions { #[serde(default)] pub emotion: Option, + + #[serde(default)] + pub modularize_imports: Option, } pub fn custom_before_pass<'a, C: Comments + 'a>( @@ -191,6 +195,10 @@ pub fn custom_before_pass<'a, C: Comments + 'a>( } }) .unwrap_or_else(|| Either::Right(noop())), + match &opts.modularize_imports { + Some(config) => Either::Left(modularize_imports::modularize_imports(config.clone())), + None => Either::Right(noop()), + } ) } diff --git a/packages/next-swc/crates/core/src/modularize_imports.rs b/packages/next-swc/crates/core/src/modularize_imports.rs new file mode 100644 index 000000000000..5322f9d872d2 --- /dev/null +++ b/packages/next-swc/crates/core/src/modularize_imports.rs @@ -0,0 +1,235 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext}; +use once_cell::sync::Lazy; +use regex::{Captures, Regex}; +use serde::{Deserialize, Serialize}; +use swc_cached::regex::CachedRegex; +use swc_ecmascript::ast::*; +use swc_ecmascript::visit::{noop_fold_type, Fold}; + +static DUP_SLASH_REGEX: Lazy = Lazy::new(|| Regex::new(r"//").unwrap()); + +#[derive(Clone, Debug, Deserialize)] +#[serde(transparent)] +pub struct Config { + pub packages: HashMap, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PackageConfig { + pub transform: String, + #[serde(default)] + pub prevent_full_import: bool, + #[serde(default)] + pub skip_default_conversion: bool, +} + +struct FoldImports { + renderer: handlebars::Handlebars<'static>, + packages: Vec<(CachedRegex, PackageConfig)>, +} + +struct Rewriter<'a> { + renderer: &'a handlebars::Handlebars<'static>, + key: &'a str, + config: &'a PackageConfig, + group: Vec<&'a str>, +} + +impl<'a> Rewriter<'a> { + fn rewrite(&self, old_decl: &ImportDecl) -> Vec { + if old_decl.type_only || old_decl.asserts.is_some() { + return vec![old_decl.clone()]; + } + + let mut out: Vec = Vec::with_capacity(old_decl.specifiers.len()); + + for spec in &old_decl.specifiers { + match spec { + ImportSpecifier::Named(named_spec) => { + #[derive(Serialize)] + #[serde(untagged)] + enum Data<'a> { + Plain(&'a str), + Array(&'a [&'a str]), + } + let mut ctx: HashMap<&str, Data> = HashMap::new(); + ctx.insert("matches", Data::Array(&self.group[..])); + ctx.insert( + "member", + Data::Plain( + named_spec + .imported + .as_ref() + .map(|x| match x { + ModuleExportName::Ident(x) => x.as_ref(), + ModuleExportName::Str(x) => x.value.as_ref(), + }) + .unwrap_or_else(|| named_spec.local.as_ref()), + ), + ); + let new_path = self + .renderer + .render_template(&self.config.transform, &ctx) + .unwrap_or_else(|e| { + panic!("error rendering template for '{}': {}", self.key, e); + }); + let new_path = DUP_SLASH_REGEX.replace_all(&new_path, |_: &Captures| "/"); + let specifier = if self.config.skip_default_conversion { + ImportSpecifier::Named(named_spec.clone()) + } else { + ImportSpecifier::Default(ImportDefaultSpecifier { + local: named_spec.local.clone(), + span: named_spec.span, + }) + }; + out.push(ImportDecl { + specifiers: vec![specifier], + src: Str::from(new_path.as_ref()), + span: old_decl.span, + type_only: false, + asserts: None, + }); + } + _ => { + if self.config.prevent_full_import { + panic!( + "import {:?} causes the entire module to be imported", + old_decl + ); + } else { + // Give up + return vec![old_decl.clone()]; + } + } + } + } + out + } +} + +impl FoldImports { + fn should_rewrite<'a>(&'a self, name: &'a str) -> Option> { + for (regex, config) in &self.packages { + let group = regex.captures(name); + if let Some(group) = group { + let group = group + .iter() + .map(|x| x.map(|x| x.as_str()).unwrap_or_default()) + .collect::>(); + return Some(Rewriter { + renderer: &self.renderer, + key: name, + config, + group, + }); + } + } + None + } +} + +impl Fold for FoldImports { + noop_fold_type!(); + fn fold_module(&mut self, mut module: Module) -> Module { + let mut new_items: Vec = vec![]; + for item in module.body { + match item { + ModuleItem::ModuleDecl(ModuleDecl::Import(decl)) => { + match self.should_rewrite(&decl.src.value) { + Some(rewriter) => { + let rewritten = rewriter.rewrite(&decl); + new_items.extend( + rewritten + .into_iter() + .map(|x| ModuleItem::ModuleDecl(ModuleDecl::Import(x))), + ); + } + None => new_items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(decl))), + } + } + x => { + new_items.push(x); + } + } + } + module.body = new_items; + module + } +} + +pub fn modularize_imports(config: Config) -> impl Fold { + let mut folder = FoldImports { + renderer: handlebars::Handlebars::new(), + packages: vec![], + }; + folder + .renderer + .register_helper("lowerCase", Box::new(helper_lower_case)); + folder + .renderer + .register_helper("upperCase", Box::new(helper_upper_case)); + folder + .renderer + .register_helper("camelCase", Box::new(helper_camel_case)); + for (mut k, v) in config.packages { + // XXX: Should we keep this hack? + if !k.starts_with('^') && !k.ends_with('$') { + k = format!("^{}$", k); + } + folder.packages.push(( + CachedRegex::new(&k).expect("transform-imports: invalid regex"), + v, + )); + } + folder +} + +fn helper_lower_case( + h: &Helper<'_, '_>, + _: &Handlebars<'_>, + _: &Context, + _: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> HelperResult { + // get parameter from helper or throw an error + let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or(""); + out.write(param.to_lowercase().as_ref())?; + Ok(()) +} + +fn helper_upper_case( + h: &Helper<'_, '_>, + _: &Handlebars<'_>, + _: &Context, + _: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> HelperResult { + // get parameter from helper or throw an error + let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or(""); + out.write(param.to_uppercase().as_ref())?; + Ok(()) +} + +fn helper_camel_case( + h: &Helper<'_, '_>, + _: &Handlebars<'_>, + _: &Context, + _: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> HelperResult { + // get parameter from helper or throw an error + let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or(""); + let value = if param.is_empty() || param.chars().next().unwrap().is_lowercase() { + Cow::Borrowed(param) + } else { + let mut it = param.chars(); + let fst = it.next().unwrap(); + Cow::Owned(fst.to_lowercase().chain(it).collect::()) + }; + out.write(value.as_ref())?; + Ok(()) +} diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index ed9cf5b247b7..9a7d8b7e5e56 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -1,6 +1,7 @@ use next_swc::{ amp_attributes::amp_attributes, emotion::{self, EmotionOptions}, + modularize_imports::modularize_imports, next_dynamic::next_dynamic, next_ssg::next_ssg, page_config::page_config_test, @@ -312,3 +313,46 @@ fn next_emotion_fixture(input: PathBuf) { &output, ); } + +#[fixture("tests/fixture/modularize-imports/**/input.js")] +fn modularize_imports_fixture(input: PathBuf) { + use next_swc::modularize_imports::PackageConfig; + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|_tr| { + modularize_imports(next_swc::modularize_imports::Config { + packages: vec![ + ( + "react-bootstrap".to_string(), + PackageConfig { + transform: "react-bootstrap/lib/{{member}}".into(), + prevent_full_import: false, + skip_default_conversion: false, + }, + ), + ( + "my-library/?(((\\w*)?/?)*)".to_string(), + PackageConfig { + transform: "my-library/{{ matches.[1] }}/{{member}}".into(), + prevent_full_import: false, + skip_default_conversion: false, + }, + ), + ( + "my-library-2".to_string(), + PackageConfig { + transform: "my-library-2/{{ camelCase member }}".into(), + prevent_full_import: false, + skip_default_conversion: true, + }, + ), + ] + .into_iter() + .collect(), + }) + }, + &input, + &output, + ); +} diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js new file mode 100644 index 000000000000..679eeeb2ddcf --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js @@ -0,0 +1,3 @@ +import { MyModule } from 'my-library'; +import { App } from 'my-library/components'; +import { Header, Footer } from 'my-library/components/App'; diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js new file mode 100644 index 000000000000..c4b5cadfeeb6 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js @@ -0,0 +1,4 @@ +import MyModule from 'my-library/MyModule'; +import App from 'my-library/components/App'; +import Header from 'my-library/components/App/Header'; +import Footer from 'my-library/components/App/Footer'; diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js new file mode 100644 index 000000000000..f152bdc8f19d --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js @@ -0,0 +1,2 @@ +import { Grid, Row, Col as Col1 } from 'react-bootstrap'; +import { MyModule, Widget } from 'my-library-2'; diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js new file mode 100644 index 000000000000..d3798eb0e9e4 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js @@ -0,0 +1,5 @@ +import Grid from "react-bootstrap/lib/Grid"; +import Row from "react-bootstrap/lib/Row"; +import Col1 from "react-bootstrap/lib/Col"; +import { MyModule } from 'my-library-2/myModule'; +import { Widget } from 'my-library-2/widget'; diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index bd7c4a6f65d7..12817af23a1e 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -62,6 +62,7 @@ fn test(input: &Path, minify: bool) { relay: None, shake_exports: None, emotion: Some(assert_json("{}")), + modularize_imports: None, }; let options = options.patch(&fm); diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 00af330c102f..33ed7b9d00ea 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -102,6 +102,7 @@ function getBaseSWCOptions({ : null, removeConsole: nextConfig?.compiler?.removeConsole, reactRemoveProperties: nextConfig?.compiler?.reactRemoveProperties, + modularizeImports: nextConfig?.experimental?.modularizeImports, relay: nextConfig?.compiler?.relay, emotion: getEmotionOptions(nextConfig, development), } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 4e8d4d4a8e53..9ad462a01dc9 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1653,6 +1653,7 @@ export default async function getBaseWebpackConfig( styledComponents: config.compiler?.styledComponents, relay: config.compiler?.relay, emotion: config.experimental?.emotion, + modularizeImports: config.experimental?.modularizeImports, }) const cache: any = { diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index 0e8c6d5a53cb..0007760e4017 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -118,6 +118,14 @@ export interface ExperimentalConfig { autoLabel?: 'dev-only' | 'always' | 'never' labelFormat?: string } + modularizeImports?: Record< + string, + { + transform: string + preventFullImport?: boolean + skipDefaultConversion?: boolean + } + > } /** From 6da769129ec94dc3f3202e89cc73ce35d4e97488 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 22 Mar 2022 07:54:05 -0700 Subject: [PATCH 04/39] Reduce hello-world middleware bundle size from 128k to 88k (#35512) Moves two utility functions from `server/router.ts` into their own file. This avoids the middleware pulling in the full Next.js router into its bundle. There are probably more opportunities like this, but this is a good start. Middleware should likely be bundled by a non-chunking optimizing compiler. --- packages/next/server/base-server.ts | 3 ++- packages/next/server/dev/next-dev-server.ts | 3 ++- packages/next/server/router-utils.ts | 17 +++++++++++++++++ packages/next/server/web/next-url.ts | 2 +- .../shared/lib/router/utils/parse-next-url.ts | 2 +- 5 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 packages/next/server/router-utils.ts diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index f02f7db199d5..a575e1ad5969 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -42,7 +42,7 @@ import { import * as envConfig from '../shared/lib/runtime-config' import { DecodeError, normalizeRepeatedSlashes } from '../shared/lib/utils' import { isTargetLikeServerless } from './utils' -import Router, { replaceBasePath, route } from './router' +import Router, { route } from './router' import { setRevalidateHeaders } from './send-payload/revalidate-headers' import { IncrementalCache } from './incremental-cache' import { execOnce } from '../shared/lib/utils' @@ -64,6 +64,7 @@ import { addRequestMeta, getRequestMeta } from './request-meta' import { createHeaderRoute, createRedirectRoute } from './server-route-utils' import { PrerenderManifest } from '../build' import { ImageConfigComplete } from '../shared/lib/image-config' +import { replaceBasePath } from './router-utils' export type FindComponentsResult = { components: LoadComponentsReturnType diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 2a18039e6481..b30545ae5bdc 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -41,7 +41,8 @@ import { } from '../../shared/lib/router/utils' import Server, { WrappedBuildError } from '../next-server' import { normalizePagePath } from '../normalize-page-path' -import Router, { hasBasePath, replaceBasePath, route } from '../router' +import Router, { route } from '../router' +import { hasBasePath, replaceBasePath } from '../router-utils' import { eventCliSession } from '../../telemetry/events' import { Telemetry } from '../../telemetry/storage' import { setGlobal } from '../../trace' diff --git a/packages/next/server/router-utils.ts b/packages/next/server/router-utils.ts new file mode 100644 index 000000000000..630521434fdb --- /dev/null +++ b/packages/next/server/router-utils.ts @@ -0,0 +1,17 @@ +export function replaceBasePath(pathname: string, basePath: string): string { + // ensure basePath is only stripped if it matches exactly + // and doesn't contain extra chars e.g. basePath /docs + // should replace for /docs, /docs/, /docs/a but not /docsss + if (hasBasePath(pathname, basePath)) { + pathname = pathname.substr(basePath.length) + if (!pathname.startsWith('/')) pathname = `/${pathname}` + } + return pathname +} + +export function hasBasePath(pathname: string, basePath: string): boolean { + return ( + typeof pathname === 'string' && + (pathname === basePath || pathname.startsWith(basePath + '/')) + ) +} diff --git a/packages/next/server/web/next-url.ts b/packages/next/server/web/next-url.ts index 532b93e64ab3..8ceaea68323e 100644 --- a/packages/next/server/web/next-url.ts +++ b/packages/next/server/web/next-url.ts @@ -1,8 +1,8 @@ import type { PathLocale } from '../../shared/lib/i18n/normalize-locale-path' import type { DomainLocale, I18NConfig } from '../config-shared' import { getLocaleMetadata } from '../../shared/lib/i18n/get-locale-metadata' -import { replaceBasePath } from '../router' import cookie from 'next/dist/compiled/cookie' +import { replaceBasePath } from '../router-utils' interface Options { base?: string | URL diff --git a/packages/next/shared/lib/router/utils/parse-next-url.ts b/packages/next/shared/lib/router/utils/parse-next-url.ts index 57985ecfaf0c..0e36e4b6191e 100644 --- a/packages/next/shared/lib/router/utils/parse-next-url.ts +++ b/packages/next/shared/lib/router/utils/parse-next-url.ts @@ -4,7 +4,7 @@ import { parseUrl } from './parse-url' import type { NextConfig, DomainLocale } from '../../../../server/config-shared' import type { ParsedUrl } from './parse-url' import type { PathLocale } from '../../i18n/normalize-locale-path' -import { hasBasePath, replaceBasePath } from '../../../../server/router' +import { hasBasePath, replaceBasePath } from '../../../../server/router-utils' interface Params { headers?: { [key: string]: string | string[] | undefined } From 72478c5c80d152ad99ce179b4ef5cfac055b98d5 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 22 Mar 2022 13:58:55 -0500 Subject: [PATCH 05/39] Update next/link error when no children are provided (#35453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update next/link error when no children are provided * update manifest * Apply suggestions from code review Co-authored-by: Balázs Orbán --- errors/link-no-children.md | 35 +++++++++++++++++++ errors/manifest.json | 4 +++ packages/next/client/link.tsx | 5 +++ .../client-navigation/pages/link-no-child.js | 10 ++++++ .../client-navigation/test/index.test.js | 8 +++++ 5 files changed, 62 insertions(+) create mode 100644 errors/link-no-children.md create mode 100644 test/integration/client-navigation/pages/link-no-child.js diff --git a/errors/link-no-children.md b/errors/link-no-children.md new file mode 100644 index 000000000000..103018f1f80e --- /dev/null +++ b/errors/link-no-children.md @@ -0,0 +1,35 @@ +# No children were passed to + +#### Why This Error Occurred + +In your application code `next/link` was used without passing a child: + +For example: + +```js +import Link from 'next/link' + +export default function Home() { + return ( + + // or + + ) +} +``` + +#### Possible Ways to Fix It + +Make sure one child is used when using ``: + +```js +import Link from 'next/link' + +export default function Home() { + return ( + + To About + + ) +} +``` diff --git a/errors/manifest.json b/errors/manifest.json index d80ef5787008..ae234227e647 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -514,6 +514,10 @@ "title": "future-webpack5-moved-to-webpack5", "path": "/errors/future-webpack5-moved-to-webpack5.md" }, + { + "title": "link-no-children", + "path": "/errors/link-no-children.md" + }, { "title": "link-multiple-children", "path": "/errors/link-multiple-children.md" diff --git a/packages/next/client/link.tsx b/packages/next/client/link.tsx index 9bb634d251ad..379d21123e15 100644 --- a/packages/next/client/link.tsx +++ b/packages/next/client/link.tsx @@ -231,6 +231,11 @@ function Link(props: React.PropsWithChildren) { try { child = React.Children.only(children) } catch (err) { + if (!children) { + throw new Error( + `No children were passed to with \`href\` of \`${props.href}\` but one child is required https://nextjs.org/docs/messages/link-no-children` + ) + } throw new Error( `Multiple children were passed to with \`href\` of \`${props.href}\` but only one child is supported https://nextjs.org/docs/messages/link-multiple-children` + (typeof window !== 'undefined' diff --git a/test/integration/client-navigation/pages/link-no-child.js b/test/integration/client-navigation/pages/link-no-child.js new file mode 100644 index 000000000000..e3786f932ee7 --- /dev/null +++ b/test/integration/client-navigation/pages/link-no-child.js @@ -0,0 +1,10 @@ +import React from 'react' +import Link from 'next/link' + +export default function Page() { + return ( +
+ Hello World. +
+ ) +} diff --git a/test/integration/client-navigation/test/index.test.js b/test/integration/client-navigation/test/index.test.js index 2414ac400a1a..b0fc4128bf95 100644 --- a/test/integration/client-navigation/test/index.test.js +++ b/test/integration/client-navigation/test/index.test.js @@ -60,6 +60,14 @@ describe('Client Navigation', () => { await browser.close() }) + it('should have proper error when no children are provided', async () => { + const browser = await webdriver(context.appPort, '/link-no-child') + expect(await hasRedbox(browser, true)).toBe(true) + expect(await getRedboxHeader(browser)).toContain( + 'No children were passed to with `href` of `/about` but one child is required' + ) + }) + it('should navigate back after reload', async () => { const browser = await webdriver(context.appPort, '/nav') await browser.elementByCss('#about-link').click() From 9fef0d55da5edef961e8ff1cf0ba954c74a6439d Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 23 Mar 2022 09:28:04 -0500 Subject: [PATCH 06/39] Normalize ssgCacheKey for /index with minimalMode de-duping (#35536) * Normalize ssgCacheKey for /index * apply test changes to i18n suite as well --- packages/next/server/base-server.ts | 4 ++ .../required-server-files-i18n.test.ts | 20 +++++---- test/production/required-server-files.test.ts | 43 +++++++++++++++---- .../required-server-files/pages/gssp.js | 7 ++- .../required-server-files/pages/index.js | 8 +++- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index a575e1ad5969..5168da0997e9 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1323,6 +1323,10 @@ export default abstract class Server { return seg }) .join('/') + + // ensure /index and / is normalized to one key + ssgCacheKey = + ssgCacheKey === '/index' && pathname === '/' ? '/' : ssgCacheKey } const doRender: () => Promise = async () => { diff --git a/test/production/required-server-files-i18n.test.ts b/test/production/required-server-files-i18n.test.ts index 43303acf2485..b74cd9bd9f0d 100644 --- a/test/production/required-server-files-i18n.test.ts +++ b/test/production/required-server-files-i18n.test.ts @@ -162,6 +162,8 @@ describe('should set-up next', () => { const res2 = await fetchViaHTTP(appPort, '/gssp', undefined, { redirect: 'manual ', }) + await next.patchFile('standalone/data.txt', 'show') + expect(res2.status).toBe(404) expect(res2.headers.get('cache-control')).toBe( 's-maxage=1, stale-while-revalidate' @@ -169,18 +171,18 @@ describe('should set-up next', () => { }) it('should render SSR page correctly', async () => { - const html = await renderViaHTTP(appPort, '/') + const html = await renderViaHTTP(appPort, '/gssp') const $ = cheerio.load(html) const data = JSON.parse($('#props').text()) - expect($('#index').text()).toBe('index page') + expect($('#gssp').text()).toBe('getServerSideProps page') expect(data.hello).toBe('world') - const html2 = await renderViaHTTP(appPort, '/') + const html2 = await renderViaHTTP(appPort, '/gssp') const $2 = cheerio.load(html2) const data2 = JSON.parse($2('#props').text()) - expect($2('#index').text()).toBe('index page') + expect($2('#gssp').text()).toBe('getServerSideProps page') expect(isNaN(data2.random)).toBe(false) expect(data2.random).not.toBe(data.random) }) @@ -244,24 +246,24 @@ describe('should set-up next', () => { it('should render SSR page correctly with x-matched-path', async () => { const html = await renderViaHTTP(appPort, '/some-other-path', undefined, { headers: { - 'x-matched-path': '/', + 'x-matched-path': '/gssp', }, }) const $ = cheerio.load(html) const data = JSON.parse($('#props').text()) - expect($('#index').text()).toBe('index page') + expect($('#gssp').text()).toBe('getServerSideProps page') expect(data.hello).toBe('world') const html2 = await renderViaHTTP(appPort, '/some-other-path', undefined, { headers: { - 'x-matched-path': '/', + 'x-matched-path': '/gssp', }, }) const $2 = cheerio.load(html2) const data2 = JSON.parse($2('#props').text()) - expect($2('#index').text()).toBe('index page') + expect($2('#gssp').text()).toBe('getServerSideProps page') expect(isNaN(data2.random)).toBe(false) expect(data2.random).not.toBe(data.random) }) @@ -508,7 +510,7 @@ describe('should set-up next', () => { }, { headers: { - 'x-matched-path': '/', + 'x-matched-path': '/gssp', }, } ) diff --git a/test/production/required-server-files.test.ts b/test/production/required-server-files.test.ts index 4f8eb6969c6c..010f7dbbf858 100644 --- a/test/production/required-server-files.test.ts +++ b/test/production/required-server-files.test.ts @@ -176,6 +176,29 @@ describe('should set-up next', () => { expect(res2.status).toBe(200) const { pageProps: props2 } = await res2.json() expect(props2.gspCalls).toBe(props.gspCalls) + + const res3 = await fetchViaHTTP(appPort, '/index', undefined, { + redirect: 'manual', + headers: { + 'x-matched-path': '/index', + }, + }) + expect(res3.status).toBe(200) + const $2 = cheerio.load(await res3.text()) + const props3 = JSON.parse($2('#props').text()) + expect(props3.gspCalls).toBeDefined() + + const res4 = await fetchViaHTTP( + appPort, + `/_next/data/${next.buildId}/index.json`, + undefined, + { + redirect: 'manual', + } + ) + expect(res4.status).toBe(200) + const { pageProps: props4 } = await res4.json() + expect(props4.gspCalls).toBe(props3.gspCalls) }) it('should set correct SWR headers with notFound gsp', async () => { @@ -218,6 +241,8 @@ describe('should set-up next', () => { const res2 = await fetchViaHTTP(appPort, '/gssp', undefined, { redirect: 'manual ', }) + await next.patchFile('standalone/data.txt', 'show') + expect(res2.status).toBe(404) expect(res2.headers.get('cache-control')).toBe( 's-maxage=1, stale-while-revalidate' @@ -225,18 +250,18 @@ describe('should set-up next', () => { }) it('should render SSR page correctly', async () => { - const html = await renderViaHTTP(appPort, '/') + const html = await renderViaHTTP(appPort, '/gssp') const $ = cheerio.load(html) const data = JSON.parse($('#props').text()) - expect($('#index').text()).toBe('index page') + expect($('#gssp').text()).toBe('getServerSideProps page') expect(data.hello).toBe('world') - const html2 = await renderViaHTTP(appPort, '/') + const html2 = await renderViaHTTP(appPort, '/gssp') const $2 = cheerio.load(html2) const data2 = JSON.parse($2('#props').text()) - expect($2('#index').text()).toBe('index page') + expect($2('#gssp').text()).toBe('getServerSideProps page') expect(isNaN(data2.random)).toBe(false) expect(data2.random).not.toBe(data.random) }) @@ -300,24 +325,24 @@ describe('should set-up next', () => { it('should render SSR page correctly with x-matched-path', async () => { const html = await renderViaHTTP(appPort, '/some-other-path', undefined, { headers: { - 'x-matched-path': '/', + 'x-matched-path': '/gssp', }, }) const $ = cheerio.load(html) const data = JSON.parse($('#props').text()) - expect($('#index').text()).toBe('index page') + expect($('#gssp').text()).toBe('getServerSideProps page') expect(data.hello).toBe('world') const html2 = await renderViaHTTP(appPort, '/some-other-path', undefined, { headers: { - 'x-matched-path': '/', + 'x-matched-path': '/gssp', }, }) const $2 = cheerio.load(html2) const data2 = JSON.parse($2('#props').text()) - expect($2('#index').text()).toBe('index page') + expect($2('#gssp').text()).toBe('getServerSideProps page') expect(isNaN(data2.random)).toBe(false) expect(data2.random).not.toBe(data.random) }) @@ -564,7 +589,7 @@ describe('should set-up next', () => { }, { headers: { - 'x-matched-path': '/', + 'x-matched-path': '/gssp', }, } ) diff --git a/test/production/required-server-files/pages/gssp.js b/test/production/required-server-files/pages/gssp.js index 653736f016f4..625cfb7c5138 100644 --- a/test/production/required-server-files/pages/gssp.js +++ b/test/production/required-server-files/pages/gssp.js @@ -1,5 +1,6 @@ import fs from 'fs' import path from 'path' +import { useRouter } from 'next/router' export async function getServerSideProps({ res }) { res.setHeader('cache-control', 's-maxage=1, stale-while-revalidate') @@ -19,6 +20,7 @@ export async function getServerSideProps({ res }) { props: { hello: 'world', data, + random: Math.random(), // make sure fetch if polyfilled example: await fetch('https://example.com').then((res) => res.text()), }, @@ -26,9 +28,12 @@ export async function getServerSideProps({ res }) { } export default function Page(props) { + const router = useRouter() + return ( <> -

getStaticProps page

+

getServerSideProps page

+

{JSON.stringify(router)}

{JSON.stringify(props)}

) diff --git a/test/production/required-server-files/pages/index.js b/test/production/required-server-files/pages/index.js index 74e172d935ab..8e07e5173461 100644 --- a/test/production/required-server-files/pages/index.js +++ b/test/production/required-server-files/pages/index.js @@ -8,12 +8,18 @@ if (localConfig.hello !== 'world') { throw new Error('oof import order is wrong, _app comes first') } -export const getServerSideProps = ({ req }) => { +let gspCalls = 0 + +export const getStaticProps = () => { + gspCalls += 1 + return { props: { hello: 'world', random: Math.random(), + gspCalls, }, + revalidate: 1, } } From 2cf6696fff59a34425cf77597e8b1a70388d7f4b Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 23 Mar 2022 16:19:58 +0100 Subject: [PATCH 07/39] Merge rsc queries handling (#35545) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../next-middleware-ssr-loader/index.ts | 1 - .../next-middleware-ssr-loader/render.ts | 21 ------------------- packages/next/server/render.tsx | 19 ++++++++++------- packages/next/server/web-server.ts | 4 ---- 4 files changed, 11 insertions(+), 34 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 51caf137f478..3e07e9ebb933 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -56,7 +56,6 @@ export default async function middlewareSSRLoader(this: any) { buildManifest, reactLoadableManifest, serverComponentManifest: ${isServerComponent} ? rscManifest : null, - isServerComponent: ${isServerComponent}, config: ${stringifiedConfig}, buildId: ${JSON.stringify(buildId)}, }) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index 77b553d2b6da..44f08b11c9ea 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -25,7 +25,6 @@ export function getRender({ buildManifest, reactLoadableManifest, serverComponentManifest, - isServerComponent, config, buildId, }: { @@ -39,7 +38,6 @@ export function getRender({ buildManifest: BuildManifest reactLoadableManifest: ReactLoadableManifest serverComponentManifest: any | null - isServerComponent: boolean config: NextConfig buildId: string }) { @@ -109,10 +107,6 @@ export function getRender({ const requestHandler = server.getRequestHandler() return async function render(request: NextRequest) { - const { nextUrl: url } = request - const { searchParams } = url - const query = Object.fromEntries(searchParams) - // Preflight request if (request.method === 'HEAD') { // Hint the client that the matched route is a SSR page. @@ -123,21 +117,6 @@ export function getRender({ }) } - const renderServerComponentData = isServerComponent - ? query.__flight__ !== undefined - : false - - const serverComponentProps = - isServerComponent && query.__props__ - ? JSON.parse(query.__props__) - : undefined - - // Extend the render options. - server.updateRenderOpts({ - renderServerComponentData, - serverComponentProps, - }) - const extendedReq = new WebNextRequest(request) const extendedRes = new WebNextResponse() requestHandler(extendedReq, extendedRes) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index c13d5ff65c12..f55aad54a077 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -231,8 +231,6 @@ export type RenderOptsPartial = { resolvedUrl?: string resolvedAsPath?: string serverComponentManifest?: any - renderServerComponentData?: boolean - serverComponentProps?: any distDir?: string locale?: string locales?: string[] @@ -451,7 +449,6 @@ export async function renderToHTML( getStaticPaths, getServerSideProps, serverComponentManifest, - serverComponentProps, isDataReq, params, previewProps, @@ -505,11 +502,17 @@ export async function renderToHTML( return '' } - let { renderServerComponentData } = renderOpts - if (isServerComponent && query.__flight__) { - renderServerComponentData = true - delete query.__flight__ - } + let renderServerComponentData = isServerComponent + ? query.__flight__ !== undefined + : false + + const serverComponentProps = + isServerComponent && query.__props__ + ? JSON.parse(query.__props__ as string) + : undefined + + delete query.__flight__ + delete query.__props__ const callMiddleware = async (method: string, args: any[], props = false) => { let results: any = props ? {} : [] diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index db88f5686bdf..dab158fffb9d 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -203,8 +203,4 @@ export default class NextWebServer extends BaseServer { components: result, } } - - public updateRenderOpts(renderOpts: Partial) { - Object.assign(this.renderOpts, renderOpts) - } } From 432261a1e15b253b8a56be42cc3698a4d7bdf513 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 23 Mar 2022 13:43:12 -0500 Subject: [PATCH 08/39] Add link for revalidate from notFound section (#35553) --- docs/api-reference/data-fetching/get-static-props.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/data-fetching/get-static-props.md b/docs/api-reference/data-fetching/get-static-props.md index c5f34276ffa8..66d734a6f9fc 100644 --- a/docs/api-reference/data-fetching/get-static-props.md +++ b/docs/api-reference/data-fetching/get-static-props.md @@ -85,7 +85,7 @@ Learn more about [Incremental Static Regeneration](/docs/basic-features/data-fet ### `notFound` -The `notFound` boolean allows the page to return a `404` status and [404 Page](/docs/advanced-features/custom-error-page.md#404-page). With `notFound: true`, the page will return a `404` even if there was a successfully generated page before. This is meant to support use cases like user-generated content getting removed by its author. +The `notFound` boolean allows the page to return a `404` status and [404 Page](/docs/advanced-features/custom-error-page.md#404-page). With `notFound: true`, the page will return a `404` even if there was a successfully generated page before. This is meant to support use cases like user-generated content getting removed by its author. Note, `notFound` follows the same `revalidate` behavior [described here](/docs/api-reference/data-fetching/get-static-props.md#revalidate) ```js export async function getStaticProps(context) { From ebe1b4cb47e2680cab214710aea4d782b885f405 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 23 Mar 2022 20:20:36 +0100 Subject: [PATCH 09/39] Upgrade react-server-dom-webpack (#35524) There're some changes since our last update that we'll need (e.g. server context). ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] 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 `yarn lint` --- ...bpack-writer.browser.development.server.js | 1100 +++++++++++++++-- ...ck-writer.browser.production.min.server.js | 57 +- .../react-server-dom-webpack.development.js | 68 +- ...react-server-dom-webpack.production.min.js | 23 +- packages/next/package.json | 2 +- yarn.lock | 9 +- 6 files changed, 1055 insertions(+), 204 deletions(-) diff --git a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js index 6f4c459c3a7e..948d711b6407 100644 --- a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js +++ b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.development.server.js @@ -1,4 +1,5 @@ -/** @license React vundefined +/** + * @license React * react-server-dom-webpack-writer.browser.development.server.js * * Copyright (c) Facebook, Inc. and its affiliates. @@ -57,9 +58,68 @@ function printWarning(level, format, args) { function scheduleWork(callback) { callback(); } +var VIEW_SIZE = 512; +var currentView = null; +var writtenBytes = 0; +function beginWriting(destination) { + currentView = new Uint8Array(VIEW_SIZE); + writtenBytes = 0; +} function writeChunk(destination, chunk) { - destination.enqueue(chunk); - return destination.desiredSize > 0; + if (chunk.length === 0) { + return; + } + + if (chunk.length > VIEW_SIZE) { + // this chunk may overflow a single view which implies it was not + // one that is cached by the streaming renderer. We will enqueu + // it directly and expect it is not re-used + if (writtenBytes > 0) { + destination.enqueue(new Uint8Array(currentView.buffer, 0, writtenBytes)); + currentView = new Uint8Array(VIEW_SIZE); + writtenBytes = 0; + } + + destination.enqueue(chunk); + return; + } + + var bytesToWrite = chunk; + var allowableBytes = currentView.length - writtenBytes; + + if (allowableBytes < bytesToWrite.length) { + // this chunk would overflow the current view. We enqueue a full view + // and start a new view with the remaining chunk + if (allowableBytes === 0) { + // the current view is already full, send it + destination.enqueue(currentView); + } else { + // fill up the current view and apply the remaining chunk bytes + // to a new view. + currentView.set(bytesToWrite.subarray(0, allowableBytes), writtenBytes); // writtenBytes += allowableBytes; // this can be skipped because we are going to immediately reset the view + + destination.enqueue(currentView); + bytesToWrite = bytesToWrite.subarray(allowableBytes); + } + + currentView = new Uint8Array(VIEW_SIZE); + writtenBytes = 0; + } + + currentView.set(bytesToWrite, writtenBytes); + writtenBytes += bytesToWrite.length; +} +function writeChunkAndReturn(destination, chunk) { + writeChunk(destination, chunk); // in web streams there is no backpressure so we can alwas write more + + return true; +} +function completeWriting(destination) { + if (currentView && writtenBytes > 0) { + destination.enqueue(new Uint8Array(currentView.buffer, 0, writtenBytes)); + currentView = null; + writtenBytes = 0; + } } function close(destination) { destination.close(); @@ -68,6 +128,9 @@ var textEncoder = new TextEncoder(); function stringToChunk(content) { return textEncoder.encode(content); } +function stringToPrecomputedChunk(content) { + return textEncoder.encode(content); +} function closeWithError(destination, error) { if (typeof destination.error === 'function') { // $FlowFixMe: This is an Error object or the destination accepts other types. @@ -108,6 +171,10 @@ function processModuleChunk(request, id, moduleMetaData) { var row = serializeRowHeader('M', id) + json + '\n'; return stringToChunk(row); } +function processProviderChunk(request, id, contextName) { + var row = serializeRowHeader('P', id) + contextName + '\n'; + return stringToChunk(row); +} function processSymbolChunk(request, id, name) { var json = stringify(name); var row = serializeRowHeader('S', id) + json + '\n'; @@ -129,53 +196,758 @@ function resolveModuleMetaData(config, moduleReference) { // ATTENTION // When adding new symbols to this file, // Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols' -// The Symbol used to tag the ReactElement-like types. If there is no native Symbol -// nor polyfill, then a plain number is used for performance. -var REACT_ELEMENT_TYPE = 0xeac7; -var REACT_PORTAL_TYPE = 0xeaca; -var REACT_FRAGMENT_TYPE = 0xeacb; -var REACT_STRICT_MODE_TYPE = 0xeacc; -var REACT_PROFILER_TYPE = 0xead2; -var REACT_PROVIDER_TYPE = 0xeacd; -var REACT_CONTEXT_TYPE = 0xeace; -var REACT_FORWARD_REF_TYPE = 0xead0; -var REACT_SUSPENSE_TYPE = 0xead1; -var REACT_SUSPENSE_LIST_TYPE = 0xead8; -var REACT_MEMO_TYPE = 0xead3; -var REACT_LAZY_TYPE = 0xead4; -var REACT_SCOPE_TYPE = 0xead7; -var REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1; -var REACT_OFFSCREEN_TYPE = 0xeae2; -var REACT_LEGACY_HIDDEN_TYPE = 0xeae3; -var REACT_CACHE_TYPE = 0xeae4; - -if (typeof Symbol === 'function' && Symbol.for) { - var symbolFor = Symbol.for; - REACT_ELEMENT_TYPE = symbolFor('react.element'); - REACT_PORTAL_TYPE = symbolFor('react.portal'); - REACT_FRAGMENT_TYPE = symbolFor('react.fragment'); - REACT_STRICT_MODE_TYPE = symbolFor('react.strict_mode'); - REACT_PROFILER_TYPE = symbolFor('react.profiler'); - REACT_PROVIDER_TYPE = symbolFor('react.provider'); - REACT_CONTEXT_TYPE = symbolFor('react.context'); - REACT_FORWARD_REF_TYPE = symbolFor('react.forward_ref'); - REACT_SUSPENSE_TYPE = symbolFor('react.suspense'); - REACT_SUSPENSE_LIST_TYPE = symbolFor('react.suspense_list'); - REACT_MEMO_TYPE = symbolFor('react.memo'); - REACT_LAZY_TYPE = symbolFor('react.lazy'); - REACT_SCOPE_TYPE = symbolFor('react.scope'); - REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode'); - REACT_OFFSCREEN_TYPE = symbolFor('react.offscreen'); - REACT_LEGACY_HIDDEN_TYPE = symbolFor('react.legacy_hidden'); - REACT_CACHE_TYPE = symbolFor('react.cache'); +// The Symbol used to tag the ReactElement-like types. +var REACT_ELEMENT_TYPE = Symbol.for('react.element'); +var REACT_FRAGMENT_TYPE = Symbol.for('react.fragment'); +var REACT_PROVIDER_TYPE = Symbol.for('react.provider'); +var REACT_SERVER_CONTEXT_TYPE = Symbol.for('react.server_context'); +var REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref'); +var REACT_MEMO_TYPE = Symbol.for('react.memo'); +var REACT_LAZY_TYPE = Symbol.for('react.lazy'); +var REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED = Symbol.for('react.default_value'); + +// A reserved attribute. +// It is handled by React separately and shouldn't be written to the DOM. +var RESERVED = 0; // A simple string attribute. +// Attributes that aren't in the filter are presumed to have this type. + +var STRING = 1; // A string attribute that accepts booleans in React. In HTML, these are called +// "enumerated" attributes with "true" and "false" as possible values. +// When true, it should be set to a "true" string. +// When false, it should be set to a "false" string. + +var BOOLEANISH_STRING = 2; // A real boolean attribute. +// When true, it should be present (set either to an empty string or its name). +// When false, it should be omitted. + +var BOOLEAN = 3; // An attribute that can be used as a flag as well as with a value. +// When true, it should be present (set either to an empty string or its name). +// When false, it should be omitted. +// For any other value, should be present with that value. + +var OVERLOADED_BOOLEAN = 4; // An attribute that must be numeric or parse as a numeric. +// When falsy, it should be removed. + +var NUMERIC = 5; // An attribute that must be positive numeric or parse as a positive numeric. +// When falsy, it should be removed. + +var POSITIVE_NUMERIC = 6; + +function PropertyInfoRecord(name, type, mustUseProperty, attributeName, attributeNamespace, sanitizeURL, removeEmptyString) { + this.acceptsBooleans = type === BOOLEANISH_STRING || type === BOOLEAN || type === OVERLOADED_BOOLEAN; + this.attributeName = attributeName; + this.attributeNamespace = attributeNamespace; + this.mustUseProperty = mustUseProperty; + this.propertyName = name; + this.type = type; + this.sanitizeURL = sanitizeURL; + this.removeEmptyString = removeEmptyString; +} // When adding attributes to this list, be sure to also add them to +// the `possibleStandardNames` module to ensure casing and incorrect +// name warnings. + + +var properties = {}; // These props are reserved by React. They shouldn't be written to the DOM. + +var reservedProps = ['children', 'dangerouslySetInnerHTML', // TODO: This prevents the assignment of defaultValue to regular +// elements (not just inputs). Now that ReactDOMInput assigns to the +// defaultValue property -- do we need this? +'defaultValue', 'defaultChecked', 'innerHTML', 'suppressContentEditableWarning', 'suppressHydrationWarning', 'style']; + +{ + reservedProps.push('innerText', 'textContent'); } +reservedProps.forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, RESERVED, false, // mustUseProperty + name, // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // A few React string attributes have a different name. +// This is a mapping from React prop names to the attribute names. + +[['acceptCharset', 'accept-charset'], ['className', 'class'], ['htmlFor', 'for'], ['httpEquiv', 'http-equiv']].forEach(function (_ref) { + var name = _ref[0], + attributeName = _ref[1]; + properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty + attributeName, // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These are "enumerated" HTML attributes that accept "true" and "false". +// In React, we let users pass `true` and `false` even though technically +// these aren't boolean attributes (they are coerced to strings). + +['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, // mustUseProperty + name.toLowerCase(), // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These are "enumerated" SVG attributes that accept "true" and "false". +// In React, we let users pass `true` and `false` even though technically +// these aren't boolean attributes (they are coerced to strings). +// Since these are SVG attributes, their attribute names are case-sensitive. + +['autoReverse', 'externalResourcesRequired', 'focusable', 'preserveAlpha'].forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, // mustUseProperty + name, // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These are HTML boolean attributes. + +['allowFullScreen', 'async', // Note: there is a special case that prevents it from being written to the DOM +// on the client side because the browsers are inconsistent. Instead we call focus(). +'autoFocus', 'autoPlay', 'controls', 'default', 'defer', 'disabled', 'disablePictureInPicture', 'disableRemotePlayback', 'formNoValidate', 'hidden', 'loop', 'noModule', 'noValidate', 'open', 'playsInline', 'readOnly', 'required', 'reversed', 'scoped', 'seamless', // Microdata +'itemScope'].forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, BOOLEAN, false, // mustUseProperty + name.toLowerCase(), // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These are the few React props that we set as DOM properties +// rather than attributes. These are all booleans. + +['checked', // Note: `option.selected` is not updated if `select.multiple` is +// disabled with `removeAttribute`. We have special logic for handling this. +'multiple', 'muted', 'selected' // NOTE: if you add a camelCased prop to this list, +// you'll need to set attributeName to name.toLowerCase() +// instead in the assignment below. +].forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, BOOLEAN, true, // mustUseProperty + name, // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These are HTML attributes that are "overloaded booleans": they behave like +// booleans, but can also accept a string value. + +['capture', 'download' // NOTE: if you add a camelCased prop to this list, +// you'll need to set attributeName to name.toLowerCase() +// instead in the assignment below. +].forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, OVERLOADED_BOOLEAN, false, // mustUseProperty + name, // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These are HTML attributes that must be positive numbers. + +['cols', 'rows', 'size', 'span' // NOTE: if you add a camelCased prop to this list, +// you'll need to set attributeName to name.toLowerCase() +// instead in the assignment below. +].forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, POSITIVE_NUMERIC, false, // mustUseProperty + name, // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These are HTML attributes that must be numbers. + +['rowSpan', 'start'].forEach(function (name) { + properties[name] = new PropertyInfoRecord(name, NUMERIC, false, // mustUseProperty + name.toLowerCase(), // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); +var CAMELIZE = /[\-\:]([a-z])/g; + +var capitalize = function (token) { + return token[1].toUpperCase(); +}; // This is a list of all SVG attributes that need special casing, namespacing, +// or boolean value assignment. Regular attributes that just accept strings +// and have the same names are omitted, just like in the HTML attribute filter. +// Some of these attributes can be hard to find. This list was created by +// scraping the MDN documentation. + + +['accent-height', 'alignment-baseline', 'arabic-form', 'baseline-shift', 'cap-height', 'clip-path', 'clip-rule', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'dominant-baseline', 'enable-background', 'fill-opacity', 'fill-rule', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-name', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'horiz-adv-x', 'horiz-origin-x', 'image-rendering', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'overline-position', 'overline-thickness', 'paint-order', 'panose-1', 'pointer-events', 'rendering-intent', 'shape-rendering', 'stop-color', 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'underline-position', 'underline-thickness', 'unicode-bidi', 'unicode-range', 'units-per-em', 'v-alphabetic', 'v-hanging', 'v-ideographic', 'v-mathematical', 'vector-effect', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'word-spacing', 'writing-mode', 'xmlns:xlink', 'x-height' // NOTE: if you add a camelCased prop to this list, +// you'll need to set attributeName to name.toLowerCase() +// instead in the assignment below. +].forEach(function (attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty + attributeName, null, // attributeNamespace + false, // sanitizeURL + false); +}); // String SVG attributes with the xlink namespace. + +['xlink:actuate', 'xlink:arcrole', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type' // NOTE: if you add a camelCased prop to this list, +// you'll need to set attributeName to name.toLowerCase() +// instead in the assignment below. +].forEach(function (attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty + attributeName, 'http://www.w3.org/1999/xlink', false, // sanitizeURL + false); +}); // String SVG attributes with the xml namespace. + +['xml:base', 'xml:lang', 'xml:space' // NOTE: if you add a camelCased prop to this list, +// you'll need to set attributeName to name.toLowerCase() +// instead in the assignment below. +].forEach(function (attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty + attributeName, 'http://www.w3.org/XML/1998/namespace', false, // sanitizeURL + false); +}); // These attribute exists both in HTML and SVG. +// The attribute name is case-sensitive in SVG so we can't just use +// the React name like we do for attributes that exist only in HTML. + +['tabIndex', 'crossOrigin'].forEach(function (attributeName) { + properties[attributeName] = new PropertyInfoRecord(attributeName, STRING, false, // mustUseProperty + attributeName.toLowerCase(), // attributeName + null, // attributeNamespace + false, // sanitizeURL + false); +}); // These attributes accept URLs. These must not allow javascript: URLS. +// These will also need to accept Trusted Types object in the future. + +var xlinkHref = 'xlinkHref'; +properties[xlinkHref] = new PropertyInfoRecord('xlinkHref', STRING, false, // mustUseProperty +'xlink:href', 'http://www.w3.org/1999/xlink', true, // sanitizeURL +false); +['src', 'href', 'action', 'formAction'].forEach(function (attributeName) { + properties[attributeName] = new PropertyInfoRecord(attributeName, STRING, false, // mustUseProperty + attributeName.toLowerCase(), // attributeName + null, // attributeNamespace + true, // sanitizeURL + true); +}); + +/** + * CSS properties which accept numbers but are not in units of "px". + */ +var isUnitlessNumber = { + animationIterationCount: true, + aspectRatio: true, + borderImageOutset: true, + borderImageSlice: true, + borderImageWidth: true, + boxFlex: true, + boxFlexGroup: true, + boxOrdinalGroup: true, + columnCount: true, + columns: true, + flex: true, + flexGrow: true, + flexPositive: true, + flexShrink: true, + flexNegative: true, + flexOrder: true, + gridArea: true, + gridRow: true, + gridRowEnd: true, + gridRowSpan: true, + gridRowStart: true, + gridColumn: true, + gridColumnEnd: true, + gridColumnSpan: true, + gridColumnStart: true, + fontWeight: true, + lineClamp: true, + lineHeight: true, + opacity: true, + order: true, + orphans: true, + tabSize: true, + widows: true, + zIndex: true, + zoom: true, + // SVG-related properties + fillOpacity: true, + floodOpacity: true, + stopOpacity: true, + strokeDasharray: true, + strokeDashoffset: true, + strokeMiterlimit: true, + strokeOpacity: true, + strokeWidth: true +}; +/** + * @param {string} prefix vendor-specific prefix, eg: Webkit + * @param {string} key style name, eg: transitionDuration + * @return {string} style name prefixed with `prefix`, properly camelCased, eg: + * WebkitTransitionDuration + */ + +function prefixKey(prefix, key) { + return prefix + key.charAt(0).toUpperCase() + key.substring(1); +} +/** + * Support style names that may come passed in prefixed by adding permutations + * of vendor prefixes. + */ + + +var prefixes = ['Webkit', 'ms', 'Moz', 'O']; // Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an +// infinite loop, because it iterates over the newly added props too. + +Object.keys(isUnitlessNumber).forEach(function (prop) { + prefixes.forEach(function (prefix) { + isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; + }); +}); + var isArrayImpl = Array.isArray; // eslint-disable-next-line no-redeclare function isArray(a) { return isArrayImpl(a); } +var startInlineScript = stringToPrecomputedChunk(''); +var startScriptSrc = stringToPrecomputedChunk(''); // Allows us to keep track of what we've already written so we can refer back to it. + +var textSeparator = stringToPrecomputedChunk(''); + +var styleAttributeStart = stringToPrecomputedChunk(' style="'); +var styleAssign = stringToPrecomputedChunk(':'); +var styleSeparator = stringToPrecomputedChunk(';'); + +var attributeSeparator = stringToPrecomputedChunk(' '); +var attributeAssign = stringToPrecomputedChunk('="'); +var attributeEnd = stringToPrecomputedChunk('"'); +var attributeEmptyString = stringToPrecomputedChunk('=""'); + +var endOfStartTag = stringToPrecomputedChunk('>'); +var endOfStartTagSelfClosing = stringToPrecomputedChunk('/>'); + +var selectedMarkerAttribute = stringToPrecomputedChunk(' selected=""'); + +var leadingNewline = stringToPrecomputedChunk('\n'); + +var DOCTYPE = stringToPrecomputedChunk(''); +var endTag1 = stringToPrecomputedChunk(''); +// A placeholder is a node inside a hidden partial tree that can be filled in later, but before +// display. It's never visible to users. We use the template tag because it can be used in every +// type of parent. '); +var completeBoundaryScript1Full = stringToPrecomputedChunk(completeBoundaryFunction + ';$RC("'); +var completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("'); +var completeBoundaryScript2 = stringToPrecomputedChunk('","'); +var completeBoundaryScript3 = stringToPrecomputedChunk('")'); +var clientRenderScript1Full = stringToPrecomputedChunk(clientRenderFunction + ';$RX("'); +var clientRenderScript1Partial = stringToPrecomputedChunk('$RX("'); +var clientRenderScript2 = stringToPrecomputedChunk('")'); + +var rendererSigil; + +{ + // Use this to detect multiple renderers using the same context + rendererSigil = {}; +} // Used to store the parent path of all context overrides in a shared linked list. +// Forming a reverse tree. + + +var rootContextSnapshot = null; // We assume that this runtime owns the "current" field on all ReactContext instances. +// This global (actually thread local) state represents what state all those "current", +// fields are currently in. + +var currentActiveSnapshot = null; + +function popNode(prev) { + { + prev.context._currentValue = prev.parentValue; + } +} + +function pushNode(next) { + { + next.context._currentValue = next.value; + } +} + +function popToNearestCommonAncestor(prev, next) { + if (prev === next) ; else { + popNode(prev); + var parentPrev = prev.parent; + var parentNext = next.parent; + + if (parentPrev === null) { + if (parentNext !== null) { + throw new Error('The stacks must reach the root at the same time. This is a bug in React.'); + } + } else { + if (parentNext === null) { + throw new Error('The stacks must reach the root at the same time. This is a bug in React.'); + } + + popToNearestCommonAncestor(parentPrev, parentNext); // On the way back, we push the new ones that weren't common. + + pushNode(next); + } + } +} + +function popAllPrevious(prev) { + popNode(prev); + var parentPrev = prev.parent; + + if (parentPrev !== null) { + popAllPrevious(parentPrev); + } +} + +function pushAllNext(next) { + var parentNext = next.parent; + + if (parentNext !== null) { + pushAllNext(parentNext); + } + + pushNode(next); +} + +function popPreviousToCommonLevel(prev, next) { + popNode(prev); + var parentPrev = prev.parent; + + if (parentPrev === null) { + throw new Error('The depth must equal at least at zero before reaching the root. This is a bug in React.'); + } + + if (parentPrev.depth === next.depth) { + // We found the same level. Now we just need to find a shared ancestor. + popToNearestCommonAncestor(parentPrev, next); + } else { + // We must still be deeper. + popPreviousToCommonLevel(parentPrev, next); + } +} + +function popNextToCommonLevel(prev, next) { + var parentNext = next.parent; + + if (parentNext === null) { + throw new Error('The depth must equal at least at zero before reaching the root. This is a bug in React.'); + } + + if (prev.depth === parentNext.depth) { + // We found the same level. Now we just need to find a shared ancestor. + popToNearestCommonAncestor(prev, parentNext); + } else { + // We must still be deeper. + popNextToCommonLevel(prev, parentNext); + } + + pushNode(next); +} // Perform context switching to the new snapshot. +// To make it cheap to read many contexts, while not suspending, we make the switch eagerly by +// updating all the context's current values. That way reads, always just read the current value. +// At the cost of updating contexts even if they're never read by this subtree. + + +function switchContext(newSnapshot) { + // The basic algorithm we need to do is to pop back any contexts that are no longer on the stack. + // We also need to update any new contexts that are now on the stack with the deepest value. + // The easiest way to update new contexts is to just reapply them in reverse order from the + // perspective of the backpointers. To avoid allocating a lot when switching, we use the stack + // for that. Therefore this algorithm is recursive. + // 1) First we pop which ever snapshot tree was deepest. Popping old contexts as we go. + // 2) Then we find the nearest common ancestor from there. Popping old contexts as we go. + // 3) Then we reapply new contexts on the way back up the stack. + var prev = currentActiveSnapshot; + var next = newSnapshot; + + if (prev !== next) { + if (prev === null) { + // $FlowFixMe: This has to be non-null since it's not equal to prev. + pushAllNext(next); + } else if (next === null) { + popAllPrevious(prev); + } else if (prev.depth === next.depth) { + popToNearestCommonAncestor(prev, next); + } else if (prev.depth > next.depth) { + popPreviousToCommonLevel(prev, next); + } else { + popNextToCommonLevel(prev, next); + } + + currentActiveSnapshot = next; + } +} +function pushProvider(context, nextValue) { + var prevValue; + + { + prevValue = context._currentValue; + context._currentValue = nextValue; + + { + if (context._currentRenderer !== undefined && context._currentRenderer !== null && context._currentRenderer !== rendererSigil) { + error('Detected multiple renderers concurrently rendering the ' + 'same context provider. This is currently unsupported.'); + } + + context._currentRenderer = rendererSigil; + } + } + + var prevNode = currentActiveSnapshot; + var newNode = { + parent: prevNode, + depth: prevNode === null ? 0 : prevNode.depth + 1, + context: context, + parentValue: prevValue, + value: nextValue + }; + currentActiveSnapshot = newNode; + return newNode; +} +function popProvider() { + var prevSnapshot = currentActiveSnapshot; + + if (prevSnapshot === null) { + throw new Error('Tried to pop a Context at the root of the app. This is a bug in React.'); + } + + { + var value = prevSnapshot.parentValue; + + if (value === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) { + prevSnapshot.context._currentValue = prevSnapshot.context._defaultValue; + } else { + prevSnapshot.context._currentValue = value; + } + } + + return currentActiveSnapshot = prevSnapshot.parent; +} +function getActiveContext() { + return currentActiveSnapshot; +} +function readContext(context) { + var value = context._currentValue ; + return value; +} + +function readContext$1(context) { + { + if (context.$$typeof !== REACT_SERVER_CONTEXT_TYPE) { + error('Only ServerContext is supported in Flight'); + } + + if (currentCache === null) { + error('Context can only be read while React is rendering. ' + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + 'In function components, you can read it directly in the function body, but not ' + 'inside Hooks like useReducer() or useMemo().'); + } + } + + return readContext(context); +} + +var Dispatcher = { + useMemo: function (nextCreate) { + return nextCreate(); + }, + useCallback: function (callback) { + return callback; + }, + useDebugValue: function () {}, + useDeferredValue: unsupportedHook, + useTransition: unsupportedHook, + getCacheForType: function (resourceType) { + if (!currentCache) { + throw new Error('Reading the cache is only supported while rendering.'); + } + + var entry = currentCache.get(resourceType); + + if (entry === undefined) { + entry = resourceType(); // TODO: Warn if undefined? + + currentCache.set(resourceType, entry); + } + + return entry; + }, + readContext: readContext$1, + useContext: readContext$1, + useReducer: unsupportedHook, + useRef: unsupportedHook, + useState: unsupportedHook, + useInsertionEffect: unsupportedHook, + useLayoutEffect: unsupportedHook, + useImperativeHandle: unsupportedHook, + useEffect: unsupportedHook, + useId: unsupportedHook, + useMutableSource: unsupportedHook, + useSyncExternalStore: unsupportedHook, + useCacheRefresh: function () { + return unsupportedRefresh; + } +}; + +function unsupportedHook() { + throw new Error('This Hook is not supported in Server Components.'); +} + +function unsupportedRefresh() { + if (!currentCache) { + throw new Error('Refreshing the cache is not supported in Server Components.'); + } +} + +var currentCache = null; +function setCurrentCache(cache) { + currentCache = cache; + return currentCache; +} +function getCurrentCache() { + return currentCache; +} + +var ContextRegistry = ReactSharedInternals.ContextRegistry; +function getOrCreateServerContext(globalName) { + if (!ContextRegistry[globalName]) { + ContextRegistry[globalName] = React.createServerContext(globalName, REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED); + } + + return ContextRegistry[globalName]; +} + var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; function defaultErrorHandler(error) { @@ -185,7 +957,7 @@ function defaultErrorHandler(error) { var OPEN = 0; var CLOSING = 1; var CLOSED = 2; -function createRequest(model, bundlerConfig, onError) { +function createRequest(model, bundlerConfig, onError, context) { var pingedSegments = []; var request = { status: OPEN, @@ -201,17 +973,25 @@ function createRequest(model, bundlerConfig, onError) { completedErrorChunks: [], writtenSymbols: new Map(), writtenModules: new Map(), + writtenProviders: new Map(), onError: onError === undefined ? defaultErrorHandler : onError, toJSON: function (key, value) { return resolveModelToJSON(request, this, key, value); } }; request.pendingChunks++; - var rootSegment = createSegment(request, model); + var rootContext = createRootContext(context); + var rootSegment = createSegment(request, model, rootContext); pingedSegments.push(rootSegment); return request; } +function createRootContext(reqContext) { + return importServerContexts(reqContext); +} + +var POP = {}; + function attemptResolveElement(type, key, ref, props) { if (ref !== null && ref !== undefined) { // When the ref moves to the regular props object this will implicitly @@ -245,6 +1025,14 @@ function attemptResolveElement(type, key, ref, props) { } switch (type.$$typeof) { + case REACT_LAZY_TYPE: + { + var payload = type._payload; + var init = type._init; + var wrappedType = init(payload); + return attemptResolveElement(wrappedType, key, ref, props); + } + case REACT_FORWARD_REF_TYPE: { var render = type.render; @@ -255,6 +1043,32 @@ function attemptResolveElement(type, key, ref, props) { { return attemptResolveElement(type.type, key, ref, props); } + + case REACT_PROVIDER_TYPE: + { + pushProvider(type._context, props.value); + + { + var extraKeys = Object.keys(props).filter(function (value) { + if (value === 'children' || value === 'value') { + return false; + } + + return true; + }); + + if (extraKeys.length !== 0) { + error('ServerContext can only have a value prop and children. Found: %s', JSON.stringify(extraKeys)); + } + } + + return [REACT_ELEMENT_TYPE, type, key, // Rely on __popProvider being serialized last to pop the provider. + { + value: props.value, + children: props.children, + __pop: POP + }]; + } } } @@ -272,11 +1086,12 @@ function pingSegment(request, segment) { } } -function createSegment(request, model) { +function createSegment(request, model, context) { var id = request.nextChunkId++; var segment = { id: id, model: model, + context: context, ping: function () { return pingSegment(request, segment); } @@ -305,8 +1120,7 @@ function escapeStringValue(value) { function isObjectPrototype(object) { if (!object) { return false; - } // $FlowFixMe - + } var ObjectPrototype = Object.prototype; @@ -405,8 +1219,7 @@ function describeValueForErrorMessage(value) { function describeObjectForErrorMessage(objectOrArray, expandedName) { if (isArray(objectOrArray)) { - var str = '['; // $FlowFixMe: Should be refined by now. - + var str = '['; var array = objectOrArray; for (var i = 0; i < array.length; i++) { @@ -431,8 +1244,7 @@ function describeObjectForErrorMessage(objectOrArray, expandedName) { str += ']'; return str; } else { - var _str = '{'; // $FlowFixMe: Should be refined by now. - + var _str = '{'; var object = objectOrArray; var names = Object.keys(object); @@ -462,6 +1274,8 @@ function describeObjectForErrorMessage(objectOrArray, expandedName) { } } +var insideContextProps = null; +var isInsideContextValue = false; function resolveModelToJSON(request, parent, key, value) { { // $FlowFixMe @@ -476,29 +1290,55 @@ function resolveModelToJSON(request, parent, key, value) { switch (value) { case REACT_ELEMENT_TYPE: return '$'; + } - case REACT_LAZY_TYPE: - throw new Error('React Lazy Components are not yet supported on the server.'); + { + if (parent[0] === REACT_ELEMENT_TYPE && parent[1] && parent[1].$$typeof === REACT_PROVIDER_TYPE && key === '3') { + insideContextProps = value; + } else if (insideContextProps === parent && key === 'value') { + isInsideContextValue = true; + } else if (insideContextProps === parent && key === 'children') { + isInsideContextValue = false; + } } // Resolve server components. - while (typeof value === 'object' && value !== null && value.$$typeof === REACT_ELEMENT_TYPE) { - // TODO: Concatenate keys of parents onto children. - var element = value; + while (typeof value === 'object' && value !== null && (value.$$typeof === REACT_ELEMENT_TYPE || value.$$typeof === REACT_LAZY_TYPE)) { + { + if (isInsideContextValue) { + error('React elements are not allowed in ServerContext'); + } + } try { - // Attempt to render the server component. - value = attemptResolveElement(element.type, element.key, element.ref, element.props); + switch (value.$$typeof) { + case REACT_ELEMENT_TYPE: + { + // TODO: Concatenate keys of parents onto children. + var element = value; // Attempt to render the server component. + + value = attemptResolveElement(element.type, element.key, element.ref, element.props); + break; + } + + case REACT_LAZY_TYPE: + { + var payload = value._payload; + var init = value._init; + value = init(payload); + break; + } + } } catch (x) { if (typeof x === 'object' && x !== null && typeof x.then === 'function') { // Something suspended, we'll need to create a new segment and resolve it later. request.pendingChunks++; - var newSegment = createSegment(request, value); + var newSegment = createSegment(request, value, getActiveContext()); var ping = newSegment.ping; x.then(ping, ping); return serializeByRefID(newSegment.id); } else { - reportError(request, x); // Something errored. We'll still send everything we have up until this point. + logRecoverableError(request, x); // Something errored. We'll still send everything we have up until this point. // We'll replace this element with a lazy reference that throws on the client // once it gets rendered. @@ -559,6 +1399,28 @@ function resolveModelToJSON(request, parent, key, value) { emitErrorChunk(request, _errorId, x); return serializeByValueID(_errorId); } + } else if (value.$$typeof === REACT_PROVIDER_TYPE) { + var providerKey = value._context._globalName; + var writtenProviders = request.writtenProviders; + var providerId = writtenProviders.get(key); + + if (providerId === undefined) { + request.pendingChunks++; + providerId = request.nextChunkId++; + writtenProviders.set(providerKey, providerId); + emitProviderChunk(request, providerId, providerKey); + } + + return serializeByValueID(providerId); + } else if (value === POP) { + popProvider(); + + { + insideContextProps = null; + isInsideContextValue = false; + } + + return undefined; } { @@ -627,7 +1489,7 @@ function resolveModelToJSON(request, parent, key, value) { throw new Error("Type " + typeof value + " is not supported in client component props. " + ("Remove " + describeKeyForErrorMessage(key) + " from this object, or avoid the entire object: " + describeObjectForErrorMessage(parent))); } -function reportError(request, error) { +function logRecoverableError(request, error) { var onError = request.onError; onError(error); } @@ -677,7 +1539,14 @@ function emitSymbolChunk(request, id, name) { request.completedModuleChunks.push(processedChunk); } +function emitProviderChunk(request, id, contextName) { + var processedChunk = processProviderChunk(request, id, contextName); + request.completedJSONChunks.push(processedChunk); +} + function retrySegment(request, segment) { + switchContext(segment.context); + try { var _value3 = segment.model; @@ -700,7 +1569,7 @@ function retrySegment(request, segment) { x.then(ping, ping); return; } else { - reportError(request, x); // This errored, we need to serialize this error to the + logRecoverableError(request, x); // This errored, we need to serialize this error to the emitErrorChunk(request, segment.id, x); } @@ -709,9 +1578,9 @@ function retrySegment(request, segment) { function performWork(request) { var prevDispatcher = ReactCurrentDispatcher.current; - var prevCache = currentCache; + var prevCache = getCurrentCache(); ReactCurrentDispatcher.current = Dispatcher; - currentCache = request.cache; + setCurrentCache(request.cache); try { var pingedSegments = request.pingedSegments; @@ -726,15 +1595,16 @@ function performWork(request) { flushCompletedChunks(request, request.destination); } } catch (error) { - reportError(request, error); + logRecoverableError(request, error); fatalError(request, error); } finally { ReactCurrentDispatcher.current = prevDispatcher; - currentCache = prevCache; + setCurrentCache(prevCache); } } function flushCompletedChunks(request, destination) { + beginWriting(); try { // We emit module chunks first in the stream so that @@ -745,8 +1615,9 @@ function flushCompletedChunks(request, destination) { for (; i < moduleChunks.length; i++) { request.pendingChunks--; var chunk = moduleChunks[i]; + var keepWriting = writeChunkAndReturn(destination, chunk); - if (!writeChunk(destination, chunk)) { + if (!keepWriting) { request.destination = null; i++; break; @@ -762,7 +1633,9 @@ function flushCompletedChunks(request, destination) { request.pendingChunks--; var _chunk = jsonChunks[i]; - if (!writeChunk(destination, _chunk)) { + var _keepWriting = writeChunkAndReturn(destination, _chunk); + + if (!_keepWriting) { request.destination = null; i++; break; @@ -780,7 +1653,9 @@ function flushCompletedChunks(request, destination) { request.pendingChunks--; var _chunk2 = errorChunks[i]; - if (!writeChunk(destination, _chunk2)) { + var _keepWriting2 = writeChunkAndReturn(destination, _chunk2); + + if (!_keepWriting2) { request.destination = null; i++; break; @@ -789,6 +1664,7 @@ function flushCompletedChunks(request, destination) { errorChunks.splice(0, i); } finally { + completeWriting(destination); } if (request.pendingChunks === 0) { @@ -813,83 +1689,51 @@ function startFlowing(request, destination) { return; } + if (request.destination !== null) { + // We're already flowing. + return; + } + request.destination = destination; try { flushCompletedChunks(request, destination); } catch (error) { - reportError(request, error); + logRecoverableError(request, error); fatalError(request, error); } } -function unsupportedHook() { - throw new Error('This Hook is not supported in Server Components.'); -} - -function unsupportedRefresh() { - if (!currentCache) { - throw new Error('Refreshing the cache is not supported in Server Components.'); - } -} - -var currentCache = null; -var Dispatcher = { - useMemo: function (nextCreate) { - return nextCreate(); - }, - useCallback: function (callback) { - return callback; - }, - useDebugValue: function () {}, - useDeferredValue: unsupportedHook, - useTransition: unsupportedHook, - getCacheForType: function (resourceType) { - if (!currentCache) { - throw new Error('Reading the cache is only supported while rendering.'); - } - - var entry = currentCache.get(resourceType); - - if (entry === undefined) { - entry = resourceType(); // TODO: Warn if undefined? - - currentCache.set(resourceType, entry); +function importServerContexts(contexts) { + if (contexts) { + var prevContext = getActiveContext(); + switchContext(rootContextSnapshot); + + for (var i = 0; i < contexts.length; i++) { + var _contexts$i = contexts[i], + name = _contexts$i[0], + _value4 = _contexts$i[1]; + var context = getOrCreateServerContext(name); + pushProvider(context, _value4); } - return entry; - }, - readContext: unsupportedHook, - useContext: unsupportedHook, - useReducer: unsupportedHook, - useRef: unsupportedHook, - useState: unsupportedHook, - useInsertionEffect: unsupportedHook, - useLayoutEffect: unsupportedHook, - useImperativeHandle: unsupportedHook, - useEffect: unsupportedHook, - useId: unsupportedHook, - useMutableSource: unsupportedHook, - useSyncExternalStore: unsupportedHook, - useCacheRefresh: function () { - return unsupportedRefresh; + var importedContext = getActiveContext(); + switchContext(prevContext); + return importedContext; } -}; -function renderToReadableStream(model, webpackMap, options) { - var request = createRequest(model, webpackMap, options ? options.onError : undefined); + return rootContextSnapshot; +} + +function renderToReadableStream(model, webpackMap, options, context) { + var request = createRequest(model, webpackMap, options ? options.onError : undefined, context); var stream = new ReadableStream({ + type: 'bytes', start: function (controller) { startWork(request); }, pull: function (controller) { - // Pull is called immediately even if the stream is not passed to anything. - // That's buffering too early. We want to start buffering once the stream - // is actually used by something so we can give it the best result possible - // at that point. - if (stream.locked) { - startFlowing(request, controller); - } + startFlowing(request, controller); }, cancel: function (reason) {} }); diff --git a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js index 2977e6eea4d4..bb235427138f 100644 --- a/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js +++ b/packages/next/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js @@ -1,4 +1,5 @@ -/** @license React vundefined +/** + * @license React * react-server-dom-webpack-writer.browser.production.min.server.js * * Copyright (c) Facebook, Inc. and its affiliates. @@ -6,21 +7,39 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -'use strict';var h=require("react");function n(a,d){a.enqueue(d);return 0=a.length?a:a.substr(0,10)+"...");case "object":if(B(a))return"[...]";a=N(a);return"Object"===a?"{...}":a;case "function":return"function";default:return String(a)}} -function P(a,d){if(B(a)){for(var c="[",b=0;b