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
29 changes: 23 additions & 6 deletions src/build/content/prerendered.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const RSC_SEGMENT_SUFFIX = '.segment.rsc'
const buildAppCacheValue = async (
path: string,
shouldUseAppPageKind: boolean,
rscIsRequired = true,
): Promise<NetlifyCachedAppPageValue | NetlifyCachedPageValue> => {
const meta = JSON.parse(await readFile(`${path}.meta`, 'utf-8')) as RouteMetadata
const html = await readFile(`${path}.html`, 'utf-8')
Expand Down Expand Up @@ -104,9 +105,16 @@ const buildAppCacheValue = async (
return {
kind: 'APP_PAGE',
html,
rscData: await readFile(`${path}.rsc`, 'base64').catch(() =>
readFile(`${path}.prefetch.rsc`, 'base64'),
),
rscData: await readFile(`${path}.rsc`, 'base64')
.catch(() => readFile(`${path}.prefetch.rsc`, 'base64'))
.catch((error) => {
if (rscIsRequired) {
throw error
}
// disabling unicorn/no-useless-undefined because we need to return undefined explicitly to satisfy types
// eslint-disable-next-line unicorn/no-useless-undefined
return undefined
}),
segmentData,
...meta,
}
Expand Down Expand Up @@ -186,6 +194,8 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
})
: false

let appRouterNotFoundDefinedInPrerenderManifest = false

