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,148 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`next.rs api should allow to write app Node.js page to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write app Node.js page to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write app Node.js page to disk: rsc diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write app Node.js page to disk: rsc issues 1`] = `[]`;
exports[`next.rs api should allow to write app Node.js route to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write app Node.js route to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write app edge page to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write app edge page to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write app edge page to disk: rsc diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write app edge page to disk: rsc issues 1`] = `[]`;
exports[`next.rs api should allow to write app edge route to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write app edge route to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write pages Node.js api to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write pages Node.js api to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write pages Node.js page to disk: data diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write pages Node.js page to disk: data issues 1`] = `[]`;
exports[`next.rs api should allow to write pages Node.js page to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write pages Node.js page to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write pages edge api to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write pages edge api to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write pages edge page to disk: data diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write pages edge page to disk: data issues 1`] = `[]`;
exports[`next.rs api should allow to write pages edge page to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write pages edge page to disk: issues 1`] = `[]`;
exports[`next.rs api should allow to write root page to disk: data diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write root page to disk: data issues 1`] = `[]`;
exports[`next.rs api should allow to write root page to disk: diagnostics 1`] = `[]`;
exports[`next.rs api should allow to write root page to disk: issues 1`] = `[]`;
exports[`next.rs api should detect the correct routes: diagnostics 1`] = `
[
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"modularizeImports": "true",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"persistentCaching": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"platform-triplet": "true",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"skipProxyUrlNormalize": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"skipTrailingSlashRedirect": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"swcEmotion": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"swcReactRemoveProperties": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"swcRelay": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"swcRemoveConsole": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"swcStyledComponents": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"transpilePackages": "false",
},
},
{
"category": "NextFeatureTelemetry_category_tbd",
"name": "EVENT_BUILD_FEATURE_USAGE",
"payload": {
"turbotrace": "false",
},
},
]
`;
exports[`next.rs api should detect the correct routes: issues 1`] = `[]`;

View File

@@ -0,0 +1,366 @@
import http from 'http'
import { join } from 'path'
import webdriver from 'next-webdriver'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import { fetchViaHTTP, findPort, retry } from 'next-test-utils'
async function createHostServer() {
const server = http.createServer((req, res) => {
res.end(`
<html>
<head>
<title>testing cross-site</title>
</head>
<body></body>
</html>
`)
})
const port = await findPort()
await new Promise<void>((res) => {
server.listen(port, () => res())
})
return {
server,
port,
}
}
describe.each([['', '/docs']])(
'allowed-dev-origins, basePath: %p',
(basePath: string) => {
let next: NextInstance
describe('warn mode', () => {
beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'misc/pages')),
public: new FileRef(join(__dirname, 'misc/public')),
},
nextConfig: {
basePath,
},
})
// render 404 page to generate
// "/_next/static/chunks/pages/_app.js"
// we need this because not found static assets
// served as plain text 404 instead of HTML.
await next.render('/404')
await retry(async () => {
// make sure host server is running
const res = await fetchViaHTTP(
next.appPort,
'/_next/static/chunks/pages/_app.js'
)
expect(res.status).toBe(200)
})
})
afterAll(() => next.destroy())
it('should warn about WebSocket from cross-site', async () => {
const { server, port } = await createHostServer()
try {
const websocketSnippet = `(() => {
const statusEl = document.createElement('p')
statusEl.id = 'status'
document.querySelector('body').appendChild(statusEl)
const ws = new WebSocket("${next.url}/_next/webpack-hmr")
ws.addEventListener('error', (err) => {
statusEl.innerText = 'error'
})
ws.addEventListener('open', () => {
statusEl.innerText = 'connected'
})
})()`
// ensure direct port with mismatching port is blocked
const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
await browser.eval(websocketSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe(
'connected'
)
})
// ensure different host is blocked
await browser.get(`https://example.vercel.sh/`)
await browser.eval(websocketSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe(
'connected'
)
})
expect(next.cliOutput).toContain('Cross origin request detected from')
} finally {
server.close()
}
})
it('should warn about loading scripts from cross-site', async () => {
const { server, port } = await createHostServer()
try {
const scriptSnippet = `(() => {
const statusEl = document.createElement('p')
statusEl.id = 'status'
document.querySelector('body').appendChild(statusEl)
const script = document.createElement('script')
script.src = "${next.url}/_next/static/chunks/pages/_app.js"
script.onerror = (err) => {
statusEl.innerText = 'error'
}
script.onload = () => {
statusEl.innerText = 'connected'
}
document.querySelector('body').appendChild(script)
})()`
// ensure direct port with mismatching port is blocked
const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
await browser.eval(scriptSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe(
'connected'
)
})
// ensure different host is blocked
await browser.get(`https://example.vercel.sh/`)
await browser.eval(scriptSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe(
'connected'
)
})
expect(next.cliOutput).toContain('Cross origin request detected from')
} finally {
server.close()
}
})
it('should warn about loading internal middleware from cross-site', async () => {
const { server, port } = await createHostServer()
try {
const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
const middlewareSnippet = `(() => {
const statusEl = document.createElement('p')
statusEl.id = 'status'
document.querySelector('body').appendChild(statusEl)
const xhr = new XMLHttpRequest()
xhr.open('GET', '${next.url}/__nextjs_error_feedback?errorCode=0&wasHelpful=true', true)
xhr.send()
xhr.onload = () => {
statusEl.innerText = "OK"
}
xhr.onerror = () => {
statusEl.innerText = "Unauthorized"
}
})()`
await browser.eval(middlewareSnippet)
await retry(async () => {
// TODO: These requests seem to be blocked regardless of our handling only when running with Turbopack
// Investigate why this is the case
if (!process.env.IS_TURBOPACK_TEST) {
expect(await browser.elementByCss('#status').text()).toBe('OK')
}
expect(next.cliOutput).toContain(
'Cross origin request detected from'
)
})
} finally {
server.close()
}
})
})
describe('block mode', () => {
beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'misc/pages')),
public: new FileRef(join(__dirname, 'misc/public')),
},
nextConfig: {
basePath,
allowedDevOrigins: ['localhost'],
},
})
// render 404 page to generate
// "/_next/static/chunks/pages/_app.js"
// since we haven't built any paths by this point
// causing this chunk to not be written to disk yet
await next.render('/404')
await retry(async () => {
// make sure host server is running
const res = await fetchViaHTTP(
next.appPort,
'/_next/static/chunks/pages/_app.js'
)
expect(res.status).toBe(200)
})
})
afterAll(() => next.destroy())
it('should not allow dev WebSocket from cross-site', async () => {
const { server, port } = await createHostServer()
try {
const websocketSnippet = `(() => {
const statusEl = document.createElement('p')
statusEl.id = 'status'
document.querySelector('body').appendChild(statusEl)
const ws = new WebSocket("${next.url}/_next/webpack-hmr")
ws.addEventListener('error', (err) => {
statusEl.innerText = 'error'
})
ws.addEventListener('open', () => {
statusEl.innerText = 'connected'
})
})()`
// ensure direct port with mismatching port is blocked
const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
await browser.eval(websocketSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe('error')
})
// ensure different host is blocked
await browser.get(`https://example.vercel.sh/`)
await browser.eval(websocketSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe('error')
})
} finally {
server.close()
}
})
it('should not allow loading scripts from cross-site', async () => {
const { server, port } = await createHostServer()
try {
const scriptSnippet = `(() => {
const statusEl = document.createElement('p')
statusEl.id = 'status'
document.querySelector('body').appendChild(statusEl)
const script = document.createElement('script')
script.src = "${next.url}/_next/static/chunks/pages/_app.js"
script.onerror = (err) => {
statusEl.innerText = 'error'
}
script.onload = () => {
statusEl.innerText = 'connected'
}
document.querySelector('body').appendChild(script)
})()`
// ensure direct port with mismatching port is blocked
const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
await browser.eval(scriptSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe('error')
})
// ensure different host is blocked
await browser.get(`https://example.vercel.sh/`)
await browser.eval(scriptSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe('error')
})
} finally {
server.close()
}
})
it('should not allow loading internal middleware from cross-site', async () => {
const { server, port } = await createHostServer()
try {
const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
const middlewareSnippet = `(() => {
const statusEl = document.createElement('p')
statusEl.id = 'status'
document.querySelector('body').appendChild(statusEl)
const xhr = new XMLHttpRequest()
xhr.open('GET', '${next.url}/__nextjs_error_feedback?errorCode=0&wasHelpful=true', true)
xhr.send()
xhr.onload = () => {
statusEl.innerText = "OK"
}
xhr.onerror = () => {
statusEl.innerText = "Unauthorized"
}
})()`
await browser.eval(middlewareSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe(
'Unauthorized'
)
})
} finally {
server.close()
}
})
it('should load images regardless of allowed origins', async () => {
const { server, port } = await createHostServer()
try {
const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
const imageSnippet = `(() => {
const statusEl = document.createElement('p')
statusEl.id = 'status'
document.querySelector('body').appendChild(statusEl)
const image = document.createElement('img')
image.src = "${next.url}/_next/image?url=%2Fimage.png&w=256&q=75"
document.querySelector('body').appendChild(image)
image.onload = () => {
statusEl.innerText = 'OK'
}
image.onerror = () => {
statusEl.innerText = 'Unauthorized'
}
})()`
await browser.eval(imageSnippet)
await retry(async () => {
expect(await browser.elementByCss('#status').text()).toBe('OK')
})
} finally {
server.close()
}
})
})
}
)

