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
284 lines
8.3 KiB
TypeScript
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')
|
|
)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
)
|