From f8e66610b65ba1712b4325daa5e2e24354a019b9 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Sat, 6 Nov 2021 19:37:24 +0100 Subject: [PATCH 01/20] Change disabled SWC message to Log.info (#31091) Based on feedback from @flybayer, this changes the `warn - ` to `info -` for the "Disabled SWC" message. ## 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` --- packages/next/build/webpack-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 71174550d9c0f25..730856f88af2440 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -452,7 +452,7 @@ export default async function getBaseWebpackConfig( let useSWCLoader = !babelConfigFile if (!loggedSwcDisabled && !useSWCLoader && babelConfigFile) { - Log.warn( + Log.info( `Disabled SWC as replacement for Babel because of custom Babel configuration "${path.relative( dir, babelConfigFile From ad981783abbb347b6510c4dee5ce969e87ab0e24 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Sun, 7 Nov 2021 09:36:39 -0800 Subject: [PATCH 02/20] Add render prop support to `
` (#30156) Adds support for render props to the `
` component, when using the [functional custom `Document`](https://github.com/vercel/next.js/pull/28515) style. This allows you to write something like this: ```tsx export default function Document() { const jsxStyleRegistry = createStyleRegistry() return (
{content => ( {content} )}
) } ``` In functional document components, this allows the `` to be wrapped, similar to `enhanceApp` (which is only available via `getInitialProps`, which is not supported by functional document components). The primary use for this is for integrating with 3rd party CSS-in-JS libraries, allowing them to attach an `useFlush` handler to [support React 18](https://github.com/reactwg/react-18/discussions/110): ```tsx import { unstable_useFlush as useFlush } from 'next/document' export default function StyledJsxWrapper({ children, registry }) { useFlush(() => { /* ... */ }) return ( {children} ) } ``` Support for `useFlush` will be added in a follow up PR. --- packages/next/pages/_document.tsx | 20 ++--- packages/next/server/render.tsx | 80 +++++++++++++------ packages/next/shared/lib/constants.ts | 1 - packages/next/shared/lib/utils.ts | 1 + .../lib/context.js | 3 + .../pages/_document.js | 20 +++++ .../pages/index.js | 7 ++ .../tests/index.test.js | 24 ++++++ 8 files changed, 122 insertions(+), 34 deletions(-) create mode 100644 test/integration/document-functional-render-prop/lib/context.js create mode 100644 test/integration/document-functional-render-prop/pages/_document.js create mode 100644 test/integration/document-functional-render-prop/pages/index.js create mode 100644 test/integration/document-functional-render-prop/tests/index.test.js diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index a1c855bef1d70f5..a69b919b16709b4 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -1,8 +1,5 @@ import React, { Component, ReactElement, ReactNode, useContext } from 'react' -import { - BODY_RENDER_TARGET, - OPTIMIZED_FONT_PROVIDERS, -} from '../shared/lib/constants' +import { OPTIMIZED_FONT_PROVIDERS } from '../shared/lib/constants' import { DocumentContext, DocumentInitialProps, @@ -763,13 +760,18 @@ export class Head extends Component< } } -export function Main() { - const { inAmpMode, docComponentsRendered } = useContext(HtmlContext) - +export function Main({ + children, +}: { + children?: (content: JSX.Element) => JSX.Element +}) { + const { inAmpMode, docComponentsRendered, useMainContent } = + useContext(HtmlContext) + const content = useMainContent(children) docComponentsRendered.Main = true - if (inAmpMode) return <>{BODY_RENDER_TARGET} - return
{BODY_RENDER_TARGET}
+ if (inAmpMode) return content + return
{content}
} export class NextScript extends Component { diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 53fa578287d73ab..fd44dbc070cefce 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -20,7 +20,6 @@ import { GetServerSideProps, GetStaticProps, PreviewData } from '../types' import { isInAmpMode } from '../shared/lib/amp' import { AmpStateContext } from '../shared/lib/amp-context' import { - BODY_RENDER_TARGET, SERVER_PROPS_ID, STATIC_PROPS_ID, STATIC_STATUS_PAGES, @@ -935,6 +934,20 @@ export async function renderToHTML( } } + const appWrappers: Array<(content: JSX.Element) => JSX.Element> = [] + const getWrappedApp = (app: JSX.Element) => { + // Prevent wrappers from reading/writing props by rendering inside an + // opaque component. Wrappers should use context instead. + const InnerApp = () => app + return ( + + {appWrappers.reduce((innerContent, fn) => { + return fn(innerContent) + }, )} + + ) + } + /** * Rules of Static & Dynamic HTML: * @@ -976,13 +989,13 @@ export async function renderToHTML( enhanceComponents(options, App, Component) const html = ReactDOMServer.renderToString( - + getWrappedApp( - + ) ) return { html, head } } @@ -1002,33 +1015,51 @@ export async function renderToHTML( } return { - bodyResult: piperFromArray([docProps.html]), + bodyResult: () => piperFromArray([docProps.html]), documentElement: (htmlProps: HtmlProps) => ( ), + useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => { + if (fn) { + throw new Error( + 'The `children` property is not supported by non-functional custom Document components' + ) + } + // @ts-ignore + return + }, head: docProps.head, headTags: await headTags(documentCtx), styles: docProps.styles, } } else { - const content = - ctx.err && ErrorDebug ? ( - - ) : ( - - - - ) + const bodyResult = async () => { + const content = + ctx.err && ErrorDebug ? ( + + ) : ( + getWrappedApp( + + ) + ) - const bodyResult = concurrentFeatures - ? process.browser - ? await renderToReadableStream(content) - : await renderToNodeStream(content, generateStaticHTML) - : piperFromArray([ReactDOMServer.renderToString(content)]) + return concurrentFeatures + ? process.browser + ? await renderToReadableStream(content) + : await renderToNodeStream(content, generateStaticHTML) + : piperFromArray([ReactDOMServer.renderToString(content)]) + } return { bodyResult, documentElement: () => (Document as any)(), + useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => { + if (fn) { + appWrappers.push(fn) + } + // @ts-ignore + return + }, head, headTags: [], styles: jsxStyleRegistry.styles(), @@ -1056,8 +1087,8 @@ export async function renderToHTML( } const hybridAmp = ampState.hybrid - const docComponentsRendered: DocumentProps['docComponentsRendered'] = {} + const { assetPrefix, buildId, @@ -1123,6 +1154,7 @@ export async function renderToHTML( head: documentResult.head, headTags: documentResult.headTags, styles: documentResult.styles, + useMainContent: documentResult.useMainContent, useMaybeDeferContent, } @@ -1181,20 +1213,20 @@ export async function renderToHTML( } } - const renderTargetIdx = documentHTML.indexOf(BODY_RENDER_TARGET) + const [renderTargetPrefix, renderTargetSuffix] = documentHTML.split( + /<\/next-js-internal-body-render-target>/ + ) const prefix: Array = [] prefix.push('') - prefix.push(documentHTML.substring(0, renderTargetIdx)) + prefix.push(renderTargetPrefix) if (inAmpMode) { prefix.push('') } let pipers: Array = [ piperFromArray(prefix), - documentResult.bodyResult, - piperFromArray([ - documentHTML.substring(renderTargetIdx + BODY_RENDER_TARGET.length), - ]), + await documentResult.bodyResult(), + piperFromArray([renderTargetSuffix]), ] const postProcessors: Array<((html: string) => Promise) | null> = ( diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index e181c24d03ecfd7..ba3a5f7e93c9ec9 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -23,7 +23,6 @@ export const BLOCKED_PAGES = ['/_document', '/_app', '/_error'] export const CLIENT_PUBLIC_FILES_PATH = 'public' export const CLIENT_STATIC_FILES_PATH = 'static' export const CLIENT_STATIC_FILES_RUNTIME = 'runtime' -export const BODY_RENDER_TARGET = '__NEXT_BODY_RENDER_TARGET__' export const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__' // server/middleware-flight-manifest.js diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 358220381649995..a74499fdba9b87c 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -221,6 +221,7 @@ export type HtmlProps = { styles?: React.ReactElement[] | React.ReactFragment head?: Array useMaybeDeferContent: MaybeDeferContentHook + useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => JSX.Element } /** diff --git a/test/integration/document-functional-render-prop/lib/context.js b/test/integration/document-functional-render-prop/lib/context.js new file mode 100644 index 000000000000000..229e13653eb1c0d --- /dev/null +++ b/test/integration/document-functional-render-prop/lib/context.js @@ -0,0 +1,3 @@ +import { createContext } from 'react' + +export default createContext(null) diff --git a/test/integration/document-functional-render-prop/pages/_document.js b/test/integration/document-functional-render-prop/pages/_document.js new file mode 100644 index 000000000000000..6f7199c4e6d00f4 --- /dev/null +++ b/test/integration/document-functional-render-prop/pages/_document.js @@ -0,0 +1,20 @@ +import { Html, Head, Main, NextScript } from 'next/document' +import Context from '../lib/context' + +export default function Document() { + return ( + + + +
+ {(children) => ( + + {children} + + )} +
+ + + + ) +} diff --git a/test/integration/document-functional-render-prop/pages/index.js b/test/integration/document-functional-render-prop/pages/index.js new file mode 100644 index 000000000000000..d00fe7fc70a6b5d --- /dev/null +++ b/test/integration/document-functional-render-prop/pages/index.js @@ -0,0 +1,7 @@ +import { useContext } from 'react' +import Context from '../lib/context' + +export default function MainRenderProp() { + const value = useContext(Context) + return {value} +} diff --git a/test/integration/document-functional-render-prop/tests/index.test.js b/test/integration/document-functional-render-prop/tests/index.test.js new file mode 100644 index 000000000000000..d7cb027d41f7edb --- /dev/null +++ b/test/integration/document-functional-render-prop/tests/index.test.js @@ -0,0 +1,24 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { findPort, launchApp, killApp, renderViaHTTP } from 'next-test-utils' + +const appDir = join(__dirname, '..') +let appPort +let app + +describe('Functional Custom Document', () => { + describe('development mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + + afterAll(() => killApp(app)) + + it('supports render props', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html).toMatch(/from render prop<\/span>/) + }) + }) +}) From cfb8cc841e6c7b80e809565cddfc4219d695c282 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sun, 7 Nov 2021 23:14:50 +0100 Subject: [PATCH 03/20] Change .web extension of document page to part of path (#31116) Fixes: #31104 This effecting users who are using expo with next (`@expo/next-adapter`) since react-native use `.web`, `.ios` and `.android` to identify platform. [Reference](https://docs.expo.dev/guides/using-electron/#%F0%9F%A7%B8-behavior) Change `document.web.js` to `document-web.js` as fallback page in web runtime --- packages/next/build/entries.ts | 2 +- packages/next/build/webpack-config.ts | 2 +- packages/next/pages/{_document.web.tsx => _document-web.tsx} | 2 ++ packages/next/taskfile.js | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) rename packages/next/pages/{_document.web.tsx => _document-web.tsx} (85%) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index be2f0b3e8236e48..e2c478c0e834c48 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -65,7 +65,7 @@ export function createPagesMapping( // we alias these in development and allow webpack to // allow falling back to the correct source file so // that HMR can work properly when a file is added/removed - const documentPage = `_document${hasServerComponents ? '.web' : ''}` + const documentPage = `_document${hasServerComponents ? '-web' : ''}` if (isDev) { pages['/_app'] = `${PAGES_DIR_ALIAS}/_app` pages['/_error'] = `${PAGES_DIR_ALIAS}/_error` diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 730856f88af2440..a59784b37ddc3a0 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -629,7 +629,7 @@ export default async function getBaseWebpackConfig( prev.push(path.join(pagesDir, `_document.${ext}`)) return prev }, [] as string[]), - `next/dist/pages/_document${hasServerComponents ? '.web' : ''}.js`, + `next/dist/pages/_document${hasServerComponents ? '-web' : ''}.js`, ] } diff --git a/packages/next/pages/_document.web.tsx b/packages/next/pages/_document-web.tsx similarity index 85% rename from packages/next/pages/_document.web.tsx rename to packages/next/pages/_document-web.tsx index 92c6462ae923e50..24421541293504c 100644 --- a/packages/next/pages/_document.web.tsx +++ b/packages/next/pages/_document-web.tsx @@ -1,3 +1,5 @@ +// Default _document page for web runtime + import React from 'react' import { Html, Head, Main, NextScript } from './_document' diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index acd995ed7b17cb9..cd5c74b37d42a14 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1123,7 +1123,7 @@ export async function pages_document(task, opts) { export async function pages_document_server(task, opts) { await task - .source('pages/_document.web.tsx') + .source('pages/_document-web.tsx') .swc('client', { dev: opts.dev }) .target('dist/pages') } From 9e03c8d4edd6aa6c38fb18a9220bb1630294a174 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 8 Nov 2021 11:39:39 +0100 Subject: [PATCH 04/20] improve windows support for benchmark (#31032) --- bench/nested-deps/next.config.js | 8 ++++---- bench/nested-deps/package.json | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bench/nested-deps/next.config.js b/bench/nested-deps/next.config.js index d06b18cf4e29c54..004e6c18198b624 100644 --- a/bench/nested-deps/next.config.js +++ b/bench/nested-deps/next.config.js @@ -1,9 +1,9 @@ +const idx = process.execArgv.indexOf('--cpu-prof') +if (idx >= 0) process.execArgv.splice(idx, 1) + module.exports = { eslint: { ignoreDuringBuilds: true, }, - experimental: { - swcLoader: true, - swcMinify: true, - }, + swcMinify: true, } diff --git a/bench/nested-deps/package.json b/bench/nested-deps/package.json index d025ab2b2ad3cae..ad48206bed2d183 100644 --- a/bench/nested-deps/package.json +++ b/bench/nested-deps/package.json @@ -1,11 +1,11 @@ { "scripts": { - "prepare": "rm -rf components && mkdir components && node ./fuzzponent.js -d 2 -s 206 -o components", - "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 ../../node_modules/.bin/next dev", - "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 ../../node_modules/.bin/next build", - "start": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 ../../node_modules/.bin/next start", - "dev-nocache": "rm -rf .next && yarn dev", - "dev-cpuprofile-nocache": "rm -rf .next && cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node --cpu-prof ../../node_modules/.bin/next", - "build-nocache": "rm -rf .next && yarn build" + "prepare": "rimraf components && mkdir components && node ./fuzzponent.js -d 2 -s 206 -o components", + "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node ../../node_modules/next/dist/bin/next dev", + "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node ../../node_modules/next/dist/bin/next build", + "start": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node ../../node_modules/next/dist/bin/next start", + "dev-nocache": "rimraf .next && yarn dev", + "dev-cpuprofile-nocache": "rimraf .next && cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node --cpu-prof ../../node_modules/next/dist/bin/next", + "build-nocache": "rimraf .next && yarn build" } } From feed67ee3640dea68b2ee907cfb64ed987f1f957 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 8 Nov 2021 13:41:49 +0100 Subject: [PATCH 05/20] Enable code splitting for the web runtime build (#31090) When using the web runtime with SSR streaming, this PR significantly improves the build speed for large applications when there're large modules shared by most pages. With another optimization, `react-dom` will now be excluded in the web runtime build if it's imported in the application. It will only take effect in the client bundle. ## 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` --- packages/next/build/webpack-config.ts | 14 ++++-- .../webpack/plugins/middleware-plugin.ts | 43 ++++++++++++------- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index a59784b37ddc3a0..b3288143b20119c 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -990,7 +990,12 @@ export default async function getBaseWebpackConfig( ? // make sure importing "next" is handled gracefully for client // bundles in case a user imported types and it wasn't removed // TODO: should we warn/error for this instead? - ['next', ...(webServerRuntime ? [{ etag: '{}', chalk: '{}' }] : [])] + [ + 'next', + ...(webServerRuntime + ? [{ etag: '{}', chalk: '{}', 'react-dom': '{}' }] + : []), + ] : !isServerless ? [ ({ @@ -1054,10 +1059,10 @@ export default async function getBaseWebpackConfig( } : {}), splitChunks: isServer - ? dev || webServerRuntime + ? dev ? false : ({ - filename: '[name].js', + filename: webServerRuntime ? 'chunks/[name].js' : '[name].js', // allow to split entrypoints chunks: ({ name }: any) => !name?.match(MIDDLEWARE_ROUTE), // size of files is not so relevant for server build @@ -1465,7 +1470,8 @@ export default async function getBaseWebpackConfig( new PagesManifestPlugin({ serverless: isLikeServerless, dev }), // MiddlewarePlugin should be after DefinePlugin so NEXT_PUBLIC_* // replacement is done before its process.env.* handling - !isServer && new MiddlewarePlugin({ dev }), + (!isServer || webServerRuntime) && + new MiddlewarePlugin({ dev, webServerRuntime }), isServer && new NextJsSsrImportPlugin(), !isServer && new BuildManifestPlugin({ diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index a42354f27a04b97..a992c1a5dcceeee 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -4,7 +4,6 @@ import { getSortedRoutes } from '../../../shared/lib/router/utils' import { MIDDLEWARE_MANIFEST, MIDDLEWARE_FLIGHT_MANIFEST, - MIDDLEWARE_SSR_RUNTIME_WEBPACK, MIDDLEWARE_BUILD_MANIFEST, MIDDLEWARE_REACT_LOADABLE_MANIFEST, } from '../../../shared/lib/constants' @@ -31,11 +30,25 @@ export interface MiddlewareManifest { } } +const middlewareManifest: MiddlewareManifest = { + sortedMiddleware: [], + clientInfo: [], + middleware: {}, + version: 1, +} export default class MiddlewarePlugin { dev: boolean - - constructor({ dev }: { dev: boolean }) { + webServerRuntime: boolean + + constructor({ + dev, + webServerRuntime, + }: { + dev: boolean + webServerRuntime: boolean + }) { this.dev = dev + this.webServerRuntime = webServerRuntime } createAssets( @@ -44,17 +57,14 @@ export default class MiddlewarePlugin { envPerRoute: Map ) { const entrypoints = compilation.entrypoints - const middlewareManifest: MiddlewareManifest = { - sortedMiddleware: [], - clientInfo: [], - middleware: {}, - version: 1, - } for (const entrypoint of entrypoints.values()) { if (!entrypoint.name) continue const result = MIDDLEWARE_FULL_ROUTE_REGEX.exec(entrypoint.name) + const ssrEntryInfo = ssrEntries.get(entrypoint.name) + if (ssrEntryInfo && !this.webServerRuntime) continue + if (!ssrEntryInfo && this.webServerRuntime) continue const location = result ? `/${result[1]}` @@ -67,14 +77,15 @@ export default class MiddlewarePlugin { } const files = ssrEntryInfo ? [ - `server/${MIDDLEWARE_SSR_RUNTIME_WEBPACK}.js`, ssrEntryInfo.requireFlightManifest ? `server/${MIDDLEWARE_FLIGHT_MANIFEST}.js` : null, `server/${MIDDLEWARE_BUILD_MANIFEST}.js`, `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js`, - `server/${entrypoint.name}.js`, - ].filter(nonNullable) + ...entrypoint.getFiles().map((file) => 'server/' + file), + ] + .filter(nonNullable) + .filter((file: string) => !file.endsWith('.hot-update.js')) : entrypoint .getFiles() .filter((file: string) => !file.endsWith('.hot-update.js')) @@ -106,9 +117,11 @@ export default class MiddlewarePlugin { } ) - assets[`server/${MIDDLEWARE_MANIFEST}`] = new sources.RawSource( - JSON.stringify(middlewareManifest, null, 2) - ) + assets[ + this.webServerRuntime + ? MIDDLEWARE_MANIFEST + : `server/${MIDDLEWARE_MANIFEST}` + ] = new sources.RawSource(JSON.stringify(middlewareManifest, null, 2)) } apply(compiler: webpack5.Compiler) { From 3ceb9c567339e50fc6326a228841615a9a9edeac Mon Sep 17 00:00:00 2001 From: Kitty Giraudel <1889710+KittyGiraudel@users.noreply.github.com> Date: Mon, 8 Nov 2021 15:58:21 +0100 Subject: [PATCH 06/20] Give priority to document.title over h1 when announcing page change (#31147) ## Improvement This pull-request should address https://github.com/vercel/next.js/issues/24021, improving the page change announcement for assistive technologies by giving priority to `document.title` over `h1`. Interestingly, it would also improve a potential performance bottleneck by skipping calls to `innerText` on the main `h1` raised in [this comment](https://github.com/vercel/next.js/pull/20428#issuecomment-962537038). --- packages/next/client/route-announcer.tsx | 30 +++---- .../client-navigation-a11y/pages/index.js | 10 ++- .../pages/page-with-h1-and-title.js | 11 +++ .../pages/page-with-h1.js | 5 -- .../client-navigation-a11y/test/index.test.js | 84 +++++++++++-------- 5 files changed, 81 insertions(+), 59 deletions(-) create mode 100644 test/integration/client-navigation-a11y/pages/page-with-h1-and-title.js diff --git a/packages/next/client/route-announcer.tsx b/packages/next/client/route-announcer.tsx index a778fe5dac448cf..2c5a772b5fcb81b 100644 --- a/packages/next/client/route-announcer.tsx +++ b/packages/next/client/route-announcer.tsx @@ -5,12 +5,15 @@ export function RouteAnnouncer() { const { asPath } = useRouter() const [routeAnnouncement, setRouteAnnouncement] = React.useState('') - // Only announce the path change, but not for the first load because screen reader will do that automatically. + // Only announce the path change, but not for the first load because screen + // reader will do that automatically. const initialPathLoaded = React.useRef(false) - // Every time the path changes, announce the route change. The announcement will be prioritized by h1, then title - // (from metadata), and finally if those don't exist, then the pathName that is in the URL. This methodology is - // inspired by Marcy Sutton's accessible client routing user testing. More information can be found here: + // Every time the path changes, announce the new page’s title following this + // priority: first the document title (from head), otherwise the first h1, or + // if none of these exist, then the pathname from the URL. This methodology is + // inspired by Marcy Sutton’s accessible client routing user testing. More + // information can be found here: // https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/ React.useEffect( () => { @@ -19,21 +22,14 @@ export function RouteAnnouncer() { return } - let newRouteAnnouncement - const pageHeader = document.querySelector('h1') + if (document.title) { + setRouteAnnouncement(document.title) + } else { + const pageHeader = document.querySelector('h1') + const content = pageHeader?.innerText ?? pageHeader?.textContent - if (pageHeader) { - newRouteAnnouncement = pageHeader.innerText || pageHeader.textContent + setRouteAnnouncement(content || asPath) } - if (!newRouteAnnouncement) { - if (document.title) { - newRouteAnnouncement = document.title - } else { - newRouteAnnouncement = asPath - } - } - - setRouteAnnouncement(newRouteAnnouncement) }, // TODO: switch to pathname + query object of dynamic route requirements [asPath] diff --git a/test/integration/client-navigation-a11y/pages/index.js b/test/integration/client-navigation-a11y/pages/index.js index b616bbe516a5da1..04ed7407e7d2199 100644 --- a/test/integration/client-navigation-a11y/pages/index.js +++ b/test/integration/client-navigation-a11y/pages/index.js @@ -2,14 +2,22 @@ import Link from 'next/link' export default () => (
+ + + Go to a page with a title and an h1 + + + - Go to a page with an h1 + Go to a page without a title but with an h1 + Go to a page without an h1, but with a title + Go to a page without an h1 or a title diff --git a/test/integration/client-navigation-a11y/pages/page-with-h1-and-title.js b/test/integration/client-navigation-a11y/pages/page-with-h1-and-title.js new file mode 100644 index 000000000000000..f746c7b0fe567f2 --- /dev/null +++ b/test/integration/client-navigation-a11y/pages/page-with-h1-and-title.js @@ -0,0 +1,11 @@ +import Head from 'next/head' + +export default () => ( +
+ + Another Page's Title + +

My heading

+
Extraneous stuff
+
+) diff --git a/test/integration/client-navigation-a11y/pages/page-with-h1.js b/test/integration/client-navigation-a11y/pages/page-with-h1.js index b3624846bc32aa9..d531c0df5b1ea38 100644 --- a/test/integration/client-navigation-a11y/pages/page-with-h1.js +++ b/test/integration/client-navigation-a11y/pages/page-with-h1.js @@ -1,10 +1,5 @@ -import Head from 'next/head' - export default () => (
- - Another Page's Title -

My heading

Extraneous stuff
diff --git a/test/integration/client-navigation-a11y/test/index.test.js b/test/integration/client-navigation-a11y/test/index.test.js index 302b1d13abc97b4..99c347726aa4d1d 100644 --- a/test/integration/client-navigation-a11y/test/index.test.js +++ b/test/integration/client-navigation-a11y/test/index.test.js @@ -11,6 +11,20 @@ import { join } from 'path' const context = {} +const navigateTo = async (browser, selector) => + await browser + .waitForElementByCss('#' + selector + '-link') + .click() + .waitForElementByCss('#' + selector) + +const getAnnouncedTitle = async (browser) => + await browser.waitForElementByCss('#__next-route-announcer__').text() + +const getDocumentTitle = async (browser) => await browser.eval('document.title') + +const getMainHeadingTitle = async (browser) => + await browser.elementByCss('h1').text() + describe('Client Navigation accessibility', () => { beforeAll(async () => { context.appPort = await findPort() @@ -19,7 +33,7 @@ describe('Client Navigation accessibility', () => { }) const prerender = [ - '/page-with-h1, /page-with-title, /page-without-h1-or-title', + '/page-with-h1-and-title, /page-with-h1, /page-with-title, /page-without-h1-or-title', ] await Promise.all( @@ -42,57 +56,55 @@ describe('Client Navigation accessibility', () => { expect(roleValue).toBe('alert') await browser.close() }) - describe('There is an h1 tag', () => { - it('has the same innerText value as the h1 tag', async () => { + + describe('There is a title but no h1 tag', () => { + it('has the innerText equal to the value of document.title', async () => { const browser = await webdriver(context.appPort, '/') - const h1Value = await browser - .waitForElementByCss('#page-with-h1-link') - .click() - .waitForElementByCss('#page-with-h1') - .elementByCss('h1') - .text() - - const routeAnnouncerValue = await browser - .waitForElementByCss('#__next-route-announcer__') - .text() - - expect(h1Value).toBe(routeAnnouncerValue) + await navigateTo(browser, 'page-with-title') + + const routeAnnouncerValue = await getAnnouncedTitle(browser) + const title = await getDocumentTitle(browser) + + expect(routeAnnouncerValue).toBe(title) await browser.close() }) }) - describe('There is a document.title, but no h1 tag', () => { - it('has the innerText equal to the value of document.title', async () => { + + describe('There is no title but a h1 tag', () => { + it('has the innerText equal to the value of h1', async () => { const browser = await webdriver(context.appPort, '/') - await browser - .waitForElementByCss('#page-with-title-link') - .click() - .waitForElementByCss('#page-with-title') + await navigateTo(browser, 'page-with-h1') - const title = await browser.eval('document.title') + const routeAnnouncerValue = await getAnnouncedTitle(browser) + const h1Value = await getMainHeadingTitle(browser) - const routeAnnouncerValue = await browser - .waitForElementByCss('#__next-route-announcer__') - .text() + expect(routeAnnouncerValue).toBe(h1Value) + await browser.close() + }) + }) - expect(title).toBe(routeAnnouncerValue) + describe('There is a title and a h1 tag', () => { + it('has the innerText equal to the value of h1', async () => { + const browser = await webdriver(context.appPort, '/') + await navigateTo(browser, 'page-with-h1-and-title') + + const routeAnnouncerValue = await getAnnouncedTitle(browser) + const title = await getDocumentTitle(browser) + + expect(routeAnnouncerValue).toBe(title) await browser.close() }) }) - describe('There is neither an h1 or a title tag', () => { + + describe('There is no title and no h1 tag', () => { it('has the innerText equal to the value of the pathname', async () => { const browser = await webdriver(context.appPort, '/') - await browser - .waitForElementByCss('#page-without-h1-or-title-link') - .click() - .waitForElementByCss('#page-without-h1-or-title') + await navigateTo(browser, 'page-without-h1-or-title') + const routeAnnouncerValue = await getAnnouncedTitle(browser) const pathname = '/page-without-h1-or-title' - const routeAnnouncerValue = await browser - .waitForElementByCss('#__next-route-announcer__') - .text() - - expect(pathname).toBe(routeAnnouncerValue) + expect(routeAnnouncerValue).toBe(pathname) await browser.close() }) }) From 9db80fece5e63215acdb4975de5c2f56e7e7288c Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 8 Nov 2021 16:19:33 +0100 Subject: [PATCH 07/20] Add webpack5 namespace to fix type errors (#31140) --- packages/next/bundles/webpack/packages/webpack.d.ts | 6 ++++++ packages/next/compiled/webpack/webpack.d.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/next/bundles/webpack/packages/webpack.d.ts b/packages/next/bundles/webpack/packages/webpack.d.ts index e2a4f8f9d7963ac..414c145be49046d 100644 --- a/packages/next/bundles/webpack/packages/webpack.d.ts +++ b/packages/next/bundles/webpack/packages/webpack.d.ts @@ -2,3 +2,9 @@ export namespace webpack { export type Compiler = any export type Plugin = any } + +export namespace webpack5 { + export type Compiler = any + export type Plugin = any + export type Configuration = any +} diff --git a/packages/next/compiled/webpack/webpack.d.ts b/packages/next/compiled/webpack/webpack.d.ts index e2a4f8f9d7963ac..414c145be49046d 100644 --- a/packages/next/compiled/webpack/webpack.d.ts +++ b/packages/next/compiled/webpack/webpack.d.ts @@ -2,3 +2,9 @@ export namespace webpack { export type Compiler = any export type Plugin = any } + +export namespace webpack5 { + export type Compiler = any + export type Plugin = any + export type Configuration = any +} From 2a801809bb810d4b1c7944f9f12c2ea7c27f66eb Mon Sep 17 00:00:00 2001 From: "jj@jjsweb.site" Date: Mon, 8 Nov 2021 09:52:15 -0600 Subject: [PATCH 08/20] v12.0.4-canary.0 --- 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/package.json | 12 ++++++------ packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lerna.json b/lerna.json index 5c86aada014bbaa..7e96c2570db7d32 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.0.3" + "version": "12.0.4-canary.0" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 9875456635b489e..3b68f790cb7b9aa 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.0.3", + "version": "12.0.4-canary.0", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 533628057232fbe..e259ff2294b8173 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.0.3", + "version": "12.0.4-canary.0", "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.0.3", + "@next/eslint-plugin-next": "12.0.4-canary.0", "@rushstack/eslint-patch": "^1.0.6", "@typescript-eslint/parser": "^4.20.0", "eslint-import-resolver-node": "^0.3.4", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index f72689e16eb8bc8..558ca20466bd256 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.0.3", + "version": "12.0.4-canary.0", "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 e92c386705e72f4..0ba5af0984c1ef6 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.0.3", + "version": "12.0.4-canary.0", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 162a4df5ee114b1..5570d4722b96c4e 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.0.3", + "version": "12.0.4-canary.0", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index c42c4a1bc461f6a..4983c5b68583c67 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.0.3", + "version": "12.0.4-canary.0", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 0fd0abf0b1d2dcb..2b6a47da1f3e1a3 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.0.3", + "version": "12.0.4-canary.0", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 801d5e45a8081aa..628e400ebcf48b7 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.0.3", + "version": "12.0.4-canary.0", "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 dcf8f87916fbe96..818dacaeba51aa9 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.0.3", + "version": "12.0.4-canary.0", "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 3ae8b07287d414d..d95c427813fb2db 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.0.3", + "version": "12.0.4-canary.0", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next/package.json b/packages/next/package.json index 5035f0604b22621..2d0fe11ff234a87 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.0.3", + "version": "12.0.4-canary.0", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -68,10 +68,10 @@ "@babel/runtime": "7.15.4", "@hapi/accept": "5.0.2", "@napi-rs/triples": "1.0.3", - "@next/env": "12.0.3", - "@next/polyfill-module": "12.0.3", - "@next/react-dev-overlay": "12.0.3", - "@next/react-refresh-utils": "12.0.3", + "@next/env": "12.0.4-canary.0", + "@next/polyfill-module": "12.0.4-canary.0", + "@next/react-dev-overlay": "12.0.4-canary.0", + "@next/react-refresh-utils": "12.0.4-canary.0", "acorn": "8.5.0", "assert": "2.0.0", "browserify-zlib": "0.2.0", @@ -154,7 +154,7 @@ "@babel/traverse": "7.15.0", "@babel/types": "7.15.0", "@napi-rs/cli": "1.2.1", - "@next/polyfill-nomodule": "12.0.3", + "@next/polyfill-nomodule": "12.0.4-canary.0", "@peculiar/webcrypto": "1.1.7", "@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 90143912e97ed08..29c88a2b0094749 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.0.3", + "version": "12.0.4-canary.0", "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 e4e2ef489d9f4c4..4b50fea40adabed 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.0.3", + "version": "12.0.4-canary.0", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", From 83cd45215affb5103dae9009c7c19b1bf97114ea Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 8 Nov 2021 11:34:30 -0500 Subject: [PATCH 09/20] Add warning when image has unused `sizes` prop (#31064) - Related to https://twitter.com/jaffathecake/status/1456579637987979265 - Closes #30640 --- docs/api-reference/next/image.md | 6 ++- packages/next/client/image.tsx | 5 +++ .../default/pages/invalid-sizes.js | 37 +++++++++++++++++++ .../default/test/index.test.js | 21 +++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/integration/image-component/default/pages/invalid-sizes.js diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 958a3e116e320df..957e53c43917d14 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -119,9 +119,11 @@ const MyImage = (props) => { A string that provides information about how wide the image will be at different breakpoints. Defaults to `100vw` (the full width of the screen) when using `layout="responsive"` or `layout="fill"`. -`sizes` is important for performance when using `layout="responsive"` or `layout="fill"` with images that take up less than the full viewport width. +If you are using `layout="fill"` or `layout="responsive"`, it's important to assign `sizes` for any image that takes up less than the full viewport width. -If you are using `layout="fill"` or `layout="responsive"` and the image will always be less than half the viewport width, include `sizes="50vw"`. Without `sizes`, the image will be sent at twice the necessary resolution, decreasing performance. +For example, when the parent element will constrain the image to always be less than half the viewport width, use `sizes="50vw"`. Without `sizes`, the image will be sent at twice the necessary resolution, decreasing performance. + +If you are using `layout="intrinsic"` or `layout="fixed"`, then `sizes` is not needed because the upperbound width is constrained already. [Learn more](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes). diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index be4e5f1d23d647a..d1a0a05ec4ac1e0 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -418,6 +418,11 @@ export default function Image({ `Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.` ) } + if (sizes && layout !== 'fill' && layout !== 'responsive') { + console.warn( + `Image with src "${src}" has "sizes" property but it will be ignored. Only use "sizes" with "layout='fill'" or "layout='responsive'".` + ) + } if (placeholder === 'blur') { if (layout !== 'fill' && (widthInt || 0) * (heightInt || 0) < 1600) { console.warn( diff --git a/test/integration/image-component/default/pages/invalid-sizes.js b/test/integration/image-component/default/pages/invalid-sizes.js new file mode 100644 index 000000000000000..736d349cc839ab2 --- /dev/null +++ b/test/integration/image-component/default/pages/invalid-sizes.js @@ -0,0 +1,37 @@ +import React from 'react' +import Image from 'next/image' + +const Page = () => { + return ( +
+

Warn when "sizes" is not used

+ + + +
+ +
+
footer
+
+ ) +} + +export default Page diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js index 71f0230cf067939..00a0097f6bc4367 100644 --- a/test/integration/image-component/default/test/index.test.js +++ b/test/integration/image-component/default/test/index.test.js @@ -726,6 +726,27 @@ function runTests(mode) { /Image with src (.*)tiff(.*) has a "loader" property that does not implement width/gm ) }) + + it('should warn when using sizes with incorrect layout', async () => { + const browser = await webdriver(appPort, '/invalid-sizes') + await browser.eval(`document.querySelector("footer").scrollIntoView()`) + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(await hasRedbox(browser)).toBe(false) + expect(warnings).toMatch( + /Image with src (.*)png(.*) has "sizes" property but it will be ignored/gm + ) + expect(warnings).toMatch( + /Image with src (.*)jpg(.*) has "sizes" property but it will be ignored/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)webp(.*) has "sizes" property but it will be ignored/gm + ) + expect(warnings).not.toMatch( + /Image with src (.*)gif(.*) has "sizes" property but it will be ignored/gm + ) + }) } else { //server-only tests it('should not create an image folder in server/chunks', async () => { From bc88831619f45357d8ee68b6b2b3abf4690d38e8 Mon Sep 17 00:00:00 2001 From: Maia Teegarden Date: Mon, 8 Nov 2021 08:35:04 -0800 Subject: [PATCH 10/20] Add next-swc jest transform (#30993) Co-authored-by: JJ Kasper Co-authored-by: Tim Neutkens --- errors/experimental-jest-transformer.md | 7 + errors/manifest.json | 4 + jest.config.js | 33 +---- package.json | 1 - packages/next/build/swc/jest.js | 89 +++++++++++++ packages/next/build/swc/options.js | 126 ++++++++++++++++++ .../build/webpack/loaders/next-swc-loader.js | 90 +------------ packages/next/jest.js | 1 + yarn.lock | 5 - 9 files changed, 232 insertions(+), 124 deletions(-) create mode 100644 errors/experimental-jest-transformer.md create mode 100644 packages/next/build/swc/jest.js create mode 100644 packages/next/build/swc/options.js create mode 100644 packages/next/jest.js diff --git a/errors/experimental-jest-transformer.md b/errors/experimental-jest-transformer.md new file mode 100644 index 000000000000000..c790e177d4583ea --- /dev/null +++ b/errors/experimental-jest-transformer.md @@ -0,0 +1,7 @@ +# "next/jest" Experimental + +#### Why This Message Occurred + +You are using `next/jest` which is currently an experimental feature of Next.js. In a future version of Next.js `next/jest` will be marked as stable. + +If you have any feedback about the transformer you can share it on this discussion: https://github.com/vercel/next.js/discussions/31152. diff --git a/errors/manifest.json b/errors/manifest.json index 4d19b214c514467..4dc90b1c186db3d 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -506,6 +506,10 @@ { "title": "middleware-new-signature", "path": "/errors/middleware-new-signature.md" + }, + { + "title": "experimental-jest-transformer", + "path": "/errors/experimental-jest-transformer.md" } ] } diff --git a/jest.config.js b/jest.config.js index 96682b8402b6fcf..10152d4f930725f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ +const path = require('path') + module.exports = { testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'], setupFilesAfterEnv: ['/jest-setup-after-env.ts'], @@ -8,36 +10,7 @@ module.exports = { transform: { '.+\\.(t|j)sx?$': [ // this matches our SWC options used in https://github.com/vercel/next.js/blob/canary/packages/next/taskfile-swc.js - '@swc/jest', - { - sourceMaps: 'inline', - module: { - type: 'commonjs', - }, - env: { - targets: { - node: '12.0.0', - }, - }, - jsc: { - loose: true, - - parser: { - syntax: 'typescript', - dynamicImport: true, - tsx: true, - }, - transform: { - react: { - pragma: 'React.createElement', - pragmaFrag: 'React.Fragment', - throwIfNamespace: true, - development: false, - useBuiltins: true, - }, - }, - }, - }, + path.join(__dirname, './packages/next/jest.js'), ], }, } diff --git a/package.json b/package.json index 7e5cebe0b4117d5..2f8f5ccfaadae1a 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "@svgr/webpack": "5.5.0", "@swc/cli": "0.1.49", "@swc/core": "1.2.97", - "@swc/jest": "0.2.3", "@testing-library/react": "11.2.5", "@types/cheerio": "0.22.16", "@types/fs-extra": "8.1.0", diff --git a/packages/next/build/swc/jest.js b/packages/next/build/swc/jest.js new file mode 100644 index 000000000000000..63c6b6fe0454522 --- /dev/null +++ b/packages/next/build/swc/jest.js @@ -0,0 +1,89 @@ +/* +Copyright (c) 2021 The swc Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +import vm from 'vm' +import { transformSync } from './index' +import { getJestSWCOptions } from './options' + +console.warn( + '"next/jest" is currently experimental. https://nextjs.org/docs/messages/experimental-jest-transformer' +) + +/** + * Loads closest package.json in the directory hierarchy + */ +function loadClosestPackageJson(attempts = 1) { + if (attempts > 5) { + throw new Error("Can't resolve main package.json file") + } + var mainPath = attempts === 1 ? './' : Array(attempts).join('../') + try { + return require(mainPath + 'package.json') + } catch (e) { + return loadClosestPackageJson(attempts + 1) + } +} + +const packageConfig = loadClosestPackageJson() +const isEsmProject = packageConfig.type === 'module' + +// Jest use the `vm` [Module API](https://nodejs.org/api/vm.html#vm_class_vm_module) for ESM. +// see https://github.com/facebook/jest/issues/9430 +const isSupportEsm = 'Module' in vm + +module.exports = { + process(src, filename, jestOptions) { + if (!/\.[jt]sx?$/.test(filename)) { + return src + } + + let swcTransformOpts = getJestSWCOptions({ + filename, + esm: isSupportEsm && isEsm(filename, jestOptions), + }) + + return transformSync(src, { ...swcTransformOpts, filename }) + }, +} + +function getJestConfig(jestConfig) { + return 'config' in jestConfig + ? // jest 27 + jestConfig.config + : // jest 26 + jestConfig +} + +function isEsm(filename, jestOptions) { + return ( + (/\.jsx?$/.test(filename) && isEsmProject) || + getJestConfig(jestOptions).extensionsToTreatAsEsm?.find((ext) => + filename.endsWith(ext) + ) + ) +} diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js new file mode 100644 index 000000000000000..ccc0e5aa00a1acb --- /dev/null +++ b/packages/next/build/swc/options.js @@ -0,0 +1,126 @@ +const nextDistPath = + /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ + +function getBaseSWCOptions({ + filename, + development, + hasReactRefresh, + globalWindow, +}) { + const isTSFile = filename.endsWith('.ts') + const isTypeScript = isTSFile || filename.endsWith('.tsx') + + return { + jsc: { + parser: { + syntax: isTypeScript ? 'typescript' : 'ecmascript', + dynamicImport: true, + // Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags. + [isTypeScript ? 'tsx' : 'jsx']: isTSFile ? false : true, + }, + + transform: { + react: { + runtime: 'automatic', + pragma: 'React.createElement', + pragmaFrag: 'React.Fragment', + throwIfNamespace: true, + development: development, + useBuiltins: true, + refresh: hasReactRefresh, + }, + optimizer: { + simplify: false, + globals: { + typeofs: { + window: globalWindow ? 'object' : 'undefined', + }, + }, + }, + regenerator: { + importPath: require.resolve('regenerator-runtime'), + }, + }, + }, + } +} + +export function getJestSWCOptions({ filename, esm }) { + let baseOptions = getBaseSWCOptions({ + filename, + development: false, + hasReactRefresh: false, + globalWindow: false, + }) + + const isNextDist = nextDistPath.test(filename) + + return { + ...baseOptions, + env: { + targets: { + // Targets the current version of Node.js + node: process.versions.node, + }, + }, + module: { + type: esm && !isNextDist ? 'es6' : 'commonjs', + }, + disableNextSsg: true, + disablePageConfig: true, + } +} + +export function getLoaderSWCOptions({ + filename, + development, + isServer, + pagesDir, + isPageFile, + hasReactRefresh, +}) { + let baseOptions = getBaseSWCOptions({ + filename, + development, + globalWindow: !isServer, + hasReactRefresh, + }) + + const isNextDist = nextDistPath.test(filename) + + if (isServer) { + return { + ...baseOptions, + // Disables getStaticProps/getServerSideProps tree shaking on the server compilation for pages + disableNextSsg: true, + disablePageConfig: true, + isDevelopment: development, + pagesDir, + isPageFile, + env: { + targets: { + // Targets the current version of Node.js + node: process.versions.node, + }, + }, + } + } else { + // Matches default @babel/preset-env behavior + baseOptions.jsc.target = 'es5' + return { + ...baseOptions, + // Ensure Next.js internals are output as commonjs modules + ...(isNextDist + ? { + module: { + type: 'commonjs', + }, + } + : {}), + disableNextSsg: !isPageFile, + isDevelopment: development, + pagesDir, + isPageFile, + } + } +} diff --git a/packages/next/build/webpack/loaders/next-swc-loader.js b/packages/next/build/webpack/loaders/next-swc-loader.js index a5e5c51fcd42675..4215bb257921edd 100644 --- a/packages/next/build/webpack/loaders/next-swc-loader.js +++ b/packages/next/build/webpack/loaders/next-swc-loader.js @@ -27,90 +27,7 @@ DEALINGS IN THE SOFTWARE. */ import { transform } from '../../swc' - -const nextDistPath = - /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ - -function getSWCOptions({ - filename, - isServer, - development, - isPageFile, - pagesDir, - isNextDist, - hasReactRefresh, -}) { - const isTSFile = filename.endsWith('.ts') - const isTypeScript = isTSFile || filename.endsWith('.tsx') - - const jsc = { - parser: { - syntax: isTypeScript ? 'typescript' : 'ecmascript', - dynamicImport: true, - // Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags. - [isTypeScript ? 'tsx' : 'jsx']: isTSFile ? false : true, - }, - - transform: { - react: { - runtime: 'automatic', - pragma: 'React.createElement', - pragmaFrag: 'React.Fragment', - throwIfNamespace: true, - development: development, - useBuiltins: true, - refresh: hasReactRefresh, - }, - optimizer: { - simplify: false, - globals: { - typeofs: { - window: isServer ? 'undefined' : 'object', - }, - }, - }, - regenerator: { - importPath: require.resolve('regenerator-runtime'), - }, - }, - } - - if (isServer) { - return { - jsc, - // Disables getStaticProps/getServerSideProps tree shaking on the server compilation for pages - disableNextSsg: true, - disablePageConfig: true, - isDevelopment: development, - pagesDir, - isPageFile, - env: { - targets: { - // Targets the current version of Node.js - node: process.versions.node, - }, - }, - } - } else { - // Matches default @babel/preset-env behavior - jsc.target = 'es5' - return { - // Ensure Next.js internals are output as commonjs modules - ...(isNextDist - ? { - module: { - type: 'commonjs', - }, - } - : {}), - disableNextSsg: !isPageFile, - isDevelopment: development, - pagesDir, - isPageFile, - jsc, - } - } -} +import { getLoaderSWCOptions } from '../../swc/options' async function loaderTransform(parentTrace, source, inputSourceMap) { // Make the loader async @@ -121,15 +38,12 @@ async function loaderTransform(parentTrace, source, inputSourceMap) { const { isServer, pagesDir, hasReactRefresh } = loaderOptions const isPageFile = filename.startsWith(pagesDir) - const isNextDist = nextDistPath.test(filename) - - const swcOptions = getSWCOptions({ + const swcOptions = getLoaderSWCOptions({ pagesDir, filename, isServer: isServer, isPageFile, development: this.mode === 'development', - isNextDist, hasReactRefresh, }) diff --git a/packages/next/jest.js b/packages/next/jest.js new file mode 100644 index 000000000000000..338e34d677b321f --- /dev/null +++ b/packages/next/jest.js @@ -0,0 +1 @@ +module.exports = require('./dist/build/swc/jest') diff --git a/yarn.lock b/yarn.lock index 9235b48274f7bcc..ae311890732e10f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4111,11 +4111,6 @@ "@swc/core-win32-ia32-msvc" "1.2.97" "@swc/core-win32-x64-msvc" "1.2.97" -"@swc/jest@0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.3.tgz#5c32aaa6298267a955d35eb67094edabd5db598f" - integrity sha512-ARZIY5OkXdFRQLHc/1i+yKrl0H3B1sa7Bu9XE8yTvYZZ4G5Ewu6oyyJBM52TiROP6EpMcF7ZeQQsKMZvzuKkNw== - "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" From ca41952d1433dbc9cbc5f5ded9f189ea1d3b1c06 Mon Sep 17 00:00:00 2001 From: Houssein Djirdeh Date: Mon, 8 Nov 2021 11:49:38 -0500 Subject: [PATCH 11/20] Expands `next/script` documentation (#31063) This PR does the following to partially address #31062: - Expands the [Script Component](https://nextjs.org/docs/basic-features/script) page in the core documentation - Adds a `next/script` API reference page --- docs/api-reference/next/script.md | 70 +++++++++++ docs/basic-features/script.md | 201 +++++++++++++++++------------- docs/manifest.json | 4 + 3 files changed, 188 insertions(+), 87 deletions(-) create mode 100644 docs/api-reference/next/script.md diff --git a/docs/api-reference/next/script.md b/docs/api-reference/next/script.md new file mode 100644 index 000000000000000..49fe558c6b16392 --- /dev/null +++ b/docs/api-reference/next/script.md @@ -0,0 +1,70 @@ +--- +description: Optimize loading of third-party scripts with the built-in Script component. +--- + +# next/script + +
+ Examples + +
+ +
+ Version History + +| Version | Changes | +| --------- | ------------------------- | +| `v11.0.0` | `next/script` introduced. | + +
+ +> **Note: This is API documentation for the Script Component. For a feature overview and usage information for scripts in Next.js, please see [Script Optimization](/docs/basic-features/script.md).** + +## Optional Props + +### src + +A path string specifying the URL of an external script. This can be either an absolute external URL or an internal path. + +### strategy + +The loading strategy of the script. + +| `strategy` | **Description** | +| ------------------- | ---------------------------------------------------------- | +| `beforeInteractive` | Load script before the page becomes interactive | +| `afterInteractive` | Load script immediately after the page becomes interactive | +| `lazyOnload` | Load script during browser idle time | + +### onLoad + +A method that returns additional JavaScript that should be executed after the script has finished loading. + +The following is an example of how to use the `onLoad` property: + +```jsx +import { useState } from 'react' +import Script from 'next/script' + +export default function Home() { + const [stripe, setStripe] = useState(null) + + return ( + <> + ``` +Or by using the `dangerouslySetInnerHTML` property: + +```jsx + - -// or - -