View File

@@ -0,0 +1,50 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'
import { check, retry } from 'next-test-utils'
describe('asset-prefix', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
})
it('should load the app properly without reloading', async () => {
const browser = await next.browser('/')
await browser.eval(`window.__v = 1`)
expect(await browser.elementByCss('#text').text()).toBe('Hello World')
await check(async () => {
const logs = await browser.log()
const hasError = logs.some((log) =>
log.message.includes('Failed to fetch')
)
return hasError ? 'error' : 'success'
}, 'success')
expect(await browser.eval(`window.__v`)).toBe(1)
})
it('should navigate to another page without hard navigating', async () => {
const browser = await next.browser('/')
await browser.eval(`window.__v = 1`)
await browser.elementByCss('[href="/page2"]').click()
await retry(async () => {
expect(await browser.elementByCss('div').text()).toBe('Page 2')
})
expect(await browser.eval(`window.__v`)).toBe(1)
})
describe('rewrites', () => {
it('rewrites that do not start with assetPrefix should still work', async () => {
const res = await next.fetch('/not-custom-asset-prefix/api/test-json', {})
expect(res.status).toBe(200)
expect(await res.text()).toBe('{"message":"test"}')
})
it('should respect rewrites that start with assetPrefix', async () => {
const res = await next.fetch('/custom-asset-prefix/api/test-json', {})
expect(res.status).toBe(200)
expect(await res.text()).toBe('{"message":"test"}')
})
})
})

View File

@@ -0,0 +1,30 @@
const ASSET_PREFIX = '/custom-asset-prefix'
module.exports = {
assetPrefix: ASSET_PREFIX,
i18n: {
locales: ['en-US'],
defaultLocale: 'en-US',
},
async rewrites() {
return {
beforeFiles: [
{
source: `/:locale/${ASSET_PREFIX}/_next/:path*`,
destination: `/${ASSET_PREFIX}/_next/:path*`,
locale: false,
},
],
afterFiles: [
{
source: `/${ASSET_PREFIX}/:path*`,
destination: '/:path*',
},
{
source: '/not-custom-asset-prefix/:path*',
destination: '/:path*',
},
],
}
},
}

View File

@@ -0,0 +1,3 @@
export default function handler(req, res) {
res.status(200).json({ message: 'test' })
}

View File

@@ -0,0 +1,11 @@
import Link from 'next/link'
import React from 'react'
export default function Page() {
return (
<>
<div id="text">Hello World</div>
<Link href="/page2">Page 2</Link>
</>
)
}

View File

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

View File

@@ -0,0 +1,34 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'
import { waitForNoRedbox } from 'next-test-utils'
// This is implemented in Turbopack, but Turbopack doesn't log the module count.
;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)(
'Skipped in Turbopack',
() => {
describe('optimizePackageImports - mui', () => {
const { next } = nextTestSetup({
env: {
NEXT_TEST_MODE: '1',
},
files: join(__dirname, 'fixture'),
dependencies: {
'@mui/material': '5.15.15',
'@emotion/react': '11.11.1',
'@emotion/styled': '11.11.0',
},
})
it('should support MUI', async () => {
// Ensure that MUI is working
const $ = await next.render$('/mui')
expect($('#button').text()).toContain('button')
expect($('#typography').text()).toContain('typography')
const browser = await next.browser('/mui')
await waitForNoRedbox(browser)
})
})
}
)

View File

@@ -0,0 +1,25 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'
import { waitForNoRedbox } from 'next-test-utils'
describe('optimizePackageImports - mui', () => {
const { next } = nextTestSetup({
env: {
NEXT_TEST_MODE: '1',
},
files: join(__dirname, 'fixture-tremor'),
dependencies: {
'@headlessui/react': '^1.7.18',
'@headlessui/tailwindcss': '^0.2.0',
'@remixicon/react': '^4.2.0',
'@tremor/react': '^3.14.1',
},
})
it('should work', async () => {
// Without barrel optimization, the reproduction breaks.
const browser = await next.browser('/')
await waitForNoRedbox(browser)
})
})

View File

@@ -0,0 +1,44 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'
// This is implemented in Turbopack, but Turbopack doesn't log the module count.
;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)(
'Skipped in Turbopack',
() => {
describe('optimizePackageImports - basic', () => {
const { next } = nextTestSetup({
env: {
NEXT_TEST_MODE: '1',
},
files: join(__dirname, 'fixture'),
dependencies: {
'lucide-react': '0.264.0',
'@headlessui/react': '1.7.17',
'@heroicons/react': '2.0.18',
'@visx/visx': '3.3.0',
'recursive-barrel': '1.0.0',
},
})
it('should handle recursive wildcard exports', async () => {
const html = await next.render('/recursive')
expect(html).toContain('<h1>42</h1>')
})
it('should support visx', async () => {
const html = await next.render('/visx')
expect(html).toContain('<linearGradient')
})
it('should not break "use client" directive in optimized packages', async () => {
const html = await next.render('/client')
expect(html).toContain('this is a client component')
})
it('should support "use client" directive in barrel file', async () => {
const html = await next.render('/client-boundary')
expect(html).toContain('<button>0</button>')
})
})
}
)

View File

@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,5 @@
import { Card } from '@tremor/react'
export default function Example() {
return <Card></Card>
}

View File

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
export default nextConfig

View File

@@ -0,0 +1,5 @@
import { Button } from 'my-client-lib'
export default function Page() {
return <Button />
}

View File

@@ -0,0 +1,9 @@
import { Client } from 'my-lib'
export default function Page() {
return (
<h1>
<Client />
</h1>
)
}

View File

@@ -0,0 +1,23 @@
// The barrel file shouldn't be transformed many times if it's imported multiple times.
import { ImageIcon } from 'lucide-react'
import { IceCream } from 'lucide-react'
import { AccessibilityIcon } from 'lucide-react'
import { VariableIcon } from 'lucide-react'
import { Table2Icon } from 'lucide-react'
import { Package2Icon } from 'lucide-react'
import { ZapIcon } from 'lucide-react'
export default function Page() {
return (
<>
<ImageIcon />
<IceCream />
<AccessibilityIcon />
<VariableIcon />
<Table2Icon />
<Package2Icon />
<ZapIcon />
</>
)
}

View File

@@ -0,0 +1,12 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,13 @@
import { Button } from '@mui/material'
import { Typography } from '@mui/material'
export default function Page() {
return (
<div>
<Button id="button">button</Button>
<Typography id="typography" variant="h1">
typography
</Typography>
</div>
)
}

View File

@@ -0,0 +1,15 @@
import { Comp } from '../components/slow-component'
import { AcademicCapIcon as A } from '@heroicons/react/20/solid'
import { AcademicCapIcon as B } from '@heroicons/react/24/outline'
import { AcademicCapIcon as C } from '@heroicons/react/24/solid'
export default function Page() {
return (
<>
<A />
<B />
<C />
<Comp />
</>
)
}

View File

@@ -0,0 +1,7 @@
'use client'
import { b_8_7_6_4 } from 'recursive-barrel'
export default function Page() {
return <h1>{b_8_7_6_4}</h1>
}

