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
57 changes: 57 additions & 0 deletions scripts/__tests__/native-build-scan.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const fs = require('fs')
const path = require('path')
const Module = require('module')

const SCRIPT_PATH = path.join(__dirname, '..', 'native-build.js')

// native-build.js is a script, not a module: it calls main() at import time and
// exports nothing. Load the real source with that call stripped so the scan
// helpers can be asserted against the actual app tree.
function loadScriptInternals() {
const source = fs.readFileSync(SCRIPT_PATH, 'utf-8')
const withoutEntrypoint = source.replace(/\nmain\(\)\s*$/, '\n')
expect(withoutEntrypoint).not.toBe(source)

const exposed =
withoutEntrypoint +
'\nmodule.exports = { isHandledByTransform, detectUncoveredServerRoutes, P0_TRANSFORMS, APP_DIR }\n'

const mod = new Module(SCRIPT_PATH, null)
mod.filename = SCRIPT_PATH
mod.paths = Module._nodeModulePaths(path.dirname(SCRIPT_PATH))
mod._compile(exposed, SCRIPT_PATH)
return mod.exports
}

const { isHandledByTransform, detectUncoveredServerRoutes, P0_TRANSFORMS, APP_DIR } = loadScriptInternals()

const toPosix = (p) => p.split(path.sep).join('/')

describe('native build server-route scan', () => {
it('treats every P0_TRANSFORMS entry as handled', () => {
for (const transform of P0_TRANSFORMS) {
expect(isHandledByTransform(transform.path)).toBe(true)
}
})

// The scan passes `path.relative(APP_DIR, full)` into the predicate. If the
// predicate compared a different path shape it would silently never match.
it('matches the relative path shape the scan actually computes', () => {
for (const transform of P0_TRANSFORMS) {
const absolute = path.join(APP_DIR, transform.path)
expect(isHandledByTransform(path.relative(APP_DIR, absolute))).toBe(true)
}
})

it('does not suppress routes that are not transformed', () => {
expect(isHandledByTransform('some/other/page.tsx')).toBe(false)
expect(isHandledByTransform('api/foo/route.ts')).toBe(false)
expect(isHandledByTransform('(mobile-ui)/claim/layout.tsx')).toBe(false)
})

it('does not flag transformed pages as uncovered server routes', () => {
const transformed = new Set(P0_TRANSFORMS.map((t) => t.path))
const offenders = detectUncoveredServerRoutes().filter((o) => transformed.has(toPosix(o.rel)))
expect(offenders).toEqual([])
})
})
10 changes: 9 additions & 1 deletion scripts/native-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ function isCoveredByDisableList(relPath) {
})
}

// P0_TRANSFORMS files are replaced with static-export-safe stubs before `next
// build`, so their server-only exports (generateMetadata, force-dynamic) never
// reach the export — the scan must not flag them.
function isHandledByTransform(relPath) {
const normalized = relPath.split(path.sep).join('/')
return P0_TRANSFORMS.some((t) => t.path === normalized)
}

function detectUncoveredServerRoutes(dir = APP_DIR, found = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name.includes('.disabled') || entry.name.startsWith('_')) continue
Expand All @@ -258,7 +266,7 @@ function detectUncoveredServerRoutes(dir = APP_DIR, found = []) {
detectUncoveredServerRoutes(full, found)
continue
}
if (isCoveredByDisableList(rel)) continue
if (isCoveredByDisableList(rel) || isHandledByTransform(rel)) continue
if (entry.name === 'route.ts' || entry.name === 'route.js') {
found.push({ rel, reason: 'route handler (cannot be statically exported)' })
continue
Expand Down
Loading