await Promise.all([
...Object.entries(manifest.routes).map(
([route, meta]): Promise<void> =>
Expand Down Expand Up @@ -215,7 +225,11 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
value = await buildAppCacheValue(
join(ctx.publishDir, 'server/app', key),
shouldUseAppPageKind,
meta.renderingMode !== 'PARTIALLY_STATIC',
)
if (route === '/_not-found') {
appRouterNotFoundDefinedInPrerenderManifest = true
}
break
case meta.dataRoute === null:
value = await buildRouteCacheValue(
Expand Down Expand Up @@ -262,9 +276,12 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
),
])

// app router 404 pages are not in the prerender manifest
// so we need to check for them manually
if (existsSync(join(ctx.publishDir, `server/app/_not-found.html`))) {
// app router 404 pages are not in the prerender manifest for some next.js versions
// so we need to check for them manually if prerender manifest does not include it
if (
!appRouterNotFoundDefinedInPrerenderManifest &&
existsSync(join(ctx.publishDir, `server/app/_not-found.html`))
) {
const lastModified = Date.now()
const key = '/404'
const value = await buildAppCacheValue(
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/ppr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"test": {
"dependencies": {
"next": "canary"
"next": "canary || >=16.0.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were not running those tests on latest even tho cacheComponents can be used on stable releases.

}
}
}
4 changes: 2 additions & 2 deletions tests/integration/cache-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ describe('app router', () => {
const blobEntries = await getBlobEntries(ctx)
expect(blobEntries.map(({ key }) => decodeBlobKey(key)).sort()).toEqual(
[
'/404',
shouldHaveAppRouterNotFoundInPrerenderManifest() ? undefined : '/404',
shouldHaveAppRouterGlobalErrorInPrerenderManifest() ? '/_global-error' : undefined,
shouldHaveAppRouterNotFoundInPrerenderManifest() ? '/_not-found' : undefined,
'/index',
Expand Down Expand Up @@ -372,7 +372,7 @@ describe('plugin', () => {
const blobEntries = await getBlobEntries(ctx)
expect(blobEntries.map(({ key }) => decodeBlobKey(key)).sort()).toEqual(
[
'/404',
shouldHaveAppRouterNotFoundInPrerenderManifest() ? undefined : '/404',
shouldHaveAppRouterGlobalErrorInPrerenderManifest() ? '/_global-error' : undefined,
shouldHaveAppRouterNotFoundInPrerenderManifest() ? '/_not-found' : undefined,
'/api/revalidate-handler',
Expand Down
65 changes: 32 additions & 33 deletions tests/integration/simple-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ test<FixtureTestContext>('Test that the simple next app is working', async (ctx)
const blobEntries = await getBlobEntries(ctx)
expect(blobEntries.map(({ key }) => decodeBlobKey(key)).sort()).toEqual(
[
'/404',
shouldHaveAppRouterNotFoundInPrerenderManifest() ? undefined : '/404',
shouldHaveAppRouterNotFoundInPrerenderManifest() ? '/_not-found' : undefined,
'/api/cached-permanent',
'/api/cached-revalidate',
Expand Down Expand Up @@ -392,41 +392,40 @@ test<FixtureTestContext>('rewrites to external addresses dont use compression',
expect(gunzipSync(page.bodyBuffer).toString('utf-8')).toContain('<title>Example Domain</title>')
})

test.skipIf(process.env.NEXT_VERSION !== 'canary')<FixtureTestContext>(
'Test that a simple next app with PPR is working',
async (ctx) => {
await createFixture('ppr', ctx)
await runPlugin(ctx)
// check if the blob entries where successful set on the build plugin
const blobEntries = await getBlobEntries(ctx)
expect(blobEntries.map(({ key }) => decodeBlobKey(key)).sort()).toEqual(
[
'/1',
'/2',
'/404',
isExperimentalPPRHardDeprecated() ? undefined : '/[dynamic]',
shouldHaveAppRouterGlobalErrorInPrerenderManifest() ? '/_global-error' : undefined,
shouldHaveAppRouterNotFoundInPrerenderManifest() ? '/_not-found' : undefined,
'/index',
'404.html',
'500.html',
].filter(Boolean),
)
test.skipIf(
process.env.NEXT_VERSION !== 'canary' && nextVersionSatisfies('<16.0.0'),
)<FixtureTestContext>('Test that a simple next app with PPR is working', async (ctx) => {
await createFixture('ppr', ctx)
await runPlugin(ctx)
// check if the blob entries where successful set on the build plugin
const blobEntries = await getBlobEntries(ctx)
expect(blobEntries.map(({ key }) => decodeBlobKey(key)).sort()).toEqual(
[
'/1',
'/2',
shouldHaveAppRouterNotFoundInPrerenderManifest() ? undefined : '/404',
isExperimentalPPRHardDeprecated() ? undefined : '/[dynamic]',
shouldHaveAppRouterGlobalErrorInPrerenderManifest() ? '/_global-error' : undefined,
shouldHaveAppRouterNotFoundInPrerenderManifest() ? '/_not-found' : undefined,
'/index',
'404.html',
'500.html',
].filter(Boolean),
)

// test the function call
const home = await invokeFunction(ctx)
expect(home.statusCode).toBe(200)
expect(load(home.body)('h1').text()).toBe('Home')
// test the function call
const home = await invokeFunction(ctx)
expect(home.statusCode).toBe(200)
expect(load(home.body)('h1').text()).toBe('Home')

const dynamicPrerendered = await invokeFunction(ctx, { url: '/1' })
expect(dynamicPrerendered.statusCode).toBe(200)
expect(load(dynamicPrerendered.body)('h1').text()).toBe('Dynamic Page: 1')
const dynamicPrerendered = await invokeFunction(ctx, { url: '/1' })
expect(dynamicPrerendered.statusCode).toBe(200)
expect(load(dynamicPrerendered.body)('h1').text()).toBe('Dynamic Page: 1')

const dynamicNotPrerendered = await invokeFunction(ctx, { url: '/3' })
expect(dynamicNotPrerendered.statusCode).toBe(200)
expect(load(dynamicNotPrerendered.body)('h1').text()).toBe('Dynamic Page: 3')
},
)
const dynamicNotPrerendered = await invokeFunction(ctx, { url: '/3' })
expect(dynamicNotPrerendered.statusCode).toBe(200)
expect(load(dynamicNotPrerendered.body)('h1').text()).toBe('Dynamic Page: 3')
})

// setup for this test only works with webpack builds due to usage of ` __non_webpack_require__` to avoid bundling a file
test.skipIf(hasDefaultTurbopackBuilds())<FixtureTestContext>(
Expand Down
53 changes: 37 additions & 16 deletions tests/utils/next-version-helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import fg from 'fast-glob'
import { coerce, gt, gte, satisfies, valid } from 'semver'
import { execaCommand } from 'execa'

const FUTURE_NEXT_PATCH_VERSION = '15.999.0'
const FUTURE_NEXT_PATCH_VERSION = '16.999.0'

const NEXT_VERSION_REQUIRES_REACT_19 = '14.3.0-canary.45'
const REACT_18_VERSION = '18.2.0'
Expand Down Expand Up @@ -114,23 +114,44 @@ export async function setNextVersionInFixture(
packageJsons.map(async (packageJsonPath) => {
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
if (packageJson.dependencies?.next) {
const versionConstraint = packageJson.test?.dependencies?.next
// We can't use semver to check "canary" or "latest", so we use a fake future minor version
const checkVersion = isSemverVersion ? resolvedVersion : FUTURE_NEXT_PATCH_VERSION
if (
operation === 'update' &&
versionConstraint &&
!(versionConstraint === 'canary'
? isNextCanary()
: satisfies(checkVersion, versionConstraint, { includePrerelease: true })) &&
version !== versionConstraint
) {
if (!silent) {
console.log(
`${logPrefix}⏩ Skipping '${packageJson.name}' because it requires next@${versionConstraint}`,
/** @type {string | undefined} */
const versionConstraints = packageJson.test?.dependencies?.next

if (versionConstraints) {
// We need to be able to define constraint such as "canary or >=16.0.0" for testing features that might have
// canary-only support (you can only even try them on canary releases) that later on get promoted to be usable in stable
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional context about "canary-only" support - Next.js tends to have checks like so https://github.com/vercel/next.js/blob/f54479b9dfea3cb9b092a1e11020db0d438454cc/packages/next/src/server/config.ts#L401-L408 or https://github.com/vercel/next.js/blob/v15.5.6/packages/next/src/server/config.ts#L371-L382 (depending on version)

So for features like this we kind of need a way to check if it's canary, but there is not a good way to check this with some kind of semver range, so we kind of need a special case for it. We already did have one, but it didn't support alternatives, which is what this change is adding, so we could continue to be able to run tests on previous canaries + newer stable versions

// releases.
// There is no proper semver range to express "canary" portion of it, so we have to implement custom logic here

if (versionConstraints.includes('(') || versionConstraints.includes(')')) {
// We currently don't have a use case for complex semver ranges, so to simplify the implementation we literally
// just split on '||' and check each part separately. This might not work work with '()' groups in semver ranges
// so we throw here for clarity that it's not supported
throw new Error(
`${logPrefix}Complex semver ranges with '()' groups are not supported in test.dependencies.next: ${versionConstraints}`,
)
}
return { packageJsonPath, needUpdate: false }

// We can't use semver to check "canary" or "latest", so we use a fake future minor version
const checkVersion = isSemverVersion ? resolvedVersion : FUTURE_NEXT_PATCH_VERSION

if (
operation === 'update' &&
version !== versionConstraints &&
!versionConstraints.split('||').some((versionConstraintUntrimmedPart) => {
const versionConstraintPart = versionConstraintUntrimmedPart.trim()
return versionConstraintPart === 'canary'
? isNextCanary()
: satisfies(checkVersion, versionConstraintPart, { includePrerelease: true })
})
) {
if (!silent) {
console.log(
`${logPrefix}⏩ Skipping '${packageJson.name}' because it requires next@${versionConstraints}`,
)
}
return { packageJsonPath, needUpdate: false }
}
}
}
return { packageJsonPath, needUpdate: true }
Expand Down
Loading