View File

@@ -0,0 +1,5 @@
import { c } from 'my-lib'
export default function Page() {
return <h1>{c}</h1>
}

View File

@@ -0,0 +1,5 @@
import { foo } from 'my-lib'
export default function Page() {
return <h1>{foo}</h1>
}

View File

@@ -0,0 +1,77 @@
'use client'
import {
IceCream,
BackpackIcon,
LucideActivity,
Code,
Menu,
SortAsc,
SortAscIcon,
LucideSortDesc,
VerifiedIcon,
CurlyBraces,
Slash,
SquareGantt,
CircleSlashed,
SquareKanban,
SquareKanbanDashed,
Stars,
Edit,
Edit2,
LucideEdit3,
TextSelection,
createLucideIcon,
} from 'lucide-react'
import { Tab, RadioGroup, Transition } from '@headlessui/react'
export function Comp() {
globalThis.__noop__ = createLucideIcon
return (
<>
<IceCream />
<BackpackIcon />
<LucideActivity />
<Code />
<Menu />
<SortAsc />
<SortAscIcon />
<LucideSortDesc />
<VerifiedIcon />
<CurlyBraces />
<Slash />
<SquareGantt />
<CircleSlashed />
<SquareKanban />
<SquareKanbanDashed />
<Stars />
<Edit />
<Edit2 />
<LucideEdit3 />
<TextSelection />
<Tab.Group>
<Tab.List>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
<Tab.Panel>Content 3</Tab.Panel>
</Tab.Panels>
</Tab.Group>
<RadioGroup>
<RadioGroup.Label>Plan</RadioGroup.Label>
<RadioGroup.Option value="startup">
{({ checked }) => <span>{checked ? 'checked' : ''} Startup</span>}
</RadioGroup.Option>
<RadioGroup.Option value="business">
{({ checked }) => <span>{checked ? 'checked' : ''} Business</span>}
</RadioGroup.Option>
</RadioGroup>
<Transition show>I will fade in and out</Transition>
</>
)
}

View File

@@ -0,0 +1,6 @@
module.exports = {
transpilePackages: ['my-client-lib'],
experimental: {
optimizePackageImports: ['my-lib', 'recursive-barrel', 'my-client-lib'],
},
}

View File

@@ -0,0 +1,6 @@
import { useState } from 'react'
export function Button() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}

View File

@@ -0,0 +1,3 @@
'use client'
export { Button } from './button'

View File

@@ -0,0 +1,2 @@
export * from './b'
export { foo } from './foo'

View File

@@ -0,0 +1 @@
export * from './c'

View File

@@ -0,0 +1 @@
export const c = 42

View File

@@ -0,0 +1,5 @@
'use client'
export function Client() {
return 'this is a client component'
}

View File

@@ -0,0 +1 @@
export const foo = 'foo'

View File

@@ -0,0 +1,2 @@
export * from './a'
export * from './client'

View File

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

View File

@@ -0,0 +1,5 @@
import { b_8_3_1_1 as v } from 'recursive-barrel'
export default function Page() {
return <h1>{v}</h1>
}

View File

@@ -0,0 +1,10 @@
import { Gradient } from '@visx/visx'
export default function Page() {
return (
<svg width={400} height={400}>
<Gradient.GradientPinkBlue id="g" from="red" to="blue" />
<rect width={400} height={400} fill="url(#g)" />
</svg>
)
}

View File

@@ -0,0 +1,56 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
describe('useDefineForClassFields SWC option', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
dependencies: {
mobx: '6.3.7',
typescript: 'latest',
'@types/react': 'latest',
'@types/node': 'latest',
'mobx-react': '7.2.1',
},
})
it('tsx should compile with useDefineForClassFields enabled', async () => {
const browser = await next.browser('/')
await browser.elementByCss('#action').click()
await check(
() => browser.elementByCss('#name').text(),
/this is my name: next/
)
})
it("Initializes resident to undefined after the call to 'super()' when with useDefineForClassFields enabled", async () => {
const browser = await next.browser('/animal')
expect(await browser.elementByCss('#dog').text()).toBe('undefined')
expect(await browser.elementByCss('#dogDecl').text()).toBe('dog')
})
async function matchLogs$(browser) {
let data_foundLog = false
let name_foundLog = false
const browserLogs = await browser.log()
browserLogs.forEach((log) => {
if (log.message.includes('data changed')) {
data_foundLog = true
}
if (log.message.includes('name changed')) {
name_foundLog = true
}
})
return [data_foundLog, name_foundLog]
}
it('set accessors from base classes wont get triggered with useDefineForClassFields enabled', async () => {
const browser = await next.browser('/derived')
await matchLogs$(browser).then(([data_foundLog, name_foundLog]) => {
expect(data_foundLog).toBe(true)
expect(name_foundLog).toBe(false)
})
})
})

View File

@@ -0,0 +1,53 @@
import React from 'react'
interface Animal {
animalStuff: any
}
interface Dog extends Animal {
dogStuff: any
}
class AnimalHouse {
resident: Animal
constructor(animal: Animal) {
this.resident = animal
}
}
class DogHouse extends AnimalHouse {
// Initializes 'resident' to 'undefined'
// after the call to 'super()' when
// using 'useDefineForClassFields'!
// @ts-ignore
resident: Dog
// useless constructor only for type checker
/* eslint-disable @typescript-eslint/no-useless-constructor */
constructor(dog: Dog) {
super(dog)
}
}
class DogHouseWithDeclare extends AnimalHouse {
declare resident: Dog
// useless constructor only for type checker
/* eslint-disable @typescript-eslint/no-useless-constructor */
constructor(dog: Dog) {
super(dog)
}
}
export default function AnimalView() {
const dog = new DogHouse({
animalStuff: 'animal',
dogStuff: 'dog',
})
const dogDeclare = new DogHouseWithDeclare({
animalStuff: 'animal',
dogStuff: 'dog',
})
return (
<>
<div id={'dog'}>{String(dog.resident)}</div>
<div id={'dogDecl'}>{dogDeclare.resident?.dogStuff}</div>
</>
)
}

View File

@@ -0,0 +1,33 @@
import React from 'react'
class Base {
set data(value: number) {
console.log('data changed to ' + value)
}
set name(value: number) {
console.log('name changed to ' + value)
}
}
class Derived extends Base {
// No longer triggers a 'console.log'
// when using 'useDefineForClassFields'.
// @ts-ignore
name = 10
constructor() {
super()
//triggers a 'console.log'
this.data = 10
}
}
export default function DerivedView() {
const obj = new Derived()
return (
<>
<div id={'data'}>{obj.data}</div>
<div id={'name'}>{obj.name}</div>
</>
)
}

View File

@@ -0,0 +1,39 @@
// @ts-ignore
import { makeObservable, observable } from 'mobx'
// @ts-ignore
import { observer } from 'mobx-react'
import React from 'react'
class Person {
//Declarations are initialized with Object.defineProperty.
// @ts-ignore
name: string
constructor() {
//without useDefineForClassFields it will be error
makeObservable(this, {
name: observable,
})
}
}
const person = new Person()
const PersonView = observer(() => {
const changeName = () => {
person.name = 'next'
}
return (
<>
<div id="name">this is my name: {person.name}</div>
<button id="action" onClick={changeName}>
Change Name
</button>
</>
)
})
export default function Home() {
return <PersonView />
}

View File

@@ -0,0 +1,22 @@
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'
describe('emotion SWC option', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'fixture'),
dependencies: {
'@emotion/react': '11.10.4',
'@emotion/styled': '11.10.4',
'@emotion/cache': '11.10.3',
},
})
it('should have styling from the css prop', async () => {
const browser = await next.browser('/')
const color = await browser
.elementByCss('#test-element')
.getComputedCss('background-color')
expect(color).toBe('rgb(255, 192, 203)')
})
})

View File

@@ -0,0 +1,5 @@
{
"compilerOptions": {
"jsxImportSource": "@emotion/react"
}
}

View File

@@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
compiler: {
emotion: true,
},
}
module.exports = nextConfig

View File

