first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
/* eslint-env jest */
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { outdent } from 'outdent'
describe('ReactRefresh app', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
test('can edit a component without losing state', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [setCount])
return (
<main>
<p>{count}</p>
<button onClick={increment}>Increment</button>
</main>
)
}
`
)
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
await session.patch(
'index.js',
outdent`
import { useCallback, useState } from 'react'
export default function Index() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [setCount])
return (
<main>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</main>
)
}
`
)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 2')
})
test('cyclic dependencies', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.write(
'NudgeOverview.js',
outdent`
import * as React from 'react';
import { foo } from './routes';
const NudgeOverview = () => {
return <span />;
foo;
};
export default NudgeOverview;
`
)
await session.write(
'SurveyOverview.js',
outdent`
const SurveyOverview = () => {
return 100;
};
export default SurveyOverview;
`
)
await session.write(
'Milestones.js',
outdent`
import React from 'react';
import { fragment } from './DashboardPage';
const Milestones = props => {
return <span />;
fragment;
};
export default Milestones;
`
)
await session.write(
'DashboardPage.js',
outdent`
import React from 'react';
import Milestones from './Milestones';
import SurveyOverview from './SurveyOverview';
import NudgeOverview from './NudgeOverview';
export const fragment = {};
const DashboardPage = () => {
return (
<>
<Milestones />
<SurveyOverview />
<NudgeOverview />
</>
);
};
export default DashboardPage;
`
)
await session.write(
'routes.js',
outdent`
import DashboardPage from './DashboardPage';
export const foo = {};
console.warn('DashboardPage at import time:', DashboardPage);
setTimeout(() => console.warn('DashboardPage after:', DashboardPage), 0);
export default DashboardPage;
`
)
await session.patch(
'index.js',
outdent`
import * as React from 'react';
import DashboardPage from './routes';
const HeroApp = (props) => {
return <p>Hello. {DashboardPage ? <DashboardPage /> : null}</p>;
};
export default HeroApp;
`
)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello. 100')
let didFullRefresh = !(await session.patch(
'SurveyOverview.js',
outdent`
const SurveyOverview = () => {
return 200;
};
export default SurveyOverview;
`
))
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello. 200')
expect(didFullRefresh).toBe(false)
didFullRefresh = !(await session.patch(
'index.js',
outdent`
import * as React from 'react';
import DashboardPage from './routes';
const HeroApp = (props) => {
return <p>Hello: {DashboardPage ? <DashboardPage /> : null}</p>;
};
export default HeroApp;
`
))
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello: 200')
expect(didFullRefresh).toBe(false)
didFullRefresh = !(await session.patch(
'SurveyOverview.js',
outdent`
const SurveyOverview = () => {
return 300;
};
export default SurveyOverview;
`
))
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Hello: 300')
expect(didFullRefresh).toBe(false)
})
})

View File

@@ -0,0 +1,310 @@
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { outdent } from 'outdent'
describe('ReactRefreshLogBox-builtins app', () => {
const { isTurbopack, next, isRspack } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
// Module trace is only available with webpack 5
test('Node.js builtins', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'node_modules/my-package/index.js',
outdent`
const dns = require('dns')
module.exports = dns
`,
],
[
'node_modules/my-package/package.json',
outdent`
{
"name": "my-package",
"version": "0.0.1"
}
`,
],
])
)
const { browser, session } = sandbox
await session.patch(
'index.js',
outdent`
import pkg from 'my-package'
export default function Hello() {
return (pkg ? <h1>Package loaded</h1> : <h1>Package did not load</h1>)
}
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'dns'",
"environmentLabel": null,
"label": "Build Error",
"source": "./node_modules/my-package/index.js (1:13)
Module not found: Can't resolve 'dns'
> 1 | const dns = require('dns')
| ^^^^^^^^^^^^^^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": " × Module not found: Can't resolve 'dns' in '<FIXME-project-root>/node_modules/my-package'",
"environmentLabel": null,
"label": "Build Error",
"source": "./node_modules/my-package/index.js
× Module not found: Can't resolve 'dns' in '<FIXME-project-root>/node_modules/my-package'
╭─[1:12]
1 │ const dns = require('dns')
· ──────────────
2 │ module.exports = dns
╰────
Import trace for requested module:
./node_modules/my-package/index.js
./index.js
./app/page.js",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'dns'",
"environmentLabel": null,
"label": "Build Error",
"source": "./node_modules/my-package/index.js (1:1)
Module not found: Can't resolve 'dns'
> 1 | const dns = require('dns')
| ^",
"stack": [],
}
`)
}
})
test('Module not found', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'index.js',
outdent`
import Comp from 'b'
export default function Oops() {
return (
<div>
<Comp>lol</Comp>
</div>
)
}
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'b'",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.js (1:1)
Module not found: Can't resolve 'b'
> 1 | import Comp from 'b'
| ^^^^^^^^^^^^^^^^^^^^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": " × Module not found: Can't resolve 'b' in '<FIXME-project-root>'",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.js
× Module not found: Can't resolve 'b' in '<FIXME-project-root>'
╭─[2:0]
1 │ import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
2 │ import Comp from 'b';
· ─────────────────────
3 │ export default function Oops() {
4 │ return /*#__PURE__*/ _jsxDEV("div", {
╰────
Import trace for requested module:
./index.js
./app/page.js",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'b'",
"environmentLabel": null,
"label": "Build Error",
"source": "./index.js (1:1)
Module not found: Can't resolve 'b'
> 1 | import Comp from 'b'
| ^",
"stack": [],
}
`)
}
})
test('Module not found empty import trace', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'app/page.js',
outdent`
'use client'
import Comp from 'b'
export default function Oops() {
return (
<div>
<Comp>lol</Comp>
</div>
)
}
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'b'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js (2:1)
Module not found: Can't resolve 'b'
> 2 | import Comp from 'b'
| ^^^^^^^^^^^^^^^^^^^^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": " × Module not found: Can't resolve 'b' in '<FIXME-project-root>/app'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js
× Module not found: Can't resolve 'b' in '<FIXME-project-root>/app'
╭─[2:0]
1 │ /* __next_internal_client_entry_do_not_use__ default auto */ import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
2 │ import Comp from 'b';
· ─────────────────────
3 │ export default function Oops() {
4 │ return /*#__PURE__*/ _jsxDEV("div", {
╰────
Import trace for requested module:
./app/page.js",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve 'b'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js (2:1)
Module not found: Can't resolve 'b'
> 2 | import Comp from 'b'
| ^",
"stack": [],
}
`)
}
})
test('Module not found missing global CSS', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
'use client'
import './non-existent.css'
export default function Page(props) {
return <p>index page</p>
}
`,
],
])
)
const { browser, session } = sandbox
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve './non-existent.css'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js (2:1)
Module not found: Can't resolve './non-existent.css'
> 2 | import './non-existent.css'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^",
"stack": [],
}
`)
} else if (isRspack) {
await expect({ browser, next }).toDisplayRedbox(`
{
"description": " × Module not found: Can't resolve './non-existent.css' in '<FIXME-project-root>/app'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js
× Module not found: Can't resolve './non-existent.css' in '<FIXME-project-root>/app'
╭─[2:0]
1 │ /* __next_internal_client_entry_do_not_use__ default auto */ import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
2 │ import './non-existent.css';
· ────────────────────────────
3 │ export default function Page(props) {
4 │ return /*#__PURE__*/ _jsxDEV("p", {
╰────",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": "Module not found: Can't resolve './non-existent.css'",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js (2:1)
Module not found: Can't resolve './non-existent.css'
> 2 | import './non-existent.css'
| ^",
"stack": [],
}
`)
}
await session.patch(
'app/page.js',
outdent`
'use client'
export default function Page(props) {
return <p>index page</p>
}
`
)
await session.waitForNoRedbox()
expect(
await session.evaluate(() => document.documentElement.innerHTML)
).toContain('index page')
})
})

View File

@@ -0,0 +1,76 @@
/* eslint-env jest */
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { outdent } from 'outdent'
// TODO: figure out why snapshots mismatch on GitHub actions
// specifically but work in docker and locally
describe.skip('ReactRefreshLogBox scss app', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
sass: 'latest',
},
skipStart: true,
})
test('scss syntax errors', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.write('index.module.scss', `.button { font-size: 5px; }`)
await session.patch(
'index.js',
outdent`
import './index.module.scss';
export default () => {
return (
<div>
<p>Hello World</p>
</div>
)
}
`
)
await session.waitForNoRedbox()
// Syntax error
await session.patch('index.module.scss', `.button { font-size: :5px; }`)
await session.waitForRedbox()
const source = await session.getRedboxSource()
expect(source).toMatchSnapshot()
// Fix syntax error
await session.patch('index.module.scss', `.button { font-size: 5px; }`)
await session.waitForNoRedbox()
})
test('scss module pure selector error', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.write('index.module.scss', `.button { font-size: 5px; }`)
await session.patch(
'index.js',
outdent`
import './index.module.scss';
export default () => {
return (
<div>
<p>Hello World</p>
</div>
)
}
`
)
// Checks for selectors that can't be prefixed.
// Selector "button" is not pure (pure selectors must contain at least one local class or id)
await session.patch('index.module.scss', `button { font-size: 5px; }`)
await session.waitForRedbox()
const source2 = await session.getRedboxSource()
expect(source2).toMatchSnapshot()
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { outdent } from 'outdent'
describe('ReactRefreshLogBox app', () => {
const { isTurbopack, next, isRspack } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
test('server-side only compilation errors', async () => {
await using sandbox = await createSandbox(next)
const { browser, session } = sandbox
await session.patch(
'app/page.js',
outdent`
'use client'
import myLibrary from 'my-non-existent-library'
export async function getStaticProps() {
return {
props: {
result: myLibrary()
}
}
}
export default function Hello(props) {
return <h1>{props.result}</h1>
}
`
)
if (isTurbopack) {
await expect(browser).toDisplayRedbox(`
{
"description": ""getStaticProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js (3:23)
"getStaticProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching
> 3 | export async function getStaticProps() {
| ^^^^^^^^^^^^^^",
"stack": [],
}
`)
} else if (isRspack) {
await expect(browser).toDisplayRedbox(`
{
"description": " ╰─▶ × Error: x "getStaticProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js
╰─▶ × Error: x "getStaticProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching
│ |
│ ,-[3:1]
│ 1 | 'use client'
│ 2 | import myLibrary from 'my-non-existent-library'
│ 3 | export async function getStaticProps() {
│ : ^^^^^^^^^^^^^^
│ 4 | return {
│ 5 | props: {
│ 6 | result: myLibrary()
\`----
Import trace for requested module:
./app/page.js",
"stack": [],
}
`)
} else {
await expect(browser).toDisplayRedbox(`
{
"description": " x "getStaticProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.js
Error: x "getStaticProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching
|
,-[3:1]
1 | 'use client'
2 | import myLibrary from 'my-non-existent-library'
3 | export async function getStaticProps() {
: ^^^^^^^^^^^^^^
4 | return {
5 | props: {
6 | result: myLibrary()
\`----
Import trace for requested module:
./app/page.js",
"stack": [],
}
`)
}
})
})

View File

@@ -0,0 +1,43 @@
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { createSandbox } from 'development-sandbox'
import { outdent } from 'outdent'
describe('ReactRefreshModule app', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
it('should allow any variable names', async () => {
await using sandbox = await createSandbox(next, new Map([]))
const { session } = sandbox
await session.waitForNoRedbox()
const variables = [
'_a',
'_b',
'currentExports',
'prevExports',
'isNoLongerABoundary',
]
for await (const variable of variables) {
await session.patch(
'app/page.js',
outdent`
'use client'
import { default as ${variable} } from 'next/link'
console.log({ ${variable} })
export default function Page() {
return null
}
`
)
await session.waitForNoRedbox()
expect(next.cliOutput).not.toContain(
`'${variable}' has already been declared`
)
}
})
})

View File

@@ -0,0 +1,385 @@
/* eslint-env jest */
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { check } from 'next-test-utils'
import { outdent } from 'outdent'
describe('ReactRefreshRegression app', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
'styled-components': '6.1.16',
'@next/mdx': 'canary',
'@mdx-js/loader': '2.2.1',
'@mdx-js/react': '2.2.1',
},
skipStart: true,
})
// https://github.com/vercel/next.js/issues/12422
// TODO-APP: port to app directory
test.skip('styled-components hydration mismatch', async () => {
const files = new Map()
files.set(
'pages/_document.js',
outdent`
import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
}
} finally {
sheet.seal()
}
}
}
`
)
await using sandbox = await createSandbox(next, files)
const { session } = sandbox
// We start here.
await session.patch(
'index.js',
outdent`
import React from 'react'
import styled from 'styled-components'
const Title = styled.h1\`
color: red;
font-size: 50px;
\`
export default () => <Title>My page</Title>
`
)
// Verify no hydration mismatch:
await session.waitForNoRedbox()
})
// https://github.com/vercel/next.js/issues/13978
test('can fast refresh a page with static generation', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'app/page.js',
outdent`
'use client'
import { useCallback, useState } from 'react'
export default function Index() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [setCount])
return (
<main>
<p>{count}</p>
<button onClick={increment}>Increment</button>
</main>
)
}
`
)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('0')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
await session.patch(
'app/page.js',
outdent`
'use client'
import { useCallback, useState } from 'react'
export default function Index() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [setCount])
return (
<main>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</main>
)
}
`
)
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 1')
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('Count: 2')
})
// https://github.com/vercel/next.js/issues/13978
test('can fast refresh a page with dynamic rendering', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'app/page.js',
outdent`
export const revalidate = 0
import Component from '../index'
export default function Page() {
return <Component />
}
`
)
await session.patch(
'index.js',
outdent`
'use client'
import { useCallback, useState } from 'react'
export default function Index() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [setCount])
return (
<main>
<p>{count}</p>
<button onClick={increment}>Increment</button>
</main>
)
}
`
)
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
'0'
)
await session.evaluate(() => document.querySelector('button').click())
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
'1'
)
await session.patch(
'index.js',
outdent`
'use client'
import { useCallback, useState } from 'react'
export default function Index() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [setCount])
return (
<main>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</main>
)
}
`
)
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
'Count: 1'
)
await session.evaluate(() => document.querySelector('button').click())
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
'Count: 2'
)
})
// https://github.com/vercel/next.js/issues/13978
test('can fast refresh a page with config', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'app/page.js',
outdent`
export const config = {}
import Component from '../index'
export default function Page() {
return <Component />
}
`
)
await session.patch(
'index.js',
outdent`
'use client'
import { useCallback, useState } from 'react'
export const config = {}
export default function Index() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [setCount])
return (
<main>
<p>{count}</p>
<button onClick={increment}>Increment</button>
</main>
)
}
`
)
await check(
() => session.evaluate(() => document.querySelector('p').textContent),
'0'
)
await session.evaluate(() => document.querySelector('button').click())
expect(
await session.evaluate(() => document.querySelector('p').textContent)
).toBe('1')
})
// https://github.com/vercel/next.js/issues/11504
test('shows an overlay for anonymous function server-side error', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'app/page.js',
`export default function () { throw new Error('boom'); }`
)
await session.waitForRedbox()
const source = await session.getRedboxSource()
expect(source.split(/\r?\n/g).slice(2).join('\n').replace(/^\n+/, ''))
.toMatchInlineSnapshot(`
"> 1 | export default function () { throw new Error('boom'); }
| ^"
`)
})
test('shows an overlay for server-side error in server component', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'app/page.js',
`export default function Page() { throw new Error('boom'); }`
)
await session.waitForRedbox()
const source = await session.getRedboxSource()
expect(source.split(/\r?\n/g).slice(2).join('\n').replace(/^\n+/, ''))
.toMatchInlineSnapshot(`
"> 1 | export default function Page() { throw new Error('boom'); }
| ^"
`)
})
test('shows an overlay for server-side error in client component', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'app/page.js',
outdent`
'use client'
export default function Page() { throw new Error('boom'); }
`
)
await session.waitForRedbox()
const source = await session.getRedboxSource()
expect(source.split(/\r?\n/g).slice(2).join('\n').replace(/^\n+/, ''))
.toMatchInlineSnapshot(`
" 1 | 'use client'
> 2 | export default function Page() { throw new Error('boom'); }
| ^"
`)
})
// https://github.com/vercel/next.js/issues/13574
test('custom loader mdx should have Fast Refresh enabled', async () => {
const files = new Map()
files.set(
'next.config.js',
outdent`
const withMDX = require("@next/mdx")({
extension: /\\.mdx?$/,
});
module.exports = withMDX({
pageExtensions: ["js", "mdx"],
});
`
)
files.set('app/content.mdx', `Hello World!`)
files.set(
'app/page.js',
outdent`
'use client'
import MDX from './content.mdx'
export default function Page() {
return <div id="content"><MDX /></div>
}
`
)
await using sandbox = await createSandbox(next, files)
const { session } = sandbox
expect(
await session.evaluate(
() => document.querySelector('#content').textContent
)
).toBe('Hello World!')
let didNotReload = await session.patch('app/content.mdx', `Hello Foo!`)
expect(didNotReload).toBe(true)
await session.waitForNoRedbox()
expect(
await session.evaluate(
() => document.querySelector('#content').textContent
)
).toBe('Hello Foo!')
didNotReload = await session.patch('app/content.mdx', `Hello Bar!`)
expect(didNotReload).toBe(true)
await session.waitForNoRedbox()
expect(
await session.evaluate(
() => document.querySelector('#content').textContent
)
).toBe('Hello Bar!')
})
})

