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
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -969,5 +969,6 @@
"968": "Invariant: getNextConfigEdge must only be called in edge runtime",
"969": "Invariant: nextConfig couldn't be loaded",
"970": "process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled",
"971": "The NEXT_DEPLOYMENT_ID environment variable value \"%s\" does not match the provided deploymentId \"%s\" in the config."
"971": "The NEXT_DEPLOYMENT_ID environment variable value \"%s\" does not match the provided deploymentId \"%s\" in the config.",
"972": "Failed to resolve pattern \"%s\": %s"
}
4 changes: 4 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,10 @@ export default async function build(
nextBuildSpan,
config,
cacheDir,
debugBuildPaths:
debugBuildAppPaths !== undefined || debugBuildPagePaths !== undefined
? { app: debugBuildAppPaths, pages: debugBuildPagePaths }
: undefined,
}

if (appDir && 'exportPathMap' in config) {
Expand Down
18 changes: 13 additions & 5 deletions packages/next/src/build/type-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ import { hrtimeDurationToString } from './duration-to-string'
function verifyTypeScriptSetup(
dir: string,
distDir: string,
intentDirs: string[],
typeCheckPreflight: boolean,
tsconfigPath: string | undefined,
disableStaticImages: boolean,
cacheDir: string | undefined,
enableWorkerThreads: boolean | undefined,
hasAppDir: boolean,
hasPagesDir: boolean,
isolatedDevBuild: boolean | undefined
isolatedDevBuild: boolean | undefined,
appDir: string | undefined,
pagesDir: string | undefined,
debugBuildPaths: { app?: string[]; pages?: string[] } | undefined
) {
const typeCheckWorker = new Worker(
require.resolve('../lib/verify-typescript-setup'),
Expand All @@ -48,14 +50,16 @@ function verifyTypeScriptSetup(
.verifyTypeScriptSetup({
dir,
distDir,
intentDirs,
typeCheckPreflight,
tsconfigPath,
disableStaticImages,
cacheDir,
hasAppDir,
hasPagesDir,
isolatedDevBuild,
appDir,
pagesDir,
debugBuildPaths,
})
.then((result) => {
typeCheckWorker.end()
Expand All @@ -76,6 +80,7 @@ export async function startTypeChecking({
pagesDir,
telemetry,
appDir,
debugBuildPaths,
}: {
cacheDir: string
config: NextConfigComplete
Expand All @@ -84,6 +89,7 @@ export async function startTypeChecking({
pagesDir?: string
telemetry: Telemetry
appDir?: string
debugBuildPaths?: { app?: string[]; pages?: string[] }
}) {
const ignoreTypeScriptErrors = Boolean(config.typescript.ignoreBuildErrors)

Expand Down Expand Up @@ -111,15 +117,17 @@ export async function startTypeChecking({
verifyTypeScriptSetup(
dir,
config.distDir,
[pagesDir, appDir].filter(Boolean) as string[],
!ignoreTypeScriptErrors,
config.typescript.tsconfigPath,
config.images.disableStaticImages,
cacheDir,
config.experimental.workerThreads,
!!appDir,
!!pagesDir,
config.experimental.isolatedDevBuild
config.experimental.isolatedDevBuild,
appDir,
pagesDir,
debugBuildPaths
).then((resolved) => {
const checkEnd = process.hrtime(typeCheckAndLintStart)
return [resolved, checkEnd] as const
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/cli/next-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,14 @@ async function runPlaywright(
const { version: typeScriptVersion } = await verifyTypeScriptSetup({
dir: baseDir,
distDir: nextConfig.distDir,
intentDirs: [pagesDir, appDir].filter(Boolean) as string[],
typeCheckPreflight: false,
tsconfigPath: nextConfig.typescript.tsconfigPath,
disableStaticImages: nextConfig.images.disableStaticImages,
hasAppDir: !!appDir,
hasPagesDir: !!pagesDir,
isolatedDevBuild: nextConfig.experimental.isolatedDevBuild,
appDir: appDir || undefined,
pagesDir: pagesDir || undefined,
})

const isUsingTypeScript = !!typeScriptVersion
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/cli/next-typegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ const nextTypegen = async (
await verifyTypeScriptSetup({
dir: baseDir,
distDir: nextConfig.distDir,
intentDirs: [pagesDir, appDir].filter(Boolean) as string[],
typeCheckPreflight: false,
tsconfigPath: nextConfig.typescript.tsconfigPath,
disableStaticImages: nextConfig.images.disableStaticImages,
hasAppDir: !!appDir,
hasPagesDir: !!pagesDir,
isolatedDevBuild: nextConfig.experimental.isolatedDevBuild,
appDir: appDir || undefined,
pagesDir: pagesDir || undefined,
})

console.log('Generating route types...')
Expand Down
136 changes: 52 additions & 84 deletions packages/next/src/lib/resolve-build-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,36 @@ interface ResolvedBuildPaths {
}

/**
* Resolves glob patterns and explicit paths to actual file paths
* Categorizes them into App Router and Pages Router paths
* Escapes bracket expressions that correspond to existing directories.
* This allows Next.js dynamic routes like [slug] to work with glob patterns.
*
* @param patterns - Array of glob patterns or explicit paths
* @param projectDir - Root project directory
* @returns Object with categorized app and page paths
* e.g., "app/blog/[slug]/** /page.tsx" → "app/blog/\[slug\]/** /page.tsx"
* (if app/blog/[slug] directory exists)
*/
function escapeExistingBrackets(pattern: string, projectDir: string): string {
// Match bracket expressions: [name], [...name], [[...name]]
const bracketRegex = /\[\[?\.\.\.[^\]]+\]?\]|\[[^\]]+\]/g
let lastIndex = 0
let result = ''
let match: RegExpExecArray | null

while ((match = bracketRegex.exec(pattern)) !== null) {
const pathPrefix = pattern.slice(0, match.index + match[0].length)
const exists = fs.existsSync(path.join(projectDir, pathPrefix))

result += pattern.slice(lastIndex, match.index)
result += exists
? match[0].replace(/\[/g, '\\[').replace(/\]/g, '\\]')
: match[0]
lastIndex = match.index + match[0].length
}

return result + pattern.slice(lastIndex)
}

/**
* Resolves glob patterns and explicit paths to actual file paths.
* Categorizes them into App Router and Pages Router paths.
*/
export async function resolveBuildPaths(
patterns: string[],
Expand All @@ -29,41 +53,30 @@ export async function resolveBuildPaths(

for (const pattern of patterns) {
const trimmed = pattern.trim()
if (!trimmed) continue

if (!trimmed) {
continue
}

// Detect if pattern is glob pattern (contains glob special chars)
const isGlobPattern = /[*?[\]{}!]/.test(trimmed)
try {
// Escape brackets that correspond to existing Next.js dynamic route directories
const escapedPattern = escapeExistingBrackets(trimmed, projectDir)
const matches = (await glob(escapedPattern, {
cwd: projectDir,
})) as string[]

if (isGlobPattern) {
try {
// Resolve glob pattern
const matches = (await glob(trimmed, {
cwd: projectDir,
})) as string[]

if (matches.length === 0) {
Log.warn(`Glob pattern "${trimmed}" did not match any files`)
}
if (matches.length === 0) {
Log.warn(`Pattern "${trimmed}" did not match any files`)
}

for (const file of matches) {
// Skip directories, only process files
if (!fs.statSync(path.join(projectDir, file)).isDirectory()) {
categorizeAndAddPath(file, appPaths, pagePaths)
}
for (const file of matches) {
if (!fs.statSync(path.join(projectDir, file)).isDirectory()) {
categorizeAndAddPath(file, appPaths, pagePaths)
}
} catch (error) {
throw new Error(
`Failed to resolve glob pattern "${trimmed}": ${
isError(error) ? error.message : String(error)
}`
)
}
} else {
// Explicit path - categorize based on prefix
categorizeAndAddPath(trimmed, appPaths, pagePaths, projectDir)
} catch (error) {
throw new Error(
`Failed to resolve pattern "${trimmed}": ${
isError(error) ? error.message : String(error)
}`
)
}
}

Expand All @@ -74,68 +87,23 @@ export async function resolveBuildPaths(
}

/**
* Categorizes a file path to either app or pages router based on its prefix,
* and normalizes it to the format expected by Next.js internal build system.
*
* The internal build system expects:
* - App router: paths with leading slash (e.g., "/page.tsx", "/dashboard/page.tsx")
* - Pages router: paths with leading slash (e.g., "/index.tsx", "/about.tsx")
* Categorizes a file path to either app or pages router based on its prefix.
*
* Examples:
* - "app/page.tsx" → appPaths.add("/page.tsx")
* - "app/dashboard/page.tsx" → appPaths.add("/dashboard/page.tsx")
* - "pages/index.tsx" → pagePaths.add("/index.tsx")
* - "pages/about.tsx" → pagePaths.add("/about.tsx")
* - "/page.tsx" → appPaths.add("/page.tsx") (already in app router format)
*/
function categorizeAndAddPath(
filePath: string,
appPaths: Set<string>,
pagePaths: Set<string>,
projectDir?: string
pagePaths: Set<string>
): void {
// Normalize path separators to forward slashes (Windows compatibility)
const normalized = filePath.replace(/\\/g, '/')

// Skip non-file entries (like directories without extensions)
if (normalized.endsWith('/')) {
return
}

if (normalized.startsWith('app/')) {
// App router path: remove 'app/' prefix and ensure leading slash
// "app/page.tsx" → "/page.tsx"
// "app/dashboard/page.tsx" → "/dashboard/page.tsx"
const withoutPrefix = normalized.slice(4) // Remove "app/"
appPaths.add('/' + withoutPrefix)
appPaths.add('/' + normalized.slice(4))
} else if (normalized.startsWith('pages/')) {
// Pages router path: remove 'pages/' prefix and add leading slash
// "pages/index.tsx" → "/index.tsx"
// "pages/about.tsx" → "/about.tsx"
const withoutPrefix = normalized.slice(6) // Remove "pages/"
pagePaths.add('/' + withoutPrefix)
} else if (normalized.startsWith('/')) {
// Leading slash suggests app router format (already in correct format)
// "/page.tsx" → "/page.tsx" (no change needed)
appPaths.add(normalized)
} else {
// No obvious prefix - try to detect based on file existence
if (projectDir) {
const appPath = path.join(projectDir, 'app', normalized)
const pagesPath = path.join(projectDir, 'pages', normalized)

if (fs.existsSync(appPath)) {
appPaths.add('/' + normalized)
} else if (fs.existsSync(pagesPath)) {
pagePaths.add('/' + normalized)
} else {
// Default to pages router for paths without clear indicator
pagePaths.add('/' + normalized)
}
} else {
// Without projectDir context, default to pages router
pagePaths.add('/' + normalized)
}
pagePaths.add('/' + normalized.slice(6))
}
}

Expand Down
66 changes: 65 additions & 1 deletion packages/next/src/lib/typescript/runTypeCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,37 @@ export interface TypeCheckResult {
incremental: boolean
}

export interface TypeCheckDirs {
app?: string
pages?: string
}

export interface DebugBuildPaths {
app?: string[]
pages?: string[]
}

/**
* Check if a file path matches any of the debug build paths.
* Both filePath and debugPaths are resolved file paths from glob.
*/
function fileMatchesDebugPaths(
filePath: string,
debugPaths: string[]
): boolean {
return debugPaths.includes(filePath)
}

export async function runTypeCheck(
typescript: typeof import('typescript'),
baseDir: string,
distDir: string,
tsConfigPath: string,
cacheDir?: string,
isAppDirEnabled?: boolean,
isolatedDevBuild?: boolean
isolatedDevBuild?: boolean,
dirs?: TypeCheckDirs,
debugBuildPaths?: DebugBuildPaths
): Promise<TypeCheckResult> {
const effectiveConfiguration = await getTypeScriptConfiguration(
typescript,
Expand Down Expand Up @@ -52,6 +75,47 @@ export async function runTypeCheck(
)
}

// Apply debug build paths filter if specified
if (dirs && debugBuildPaths) {
const { app: appDir, pages: pagesDir } = dirs
const { app: debugAppPaths, pages: debugPagePaths } = debugBuildPaths

fileNames = fileNames.filter((fileName) => {
// Check if file is in app directory
if (appDir && fileName.startsWith(appDir + path.sep)) {
// If debugAppPaths is undefined, include all app files
if (debugAppPaths === undefined) {
return true
}
// If debugAppPaths is empty array, exclude all app files
if (debugAppPaths.length === 0) {
return false
}
// Check if file matches any of the debug paths
const relativeToApp = fileName.slice(appDir.length)
return fileMatchesDebugPaths(relativeToApp, debugAppPaths)
}

// Check if file is in pages directory
if (pagesDir && fileName.startsWith(pagesDir + path.sep)) {
// If debugPagePaths is undefined, include all pages files
if (debugPagePaths === undefined) {
return true
}
// If debugPagePaths is empty array, exclude all pages files
if (debugPagePaths.length === 0) {
return false
}
// Check if file matches any of the debug paths
const relativeToPages = fileName.slice(pagesDir.length)
return fileMatchesDebugPaths(relativeToPages, debugPagePaths)
}

// Keep files outside app/pages directories (shared code, etc.)
return true
})
}

if (fileNames.length < 1) {
return {
hasWarnings: false,
Expand Down
Loading
Loading