@@ -0,0 +1,15 @@
import createCache from '@emotion/cache'
import { CacheProvider } from '@emotion/react'
import { globalStyles } from '../shared/styles'
const cache = createCache({ key: 'next' })
const App = ({ Component, pageProps }) => (
<CacheProvider value={cache}>
{globalStyles}
<Component {...pageProps} />
</CacheProvider>
)
export default App

View File

@@ -0,0 +1,33 @@
import { css } from '@emotion/react'
import {
Animated,
Basic,
bounce,
Combined,
Pink,
BasicExtended,
ComponentSelectorsExtended,
} from '../shared/styles'
const Home = () => (
<div
id={'test-element'}
css={css`
display: flex;
flex-direction: column;
background-color: pink;
`}
>
<Basic>Cool Styles</Basic>
<Pink>Pink text</Pink>
<Combined>
With <code>:hover</code>.
</Combined>
<Animated animation={bounce}>Let's bounce.</Animated>
<ComponentSelectorsExtended>
<BasicExtended>Nested</BasicExtended>
</ComponentSelectorsExtended>
</div>
)
export default Home

View File

@@ -0,0 +1,83 @@
import { css, Global, keyframes } from '@emotion/react'
import styled from '@emotion/styled'
export const globalStyles = (
<Global
styles={css`
html,
body {
padding: 3rem 1rem;
margin: 0;
background: papayawhip;
min-height: 100%;
font-family: Helvetica, Arial, sans-serif;
font-size: 24px;
}
`}
/>
)
export const basicStyles = css({
backgroundColor: 'white',
color: 'cornflowerblue',
border: '1px solid lightgreen',
borderRight: 'none',
borderBottom: 'none',
boxShadow: '5px 5px 0 0 lightgreen, 10px 10px 0 0 lightyellow',
transition: 'all 0.1s linear',
margin: '3rem 0',
padding: '1rem 0.5rem',
})
export const hoverStyles = css`
&:hover {
color: white;
background-color: lightgray;
border-color: aqua;
box-shadow:
-15px -15px 0 0 aqua,
-30px -30px 0 0 cornflowerblue;
}
`
export const bounce = keyframes`
from {
transform: scale(1.01);
}
to {
transform: scale(0.99);
}
`
export const Basic = styled.div`
${basicStyles};
`
export const Combined = styled.div`
${basicStyles};
${hoverStyles};
& code {
background-color: linen;
}
`
export const Pink = styled(Basic)({
color: 'hotpink',
})
export const BasicExtended = styled(Basic)``
export const ComponentSelectorsExtended = styled.div`
${BasicExtended} {
color: green;
}
box-shadow: -5px -5px 0 0 green;
`
export const Animated = styled.div`
${basicStyles};
${hoverStyles};
& code {
background-color: linen;
}
animation: ${({ animation }) => animation} 0.2s infinite ease-in-out alternate;
`

View File

@@ -0,0 +1,3 @@
{
"hello": "world"
}

View File

@@ -0,0 +1,27 @@
import { useRouter } from 'next/router'
export default function Gsp(props) {
if (useRouter().isFallback) {
return 'Loading...'
}
return (
<>
<p id="change">change me</p>
<p id="props">{JSON.stringify(props)}</p>
<div style={{ height: 1000, width: 50, background: 'cyan' }}></div>
<p id="scroll-target">bottom</p>
</>
)
}
export const getStaticProps = async () => {
const count = 1
return {
props: {
count,
random: Math.random(),
},
}
}

View File

@@ -0,0 +1,40 @@
import { useRouter } from 'next/router'
export default function Gsp(props) {
if (useRouter().isFallback) {
return 'Loading...'
}
return (
<>
<p id="change">change me</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}
export const getStaticProps = async ({ params }) => {
const count = 1
if (params.post === 'second') {
await new Promise((resolve) => setTimeout(resolve, 2000))
}
return {
props: {
count,
params,
random: Math.random(),
},
}
}
export const getStaticPaths = () => {
/* eslint-disable-next-line no-unused-vars */
const paths = 1
return {
paths: [{ params: { post: 'first' } }, { params: { post: 'second' } }],
fallback: true,
}
}

View File

@@ -0,0 +1,20 @@
export default function Gssp(props) {
return (
<>
<p id="change">change me</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}
export const getServerSideProps = ({ params }) => {
const count = 1
return {
props: {
count,
params,
random: Math.random(),
},
}
}

View File

@@ -0,0 +1,27 @@
import { useRouter } from 'next/router'
import data from '../lib/data.json'
export default function Gsp(props) {
if (useRouter().isFallback) {
return 'Loading...'
}
return (
<>
<p id="change">change me</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}
export const getStaticProps = async () => {
const count = 1
return {
props: {
count,
data,
random: Math.random(),
},
}
}

View File

@@ -0,0 +1,376 @@
/* eslint-env jest */
import { join } from 'path'
import webdriver from 'next-webdriver'
import { createNext, FileRef } from 'e2e-utils'
import { waitForNoRedbox, check } from 'next-test-utils'
import { NextInstance } from 'e2e-utils'
const installCheckVisible = (browser) => {
return browser.eval(`(function() {
window.checkInterval = setInterval(function() {
const root = document.querySelector('nextjs-portal').shadowRoot;
const statusElement = root.querySelector('[data-indicator-status]')
const badge = root.querySelector('[data-next-badge]')
const status = badge ? badge.getAttribute('data-status') : null
// Check if we're showing any status
window.showedBuilder = window.showedBuilder || (
statusElement !== null || (status && status !== 'none')
)
if (window.showedBuilder) clearInterval(window.checkInterval)
}, 50)
})()`)
}
describe('GS(S)P Server-Side Change Reloading', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, '../pages')),
lib: new FileRef(join(__dirname, '../lib')),
},
})
})
afterAll(() => next.destroy())
it('should not reload page when client-side is changed too GSP', async () => {
const browser = await webdriver(next.url, '/gsp-blog/first')
await check(() => browser.elementByCss('#change').text(), 'change me')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
const page = 'pages/gsp-blog/[post].js'
const originalContent = await next.readFile(page)
await next.patchFile(page, originalContent.replace('change me', 'changed'))
await check(() => browser.elementByCss('#change').text(), 'changed')
expect(await browser.eval(`window.beforeChange`)).toBe('hi')
const props2 = JSON.parse(await browser.elementByCss('#props').text())
expect(props).toEqual(props2)
await next.patchFile(page, originalContent)
await check(() => browser.elementByCss('#change').text(), 'change me')
})
it('should update page when getStaticProps is changed only', async () => {
const browser = await webdriver(next.url, '/gsp-blog/first')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/gsp-blog/[post].js'
const originalContent = await next.readFile(page)
await next.patchFile(
page,
originalContent.replace('count = 1', 'count = 2')
)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'2'
)
expect(await browser.eval(`window.beforeChange`)).toBe('hi')
await next.patchFile(page, originalContent)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'1'
)
})
it('should show indicator when re-fetching data', async () => {
const browser = await webdriver(next.url, '/gsp-blog/second')
await installCheckVisible(browser)
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/gsp-blog/[post].js'
const originalContent = await next.readFile(page)
await next.patchFile(
page,
originalContent.replace('count = 1', 'count = 2')
)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'2'
)
expect(await browser.eval(`window.beforeChange`)).toBe('hi')
expect(await browser.eval(`window.showedBuilder`)).toBe(true)
await next.patchFile(page, originalContent)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'1'
)
})
it('should update page when getStaticPaths is changed only', async () => {
const browser = await webdriver(next.url, '/gsp-blog/first')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/gsp-blog/[post].js'
const originalContent = await next.readFile(page)
await next.patchFile(
page,
originalContent.replace('paths = 1', 'paths = 2')
)
expect(await browser.eval('window.beforeChange')).toBe('hi')
await next.patchFile(page, originalContent)
})
it('should update page when getStaticProps is changed only for /index', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/index.js'
const originalContent = await next.readFile(page)
await next.patchFile(
page,
originalContent.replace('count = 1', 'count = 2')
)
expect(await browser.eval('window.beforeChange')).toBe('hi')
await next.patchFile(page, originalContent)
})
it('should update page when getStaticProps is changed only for /another/index', async () => {
const browser = await webdriver(next.url, '/another')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/another/index.js'
const originalContent = await next.readFile(page)
await next.patchFile(
page,
originalContent.replace('count = 1', 'count = 2')
)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'2'
)
expect(await browser.eval('window.beforeChange')).toBe('hi')
await next.patchFile(page, originalContent)
})
it('should keep scroll position when updating from change in getStaticProps', async () => {
const browser = await webdriver(next.url, '/another')
await browser.eval(
'document.getElementById("scroll-target").scrollIntoView()'
)
const scrollPosition = await browser.eval(
'document.documentElement.scrollTop'
)
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/another/index.js'
const originalContent = await next.readFile(page)
await next.patchFile(
page,
originalContent.replace('count = 1', 'count = 2')
)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'2'
)
expect(await browser.eval('window.beforeChange')).toBe('hi')
expect(await browser.eval('document.documentElement.scrollTop')).toBe(
scrollPosition
)
await next.patchFile(page, originalContent)
})
it('should not reload page when client-side is changed too GSSP', async () => {
const browser = await webdriver(next.url, '/gssp-blog/first')
await check(() => browser.elementByCss('#change').text(), 'change me')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
const page = 'pages/gssp-blog/[post].js'
const originalContent = await next.readFile(page)
await next.patchFile(page, originalContent.replace('change me', 'changed'))
await check(() => browser.elementByCss('#change').text(), 'changed')
expect(await browser.eval(`window.beforeChange`)).toBe('hi')
const props2 = JSON.parse(await browser.elementByCss('#props').text())
expect(props).toEqual(props2)
await next.patchFile(page, originalContent)
await check(() => browser.elementByCss('#change').text(), 'change me')
})
it('should update page when getServerSideProps is changed only', async () => {
const browser = await webdriver(next.url, '/gssp-blog/first')
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'1'
)
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/gssp-blog/[post].js'
const originalContent = await next.readFile(page)
await next.patchFile(
page,
originalContent.replace('count = 1', 'count = 2')
)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'2'
)
expect(await browser.eval(`window.beforeChange`)).toBe('hi')
await next.patchFile(page, originalContent)
await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'1'
)
})
it('should update on props error in getStaticProps', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/index.js'
const originalContent = await next.readFile(page)
try {
await next.patchFile(page, originalContent.replace('props:', 'propss:'))
await expect(browser).toDisplayRedbox(`
{
"code": "E394",
"description": "Additional keys were returned from \`getStaticProps\`. Properties intended for your component must be nested under the \`props\` key, e.g.:
return { props: { title: 'My Title', content: '...' } }
Keys that need to be moved: propss.
Read more: https://nextjs.org/docs/messages/invalid-getstaticprops-value",
"environmentLabel": null,
"label": "Runtime Error",
"source": null,
"stack": [],
}
`)
await next.patchFile(page, originalContent)
await waitForNoRedbox(browser)
} finally {
await next.patchFile(page, originalContent)
}
})
it('should update on thrown error in getStaticProps', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
const page = 'pages/index.js'
const originalContent = await next.readFile(page)
try {
await next.patchFile(
page,
originalContent.replace(
'const count',
'throw new Error("custom oops"); const count'
)
)
await expect(browser).toDisplayRedbox(`
{
"code": "E394",
"description": "custom oops",
"environmentLabel": null,
"label": "Runtime Error",
"source": "pages/index.js (18:9) @ getStaticProps
> 18 | throw new Error("custom oops"); const count = 1
| ^",
"stack": [
"getStaticProps pages/index.js (18:9)",
],
}
`)
expect(next.cliOutput).toMatch(/custom oops/)
await next.patchFile(page, originalContent)
await waitForNoRedbox(browser)
} finally {
await next.patchFile(page, originalContent)
}
})
it('should refresh data when server import is updated', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval(`window.beforeChange = 'hi'`)
const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)
expect(props.data).toEqual({ hello: 'world' })
const page = 'lib/data.json'
const originalContent = await next.readFile(page)
try {
await next.patchFile(page, JSON.stringify({ hello: 'replaced!!' }))
await check(async () => {
const props = JSON.parse(await browser.elementByCss('#props').text())
return props.count === 1 && props.data.hello === 'replaced!!'
? 'success'
: JSON.stringify(props)
}, 'success')
expect(await browser.eval('window.beforeChange')).toBe('hi')
await next.patchFile(page, originalContent)
await check(async () => {
const props = JSON.parse(await browser.elementByCss('#props').text())
return props.count === 1 && props.data.hello === 'world'
? 'success'
: JSON.stringify(props)
}, 'success')
} finally {
await next.patchFile(page, originalContent)
}
})
})

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,12 @@
export default () => {
return (
<div id="dynamic-component">
Dynamic Component
<style jsx>{`
div {
font-size: 100px;
}
`}</style>
</div>
)
}

