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
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_next_root_app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"packages/next-plugin/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch"
},
"note": "Next apps with `app/` or `pages/` at the project root (no `src/`) are prewarmed and waited on like `src/` apps, so Turbopack production builds no longer fall back to a 2.5 s idle wait before serving the base CSS. Source scans skip dot directories such as `.next` and `.git`",
"date": "2026-11-15T00:00:00.000Z"
}
42 changes: 42 additions & 0 deletions packages/next-plugin/src/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,28 @@ describe('DevupUINextPlugin', () => {
})
})

it('reads routes from the project root when it holds app or pages', () => {
// Given: a Next app with `app/` at the root and no `src/`.
existsSyncSpy.mockImplementation(
(path: fs.PathLike) => String(path) === resolve('app'),
)
const graphSpy = spyOn(
importGraphModule,
'buildStaticImportGraph',
).mockReturnValue(createSingleFileGraph(resolve('app/page.tsx')))
try {
// When
DevupUI({}).webpack!(createWebpackConfig(), createWebpackContext())
// Then
expect(graphSpy).toHaveBeenCalledWith(
process.cwd(),
resolve('tsconfig.json'),
)
} finally {
graphSpy.mockRestore()
}
})

it('should apply webpack plugin with dev', async () => {
const ret = DevupUI({})

Expand Down Expand Up @@ -1027,6 +1049,26 @@ export const box = style({ color: 'red' })`
expect.objectContaining({ sourceMap: true }),
)
})
it('keeps configuring Turbopack when the import graph cannot be read', () => {
process.env.TURBOPACK = '1'
process.env.DEVUP_UI_PROFILE = '1'
const graphSpy = spyOn(
importGraphModule,
'buildStaticImportGraph',
).mockImplementation(() => {
throw new Error('unreadable')
})
const profileSpy = spyOn(console, 'info').mockImplementation(() => {})
try {
expect(DevupUI({}).turbopack?.rules).toBeDefined()
expect(profileSpy).toHaveBeenCalledWith(
expect.stringContaining('"failed":true'),
)
} finally {
profileSpy.mockRestore()
graphSpy.mockRestore()
}
})
it('should create theme.d.ts file', async () => {
process.env.TURBOPACK = '1'
existsSyncSpy.mockReturnValue(true)
Expand Down
14 changes: 12 additions & 2 deletions packages/next-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ export function resetTurboSetupCacheForTesting(): void {
productionTurboSetupOwner = `${performance.timeOrigin}-${Math.random()}`
}

/**
* The directory Next reads routes from: the project root when it holds `app`
* or `pages`, otherwise `src`, as Next itself decides.
*/
function sourceDir(cwd: string): string {
return ['app', 'pages'].some((dir) => existsSync(resolve(cwd, dir)))
? cwd
: resolve(cwd, 'src')
}

/**
* Devup UI Next Plugin
* @param config
Expand Down Expand Up @@ -302,7 +312,7 @@ export function DevupUI(
let staticGraph: StaticImportGraph | undefined
const graphStartedAt = profileStart()
try {
const srcDir = resolve(process.cwd(), 'src')
const srcDir = sourceDir(process.cwd())
const tsconfigPath = resolve(process.cwd(), 'tsconfig.json')
const cwd = process.cwd()
// One scan+parse of the source tree, shared by all three consumers below.
Expand Down Expand Up @@ -638,7 +648,7 @@ export function DevupUI(
)
if (!_options.dev && productionPrewarmFiles === undefined) {
const cwd = process.cwd()
const srcDir = resolve(cwd, 'src')
const srcDir = sourceDir(cwd)
const tsconfigPath = resolve(cwd, 'tsconfig.json')
const graph = buildStaticImportGraph(srcDir, tsconfigPath)
productionPrewarmFiles = collectProductionPrewarmFiles({
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-utils/src/__tests__/numbering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('collectNumberedFiles', () => {
write('src/a.tsx')
write('src/nested/c.ts')
write('src/readme.md')
write('src/.cache/skipped.tsx')
write('node_modules/@acme/ui/Button.tsx')
write('node_modules/@acme/ui/node_modules/dep/skipped.tsx')
})
Expand Down
6 changes: 4 additions & 2 deletions packages/plugin-utils/src/import-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,8 @@ export function compareCodePoints(a: string, b: string): number {

/**
* Enumerate every extractable source file under `srcDir`, sorted by POSIX path
* (deterministic order). Skips `node_modules`, test/spec files, and non-JS/TS
* (deterministic order). Skips `node_modules`, dot directories such as
* `.next` and `.git`, test/spec files, and non-JS/TS
* files — the SAME filter `buildCanonicalMap` uses internally, so a plugin can
* pre-warm the extractor over exactly the file set the canonical map was built
* from. Returns absolute paths.
Expand All @@ -635,7 +636,8 @@ export function listSourceFiles(
for (const entry of entries) {
const entryPath = join(dir, entry.name)
if (entry.isDirectory()) {
if (entry.name === 'node_modules') continue
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
continue
visit(entryPath)
continue
}
Expand Down
Loading