From 8ded79728e228b39e30cab12cb1f46be8fbaf9bb Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 9 Dec 2022 23:29:57 +0100 Subject: [PATCH 01/16] Support for named slots in type checking (#43906) Follow up to #43903, this PR adds named slots to the generated typings, and provides better error messages for `next build`. For example, a layout can have `test` as a prop because it has `@test` co-located. But `invalid` is not a possible prop here: ![CleanShot 2022-12-09 at 21 24 21@2x](https://user-images.githubusercontent.com/3676859/206790150-0e2d7905-fad8-4b26-86ee-d5e69a5ad0f9.png) And here's the error when running `next build`: CleanShot 2022-12-09 at 21 21 11@2x ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../webpack/plugins/flight-types-plugin.ts | 32 ++++++++++++++++--- .../lib/typescript/diagnosticFormatter.ts | 16 ++++++++-- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 9d92207b222ae5b..0213ea62b1fd350 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -1,4 +1,5 @@ import path from 'path' +import { promises as fs } from 'fs' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { WEBPACK_LAYERS } from '../../../lib/constants' @@ -17,6 +18,7 @@ function createTypeGuardFile( relativePath: string, options: { type: 'layout' | 'page' + slots?: string[] } ) { return `// File: ${fullPath} @@ -32,6 +34,11 @@ interface PageProps { } interface LayoutProps { children: React.ReactNode +${ + options.slots + ? options.slots.map((slot) => ` ${slot}: React.ReactNode`).join('\n') + : '' +} params: any } @@ -68,6 +75,18 @@ type NonNegative = T extends Zero ? T : Negative extends n ` } +async function collectNamedSlots(layoutPath: string) { + const layoutDir = path.dirname(layoutPath) + const items = await fs.readdir(layoutDir, { withFileTypes: true }) + const slots = [] + for (const item of items) { + if (item.isDirectory() && item.name.startsWith('@')) { + slots.push(item.name.slice(1)) + } + } + return slots +} + export class FlightTypesPlugin { dir: string appDir: string @@ -84,7 +103,7 @@ export class FlightTypesPlugin { apply(compiler: webpack.Compiler) { const assetPrefix = this.dev ? '..' : this.isEdgeServer ? '..' : '../..' - const handleModule = (_mod: webpack.Module, assets: any) => { + const handleModule = async (_mod: webpack.Module, assets: any) => { if (_mod.layer !== WEBPACK_LAYERS.server) return const mod: webpack.NormalModule = _mod as any @@ -111,9 +130,11 @@ export class FlightTypesPlugin { const assetPath = assetPrefix + '/' + typePath.replace(/\\/g, '/') if (IS_LAYOUT) { + const slots = await collectNamedSlots(mod.resource) assets[assetPath] = new sources.RawSource( createTypeGuardFile(mod.resource, relativeImportPath, { type: 'layout', + slots, }) ) as unknown as webpack.sources.RawSource } else if (IS_PAGE) { @@ -126,19 +147,22 @@ export class FlightTypesPlugin { } compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - compilation.hooks.processAssets.tap( + compilation.hooks.processAssets.tapAsync( { name: PLUGIN_NAME, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, }, - (assets) => { + async (assets, callback) => { + const promises: Promise[] = [] for (const entrypoint of compilation.entrypoints.values()) { for (const chunk of entrypoint.chunks) { compilation.chunkGraph.getChunkModules(chunk).forEach((mod) => { - handleModule(mod, assets) + promises.push(handleModule(mod, assets)) }) } } + await Promise.all(promises) + callback() } ) }) diff --git a/packages/next/lib/typescript/diagnosticFormatter.ts b/packages/next/lib/typescript/diagnosticFormatter.ts index 26aeaf7bd395986..d5f8ef5c9e976dd 100644 --- a/packages/next/lib/typescript/diagnosticFormatter.ts +++ b/packages/next/lib/typescript/diagnosticFormatter.ts @@ -103,14 +103,24 @@ function getFormattedLayoutAndPageDiagnosticMessageText( } break case 2741: - const incompatProp = item.messageText.match( + const incompatPageProp = item.messageText.match( /Property '(.+)' is missing in type 'PageProps'/ ) - if (incompatProp) { + if (incompatPageProp) { main += '\n' + ' '.repeat(indent * 2) main += `Prop "${chalk.bold( - incompatProp[1] + incompatPageProp[1] )}" will never be passed. Remove it from the component's props.` + } else { + const extraLayoutProp = item.messageText.match( + /Property '(.+)' is missing in type 'LayoutProps' but required in type '(.+)'/ + ) + if (extraLayoutProp) { + main += '\n' + ' '.repeat(indent * 2) + main += `Prop "${chalk.bold( + extraLayoutProp[1] + )}" is not valid for this Layout, remove it to fix.` + } } break default: From 6c1dd229d74e266d3db7807b326541cd3b94e91d Mon Sep 17 00:00:00 2001 From: AZM Date: Sun, 11 Dec 2022 00:38:43 +0900 Subject: [PATCH 02/16] Update compiler.md (#43872) The sentence is very confusing. Excessive/invalid usage of English grammar ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [x] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [x] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [x] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: Steven <229881+styfle@users.noreply.github.com> --- docs/advanced-features/compiler.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-features/compiler.md b/docs/advanced-features/compiler.md index 9a7cbd75a5690e1..ce0a8ebc6638653 100644 --- a/docs/advanced-features/compiler.md +++ b/docs/advanced-features/compiler.md @@ -77,7 +77,7 @@ module.exports = { ### Jest -Jest support not only includes the transformation previously provided by Babel, but also simplifies configuring Jest together with Next.js including: +The Next.js Compiler transpiles your tests and simplifies configuring Jest together with Next.js including: - Auto mocking of `.css`, `.module.css` (and their `.scss` variants), and image imports - Automatically sets up `transform` using SWC From 3833aed08dd890231f78970d001416f5c1da17f2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 10 Dec 2022 18:35:13 +0100 Subject: [PATCH 03/16] Fix next/dynamic types for resolving named export module (#43923) ## Bug Fixes: #43915 - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) --- .gitignore | 3 ++- packages/next/shared/lib/dynamic.tsx | 24 ++++++++++++------- .../typescript-basic/app/components/named.tsx | 3 +++ .../typescript-basic/app/pages/dynamic.tsx | 10 ++++++++ 4 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 test/production/typescript-basic/app/components/named.tsx create mode 100644 test/production/typescript-basic/app/pages/dynamic.tsx diff --git a/.gitignore b/.gitignore index 9a0d37330ba3792..0474f7c1883a746 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ coverage # test output test/**/out* test/**/next-env.d.ts +test/**/tsconfig.json .DS_Store /e2e-tests test/tmp/** @@ -46,4 +47,4 @@ test-timings.json # Cache *.tsbuildinfo -.swc/ \ No newline at end of file +.swc/ diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx index d3b639b68ef34d8..042438119d39a5b 100644 --- a/packages/next/shared/lib/dynamic.tsx +++ b/packages/next/shared/lib/dynamic.tsx @@ -2,11 +2,19 @@ import React, { lazy, Suspense } from 'react' import Loadable from './loadable' import NoSSR from './dynamic-no-ssr' -type ComponentModule

= { default: React.ComponentType

} +type ComponentModule

= { default: React.ComponentType

} -export type LoaderComponent

= Promise> +export declare type LoaderComponent

= Promise< + React.ComponentType

| ComponentModule

+> -export type Loader

= () => LoaderComponent

+type NormalizedLoader

= () => Promise<{ + default: React.ComponentType

+}> + +export declare type Loader

= + | (() => LoaderComponent

) + | LoaderComponent

export type LoaderMap = { [module: string]: () => Loader } @@ -26,8 +34,8 @@ export type DynamicOptionsLoadingProps = { // Normalize loader to return the module as form { default: Component } for `React.lazy`. // Also for backward compatible since next/dynamic allows to resolve a component directly with loader // Client component reference proxy need to be converted to a module. -function convertModule(mod: ComponentModule) { - return { default: mod.default || mod } +function convertModule

(mod: React.ComponentType

| ComponentModule

) { + return { default: (mod as ComponentModule

).default || mod } } export type DynamicOptions

= LoadableGeneratedOptions & { @@ -50,7 +58,7 @@ export type LoadableFn

= ( export type LoadableComponent

= React.ComponentType

export function noSSR

( - LoadableInitializer: Loader, + LoadableInitializer: NormalizedLoader

, loadableOptions: DynamicOptions

): React.ComponentType

{ // Removing webpack and modules means react-loadable won't try preloading @@ -118,7 +126,7 @@ export default function dynamic

( // Support for passing options, eg: dynamic(import('../hello-world'), {loading: () =>

Loading something

}) loadableOptions = { ...loadableOptions, ...options } - const loaderFn = loadableOptions.loader as Loader

+ const loaderFn = loadableOptions.loader as () => LoaderComponent

const loader = () => loaderFn().then(convertModule) // coming from build/babel/plugins/react-loadable-plugin.js @@ -135,7 +143,7 @@ export default function dynamic

( if (typeof loadableOptions.ssr === 'boolean') { if (!loadableOptions.ssr) { delete loadableOptions.ssr - return noSSR(loader as Loader, loadableOptions) + return noSSR(loader, loadableOptions) } delete loadableOptions.ssr } diff --git a/test/production/typescript-basic/app/components/named.tsx b/test/production/typescript-basic/app/components/named.tsx new file mode 100644 index 000000000000000..4af8c9472dd33ab --- /dev/null +++ b/test/production/typescript-basic/app/components/named.tsx @@ -0,0 +1,3 @@ +export function NamedExport() { + return <>named-export +} diff --git a/test/production/typescript-basic/app/pages/dynamic.tsx b/test/production/typescript-basic/app/pages/dynamic.tsx new file mode 100644 index 000000000000000..b008af163770ad7 --- /dev/null +++ b/test/production/typescript-basic/app/pages/dynamic.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import dynamic from 'next/dynamic' + +const NamedExport = dynamic(() => + import('../components/named').then((mod) => mod.NamedExport) +) + +export default function Dynamic() { + return +} From 1fb4cad2a8329811b5ccde47217b4a6ae739124e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 10 Dec 2022 20:07:56 +0100 Subject: [PATCH 04/16] Add auto completion for prop names and types to the TS plugin (#43909) For example, [named slots](https://nextjs.org/blog/layouts-rfc#convention:~:text=After%20this%20change%2C%20the%20layout%20will%20receive%20a%20prop%20called%C2%A0customProp%C2%A0instead%20of%C2%A0children.) should be hinted when typing: ```ts export default function Layout({ f| ^foo ``` And the prop type: ```ts export default function Layout({ foo }: { f| ^foo: React.ReactChildren ``` And [params](https://beta.nextjs.org/docs/api-reference/file-conventions/page#params-optional): ```ts export default function Page({ p| ``` NEXT-178 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/next/server/next-typescript.ts | 99 ++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/next/server/next-typescript.ts b/packages/next/server/next-typescript.ts index 011a07867730e65..665b7069b4ba47d 100644 --- a/packages/next/server/next-typescript.ts +++ b/packages/next/server/next-typescript.ts @@ -203,6 +203,10 @@ export function createTSPlugin(modules: { '^' + (projectDir + '(/src)?/app').replace(/[\\/]/g, '[\\/]') ) + const isPositionInsideNode = (position: number, node: ts.Node) => { + const start = node.getFullStart() + return start <= position && position <= node.getFullWidth() + start + } const isAppEntryFile = (filePath: string) => { return ( appDir.test(filePath) && @@ -388,6 +392,99 @@ export function createTSPlugin(modules: { ] as ts.CompletionEntry[] }) + const program = info.languageService.getProgram() + const source = program?.getSourceFile(fileName) + if (!source || !program) return prior + + ts.forEachChild(source!, (node) => { + // Auto completion for default export function's props. + if ( + isDefaultFunctionExport(node) && + isPositionInsideNode(position, node) + ) { + const paramNode = (node as ts.FunctionDeclaration).parameters?.[0] + if (isPositionInsideNode(position, paramNode)) { + const props = paramNode?.name + if (props && ts.isObjectBindingPattern(props)) { + let validProps = [] + let validPropsWithType = [] + let type: string + + if (isPageFile(fileName)) { + // For page entries (page.js), it can only have `params` and `searchParams` + // as the prop names. + validProps = ALLOWED_PAGE_PROPS + validPropsWithType = ALLOWED_PAGE_PROPS + type = 'page' + } else { + // For layout entires, check if it has any named slots. + const currentDir = path.dirname(fileName) + const items = fs.readdirSync(currentDir, { + withFileTypes: true, + }) + const slots = [] + for (const item of items) { + if (item.isDirectory() && item.name.startsWith('@')) { + slots.push(item.name.slice(1)) + } + } + validProps = ALLOWED_LAYOUT_PROPS.concat(slots) + validPropsWithType = ALLOWED_LAYOUT_PROPS.concat( + slots.map((s) => `${s}: React.ReactNode`) + ) + type = 'layout' + } + + // Auto completion for props + for (const element of props.elements) { + if (isPositionInsideNode(position, element)) { + const nameNode = element.propertyName || element.name + + if (isPositionInsideNode(position, nameNode)) { + for (const name of validProps) { + prior.entries.push({ + name, + insertText: name, + sortText: '_' + name, + kind: ts.ScriptElementKind.memberVariableElement, + kindModifiers: ts.ScriptElementKindModifier.none, + labelDetails: { + description: `Next.js ${type} prop`, + }, + } as ts.CompletionEntry) + } + } + + break + } + } + + // Auto completion for types + if (paramNode.type && ts.isTypeLiteralNode(paramNode.type)) { + for (const member of paramNode.type.members) { + if (isPositionInsideNode(position, member)) { + for (const name of validPropsWithType) { + prior.entries.push({ + name, + insertText: name, + sortText: '_' + name, + kind: ts.ScriptElementKind.memberVariableElement, + kindModifiers: ts.ScriptElementKindModifier.none, + labelDetails: { + description: `Next.js ${type} prop type`, + }, + } as ts.CompletionEntry) + } + + break + } + } + } + } + } + } + }) + return prior } @@ -732,7 +829,7 @@ export function createTSPlugin(modules: { const props = (node as ts.FunctionDeclaration).parameters?.[0]?.name if (props && ts.isObjectBindingPattern(props)) { for (const prop of (props as ts.ObjectBindingPattern).elements) { - const propName = prop.name.getText() + const propName = (prop.propertyName || prop.name).getText() if (!validProps.includes(propName)) { prior.push({ file: source, From 6cfebfb02c2a52a1f99fca59a2eac2d704d053db Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 11 Dec 2022 01:08:07 +0100 Subject: [PATCH 05/16] Skip creating VSCode config and `.gitignore` if running in CI (#43935) NEXT-233 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- packages/next/lib/verifyTypeScriptSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index d280046e3f1e5dd..2daa8b9ab1767d4 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -125,7 +125,7 @@ export async function verifyTypeScriptSetup({ // Next.js' types: await writeAppTypeDeclarations(dir, !disableStaticImages) - if (isAppDirEnabled) { + if (isAppDirEnabled && !isCI) { await writeVscodeConfigurations(dir, tsPath) } From ce0bcd38f2df83b3403ba561b6ac6e711b7d3a94 Mon Sep 17 00:00:00 2001 From: Max Proske Date: Sun, 11 Dec 2022 17:53:08 -0800 Subject: [PATCH 06/16] Convert `with-gsap`, `with-mqtt-js`, `with-mux-video` examples to Typescript (#43874) Converted three more examples to TypeScript. Changes to individual examples pushed as separate commits. ## Documentation / Examples - [X] Make sure the linting passes by running `pnpm build && pnpm lint` - [X] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../components/{Content.js => Content.tsx} | 9 +++---- .../components/{Home.js => Home.tsx} | 4 +-- .../components/{Title.js => Title.tsx} | 17 ++++++++----- examples/with-gsap/package.json | 14 ++++++++--- examples/with-gsap/pages/_app.js | 7 ------ examples/with-gsap/pages/_app.tsx | 8 ++++++ examples/with-gsap/pages/_document.js | 18 ------------- examples/with-gsap/pages/api/hello.js | 5 ---- .../with-gsap/pages/{index.js => index.tsx} | 8 +++--- examples/with-gsap/tsconfig.json | 20 +++++++++++++++ examples/with-mqtt-js/environment.d.ts | 8 ++++++ .../lib/{useMqtt.js => useMqtt.ts} | 18 +++++++++---- examples/with-mqtt-js/package.json | 8 +++++- .../pages/{index.js => index.tsx} | 13 +++++----- examples/with-mqtt-js/tsconfig.json | 20 +++++++++++++++ .../components/{button.js => button.tsx} | 6 ++++- .../{error-message.js => error-message.tsx} | 6 ++++- .../components/{layout.js => layout.tsx} | 12 ++++++++- .../components/{spinner.js => spinner.tsx} | 7 +++++- .../{upload-form.js => upload-form.tsx} | 24 +++++++++++------- .../{upload-page.js => upload-page.tsx} | 6 ++++- .../{constants.js => constants.ts} | 0 examples/with-mux-video/package.json | 14 ++++++++--- .../pages/api/asset/{[id].js => [id].ts} | 10 +++++--- .../pages/api/{upload.js => upload.ts} | 6 ++++- .../pages/api/upload/{[id].js => [id].ts} | 8 ++++-- .../pages/asset/{[id].js => [id].tsx} | 4 +-- .../pages/{index.js => index.tsx} | 0 .../pages/v/{[id].js => [id].tsx} | 25 ++++++++++++++++--- examples/with-mux-video/tsconfig.json | 20 +++++++++++++++ 30 files changed, 231 insertions(+), 94 deletions(-) rename examples/with-gsap/components/{Content.js => Content.tsx} (83%) rename examples/with-gsap/components/{Home.js => Home.tsx} (90%) rename examples/with-gsap/components/{Title.js => Title.tsx} (64%) delete mode 100644 examples/with-gsap/pages/_app.js create mode 100644 examples/with-gsap/pages/_app.tsx delete mode 100644 examples/with-gsap/pages/_document.js delete mode 100644 examples/with-gsap/pages/api/hello.js rename examples/with-gsap/pages/{index.js => index.tsx} (90%) create mode 100644 examples/with-gsap/tsconfig.json create mode 100644 examples/with-mqtt-js/environment.d.ts rename examples/with-mqtt-js/lib/{useMqtt.js => useMqtt.ts} (71%) rename examples/with-mqtt-js/pages/{index.js => index.tsx} (81%) create mode 100644 examples/with-mqtt-js/tsconfig.json rename examples/with-mux-video/components/{button.js => button.tsx} (78%) rename examples/with-mux-video/components/{error-message.js => error-message.tsx} (71%) rename examples/with-mux-video/components/{layout.js => layout.tsx} (95%) rename examples/with-mux-video/components/{spinner.js => spinner.tsx} (90%) rename examples/with-mux-video/components/{upload-form.js => upload-form.tsx} (84%) rename examples/with-mux-video/components/{upload-page.js => upload-page.tsx} (93%) rename examples/with-mux-video/{constants.js => constants.ts} (100%) rename examples/with-mux-video/pages/api/asset/{[id].js => [id].ts} (66%) rename examples/with-mux-video/pages/api/{upload.js => upload.ts} (80%) rename examples/with-mux-video/pages/api/upload/{[id].js => [id].ts} (71%) rename examples/with-mux-video/pages/asset/{[id].js => [id].tsx} (95%) rename examples/with-mux-video/pages/{index.js => index.tsx} (100%) rename examples/with-mux-video/pages/v/{[id].js => [id].tsx} (86%) create mode 100644 examples/with-mux-video/tsconfig.json diff --git a/examples/with-gsap/components/Content.js b/examples/with-gsap/components/Content.tsx similarity index 83% rename from examples/with-gsap/components/Content.js rename to examples/with-gsap/components/Content.tsx index 7f48fb904c39bf1..7f67d3e7a275556 100644 --- a/examples/with-gsap/components/Content.js +++ b/examples/with-gsap/components/Content.tsx @@ -1,10 +1,11 @@ import { useEffect, useRef } from 'react' import { gsap } from 'gsap' -const Content = () => { +export default function Content() { let line1 = useRef(null) + useEffect(() => { - gsap.from([line1], 0.6, { + gsap.from([line1.current], 0.6, { delay: 0.9, ease: 'power3.out', y: 24, @@ -15,7 +16,7 @@ const Content = () => { }, [line1]) return ( -

(line1 = el)} className="line"> +

A Simple example using{' '} {

) } - -export default Content diff --git a/examples/with-gsap/components/Home.js b/examples/with-gsap/components/Home.tsx similarity index 90% rename from examples/with-gsap/components/Home.js rename to examples/with-gsap/components/Home.tsx index 25d491531bbf279..065619f779c4421 100644 --- a/examples/with-gsap/components/Home.js +++ b/examples/with-gsap/components/Home.tsx @@ -1,7 +1,7 @@ import Title from './Title' import Content from './Content' -const Home = () => { +export default function Home() { return (
@@ -15,5 +15,3 @@ const Home = () => { </div> ) } - -export default Home diff --git a/examples/with-gsap/components/Title.js b/examples/with-gsap/components/Title.tsx similarity index 64% rename from examples/with-gsap/components/Title.js rename to examples/with-gsap/components/Title.tsx index 2761edbee1ca6d6..7f250ea59be6786 100644 --- a/examples/with-gsap/components/Title.js +++ b/examples/with-gsap/components/Title.tsx @@ -1,11 +1,17 @@ import { useEffect, useRef } from 'react' import { gsap } from 'gsap' -const Title = ({ lineContent, lineContent2 }) => { +type TitleProps = { + lineContent: string + lineContent2: string +} + +export default function Title({ lineContent, lineContent2 }: TitleProps) { let line1 = useRef(null) let line2 = useRef(null) + useEffect(() => { - gsap.from([line1, line2], 0.8, { + gsap.from([line1.current, line2.current], 0.8, { delay: 0.8, ease: 'power3.out', y: 64, @@ -14,20 +20,19 @@ const Title = ({ lineContent, lineContent2 }) => { }, }) }, [line1, line2]) + return ( <h1 className="page-title"> <div className="line-wrap"> - <div ref={(el) => (line1 = el)} className="line"> + <div ref={line1} className="line"> {lineContent} </div> </div> <div className="line-wrap"> - <div ref={(el) => (line2 = el)} className="line"> + <div ref={line2} className="line"> {lineContent2} </div> </div> </h1> ) } - -export default Title diff --git a/examples/with-gsap/package.json b/examples/with-gsap/package.json index 37ed68998474a7b..888b6ea86dbc763 100644 --- a/examples/with-gsap/package.json +++ b/examples/with-gsap/package.json @@ -6,11 +6,19 @@ "start": "next start" }, "dependencies": { - "gsap": "^3.0.1", + "gsap": "^3.11.3", "next": "latest", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-transition-group": "^4.3.0", - "sass": "1.29.0" + "react-transition-group": "^4.4.5", + "sass": "^1.56.2" + }, + "devDependencies": { + "@types/gsap": "^3.0.0", + "@types/node": "^18.11.12", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.9", + "@types/react-transition-group": "^4.4.5", + "typescript": "^4.9.4" } } diff --git a/examples/with-gsap/pages/_app.js b/examples/with-gsap/pages/_app.js deleted file mode 100644 index 7b11d92cf4b6b60..000000000000000 --- a/examples/with-gsap/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import '../App.scss' - -function MyApp({ Component, pageProps }) { - return <Component {...pageProps} /> -} - -export default MyApp diff --git a/examples/with-gsap/pages/_app.tsx b/examples/with-gsap/pages/_app.tsx new file mode 100644 index 000000000000000..e12b69b03d7995f --- /dev/null +++ b/examples/with-gsap/pages/_app.tsx @@ -0,0 +1,8 @@ +import type { AppProps } from 'next/app' +import '../App.scss' + +function MyApp({ Component, pageProps }: AppProps) { + return <Component {...pageProps} /> +} + +export default MyApp diff --git a/examples/with-gsap/pages/_document.js b/examples/with-gsap/pages/_document.js deleted file mode 100644 index c0219bc9e93dae6..000000000000000 --- a/examples/with-gsap/pages/_document.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Html, Head, Main, NextScript } from 'next/document' - -export default function Document() { - return ( - <Html> - <Head> - <link - href="https://fonts.googleapis.com/css?family=Bebas+Neue|Poppins:300,400&display=swap" - rel="stylesheet" - /> - </Head> - <body> - <Main /> - <NextScript /> - </body> - </Html> - ) -} diff --git a/examples/with-gsap/pages/api/hello.js b/examples/with-gsap/pages/api/hello.js deleted file mode 100644 index df63de88fa67cb0..000000000000000 --- a/examples/with-gsap/pages/api/hello.js +++ /dev/null @@ -1,5 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction - -export default function handler(req, res) { - res.status(200).json({ name: 'John Doe' }) -} diff --git a/examples/with-gsap/pages/index.js b/examples/with-gsap/pages/index.tsx similarity index 90% rename from examples/with-gsap/pages/index.js rename to examples/with-gsap/pages/index.tsx index 7517015223891d9..0c38d450fa917c4 100644 --- a/examples/with-gsap/pages/index.js +++ b/examples/with-gsap/pages/index.tsx @@ -2,8 +2,8 @@ import { CSSTransition } from 'react-transition-group' import { gsap } from 'gsap' import Home from '../components/Home' -function App() { - const onEnter = (node) => { +export default function HomePage() { + const onEnter = (node: any) => { gsap.from( [node.children[0].firstElementChild, node.children[0].lastElementChild], 0.6, @@ -18,7 +18,7 @@ function App() { } ) } - const onExit = (node) => { + const onExit = (node: any) => { gsap.to( [node.children[0].firstElementChild, node.children[0].lastElementChild], 0.6, @@ -51,5 +51,3 @@ function App() { </> ) } - -export default App diff --git a/examples/with-gsap/tsconfig.json b/examples/with-gsap/tsconfig.json new file mode 100644 index 000000000000000..99710e857874ff8 --- /dev/null +++ b/examples/with-gsap/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/with-mqtt-js/environment.d.ts b/examples/with-mqtt-js/environment.d.ts new file mode 100644 index 000000000000000..96efc89f4ae691a --- /dev/null +++ b/examples/with-mqtt-js/environment.d.ts @@ -0,0 +1,8 @@ +declare namespace NodeJS { + export interface ProcessEnv { + readonly NEXT_PUBLIC_MQTT_URI: string + readonly NEXT_PUBLIC_MQTT_USERNAME: string + readonly NEXT_PUBLIC_MQTT_PASSWORD: string + readonly NEXT_PUBLIC_MQTT_CLIENTID: string + } +} diff --git a/examples/with-mqtt-js/lib/useMqtt.js b/examples/with-mqtt-js/lib/useMqtt.ts similarity index 71% rename from examples/with-mqtt-js/lib/useMqtt.js rename to examples/with-mqtt-js/lib/useMqtt.ts index 973fab6059eb8f7..8e049d4eb60ebe7 100644 --- a/examples/with-mqtt-js/lib/useMqtt.js +++ b/examples/with-mqtt-js/lib/useMqtt.ts @@ -1,13 +1,21 @@ +import type { MqttClient, IClientOptions } from 'mqtt' import MQTT from 'mqtt' import { useEffect, useRef } from 'react' +interface useMqttProps { + uri: string + options?: IClientOptions + topicHandlers?: { topic: string; handler: (payload: any) => void }[] + onConnectedHandler?: (client: MqttClient) => void +} + function useMqtt({ uri, options = {}, topicHandlers = [{ topic: '', handler: ({ topic, payload, packet }) => {} }], onConnectedHandler = (client) => {}, -}) { - const clientRef = useRef(null) +}: useMqttProps) { + const clientRef = useRef<MqttClient | null>(null) useEffect(() => { if (clientRef.current) return @@ -23,9 +31,9 @@ function useMqtt({ const client = clientRef.current topicHandlers.forEach((th) => { - client.subscribe(th.topic) + client?.subscribe(th.topic) }) - client.on('message', (topic, rawPayload, packet) => { + client?.on('message', (topic: string, rawPayload: any, packet: any) => { const th = topicHandlers.find((t) => t.topic === topic) let payload try { @@ -36,7 +44,7 @@ function useMqtt({ if (th) th.handler({ topic, payload, packet }) }) - client.on('connect', () => { + client?.on('connect', () => { if (onConnectedHandler) onConnectedHandler(client) }) diff --git a/examples/with-mqtt-js/package.json b/examples/with-mqtt-js/package.json index 798499c254e0fb8..61613393e082a33 100644 --- a/examples/with-mqtt-js/package.json +++ b/examples/with-mqtt-js/package.json @@ -6,9 +6,15 @@ "start": "next start" }, "dependencies": { - "mqtt": "4.2.6", + "mqtt": "^4.3.7", "next": "latest", "react": "^18.2.0", "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^18.11.12", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.9", + "typescript": "^4.9.4" } } diff --git a/examples/with-mqtt-js/pages/index.js b/examples/with-mqtt-js/pages/index.tsx similarity index 81% rename from examples/with-mqtt-js/pages/index.js rename to examples/with-mqtt-js/pages/index.tsx index 6de3eedd3333474..a8cae982c1d8505 100644 --- a/examples/with-mqtt-js/pages/index.js +++ b/examples/with-mqtt-js/pages/index.tsx @@ -1,9 +1,10 @@ import { useState, useRef } from 'react' +import type { MqttClient } from 'mqtt' import useMqtt from '../lib/useMqtt' export default function Home() { - const [incommingMessages, setIncommingMessages] = useState([]) - const addMessage = (message) => { + const [incommingMessages, setIncommingMessages] = useState<any[]>([]) + const addMessage = (message: any) => { setIncommingMessages((incommingMessages) => [...incommingMessages, message]) } const clearMessages = () => { @@ -13,14 +14,14 @@ export default function Home() { const incommingMessageHandlers = useRef([ { topic: 'topic1', - handler: (msg) => { + handler: (msg: string) => { addMessage(msg) }, }, ]) - const mqttClientRef = useRef(null) - const setMqttClient = (client) => { + const mqttClientRef = useRef<MqttClient | null>(null) + const setMqttClient = (client: MqttClient) => { mqttClientRef.current = client } useMqtt({ @@ -34,7 +35,7 @@ export default function Home() { onConnectedHandler: (client) => setMqttClient(client), }) - const publishMessages = (client) => { + const publishMessages = (client: any) => { if (!client) { console.log('(publishMessages) Cannot publish, mqttClient: ', client) return diff --git a/examples/with-mqtt-js/tsconfig.json b/examples/with-mqtt-js/tsconfig.json new file mode 100644 index 000000000000000..99710e857874ff8 --- /dev/null +++ b/examples/with-mqtt-js/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/with-mux-video/components/button.js b/examples/with-mux-video/components/button.tsx similarity index 78% rename from examples/with-mux-video/components/button.js rename to examples/with-mux-video/components/button.tsx index b76d6e0ad3c1838..f1349302c276ffd 100644 --- a/examples/with-mux-video/components/button.js +++ b/examples/with-mux-video/components/button.tsx @@ -1,4 +1,8 @@ -export default function Button({ children, ...otherProps }) { +type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { + children: React.ReactNode +} + +export default function Button({ children, ...otherProps }: ButtonProps) { return ( <> <button {...otherProps}>{children}</button> diff --git a/examples/with-mux-video/components/error-message.js b/examples/with-mux-video/components/error-message.tsx similarity index 71% rename from examples/with-mux-video/components/error-message.js rename to examples/with-mux-video/components/error-message.tsx index 75ae50f96796f6d..140de09a219efd9 100644 --- a/examples/with-mux-video/components/error-message.js +++ b/examples/with-mux-video/components/error-message.tsx @@ -1,4 +1,8 @@ -export default function ErrorMessage({ message }) { +interface ErrorMessageProps { + message?: string +} + +export default function ErrorMessage({ message }: ErrorMessageProps) { return ( <> <div className="message">{message || 'Unknown error'}</div> diff --git a/examples/with-mux-video/components/layout.js b/examples/with-mux-video/components/layout.tsx similarity index 95% rename from examples/with-mux-video/components/layout.js rename to examples/with-mux-video/components/layout.tsx index 76c050a15951569..3d3995190a09607 100644 --- a/examples/with-mux-video/components/layout.js +++ b/examples/with-mux-video/components/layout.tsx @@ -1,6 +1,16 @@ import Head from 'next/head' import { MUX_HOME_PAGE_URL } from '../constants' +interface LayoutProps { + title?: string + description?: string + metaTitle?: string + metaDescription?: string + image?: string + children: React.ReactNode + loadTwitterWidget?: boolean +} + export default function Layout({ title, description, @@ -9,7 +19,7 @@ export default function Layout({ image = 'https://with-mux-video.vercel.app/mux-nextjs-og-image.png', children, loadTwitterWidget, -}) { +}: LayoutProps) { return ( <div className="container"> <Head> diff --git a/examples/with-mux-video/components/spinner.js b/examples/with-mux-video/components/spinner.tsx similarity index 90% rename from examples/with-mux-video/components/spinner.js rename to examples/with-mux-video/components/spinner.tsx index 7f8b4a11a009c64..59a861a3aac47f9 100644 --- a/examples/with-mux-video/components/spinner.js +++ b/examples/with-mux-video/components/spinner.tsx @@ -1,4 +1,9 @@ -export default function Spinner({ size = 6, color = '#999' }) { +interface SpinnerProps { + size?: Number + color?: string +} + +export default function Spinner({ size = 6, color = '#999' }: SpinnerProps) { return ( <> <div className="spinner" /> diff --git a/examples/with-mux-video/components/upload-form.js b/examples/with-mux-video/components/upload-form.tsx similarity index 84% rename from examples/with-mux-video/components/upload-form.js rename to examples/with-mux-video/components/upload-form.tsx index 6b9c8509a3e2395..e2b95ec21c273ce 100644 --- a/examples/with-mux-video/components/upload-form.js +++ b/examples/with-mux-video/components/upload-form.tsx @@ -6,7 +6,7 @@ import Button from './button' import Spinner from './spinner' import ErrorMessage from './error-message' -const fetcher = (url) => { +const fetcher = (url: string) => { return fetch(url).then((res) => res.json()) } @@ -14,9 +14,9 @@ const UploadForm = () => { const [isUploading, setIsUploading] = useState(false) const [isPreparing, setIsPreparing] = useState(false) const [uploadId, setUploadId] = useState(null) - const [progress, setProgress] = useState(null) + const [progress, setProgress] = useState<Number | null>(null) const [errorMessage, setErrorMessage] = useState('') - const inputRef = useRef(null) + const inputRef = useRef<HTMLInputElement>(null) const { data, error } = useSwr( () => (isPreparing ? `/api/upload/${uploadId}` : null), @@ -30,7 +30,6 @@ const UploadForm = () => { if (upload && upload.asset_id) { Router.push({ pathname: `/asset/${upload.asset_id}`, - scroll: false, }) } }, [upload]) @@ -54,18 +53,25 @@ const UploadForm = () => { } } - const startUpload = (evt) => { + const startUpload = () => { setIsUploading(true) + + const files = inputRef.current?.files + if (!files) { + setErrorMessage('An unexpected issue occurred') + return + } + const upload = UpChunk.createUpload({ endpoint: createUpload, - file: inputRef.current.files[0], + file: files[0], }) - upload.on('error', (err) => { + upload.on('error', (err: any) => { setErrorMessage(err.detail.message) }) - upload.on('progress', (progress) => { + upload.on('progress', (progress: any) => { setProgress(Math.floor(progress.detail)) }) @@ -90,7 +96,7 @@ const UploadForm = () => { </> ) : ( <label> - <Button type="button" onClick={() => inputRef.current.click()}> + <Button type="button" onClick={() => inputRef.current?.click()}> Select a video file </Button> <input type="file" onChange={startUpload} ref={inputRef} /> diff --git a/examples/with-mux-video/components/upload-page.js b/examples/with-mux-video/components/upload-page.tsx similarity index 93% rename from examples/with-mux-video/components/upload-page.js rename to examples/with-mux-video/components/upload-page.tsx index bc945f16c050a01..105a776c27af326 100644 --- a/examples/with-mux-video/components/upload-page.js +++ b/examples/with-mux-video/components/upload-page.tsx @@ -1,7 +1,11 @@ import Layout from './layout' import { MUX_HOME_PAGE_URL } from '../constants' -export default function UploadPage({ children }) { +interface UploadPageProps { + children: React.ReactNode +} + +export default function UploadPage({ children }: UploadPageProps) { return ( <Layout title="Welcome to Mux + Next.js" diff --git a/examples/with-mux-video/constants.js b/examples/with-mux-video/constants.ts similarity index 100% rename from examples/with-mux-video/constants.js rename to examples/with-mux-video/constants.ts diff --git a/examples/with-mux-video/package.json b/examples/with-mux-video/package.json index 5d858c79ee6b70e..b37ed8e43989d98 100644 --- a/examples/with-mux-video/package.json +++ b/examples/with-mux-video/package.json @@ -6,12 +6,18 @@ "start": "next start" }, "dependencies": { - "@mux/mux-node": "^2.8.0", - "@mux/mux-player-react": "latest", - "@mux/upchunk": "^2.3.1", + "@mux/mux-node": "^7.0.0", + "@mux/mux-player-react": "^1.4.0", + "@mux/upchunk": "^3.0.0", "next": "latest", "react": "^18.2.0", "react-dom": "^18.2.0", - "swr": "^0.2.0" + "swr": "^1.3.0" + }, + "devDependencies": { + "@types/node": "^18.11.10", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.9", + "typescript": "^4.9.3" } } diff --git a/examples/with-mux-video/pages/api/asset/[id].js b/examples/with-mux-video/pages/api/asset/[id].ts similarity index 66% rename from examples/with-mux-video/pages/api/asset/[id].js rename to examples/with-mux-video/pages/api/asset/[id].ts index a9bf34692ac5ffb..014ae2cb1c7c65a 100644 --- a/examples/with-mux-video/pages/api/asset/[id].js +++ b/examples/with-mux-video/pages/api/asset/[id].ts @@ -1,19 +1,23 @@ +import type { NextApiRequest, NextApiResponse } from 'next' import Mux from '@mux/mux-node' const { Video } = new Mux() -export default async function assetHandler(req, res) { +export default async function assetHandler( + req: NextApiRequest, + res: NextApiResponse +) { const { method } = req switch (method) { case 'GET': try { - const asset = await Video.Assets.get(req.query.id) + const asset = await Video.Assets.get(req.query.id as string) res.json({ asset: { id: asset.id, status: asset.status, errors: asset.errors, - playback_id: asset.playback_ids[0].id, + playback_id: asset.playback_ids![0].id, }, }) } catch (e) { diff --git a/examples/with-mux-video/pages/api/upload.js b/examples/with-mux-video/pages/api/upload.ts similarity index 80% rename from examples/with-mux-video/pages/api/upload.js rename to examples/with-mux-video/pages/api/upload.ts index 1ee2f2b4e92966f..a79a688a2ec246c 100644 --- a/examples/with-mux-video/pages/api/upload.js +++ b/examples/with-mux-video/pages/api/upload.ts @@ -1,7 +1,11 @@ +import type { NextApiRequest, NextApiResponse } from 'next' import Mux from '@mux/mux-node' const { Video } = new Mux() -export default async function uploadHandler(req, res) { +export default async function uploadHandler( + req: NextApiRequest, + res: NextApiResponse +) { const { method } = req switch (method) { diff --git a/examples/with-mux-video/pages/api/upload/[id].js b/examples/with-mux-video/pages/api/upload/[id].ts similarity index 71% rename from examples/with-mux-video/pages/api/upload/[id].js rename to examples/with-mux-video/pages/api/upload/[id].ts index 86f0a3f4320a49d..e681a15709f9dc9 100644 --- a/examples/with-mux-video/pages/api/upload/[id].js +++ b/examples/with-mux-video/pages/api/upload/[id].ts @@ -1,13 +1,17 @@ +import type { NextApiRequest, NextApiResponse } from 'next' import Mux from '@mux/mux-node' const { Video } = new Mux() -export default async function uploadHandler(req, res) { +export default async function uploadHandler( + req: NextApiRequest, + res: NextApiResponse +) { const { method } = req switch (method) { case 'GET': try { - const upload = await Video.Uploads.get(req.query.id) + const upload = await Video.Uploads.get(req.query.id as string) res.json({ upload: { status: upload.status, diff --git a/examples/with-mux-video/pages/asset/[id].js b/examples/with-mux-video/pages/asset/[id].tsx similarity index 95% rename from examples/with-mux-video/pages/asset/[id].js rename to examples/with-mux-video/pages/asset/[id].tsx index efee2ff66ebfcb3..e1b2645c932dd08 100644 --- a/examples/with-mux-video/pages/asset/[id].js +++ b/examples/with-mux-video/pages/asset/[id].tsx @@ -6,7 +6,7 @@ import Spinner from '../../components/spinner' import ErrorMessage from '../../components/error-message' import UploadPage from '../../components/upload-page' -const fetcher = (url) => { +const fetcher = (url: string) => { return fetch(url).then((res) => res.json()) } @@ -27,7 +27,7 @@ export default function Asset() { } }, [asset]) - let errorMessage + let errorMessage: string = '' if (error) { errorMessage = 'Error fetching api' diff --git a/examples/with-mux-video/pages/index.js b/examples/with-mux-video/pages/index.tsx similarity index 100% rename from examples/with-mux-video/pages/index.js rename to examples/with-mux-video/pages/index.tsx diff --git a/examples/with-mux-video/pages/v/[id].js b/examples/with-mux-video/pages/v/[id].tsx similarity index 86% rename from examples/with-mux-video/pages/v/[id].js rename to examples/with-mux-video/pages/v/[id].tsx index 3c6815970b5d501..d6f832c24efff50 100644 --- a/examples/with-mux-video/pages/v/[id].js +++ b/examples/with-mux-video/pages/v/[id].tsx @@ -1,3 +1,8 @@ +import type { + InferGetStaticPropsType, + GetStaticProps, + GetStaticPaths, +} from 'next' import MuxPlayer from '@mux/mux-player-react' import Link from 'next/link' import Layout from '../../components/layout' @@ -5,20 +10,29 @@ import Spinner from '../../components/spinner' import { MUX_HOME_PAGE_URL } from '../../constants' import { useRouter } from 'next/router' -export function getStaticProps({ params: { id: playbackId } }) { +type Params = { + id?: string +} + +export const getStaticProps: GetStaticProps = async ({ params }) => { + const { id: playbackId } = params as Params const poster = `https://image.mux.com/${playbackId}/thumbnail.png` return { props: { playbackId, poster } } } -export function getStaticPaths() { +export const getStaticPaths: GetStaticPaths = () => { return { paths: [], fallback: true, } } -const Code = ({ children }) => ( +type CodeProps = { + children: React.ReactNode +} + +const Code = ({ children }: CodeProps) => ( <> <span className="code">{children}</span> <style jsx>{` @@ -32,7 +46,10 @@ const Code = ({ children }) => ( </> ) -export default function Playback({ playbackId, poster }) { +export default function Playback({ + playbackId, + poster, +}: InferGetStaticPropsType<typeof getStaticProps>) { const router = useRouter() if (router.isFallback) { diff --git a/examples/with-mux-video/tsconfig.json b/examples/with-mux-video/tsconfig.json new file mode 100644 index 000000000000000..99710e857874ff8 --- /dev/null +++ b/examples/with-mux-video/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} From bfd96ac012cec1c6990823d4489ebea79601a605 Mon Sep 17 00:00:00 2001 From: Max Proske <max@mproske.com> Date: Sun, 11 Dec 2022 18:10:14 -0800 Subject: [PATCH 07/16] Fix `with-webassembly` example and convert to Typescript (#43677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes `with-webassembly` example unable to build with the current `next.config.js`. https://github.com/vercel/next.js/issues/29362#issuecomment-932767530 - Converted example to TypeScript ```js // Before config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' // After config.output.webassemblyModuleFilename = isServer && !dev ? '../static/wasm/[modulehash].wasm' : 'static/wasm/[modulehash].wasm' ``` ``` > Build error occurred Error: Export encountered errors on following paths: / at /Users/max/dev/next.js/examples/with-webassembly/node_modules/next/dist/export/index.js:408:19 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async Span.traceAsyncFn (/Users/max/dev/next.js/examples/with-webassembly/node_modules/next/dist/trace/trace.js:79:20) at async /Users/max/dev/next.js/examples/with-webassembly/node_modules/next/dist/build/index.js:1342:21 at async Span.traceAsyncFn (/Users/max/dev/next.js/examples/with-webassembly/node_modules/next/dist/trace/trace.js:79:20) at async /Users/max/dev/next.js/examples/with-webassembly/node_modules/next/dist/build/index.js:1202:17 at async Span.traceAsyncFn (/Users/max/dev/next.js/examples/with-webassembly/node_modules/next/dist/trace/trace.js:79:20) at async Object.build [as default] (/Users/max/dev/next.js/examples/with-webassembly/node_modules/next/dist/build/index.js:65:29)  ELIFECYCLE  Command failed with exit code 1. ``` ## Documentation / Examples - [X] Make sure the linting passes by running `pnpm build && pnpm lint` - [X] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- examples/with-webassembly/README.md | 2 ++ .../components/RustComponent.tsx | 24 +++++++++++++++++++ examples/with-webassembly/next.config.js | 13 +++++++--- examples/with-webassembly/package.json | 16 +++++++++---- examples/with-webassembly/pages/api/edge.js | 11 --------- examples/with-webassembly/pages/api/edge.ts | 16 +++++++++++++ examples/with-webassembly/pages/index.js | 24 ------------------- examples/with-webassembly/pages/index.tsx | 15 ++++++++++++ examples/with-webassembly/tsconfig.json | 20 ++++++++++++++++ examples/with-webassembly/wasm.d.ts | 3 +++ 10 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 examples/with-webassembly/components/RustComponent.tsx delete mode 100644 examples/with-webassembly/pages/api/edge.js create mode 100644 examples/with-webassembly/pages/api/edge.ts delete mode 100644 examples/with-webassembly/pages/index.js create mode 100644 examples/with-webassembly/pages/index.tsx create mode 100644 examples/with-webassembly/tsconfig.json create mode 100644 examples/with-webassembly/wasm.d.ts diff --git a/examples/with-webassembly/README.md b/examples/with-webassembly/README.md index b2f9822c7fd2e0c..e846c711927e056 100644 --- a/examples/with-webassembly/README.md +++ b/examples/with-webassembly/README.md @@ -32,4 +32,6 @@ To compile `src/add.rs` to `add.wasm` run: npm run build-rust # or yarn build-rust +# or +pnpm build-rust ``` diff --git a/examples/with-webassembly/components/RustComponent.tsx b/examples/with-webassembly/components/RustComponent.tsx new file mode 100644 index 000000000000000..6acf05307bf666f --- /dev/null +++ b/examples/with-webassembly/components/RustComponent.tsx @@ -0,0 +1,24 @@ +import type { AddModuleExports } from '../wasm' +import dynamic from 'next/dynamic' + +interface RustComponentProps { + number: Number +} + +const RustComponent = dynamic({ + loader: async () => { + // Import the wasm module + // @ts-ignore + const exports = (await import('../add.wasm')) as AddModuleExports + const { add_one: addOne } = exports + + // Return a React component that calls the add_one method on the wasm module + return ({ number }: RustComponentProps) => ( + <div> + <>{addOne(number)}</> + </div> + ) + }, +}) + +export default RustComponent diff --git a/examples/with-webassembly/next.config.js b/examples/with-webassembly/next.config.js index 4d5f3d98c8b8c9d..42ea82bf4049fce 100644 --- a/examples/with-webassembly/next.config.js +++ b/examples/with-webassembly/next.config.js @@ -1,7 +1,12 @@ /** @type {import('next').NextConfig} */ -module.exports = { - webpack(config) { - config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' +const nextConfig = { + webpack(config, { isServer, dev }) { + // Use the client static directory in the server bundle and prod mode + // Fixes `Error occurred prerendering page "/"` + config.output.webassemblyModuleFilename = + isServer && !dev + ? '../static/wasm/[modulehash].wasm' + : 'static/wasm/[modulehash].wasm' // Since Webpack 5 doesn't enable WebAssembly by default, we should do it manually config.experiments = { ...config.experiments, asyncWebAssembly: true } @@ -9,3 +14,5 @@ module.exports = { return config }, } + +module.exports = nextConfig diff --git a/examples/with-webassembly/package.json b/examples/with-webassembly/package.json index fe8da1928db97d3..aeee0eac806674a 100644 --- a/examples/with-webassembly/package.json +++ b/examples/with-webassembly/package.json @@ -1,14 +1,20 @@ { "private": true, - "dependencies": { - "next": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, "scripts": { "dev": "next", "build": "next build", "build-rust": "rustc --target wasm32-unknown-unknown -O --crate-type=cdylib src/add.rs -o add.wasm", "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^18.11.10", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.9", + "typescript": "^4.9.3" } } diff --git a/examples/with-webassembly/pages/api/edge.js b/examples/with-webassembly/pages/api/edge.js deleted file mode 100644 index f724fd32280f5e1..000000000000000 --- a/examples/with-webassembly/pages/api/edge.js +++ /dev/null @@ -1,11 +0,0 @@ -import add_module from '../../add.wasm?module' - -const instance$ = WebAssembly.instantiate(add_module) - -export default async function edgeExample() { - const { exports } = await instance$ - const number = exports.add_one(10) - return new Response(`got: ${number}`) -} - -export const config = { runtime: 'experimental-edge' } diff --git a/examples/with-webassembly/pages/api/edge.ts b/examples/with-webassembly/pages/api/edge.ts new file mode 100644 index 000000000000000..0e9a30908ae409a --- /dev/null +++ b/examples/with-webassembly/pages/api/edge.ts @@ -0,0 +1,16 @@ +import type { AddModuleExports } from '../../wasm' +// @ts-ignore +import addWasm from '../../add.wasm?module' + +const module$ = WebAssembly.instantiate(addWasm) + +export default async function handler() { + const instance = (await module$) as any + const exports = instance.exports as AddModuleExports + const { add_one: addOne } = exports + const number = addOne(10) + + return new Response(`got: ${number}`) +} + +export const config = { runtime: 'experimental-edge' } diff --git a/examples/with-webassembly/pages/index.js b/examples/with-webassembly/pages/index.js deleted file mode 100644 index ba2b622f49f1d4c..000000000000000 --- a/examples/with-webassembly/pages/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import { withRouter } from 'next/router' -import dynamic from 'next/dynamic' -import Link from 'next/link' - -const RustComponent = dynamic({ - loader: async () => { - // Import the wasm module - const rustModule = await import('../add.wasm') - // Return a React component that calls the add_one method on the wasm module - return (props) => <div>{rustModule.add_one(props.number)}</div> - }, -}) - -const Page = ({ router: { query } }) => { - const number = parseInt(query.number || 30) - return ( - <div> - <RustComponent number={number} /> - <Link href={`/?number=${number + 1}`}>+</Link> - </div> - ) -} - -export default withRouter(Page) diff --git a/examples/with-webassembly/pages/index.tsx b/examples/with-webassembly/pages/index.tsx new file mode 100644 index 000000000000000..6d6aba712f39194 --- /dev/null +++ b/examples/with-webassembly/pages/index.tsx @@ -0,0 +1,15 @@ +import { useRouter } from 'next/router' +import Link from 'next/link' +import RustComponent from '../components/RustComponent' + +export default function Page() { + const { query } = useRouter() + const number = parseInt(query.number as string) || 30 + + return ( + <div> + <RustComponent number={number} /> + <Link href={`/?number=${number + 1}`}>+</Link> + </div> + ) +} diff --git a/examples/with-webassembly/tsconfig.json b/examples/with-webassembly/tsconfig.json new file mode 100644 index 000000000000000..5b3c8783e426628 --- /dev/null +++ b/examples/with-webassembly/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "wasm.d.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/with-webassembly/wasm.d.ts b/examples/with-webassembly/wasm.d.ts new file mode 100644 index 000000000000000..db4e6b84e3ea33b --- /dev/null +++ b/examples/with-webassembly/wasm.d.ts @@ -0,0 +1,3 @@ +export interface AddModuleExports { + add_one(number: Number): Number +} From af0ac941dedd29ca59b6230416b4533ba230ff9c Mon Sep 17 00:00:00 2001 From: labyrinthitis <114704834+labyrinthitis@users.noreply.github.com> Date: Sun, 11 Dec 2022 18:33:38 -0800 Subject: [PATCH 08/16] corrected /examples/github-pages readme (#43766) `branch` is the correct subheading direction; the `source` subheading represents `deploying from a branch` or from a `github action` ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- examples/github-pages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/github-pages/README.md b/examples/github-pages/README.md index 05d81b5f3a5cad3..819ef854e55762d 100644 --- a/examples/github-pages/README.md +++ b/examples/github-pages/README.md @@ -22,7 +22,7 @@ pnpm create next-app --example github-pages nextjs-github-pages 1. Edit `next.config.js` to match your GitHub repository name. 1. Push the starter code to the `main` branch. 1. Run the `deploy` script (e.g. `npm run deploy`) to create the `gh-pages` branch. -1. On GitHub, go to **Settings** > **Pages** > **Source**, and choose `gh-pages` as the branch with the `/root` folder. Hit **Save**. +1. On GitHub, go to **Settings** > **Pages** > **Branch**, and choose `gh-pages` as the branch with the `/root` folder. Hit **Save**. 1. Make a change. 1. Run the `deploy` script again to push the changes to GitHub Pages. From 50caf8b191b51e22f31a61eeb2ef88390419bab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= <hannes.lund@gmail.com> Date: Mon, 12 Dec 2022 03:46:29 +0100 Subject: [PATCH 09/16] Add helpful error for createContext used in Server Components (#43747) Format runtime server errors similarly to how build errors [are formatted](https://github.com/vercel/next.js/blob/canary/packages/next/client/dev/error-overlay/format-webpack-messages.js). Add helpful message when createContext is used in Server Components. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- errors/context-in-server-component.md | 30 ++++ errors/manifest.json | 4 + packages/next/lib/format-server-error.ts | 12 ++ packages/next/server/dev/next-dev-server.ts | 2 + .../server-components.test.ts.snap | 42 +++++ .../acceptance-app/server-components.test.ts | 160 ++++++++++++++++++ 6 files changed, 250 insertions(+) create mode 100644 errors/context-in-server-component.md create mode 100644 packages/next/lib/format-server-error.ts create mode 100644 test/development/acceptance-app/__snapshots__/server-components.test.ts.snap create mode 100644 test/development/acceptance-app/server-components.test.ts diff --git a/errors/context-in-server-component.md b/errors/context-in-server-component.md new file mode 100644 index 000000000000000..1dcbfd8a2f7ce39 --- /dev/null +++ b/errors/context-in-server-component.md @@ -0,0 +1,30 @@ +# createContext in a Server Component + +#### Why This Error Occurred + +You are using `createContext` in a Server Component but it only works in Client Components. + +#### Possible Ways to Fix It + +Mark the component using `createContext` as a Client Component by adding `'use client'` at the top of the file. + +##### Before + +```jsx +import { createContext } from 'react' + +const Context = createContext() +``` + +##### After + +```jsx +'use client' +import { createContext } from 'react' + +const Context = createContext() +``` + +### Useful Links + +[Server and Client Components](https://beta.nextjs.org/docs/rendering/server-and-client-components#context) diff --git a/errors/manifest.json b/errors/manifest.json index f2ed0ae1656e749..7273d829894c5ec 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -773,6 +773,10 @@ { "title": "fast-refresh-reload", "path": "/errors/fast-refresh-reload.md" + }, + { + "title": "context-in-server-component", + "path": "/errors/context-in-server-component.md" } ] } diff --git a/packages/next/lib/format-server-error.ts b/packages/next/lib/format-server-error.ts new file mode 100644 index 000000000000000..477a837741d308d --- /dev/null +++ b/packages/next/lib/format-server-error.ts @@ -0,0 +1,12 @@ +export function formatServerError(error: Error): void { + if (error.message.includes('createContext is not a function')) { + const message = + 'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component' + error.message = message + if (error.stack) { + const lines = error.stack.split('\n') + lines[0] = message + error.stack = lines.join('\n') + } + } +} diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 3b793ba777e35bc..336686fbe51f339 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -76,6 +76,7 @@ import { } from '../../build/utils' import { getDefineEnv } from '../../build/webpack-config' import loadJsConfig from '../../build/load-jsconfig' +import { formatServerError } from '../../lib/format-server-error' // Load ReactDevOverlay only when needed let ReactDevOverlayImpl: FunctionComponent @@ -1027,6 +1028,7 @@ export default class DevServer extends Server { return await super.run(req, res, parsedUrl) } catch (error) { const err = getProperError(error) + formatServerError(err) this.logErrorWithOriginalStack(err).catch(() => {}) if (!res.sent) { res.statusCode = 500 diff --git a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap new file mode 100644 index 000000000000000..5c76475e0fc1da1 --- /dev/null +++ b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called 1`] = ` +" 1 of 1 unhandled error +Server Error + +TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context + +This error happened while generating the page. Any console logs will be displayed in the terminal window. + +app/page.js (3:24) @ React + + 1 | + 2 | import React from 'react' +> 3 | const Context = React.createContext() + | ^ + 4 | export default function Page() { + 5 | return ( + 6 | <>" +`; + +exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called in external package 1`] = ` +" 1 of 1 unhandled error +Server Error + +TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context + +This error happened while generating the page. Any console logs will be displayed in the terminal window. + +null" +`; + +exports[`Error Overlay for server components createContext called in Server Component should show error when createContext is called in external package 1`] = ` +" 1 of 1 unhandled error +Server Error + +TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context + +This error happened while generating the page. Any console logs will be displayed in the terminal window. + +null" +`; diff --git a/test/development/acceptance-app/server-components.test.ts b/test/development/acceptance-app/server-components.test.ts new file mode 100644 index 000000000000000..47246440498c07c --- /dev/null +++ b/test/development/acceptance-app/server-components.test.ts @@ -0,0 +1,160 @@ +/* eslint-env jest */ +import { sandbox } from './helpers' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import path from 'path' + +describe('Error Overlay for server components', () => { + if (process.env.NEXT_TEST_REACT_VERSION === '^17') { + it('should skip for react v17', () => {}) + return + } + + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, + }) + }) + afterAll(() => next.destroy()) + + describe('createContext called in Server Component', () => { + it('should show error when React.createContext is called', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + import React from 'react' + const Context = React.createContext() + export default function Page() { + return ( + <> + <Context.Provider value="hello"> + <h1>Page</h1> + </Context.Provider> + </> + ) + }`, + ], + ]) + ) + + // TODO-APP: currently requires a full reload because moving from a client component to a server component isn't causing a Fast Refresh yet. + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource(true)).toMatchSnapshot() + expect(next.cliOutput).toContain( + 'createContext only works in Client Components' + ) + + await cleanup() + }) + + it('should show error when React.createContext is called in external package', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/my-package/index.js', + ` + const React = require('react') + module.exports = React.createContext() + `, + ], + [ + 'node_modules/my-package/package.json', + ` + { + "name": "my-package", + "version": "0.0.1" + } + `, + ], + [ + 'app/page.js', + ` + import Context from 'my-package' + export default function Page() { + return ( + <> + <Context.Provider value="hello"> + <h1>Page</h1> + </Context.Provider> + </> + ) + }`, + ], + ]) + ) + + // TODO-APP: currently requires a full reload because moving from a client component to a server component isn't causing a Fast Refresh yet. + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource(true)).toMatchSnapshot() + expect(next.cliOutput).toContain( + 'createContext only works in Client Components' + ) + + await cleanup() + }) + + it('should show error when createContext is called in external package', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/my-package/index.js', + ` + const { createContext } = require('react') + module.exports = createContext() + `, + ], + [ + 'node_modules/my-package/package.json', + ` + { + "name": "my-package", + "version": "0.0.1" + } + `, + ], + [ + 'app/page.js', + ` + import Context from 'my-package' + export default function Page() { + return ( + <> + <Context.Provider value="hello"> + <h1>Page</h1> + </Context.Provider> + </> + ) + }`, + ], + ]) + ) + + // TODO-APP: currently requires a full reload because moving from a client component to a server component isn't causing a Fast Refresh yet. + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource(true)).toMatchSnapshot() + expect(next.cliOutput).toContain( + 'createContext only works in Client Components' + ) + + await cleanup() + }) + }) +}) From 5cf7408bb26eafb561010e94e5964b48e95a383d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= <hannes.lund@gmail.com> Date: Mon, 12 Dec 2022 03:51:07 +0100 Subject: [PATCH 10/16] Update @next/font data (#43883) Update @next/font data ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- packages/font/src/google/font-data.json | 140 +++++++++++++++- packages/font/src/google/index.ts | 212 ++++++++++++++++++++++-- 2 files changed, 339 insertions(+), 13 deletions(-) diff --git a/packages/font/src/google/font-data.json b/packages/font/src/google/font-data.json index e5d7b257096a468..8c81c7527875c06 100644 --- a/packages/font/src/google/font-data.json +++ b/packages/font/src/google/font-data.json @@ -1883,6 +1883,29 @@ } ] }, + "Chivo Mono": { + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], + "styles": ["normal", "italic"], + "axes": [ + { + "tag": "wght", + "min": 100, + "max": 900, + "defaultValue": 400 + } + ] + }, "Chonburi": { "weights": ["400"], "styles": ["normal"] @@ -2834,8 +2857,16 @@ "styles": ["normal"] }, "Frank Ruhl Libre": { - "weights": ["300", "400", "500", "700", "900"], - "styles": ["normal"] + "weights": ["300", "400", "500", "600", "700", "800", "900", "variable"], + "styles": ["normal"], + "axes": [ + { + "tag": "wght", + "min": 300, + "max": 900, + "defaultValue": 400 + } + ] }, "Fraunces": { "weights": [ @@ -3375,6 +3406,29 @@ "weights": ["400"], "styles": ["normal"] }, + "Hanken Grotesk": { + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], + "styles": ["normal", "italic"], + "axes": [ + { + "tag": "wght", + "min": 100, + "max": 900, + "defaultValue": 400 + } + ] + }, "Hanuman": { "weights": ["100", "300", "400", "700", "900"], "styles": ["normal"] @@ -4893,6 +4947,34 @@ "weights": ["200", "300", "400", "600", "700", "800", "900"], "styles": ["normal"] }, + "Martian Mono": { + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "variable" + ], + "styles": ["normal"], + "axes": [ + { + "tag": "wdth", + "min": 75, + "max": 112.5, + "defaultValue": 100 + }, + { + "tag": "wght", + "min": 100, + "max": 800, + "defaultValue": 400 + } + ] + }, "Marvel": { "weights": ["400", "700"], "styles": ["normal", "italic"] @@ -7295,6 +7377,18 @@ "weights": ["100", "200", "300", "400", "500", "600", "700", "800", "900"], "styles": ["normal"] }, + "Noto Serif NP Hmong": { + "weights": ["400", "500", "600", "700", "variable"], + "styles": ["normal"], + "axes": [ + { + "tag": "wght", + "min": 400, + "max": 700, + "defaultValue": 400 + } + ] + }, "Noto Serif Nyiakeng Puachue Hmong": { "weights": ["400", "500", "600", "700", "variable"], "styles": ["normal"], @@ -8694,6 +8788,10 @@ } ] }, + "Rubik 80s Fade": { + "weights": ["400"], + "styles": ["normal"] + }, "Rubik Beastly": { "weights": ["400"], "styles": ["normal"] @@ -8714,6 +8812,10 @@ "weights": ["400"], "styles": ["normal"] }, + "Rubik Gemstones": { + "weights": ["400"], + "styles": ["normal"] + }, "Rubik Glitch": { "weights": ["400"], "styles": ["normal"] @@ -8746,6 +8848,18 @@ "weights": ["400"], "styles": ["normal"] }, + "Rubik Spray Paint": { + "weights": ["400"], + "styles": ["normal"] + }, + "Rubik Storm": { + "weights": ["400"], + "styles": ["normal"] + }, + "Rubik Vinyl": { + "weights": ["400"], + "styles": ["normal"] + }, "Rubik Wet Paint": { "weights": ["400"], "styles": ["normal"] @@ -9803,6 +9917,28 @@ "weights": ["400"], "styles": ["normal"] }, + "Unbounded": { + "weights": [ + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], + "styles": ["normal"], + "axes": [ + { + "tag": "wght", + "min": 200, + "max": 900, + "defaultValue": 400 + } + ] + }, "Uncial Antiqua": { "weights": ["400"], "styles": ["normal"] diff --git a/packages/font/src/google/index.ts b/packages/font/src/google/index.ts index f1d20fdafbdac45..0f4c8434c43dc4d 100644 --- a/packages/font/src/google/index.ts +++ b/packages/font/src/google/index.ts @@ -3440,7 +3440,7 @@ export declare function Cambo< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'latin'> + subsets?: Array<'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Candal< T extends CssVariable | undefined = undefined @@ -3953,6 +3953,31 @@ export declare function Chivo< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Chivo_Mono< + T extends CssVariable | undefined = undefined +>(options?: { + weight?: + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900' + | 'variable' + | Array< + '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' + > + style?: 'normal' | 'italic' | Array<'normal' | 'italic'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext' | 'vietnamese'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Chonburi< T extends CssVariable | undefined = undefined >(options: { @@ -6267,14 +6292,17 @@ export declare function Francois_One< }): T extends undefined ? NextFont : NextFontWithVariable export declare function Frank_Ruhl_Libre< T extends CssVariable | undefined = undefined ->(options: { - weight: +>(options?: { + weight?: | '300' | '400' | '500' + | '600' | '700' + | '800' | '900' - | Array<'300' | '400' | '500' | '700' | '900'> + | 'variable' + | Array<'300' | '400' | '500' | '600' | '700' | '800' | '900'> style?: 'normal' | Array<'normal'> display?: Display variable?: T @@ -7411,6 +7439,31 @@ export declare function Handlee< adjustFontFallback?: boolean subsets?: Array<'latin'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Hanken_Grotesk< + T extends CssVariable | undefined = undefined +>(options?: { + weight?: + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900' + | 'variable' + | Array< + '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' + > + style?: 'normal' | 'italic' | Array<'normal' | 'italic'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'cyrillic-ext' | 'latin' | 'latin-ext' | 'vietnamese'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Hanuman< T extends CssVariable | undefined = undefined >(options: { @@ -10935,7 +10988,9 @@ export declare function Marmelad< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'cyrillic' | 'latin' | 'latin-ext'> + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'latin' | 'latin-ext' | 'vietnamese' + > }): T extends undefined ? NextFont : NextFontWithVariable export declare function Martel< T extends CssVariable | undefined = undefined @@ -10977,6 +11032,29 @@ export declare function Martel_Sans< adjustFontFallback?: boolean subsets?: Array<'devanagari' | 'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Martian_Mono< + T extends CssVariable | undefined = undefined +>(options?: { + weight?: + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | 'variable' + | Array<'100' | '200' | '300' | '400' | '500' | '600' | '700' | '800'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> + axes?: 'wdth'[] +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Marvel< T extends CssVariable | undefined = undefined >(options: { @@ -12412,7 +12490,7 @@ export declare function Noto_Naskh_Arabic< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'arabic'> + subsets?: Array<'arabic' | 'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Nastaliq_Urdu< T extends CssVariable | undefined = undefined @@ -13196,7 +13274,7 @@ export declare function Noto_Sans_Hanifi_Rohingya< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'hanifi-rohingya'> + subsets?: Array<'hanifi-rohingya' | 'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Hanunoo< T extends CssVariable | undefined = undefined @@ -13869,7 +13947,7 @@ export declare function Noto_Sans_Mro< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'mro'> + subsets?: Array<'latin' | 'latin-ext' | 'mro'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Multani< T extends CssVariable | undefined = undefined @@ -14457,7 +14535,7 @@ export declare function Noto_Sans_Symbols_2< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'symbols'> + subsets?: Array<'latin' | 'latin-ext' | 'symbols'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Syriac< T extends CssVariable | undefined = undefined @@ -14717,7 +14795,7 @@ export declare function Noto_Sans_Tifinagh< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'tifinagh'> + subsets?: Array<'latin' | 'latin-ext' | 'tifinagh'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Tirhuta< T extends CssVariable | undefined = undefined @@ -15319,6 +15397,24 @@ export declare function Noto_Serif_Myanmar< adjustFontFallback?: boolean subsets?: Array<'myanmar'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Noto_Serif_NP_Hmong< + T extends CssVariable | undefined = undefined +>(options?: { + weight?: + | '400' + | '500' + | '600' + | '700' + | 'variable' + | Array<'400' | '500' | '600' | '700'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'nyiakeng-puachue-hmong'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Serif_Nyiakeng_Puachue_Hmong< T extends CssVariable | undefined = undefined >(options?: { @@ -17631,7 +17727,7 @@ export declare function Reem_Kufi< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'arabic' | 'latin'> + subsets?: Array<'arabic' | 'latin' | 'latin-ext' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Reem_Kufi_Fun< T extends CssVariable | undefined = undefined @@ -18125,6 +18221,20 @@ export declare function Rubik< 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' > }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Rubik_80s_Fade< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' + > +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Rubik_Beastly< T extends CssVariable | undefined = undefined >(options: { @@ -18195,6 +18305,20 @@ export declare function Rubik_Distressed< 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' > }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Rubik_Gemstones< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' + > +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Rubik_Glitch< T extends CssVariable | undefined = undefined >(options: { @@ -18305,6 +18429,48 @@ export declare function Rubik_Puddles< 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' > }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Rubik_Spray_Paint< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' + > +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Rubik_Storm< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' + > +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Rubik_Vinyl< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'hebrew' | 'latin' | 'latin-ext' + > +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Rubik_Wet_Paint< T extends CssVariable | undefined = undefined >(options: { @@ -20943,6 +21109,30 @@ export declare function Ultra< adjustFontFallback?: boolean subsets?: Array<'latin'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Unbounded< + T extends CssVariable | undefined = undefined +>(options?: { + weight?: + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900' + | 'variable' + | Array<'200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'latin' | 'latin-ext' | 'vietnamese' + > +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Uncial_Antiqua< T extends CssVariable | undefined = undefined >(options: { From 6ba53d8f1fdad3c0586c04627b3ab4016fe7624f Mon Sep 17 00:00:00 2001 From: JJ Kasper <jj@jjsweb.site> Date: Sun, 11 Dec 2022 21:43:08 -0600 Subject: [PATCH 11/16] Update flakey dev context tests (#43951) x-ref: https://github.com/vercel/next.js/actions/runs/3672193364/jobs/6208192356 x-ref: https://github.com/vercel/next.js/actions/runs/3672193364/jobs/6208192142 x-ref: https://github.com/vercel/next.js/actions/runs/3672172302/jobs/6208150047 --- .../server-components.test.ts.snap | 42 ------------------- .../acceptance-app/server-components.test.ts | 27 ++++++++++-- 2 files changed, 23 insertions(+), 46 deletions(-) delete mode 100644 test/development/acceptance-app/__snapshots__/server-components.test.ts.snap diff --git a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap deleted file mode 100644 index 5c76475e0fc1da1..000000000000000 --- a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called 1`] = ` -" 1 of 1 unhandled error -Server Error - -TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context - -This error happened while generating the page. Any console logs will be displayed in the terminal window. - -app/page.js (3:24) @ React - - 1 | - 2 | import React from 'react' -> 3 | const Context = React.createContext() - | ^ - 4 | export default function Page() { - 5 | return ( - 6 | <>" -`; - -exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called in external package 1`] = ` -" 1 of 1 unhandled error -Server Error - -TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context - -This error happened while generating the page. Any console logs will be displayed in the terminal window. - -null" -`; - -exports[`Error Overlay for server components createContext called in Server Component should show error when createContext is called in external package 1`] = ` -" 1 of 1 unhandled error -Server Error - -TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context - -This error happened while generating the page. Any console logs will be displayed in the terminal window. - -null" -`; diff --git a/test/development/acceptance-app/server-components.test.ts b/test/development/acceptance-app/server-components.test.ts index 47246440498c07c..cdace90cd7b8d05 100644 --- a/test/development/acceptance-app/server-components.test.ts +++ b/test/development/acceptance-app/server-components.test.ts @@ -3,6 +3,7 @@ import { sandbox } from './helpers' import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' import path from 'path' +import { check } from 'next-test-utils' describe('Error Overlay for server components', () => { if (process.env.NEXT_TEST_REACT_VERSION === '^17') { @@ -51,7 +52,12 @@ describe('Error Overlay for server components', () => { await browser.refresh() expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource(true)).toMatchSnapshot() + await check(async () => { + expect(await session.getRedboxSource(true)).toContain( + `TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component` + ) + return 'success' + }, 'success') expect(next.cliOutput).toContain( 'createContext only works in Client Components' ) @@ -100,7 +106,14 @@ describe('Error Overlay for server components', () => { await browser.refresh() expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource(true)).toMatchSnapshot() + + await check(async () => { + expect(await session.getRedboxSource(true)).toContain( + `TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component` + ) + return 'success' + }, 'success') + expect(next.cliOutput).toContain( 'createContext only works in Client Components' ) @@ -149,11 +162,17 @@ describe('Error Overlay for server components', () => { await browser.refresh() expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource(true)).toMatchSnapshot() + + await check(async () => { + expect(await session.getRedboxSource(true)).toContain( + `TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component` + ) + return 'success' + }, 'success') + expect(next.cliOutput).toContain( 'createContext only works in Client Components' ) - await cleanup() }) }) From f6f1f50238aab9d9da766a19f800d562b28d568a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= <hannes.lund@gmail.com> Date: Mon, 12 Dec 2022 05:35:55 +0100 Subject: [PATCH 12/16] Increase stack trace limit on the server (#43800) Increases `Error.stackTraceLimit` on the server to match the client. Adds `if (typeof window !== 'undefined') {` in `use-error-handler`, otherwise it also affects the server - but only after that file is compiled. Closes NEXT-125 ### Before ![image](https://user-images.githubusercontent.com/25056922/206123948-0c92009a-e5e8-4519-9862-1c1b83d88168.png) ### After ![image](https://user-images.githubusercontent.com/25056922/206124053-ba792463-76d4-457c-ac3b-d3e5b95b7bf9.png) ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: JJ Kasper <jj@jjsweb.site> --- .../internal/helpers/use-error-handler.ts | 9 ++- packages/next/server/dev/next-dev-server.ts | 4 ++ .../acceptance-app/ReactRefreshLogBox.test.ts | 68 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts index bd96c248ea0c71b..fa86bc92f4434ca 100644 --- a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts +++ b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts @@ -22,9 +22,12 @@ function isHydrationError(error: Error): boolean { ) } -try { - Error.stackTraceLimit = 50 -} catch {} +if (typeof window !== 'undefined') { + try { + // Increase the number of stack frames on the client + Error.stackTraceLimit = 50 + } catch {} +} const errorQueue: Array<Error> = [] const rejectionQueue: Array<Error> = [] diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 336686fbe51f339..4f126062f435109 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -150,6 +150,10 @@ export default class DevServer extends Server { } constructor(options: Options) { + try { + // Increase the number of stack frames on the server + Error.stackTraceLimit = 50 + } catch {} super({ ...options, dev: true }) this.persistPatchedGlobals() this.renderOpts.dev = true diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts index 6e73c94dfa900eb..7b7a416c68bc75f 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts @@ -1153,4 +1153,72 @@ describe('ReactRefreshLogBox app', () => { await cleanup() }) + + test('Call stack count is correct for server error', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + export default function Page() { + throw new Error('Server error') + } +`, + ], + ]) + ) + + expect(await session.hasRedbox(true)).toBe(true) + + // Open full Call Stack + await browser + .elementByCss('[data-nextjs-data-runtime-error-collapsed-action]') + .click() + const callStackCount = ( + await browser.elementsByCss('[data-nextjs-call-stack-frame]') + ).length + + // Expect more than the default amount of frames + // The default stackTraceLimit results in max 9 [data-nextjs-call-stack-frame] elements + expect(callStackCount).toBeGreaterThan(9) + + await cleanup() + }) + + test('Call stack count is correct for client error', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + 'use client' + export default function Page() { + if (typeof window !== 'undefined') { + throw new Error('Client error') + } + return null + } +`, + ], + ]) + ) + + expect(await session.hasRedbox(true)).toBe(true) + + // Open full Call Stack + await browser + .elementByCss('[data-nextjs-data-runtime-error-collapsed-action]') + .click() + const callStackCount = ( + await browser.elementsByCss('[data-nextjs-call-stack-frame]') + ).length + + // Expect more than the default amount of frames + // The default stackTraceLimit results in max 9 [data-nextjs-call-stack-frame] elements + expect(callStackCount).toBeGreaterThan(9) + + await cleanup() + }) }) From d2c23bb5e83397b996b143d2a020ae7816220251 Mon Sep 17 00:00:00 2001 From: Steven <steven@ceriously.com> Date: Mon, 12 Dec 2022 00:02:52 -0500 Subject: [PATCH 13/16] Refactor image optimization util (#43868) This PR doesn't change any behavior, its just refactoring. - renamed `webpack/loaders/next-image-loader.js` to `.ts` - moved duplicate code into shared function `optimizeImage()` - support `height` as optional param - convert `extension` to `contentType` --- ...t-image-loader.js => next-image-loader.ts} | 40 ++- packages/next/server/image-optimizer.ts | 274 ++++++++---------- 2 files changed, 149 insertions(+), 165 deletions(-) rename packages/next/build/webpack/loaders/{next-image-loader.js => next-image-loader.ts} (77%) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.ts similarity index 77% rename from packages/next/build/webpack/loaders/next-image-loader.js rename to packages/next/build/webpack/loaders/next-image-loader.ts index 7a24bd9c7b4a0ef..cc33c6c532a231d 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.ts @@ -1,14 +1,23 @@ +import isAnimated from 'next/dist/compiled/is-animated' import loaderUtils from 'next/dist/compiled/loader-utils3' -import { resizeImage, getImageSize } from '../../../server/image-optimizer' +import { optimizeImage, getImageSize } from '../../../server/image-optimizer' const BLUR_IMG_SIZE = 8 const BLUR_QUALITY = 70 const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next/client/image.tsx -function nextImageLoader(content) { +interface Options { + isServer: boolean + isDev: boolean + assetPrefix: string + basePath: string +} + +function nextImageLoader(this: any, content: Buffer) { const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader') return imageLoaderSpan.traceAsyncFn(async () => { - const { isServer, isDev, assetPrefix, basePath } = this.getOptions() + const options: Options = this.getOptions() + const { isServer, isDev, assetPrefix, basePath } = options const context = this.rootContext const opts = { context, content } const interpolatedName = loaderUtils.interpolateName( @@ -33,9 +42,9 @@ function nextImageLoader(content) { throw err } - let blurDataURL - let blurWidth - let blurHeight + let blurDataURL: string + let blurWidth: number + let blurHeight: number if (VALID_BLUR_EXT.includes(extension)) { // Shrink the image's largest dimension @@ -60,14 +69,23 @@ function nextImageLoader(content) { const prefix = 'http://localhost' const url = new URL(`${basePath || ''}/_next/image`, prefix) url.searchParams.set('url', outputPath) - url.searchParams.set('w', blurWidth) - url.searchParams.set('q', BLUR_QUALITY) + url.searchParams.set('w', String(blurWidth)) + url.searchParams.set('q', String(BLUR_QUALITY)) blurDataURL = url.href.slice(prefix.length) } else { const resizeImageSpan = imageLoaderSpan.traceChild('image-resize') - const resizedImage = await resizeImageSpan.traceAsyncFn(() => - resizeImage(content, blurWidth, blurHeight, extension, BLUR_QUALITY) - ) + const resizedImage = await resizeImageSpan.traceAsyncFn(() => { + if (isAnimated(content)) { + return content + } + return optimizeImage({ + buffer: content, + width: blurWidth, + height: blurHeight, + contentType: `image/${extension}`, + quality: BLUR_QUALITY, + }) + }) const blurDataURLSpan = imageLoaderSpan.traceChild( 'image-base64-tostring' ) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index d0518eb7ab98358..6dc952e90c4bdc8 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -390,6 +390,119 @@ export function getMaxAge(str: string | null): number { return 0 } +export async function optimizeImage({ + buffer, + contentType, + quality, + width, + height, + nextConfigOutput, +}: { + buffer: Buffer + contentType: string + quality: number + width: number + height?: number + nextConfigOutput?: 'standalone' +}): Promise<Buffer> { + let optimizedBuffer = buffer + if (sharp) { + // Begin sharp transformation logic + const transformer = sharp(buffer) + + transformer.rotate() + + if (height) { + transformer.resize(width, height) + } else { + const { width: metaWidth } = await transformer.metadata() + + if (metaWidth && metaWidth > width) { + transformer.resize(width) + } + } + + if (contentType === AVIF) { + if (transformer.avif) { + const avifQuality = quality - 15 + transformer.avif({ + quality: Math.max(avifQuality, 0), + chromaSubsampling: '4:2:0', // same as webp + }) + } else { + console.warn( + chalk.yellow.bold('Warning: ') + + `Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` + + 'Read more: https://nextjs.org/docs/messages/sharp-version-avif' + ) + transformer.webp({ quality }) + } + } else if (contentType === WEBP) { + transformer.webp({ quality }) + } else if (contentType === PNG) { + transformer.png({ quality }) + } else if (contentType === JPEG) { + transformer.jpeg({ quality }) + } + + optimizedBuffer = await transformer.toBuffer() + // End sharp transformation logic + } else { + if (showSharpMissingWarning && nextConfigOutput) { + // TODO: should we ensure squoosh also works even though we don't + // recommend it be used in production and this is a production feature + console.error( + `Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production` + ) + throw new ImageError(500, 'internal server error') + } + // Show sharp warning in production once + if (showSharpMissingWarning) { + console.warn( + chalk.yellow.bold('Warning: ') + + `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically for Image Optimization.\n` + + 'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production' + ) + showSharpMissingWarning = false + } + + // Begin Squoosh transformation logic + const orientation = await getOrientation(buffer) + + const operations: Operation[] = [] + + if (orientation === Orientation.RIGHT_TOP) { + operations.push({ type: 'rotate', numRotations: 1 }) + } else if (orientation === Orientation.BOTTOM_RIGHT) { + operations.push({ type: 'rotate', numRotations: 2 }) + } else if (orientation === Orientation.LEFT_BOTTOM) { + operations.push({ type: 'rotate', numRotations: 3 }) + } else { + // TODO: support more orientations + // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const _: never = orientation + } + + if (height) { + operations.push({ type: 'resize', width, height }) + } else { + operations.push({ type: 'resize', width }) + } + + if (contentType === AVIF) { + optimizedBuffer = await processBuffer(buffer, operations, 'avif', quality) + } else if (contentType === WEBP) { + optimizedBuffer = await processBuffer(buffer, operations, 'webp', quality) + } else if (contentType === PNG) { + optimizedBuffer = await processBuffer(buffer, operations, 'png', quality) + } else if (contentType === JPEG) { + optimizedBuffer = await processBuffer(buffer, operations, 'jpeg', quality) + } + } + + return optimizedBuffer +} + export async function imageOptimizer( _req: IncomingMessage, _res: ServerResponse, @@ -504,114 +617,13 @@ export async function imageOptimizer( contentType = JPEG } try { - let optimizedBuffer: Buffer | undefined - if (sharp) { - // Begin sharp transformation logic - const transformer = sharp(upstreamBuffer) - - transformer.rotate() - - const { width: metaWidth } = await transformer.metadata() - - if (metaWidth && metaWidth > width) { - transformer.resize(width) - } - - if (contentType === AVIF) { - if (transformer.avif) { - const avifQuality = quality - 15 - transformer.avif({ - quality: Math.max(avifQuality, 0), - chromaSubsampling: '4:2:0', // same as webp - }) - } else { - console.warn( - chalk.yellow.bold('Warning: ') + - `Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` + - 'Read more: https://nextjs.org/docs/messages/sharp-version-avif' - ) - transformer.webp({ quality }) - } - } else if (contentType === WEBP) { - transformer.webp({ quality }) - } else if (contentType === PNG) { - transformer.png({ quality }) - } else if (contentType === JPEG) { - transformer.jpeg({ quality }) - } - - optimizedBuffer = await transformer.toBuffer() - // End sharp transformation logic - } else { - if (showSharpMissingWarning && nextConfig.output === 'standalone') { - // TODO: should we ensure squoosh also works even though we don't - // recommend it be used in production and this is a production feature - console.error( - `Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production` - ) - throw new ImageError(500, 'internal server error') - } - // Show sharp warning in production once - if (showSharpMissingWarning) { - console.warn( - chalk.yellow.bold('Warning: ') + - `For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically for Image Optimization.\n` + - 'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production' - ) - showSharpMissingWarning = false - } - - // Begin Squoosh transformation logic - const orientation = await getOrientation(upstreamBuffer) - - const operations: Operation[] = [] - - if (orientation === Orientation.RIGHT_TOP) { - operations.push({ type: 'rotate', numRotations: 1 }) - } else if (orientation === Orientation.BOTTOM_RIGHT) { - operations.push({ type: 'rotate', numRotations: 2 }) - } else if (orientation === Orientation.LEFT_BOTTOM) { - operations.push({ type: 'rotate', numRotations: 3 }) - } else { - // TODO: support more orientations - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // const _: never = orientation - } - - operations.push({ type: 'resize', width }) - - if (contentType === AVIF) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'avif', - quality - ) - } else if (contentType === WEBP) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'webp', - quality - ) - } else if (contentType === PNG) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'png', - quality - ) - } else if (contentType === JPEG) { - optimizedBuffer = await processBuffer( - upstreamBuffer, - operations, - 'jpeg', - quality - ) - } - - // End Squoosh transformation logic - } + let optimizedBuffer = await optimizeImage({ + buffer: upstreamBuffer, + contentType, + quality, + width, + nextConfigOutput: nextConfig.output, + }) if (optimizedBuffer) { if (isDev && width <= BLUR_IMG_SIZE && quality === BLUR_QUALITY) { // During `next dev`, we don't want to generate blur placeholders with webpack @@ -743,52 +755,6 @@ export function sendResponse( } } -export async function resizeImage( - content: Buffer, - width: number, - height: number, - // Should match VALID_BLUR_EXT - extension: 'avif' | 'webp' | 'png' | 'jpeg', - quality: number -): Promise<Buffer> { - if (isAnimated(content)) { - return content - } else if (sharp) { - const transformer = sharp(content) - - if (extension === 'avif') { - if (transformer.avif) { - transformer.avif({ quality }) - } else { - console.warn( - chalk.yellow.bold('Warning: ') + - `Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version.\n` + - 'Read more: https://nextjs.org/docs/messages/sharp-version-avif' - ) - transformer.webp({ quality }) - } - } else if (extension === 'webp') { - transformer.webp({ quality }) - } else if (extension === 'png') { - transformer.png({ quality }) - } else if (extension === 'jpeg') { - transformer.jpeg({ quality }) - } - transformer.resize(width, height) - const buf = await transformer.toBuffer() - return buf - } else { - const resizeOperationOpts: Operation = { type: 'resize', width, height } - const buf = await processBuffer( - content, - [resizeOperationOpts], - extension, - quality - ) - return buf - } -} - export async function getImageSize( buffer: Buffer, // Should match VALID_BLUR_EXT From b3dfa037670c8601332edb7c749b48f588d9e72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= <hannes.lund@gmail.com> Date: Mon, 12 Dec 2022 12:53:38 +0100 Subject: [PATCH 14/16] useSearchParams - bailout to client rendering during static generation (#43603) Currently `useSearchParams` bails out of static generation altogether, forcing the page to be dynamic. This behaviour is wrong. Instead it should still be statically generated, but `useSearchParams` should only run on the client. This is achieved by throwing a "bailout to client rendering" error. If there's no suspense boundary the whole page will bailout to be rendered on the client. If there is a suspense boundary it will only bailout from that point. ~This PR also adds handling for `export const dynamic = 'force-static'` combined with `useSearchParams`. If it is enabled it will return an empty `ReadonlyURLSearchParams` and skip the bailout to client rendering. Since the `staticGenerationAsyncStorage` only is available on the server - `forceStatic` is sent to the `app-router` to enable sending an empty `URLSearchParams` to match the server response.~ https://github.com/vercel/next.js/pull/43603#discussion_r1042071542 the implementation was wrong, added skipped tests and todo comment for now. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../components/bailout-to-client-rendering.ts | 19 ++ packages/next/client/components/navigation.ts | 5 +- .../components/static-generation-bailout.ts | 4 +- test/e2e/app-dir/app-static.test.ts | 186 +++++++++++++----- .../hooks/use-search-params/[slug]/layout.js | 7 - .../use-search-params/force-static/page.js | 19 ++ .../app/hooks/use-search-params/page.js | 10 + .../{[slug]/page.js => search-params.js} | 6 +- .../use-search-params/with-suspense/page.js | 10 + test/e2e/app-dir/app-static/next.config.js | 3 +- 10 files changed, 204 insertions(+), 65 deletions(-) create mode 100644 packages/next/client/components/bailout-to-client-rendering.ts delete mode 100644 test/e2e/app-dir/app-static/app/hooks/use-search-params/[slug]/layout.js create mode 100644 test/e2e/app-dir/app-static/app/hooks/use-search-params/force-static/page.js create mode 100644 test/e2e/app-dir/app-static/app/hooks/use-search-params/page.js rename test/e2e/app-dir/app-static/app/hooks/use-search-params/{[slug]/page.js => search-params.js} (82%) create mode 100644 test/e2e/app-dir/app-static/app/hooks/use-search-params/with-suspense/page.js diff --git a/packages/next/client/components/bailout-to-client-rendering.ts b/packages/next/client/components/bailout-to-client-rendering.ts new file mode 100644 index 000000000000000..2658280b863a7f2 --- /dev/null +++ b/packages/next/client/components/bailout-to-client-rendering.ts @@ -0,0 +1,19 @@ +import { suspense } from '../../shared/lib/dynamic-no-ssr' +import { staticGenerationAsyncStorage } from './static-generation-async-storage' + +export function bailoutToClientRendering(): boolean | never { + const staticGenerationStore = + staticGenerationAsyncStorage && 'getStore' in staticGenerationAsyncStorage + ? staticGenerationAsyncStorage?.getStore() + : staticGenerationAsyncStorage + + if (staticGenerationStore?.forceStatic) { + return true + } + + if (staticGenerationStore?.isStaticGeneration) { + suspense() + } + + return false +} diff --git a/packages/next/client/components/navigation.ts b/packages/next/client/components/navigation.ts index 2c8990a9e64a849..c2186730f891c08 100644 --- a/packages/next/client/components/navigation.ts +++ b/packages/next/client/components/navigation.ts @@ -12,7 +12,7 @@ import { PathnameContext, // LayoutSegmentsContext, } from '../../shared/lib/hooks-client-context' -import { staticGenerationBailout } from './static-generation-bailout' +import { bailoutToClientRendering } from './bailout-to-client-rendering' const INTERNAL_URLSEARCHPARAMS_INSTANCE = Symbol( 'internal for urlsearchparams readonly' @@ -76,7 +76,8 @@ export function useSearchParams() { return new ReadonlyURLSearchParams(searchParams || new URLSearchParams()) }, [searchParams]) - if (staticGenerationBailout('useSearchParams')) { + if (bailoutToClientRendering()) { + // TODO-APP: handle dynamic = 'force-static' here and on the client return readonlySearchParams } diff --git a/packages/next/client/components/static-generation-bailout.ts b/packages/next/client/components/static-generation-bailout.ts index 4b665567d202415..0957622e9192921 100644 --- a/packages/next/client/components/static-generation-bailout.ts +++ b/packages/next/client/components/static-generation-bailout.ts @@ -1,7 +1,7 @@ import { DynamicServerError } from './hooks-server-context' import { staticGenerationAsyncStorage } from './static-generation-async-storage' -export function staticGenerationBailout(reason: string) { +export function staticGenerationBailout(reason: string): boolean | never { const staticGenerationStore = staticGenerationAsyncStorage && 'getStore' in staticGenerationAsyncStorage ? staticGenerationAsyncStorage?.getStore() @@ -17,4 +17,6 @@ export function staticGenerationBailout(reason: string) { } throw new DynamicServerError(reason) } + + return false } diff --git a/test/e2e/app-dir/app-static.test.ts b/test/e2e/app-dir/app-static.test.ts index 66a08b6c3a8071f..bca51dbeda6c9e8 100644 --- a/test/e2e/app-dir/app-static.test.ts +++ b/test/e2e/app-dir/app-static.test.ts @@ -65,7 +65,15 @@ describe('app-dir static/dynamic handling', () => { 'hooks/use-pathname/[slug]/page.js', 'hooks/use-pathname/slug.html', 'hooks/use-pathname/slug.rsc', - 'hooks/use-search-params/[slug]/page.js', + 'hooks/use-search-params.html', + 'hooks/use-search-params.rsc', + 'hooks/use-search-params/force-static.html', + 'hooks/use-search-params/force-static.rsc', + 'hooks/use-search-params/force-static/page.js', + 'hooks/use-search-params/page.js', + 'hooks/use-search-params/with-suspense.html', + 'hooks/use-search-params/with-suspense.rsc', + 'hooks/use-search-params/with-suspense/page.js', 'ssg-preview.html', 'ssg-preview.rsc', 'ssg-preview/[[...route]]/page.js', @@ -146,6 +154,21 @@ describe('app-dir static/dynamic handling', () => { initialRevalidateSeconds: false, srcRoute: '/hooks/use-pathname/[slug]', }, + '/hooks/use-search-params': { + dataRoute: '/hooks/use-search-params.rsc', + initialRevalidateSeconds: false, + srcRoute: '/hooks/use-search-params', + }, + '/hooks/use-search-params/force-static': { + dataRoute: '/hooks/use-search-params/force-static.rsc', + initialRevalidateSeconds: false, + srcRoute: '/hooks/use-search-params/force-static', + }, + '/hooks/use-search-params/with-suspense': { + dataRoute: '/hooks/use-search-params/with-suspense.rsc', + initialRevalidateSeconds: false, + srcRoute: '/hooks/use-search-params/with-suspense', + }, '/force-static/first': { dataRoute: '/force-static/first.rsc', initialRevalidateSeconds: false, @@ -526,23 +549,28 @@ describe('app-dir static/dynamic handling', () => { expect(secondDate).not.toBe(initialDate) }) - describe('hooks', () => { - describe('useSearchParams', () => { - if (isDev) { - it('should bail out to client rendering during SSG', async () => { - const res = await fetchViaHTTP( - next.url, - '/hooks/use-search-params/slug' - ) - const html = await res.text() - expect(html).toInclude('<html id="__next_error__">') - }) - } + describe('useSearchParams', () => { + describe('client', () => { + it('should bailout to client rendering - without suspense boundary', async () => { + const browser = await webdriver( + next.url, + '/hooks/use-search-params?first=value&second=other&third' + ) - it('should have the correct values', async () => { + expect(await browser.elementByCss('#params-first').text()).toBe('value') + expect(await browser.elementByCss('#params-second').text()).toBe( + 'other' + ) + expect(await browser.elementByCss('#params-third').text()).toBe('') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) + }) + + it('should bailout to client rendering - with suspense boundary', async () => { const browser = await webdriver( next.url, - '/hooks/use-search-params/slug?first=value&second=other&third' + '/hooks/use-search-params/with-suspense?first=value&second=other&third' ) expect(await browser.elementByCss('#params-first').text()).toBe('value') @@ -555,6 +583,31 @@ describe('app-dir static/dynamic handling', () => { ) }) + it.skip('should have empty search params on force-static', async () => { + const browser = await webdriver( + next.url, + '/hooks/use-search-params/force-static?first=value&second=other&third' + ) + + expect(await browser.elementByCss('#params-first').text()).toBe('N/A') + expect(await browser.elementByCss('#params-second').text()).toBe('N/A') + expect(await browser.elementByCss('#params-third').text()).toBe('N/A') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) + + await browser.elementById('to-use-search-params').click() + await browser.waitForElementByCss('#hooks-use-search-params') + + // Should not be empty after navigating to another page with useSearchParams + expect(await browser.elementByCss('#params-first').text()).toBe('1') + expect(await browser.elementByCss('#params-second').text()).toBe('2') + expect(await browser.elementByCss('#params-third').text()).toBe('3') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) + }) + // TODO-APP: re-enable after investigating rewrite params if (!(global as any).isNextDeploy) { it('should have values from canonical url on rewrite', async () => { @@ -572,52 +625,89 @@ describe('app-dir static/dynamic handling', () => { }) } }) - - // TODO: needs updating as usePathname should not bail - describe.skip('usePathname', () => { - if (isDev) { - it('should bail out to client rendering during SSG', async () => { - const res = await fetchViaHTTP(next.url, '/hooks/use-pathname/slug') + // Don't run these tests in dev mode since they won't be statically generated + if (!isDev) { + describe('server response', () => { + it('should bailout to client rendering - without suspense boundary', async () => { + const res = await fetchViaHTTP(next.url, '/hooks/use-search-params') const html = await res.text() expect(html).toInclude('<html id="__next_error__">') }) - } - it('should have the correct values', async () => { - const browser = await webdriver(next.url, '/hooks/use-pathname/slug') + it('should bailout to client rendering - with suspense boundary', async () => { + const res = await fetchViaHTTP( + next.url, + '/hooks/use-search-params/with-suspense' + ) + const html = await res.text() + expect(html).toInclude('<p>search params suspense</p>') + }) - expect(await browser.elementByCss('#pathname').text()).toBe( - '/hooks/use-pathname/slug' - ) - }) + it.skip('should have empty search params on force-static', async () => { + const res = await fetchViaHTTP( + next.url, + '/hooks/use-search-params/force-static?first=value&second=other&third' + ) + const html = await res.text() - it('should have values from canonical url on rewrite', async () => { - const browser = await webdriver(next.url, '/rewritten-use-pathname') + // Shouild not bail out to client rendering + expect(html).not.toInclude('<p>search params suspense</p>') - expect(await browser.elementByCss('#pathname').text()).toBe( - '/rewritten-use-pathname' - ) + // Use empty search params instead + const $ = cheerio.load(html) + expect($('#params-first').text()).toBe('N/A') + expect($('#params-second').text()).toBe('N/A') + expect($('#params-third').text()).toBe('N/A') + expect($('#params-not-real').text()).toBe('N/A') + }) }) - }) + } + }) - if (!(global as any).isNextDeploy) { - it('should show a message to leave feedback for `appDir`', async () => { - expect(next.cliOutput).toContain( - `Thank you for testing \`appDir\` please leave your feedback at https://nextjs.link/app-feedback` - ) + // TODO: needs updating as usePathname should not bail + describe.skip('usePathname', () => { + if (isDev) { + it('should bail out to client rendering during SSG', async () => { + const res = await fetchViaHTTP(next.url, '/hooks/use-pathname/slug') + const html = await res.text() + expect(html).toInclude('<html id="__next_error__">') }) } - it('should keep querystring on static page', async () => { - const browser = await webdriver(next.url, '/blog/tim?message=hello-world') - const checkUrl = async () => - expect(await browser.url()).toBe( - next.url + '/blog/tim?message=hello-world' - ) + it('should have the correct values', async () => { + const browser = await webdriver(next.url, '/hooks/use-pathname/slug') + + expect(await browser.elementByCss('#pathname').text()).toBe( + '/hooks/use-pathname/slug' + ) + }) + + it('should have values from canonical url on rewrite', async () => { + const browser = await webdriver(next.url, '/rewritten-use-pathname') - checkUrl() - await waitFor(1000) - checkUrl() + expect(await browser.elementByCss('#pathname').text()).toBe( + '/rewritten-use-pathname' + ) + }) + }) + + if (!(global as any).isNextDeploy) { + it('should show a message to leave feedback for `appDir`', async () => { + expect(next.cliOutput).toContain( + `Thank you for testing \`appDir\` please leave your feedback at https://nextjs.link/app-feedback` + ) }) + } + + it('should keep querystring on static page', async () => { + const browser = await webdriver(next.url, '/blog/tim?message=hello-world') + const checkUrl = async () => + expect(await browser.url()).toBe( + next.url + '/blog/tim?message=hello-world' + ) + + checkUrl() + await waitFor(1000) + checkUrl() }) }) diff --git a/test/e2e/app-dir/app-static/app/hooks/use-search-params/[slug]/layout.js b/test/e2e/app-dir/app-static/app/hooks/use-search-params/[slug]/layout.js deleted file mode 100644 index 1cd13ef8bb49f91..000000000000000 --- a/test/e2e/app-dir/app-static/app/hooks/use-search-params/[slug]/layout.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Layout({ children }) { - return children -} - -export function generateStaticParams() { - return [{ slug: 'slug' }] -} diff --git a/test/e2e/app-dir/app-static/app/hooks/use-search-params/force-static/page.js b/test/e2e/app-dir/app-static/app/hooks/use-search-params/force-static/page.js new file mode 100644 index 000000000000000..b7f4512e4a011d8 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/hooks/use-search-params/force-static/page.js @@ -0,0 +1,19 @@ +export const dynamic = 'force-static' + +import Link from 'next/link' +import { Suspense } from 'react' +import UseSearchParams from '../search-params' + +export default function Page() { + return ( + <Suspense fallback={<p>search params suspense</p>}> + <UseSearchParams /> + <Link + id="to-use-search-params" + href="/hooks/use-search-params?first=1&second=2&third=3" + > + To / + </Link> + </Suspense> + ) +} diff --git a/test/e2e/app-dir/app-static/app/hooks/use-search-params/page.js b/test/e2e/app-dir/app-static/app/hooks/use-search-params/page.js new file mode 100644 index 000000000000000..0a9904e2a08c118 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/hooks/use-search-params/page.js @@ -0,0 +1,10 @@ +import UseSearchParams from './search-params' + +export default function Page() { + return ( + <> + <p id="hooks-use-search-params" /> + <UseSearchParams /> + </> + ) +} diff --git a/test/e2e/app-dir/app-static/app/hooks/use-search-params/[slug]/page.js b/test/e2e/app-dir/app-static/app/hooks/use-search-params/search-params.js similarity index 82% rename from test/e2e/app-dir/app-static/app/hooks/use-search-params/[slug]/page.js rename to test/e2e/app-dir/app-static/app/hooks/use-search-params/search-params.js index 76e03025e838e55..53ff9fc5c94d149 100644 --- a/test/e2e/app-dir/app-static/app/hooks/use-search-params/[slug]/page.js +++ b/test/e2e/app-dir/app-static/app/hooks/use-search-params/search-params.js @@ -1,11 +1,7 @@ 'use client' import { useSearchParams } from 'next/navigation' -export const config = { - dynamicParams: false, -} - -export default function Page() { +export default function UseSearchParams() { const params = useSearchParams() return ( diff --git a/test/e2e/app-dir/app-static/app/hooks/use-search-params/with-suspense/page.js b/test/e2e/app-dir/app-static/app/hooks/use-search-params/with-suspense/page.js new file mode 100644 index 000000000000000..3cc4bde9ab1ea06 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/hooks/use-search-params/with-suspense/page.js @@ -0,0 +1,10 @@ +import { Suspense } from 'react' +import UseSearchParams from '../search-params' + +export default function Page() { + return ( + <Suspense fallback={<p>search params suspense</p>}> + <UseSearchParams /> + </Suspense> + ) +} diff --git a/test/e2e/app-dir/app-static/next.config.js b/test/e2e/app-dir/app-static/next.config.js index e03d15af21fbdb5..63c84d019a4b20e 100644 --- a/test/e2e/app-dir/app-static/next.config.js +++ b/test/e2e/app-dir/app-static/next.config.js @@ -14,8 +14,7 @@ module.exports = { }, { source: '/rewritten-use-search-params', - destination: - '/hooks/use-search-params/slug?first=value&second=other%20value&third', + destination: '/hooks/use-search-params', }, { source: '/rewritten-use-pathname', From 45eea0a57e2ffb9372985df96c21d957b823765a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= <hannes.lund@gmail.com> Date: Mon, 12 Dec 2022 13:27:06 +0100 Subject: [PATCH 15/16] Open server component errors fullscreen (#43887) https://github.com/vercel/next.js/pull/43844 continued. Make sure Server Component errors always open in fullscreen mode. Currently they only open in fullscreen on initial load - not when adding an Error when updating the file. If in test env, call `self.__NEXT_HMR_CB` after server component HMR was successful, used in tests for the error overlay. Closes NEXT-202 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../react-dev-overlay/hot-reloader-client.tsx | 7 +++ .../internal/ReactDevOverlay.tsx | 4 +- .../acceptance-app/ReactRefreshLogBox.test.ts | 48 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx index 84a1573c9ad9d80..09d90f1fb27871f 100644 --- a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx +++ b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx @@ -348,6 +348,13 @@ function processMessage( dispatcher.onRefresh() }) + if (process.env.__NEXT_TEST_MODE) { + if (self.__NEXT_HMR_CB) { + self.__NEXT_HMR_CB() + self.__NEXT_HMR_CB = null + } + } + return } case 'reloadPage': { diff --git a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx index 931a1c93ca74517..5d0c233240fb2f7 100644 --- a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx +++ b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx @@ -75,10 +75,10 @@ class ReactDevOverlay extends React.PureComponent< /> ) : hasBuildError ? ( <BuildError message={state.buildError!} /> - ) : hasRuntimeErrors ? ( - <Errors initialDisplayState="minimized" errors={state.errors} /> ) : reactError ? ( <Errors initialDisplayState="fullscreen" errors={[reactError]} /> + ) : hasRuntimeErrors ? ( + <Errors initialDisplayState="minimized" errors={state.errors} /> ) : undefined} </ShadowPortal> ) : undefined} diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts index 7b7a416c68bc75f..9fa41067ff307ba 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts @@ -1221,4 +1221,52 @@ describe('ReactRefreshLogBox app', () => { await cleanup() }) + + test('Server component errors should open up in fullscreen', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + // Start with error + [ + 'app/page.js', + ` + export default function Page() { + throw new Error('Server component error') + return <p id="text">Hello world</p> + } + `, + ], + ]) + ) + expect(await session.hasRedbox(true)).toBe(true) + + // Remove error + await session.patch( + 'app/page.js', + ` + export default function Page() { + return <p id="text">Hello world</p> + } + ` + ) + expect(await browser.waitForElementByCss('#text').text()).toBe( + 'Hello world' + ) + expect(await session.hasRedbox()).toBe(false) + + // Re-add error + await session.patch( + 'app/page.js', + ` + export default function Page() { + throw new Error('Server component error!') + return <p id="text">Hello world</p> + } + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + + await cleanup() + }) }) From dcad00d6e733645f055923fd0d9165827c3e4b39 Mon Sep 17 00:00:00 2001 From: Tim Neutkens <tim@timneutkens.nl> Date: Mon, 12 Dec 2022 15:10:54 +0100 Subject: [PATCH 16/16] Add VSCode settings and recommended extensions for Next.js repository (#43954) --- .vscode/extensions.json | 24 +++++++++++++++++++++ .vscode/settings.json | 47 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000000..75f549ad6d9a4e1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,24 @@ +{ + "recommendations": [ + // Linting / Formatting + "rust-lang.rust-analyzer", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "usernamehw.errorlens", + + // Testing + "orta.vscode-jest", + + // PR management / Reviewing + "github.vscode-pull-request-github", + + // Showing todo comments + "gruntfuggly.todo-tree", + + // Collaborating + "ms-vsliveshare.vsliveshare", + + // Debugging + "ms-vscode.vscode-js-profile-flame" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 306e2cb06dd865a..43daf375695185b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,54 @@ { + // Formatting using Prettier by default for all languages + "editor.defaultFormatter": "esbenp.prettier-vscode", + // Formatting using Prettier for JavaScript, overrides VSCode default. + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + // Formatting using Rust-Analyzer for Rust. + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + // Linting using ESLint. "eslint.validate": [ "javascript", "javascriptreact", - { "language": "typescript", "autoFix": true }, - { "language": "typescriptreact", "autoFix": true } + "typescript", + "typescriptreact" ], - "debug.javascript.unmapMissingSources": true, + // Disable Jest autoRun as otherwise it will start running all tests the first time. "jest.autoRun": "off", + + // Debugging. + "debug.javascript.unmapMissingSources": true, + "files.exclude": { "**/node_modules": false, "node_modules": true, "*[!test]**/node_modules": true - } + }, + + // Ensure enough terminal history is preserved when running tests. + "terminal.integrated.scrollback": 10000, + + // Configure todo-tree to exclude node_modules, dist, and compiled. + "todo-tree.filtering.excludeGlobs": [ + "**/node_modules", + "**/dist", + "**/compiled" + ], + // Match TODO-APP in addition to other TODOs. + "todo-tree.general.tags": [ + "BUG", + "HACK", + "FIXME", + "TODO", + "XXX", + "[ ]", + "[x]", + "TODO-APP" + ], + + // Disable TypeScript surveys. + "typescript.surveys.enabled": false }