View File

@@ -0,0 +1,5 @@
This
is
}}}
invalid
js

View File

@@ -0,0 +1,5 @@
This
is
}}}
invalid
js

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,12 @@
import { useRouter } from 'next/router'
export default function Page(props) {
const router = useRouter()
return (
<>
<p>auto-export router.isReady</p>
<p id="query">{JSON.stringify(router.query)}</p>
<p id="ready">{router.isReady ? 'yes' : 'no'}</p>
</>
)
}

View File

@@ -0,0 +1,20 @@
import { useRouter } from 'next/router'
export default function Page(props) {
const router = useRouter()
return (
<>
<p>getStaticProps router.isReady</p>
<p id="query">{JSON.stringify(router.query)}</p>
<p id="ready">{router.isReady ? 'yes' : 'no'}</p>
</>
)
}
export function getStaticProps() {
return {
props: {
now: Date.now(),
},
}
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function About4() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

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

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-contact-page">
<p>This is the contact page.</p>
</div>
)
}

View File

@@ -0,0 +1,19 @@
import React from 'react'
export default class Counter extends React.Component {
state = { count: 0 }
incr() {
const { count } = this.state
this.setState({ count: count + 1 })
}
render() {
return (
<div>
<p>COUNT: {this.state.count}</p>
<button onClick={() => this.incr()}>Increment</button>
</div>
)
}
}

View File

@@ -0,0 +1,11 @@
import React from 'react'
export default class Page extends React.Component {
static getInitialProps() {
const error = new Error('an-expected-error-in-gip')
throw error
}
render() {
return <div>Hello</div>
}
}

View File

@@ -0,0 +1,11 @@
import Link from 'next/link'
export default function Page() {
return (
<div>
<Link href="/hmr/error-in-gip" id="error-in-gip-link">
Bad Page
</Link>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function NonLatin(props) {
return <div>テスト</div>
}

View File

@@ -0,0 +1,4 @@
export default function () {
// eslint-disable-next-line no-undef
return whoops
}

View File

@@ -0,0 +1,8 @@
import React from 'react'
import dynamic from 'next/dynamic'
const HmrDynamic = dynamic(import('../../components/hmr/dynamic'))
export default function Page() {
return <HmrDynamic />
}

View File

@@ -0,0 +1,20 @@
import React, { Component } from 'react'
export default class StyleStateFul extends Component {
render() {
return (
<React.Fragment>
<div className="hmr-style-page">
<p>
This is the style page.
<style jsx>{`
p {
font-size: 100px;
}
`}</style>
</p>
</div>
</React.Fragment>
)
}
}

View File

@@ -0,0 +1,17 @@
import React from 'react'
export default function Style() {
return (
<React.Fragment>
<div className="hmr-style-page">
<p>
This is the style page.
<style jsx>{`
p {
font-size: 100px;
}
`}</style>
</p>
</div>
</React.Fragment>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page(props) {
return <p>is server {typeof window}</p>
}

View File

@@ -0,0 +1,112 @@
import {
waitForRedbox,
getBrowserBodyText,
retry,
waitFor,
} from 'next-test-utils'
import { createNext, nextTestSetup } from 'e2e-utils'
export function runBasicHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next, isTurbopack } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
it('should have correct router.isReady for auto-export page', async () => {
let browser = await next.browser(basePath + '/auto-export-is-ready')
expect(await browser.elementByCss('#ready').text()).toBe('yes')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({})
browser = await next.browser(basePath + '/auto-export-is-ready?hello=world')
await retry(async () => {
expect(await browser.elementByCss('#ready').text()).toBe('yes')
})
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
hello: 'world',
})
})
it('should have correct router.isReady for getStaticProps page', async () => {
let browser = await next.browser(basePath + '/gsp-is-ready')
expect(await browser.elementByCss('#ready').text()).toBe('yes')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({})
browser = await next.browser(basePath + '/gsp-is-ready?hello=world')
await retry(async () => {
expect(await browser.elementByCss('#ready').text()).toBe('yes')
})
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
hello: 'world',
})
})
;(isTurbopack ? it : it.skip)(
'should have correct compile timing after fixing error',
async () => {
const browser = await next.browser(basePath + '/auto-export-is-ready')
let outputLength
await next.patchFile(
'pages/auto-export-is-ready.js',
(content) => `import hello from 'non-existent'\n` + content,
async () => {
await waitForRedbox(browser)
await waitFor(3000)
outputLength = next.cliOutput.length
}
)
let compileTimeStr
await retry(async () => {
compileTimeStr = next.cliOutput.substring(outputLength)
expect(compileTimeStr).toMatch(/Compiled.*?/i)
})
const matches = [
...compileTimeStr.match(/Compiled.*? in ([\d.]{1,})\s?(?:s|ms)/i),
]
const [, compileTime, timeUnit] = matches
let compileTimeMs = parseFloat(compileTime)
if (timeUnit === 's') {
compileTimeMs = compileTimeMs * 1000
}
expect(compileTimeMs).toBeLessThan(3000)
}
)
it('should reload the page when the server restarts', async () => {
const browser = await next.browser(basePath + '/hmr/about')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.destroy()
let reloadPromise = new Promise((resolve) => {
browser.on('request', (req) => {
if (req.url().endsWith('/hmr/about')) {
resolve(req.url())
}
})
})
const secondNext = await createNext({
files: __dirname,
nextConfig,
forcedPort: next.appPort,
})
await reloadPromise
await secondNext.destroy()
})
}

