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
464 lines
15 KiB
TypeScript
464 lines
15 KiB
TypeScript
/* eslint-disable jest/no-standalone-expect */
|
|
import { nextTestSetup, isNextDev } from 'e2e-utils'
|
|
import { waitFor } from 'next-test-utils'
|
|
import fs from 'fs/promises'
|
|
import { readFileSync, existsSync } from 'fs'
|
|
import path from 'path'
|
|
import type { TraceEvent } from 'next/dist/trace'
|
|
|
|
function parseTraceFile(tracePath: string): TraceEvent[] {
|
|
const traceContent = readFileSync(tracePath, 'utf8')
|
|
const traceLines = traceContent
|
|
.trim()
|
|
.split('\n')
|
|
.filter((line) => line.trim())
|
|
|
|
const allEvents: TraceEvent[] = []
|
|
for (const line of traceLines) {
|
|
const events = JSON.parse(line) as TraceEvent[]
|
|
allEvents.push(...events)
|
|
}
|
|
return allEvents
|
|
}
|
|
|
|
async function getDirectorySize(dirPath: string): Promise<number> {
|
|
try {
|
|
await fs.access(dirPath)
|
|
} catch {
|
|
return 0
|
|
}
|
|
let totalSize = 0
|
|
const entries = await fs.readdir(dirPath, {
|
|
recursive: true,
|
|
withFileTypes: true,
|
|
})
|
|
for (const entry of entries) {
|
|
if (entry.isFile()) {
|
|
const filePath = path.join(entry.parentPath ?? entry.path, entry.name)
|
|
const stat = await fs.stat(filePath)
|
|
totalSize += stat.size
|
|
}
|
|
}
|
|
return totalSize
|
|
}
|
|
|
|
for (const cacheEnabled of [false, true]) {
|
|
describe(`filesystem-caching with cache ${cacheEnabled ? 'enabled' : 'disabled'}`, () => {
|
|
beforeAll(() => {
|
|
process.env.NEXT_PUBLIC_ENV_VAR = 'hello world'
|
|
})
|
|
afterAll(() => {
|
|
delete process.env.NEXT_PUBLIC_ENV_VAR
|
|
})
|
|
|
|
let envVars = [
|
|
`ENABLE_CACHING=${cacheEnabled ? '1' : ''}`,
|
|
// Make it easier to run in development, test directories are cleared between runs already so this is safe.
|
|
`TURBO_ENGINE_IGNORE_DIRTY=1`,
|
|
// decrease the idle timeout to make the test more reliable
|
|
`TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000`,
|
|
].join(' ')
|
|
|
|
const { skipped, next, isTurbopack } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
packageJson: {
|
|
scripts: {
|
|
build: `${envVars} next build`,
|
|
dev: `${envVars} next dev`,
|
|
start: 'next start',
|
|
},
|
|
},
|
|
// We need to use npm here as pnpms symlinks trigger a weird bug (kernel bug?)
|
|
installCommand: 'npm i',
|
|
// Next is always started with caching, but this can disable it for the followup restarts
|
|
buildCommand: `npm run build`,
|
|
startCommand: isNextDev ? 'npm run dev' : 'npm run start',
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
beforeAll(() => {
|
|
// We can skip the dev watch delay since this is not an HMR test
|
|
;(next as any).handleDevWatchDelayBeforeChange = () => {}
|
|
;(next as any).handleDevWatchDelayAfterChange = () => {}
|
|
})
|
|
|
|
async function restartCycle(): Promise<number> {
|
|
await stop()
|
|
const cacheSize = await getCacheSize()
|
|
await start()
|
|
return cacheSize
|
|
}
|
|
|
|
async function stop() {
|
|
if (isNextDev) {
|
|
// Give FileSystem Cache time to write to disk
|
|
// Turbopack is configured to wait 1s above.
|
|
// Webpack has an idle timeout (after large changes) of 1s
|
|
// and we give time a bit more to allow writing to disk
|
|
await waitFor(3000)
|
|
}
|
|
await next.stop()
|
|
}
|
|
|
|
async function start() {
|
|
await next.start()
|
|
}
|
|
|
|
async function getCacheSize(): Promise<number> {
|
|
return getDirectorySize(
|
|
path.join(next.testDir, '.next', 'cache', 'turbopack')
|
|
)
|
|
}
|
|
|
|
// Ensure each test starts with a fresh server and no test leaks a
|
|
// running server into the next one.
|
|
beforeEach(async () => {
|
|
// stop() is a no-op if already stopped; start() rebuilds + starts.
|
|
await stop()
|
|
await start()
|
|
})
|
|
afterEach(async () => {
|
|
await stop()
|
|
})
|
|
|
|
// Very flakey with Webpack enabled
|
|
;(process.env.IS_TURBOPACK_TEST ? it : it.skip)(
|
|
'should cache or not cache loaders',
|
|
async () => {
|
|
let appTimestamp, unchangedTimestamp, appClientTimestamp, pagesTimestamp
|
|
{
|
|
const browser = await next.browser('/')
|
|
appTimestamp = await browser.elementByCss('main').text()
|
|
expect(appTimestamp).toMatch(/Timestamp = \d+$/)
|
|
await browser.close()
|
|
}
|
|
{
|
|
const browser = await next.browser('/unchanged')
|
|
unchangedTimestamp = await browser.elementByCss('main').text()
|
|
expect(unchangedTimestamp).toMatch(/Timestamp = \d+$/)
|
|
await browser.close()
|
|
}
|
|
{
|
|
const browser = await next.browser('/client')
|
|
appClientTimestamp = await browser.elementByCss('main').text()
|
|
expect(appClientTimestamp).toMatch(/Timestamp = \d+$/)
|
|
await browser.close()
|
|
}
|
|
{
|
|
const browser = await next.browser('/pages')
|
|
pagesTimestamp = await browser.elementByCss('main').text()
|
|
expect(pagesTimestamp).toMatch(/Timestamp = \d+$/)
|
|
await browser.close()
|
|
}
|
|
const initialCacheSize = await restartCycle()
|
|
|
|
{
|
|
const browser = await next.browser('/')
|
|
const newTimestamp = await browser.elementByCss('main').text()
|
|
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
|
|
if (cacheEnabled) {
|
|
expect(newTimestamp).toBe(appTimestamp)
|
|
} else {
|
|
expect(newTimestamp).not.toBe(appTimestamp)
|
|
}
|
|
await browser.close()
|
|
}
|
|
{
|
|
const browser = await next.browser('/unchanged')
|
|
const newTimestamp = await browser.elementByCss('main').text()
|
|
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
|
|
if (cacheEnabled) {
|
|
expect(newTimestamp).toBe(unchangedTimestamp)
|
|
} else {
|
|
expect(newTimestamp).not.toBe(unchangedTimestamp)
|
|
}
|
|
await browser.close()
|
|
}
|
|
{
|
|
const browser = await next.browser('/client')
|
|
const newTimestamp = await browser.elementByCss('main').text()
|
|
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
|
|
if (cacheEnabled) {
|
|
expect(newTimestamp).toBe(appClientTimestamp)
|
|
} else {
|
|
expect(newTimestamp).not.toBe(appClientTimestamp)
|
|
}
|
|
await browser.close()
|
|
}
|
|
{
|
|
const browser = await next.browser('/pages')
|
|
const newTimestamp = await browser.elementByCss('main').text()
|
|
expect(newTimestamp).toMatch(/Timestamp = \d+$/)
|
|
if (cacheEnabled) {
|
|
expect(newTimestamp).toBe(pagesTimestamp)
|
|
} else {
|
|
expect(newTimestamp).not.toBe(pagesTimestamp)
|
|
}
|
|
await browser.close()
|
|
}
|
|
|
|
if (cacheEnabled) {
|
|
const finalCacheSize = await restartCycle()
|
|
const increasePercent = (
|
|
(finalCacheSize / initialCacheSize - 1) *
|
|
100
|
|
).toFixed(2)
|
|
console.log(
|
|
`Cache size: ${(initialCacheSize / 1024 / 1024).toFixed(2)} MB -> ${(finalCacheSize / 1024 / 1024).toFixed(2)} MB (${increasePercent}%)`
|
|
)
|
|
expect(finalCacheSize).toBeLessThanOrEqual(initialCacheSize * 1.1)
|
|
}
|
|
}
|
|
)
|
|
|
|
function makeTextCheck(url: string, text: string) {
|
|
return textCheck.bind(null, url, text)
|
|
}
|
|
|
|
async function textCheck(url: string, text: string) {
|
|
const browser = await next.browser(url)
|
|
expect(await browser.elementByCss('p').text()).toBe(text)
|
|
await browser.close()
|
|
}
|
|
|
|
function makeFileEdit(file: string) {
|
|
return async (inner: () => Promise<void>) => {
|
|
await next.patchFile(
|
|
file,
|
|
(content) => {
|
|
return content.replace('hello world', 'hello filesystem cache')
|
|
},
|
|
inner
|
|
)
|
|
}
|
|
}
|
|
|
|
interface Change {
|
|
checkInitial(): Promise<void>
|
|
withChange(previous: () => Promise<void>): Promise<void>
|
|
checkChanged(): Promise<void>
|
|
fullInvalidation?: boolean
|
|
largeCacheIncrease?: boolean
|
|
}
|
|
const POTENTIAL_CHANGES: Record<string, Change> = {
|
|
'RSC change': {
|
|
checkInitial: makeTextCheck('/', 'hello world'),
|
|
withChange: makeFileEdit('app/page.tsx'),
|
|
checkChanged: makeTextCheck('/', 'hello filesystem cache'),
|
|
},
|
|
'RCC change': {
|
|
checkInitial: makeTextCheck('/client', 'hello world'),
|
|
withChange: makeFileEdit('app/client/page.tsx'),
|
|
checkChanged: makeTextCheck('/client', 'hello filesystem cache'),
|
|
},
|
|
'Pages change': {
|
|
checkInitial: makeTextCheck('/pages', 'hello world'),
|
|
withChange: makeFileEdit('pages/pages.tsx'),
|
|
checkChanged: makeTextCheck('/pages', 'hello filesystem cache'),
|
|
},
|
|
'rename app page': {
|
|
checkInitial: makeTextCheck('/remove-me', 'hello world'),
|
|
async withChange(inner) {
|
|
await next.renameFolder('app/remove-me', 'app/add-me')
|
|
try {
|
|
await inner()
|
|
} finally {
|
|
await next.renameFolder('app/add-me', 'app/remove-me')
|
|
}
|
|
},
|
|
checkChanged: makeTextCheck('/add-me', 'hello world'),
|
|
largeCacheIncrease: true,
|
|
},
|
|
// TODO fix this case with Turbopack
|
|
...(isTurbopack
|
|
? {}
|
|
: {
|
|
'loader change': {
|
|
async checkInitial() {
|
|
await textCheck('/loader', 'hello world')
|
|
await textCheck('/loader/client', 'hello world')
|
|
},
|
|
withChange: makeFileEdit('my-loader.js'),
|
|
async checkChanged() {
|
|
await textCheck('/loader', 'hello filesystem cache')
|
|
await textCheck('/loader/client', 'hello filesystem cache')
|
|
},
|
|
fullInvalidation: !isTurbopack,
|
|
},
|
|
}),
|
|
'next config change': {
|
|
async checkInitial() {
|
|
await textCheck('/next-config', 'hello world')
|
|
await textCheck('/next-config/client', 'hello world')
|
|
},
|
|
withChange: makeFileEdit('next.config.js'),
|
|
async checkChanged() {
|
|
await textCheck('/next-config', 'hello filesystem cache')
|
|
await textCheck('/next-config/client', 'hello filesystem cache')
|
|
},
|
|
fullInvalidation: !isTurbopack,
|
|
},
|
|
'env var change': {
|
|
async checkInitial() {
|
|
await textCheck('/env', 'hello world')
|
|
await textCheck('/env/client', 'hello world')
|
|
},
|
|
async withChange(inner) {
|
|
process.env.NEXT_PUBLIC_ENV_VAR = 'hello filesystem cache'
|
|
try {
|
|
await inner()
|
|
} finally {
|
|
process.env.NEXT_PUBLIC_ENV_VAR = 'hello world'
|
|
}
|
|
},
|
|
async checkChanged() {
|
|
await textCheck('/env', 'hello filesystem cache')
|
|
await textCheck('/env/client', 'hello filesystem cache')
|
|
},
|
|
},
|
|
} as const
|
|
|
|
// Checking only single change and all combined for performance reasons.
|
|
const combinations = Object.entries(POTENTIAL_CHANGES).map(([k, v]) => [
|
|
k,
|
|
[v],
|
|
]) as Array<[string, Array<Change>]>
|
|
combinations.push([
|
|
Object.keys(POTENTIAL_CHANGES).join(', '),
|
|
Object.values(POTENTIAL_CHANGES),
|
|
])
|
|
|
|
for (const [name, changes] of combinations) {
|
|
// Very flakey with Webpack enabled
|
|
;(process.env.IS_TURBOPACK_TEST ? it : it.skip)(
|
|
`should allow to change files while stopped (${name})`,
|
|
async () => {
|
|
let fullInvalidation = !cacheEnabled
|
|
let largeCacheIncrease = false
|
|
for (const change of changes) {
|
|
await change.checkInitial()
|
|
if (change.fullInvalidation) {
|
|
fullInvalidation = true
|
|
}
|
|
if (change.largeCacheIncrease) {
|
|
largeCacheIncrease = true
|
|
}
|
|
}
|
|
|
|
let unchangedTimestamp: string
|
|
if (!fullInvalidation) {
|
|
const browser = await next.browser('/unchanged')
|
|
unchangedTimestamp = await browser.elementByCss('main').text()
|
|
expect(unchangedTimestamp).toMatch(/Timestamp = \d+$/)
|
|
await browser.close()
|
|
}
|
|
|
|
async function checkChanged() {
|
|
for (const change of changes) {
|
|
await change.checkChanged()
|
|
}
|
|
|
|
if (!fullInvalidation) {
|
|
const browser = await next.browser('/unchanged')
|
|
const timestamp = await browser.elementByCss('main').text()
|
|
expect(unchangedTimestamp).toEqual(timestamp)
|
|
await browser.close()
|
|
}
|
|
}
|
|
|
|
await stop()
|
|
const initialCacheSize = await getCacheSize()
|
|
|
|
async function inner() {
|
|
await start()
|
|
await checkChanged()
|
|
// Some no-op change builds
|
|
for (let i = 0; i < 2; i++) {
|
|
await restartCycle()
|
|
await checkChanged()
|
|
}
|
|
await stop()
|
|
}
|
|
|
|
let current = inner
|
|
for (const change of changes) {
|
|
const prev = current
|
|
current = () => change.withChange(prev)
|
|
}
|
|
await current()
|
|
|
|
if (cacheEnabled) {
|
|
const finalCacheSize = await getCacheSize()
|
|
const maxIncrease = largeCacheIncrease ? 1.5 : 1.1
|
|
const increasePercent = (
|
|
(finalCacheSize / initialCacheSize - 1) *
|
|
100
|
|
).toFixed(2)
|
|
console.log(
|
|
`Cache size (${name}): ${(initialCacheSize / 1024 / 1024).toFixed(2)} MB -> ${(finalCacheSize / 1024 / 1024).toFixed(2)} MB (${increasePercent}%)`
|
|
)
|
|
expect(finalCacheSize).toBeLessThanOrEqual(
|
|
initialCacheSize * maxIncrease
|
|
)
|
|
}
|
|
|
|
await start()
|
|
for (const change of changes) {
|
|
await change.checkInitial()
|
|
}
|
|
|
|
if (!fullInvalidation) {
|
|
const browser = await next.browser('/unchanged')
|
|
const timestamp = await browser.elementByCss('main').text()
|
|
expect(unchangedTimestamp).toEqual(timestamp)
|
|
await browser.close()
|
|
}
|
|
},
|
|
200000
|
|
)
|
|
}
|
|
|
|
if (cacheEnabled) {
|
|
;(process.env.IS_TURBOPACK_TEST ? it : it.skip)(
|
|
'should emit turbopack-persistence trace spans',
|
|
async () => {
|
|
// Trigger a page load so persistence has data to write
|
|
const browser = await next.browser('/')
|
|
expect(await browser.elementByCss('main').text()).toMatch(
|
|
/Timestamp = \d+$/
|
|
)
|
|
await browser.close()
|
|
|
|
// Wait for persistence to write to disk
|
|
await stop()
|
|
|
|
const traceDir = isNextDev ? '.next/dev' : '.next'
|
|
const tracePath = path.join(next.testDir, traceDir, 'trace')
|
|
expect(existsSync(tracePath)).toBe(true)
|
|
|
|
const events = parseTraceFile(tracePath)
|
|
const persistenceEvents = events.filter(
|
|
(e) => e.name === 'turbopack-persistence'
|
|
)
|
|
|
|
expect(persistenceEvents.length).toBeGreaterThan(0)
|
|
|
|
// Verify the persistence event has the expected attributes
|
|
const event = persistenceEvents[0]
|
|
expect(event.duration).toBeGreaterThanOrEqual(0)
|
|
expect(event.tags).toBeDefined()
|
|
const tags = event.tags as Record<string, unknown>
|
|
expect(tags.reason).toBeDefined()
|
|
expect(typeof tags.snapshot_duration_ms).toBe('number')
|
|
expect(typeof tags.persist_duration_ms).toBe('number')
|
|
expect(typeof tags.task_count).toBe('number')
|
|
}
|
|
)
|
|
}
|
|
})
|
|
}
|