@wular/pnext 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/reference/dev.md +17 -4
- package/src/api/dynamic.tsx +1 -0
- package/src/api/link.tsx +1 -0
- package/src/cli/analyze.ts +1 -1
- package/src/cli/boot/trace.ts +40 -7
- package/src/cli/build.ts +2 -2
- package/src/cli/dev.ts +5 -10
- package/src/cli/migrate/package-json.ts +1 -1
- package/src/cli/migrate/report.ts +5 -3
- package/src/cli/migrate/run.ts +30 -4
- package/src/cli/migrate/scan.ts +34 -7
- package/src/cli/typegen.ts +8 -1
- package/src/client/build.ts +4 -5
- package/src/client/profile.ts +4 -3
- package/src/client/router/runtime.ts +8 -0
- package/src/compat/actions/config.ts +2 -0
- package/src/compat/aliases.ts +2 -2
- package/src/compat/bundler/react-profiler.tsx +1 -0
- package/src/compat/cache-control.ts +49 -119
- package/src/compat/client/errors/global-error.ts +10 -5
- package/src/compat/client/errors/install.ts +30 -6
- package/src/compat/client/errors/lazy.ts +32 -2
- package/src/compat/next/client-script.tsx +1 -0
- package/src/compat/next/config-headers.ts +139 -0
- package/src/compat/next/config-loader.ts +32 -2
- package/src/compat/next/dynamic.tsx +1 -0
- package/src/compat/next/error.tsx +1 -0
- package/src/compat/next/form.tsx +1 -0
- package/src/compat/next/head.tsx +1 -0
- package/src/compat/next/headers.ts +12 -6
- package/src/compat/next/image/client.tsx +1 -0
- package/src/compat/next/image.tsx +1 -0
- package/src/compat/next/legacy-image.tsx +1 -0
- package/src/compat/next/legacy-request-apis.ts +62 -0
- package/src/compat/next/link.tsx +1 -0
- package/src/compat/next/rewrites.ts +7 -0
- package/src/compat/next/script.tsx +1 -0
- package/src/compat/register/bundler.ts +6 -1
- package/src/compat/segment/loading-boundary.ts +1 -5
- package/src/compat/validation/validate.ts +10 -2
- package/src/config.ts +12 -1
- package/src/css/build.ts +111 -4
- package/src/dev/internal-error.ts +128 -0
- package/src/dev/restart/global-css.ts +3 -3
- package/src/dev/restart/route-facts.ts +3 -3
- package/src/dev/server.ts +2 -2
- package/src/render/renderer.ts +20 -2
- package/src/render/slots.tsx +1 -0
- package/src/request/context.ts +15 -0
- package/src/runtime/loader.ts +2 -2
- package/src/runtime/module-cache.ts +12 -5
- package/src/runtime/modules.ts +44 -31
- package/src/runtime/vendor-build.ts +5 -7
- package/src/runtime/vendor.ts +29 -13
- package/src/types.ts +10 -1
- package/src/utils/code.ts +5 -0
- package/src/utils/dev-profile.ts +1 -1
- package/src/utils/esbuild.ts +37 -2
- package/src/utils/trace-flags.ts +66 -0
package/package.json
CHANGED
package/reference/dev.md
CHANGED
|
@@ -56,7 +56,20 @@ Vercel has a dedicated adapter: `pnext build --adapter vercel` writes Build Outp
|
|
|
56
56
|
|
|
57
57
|
- `PNEXT_COMPAT=next`: Next compatibility without a config file. See [Compatibility](./compat.md).
|
|
58
58
|
- `PNEXT_TYPECHECK=classic`: in-process TypeScript checker instead of the native one.
|
|
59
|
-
- `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
- `PNEXT_TRACE=<scopes>`: turn on diagnostic output. Comma-separated scopes, each `scope` or
|
|
60
|
+
`scope=value`; `1` means `boot`. A scope is all-or-nothing — it turns on everything it covers.
|
|
61
|
+
Example: `PNEXT_TRACE=boot,server=/tmp/modules.json`.
|
|
62
|
+
|
|
63
|
+
| scope | prints / does |
|
|
64
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `boot` | boot-phase timings and memory readings |
|
|
66
|
+
| `server` | dev request, render and build timings; per-module compile attribution; route-facts and global-css hit/miss; why each artifact was recompiled; load-plugin registrations |
|
|
67
|
+
| `vendor` | vendor saturation spans, esbuild plugin-callback attribution, and the preplan / native decision report |
|
|
68
|
+
| `client` | client-build phase timings, plus the esbuild metafile written into the output directory |
|
|
69
|
+
| `all` | the four above |
|
|
70
|
+
|
|
71
|
+
Two scopes take a `=<path>` value:
|
|
72
|
+
|
|
73
|
+
- `server=<path>` dumps the per-module compile list as JSON. A `.jsonl` path collects the
|
|
74
|
+
heavy-package facade profile instead — the two are never wanted at once, so the suffix picks.
|
|
75
|
+
- `vendor=<path>` appends the JSONL workload trace `bench/tools/vendor-analyze.ts` reads.
|
package/src/api/dynamic.tsx
CHANGED
package/src/api/link.tsx
CHANGED
package/src/cli/analyze.ts
CHANGED
|
@@ -474,7 +474,7 @@ function printAnalyzeFiles(result: AnalyzeResult, options: { details: boolean })
|
|
|
474
474
|
console.log(
|
|
475
475
|
`Other files: ${formatFileCount(sorted)} ${formatTotalSize(sorted, result.compression)}`,
|
|
476
476
|
)
|
|
477
|
-
console.log('
|
|
477
|
+
console.log('List them: pnext analyze --files')
|
|
478
478
|
return
|
|
479
479
|
}
|
|
480
480
|
|
package/src/cli/boot/trace.ts
CHANGED
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Boot phase timings for `pnext dev`, printed
|
|
2
|
+
* Boot phase timings for `pnext dev`, printed only under `PNEXT_TRACE=boot` (or `=1`).
|
|
3
3
|
*
|
|
4
4
|
* Timestamps are `performance.now()`, whose origin is process start, so the
|
|
5
5
|
* first mark's absolute value is everything before it: the bun runtime coming
|
|
6
|
-
* up plus this module graph evaluating. Off, a mark costs one
|
|
7
|
-
*
|
|
6
|
+
* up plus this module graph evaluating. Off, a mark costs one clock read and
|
|
7
|
+
* one rss read; nothing is ever printed.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { loadavg, cpus, arch, platform } from 'node:os'
|
|
10
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { traceEnabled } from '../../utils/trace-flags'
|
|
13
|
+
|
|
14
|
+
const enabled = traceEnabled('boot')
|
|
11
15
|
const marks: [string, number, number][] = []
|
|
12
16
|
|
|
17
|
+
// Bun (<=1.3.x) lacks the memoryUsage.rss fast path Node has.
|
|
18
|
+
const rss = () =>
|
|
19
|
+
typeof process.memoryUsage.rss === 'function'
|
|
20
|
+
? process.memoryUsage.rss()
|
|
21
|
+
: process.memoryUsage().rss
|
|
22
|
+
|
|
13
23
|
export function markBoot(name: string) {
|
|
14
|
-
if (enabled) marks.push([name, performance.now(),
|
|
24
|
+
if (enabled) marks.push([name, performance.now(), rss()])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Environment facts a slow-boot report needs alongside the stage timings: the same trace on a
|
|
28
|
+
// loaded box, a cold cache, or a huge node_modules reads completely differently.
|
|
29
|
+
function traceHeader(): string[] {
|
|
30
|
+
const load = loadavg()
|
|
31
|
+
.map(value => value.toFixed(1))
|
|
32
|
+
.join(' ')
|
|
33
|
+
const packages = countTopLevel('node_modules')
|
|
34
|
+
const outDir = existsSync(path.resolve('.pnext', 'dev', 'cache')) ? 'warm' : 'cold'
|
|
35
|
+
return [
|
|
36
|
+
` runtime ${typeof Bun !== 'undefined' ? `bun ${Bun.version}` : `node ${process.version}`} · ${platform()} ${arch()} · ${cpus().length} cpus · load ${load}`,
|
|
37
|
+
` app ${packages} node_modules entries · .pnext/dev cache ${outDir}`,
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function countTopLevel(dir: string): number {
|
|
42
|
+
try {
|
|
43
|
+
return readdirSync(path.resolve(dir)).length
|
|
44
|
+
} catch {
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
15
47
|
}
|
|
16
48
|
|
|
17
49
|
export function printBootTrace() {
|
|
18
50
|
if (!enabled || marks.length === 0) return
|
|
19
|
-
const lines = ['boot trace (ms since process start):']
|
|
51
|
+
const lines = ['boot trace (ms since process start):', ...traceHeader()]
|
|
20
52
|
let previous = 0
|
|
21
53
|
for (const [name, at, rss] of marks) {
|
|
22
54
|
const mb = (rss / 1048576).toFixed(0).padStart(5)
|
|
@@ -25,5 +57,6 @@ export function printBootTrace() {
|
|
|
25
57
|
)
|
|
26
58
|
previous = at
|
|
27
59
|
}
|
|
60
|
+
lines.push(` total @${performance.now().toFixed(1)}`)
|
|
28
61
|
console.log(lines.join('\n'))
|
|
29
62
|
}
|
package/src/cli/build.ts
CHANGED
|
@@ -1086,8 +1086,8 @@ function printBuildSummary(
|
|
|
1086
1086
|
}
|
|
1087
1087
|
console.log(` ${dim(`${parts.join(', ')} → ${outDir}`)}`)
|
|
1088
1088
|
console.log('')
|
|
1089
|
-
console.log(`
|
|
1090
|
-
console.log(` ${cyan('pnext analyze')}
|
|
1089
|
+
console.log(` Serve the production build: ${cyan('pnext start')}`)
|
|
1090
|
+
console.log(` Analyze the build: ${cyan('pnext analyze')}`)
|
|
1091
1091
|
}
|
|
1092
1092
|
|
|
1093
1093
|
function staticMetadataSummaryFiles(staticFiles: Record<string, StaticFileMetadata>) {
|
package/src/cli/dev.ts
CHANGED
|
@@ -125,16 +125,11 @@ function watchEsbuildMemory() {
|
|
|
125
125
|
),
|
|
126
126
|
)
|
|
127
127
|
void (async () => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
// stop() can strand in-flight build promises unsettled; fail their
|
|
135
|
-
// waiters so requests retry against the respawned service.
|
|
136
|
-
const { failInFlightClientBuilds } = await import('../dev/server')
|
|
137
|
-
failInFlightClientBuilds('esbuild service restarted by the memory watchdog')
|
|
128
|
+
// The idle-guarded stop: a raw esbuild.stop() under an in-flight compile
|
|
129
|
+
// fails that request with a 500, so a busy service drains first. In-flight
|
|
130
|
+
// builds are untouched, so their waiters must not be failed here.
|
|
131
|
+
const { stopEsbuildService } = await import('../utils/esbuild')
|
|
132
|
+
stopEsbuildService()
|
|
138
133
|
})()
|
|
139
134
|
}, 15_000)
|
|
140
135
|
timer.unref()
|
|
@@ -147,7 +147,7 @@ export async function migratePackageJson(
|
|
|
147
147
|
if (leftovers.length > 0) {
|
|
148
148
|
result.reports.push({
|
|
149
149
|
title: 'Next-specific packages still installed',
|
|
150
|
-
detail: 'Optional cleanup — these are unused under pnext. See
|
|
150
|
+
detail: 'Optional cleanup — these are unused under pnext. See https://pnext.dev/docs/compat.',
|
|
151
151
|
files: leftovers,
|
|
152
152
|
})
|
|
153
153
|
}
|
|
@@ -25,10 +25,12 @@ export interface ReportItem {
|
|
|
25
25
|
export interface MigrationResult {
|
|
26
26
|
edits: Edit[]
|
|
27
27
|
reports: ReportItem[]
|
|
28
|
+
/** Files reading cookies()/headers()/draftMode() synchronously (Next 14 style). */
|
|
29
|
+
syncRequestApis: string[]
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export function emptyResult(): MigrationResult {
|
|
31
|
-
return { edits: [], reports: [] }
|
|
33
|
+
return { edits: [], reports: [], syncRequestApis: [] }
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function printHeader(root: string, dryRun: boolean) {
|
|
@@ -63,8 +65,8 @@ export function printResult(result: MigrationResult, options: { install: string
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
lines.push(`\n${bold('Next steps')}`)
|
|
66
|
-
lines.push(`
|
|
67
|
-
lines.push(` ${cyan('pnext dev')}
|
|
68
|
+
lines.push(` 1. Install the new dependencies: ${cyan(options.install)}`)
|
|
69
|
+
lines.push(` 2. Start the app: ${cyan('pnext dev')}`)
|
|
68
70
|
console.log(lines.join('\n'))
|
|
69
71
|
}
|
|
70
72
|
|
package/src/cli/migrate/run.ts
CHANGED
|
@@ -37,32 +37,58 @@ export async function migrateApp(directory: string | undefined, options: { dryRu
|
|
|
37
37
|
printHeader(root, options.dryRun)
|
|
38
38
|
const result = emptyResult()
|
|
39
39
|
if (pkg) await migratePackageJson(root, pkg, result, options.dryRun)
|
|
40
|
+
// The scan runs first: what it finds (sync request APIs) decides what the config enables.
|
|
41
|
+
await withSpinner('Scanning sources ...', () => scanSources(root, result))
|
|
40
42
|
await createPnextConfig(root, result, options.dryRun)
|
|
41
43
|
await migrateTsconfig(root, result, options.dryRun)
|
|
42
44
|
await removeNextEnv(root, result, options.dryRun)
|
|
43
45
|
await updateGitignore(root, result, options.dryRun)
|
|
44
|
-
await withSpinner('Scanning sources ...', () => scanSources(root, result))
|
|
45
46
|
|
|
46
47
|
printResult(result, { install: installCommand(root) })
|
|
47
48
|
return 0
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
async function createPnextConfig(root: string, result: MigrationResult, dryRun: boolean) {
|
|
52
|
+
const legacy = result.syncRequestApis.length > 0
|
|
51
53
|
const existing = PNEXT_CONFIG_FILES.find(name => existsSync(path.join(root, name)))
|
|
52
54
|
if (existing) {
|
|
53
55
|
result.reports.push({
|
|
54
56
|
title: `${existing} already exists`,
|
|
55
|
-
detail: 'Make sure it sets compat: { next: true }. See
|
|
57
|
+
detail: 'Make sure it sets compat: { next: true }. See https://pnext.dev/docs/compat.',
|
|
56
58
|
})
|
|
59
|
+
if (legacy) reportSyncRequestApis(result, existing)
|
|
57
60
|
return
|
|
58
61
|
}
|
|
59
62
|
if (!dryRun) {
|
|
60
63
|
await writeFile(
|
|
61
64
|
path.join(root, 'pnext.config.ts'),
|
|
62
|
-
|
|
65
|
+
legacy
|
|
66
|
+
? 'export default {\n' +
|
|
67
|
+
' compat: {\n' +
|
|
68
|
+
' // Sync cookies()/headers() still work; migrate to `await` and drop this.\n' +
|
|
69
|
+
' next: { legacyRequestAPIs: true },\n' +
|
|
70
|
+
' },\n' +
|
|
71
|
+
'};\n'
|
|
72
|
+
: 'export default { compat: { next: true } };\n',
|
|
63
73
|
)
|
|
64
74
|
}
|
|
65
|
-
result.edits.push({
|
|
75
|
+
result.edits.push({
|
|
76
|
+
file: 'pnext.config.ts',
|
|
77
|
+
description: legacy
|
|
78
|
+
? 'created with compat.next enabled (legacyRequestAPIs)'
|
|
79
|
+
: 'created with compat.next enabled',
|
|
80
|
+
})
|
|
81
|
+
if (legacy) reportSyncRequestApis(result)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function reportSyncRequestApis(result: MigrationResult, existingConfig?: string) {
|
|
85
|
+
result.reports.push({
|
|
86
|
+
title: 'Sync cookies()/headers() detected',
|
|
87
|
+
detail: existingConfig
|
|
88
|
+
? `Set compat: { next: { legacyRequestAPIs: true } } in ${existingConfig} to keep these working, then migrate to \`await\` and remove it. See https://pnext.dev/docs/compat`
|
|
89
|
+
: 'Enabled compat.next.legacyRequestAPIs; migrate to `await` and remove it. See https://pnext.dev/docs/compat',
|
|
90
|
+
files: [...new Set(result.syncRequestApis)],
|
|
91
|
+
})
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
async function removeNextEnv(root: string, result: MigrationResult, dryRun: boolean) {
|
package/src/cli/migrate/scan.ts
CHANGED
|
@@ -17,6 +17,11 @@ const LOADERS: Record<string, 'ts' | 'tsx' | 'js' | 'jsx'> = {
|
|
|
17
17
|
'.cjs': 'js',
|
|
18
18
|
}
|
|
19
19
|
const STREAMING_APIS = ['renderToReadableStream', 'renderToPipeableStream']
|
|
20
|
+
// `cookies().get(...)` / `headers()['x']` with no `await` — Next 14 sync request-API reads.
|
|
21
|
+
const CALL_THEN_ACCESS = /(await\s+)?\b([\w$]+)\s*\(\s*\)\s*(?:\.\s*([\w$]+)|\[)/g
|
|
22
|
+
const NEXT_HEADERS_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]next\/headers['"]/g
|
|
23
|
+
const REQUEST_APIS = new Set(['cookies', 'headers', 'draftMode'])
|
|
24
|
+
const PROMISE_METHODS = new Set(['then', 'catch', 'finally'])
|
|
20
25
|
const SPECIAL_PAGES = ['_app', '_document', '_error']
|
|
21
26
|
|
|
22
27
|
export async function scanSources(root: string, result: MigrationResult) {
|
|
@@ -49,13 +54,14 @@ export async function scanSources(root: string, result: MigrationResult) {
|
|
|
49
54
|
}
|
|
50
55
|
if (specifier === 'next/head') headImports.push(relative)
|
|
51
56
|
}
|
|
57
|
+
if (readsRequestApiSynchronously(text)) result.syncRequestApis.push(relative)
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
if (deepImports.length > 0) {
|
|
55
61
|
result.reports.push({
|
|
56
62
|
title: 'Unsupported next/dist/* deep imports',
|
|
57
63
|
detail:
|
|
58
|
-
'Only five next/dist paths are shimmed — replace these with public APIs. See
|
|
64
|
+
'Only five next/dist paths are shimmed — replace these with public APIs. See https://pnext.dev/docs/compat ("Smaller surfaces").',
|
|
59
65
|
files: deepImports,
|
|
60
66
|
})
|
|
61
67
|
}
|
|
@@ -63,16 +69,16 @@ export async function scanSources(root: string, result: MigrationResult) {
|
|
|
63
69
|
result.reports.push({
|
|
64
70
|
title: 'react-dom/server streaming APIs',
|
|
65
71
|
detail:
|
|
66
|
-
'preact/compat does not provide renderToReadableStream/renderToPipeableStream — use pnext rendering instead. See
|
|
72
|
+
'preact/compat does not provide renderToReadableStream/renderToPipeableStream — use pnext rendering instead. See https://pnext.dev/docs/compat.',
|
|
67
73
|
files: [...new Set(streaming)],
|
|
68
74
|
})
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
if (headImports.length > 0) {
|
|
72
78
|
result.reports.push({
|
|
73
|
-
title: 'next/head
|
|
79
|
+
title: 'next/head is ignored in the app router',
|
|
74
80
|
detail:
|
|
75
|
-
'Move these tags to metadata exports
|
|
81
|
+
'pnext ignores next/head in app-directory components (matching Next). Move these tags to metadata exports or the root layout <head>. See https://pnext.dev/docs/compat.',
|
|
76
82
|
files: [...new Set(headImports)],
|
|
77
83
|
})
|
|
78
84
|
}
|
|
@@ -81,6 +87,27 @@ export async function scanSources(root: string, result: MigrationResult) {
|
|
|
81
87
|
await scanNextConfig(root, result)
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/**
|
|
91
|
+
* A next/headers request API called and immediately read, with no `await` — the Next 14 shape that
|
|
92
|
+
* throws under Next 16 semantics. Local aliases (`cookies as getCookies`) count; `.then()` chains do not.
|
|
93
|
+
*/
|
|
94
|
+
function readsRequestApiSynchronously(text: string): boolean {
|
|
95
|
+
const locals = new Set<string>()
|
|
96
|
+
for (const match of text.matchAll(NEXT_HEADERS_IMPORT)) {
|
|
97
|
+
for (const binding of match[1]!.split(',')) {
|
|
98
|
+
const [imported, local] = binding.trim().split(/\s+as\s+/)
|
|
99
|
+
if (imported && REQUEST_APIS.has(imported)) locals.add(local ?? imported)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (locals.size === 0) return false
|
|
103
|
+
for (const match of text.matchAll(CALL_THEN_ACCESS)) {
|
|
104
|
+
if (match[1] || !locals.has(match[2]!)) continue
|
|
105
|
+
if (match[3] && PROMISE_METHODS.has(match[3])) continue
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
function scanSpecialPages(root: string, result: MigrationResult) {
|
|
85
112
|
const found: string[] = []
|
|
86
113
|
for (const base of ['pages', path.join('src', 'pages')]) {
|
|
@@ -95,7 +122,7 @@ function scanSpecialPages(root: string, result: MigrationResult) {
|
|
|
95
122
|
result.reports.push({
|
|
96
123
|
title: 'pages/_app, _document and _error are ignored',
|
|
97
124
|
detail:
|
|
98
|
-
'Move this setup into the app-router root layout — pnext never loads these files. See
|
|
125
|
+
'Move this setup into the app-router root layout — pnext never loads these files. See https://pnext.dev/docs/compat.',
|
|
99
126
|
files: found,
|
|
100
127
|
})
|
|
101
128
|
}
|
|
@@ -110,8 +137,8 @@ async function scanNextConfig(root: string, result: MigrationResult) {
|
|
|
110
137
|
result.reports.push({
|
|
111
138
|
title: `${name} defines a webpack function`,
|
|
112
139
|
detail: svgr
|
|
113
|
-
? 'The webpack function is never executed; SVGR is auto-detected from @svgr/webpack, so SVG imports keep working. See
|
|
114
|
-
: 'The webpack function is never executed — port any custom loaders/plugins to pnext config. See
|
|
140
|
+
? 'The webpack function is never executed; SVGR is auto-detected from @svgr/webpack, so SVG imports keep working. See https://pnext.dev/docs/compat.'
|
|
141
|
+
: 'The webpack function is never executed — port any custom loaders/plugins to pnext config. See https://pnext.dev/docs/compat.',
|
|
115
142
|
})
|
|
116
143
|
return
|
|
117
144
|
}
|
package/src/cli/typegen.ts
CHANGED
|
@@ -145,7 +145,14 @@ function paramsType(route: RouteManifestEntry) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
async function writeIfChanged(file: string, source: string) {
|
|
148
|
-
|
|
148
|
+
// A concurrent typegen (the dev server runs one off the critical path, a CLI run can overlap it
|
|
149
|
+
// on the same out-dir) can delete the file between the existence check and the read; a failed
|
|
150
|
+
// read just means "changed", never a crash.
|
|
151
|
+
try {
|
|
152
|
+
if (existsSync(file) && (await readFile(file, 'utf8')) === source) return
|
|
153
|
+
} catch {
|
|
154
|
+
// fall through to the write
|
|
155
|
+
}
|
|
149
156
|
await ensureDir(path.dirname(file))
|
|
150
157
|
await writeFile(file, source)
|
|
151
158
|
}
|
package/src/client/build.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { clientReferenceId, ssrClientReference, type ClientReference } from './r
|
|
|
56
56
|
import { shouldReactCompile, transformReactCompiler } from './react-compiler'
|
|
57
57
|
import { createVerboseLogger, formatDuration } from '../utils/verbose'
|
|
58
58
|
import { dim as dimText } from '../utils/ansi'
|
|
59
|
+
import { traceEnabled } from '../utils/trace-flags'
|
|
59
60
|
import type { RouteManifestEntry } from '../types'
|
|
60
61
|
import {
|
|
61
62
|
applyClientSourceAsyncPreTransforms,
|
|
@@ -1016,7 +1017,7 @@ function deploymentIdDefine(): Record<string, string> {
|
|
|
1016
1017
|
* Wrap every plugin's onResolve/onLoad so the profile reports, per plugin, how
|
|
1017
1018
|
* many callbacks esbuild made and how long they held the JS thread. Each call
|
|
1018
1019
|
* is an IPC round trip, so the counts are what distinguishes "our transforms are
|
|
1019
|
-
* slow" from "we are answering 40k questions". Off unless
|
|
1020
|
+
* slow" from "we are answering 40k questions". Off unless PNEXT_TRACE=client.
|
|
1020
1021
|
*/
|
|
1021
1022
|
function instrumentedPlugins(plugins: Plugin[]): Plugin[] {
|
|
1022
1023
|
if (!clientProfile.enabled) return plugins
|
|
@@ -1657,8 +1658,7 @@ async function profileClientBuild<T>(
|
|
|
1657
1658
|
dev: boolean | undefined,
|
|
1658
1659
|
task: () => Promise<T>,
|
|
1659
1660
|
) {
|
|
1660
|
-
|
|
1661
|
-
if (!process.env.PNEXT_DEV_PROFILE) return task()
|
|
1661
|
+
if (!traceEnabled('server')) return task()
|
|
1662
1662
|
const start = performance.now()
|
|
1663
1663
|
try {
|
|
1664
1664
|
return await task()
|
|
@@ -2075,8 +2075,7 @@ function rootFromFile(file: string) {
|
|
|
2075
2075
|
// the metafile: the package name for node_modules code, the file stem for app
|
|
2076
2076
|
// code. Falls back to "shared" when a build ran without a metafile.
|
|
2077
2077
|
async function nameSharedClientChunks(outDir: string, metafile?: Metafile) {
|
|
2078
|
-
|
|
2079
|
-
if (metafile && process.env.PNEXT_CLIENT_METAFILE) {
|
|
2078
|
+
if (metafile && traceEnabled('client')) {
|
|
2080
2079
|
await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile))
|
|
2081
2080
|
}
|
|
2082
2081
|
const chunksDir = path.join(outDir, 'chunks')
|
package/src/client/profile.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Phase decomposition for the client stage (read / transform / react-compiler /
|
|
2
|
-
// esbuild / write), off unless
|
|
2
|
+
// esbuild / write), off unless PNEXT_TRACE=client. The bench needs the
|
|
3
3
|
// split to prove where the residual sits; a build must not pay for it.
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { traceEnabled } from '../utils/trace-flags'
|
|
5
|
+
|
|
6
|
+
const enabled = traceEnabled('client')
|
|
6
7
|
|
|
7
8
|
const totals = new Map<string, number>()
|
|
8
9
|
const spans = new Map<string, [number, number][]>()
|
|
@@ -5138,7 +5138,11 @@ export function onPopState() {
|
|
|
5138
5138
|
}
|
|
5139
5139
|
|
|
5140
5140
|
const eagerLinks = new WeakSet<Element>()
|
|
5141
|
+
// Per DOCUMENT, not per process: an observer belongs to the document whose links it watches.
|
|
5142
|
+
// A browser page never swaps documents, but multi-document hosts (jsdom embedders, the test
|
|
5143
|
+
// process) do, and a stale observer silently never fires for the new document's links.
|
|
5141
5144
|
let visibleLinkObserver: IntersectionObserver | undefined
|
|
5145
|
+
let visibleLinkObserverDoc: Document | undefined
|
|
5142
5146
|
|
|
5143
5147
|
// `load` links prefetch as soon as they appear; `visible` links when they
|
|
5144
5148
|
// enter the viewport. Both arrive with the page or with later client renders,
|
|
@@ -5168,6 +5172,10 @@ function scanEagerPrefetchLinks(root: Element) {
|
|
|
5168
5172
|
if (href) void prefetchRoute(href, { element: link, full: isFullPrefetchLink(link) })
|
|
5169
5173
|
continue
|
|
5170
5174
|
}
|
|
5175
|
+
if (visibleLinkObserverDoc !== document) {
|
|
5176
|
+
visibleLinkObserver = undefined
|
|
5177
|
+
visibleLinkObserverDoc = document
|
|
5178
|
+
}
|
|
5171
5179
|
visibleLinkObserver ??= new IntersectionObserver(entries => {
|
|
5172
5180
|
for (const entry of entries) {
|
|
5173
5181
|
if (!entry.isIntersecting) {
|
|
@@ -22,6 +22,8 @@ export interface NextConfigShape {
|
|
|
22
22
|
experimental?: { serverActions?: { bodySizeLimit?: unknown } | boolean }
|
|
23
23
|
rewrites?: () => unknown
|
|
24
24
|
redirects?: () => unknown
|
|
25
|
+
headers?: () => unknown
|
|
26
|
+
poweredByHeader?: boolean
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export async function loadServerActionsConfig(root: string): Promise<ServerActionsConfig> {
|
package/src/compat/aliases.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function nextCompatEnabled(config: CompatConfig) {
|
|
|
22
22
|
return Boolean(config.compat?.next)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** The only `next/dist/*` deep imports pnext shims (see
|
|
25
|
+
/** The only `next/dist/*` deep imports pnext shims (see https://pnext.dev/docs/compat "Smaller surfaces"). */
|
|
26
26
|
export const SHIMMED_NEXT_DIST_PATHS = [
|
|
27
27
|
'next/dist/client/components/app-router-headers',
|
|
28
28
|
'next/dist/server/web/spec-extension/unstable-cache',
|
|
@@ -50,7 +50,7 @@ export function missingCompatImportError(
|
|
|
50
50
|
`'${specifier}' is not a supported import: next/dist internals are not part of the ` +
|
|
51
51
|
`public surface pnext shims. Only these next/dist/* paths are shimmed:\n` +
|
|
52
52
|
SHIMMED_NEXT_DIST_PATHS.map(path => ` - ${path}`).join('\n') +
|
|
53
|
-
`\nSee
|
|
53
|
+
`\nSee https://pnext.dev/docs/compat ("Smaller surfaces") and report the library that needs a new shim.`
|
|
54
54
|
)
|
|
55
55
|
}
|
|
56
56
|
}
|