View File

@@ -0,0 +1,664 @@
import { join } from 'path'
import {
waitForRedbox,
waitForNoRedbox,
getBrowserBodyText,
getRedboxHeader,
getRedboxDescription,
getRedboxSource,
retry,
waitFor,
trimEndMultiline,
getDistDir,
} from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
import { outdent } from 'outdent'
export function runErrorRecoveryHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
it('should recover from 404 after a page has been added', async () => {
const browser = await next.browser(basePath + '/hmr/new-page')
expect(await browser.elementByCss('body').text()).toMatch(
/This page could not be found/
)
expect(next.cliOutput).toContain('GET /hmr/new-page 404')
let cliOutputLength = next.cliOutput.length
// Add the page
await next.patchFile(
join('pages', 'hmr', 'new-page.js'),
'export default () => (<div id="new-page">the-new-page</div>)',
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/new-page 200'
)
cliOutputLength = next.cliOutput.length
}
)
// page was deleted at the end of patchFile
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This page could not be found/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/new-page 404'
)
})
it('should recover from 404 after a page has been added with dynamic segments', async () => {
const browser = await next.browser(basePath + '/hmr/foo/page')
expect(await browser.elementByCss('body').text()).toMatch(
/This page could not be found/
)
expect(next.cliOutput).toContain('GET /hmr/foo/page 404')
let cliOutputLength = next.cliOutput.length
// Add the page
await next.patchFile(
join('pages', 'hmr', '[foo]', 'page.js'),
'export default () => (<div id="new-page">the-new-page</div>)',
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/foo/page 200'
)
cliOutputLength = next.cliOutput.length
}
)
// page was deleted at the end of patchFile
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This page could not be found/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/foo/page 404'
)
})
;(process.env.IS_TURBOPACK_TEST ? it.skip : it)(
// this test fails frequently with turbopack
'should not continously poll a custom error page',
async () => {
await next.patchFile(
join('pages', '_error.js'),
outdent`
function Error({ statusCode, message, count }) {
return (
<div>
Error Message: {message}
</div>
)
}
Error.getInitialProps = async ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
console.log('getInitialProps called');
return {
statusCode,
message: err ? err.message : 'Oops...',
}
}
export default Error
`,
async () => {
// navigate to a 404 page
await next.browser(basePath + '/does-not-exist')
await retry(() => {
expect(next.cliOutput).toMatch(/getInitialProps called/)
})
const outputIndex = next.cliOutput.length
// wait a few seconds to ensure polling didn't happen
await waitFor(3000)
const logOccurrences =
next.cliOutput.slice(outputIndex).split('getInitialProps called')
.length - 1
expect(logOccurrences).toBe(0)
}
)
}
)
it('should detect syntax errors and recover', async () => {
const browser = await next.browser(basePath + '/hmr/about2')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about2.js'),
(content) => content.replace('</div>', 'div'),
async () => {
await waitForRedbox(browser)
const source = next.normalizeTestDirContent(
await getRedboxSource(browser)
)
if (process.env.IS_TURBOPACK_TEST) {
expect(source).toMatchInlineSnapshot(`
"./pages/hmr/about2.js (7:1)
Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
5 | div
6 | )
> 7 | }
| ^
8 |
Parsing ecmascript source code failed"
`)
} else if (process.env.NEXT_RSPACK) {
expect(trimEndMultiline(source)).toMatchInlineSnapshot(`
"./pages/hmr/about2.js
╰─▶ × Error: x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
│ ,-[7:1]
│ 4 | <p>This is the about page.</p>
│ 5 | div
│ 6 | )
│ 7 | }
│ : ^
\`----
│ x Expected '</', got '<eof>'
│ ,-[7:3]
│ 5 | div
│ 6 | )
│ 7 | }
\`----
│ Caused by:
│ Syntax Error
Import trace for requested module:
./pages/hmr/about2.js"
`)
} else {
expect(source).toMatchInlineSnapshot(`
"./pages/hmr/about2.js
Error: x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
,-[7:1]
4 | <p>This is the about page.</p>
5 | div
6 | )
7 | }
: ^
\`----
x Expected '</', got '<eof>'
,-[7:3]
5 | div
6 | )
7 | }
\`----
Caused by:
Syntax Error
Import trace for requested module:
./pages/hmr/about2.js"
`)
}
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
if (!process.env.IS_TURBOPACK_TEST) {
// Turbopack doesn't have this restriction
it('should show the error on all pages', async () => {
const browser = await next.browser(basePath + '/hmr/contact')
await next.render(basePath + '/hmr/about2')
await next.patchFile(
join('pages', 'hmr', 'about2.js'),
(content) => content.replace('</div>', 'div'),
async () => {
// Ensure dev server has time to break:
await new Promise((resolve) => setTimeout(resolve, 2000))
await waitForRedbox(browser)
expect(await getRedboxSource(browser)).toContain(
"Expected '</', got '<eof>'"
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the contact page/
)
})
})
}
it('should detect runtime errors on the module scope', async () => {
const browser = await next.browser(basePath + '/hmr/about3')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about3.js'),
(content) => content.replace('export', 'aa=20;\nexport'),
async () => {
await waitForRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover from errors in the render function', async () => {
const browser = await next.browser(basePath + '/hmr/about4')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about4.js'),
(content) =>
content.replace(
'return',
'throw new Error("an-expected-error");\nreturn'
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxSource(browser)).toMatch(/an-expected-error/)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover after exporting an invalid page', async () => {
const browser = await next.browser(basePath + '/hmr/about5')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about5.js'),
(content) =>
content.replace(
'export default',
'export default {};\nexport const fn ='
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"The default export is not a React Component in page: "/hmr/about5""`
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover after a bad return from the render function', async () => {
const browser = await next.browser(basePath + '/hmr/about6')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about6.js'),
(content) =>
content.replace(
'export default',
'export default () => /search/;\nexport const fn ='
),
async () => {
await waitForRedbox(browser)
// TODO: Replace this when webpack 5 is the default
expect(await getRedboxHeader(browser)).toMatch(
`Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.`
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover after undefined exported as default', async () => {
const browser = await next.browser(basePath + '/hmr/about7')
const aboutPage = join('pages', 'hmr', 'about7.js')
const aboutContent = await next.readFile(aboutPage)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
aboutPage,
aboutContent.replace(
'export default',
'export default undefined;\nexport const fn ='
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"The default export is not a React Component in page: "/hmr/about7""`
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await waitForNoRedbox(browser)
})
it('should recover after webpack parse error in an imported file', async () => {
const browser = await next.browser(basePath + '/hmr/about8')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about8.js'),
(content) =>
content.replace(
'export default',
'import "../../components/parse-error.xyz"\nexport default'
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch('Build Error')
if (process.env.IS_TURBOPACK_TEST) {
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
"./components/parse-error.xyz
Unknown module type
This module doesn't have an associated type. Use a known file extension, or register a loader for it.
Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders"
`)
} else if (process.env.NEXT_RSPACK) {
expect(trimEndMultiline(await getRedboxSource(browser)))
.toMatchInlineSnapshot(`
"./components/parse-error.xyz
× Module parse failed:
╰─▶ × JavaScript parse error: Expression expected
╭─[3:0]
1 │ This
2 │ is
3 │ }}}
· ─
4 │ invalid
5 │ js
╰────
help:
You may need an appropriate loader to handle this file type.
Import trace for requested module:
./components/parse-error.xyz
./pages/hmr/about8.js"
`)
} else {
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
"./components/parse-error.xyz
Module parse failed: Unexpected token (3:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| This
| is
> }}}
| invalid
| js
Import trace for requested module:
./components/parse-error.xyz
./pages/hmr/about8.js"
`)
}
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await waitForNoRedbox(browser)
})
it('should recover after loader parse error in an imported file', async () => {
const browser = await next.browser(basePath + '/hmr/about9')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about9.js'),
(content) =>
content.replace(
'export default',
'import "../../components/parse-error.js"\nexport default'
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch('Build Error')
let redboxSource = await getRedboxSource(browser)
redboxSource = redboxSource.replace(`${next.testDir}`, '.')
if (process.env.IS_TURBOPACK_TEST) {
expect(next.normalizeTestDirContent(redboxSource))
.toMatchInlineSnapshot(`
"./components/parse-error.js (3:1)
Expression expected
1 | This
2 | is
> 3 | }}}
| ^
4 | invalid
5 | js
Parsing ecmascript source code failed
Import traces:
Browser:
./components/parse-error.js
./pages/hmr/about9.js
SSR:
./components/parse-error.js
./pages/hmr/about9.js"
`)
} else if (process.env.NEXT_RSPACK) {
expect(trimEndMultiline(next.normalizeTestDirContent(redboxSource)))
.toMatchInlineSnapshot(`
"./components/parse-error.js
╰─▶ × Error: x Expression expected
│ ,-[3:1]
│ 1 | This
│ 2 | is
│ 3 | }}}
│ : ^
│ 4 | invalid
│ 5 | js
\`----
│ Caused by:
│ Syntax Error
Import trace for requested module:
./components/parse-error.js
./pages/hmr/about9.js"
`)
} else {
redboxSource = redboxSource.substring(
0,
redboxSource.indexOf('`----')
)
expect(next.normalizeTestDirContent(redboxSource))
.toMatchInlineSnapshot(`
"./components/parse-error.js
Error: x Expression expected
,-[3:1]
1 | This
2 | is
3 | }}}
: ^
4 | invalid
5 | js
"
`)
}
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await waitForNoRedbox(browser)
})
it('should recover from errors in getInitialProps in client', async () => {
const browser = await next.browser(basePath + '/hmr')
const erroredPage = join('pages', 'hmr', 'error-in-gip.js')
const errorContent = await next.readFile(erroredPage)
await browser.elementByCss('#error-in-gip-link').click()
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"an-expected-error-in-gip"`
)
await next.patchFile(
erroredPage,
(content) => content.replace('throw error', 'return {}'),
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
})
await next.patchFile(erroredPage, errorContent)
await retry(async () => {
await browser.refresh()
await waitFor(2000)
const text = await getBrowserBodyText(browser)
if (text.includes('Hello')) {
throw new Error('waiting')
}
return expect(await getRedboxSource(browser)).toMatch(
/an-expected-error-in-gip/
)
})
}
)
})
it('should recover after an error reported via SSR', async () => {
const browser = await next.browser(basePath + '/hmr/error-in-gip')
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"an-expected-error-in-gip"`
)
await next.patchFile(
join('pages', 'hmr', 'error-in-gip.js'),
(content) => content.replace('throw error', 'return {}'),
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
})
}
)
await retry(async () => {
await browser.refresh()
await waitFor(2000)
const text = await getBrowserBodyText(browser)
if (text.includes('Hello')) {
throw new Error('waiting')
}
return expect(await getRedboxSource(browser)).toMatch(
/an-expected-error-in-gip/
)
})
})
if (!process.env.IS_TURBOPACK_TEST) {
it('should have client HMR events in trace file', async () => {
const traceData = await next.readFile(`${getDistDir()}/trace`)
expect(traceData).toContain('client-hmr-latency')
expect(traceData).toContain('client-error')
expect(traceData).toContain('client-success')
expect(traceData).toContain('client-full-reload')
})
}
}

