Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5280,7 +5280,13 @@ async function resolveLazyDevValidationInputs(
}

if ('syncInterruptReason' in inputs) {
await logMessagesAndSendErrorsToBrowser([inputs.syncInterruptReason], ctx)
await logMessagesAndSendErrorsToBrowser(
[inputs.syncInterruptReason],
ctx,
// We're not going to run validation, so mark this as the validation result for tests.
{ logAsValidationResult: true }
)

return VALIDATION_BAILOUT
}
return inputs
Expand All @@ -5291,7 +5297,12 @@ function forwardErrorsFromWarmRender(
ctx: AppRenderContext
) {
if ('syncInterruptReason' in inputs) {
void logMessagesAndSendErrorsToBrowser([inputs.syncInterruptReason], ctx)
void logMessagesAndSendErrorsToBrowser(
[inputs.syncInterruptReason],
ctx,
// We're not going to run validation, so mark this as the validation result for tests.
{ logAsValidationResult: true }
)
return true
}

Expand Down Expand Up @@ -6397,11 +6408,24 @@ function createAsyncApiPromises(
*/
async function logMessagesAndSendErrorsToBrowser(
messages: unknown[],
ctx: AppRenderContext
ctx: AppRenderContext,
options: { logAsValidationResult?: boolean } = {}
): Promise<void> {
const { htmlRequestId, renderOpts } = ctx
const logAsValidationResult =
process.env.__NEXT_TEST_MODE &&
process.env.NEXT_TEST_LOG_VALIDATION &&
options.logAsValidationResult

const { htmlRequestId, requestId, renderOpts } = ctx
const url = ctx.url.href
const { sendErrorsToBrowser } = renderOpts

if (logAsValidationResult) {
console.log(
formatValidationEvent({ type: 'validation_start', requestId, url })
)
}

const errors: Error[] = []
for (const message of messages) {
// Log the error to the CLI. Prevent the logs from being dimmed, which we
Expand All @@ -6421,6 +6445,12 @@ async function logMessagesAndSendErrorsToBrowser(
}
}

if (logAsValidationResult) {
console.log(
formatValidationEvent({ type: 'validation_end', requestId, url })
)
}

if (errors.length > 0) {
if (!sendErrorsToBrowser) {
throw new InvariantError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ describe('instant-validation-build', () => {
}

beforeAll(async () => {
await next.build({ args: ['--experimental-build-mode', 'compile'] })
const result = await next.build({
args: ['--experimental-build-mode', 'compile'],
})
if (result.exitCode !== 0) {
throw new Error(
`Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}`
)
}
})

describe('basic dynamic hole detection', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import type { ValidationEvent } from 'next/dist/server/app-render/dev-validation-events'
import { createGetInstantInsight } from 'e2e-utils/instant-validation'

describe('instant validation causes', () => {
const { next, skipped, isNextDev } = nextTestSetup({
Expand Down Expand Up @@ -28,67 +27,11 @@ describe('instant validation causes', () => {
return next.cliOutput.slice(currentCliOutputIndex)
}

function parseValidationMessages(output: string): ValidationEvent[] {
const messageRe = /<VALIDATION_MESSAGE>(.*?)<\/VALIDATION_MESSAGE>/g
const events: ValidationEvent[] = []
let match: RegExpExecArray | null
while ((match = messageRe.exec(output)) !== null) {
try {
events.push(JSON.parse(match[1]))
} catch (err) {
throw new Error(`Failed to parse message '${match[1]}'`, {
cause: err,
})
}
}
return events
}

function normalizeValidationUrl(url: string): string {
const parsed = new URL(url, 'http://n')
parsed.searchParams.delete('_rsc')
return parsed.pathname + parsed.search + parsed.hash
}

async function waitForValidation(targetUrl: string) {
const parsedTargetUrl = new URL(targetUrl)
const relativeTargetUrl =
parsedTargetUrl.pathname + parsedTargetUrl.search + parsedTargetUrl.hash

const requestId = await retry(
async () => {
const events = parseValidationMessages(getCliOutputSinceMark())
const start = events.find(
(e) =>
e.type === 'validation_start' &&
normalizeValidationUrl(e.url) === relativeTargetUrl
)
expect(start).toBeDefined()
return start!.requestId
},
undefined,
undefined,
`wait for validation of '${relativeTargetUrl}' to start`
)

await retry(
async () => {
const events = parseValidationMessages(getCliOutputSinceMark())
const end = events.find(
(e) => e.type === 'validation_end' && e.requestId === requestId
)
expect(end).toBeDefined()
},
undefined,
undefined,
'wait for validation to end'
)
}
const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next)

it('named export - export { instant }', async () => {
const browser = await next.browser('/named-export')
await waitForValidation(await browser.url())
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -117,8 +60,7 @@ describe('instant validation causes', () => {

it('aliased export - export { instantConfig as instant }', async () => {
const browser = await next.browser('/aliased-export')
await waitForValidation(await browser.url())
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -147,8 +89,7 @@ describe('instant validation causes', () => {

it('re-export - export { instant } from "./config"', async () => {
const browser = await next.browser('/reexport')
await waitForValidation(await browser.url())
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -177,11 +118,10 @@ describe('instant validation causes', () => {

it('indirect export - const instantConfig = _instant; export { instantConfig as instant }', async () => {
const browser = await next.browser('/indirect-export')
await waitForValidation(await browser.url())
// Ideally we'd be pointing at the original value declaration.
// We're not following declarations recursively mostly to keep the implementation simpler
// presuming that almost all configs are just `export const instant = ...`
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { nextTestSetup } from 'e2e-utils'
import { expectBuildValidationSkipped } from 'e2e-utils/instant-validation'
import {
createGetInstantInsight,
expectBuildValidationSkipped,
} from 'e2e-utils/instant-validation'
import { waitForNoErrorToast } from '../../../lib/next-test-utils'

// This fixture intentionally omits `experimental.instantInsights` from
Expand Down Expand Up @@ -27,9 +30,30 @@ describe('instant validation - default level', () => {
return
}

let currentCliOutputIndex = 0
beforeEach(() => {
currentCliOutputIndex = next.cliOutput.length
})

function getCliOutputSinceMark(): string {
if (next.cliOutput.length < currentCliOutputIndex) {
currentCliOutputIndex = 0
}
return next.cliOutput.slice(currentCliOutputIndex)
}

const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next)

if (isNextStart) {
beforeAll(async () => {
await next.build({ args: ['--experimental-build-mode', 'compile'] })
const result = await next.build({
args: ['--experimental-build-mode', 'compile'],
})
if (result.exitCode !== 0) {
throw new Error(
`Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}`
)
}
})
afterEach(async () => {
await next.stop()
Expand All @@ -55,7 +79,7 @@ describe('instant validation - default level', () => {
describe('dev', () => {
it('bare page: framework default matches `warning`, implicit validation fires', async () => {
const browser = await next.browser('/bare')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"description": "Next.js encountered uncached data during a navigation.",
"environmentLabel": "Server",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { nextTestSetup } from 'e2e-utils'
import {
createGetInstantInsight,
expectBuildValidationSkipped,
extractBuildValidationError,
} from 'e2e-utils/instant-validation'
Expand All @@ -21,9 +22,30 @@ describe('instant validation - level error', () => {
return
}

let currentCliOutputIndex = 0
beforeEach(() => {
currentCliOutputIndex = next.cliOutput.length
})

function getCliOutputSinceMark(): string {
if (next.cliOutput.length < currentCliOutputIndex) {
currentCliOutputIndex = 0
}
return next.cliOutput.slice(currentCliOutputIndex)
}

const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next)

if (isNextStart) {
beforeAll(async () => {
await next.build({ args: ['--experimental-build-mode', 'compile'] })
const result = await next.build({
args: ['--experimental-build-mode', 'compile'],
})
if (result.exitCode !== 0) {
throw new Error(
`Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}`
)
}
})
afterEach(async () => {
await next.stop()
Expand Down Expand Up @@ -58,7 +80,7 @@ describe('instant validation - level error', () => {
describe('dev', () => {
it('bare page: implicit validation surfaces a redbox (error level fires)', async () => {
const browser = await next.browser('/bare')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"description": "Next.js encountered uncached data during a navigation.",
"environmentLabel": "Server",
Expand All @@ -75,7 +97,7 @@ describe('instant validation - level error', () => {

it('explicit-error page: explicit override at the configured level, instant redbox in dev', async () => {
const browser = await next.browser('/explicit-error')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -104,7 +126,7 @@ describe('instant validation - level error', () => {

it('explicit-true page: aliases to error level, instant redbox in dev', async () => {
const browser = await next.browser('/explicit-true')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -133,7 +155,7 @@ describe('instant validation - level error', () => {

it('explicit-warning page: per-segment de-escalation still validates in dev', async () => {
const browser = await next.browser('/explicit-warning')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -171,7 +193,7 @@ describe('instant validation - level error', () => {
// that's per-segment — it doesn't shield descendants. The bare
// page should still surface an instant redbox in dev.
const browser = await next.browser('/layered')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"description": "Next.js encountered uncached data during a navigation.",
"environmentLabel": "Server",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { nextTestSetup } from 'e2e-utils'
import {
createGetInstantInsight,
expectBuildValidationSkipped,
extractBuildValidationError,
} from 'e2e-utils/instant-validation'
Expand All @@ -21,9 +22,30 @@ describe('instant validation - level manual-error', () => {
return
}

let currentCliOutputIndex = 0
beforeEach(() => {
currentCliOutputIndex = next.cliOutput.length
})

function getCliOutputSinceMark(): string {
if (next.cliOutput.length < currentCliOutputIndex) {
currentCliOutputIndex = 0
}
return next.cliOutput.slice(currentCliOutputIndex)
}

const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next)

if (isNextStart) {
beforeAll(async () => {
await next.build({ args: ['--experimental-build-mode', 'compile'] })
const result = await next.build({
args: ['--experimental-build-mode', 'compile'],
})
if (result.exitCode !== 0) {
throw new Error(
`Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}`
)
}
})
afterEach(async () => {
await next.stop()
Expand Down Expand Up @@ -65,7 +87,7 @@ describe('instant validation - level manual-error', () => {

it('explicit-error page: explicit override at the configured level, instant redbox in dev', async () => {
const browser = await next.browser('/explicit-error')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -94,7 +116,7 @@ describe('instant validation - level manual-error', () => {

it('explicit-true page: aliases to error level, instant redbox in dev', async () => {
const browser = await next.browser('/explicit-true')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down Expand Up @@ -123,7 +145,7 @@ describe('instant validation - level manual-error', () => {

it('explicit-warning page: per-segment de-escalation still validates in dev', async () => {
const browser = await next.browser('/explicit-warning')
await expect(browser).toDisplayCollapsedRedbox(`
expect(await getInstantInsight(browser)).toMatchInlineSnapshot(`
{
"cause": [
{
Expand Down
Loading
Loading