View File

@@ -0,0 +1,496 @@
/* eslint-env jest */
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { outdent } from 'outdent'
describe('ReactRefreshRequire app', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L989-L1048
test('re-runs accepted modules', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'index.js',
`export default function Noop() { return null; };`
)
await session.write(
'./foo.js',
`window.log.push('init FooV1'); require('./bar');`
)
await session.write(
'./bar.js',
`window.log.push('init BarV1'); export default function Bar() { return null; };`
)
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'index.js',
`require('./foo'); export default function Noop() { return null; };`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init FooV1',
'init BarV1',
])
// We only edited Bar, and it accepted.
// So we expect it to re-run alone.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'./bar.js',
`window.log.push('init BarV2'); export default function Bar() { return null; };`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init BarV2',
])
// We only edited Bar, and it accepted.
// So we expect it to re-run alone.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'./bar.js',
`window.log.push('init BarV3'); export default function Bar() { return null; };`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init BarV3',
])
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled();
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1050-L1137
test('propagates a hot update to closest accepted module', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'index.js',
`export default function Noop() { return null; };`
)
await session.write(
'./foo.js',
outdent`
window.log.push('init FooV1');
require('./bar');
// Exporting a component marks it as auto-accepting.
export default function Foo() {};
`
)
await session.write('./bar.js', `window.log.push('init BarV1');`)
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'index.js',
`require('./foo'); export default function Noop() { return null; };`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init FooV1',
'init BarV1',
])
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
await session.evaluate(() => ((window as any).log = []))
await session.patch('./bar.js', `window.log.push('init BarV2');`)
expect(await session.evaluate(() => (window as any).log)).toEqual([
// // FIXME: Metro order:
// 'init BarV2',
// 'init FooV1',
'init FooV1',
'init BarV2',
// Webpack runs in this order because it evaluates modules parent down, not
// child up. Parents will re-run child modules in the order that they're
// imported from the parent.
])
// We edited Bar, but it doesn't accept.
// So we expect it to re-run together with Foo which does.
await session.evaluate(() => ((window as any).log = []))
await session.patch('./bar.js', `window.log.push('init BarV3');`)
expect(await session.evaluate(() => (window as any).log)).toEqual([
// // FIXME: Metro order:
// 'init BarV3',
// 'init FooV1',
'init FooV1',
'init BarV3',
// Webpack runs in this order because it evaluates modules parent down, not
// child up. Parents will re-run child modules in the order that they're
// imported from the parent.
])
// We edited Bar so that it accepts itself.
// We still re-run Foo because the exports of Bar changed.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'./bar.js',
outdent`
window.log.push('init BarV3');
// Exporting a component marks it as auto-accepting.
export default function Bar() {};
`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
// // FIXME: Metro order:
// 'init BarV3',
// 'init FooV1',
'init FooV1',
'init BarV3',
// Webpack runs in this order because it evaluates modules parent down, not
// child up. Parents will re-run child modules in the order that they're
// imported from the parent.
])
// Further edits to Bar don't re-run Foo.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'./bar.js',
outdent`
window.log.push('init BarV4');
export default function Bar() {};
`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init BarV4',
])
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled();
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1139-L1307
test('propagates hot update to all inverse dependencies', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
await session.patch(
'index.js',
`export default function Noop() { return null; };`
)
// This is the module graph:
// MiddleA*
// / \
// Root* - MiddleB* - Leaf
// \
// MiddleC
//
// * - accepts update
//
// We expect that editing Leaf will propagate to
// MiddleA and MiddleB both of which can handle updates.
await session.write(
'root.js',
outdent`
window.log.push('init RootV1');
import './middleA';
import './middleB';
import './middleC';
export default function Root() {};
`
)
await session.write(
'middleA.js',
outdent`
log.push('init MiddleAV1');
import './leaf';
export default function MiddleA() {};
`
)
await session.write(
'middleB.js',
outdent`
log.push('init MiddleBV1');
import './leaf';
export default function MiddleB() {};
`
)
// This one doesn't import leaf and also doesn't export a component (so it
// doesn't accept updates).
await session.write(
'middleC.js',
`log.push('init MiddleCV1'); export default {};`
)
// Doesn't accept its own updates; they will propagate.
await session.write(
'leaf.js',
`log.push('init LeafV1'); export default {};`
)
// Bootstrap:
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'index.js',
`require('./root'); export default function Noop() { return null; };`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init LeafV1',
'init MiddleAV1',
'init MiddleBV1',
'init MiddleCV1',
'init RootV1',
])
// We edited Leaf, but it doesn't accept.
// So we expect it to re-run together with MiddleA and MiddleB which do.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'leaf.js',
`log.push('init LeafV2'); export default {};`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init LeafV2',
'init MiddleAV1',
'init MiddleBV1',
])
// Let's try the same one more time.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'leaf.js',
`log.push('init LeafV3'); export default {};`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init LeafV3',
'init MiddleAV1',
'init MiddleBV1',
])
// Now edit MiddleB. It should accept and re-run alone.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'middleB.js',
outdent`
log.push('init MiddleBV2');
import './leaf';
export default function MiddleB() {};
`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init MiddleBV2',
])
// Finally, edit MiddleC. It didn't accept so it should bubble to Root.
await session.evaluate(() => ((window as any).log = []))
await session.patch(
'middleC.js',
`log.push('init MiddleCV2'); export default {};`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init MiddleCV2',
'init RootV1',
])
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled()
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1309-L1406
test('runs dependencies before dependents', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1408-L1498
test('provides fresh value for module.exports in parents', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1500-L1590
test('provides fresh value for exports.* in parents', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1592-L1688
test('provides fresh value for ES6 named import in parents', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1690-L1786
test('provides fresh value for ES6 default import in parents', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1788-L1899
test('stops update propagation after module-level errors', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1901-L2010
test('can continue hot updates after module-level errors with module.exports', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2012-L2123
test('can continue hot updates after module-level errors with ES6 exports', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2125-L2233
test('does not accumulate stale exports over time', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2235-L2279
test('bails out if update bubbles to the root via the only path', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2281-L2371
test('bails out if the update bubbles to the root via one of the paths', async () => {
// TODO:
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2373-L2472
// TODO-APP: investigate why it fails in app
test.skip('propagates a module that stops accepting in next version', async () => {
await using sandbox = await createSandbox(next)
const { session } = sandbox
// Accept in parent
await session.write(
'./foo.js',
`;(typeof global !== 'undefined' ? global : window).log.push('init FooV1'); import './bar'; export default function Foo() {};`
)
// Accept in child
await session.write(
'./bar.js',
`;(typeof global !== 'undefined' ? global : window).log.push('init BarV1'); export default function Bar() {};`
)
// Bootstrap:
await session.patch(
'index.js',
`;(typeof global !== 'undefined' ? global : window).log = []; require('./foo'); export default () => null;`
)
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init BarV1',
'init FooV1',
])
let didFullRefresh = false
// Verify the child can accept itself:
await session.evaluate(() => ((window as any).log = []))
didFullRefresh =
didFullRefresh ||
!(await session.patch(
'./bar.js',
`window.log.push('init BarV1.1'); export default function Bar() {};`
))
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init BarV1.1',
])
// Now let's change the child to *not* accept itself.
// We'll expect that now the parent will handle the evaluation.
await session.evaluate(() => ((window as any).log = []))
didFullRefresh =
didFullRefresh ||
!(await session.patch(
'./bar.js',
// It's important we still export _something_, otherwise webpack will
// also emit an extra update to the parent module. This happens because
// webpack converts the module from ESM to CJS, which means the parent
// module must update how it "imports" the module (drops interop code).
// TODO: propose that webpack interrupts the current update phase when
// `module.hot.invalidate()` is called.
`window.log.push('init BarV2'); export {};`
))
// We re-run Bar and expect to stop there. However,
// it didn't export a component, so we go higher.
// We stop at Foo which currently _does_ export a component.
expect(await session.evaluate(() => (window as any).log)).toEqual([
// Bar evaluates twice:
// 1. To invalidate itself once it realizes it's no longer acceptable.
// 2. As a child of Foo re-evaluating.
'init BarV2',
'init BarV2',
'init FooV1',
])
// Change it back so that the child accepts itself.
await session.evaluate(() => ((window as any).log = []))
didFullRefresh =
didFullRefresh ||
!(await session.patch(
'./bar.js',
`window.log.push('init BarV2'); export default function Bar() {};`
))
// Since the export list changed, we have to re-run both the parent
// and the child.
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init BarV2',
'init FooV1',
])
// TODO:
// expect(Refresh.performReactRefresh).toHaveBeenCalled();
// expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
expect(didFullRefresh).toBe(false)
// But editing the child alone now doesn't reevaluate the parent.
await session.evaluate(() => ((window as any).log = []))
didFullRefresh =
didFullRefresh ||
!(await session.patch(
'./bar.js',
`window.log.push('init BarV3'); export default function Bar() {};`
))
expect(await session.evaluate(() => (window as any).log)).toEqual([
'init BarV3',
])
// Finally, edit the parent in a way that changes the export.
// It would still be accepted on its own -- but it's incompatible
// with the past version which didn't have two exports.
await session.evaluate(() => window.localStorage.setItem('init', ''))
didFullRefresh =
didFullRefresh ||
!(await session.patch(
'./foo.js',
`
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem('init', 'init FooV2')
}
export function Foo() {};
export function FooFoo() {};`
))
// Check that we attempted to evaluate, but had to fall back to full refresh.
expect(
await session.evaluate(() => window.localStorage.getItem('init'))
).toEqual('init FooV2')
// expect(Refresh.performFullRefresh).toHaveBeenCalled();
expect(didFullRefresh).toBe(true)
})
// https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2474-L2521
test('can replace a module before it is loaded', async () => {
// TODO:
})
})

View File

@@ -0,0 +1,69 @@
import { FileRef, nextTestSetup } from 'e2e-utils'
import { createSandbox } from 'development-sandbox'
import path from 'path'
jest.setTimeout(240 * 1000)
describe('Error overlay - RSC build errors', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'app-hmr-changes')),
dependencies: {
'@next/mdx': 'canary',
'react-wrap-balancer': '^0.2.4',
'react-tweet': '^3.2.0',
'@mdx-js/react': '^2.3.0',
tailwindcss: '^3.2.6',
typescript: 'latest',
'@types/react': '^18.0.28',
'@types/react-dom': '^18.0.10',
'image-size': '^1.0.2',
autoprefixer: '^10.4.13',
},
skipStart: true,
})
// TODO: The error overlay is not closed when restoring the working code.
;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)(
'Skipped in webpack',
() => {
it('should handle successive HMR changes with errors correctly', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/2020/develop-preview-test'
)
const { session } = sandbox
expect(
await session.evaluate('document.documentElement.innerHTML')
).toContain('A few years ago I tweeted')
const pagePath = 'app/(post)/2020/develop-preview-test/page.mdx'
const originalPage = await next.readFile(pagePath)
const break1 = originalPage.replace('break 1', '<Figure>')
await session.patch(pagePath, break1)
const break2 = break1.replace('{/* break point 2 */}', '<Figure />')
await session.patch(pagePath, break2)
for (let i = 0; i < 5; i++) {
await session.patch(pagePath, break2.replace('break 3', '<Hello />'))
await session.patch(pagePath, break2)
await session.waitForRedbox()
await session.patch(pagePath, break1)
await session.patch(pagePath, originalPage)
await session.waitForNoRedbox()
}
expect(
await session.evaluate('document.documentElement.innerHTML')
).toContain('A few years ago I tweeted')
})
}
)
})