View File

@@ -0,0 +1,91 @@
import { getRedboxHeader, retry } from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
export function runFullReloadHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
it('should warn about full reload in cli output - anonymous page function', async () => {
const start = next.cliOutput.length
const browser = await next.browser(
basePath + '/hmr/anonymous-page-function'
)
const cliWarning =
'Fast Refresh had to perform a full reload when ./pages/hmr/anonymous-page-function.js changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload'
expect(await browser.elementByCss('p').text()).toBe('hello world')
expect(next.cliOutput.slice(start)).not.toContain(cliWarning)
const currentFileContent = await next.readFile(
'./pages/hmr/anonymous-page-function.js'
)
const newFileContent = currentFileContent.replace(
'<p>hello world</p>',
'<p id="updated">hello world!!!</p>'
)
await next.patchFile(
'./pages/hmr/anonymous-page-function.js',
newFileContent
)
expect(await browser.waitForElementByCss('#updated').text()).toBe(
'hello world!!!'
)
// CLI warning
expect(next.cliOutput.slice(start)).toContain(cliWarning)
// Browser warning
const browserLogs = await browser.log()
expect(
browserLogs.some(({ message }) =>
message.includes(
"Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree."
)
)
).toBeTruthy()
})
it('should warn about full reload in cli output - runtime-error', async () => {
const start = next.cliOutput.length
const browser = await next.browser(basePath + '/hmr/runtime-error')
const cliWarning =
'Fast Refresh had to perform a full reload due to a runtime error.'
await retry(async () => {
expect(await getRedboxHeader(browser)).toMatch(/whoops is not defined/)
})
expect(next.cliOutput.slice(start)).not.toContain(cliWarning)
const currentFileContent = await next.readFile(
'./pages/hmr/runtime-error.js'
)
const newFileContent = currentFileContent.replace(
'whoops',
'<p id="updated">whoops</p>'
)
await next.patchFile('./pages/hmr/runtime-error.js', newFileContent)
expect(await browser.waitForElementByCss('#updated').text()).toBe('whoops')
// CLI warning
expect(next.cliOutput.slice(start)).toContain(cliWarning)
// Browser warning
const browserLogs = await browser.log()
expect(
browserLogs.some(({ message }) =>
message.includes(
'[Fast Refresh] performing full reload because your application had an unrecoverable error'
)
)
).toBeTruthy()
})
}

View File

