Files
next.js/test/production/deterministic-build/deployment-id.test.ts
Arian Tron 61f56f997c
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
first commit
2026-03-10 19:37:31 +03:30

284 lines
8.3 KiB
TypeScript

import { FileRef, NextInstance, nextTestSetup } from 'e2e-utils'
import path from 'path'
import fs from 'fs/promises'
import { promisify } from 'util'
import crypto from 'crypto'
import globOrig from 'glob'
import { diff } from 'jest-diff'
const glob = promisify(globOrig)
const IGNORE_CONTENT_NEXT_REGEX = new RegExp(
[
// This contains the deployment id, but these changing fields are stripped by the builder
'routes-manifest\\.json',
// These contain the build id and deployment id (but are not deployed to the serverless function)
'.*\\.html',
'.*\\.rsc',
// These are not critical, as they aren't deployed to the serverless function
'client-build-manifest\\.json',
'fallback-build-manifest\\.json',
]
.map((v) => '(?:\\/|^)' + v + '$')
.join('|')
)
async function readFilesNext(
next: NextInstance
): Promise<Map<string, Map<string, string>>> {
// These are cosmetic files which aren't deployed.
const IGNORE = /^trace$|^trace-build$/
const files = (
(await glob('**/*', {
cwd: path.join(next.testDir, next.distDir),
nodir: true,
dot: true,
})) as string[]
)
.filter((f) => !IGNORE.test(f) && !IGNORE_CONTENT_NEXT_REGEX.test(f))
.sort()
return new Map([
[
'next',
new Map(
await Promise.all(
files.map(async (f) => {
const content = await next.readFile(path.join(next.distDir, f))
return [f, content] as const
})
)
),
],
])
}
async function readFilesBuilder(
next: NextInstance
): Promise<Map<string, Map<string, string>>> {
const functions = (
(await glob('.vercel/output/functions/*.func/.vc-config.json', {
cwd: next.testDir,
nodir: true,
})) as string[]
).sort()
return new Map(
await Promise.all(
functions.map(async (fn) => {
let config = await next.readJSON(fn)
let fnDir = path.dirname(fn)
let files = [
...(
await glob('**/*', {
cwd: path.join(next.testDir, fnDir),
nodir: true,
dot: true,
ignore: ['.vc-config.json'],
})
).map((f) => path.join(fnDir, f)),
...Object.values(config.filePathMap),
] as string[]
files.sort()
return [
fn,
new Map(
await Promise.all(
files.map(async (f: string) => {
let symlinkTarget: string | undefined = await fs
.readlink(path.join(next.testDir, f))
.catch(() => null)
if (symlinkTarget) {
return [f, symlinkTarget] as const
} else if (f.includes('node_modules')) {
// Use hash to avoid OOMs from loading all node_modules content
return [
f,
crypto
.createHash('sha1')
.update(await next.readFile(f))
.digest('hex'),
] as const
} else {
return [f, await next.readFile(f)] as const
}
})
)
),
] as const
})
)
)
}
async function runTest(
next: NextInstance,
readFiles: (next: NextInstance) => Promise<Map<string, Map<string, string>>>
) {
// Same for both builds
next.env['__NEXT_IMMUTABLE_ASSET_TOKEN'] = 'imm-token'
// First build
next.env['NEXT_DEPLOYMENT_ID'] = 'foo-dpl-id'
expect((await next.build()).exitCode).toBe(0)
let run1 = await readFiles(next)
// Second build
next.env['NEXT_DEPLOYMENT_ID'] = 'bar-dpl-id'
expect((await next.build()).exitCode).toBe(0)
let run2 = await readFiles(next)
// First, compare file names
let run1FileNames = [...run1.entries()].map(([fn, files]) => [
fn,
[...files.keys()],
])
let run2FileNames = [...run2.entries()].map(([fn, files]) => [
fn,
[...files.keys()],
])
expect(run1FileNames).toEqual(run2FileNames)
let run1Map = new Map(run1)
let run2Map = new Map(run2)
let errors = []
for (const [fn, files1] of run1Map) {
const files2 = run2Map.get(fn)
for (const [fileName, content1] of files1) {
const content2 = files2?.get(fileName)
if (content1 !== content2) {
errors.push(
`File content mismatch for ${fileName} in ${fn}\n\n` +
diff(content1 ?? '', content2 ?? '', {
contextLines: 2,
expand: false,
})
)
}
}
}
for (const [fn, files2] of run2Map) {
for (const [fileName, content2] of files2) {
if (!run1Map.get(fn)?.has(fileName)) {
errors.push(
`File content mismatch for ${fileName} in ${fn}\n\n` +
diff('', content2 ?? '', {
contextLines: 2,
expand: false,
})
)
}
}
}
if (errors.length > 0) {
throw new Error(errors.join('\n\n'))
}
return { run1, run2 }
}
const FILES = {
standard: {
app: new FileRef(path.join(__dirname, 'standard', 'app')),
pages: new FileRef(path.join(__dirname, 'standard', 'pages')),
public: new FileRef(path.join(__dirname, 'standard', 'public')),
'instrumentation.ts': new FileRef(
path.join(__dirname, 'standard', 'instrumentation.ts')
),
'middleware.ts': new FileRef(
path.join(__dirname, 'standard', 'middleware.ts')
),
'next.config.js': new FileRef(
path.join(__dirname, 'standard', 'next.config.js')
),
},
cacheComponents: {
app: new FileRef(path.join(__dirname, 'cache-components', 'app')),
'next.config.js': new FileRef(
path.join(__dirname, 'cache-components', 'next.config.js')
),
},
}
// Webpack itself isn't deterministic
;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)(
'deterministic build - changing deployment id',
() => {
describe('standard - .next folder', () => {
const { next } = nextTestSetup({
files: {
...FILES.standard,
},
env: {
NOW_BUILDER: '1',
},
skipStart: true,
skipDeployment: true,
disableAutoSkewProtection: true,
})
it('should produce identical build outputs even when changing deployment id', async () => {
await runTest(next, readFilesNext)
})
})
describe.each([
{ test: 'standard', mode: 'builder' },
{ test: 'standard', mode: 'adapter' },
{ test: 'cacheComponents', mode: 'builder' },
{ test: 'cacheComponents', mode: 'adapter' },
])('build output API - $test $mode', ({ test, mode }) => {
const { next } = nextTestSetup({
files: {
// A mock file to be able to run `vercel build` without logging in
'.vercel/project.json': `{ "projectId": "prj_", "orgId": "team_", "settings": {} }`,
...FILES[test],
},
packageJson: {
scripts: {
dev: 'next dev',
build: 'next build',
start: 'next start',
},
},
// We use NEXT_TEST_PREFER_OFFLINE, so just declaring `vercel: latest` as a dependency still
// doesn't force the latest version.
buildCommand: 'pnpm dlx vercel@latest build',
env:
mode === 'adapter'
? {
NEXT_ENABLE_ADAPTER: '1',
}
: undefined,
skipStart: true,
skipDeployment: true,
disableAutoSkewProtection: true,
})
it('should produce identical build outputs even when changing deployment id', async () => {
let { run1, run2 } = await runTest(next, readFilesBuilder)
expect(run1.size).toBeGreaterThan(0)
expect([...run1.keys()]).toEqual([...run2.keys()])
if (test === 'standard') {
expect([...run1.keys()]).toIncludeAllMembers([
'.vercel/output/functions/app-page.func/.vc-config.json',
'.vercel/output/functions/app-page.rsc.func/.vc-config.json',
'.vercel/output/functions/app-route.func/.vc-config.json',
'.vercel/output/functions/app-route.rsc.func/.vc-config.json',
'.vercel/output/functions/pages-dynamic.func/.vc-config.json',
'.vercel/output/functions/pages-static-gsp.func/.vc-config.json',
])
expect([...run1.keys()]).toSatisfyAny((k) =>
k.includes('middleware.func')
)
}
})
})
}
)