View File

@@ -0,0 +1,98 @@
/* eslint-env jest */
import path from 'path'
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
describe('dynamic metadata error', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
it('should error when id is missing in generateImageMetadata', async () => {
const iconFilePath = 'app/metadata-base/unset/icon.tsx'
const contentMissingIdProperty = `
import { ImageResponse } from 'next/og'
export async function generateImageMetadata() {
return [
{
contentType: 'image/png',
size: { width: 48, height: 48 },
// id: 100,
},
{
contentType: 'image/png',
size: { width: 48, height: 48 },
id: 101,
},
]
}
export default function icon() {
return new ImageResponse(<div>icon</div>)
}
`
await using _sandbox = await createSandbox(
next,
new Map([[iconFilePath, contentMissingIdProperty]]),
'/metadata-base/unset/icon/100'
)
await retry(async () => {
expect(next.cliOutput).toContain(
`id property is required for every item returned from generateImageMetadata`
)
})
})
it('should error when id is missing in generateSitemaps', async () => {
const sitemapFilePath = 'app/metadata-base/unset/sitemap.tsx'
const contentMissingIdProperty = `
import { MetadataRoute } from 'next'
export async function generateSitemaps() {
return [
{ },
]
}
export default function sitemap({ id }): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com/',
lastModified: '2021-01-01',
},
]
}`
await using _sandbox = await createSandbox(
next,
new Map([[sitemapFilePath, contentMissingIdProperty]]),
'/metadata-base/unset/sitemap/100.xml'
)
await retry(async () => {
expect(next.cliOutput).toContain(
`id property is required for every item returned from generateSitemaps`
)
})
})
it('should error if the default export of dynamic image is missing', async () => {
const ogImageFilePath = 'app/opengraph-image.tsx'
const ogImageFileContentWithoutDefaultExport = `
// Missing default export
export function foo() {}
`
await using _sandbox = await createSandbox(
next,
new Map([[ogImageFilePath, ogImageFileContentWithoutDefaultExport]]),
'/opengraph-image'
)
await retry(async () => {
expect(next.cliOutput).toContain(`Default export is missing in`)
})
})
})