@@ -0,0 +1,277 @@
import { join } from 'path'
import { getBrowserBodyText, retry, waitFor } from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
export function runHotModuleReloadHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
describe('delete a page and add it back', () => {
it('should load the page properly', async () => {
const contactPagePath = join('pages', 'hmr', 'contact.js')
const newContactPagePath = join('pages', 'hmr', '_contact.js')
const browser = await next.browser(basePath + '/hmr/contact')
try {
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the contact page.')
expect(next.cliOutput).toMatch(/GET .*\/hmr\/contact 200/)
let cliOutputLength = next.cliOutput.length
// Rename the file to mimic a deleted page
await next.renameFile(contactPagePath, newContactPagePath)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This page could not be found/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toMatch(
/GET .*\/hmr\/contact 404/
)
cliOutputLength = next.cliOutput.length
// Rename the file back to the original filename
await next.renameFile(newContactPagePath, contactPagePath)
// wait until the page comes back
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the contact page/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toMatch(
/GET .*\/hmr\/contact 200/
)
} finally {
await next
.renameFile(newContactPagePath, contactPagePath)
.catch(() => {})
}
})
})
describe('editing a page', () => {
it('should detect the changes and display it', async () => {
const browser = await next.browser(basePath + '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
const aboutPagePath = join('pages', 'hmr', 'about.js')
const originalContent = await next.readFile(aboutPagePath)
const editedContent = originalContent.replace(
'This is the about page',
'COOL page'
)
// change the content
try {
await next.patchFile(aboutPagePath, editedContent)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/COOL page/)
})
} finally {
// add the original content
await next.patchFile(aboutPagePath, originalContent)
}
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should not reload unrelated pages', async () => {
const browser = await next.browser(basePath + '/hmr/counter')
const text = await browser
.elementByCss('button')
.click()
.elementByCss('button')
.click()
.elementByCss('p')
.text()
expect(text).toBe('COUNT: 2')
const aboutPagePath = join('pages', 'hmr', 'about.js')
const originalContent = await next.readFile(aboutPagePath)
const editedContent = originalContent.replace(
'This is the about page',
'COOL page'
)
try {
// Change the about.js page
await next.patchFile(aboutPagePath, editedContent)
// Check whether the this page has reloaded or not.
await retry(async () => {
expect(await browser.elementByCss('p').text()).toMatch(/COUNT: 2/)
})
} finally {
// restore the about page content.
await next.patchFile(aboutPagePath, originalContent)
}
})
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
// Also: https://github.com/vercel/styled-jsx/issues/425
it('should update styles correctly', async () => {
const browser = await next.browser(basePath + '/hmr/style')
const pTag = await browser.elementByCss('.hmr-style-page p')
const initialFontSize = await pTag.getComputedCss('font-size')
expect(initialFontSize).toBe('100px')
const pagePath = join('pages', 'hmr', 'style.js')
const originalContent = await next.readFile(pagePath)
const editedContent = originalContent.replace('100px', '200px')
// Change the page
await next.patchFile(pagePath, editedContent)
try {
// Check whether the this page has reloaded or not.
await retry(async () => {
const editedPTag = await browser.elementByCss('.hmr-style-page p')
expect(await editedPTag.getComputedCss('font-size')).toBe('200px')
})
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
await next.patchFile(pagePath, originalContent)
}
})
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
// Also: https://github.com/vercel/styled-jsx/issues/425
it('should update styles in a stateful component correctly', async () => {
const browser = await next.browser(
basePath + '/hmr/style-stateful-component'
)
const pagePath = join('pages', 'hmr', 'style-stateful-component.js')
const originalContent = await next.readFile(pagePath)
try {
const pTag = await browser.elementByCss('.hmr-style-page p')
const initialFontSize = await pTag.getComputedCss('font-size')
expect(initialFontSize).toBe('100px')
const editedContent = originalContent.replace('100px', '200px')
// Change the page
await next.patchFile(pagePath, editedContent)
// Check whether the this page has reloaded or not.
await retry(async () => {
const editedPTag = await browser.elementByCss('.hmr-style-page p')
expect(await editedPTag.getComputedCss('font-size')).toBe('200px')
})
} finally {
await next.patchFile(pagePath, originalContent)
}
})
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
// Also: https://github.com/vercel/styled-jsx/issues/425
it('should update styles in a dynamic component correctly', async () => {
const browser = await next.browser(
basePath + '/hmr/style-dynamic-component'
)
const secondBrowser = await next.browser(
basePath + '/hmr/style-dynamic-component'
)
const pagePath = join('components', 'hmr', 'dynamic.js')
const originalContent = await next.readFile(pagePath)
try {
const div = await browser.elementByCss('#dynamic-component')
const initialClientClassName = await div.getAttribute('class')
const initialFontSize = await div.getComputedCss('font-size')
expect(initialFontSize).toBe('100px')
const initialHtml = await next.render(
basePath + '/hmr/style-dynamic-component'
)
expect(initialHtml.includes('100px')).toBeTruthy()
const $initialHtml = await next.render$(
basePath + '/hmr/style-dynamic-component'
)
const initialServerClassName =
$initialHtml('#dynamic-component').attr('class')
expect(initialClientClassName === initialServerClassName).toBeTruthy()
const editedContent = originalContent.replace('100px', '200px')
// Change the page
await next.patchFile(pagePath, editedContent)
// wait for 5 seconds
await waitFor(5000)
// Check whether the this page has reloaded or not.
const editedDiv = await secondBrowser.elementByCss('#dynamic-component')
const editedClientClassName = await editedDiv.getAttribute('class')
const editedFontSize = await editedDiv.getComputedCss('font-size')
const browserHtml = await secondBrowser.eval(
'document.documentElement.innerHTML'
)
expect(editedFontSize).toBe('200px')
expect(browserHtml.includes('font-size:200px')).toBe(true)
expect(browserHtml.includes('font-size:100px')).toBe(false)
const editedHtml = await next.render(
basePath + '/hmr/style-dynamic-component'
)
expect(editedHtml.includes('200px')).toBeTruthy()
const $editedHtml = await next.render$(
basePath + '/hmr/style-dynamic-component'
)
const editedServerClassName =
$editedHtml('#dynamic-component').attr('class')
expect(editedClientClassName === editedServerClassName).toBe(true)
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
await next.patchFile(pagePath, originalContent)
}
})
it('should not full reload when nonlatin characters are used', async () => {
const browser = await next.browser(basePath + '/hmr/nonlatin')
const pagePath = join('pages', 'hmr', 'nonlatin.js')
const originalContent = await next.readFile(pagePath)
try {
const timeOrigin = await browser.eval('performance.timeOrigin')
const editedContent = originalContent.replace(
'<div>テスト</div>',
'<div class="updated">テスト</div>'
)
// Change the page
await next.patchFile(pagePath, editedContent)
await browser.waitForElementByCss('.updated')
expect(await browser.eval('performance.timeOrigin')).toEqual(timeOrigin)
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
await next.patchFile(pagePath, originalContent)
}
})
})
}

View File

@@ -0,0 +1,85 @@
import { join } from 'path'
import webdriver from 'next-webdriver'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import { check } from 'next-test-utils'
describe('Legacy decorators SWC option', () => {
describe('with extended tsconfig', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
'tsconfig.json': new FileRef(
join(__dirname, 'legacy-decorators/tsconfig-extended.json')
),
'tsconfig-base.json': new FileRef(
join(__dirname, 'legacy-decorators/jsconfig.json')
),
pages: new FileRef(join(__dirname, 'legacy-decorators/pages')),
},
dependencies: {
mobx: '6.3.7',
'mobx-react': '7.2.1',
},
})
})
afterAll(() => next.destroy())
it('should compile with legacy decorators enabled from extended config', async () => {
let browser
try {
browser = await webdriver(next.url, '/')
const text = await browser.elementByCss('#count').text()
expect(text).toBe('Current number: 0')
await browser.elementByCss('#increase').click()
await check(
() => browser.elementByCss('#count').text(),
/Current number: 1/
)
} finally {
if (browser) {
await browser.close()
}
}
})
})
describe('with base config', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
'jsconfig.json': new FileRef(
join(__dirname, 'legacy-decorators/jsconfig.json')
),
pages: new FileRef(join(__dirname, 'legacy-decorators/pages')),
},
dependencies: {
mobx: '6.3.7',
'mobx-react': '7.2.1',
},
})
})
afterAll(() => next.destroy())
it('should compile with legacy decorators enabled', async () => {
let browser
try {
browser = await webdriver(next.url, '/')
const text = await browser.elementByCss('#count').text()
expect(text).toBe('Current number: 0')
await browser.elementByCss('#increase').click()
await check(
() => browser.elementByCss('#count').text(),
/Current number: 1/
)
} finally {
if (browser) {
await browser.close()
}
}
})
})
})

View File

@@ -0,0 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true
}
}

View File

@@ -0,0 +1,35 @@
import React from 'react'
import { makeAutoObservable } from 'mobx'
import { observer } from 'mobx-react'
class Counter {
count = 0
constructor() {
makeAutoObservable(this)
}
increase() {
this.count += 1
}
}
const myCounter = new Counter()
@observer
class CounterView extends React.Component {
render() {
return (
<>
<span id="count">Current number: {this.props.counter.count}</span>
<button id="increase" onClick={() => this.props.counter.increase()}>
Increase
</button>
</>
)
}
}
export default function Home() {
return <CounterView counter={myCounter} />
}

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