View File

@@ -0,0 +1,179 @@
import { check, retry } from 'next-test-utils'
import type { Playwright } from 'next-webdriver'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { createSandbox } from 'development-sandbox'
import { outdent } from 'outdent'
async function clickSourceFile(browser: Playwright) {
await browser.waitForElementByCss(
'[data-with-open-in-editor-link-source-file]'
)
await browser
.elementByCss('[data-with-open-in-editor-link-source-file]')
.click()
}
async function clickImportTraceFiles(browser: Playwright) {
await browser.waitForElementByCss(
'[data-with-open-in-editor-link-import-trace]'
)
const collapsedFrameworkGroups = await browser.elementsByCss(
'[data-with-open-in-editor-link-import-trace]'
)
for (const collapsedFrameworkButton of collapsedFrameworkGroups) {
await collapsedFrameworkButton.click()
}
}
describe('Error overlay - editor links', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
it('should be possible to open source file on build error', async () => {
let editorRequestsCount = 0
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
import Component from '../index'
export default function Page() {
return <Component />
}
`,
],
]),
undefined,
{
beforePageLoad(page) {
page.route('**/__nextjs_launch-editor**', (route) => {
editorRequestsCount += 1
route.fulfill()
})
},
}
)
const { session, browser } = sandbox
await session.patch(
'index.js',
outdent`
import { useState } from 'react'
export default () => 'hello world'
`
)
// Ensure the Next Logo is not loading. This is to assert that the build did stop.
await retry(async () => {
const loaded = await browser.eval(() => {
return Boolean(
[].slice
.call(document.querySelectorAll('nextjs-portal'))
.find((p) => {
const badge = p.shadowRoot.querySelector('[data-next-badge]')
// Check if badge exists and is not showing any loading status
return (
badge &&
(badge.getAttribute('data-status') === 'none' ||
!badge.getAttribute('data-status'))
)
})
)
})
expect(loaded).toBe(true)
})
await session.waitForRedbox()
await clickSourceFile(browser)
await check(() => editorRequestsCount, /1/)
})
;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)(
'opening links in import traces',
() => {
it('should be possible to open import trace files on RSC parse error', async () => {
let editorRequestsCount = 0
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
import Component from '../index'
export default function Page() {
return <Component />
}
`,
],
['mod1.js', "import './mod2.js'"],
['mod2.js', '{{{{{'],
]),
undefined,
{
beforePageLoad(page) {
page.route('**/__nextjs_launch-editor**', (route) => {
editorRequestsCount += 1
route.fulfill()
})
},
}
)
const { session, browser } = sandbox
await session.patch(
'index.js',
outdent`
import './mod1'
export default () => 'hello world'
`
)
await session.waitForRedbox()
await clickImportTraceFiles(browser)
await check(() => editorRequestsCount, /4/)
})
it('should be possible to open import trace files on module not found error', async () => {
let editorRequestsCount = 0
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
import Component from '../index'
export default function Page() {
return <Component />
}
`,
],
['mod1.js', "import './mod2.js'"],
['mod2.js', 'import "boom"'],
]),
undefined,
{
beforePageLoad(page) {
page.route('**/__nextjs_launch-editor**', (route) => {
editorRequestsCount += 1
route.fulfill()
})
},
}
)
const { session, browser } = sandbox
await session.patch(
'index.js',
outdent`
import './mod1'
export default () => 'hello world'
`
)
await session.waitForRedbox()
await clickImportTraceFiles(browser)
await check(() => editorRequestsCount, /3/)
})
}
)
})

View File

@@ -0,0 +1,66 @@
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { createSandbox } from 'development-sandbox'
import { outdent } from 'outdent'
describe('Error overlay - error message urls', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
it('should be possible to click url in build error', async () => {
await using sandbox = await createSandbox(next)
const { session, browser } = sandbox
const content = await next.readFile('app/page.js')
await session.patch(
'app/page.js',
content + '\nexport function getServerSideProps() {}'
)
await session.waitForRedbox()
const link = await browser.elementByCss(
'[data-nextjs-terminal] a, [data-nextjs-codeframe] a'
)
const text = await link.text()
const href = await link.getAttribute('href')
expect(text).toEqual(
'https://nextjs.org/docs/app/building-your-application/data-fetching'
)
expect(href).toEqual(
'https://nextjs.org/docs/app/building-your-application/data-fetching'
)
})
it('should be possible to click url in runtime error', async () => {
await using sandbox = await createSandbox(
next,
new Map([
[
'app/page.js',
outdent`
'use client'
export default function Page() {
return typeof window === 'undefined' ? 'HELLO' : 'WORLD'
}
`,
],
])
)
const { session, browser } = sandbox
await session.openRedbox()
const link = await browser.elementByCss('#nextjs__container_errors__link a')
const text = await link.text()
const href = await link.getAttribute('href')
expect(text).toEqual(
'https://nextjs.org/docs/messages/react-hydration-error'
)
expect(href).toEqual(
'https://nextjs.org/docs/messages/react-hydration-error'
)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,215 @@
A few years ago I tweeted a simple guideline that
[challenged conventional wisdom](https://twitter.com/swyx/status/1261202288476971008)
and became the norm for many people learning
[how to test their frontends](https://kentcdodds.com/blog/write-tests)
effectively.
Writing tests is an **investment**, and like any other investment,
it has to be evaluated in terms of its **return** and **risks** (e.g.:
[opportunity costs](https://en.wikipedia.org/wiki/Opportunity_cost)) it incurs in.
The "not too many" keys in on that. There is, after all, too much of a
good thing when it comes to spending time writing tests instead of
features. We can see an application of this insight also to availability
by [Google's popular SRE book](https://landing.google.com/sre/sre-book/chapters/embracing-risk/):
> You might expect Google to try to build 100% reliable services—ones that
> never fail. It turns out that past a certain point, however, increasing
> reliability is worse for a service (and its users) rather than better!
> Extreme reliability comes at a cost: maximizing stability limits how fast
> new features can be developed and how quickly products can be delivered to
> users, and dramatically increases their cost, which in turn reduces the
> numbers of features a team can afford to offer
In this essay I want to make the case that
**prioritizing end-to-end (E2E) testing** for the critical parts of
your app will reduce risk and give you the best return. Further, I'll show
how you can adopt this methodology in mere minutes.
## Why end-to-end?
In addition to integration tests, I want to now make the case that modern
deployment workflows in combination with serverless testing solutions can
now fully revert the conventional testing pyramid:
<Image
alt={
<span>
Martin Fowler's conventional{' '}
<a
href="https://martinfowler.com/articles/practical-test-pyramid.html#TheTestPyramid"
target="_blank"
rel="noreferrer"
>
testing pyramid
</a>
. What if 🐢 and 💲 went away?
</span>
}
src="/images/develop-preview-test/pyramid.jpg"
/>
As it turns out,
[focusing on the customer first](https://blog.aboutamazon.com/company-news/2016-letter-to-shareholders)
is the best way to run a business, but also the best way to ascertain
software quality.
You might be using the fanciest new compiler, the newest type system, but
won't do well if, after you ship, your site doesn't load in Chrome at all
because you forgot to send the right HTTP headers for your `.js` files.
Or you use features not compatible with Safari.
Modern cloud software has a great deal of complexity that cannot be
ascertained "in the lab", or by running artificial tests in your computer.
We normally deploy software to dozens of operating systems, mobile
devices, web browsers, networks of varying stability and performance… And
the environment is always changing, as we take more and more dependencies
on hosted services and third-party APIs.
Furthermore, software quality goes beyond correctness. It's not just about
doing "the right thing".
[Quality encapsulates reliability](https://twitter.com/garybernhardt/status/1007699924866093056)
(whether your program stays working correctly over time) and
[performance](https://craigmod.com/essays/fast_software/)
(it must do the right thing quickly).
## End-to-end made possible
First, back when I argued for focusing on integration in 2016 and made no
mention of end-to-end tests, we didn't yet have the deployment
infrastructure we have today.
With [Vercel](https://vercel.com/),
every push to git gets deployed and given an URL. This can be done cost
effectively by focusing on incremental computation, where the underlying
static files and serverless functions used to power a deployment are
mostly re-used from deploy to deploy.
<Image
alt={
<span>
An example of a PR automatically deployed{' '}
<a
target="_blank"
href="https://github.com/rauchg/blog/pull/35#issuecomment-570270061"
rel="noreferrer"
>
for this very blog
</a>
</span>
}
src="/images/develop-preview-test/github-comment.jpg"
/>
Just like GitHub is capable of keeping the history of your code forever,
so can Vercel. Because deploys are
[stateless](/2020/2019-in-review#static-is-the-new-dynamic),
the resources are only used on-demand and scale infinitely with traffic.
That gets rid of an obvious initial objection: that it would be difficult
or costly to perform end-to-end tests against a live individualized
version of your website. Instead of thinking about a single staging server
that end-to-end tests are run against, now you can have millions of
concurrent "staging" environments.
I say "staging" in quotes because in reality, you run tests against the
real production infrastructure, which has enormous reliability benefits.
Not only are certain
[features these days only enabled when SSL is on](https://developer.mozilla.org/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts),
but having every other production feature enabled, like brotli
compression and HTTP/2, reduces your risk of outages:
<Tweet
id="921185541487341568"
caption="The risks of environment differences, as explained by Kubernetes' creator"
/>
Having made staging instant, prod-like and cost-effective, the remaining
objection is that running the actual end-to-end tests would be expensive
or slow, going by the pyramid above.
However, the introduction of
[puppeteer](https://github.com/puppeteer/puppeteer),
Chrome's headless web browser, combined with serverless compute that can
instantly scale to thousands of concurrent executions (see this
[Stanford paper](https://stanford.edu/~sadjad/gg-paper.pdf)
for an example) are <b>now making E2E both fast and cheap</b>.
## End-to-end made easy
To show the **Develop -> Preview -> Test** workflow in action, I'm
going to demonstrate how easily I can add an E2E test for
[my open-source blog](https://github.com/rauchg/blog)
using [Vercel](https://vercel.com) in combination with
[Checkly](https://www.checklyhq.com)
on top of GitHub.
First, I'm going to define an end-to-end test to ascertain that my blog
always contains the string "Guillermo Rauch" in the browser title. As I'm
writing this, it's not the case, so I expect this to fail.
Checkly gives me full access to the Node and puppeteer APIs. Notice I
don't have to write any ceremony, setup or scheduling logic. I'm "inside"
the function that will later be invoked:
<Image
alt="The ENVIRONMENT_URL env variable will be populated with each preview deploy URL"
src="/images/develop-preview-test/checkly.jpg"
/>
Then, I installed the
[Checkly GitHub app](https://www.checklyhq.com/docs/cicd/github/#setting-up-your-github-integration)
and under the CI/CD tab of the check, I linked it to my
`rauchg/blog` repository.
I created a git branch where I introduced the `<title>`
tag for my blog, but on purpose
[I misspelt my name](https://github.com/rauchg/blog/pull/53/commits/b5c30eaef41944f36cf14e7d7f8be9be9953709f).
As soon as I created my pull request, the check was already failing. In
just a few seconds, I pushed, deployed, a headless browser simulating a
visitor ran, and my error was exposed:
break 1
<Image
alt="I can also configure my testing check as mandatory and make this PR unmergeable"
src="/images/develop-preview-test/failing-check.jpg"
/>
break 2
break 3
This happened with absolutely zero additional configuration. Vercel
supplied a URL, Checkly tested it, GitHub was notified. After pushing
again, the check re-runs automatically:
<Image
alt="Each commit gets its own deploy preview, and its own checks"
src="/images/develop-preview-test/commits.jpg"
/>
With my typo fixed, I'm free to merge. Merging to master will deploy my
changes to `rauchg.com` automatically.
<Image
alt="After pressing the green button, we go live. With confidence."
src="/images/develop-preview-test/github-green.jpg"
/>
## Conclusion
Notably, Checkly allows us to configure multiple locations in the world
that the tests get executed from, as well as
<b>invoking our checks over time</b>.
Leslie Lamport
[famously said](https://lamport.azurewebsites.net/pubs/distributed-system.txt)
that a distributed system is one where "the failure of a computer you
didn't even know existed can render your own computer unusable".
As our systems become more complex, with ever-changing platforms and
dependencies on other cloud systems, continuously testing just like our
users do is our best bet to tame the chaos.

View File

@@ -0,0 +1,13 @@
import Link from 'next/link'
export function A({ children, className = '', href, ...props }) {
return (
<Link
href={href}
className={`border-b text-gray-600 border-gray-300 transition-[border-color] hover:border-gray-600 dark:text-white dark:border-gray-500 dark:hover:border-white ${className}`}
{...props}
>
{children}
</Link>
)
}

View File

@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'
export function Blockquote({ children }: { children: ReactNode }) {
return (
<blockquote className="my-5 text-gray-500 pl-3 border-l-4 dark:border-gray-600 dark:text-gray-400">
{children}
</blockquote>
)
}

View File

@@ -0,0 +1,6 @@
export const Callout = ({ emoji = null, text }) => (
<div className="bg-gray-200 dark:bg-[#333] dark:text-gray-300 flex items-start p-3 my-6 text-base">
<span className="block w-6 text-center text-xl mr-2">{emoji}</span>
<span className="block grow">{text}</span>
</div>
)

View File

@@ -0,0 +1,12 @@
import Balancer from 'react-wrap-balancer'
import type { ReactNode } from 'react'
export function Caption({ children }: { children: ReactNode }) {
return (
<p className="text-xs my-3 font-mono text-gray-500 text-center leading-normal">
<Balancer>
<span className="[&>a]:post-link">{children}</span>
</Balancer>
</p>
)
}

View File

@@ -0,0 +1,28 @@
import { A } from './a'
import { P } from './p'
export const FootNotes = ({ children }) => (
<div className="text-base before:w-[200px] before:m-auto before:content[''] before:border-t before:border-gray-300 dark:before:border-[#444] before:block before:my-10">
{children}
</div>
)
export const Ref = ({ id }) => (
<a
href={`#f${id}`}
id={`s${id}`}
className="relative text-xs top-[-5px] no-underline"
>
[{id}]
</a>
)
export const FootNote = ({ id, children }) => (
<P>
{id}.{' '}
<A href={`#s${id}`} id={`f${id}`} className="no-underline">
^
</A>{' '}
{children}
</P>
)

View File

@@ -0,0 +1,5 @@
export function H1({ children }) {
return (
<h1 className="text-2xl font-bold mb-1 dark:text-gray-100">{children}</h1>
)
}

View File

@@ -0,0 +1,3 @@
export function H2({ children }) {
return <h2 className="group font-bold text-xl my-8 relative">{children}</h2>
}

View File

@@ -0,0 +1,3 @@
export function H3({ children }) {
return <h3 className="group font-bold text-lg my-8 relative">{children}</h3>
}

View File

@@ -0,0 +1,5 @@
export function HR() {
return (
<div className="my-8 text-center after:content-['﹡﹡﹡'] after:text-sm after:text-center after:inline" />
)
}

View File

@@ -0,0 +1,37 @@
import sizeOf from 'image-size'
import { join } from 'path'
import { readFile } from 'fs/promises'
import NextImage from 'next/image'
export async function Image({ src, alt = null, width = null, height = null }) {
if (!src.startsWith('data:') && (width === null || height === null)) {
let imageBuffer = null
if (src.startsWith('http')) {
imageBuffer = Buffer.from(
await fetch(src).then((res) => res.arrayBuffer())
)
} else {
imageBuffer = await readFile(
new URL(join(import.meta.url, '..', '..', '..', '..', 'public', src))
.pathname
)
}
;({ width, height } = sizeOf(imageBuffer))
}
return (
<span className="my-5 flex flex-col items-center">
{src.startsWith('data:') ? (
<img src={src} alt={alt} />
) : (
<NextImage width={width} height={height} alt={alt} src={src} />
)}
{alt && (
<span className="block font-mono text-xs mt-5 text-center">{alt}</span>
)}
</span>
)
}

View File

@@ -0,0 +1,19 @@
// we use `[ul_&]` prefix for the <UL> variety
export function LI({ children }) {
return (
<li
className={`
my-2
[ul_&]:relative
[ul_&]:pl-4
[ul_&]:before:text-gray-400
[ul_&]:before:content-['']
[ul_&]:before:mr-2
[ul_&]:before:absolute
[ul_&]:before:-ml-4
`}
>
{children}
</li>
)
}

View File

@@ -0,0 +1,3 @@
export function OL({ children }) {
return <ol className="my-5 list-decimal list-inside">{children}</ol>
}

View File

@@ -0,0 +1,3 @@
export function P({ children }) {
return <p className="my-5 [blockquote_&]:my-2">{children}</p>
}

View File

@@ -0,0 +1,24 @@
import { Caption } from './caption'
export const Snippet = ({ children, scroll = true, caption = null }) => (
<div className="my-6">
<pre
className={`
p-4
text-sm
bg-gray-800 text-white
dark:bg-[#222] dark:text-gray-300
${
scroll
? 'overflow-scroll'
: 'whitespace-pre-wrap break-all overflow-hidden'
}
`}
>
<code>{children}</code>
</pre>
{caption != null ? <Caption>{caption}</Caption> : null}
</div>
)

View File

@@ -0,0 +1,30 @@
import type { ReactNode } from 'react'
import { Tweet as ReactTweet, TweetComponents } from 'react-tweet'
import Image from 'next/image'
import { Caption } from './caption'
const components: TweetComponents = {
AvatarImg: (props) => <Image {...props} />,
MediaImg: (props) => <Image {...props} fill unoptimized />,
}
// we import this globally in the top-most layout.tsx file
// until Next.js lands suspense-y CSS support
// import "./tweet.css";
interface TweetArgs {
id: string
caption: ReactNode
}
export async function Tweet({ id, caption }: TweetArgs) {
return (
<div className="my-6">
<div className="flex justify-center">
<ReactTweet id={id} components={components} />
</div>
{caption && <Caption>{caption}</Caption>}
</div>
)
}

View File

@@ -0,0 +1,3 @@
export function UL({ children }) {
return <ul className="my-5 list-none list-inside">{children}</ul>
}

View File

@@ -0,0 +1,8 @@
export default function Root({ children }) {
return (
<html>
<head />
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,43 @@
import { A as a } from 'app/(post)/components/a'
import { P as p } from 'app/(post)/components/p'
import { H1 as h1 } from 'app/(post)/components/h1'
import { H2 as h2 } from 'app/(post)/components/h2'
import { H3 as h3 } from 'app/(post)/components/h3'
import { OL as ol } from 'app/(post)/components/ol'
import { UL as ul } from 'app/(post)/components/ul'
import { LI as li } from 'app/(post)/components/li'
import { HR as hr } from 'app/(post)/components/hr'
import { Tweet } from 'app/(post)/components/tweet'
import { Image } from 'app/(post)/components/image'
import { Snippet } from 'app/(post)/components/snippet'
import { Caption } from 'app/(post)/components/caption'
import { Callout } from 'app/(post)/components/callout'
import { Ref, FootNotes, FootNote } from 'app/(post)/components/footnotes'
import { Blockquote as blockquote } from 'app/(post)/components/blockquote'
const components = {
a,
h1,
h2,
h3,
p,
ol,
ul,
li,
hr,
pre: Snippet,
img: Image,
blockquote,
Tweet,
Image,
Snippet,
Caption,
Callout,
Ref,
FootNotes,
FootNote,
}
export function useMDXComponents() {
return components
}

View File

@@ -0,0 +1,16 @@
const withMDX = require('@next/mdx')()
module.exports = withMDX({
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
experimental: {
mdxRs: true,
},
images: {
domains: [
'pbs.twimg.com',
'abs.twimg.com',
'm.media-amazon.com',
'images-na.ssl-images-amazon.com',
],
},
})

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,20 @@
const plugin = require('tailwindcss/plugin')
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ['./app/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [
plugin(function ({ addVariant }) {
// this class is applied to `html` by `app/theme-efect.ts`, similar
// to how `dark:` gets enabled
addVariant('theme-system', '.theme-system &')
}),
],
future: {
hoverOnlyWhenSupported: true,
},
}

View File

@@ -0,0 +1,9 @@
'use client'
const isClient = typeof window !== 'undefined'
export default function Component() {
return (
<div>
<p>{isClient ? 'client' : 'server'}</p>
</div>
)
}

View File

@@ -0,0 +1,8 @@
export default function RootLayout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,8 @@
import Component from './component'
export default function Mismatch() {
return (
<main>
<Component />
</main>
)
}

View File

@@ -0,0 +1,5 @@
'use client'
import Component from '../../index'
export default function Page() {
return <Component />
}

View File

@@ -0,0 +1,8 @@
export default function RootLayout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,5 @@
'use client'
import Component from '../index'
export default function Page() {
return <Component />
}

View File

@@ -0,0 +1,4 @@
import Component from '../../index'
export default function Page() {
return <Component />
}

View File

@@ -0,0 +1 @@
export default () => 'new sandbox'

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -0,0 +1,17 @@
'use client'
export default function Page() {
return (
<p>
<span>
<span>
<span>
<span>
<p>hello world</p>
</span>
</span>
</span>
</span>
</p>
)
}

View File

@@ -0,0 +1,13 @@
'use client'
export default function Page() {
return (
<div>
<div>
<p>
<div>Nested div under p tag</div>
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
'use client'
const isClient = typeof window !== 'undefined'
export default function Mismatch() {
return <div className="parent">{isClient && <main className="only" />}</div>
}

View File

@@ -0,0 +1,7 @@
'use client'
const isServer = typeof window === 'undefined'
export default function Mismatch() {
return <div className="parent">{isServer && <main className="only" />}</div>
}

View File

@@ -0,0 +1,17 @@
'use client'
import React from 'react'
const isClient = typeof window !== 'undefined'
export default function Mismatch() {
return (
<div className="parent">
<React.Suspense fallback={<p>Loading...</p>}>
<header className="1" />
{isClient && <main className="second" />}
<footer className="3" />
</React.Suspense>
</div>
)
}

View File

@@ -0,0 +1,13 @@
'use client'
const isClient = typeof window !== 'undefined'
export default function Mismatch() {
return (
<div className="parent">
<header className="1" />
{isClient && 'second'}
<footer className="3" />
</div>
)
}

View File

@@ -0,0 +1,11 @@
'use client'
export default function Page() {
return (
<table>
<tbody>
<tr>test</tr>
</tbody>
</table>
)
}

View File

@@ -0,0 +1,7 @@
'use client'
const isServer = typeof window === 'undefined'
export default function Mismatch() {
return <div className="parent">{isServer && 'only'}</div>
}

View File

@@ -0,0 +1,10 @@
'use client'
export default function Page() {
return (
<table>
{' '}
<tbody></tbody>
</table>
)
}

View File

@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,9 @@
'use client'
export default function Page() {
return (
<p>
<p>Nested p tags</p>
</p>
)
}

View File

@@ -0,0 +1,11 @@
'use client'
const isClient = typeof window !== 'undefined'
export default function Mismatch() {
return (
<div className="parent">
<main className="child">{isClient ? 'client' : 'server'}</main>
</div>
)
}

View File

@@ -0,0 +1,9 @@
'use client'
export default function Page() {
return (
<div>
<tr></tr>
</div>
)
}

View File

@@ -0,0 +1,13 @@
'use client'
import { useId } from 'react'
export default function Page() {
let id = useId()
return (
<div className="parent" data-id={id}>
Hello World
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return 'page'
}

View File

@@ -0,0 +1,13 @@
'use client'
import { ReactNode } from 'react'
const isServer = typeof window === 'undefined'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html {...(isServer ? { className: 'server-html' } : undefined)}>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,14 @@
import Script from 'next/script'
import { ReactNode } from 'react'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
<Script
src="https://example.com/script.js"
strategy="beforeInteractive"
/>
</html>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <div>Hello World</div>
}

View File

@@ -0,0 +1,15 @@
'use client'
import dynamic from 'next/dynamic'
const Component = dynamic(async () => () => <p>hello dynamic world</p>, {
ssr: false,
})
export default function Page() {
return (
<>
<Component />
</>
)
}

View File

@@ -0,0 +1,7 @@
'use client'
// export function getServerSideProps() { { props: {} } }
export default function Page() {
return 'client-gssp'
}

View File

@@ -0,0 +1,7 @@
'use client'
// export function getStaticProps() { return { props: {} }}
export default function Page() {
return 'client-gsp'
}

View File

@@ -0,0 +1,9 @@
'use client'
export default function Page() {
return 'client-metadata'
}
// export const metadata = { title: 'client-metadata' }
// export async function generateMetadata() { return { title: 'client-metadata' } }

View File

@@ -0,0 +1,7 @@
'use client'
import ServerOnly from './server-only-lib'
export default function page() {
return <ServerOnly />
}

View File

@@ -0,0 +1,5 @@
// import 'server-only'
export default function ServerOnlyLib() {
return 'server-only-lib'
}

View File

@@ -0,0 +1,4 @@
// import { useState } from 'react'
export default function Component() {
return <div>Component</div>
}

View File

@@ -0,0 +1,5 @@
import Component from './component'
export default function Page() {
return <Component />
}

View File

@@ -0,0 +1,8 @@
export default function RootLayout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,11 @@
export default function page(props) {
return <p>mutate</p>
}
export async function generateMetadata(props, parent) {
const parentMetadata = await parent
return {
...parentMetadata,
}
}

View File

@@ -0,0 +1,5 @@
// import { Component } from 'react'
export default function Page() {
return null
}

View File

@@ -0,0 +1,5 @@
// import 'client-only'
export default function ClientOnlyLib() {
return 'client-only-lib'
}

View File

@@ -0,0 +1,5 @@
import ClientOnly from './client-only-lib'
export default function page() {
return <ClientOnly />
}

View File

@@ -0,0 +1,3 @@
'use client'
export default function Error() {}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <p>Hello world</p>
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return 'server-metadata'
}
export const metadata = { title: 'server-metadata' }
// export async function generateMetadata() { return { title: 'server-metadata' } }

View File

@@ -0,0 +1,8 @@
'use client'
import { cacheLife } from 'next/cache'
console.log({ cacheLife })
export default function Page() {
return null
}

View File

@@ -0,0 +1,8 @@
'use client'
import { cacheTag } from 'next/cache'
console.log({ cacheTag })
export default function Page() {
return null
}

View File

@@ -0,0 +1,8 @@
'use client'
import { revalidatePath } from 'next/cache'
console.log({ revalidatePath })
export default function Page() {
return null
}

View File

@@ -0,0 +1,8 @@
'use client'
import { revalidateTag } from 'next/cache'
console.log({ revalidateTag })
export default function Page() {
return null
}

View File

@@ -0,0 +1,8 @@
'use client'
import { unstable_cache } from 'next/cache'
console.log({ unstable_cache })
export default function Page() {
return null
}

View File

@@ -0,0 +1,8 @@
'use client'
import { unstable_noStore } from 'next/cache'
console.log({ unstable_noStore })
export default function Page() {
return null
}

View File

@@ -0,0 +1,10 @@
'use client'
// This currently bypasses the compile time import checks from `react_server_components.rs`,
// but we should still catch it during import resolution.
const { whatever } = await import('next/root-params')
console.log({ whatever })
export default function Page() {
return null
}

View File

@@ -0,0 +1,8 @@
'use client'
import { whatever } from 'next/root-params'
console.log({ whatever })
export default function Page() {
return null
}

View File

@@ -0,0 +1,5 @@
import { whatever } from 'next/root-params'
export default async function Page() {
return <>{await whatever()}</>
}

View File

@@ -0,0 +1,3 @@
export function Page() {
return <p>Page</p>
}

View File

@@ -0,0 +1,7 @@
import { Component } from 'react'
console.log({ Component })
export default function Page() {
return null
}

View File

@@ -0,0 +1,7 @@
import { createContext } from 'react'
console.log({ createContext })
export default function Page() {
return null
}

View File

@@ -0,0 +1,7 @@
import { createFactory } from 'react'
console.log({ createFactory })
export default function Page() {
return null
}

View File

@@ -0,0 +1,7 @@
import { PureComponent } from 'react'
console.log({ PureComponent })
export default function Page() {
return null
}

View File

@@ -0,0 +1,7 @@
import { useActionState } from 'react'
console.log({ useActionState })
export default function Page() {
return null
}

Some files were not shown because too many files have changed in this diff Show More