@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/src/render/renderer.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
import { captureCacheScope, runWithCacheScope } from '../request/cache'
|
|
107
107
|
import { registerServerRuntime } from '../runtime/loader'
|
|
108
108
|
import { readText } from '../utils/fs'
|
|
109
|
+
import { traceEnabled } from '../utils/trace-flags'
|
|
109
110
|
import {
|
|
110
111
|
flushDevProfileLines,
|
|
111
112
|
formatProfileDuration,
|
|
@@ -168,6 +169,7 @@ import {
|
|
|
168
169
|
type RouteParamsSnapshot,
|
|
169
170
|
} from './island-context'
|
|
170
171
|
import { escapeHtml } from '../utils/html'
|
|
172
|
+
import { internalErrorHtml, isPnextInternalError } from '../dev/internal-error'
|
|
171
173
|
import type {
|
|
172
174
|
Metadata,
|
|
173
175
|
MetadataLink,
|
|
@@ -1094,8 +1096,7 @@ interface ProfileRenderOptions {
|
|
|
1094
1096
|
}
|
|
1095
1097
|
|
|
1096
1098
|
function renderProfile(options: ProfileRenderOptions): RenderProfile | undefined {
|
|
1097
|
-
|
|
1098
|
-
if (!process.env.PNEXT_DEV_PROFILE || !options.dev) return undefined
|
|
1099
|
+
if (!traceEnabled('server') || !options.dev) return undefined
|
|
1099
1100
|
return { label: options.route.route }
|
|
1100
1101
|
}
|
|
1101
1102
|
|
|
@@ -2992,6 +2993,23 @@ async function pageRender(options: RenderOptions, stream: boolean): Promise<Page
|
|
|
2992
2993
|
},
|
|
2993
2994
|
)
|
|
2994
2995
|
}
|
|
2996
|
+
// An error thrown entirely inside pnext's own frames is a framework bug the app cannot fix:
|
|
2997
|
+
// in dev it gets the loud internal-error document (full trace + prefilled GitHub report)
|
|
2998
|
+
// instead of hiding behind the app's error boundaries or the generic global-error page.
|
|
2999
|
+
if (options.dev) {
|
|
3000
|
+
const resolved = resolveThrownError(error, nextCompatEnabled(options.config))
|
|
3001
|
+
if (isPnextInternalError(resolved)) {
|
|
3002
|
+
logDevError(options, resolved)
|
|
3003
|
+
return globalErrorPage(
|
|
3004
|
+
internalErrorHtml({
|
|
3005
|
+
error: resolved,
|
|
3006
|
+
route: options.route.route,
|
|
3007
|
+
url: String(options.url),
|
|
3008
|
+
digest: (resolved as Error & { digest?: string }).digest,
|
|
3009
|
+
}),
|
|
3010
|
+
)
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
2995
3013
|
// A nearest error.* boundary catches first (rendered inside the document).
|
|
2996
3014
|
// Only when no error boundary exists does global-error.* take over the whole
|
|
2997
3015
|
// document (it supplies its own <html>/<body>); otherwise the normal error
|
package/src/render/slots.tsx
CHANGED
package/src/request/context.ts
CHANGED
|
@@ -346,6 +346,21 @@ function dynamicBailoutRoute(): string | undefined {
|
|
|
346
346
|
return typeof route === 'string' ? route : undefined
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
/**
|
|
350
|
+
* compat.next.legacyRequestAPIs — Next 15's transitional sync request APIs. Core owns the boolean cell
|
|
351
|
+
* (set from loadConfig); compat's cookies()/headers()/draftMode() read it. globalThis-anchored so a
|
|
352
|
+
* prebundled server entry carrying its own copy of this module sees the same flag.
|
|
353
|
+
*/
|
|
354
|
+
const LEGACY_REQUEST_APIS = Symbol.for('pnext.legacyRequestAPIs')
|
|
355
|
+
|
|
356
|
+
export function setLegacyRequestAPIs(enabled: boolean): void {
|
|
357
|
+
;(globalThis as Record<PropertyKey, unknown>)[LEGACY_REQUEST_APIS] = enabled
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function legacyRequestAPIs(): boolean {
|
|
361
|
+
return (globalThis as Record<PropertyKey, unknown>)[LEGACY_REQUEST_APIS] === true
|
|
362
|
+
}
|
|
363
|
+
|
|
349
364
|
export function requireRequest(api: string) {
|
|
350
365
|
const probeRoute = dynamicBailoutRoute()
|
|
351
366
|
if (probeRoute !== undefined) {
|
package/src/runtime/loader.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { readNodeModuleBundle, writeNodeModuleBundle } from '../dev/restart/node
|
|
|
44
44
|
import { setBasePathPrefix, setDefaultPrefetchMode, setTrailingSlashUrls } from '../routing/href'
|
|
45
45
|
import { resolveExternalLoadTarget, resolveImport, workspacePackageRoots } from '../resolve/imports'
|
|
46
46
|
import { escapeRegex } from '../utils/code'
|
|
47
|
+
import { traceEnabled } from '../utils/trace-flags'
|
|
47
48
|
import { writeFileAtomic } from '../utils/fs'
|
|
48
49
|
|
|
49
50
|
const require = createRequire(import.meta.url)
|
|
@@ -302,8 +303,7 @@ function registerResolvePlugin() {
|
|
|
302
303
|
function registerLoadPlugin(root: string) {
|
|
303
304
|
if (registeredLoadRoots.has(root)) return
|
|
304
305
|
registeredLoadRoots.add(root)
|
|
305
|
-
|
|
306
|
-
if (process.env.PNEXT_DEBUG_LOADPLUGIN) {
|
|
306
|
+
if (traceEnabled('server')) {
|
|
307
307
|
console.error(`[loadplugin] register root=${root} extras=${currentLoadExtras().join(',')}`)
|
|
308
308
|
}
|
|
309
309
|
loadPluginExtras.set(root, currentLoadExtras())
|
|
@@ -15,6 +15,7 @@ import path from 'node:path'
|
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
16
|
import type { ResolvedConfig } from '../config'
|
|
17
17
|
import { writeFileAtomic } from '../utils/fs'
|
|
18
|
+
import { traceEnabled } from '../utils/trace-flags'
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Kill switch for the request-head trims: the naming walk's synchronous source
|
|
@@ -224,12 +225,11 @@ function createDevModuleCache(
|
|
|
224
225
|
.slice(0, 16)
|
|
225
226
|
establishCacheTrust(root)
|
|
226
227
|
const records = indexRecords(readIndexFile(indexFile), key, workspaceRoot)
|
|
227
|
-
//
|
|
228
|
+
// PNEXT_TRACE=server names every source whose hash differs from the one
|
|
228
229
|
// the last boot persisted — i.e. exactly why an artifact was renamed and
|
|
229
230
|
// recompiled instead of reused.
|
|
230
231
|
const booted =
|
|
231
|
-
|
|
232
|
-
process.env.PNEXT_DEV_CACHE_DEBUG && records.size > 0
|
|
232
|
+
traceEnabled('server') && records.size > 0
|
|
233
233
|
? new Map([...records].map(([file, record]) => [file, record.srcHash] as const))
|
|
234
234
|
: undefined
|
|
235
235
|
const pending = new Map<string, Promise<SourceRecord>>()
|
|
@@ -613,8 +613,15 @@ function markerFile(root: string) {
|
|
|
613
613
|
*/
|
|
614
614
|
function establishCacheTrust(root: string) {
|
|
615
615
|
const current = readMarker(root)
|
|
616
|
-
|
|
617
|
-
|
|
616
|
+
// Generation-qualified: artifacts embed paths shaped by pnext's own compile/vendor source (vendor
|
|
617
|
+
// keys, emitted import shapes), so a cache written by a different pnext generation must not be
|
|
618
|
+
// reused - 0.0.9-era modules kept importing vendor artifacts 0.0.10 keyed differently, and every
|
|
619
|
+
// request through one 500'd until a manual wipe. A mismatched or legacy marker distrusts the
|
|
620
|
+
// folder; everything recompiles in place.
|
|
621
|
+
const generation = `${frameworkFingerprint()}:`
|
|
622
|
+
const trusted = current?.startsWith(generation) ? current : undefined
|
|
623
|
+
const value = trusted ?? `${generation}${randomUUID()}`
|
|
624
|
+
if (!trusted) {
|
|
618
625
|
writeMarker(root, value)
|
|
619
626
|
wiped.add(root)
|
|
620
627
|
}
|
package/src/runtime/modules.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from './loader'
|
|
24
24
|
import { externalServerPackageHref, runtimeServerImportTarget } from '../runtime/vendor-build'
|
|
25
25
|
import { writeFileAtomic } from '../utils/fs'
|
|
26
|
+
import { traceEnabled, traceValue } from '../utils/trace-flags'
|
|
26
27
|
import { createHash } from 'node:crypto'
|
|
27
28
|
import {
|
|
28
29
|
extraLoadableExtensions,
|
|
@@ -39,12 +40,13 @@ import {
|
|
|
39
40
|
type CompatAliasTarget,
|
|
40
41
|
type ResolvedConfig,
|
|
41
42
|
} from '../config'
|
|
42
|
-
import { readText, toPosixPath } from '../utils/fs'
|
|
43
|
+
import { readText, toPosixPath, writeText } from '../utils/fs'
|
|
43
44
|
import {
|
|
44
45
|
type ExternalLoadTarget,
|
|
45
46
|
externalPackageImportTarget,
|
|
46
47
|
getExternalPackagePolicy,
|
|
47
48
|
isCommonJsModuleSource,
|
|
49
|
+
isEsmModuleFile,
|
|
48
50
|
packageNameOfSpecifier,
|
|
49
51
|
resolveImport,
|
|
50
52
|
resolveExternalLoadTarget,
|
|
@@ -99,7 +101,7 @@ const buildQueue: (() => void)[] = []
|
|
|
99
101
|
const moduleBuilds = new Map<string, Promise<string>>()
|
|
100
102
|
const routeBundleBuilds = new Map<string, Promise<string>>()
|
|
101
103
|
|
|
102
|
-
//
|
|
104
|
+
// PNEXT_TRACE=server: attribute every per-module compile to its profile,
|
|
103
105
|
// so cross-profile duplicate work is countable rather than inferred. Counts are
|
|
104
106
|
// load-insensitive, which is what makes them usable on a contended box.
|
|
105
107
|
interface ModuleStat {
|
|
@@ -109,9 +111,9 @@ interface ModuleStat {
|
|
|
109
111
|
}
|
|
110
112
|
// `${profile}:${file}` of modules that fell off the oxc fast path onto esbuild.
|
|
111
113
|
const esbuildFallbacks = new Set<string>()
|
|
112
|
-
const moduleStats: Map<string, ModuleStat[]> | undefined =
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
const moduleStats: Map<string, ModuleStat[]> | undefined = traceEnabled('server')
|
|
115
|
+
? new Map()
|
|
116
|
+
: undefined
|
|
115
117
|
let moduleStatsFlush: Timer | undefined
|
|
116
118
|
|
|
117
119
|
function noteModuleCompile(file: string, profile: string, ms: number, esbuild: boolean) {
|
|
@@ -163,10 +165,11 @@ function reportModuleStats() {
|
|
|
163
165
|
`${duplicateCompiles} redundant compiles, ${duplicateMs.toFixed(0)} ms nested-wall`,
|
|
164
166
|
].join('\n'),
|
|
165
167
|
)
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
const out =
|
|
168
|
+
// PNEXT_TRACE=server=<path>: also dump the per-file list, so a floor bench can
|
|
169
|
+
// replay the exact module set a route compiles in each profile. A `.jsonl` path
|
|
170
|
+
// belongs to the heavy-package profile instead (see vendor.ts).
|
|
171
|
+
const out = traceValue('server')
|
|
172
|
+
if (out?.endsWith('.jsonl')) return
|
|
170
173
|
if (!out) return
|
|
171
174
|
const files = [...moduleStats.entries()].flatMap(([file, entries]) =>
|
|
172
175
|
entries.map(entry => ({ file, profile: entry.profile, ms: entry.ms, esbuild: entry.esbuild })),
|
|
@@ -571,8 +574,7 @@ function startRouteVendorStage(config: ResolvedConfig, route: RouteManifestEntry
|
|
|
571
574
|
}
|
|
572
575
|
}
|
|
573
576
|
for (const specifier of demand) dropped.delete(specifier)
|
|
574
|
-
|
|
575
|
-
if (process.env.PNEXT_DEV_PROFILE) {
|
|
577
|
+
if (traceEnabled('server')) {
|
|
576
578
|
console.log(
|
|
577
579
|
`dev-import vendor seed walk ${route.route} in ${formatDuration(performance.now() - seedT0)} (kept ${demand.size}, dropped ${dropped.size})`,
|
|
578
580
|
)
|
|
@@ -1873,26 +1875,21 @@ async function resolveDevModuleSpecifier(
|
|
|
1873
1875
|
)
|
|
1874
1876
|
return { path: `${pathToFileHref(stub)}${hash}`, external: true }
|
|
1875
1877
|
}
|
|
1876
|
-
//
|
|
1877
|
-
//
|
|
1878
|
-
//
|
|
1879
|
-
//
|
|
1880
|
-
//
|
|
1881
|
-
|
|
1878
|
+
// A `require()` of an ES module cannot take the compiled-path rewrite: the artifact is ESM, and
|
|
1879
|
+
// requiring one throws the moment it is not synchronously evaluable. Let esbuild bundle it and do
|
|
1880
|
+
// the interop, exactly as the alias branch above already does for a require-call. Under an
|
|
1881
|
+
// npm-layout install this is the common case - node_modules is INSIDE workspaceRoot, so a
|
|
1882
|
+
// package's own ESM file counts as first-party source here.
|
|
1883
|
+
if (kind === 'require-call' && isEsmModuleFile(resolved)) return { path: resolved }
|
|
1882
1884
|
const importerFile = importer && !importer.startsWith('<') ? path.resolve(importer) : ''
|
|
1885
|
+
// `importerFile !== file` keeps this to imports the write pass misses: the entry's OWN local
|
|
1886
|
+
// imports are already walked by writeDevModuleUncached, so another importer means esbuild INLINED
|
|
1887
|
+
// that module here and nobody compiles its relative siblings. It can never also key on the importer
|
|
1888
|
+
// being out-of-workspace - under an npm-layout install pnext's runtime files live in the app's
|
|
1889
|
+
// node_modules, and the sibling was then silently never written. Recompiling is not a risk:
|
|
1890
|
+
// writeDevModule returns an already-written output as-is, so island identity survives navigation.
|
|
1883
1891
|
const bundledCompatImporter =
|
|
1884
|
-
isLocalSourceSpecifier &&
|
|
1885
|
-
importerFile !== '' &&
|
|
1886
|
-
importerFile !== path.resolve(file) &&
|
|
1887
|
-
!isInside(config.workspaceRoot, importerFile) &&
|
|
1888
|
-
!isInside(config.workspaceRoot, resolved)
|
|
1889
|
-
// The `importerFile !== file` term above keeps this to imports the write pass misses: the entry's
|
|
1890
|
-
// OWN local imports are already walked by writeDevModuleUncached. Everything deeper - a relative
|
|
1891
|
-
// sibling of a compat file esbuild inlined into the entry - is nobody's job but this one, whether
|
|
1892
|
-
// the entry is a workspace module or a node_modules-nested 'use client' component. Gating on an
|
|
1893
|
-
// in-workspace entry left those nested client references importing a file that was never written.
|
|
1894
|
-
// Recompiling is not a risk: writeDevModule returns an already-written output as-is, so island
|
|
1895
|
-
// identity across a soft navigation is preserved.
|
|
1892
|
+
isLocalSourceSpecifier && importerFile !== '' && importerFile !== path.resolve(file)
|
|
1896
1893
|
if (bundledCompatImporter) {
|
|
1897
1894
|
// Compile it here, keeping the same compiled-path rewrite so the module stays a single instance -
|
|
1898
1895
|
// pointing at the raw source instead would load a SECOND copy of the compat runtime graph and split its
|
|
@@ -2422,6 +2419,14 @@ async function linkDirectoryAssetContext(
|
|
|
2422
2419
|
}
|
|
2423
2420
|
if (existsSync(target)) return
|
|
2424
2421
|
await mkdir(path.dirname(target), { recursive: true })
|
|
2422
|
+
// A package manifest linked whole makes the staged mirror a RESOLVABLE package: a bare
|
|
2423
|
+
// `pkg/sub` import from a compiled artifact finds this copy first and fails on an `exports`
|
|
2424
|
+
// subpath that only exists in the real tree. Stage the one field the mirror needs (`type`,
|
|
2425
|
+
// which decides how a compiled `.js` sibling parses) and none of the resolution map.
|
|
2426
|
+
if (entry.name === 'package.json') {
|
|
2427
|
+
await writeText(target, await stagedManifestSource(source))
|
|
2428
|
+
return
|
|
2429
|
+
}
|
|
2425
2430
|
try {
|
|
2426
2431
|
await symlink(source, target, 'file')
|
|
2427
2432
|
} catch {
|
|
@@ -2431,6 +2436,15 @@ async function linkDirectoryAssetContext(
|
|
|
2431
2436
|
)
|
|
2432
2437
|
}
|
|
2433
2438
|
|
|
2439
|
+
async function stagedManifestSource(file: string) {
|
|
2440
|
+
try {
|
|
2441
|
+
const manifest = JSON.parse(await readText(file)) as { type?: unknown }
|
|
2442
|
+
return JSON.stringify(typeof manifest.type === 'string' ? { type: manifest.type } : {})
|
|
2443
|
+
} catch {
|
|
2444
|
+
return '{}'
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2434
2448
|
// Recursively stage an asset directory: link non-code leaf files and recurse
|
|
2435
2449
|
// into subdirectories, but never link code files — those compile through the
|
|
2436
2450
|
// module graph into their own staged output and must not be shadowed by a raw
|
|
@@ -2535,8 +2549,7 @@ function uniqueFiles(files: string[]) {
|
|
|
2535
2549
|
}
|
|
2536
2550
|
|
|
2537
2551
|
async function profileDevImport<T>(label: string, task: () => Promise<T>) {
|
|
2538
|
-
|
|
2539
|
-
if (!process.env.PNEXT_DEV_PROFILE) return task()
|
|
2552
|
+
if (!traceEnabled('server')) return task()
|
|
2540
2553
|
const start = performance.now()
|
|
2541
2554
|
try {
|
|
2542
2555
|
return await task()
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from '../resolve/imports'
|
|
42
42
|
import { clearNodeModulesResolutionCache } from '../resolve/engine'
|
|
43
43
|
import { cachedExistsSync, clearNodeModulesFsCache } from '../utils/fs-cache'
|
|
44
|
+
import { traceEnabled } from '../utils/trace-flags'
|
|
44
45
|
import {
|
|
45
46
|
clientReferenceExportNames,
|
|
46
47
|
clientReferenceModuleSource,
|
|
@@ -729,8 +730,7 @@ export function vendorPreplanReport() {
|
|
|
729
730
|
return { ...vendorPreplanStats, reasons: vendorPreplanStats.reasons.slice(0, 60) }
|
|
730
731
|
}
|
|
731
732
|
|
|
732
|
-
|
|
733
|
-
const vendorPreplanDebug = () => process.env.PNEXT_VENDOR_PREPLAN_DEBUG === '1'
|
|
733
|
+
const vendorPreplanDebug = () => traceEnabled('vendor')
|
|
734
734
|
let preplanReportFlush: ReturnType<typeof setTimeout> | undefined
|
|
735
735
|
function schedulePreplanReport() {
|
|
736
736
|
if (!vendorPreplanDebug()) return
|
|
@@ -1130,12 +1130,11 @@ async function buildVendorGroup(
|
|
|
1130
1130
|
await emitNestedExternalTraceCopies(built[0]!.file, context.nestedExternalFiles)
|
|
1131
1131
|
}
|
|
1132
1132
|
|
|
1133
|
-
// vendor plugin-callback attribution (
|
|
1133
|
+
// vendor plugin-callback attribution (PNEXT_TRACE=vendor, dormant otherwise). Every
|
|
1134
1134
|
// onResolve/onLoad is an IPC round trip esbuild's bundler blocks on, against the one JS thread that
|
|
1135
1135
|
// answers them; counts separate "our callbacks are slow" from "we answer many questions".
|
|
1136
1136
|
|
|
1137
|
-
|
|
1138
|
-
const vendorProfileEnabled = process.env.PNEXT_VENDOR_PROFILE === '1'
|
|
1137
|
+
const vendorProfileEnabled = traceEnabled('vendor')
|
|
1139
1138
|
const vendorProfileTotals = new Map<string, number>()
|
|
1140
1139
|
|
|
1141
1140
|
function vendorProfileCount(key: string, by = 1) {
|
|
@@ -2532,8 +2531,7 @@ function isResolveFailure(error: unknown) {
|
|
|
2532
2531
|
}
|
|
2533
2532
|
|
|
2534
2533
|
async function profileRuntimeStep<T>(label: string, task: () => Promise<T>) {
|
|
2535
|
-
|
|
2536
|
-
if (!process.env.PNEXT_DEV_PROFILE) return task()
|
|
2534
|
+
if (!traceEnabled('server')) return task()
|
|
2537
2535
|
const start = performance.now()
|
|
2538
2536
|
try {
|
|
2539
2537
|
return await task()
|
package/src/runtime/vendor.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { isCommonJsModuleSource } from '../resolve/imports'
|
|
|
42
42
|
import { outputSpecifiers } from './module-transform'
|
|
43
43
|
import { escapeRegex, isIdentifier, uniqueIdentifier } from '../utils/code'
|
|
44
44
|
import { cachedExistsSync } from '../utils/fs-cache'
|
|
45
|
+
import { traceEnabled, traceValue } from '../utils/trace-flags'
|
|
45
46
|
import { writeFileAtomic } from '../utils/fs'
|
|
46
47
|
|
|
47
48
|
// --------------------------------------------------------------------------
|
|
@@ -217,7 +218,7 @@ function promoteVendorWork(work: VendorPromotable | undefined) {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
// --------------------------------------------------------------------------
|
|
220
|
-
// saturation trace (
|
|
221
|
+
// saturation trace (PNEXT_TRACE=vendor)
|
|
221
222
|
// --------------------------------------------------------------------------
|
|
222
223
|
|
|
223
224
|
/** One build's occupancy of a worker slot — the active-builds-over-time input. */
|
|
@@ -230,8 +231,7 @@ export interface VendorBuildSpan {
|
|
|
230
231
|
|
|
231
232
|
const vendorSpans: VendorBuildSpan[] = []
|
|
232
233
|
|
|
233
|
-
|
|
234
|
-
const vendorTracing = () => process.env.PNEXT_VENDOR_TRACE === '1'
|
|
234
|
+
const vendorTracing = () => traceEnabled('vendor')
|
|
235
235
|
|
|
236
236
|
function openVendorSpan(label: string, nested: boolean) {
|
|
237
237
|
if (!vendorTracing()) return undefined
|
|
@@ -245,14 +245,13 @@ function closeVendorSpan(span: VendorBuildSpan | undefined) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// --------------------------------------------------------------------------
|
|
248
|
-
// TEMPORARY JSONL workload trace (
|
|
248
|
+
// TEMPORARY JSONL workload trace (PNEXT_TRACE=vendor=<path>)
|
|
249
249
|
// --------------------------------------------------------------------------
|
|
250
250
|
|
|
251
|
-
// A
|
|
252
|
-
// bench/tools/vendor-analyze.ts;
|
|
251
|
+
// A path-valued `vendor` scope appends build/request/edge rows for
|
|
252
|
+
// bench/tools/vendor-analyze.ts; a bare `vendor` means the in-memory spans only.
|
|
253
253
|
const vendorTraceFile = (() => {
|
|
254
|
-
|
|
255
|
-
const value = process.env.PNEXT_VENDOR_TRACE
|
|
254
|
+
const value = traceValue('vendor')
|
|
256
255
|
if (!value || !(value.includes('/') || value.endsWith('.jsonl'))) return undefined
|
|
257
256
|
return path.resolve(value)
|
|
258
257
|
})()
|
|
@@ -881,7 +880,10 @@ const ESM_DIST_ENTRY_MARKER = 'entry'
|
|
|
881
880
|
|
|
882
881
|
const esmDists = new Map<string, Promise<string | undefined>>()
|
|
883
882
|
|
|
884
|
-
export function copyBrowserReadyEsmDist(
|
|
883
|
+
export function copyBrowserReadyEsmDist(
|
|
884
|
+
entry: string,
|
|
885
|
+
distDir: string,
|
|
886
|
+
): Promise<string | undefined> {
|
|
885
887
|
// One copy per destination: a second copier would rewrite files the first has
|
|
886
888
|
// already published, and copyFile is not atomic against a concurrent import.
|
|
887
889
|
let dist = esmDists.get(distDir)
|
|
@@ -889,14 +891,24 @@ export function copyBrowserReadyEsmDist(entry: string, distDir: string) {
|
|
|
889
891
|
dist = copyBrowserReadyEsmDistUncached(entry, distDir)
|
|
890
892
|
esmDists.set(distDir, dist)
|
|
891
893
|
}
|
|
892
|
-
|
|
894
|
+
// A `pnext build` wiping `.pnext` under this server deletes published copies out from under the
|
|
895
|
+
// memo; a bundle repointed at a vanished dist 500s on every import. Re-verify against disk and
|
|
896
|
+
// re-copy - the same only-the-stale-holder-evicts rule dropVendorBundle applies to bundles.
|
|
897
|
+
return dist.then(published => {
|
|
898
|
+
if (published === undefined || existsSync(published)) return published
|
|
899
|
+
if (esmDists.get(distDir) === dist) esmDists.delete(distDir)
|
|
900
|
+
return copyBrowserReadyEsmDist(entry, distDir)
|
|
901
|
+
})
|
|
893
902
|
}
|
|
894
903
|
|
|
895
904
|
async function copyBrowserReadyEsmDistUncached(entry: string, distDir: string) {
|
|
896
905
|
const existing = await readFile(path.join(distDir, ESM_DIST_ENTRY_MARKER), 'utf8').catch(
|
|
897
906
|
() => undefined,
|
|
898
907
|
)
|
|
899
|
-
|
|
908
|
+
// The marker without its entry is a half-deleted copy: fall through and rewrite it.
|
|
909
|
+
if (existing !== undefined && existsSync(path.join(distDir, existing))) {
|
|
910
|
+
return path.join(distDir, existing)
|
|
911
|
+
}
|
|
900
912
|
|
|
901
913
|
const closure = await collectBrowserReadyEsmClosure(entry)
|
|
902
914
|
if (!closure) return undefined
|
|
@@ -1050,8 +1062,12 @@ export function cjsNoFacadeMarker(bundleFile: string) {
|
|
|
1050
1062
|
|
|
1051
1063
|
const facades = new Map<string, Promise<string | undefined>>()
|
|
1052
1064
|
|
|
1053
|
-
//
|
|
1054
|
-
|
|
1065
|
+
// A `.jsonl` value on the server scope selects this profile; any other path is the
|
|
1066
|
+
// per-module stats dump (runtime/modules.ts).
|
|
1067
|
+
const heavyProfFile = (() => {
|
|
1068
|
+
const value = traceValue('server')
|
|
1069
|
+
return value?.endsWith('.jsonl') ? value : undefined
|
|
1070
|
+
})()
|
|
1055
1071
|
export function heavyProfRow(row: Record<string, unknown>) {
|
|
1056
1072
|
if (!heavyProfFile) return
|
|
1057
1073
|
try {
|
package/src/types.ts
CHANGED
|
@@ -56,11 +56,20 @@ export interface PNextAdapterConfig {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export interface PNextCompatConfig {
|
|
59
|
-
next?: boolean
|
|
59
|
+
next?: boolean | PNextNextCompatConfig
|
|
60
60
|
react?: boolean
|
|
61
61
|
reactCompiler?: boolean
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Opt-in Next-compat behaviours. Everything here defaults off. */
|
|
65
|
+
export interface PNextNextCompatConfig {
|
|
66
|
+
/**
|
|
67
|
+
* Next 15's transitional sync request APIs: cookies()/headers()/draftMode() return a Promise that
|
|
68
|
+
* also carries the sync surface, warning once per callsite. Off by default (Next 16 semantics).
|
|
69
|
+
*/
|
|
70
|
+
legacyRequestAPIs?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
export interface MetadataLink {
|
|
65
74
|
rel: string
|
|
66
75
|
url: string
|
package/src/utils/code.ts
CHANGED
|
@@ -36,3 +36,8 @@ export function uniqueIdentifier(source: string, base: string, ...used: string[]
|
|
|
36
36
|
export function isIdentifier(value: string) {
|
|
37
37
|
return /^[A-Za-z_$][\w$]*$/.test(value)
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** Source with comments removed; the `[^:]` guard keeps `://` inside string URLs intact. */
|
|
41
|
+
export function stripComments(source: string): string {
|
|
42
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
43
|
+
}
|
package/src/utils/dev-profile.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Buffered sink for
|
|
2
|
+
* Buffered sink for PNEXT_TRACE=server lines. A warm request emits many phase lines, and writing each
|
|
3
3
|
* straight to stdout inflated every enclosing span it sat inside - the measurement changed what it
|
|
4
4
|
* measured. Lines accumulate here and the request handler flushes them in one write.
|
|
5
5
|
*/
|
package/src/utils/esbuild.ts
CHANGED
|
@@ -27,10 +27,45 @@ function track<T>(promise: Promise<T>): Promise<T> {
|
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// A service killed out from under an in-flight call (memory watchdog, crash) fails it with this
|
|
31
|
+
// exact message; the next call respawns the service, so one retry turns a user-visible 500 into
|
|
32
|
+
// a slightly slower response.
|
|
33
|
+
// "no longer running": issued against a dead cached service. "was stopped": in flight when a
|
|
34
|
+
// stop() (watchdog or the reset below) tore the service down.
|
|
35
|
+
const serviceDied = (error: unknown) =>
|
|
36
|
+
error instanceof Error &&
|
|
37
|
+
(error.message.includes('The service is no longer running') ||
|
|
38
|
+
error.message.includes('The service was stopped'))
|
|
39
|
+
|
|
40
|
+
// esbuild caches the dead service until stop() resets it. The reset is epoch-guarded: every call
|
|
41
|
+
// that raced the same dead service shares one stop(), because a second stop() would kill the fresh
|
|
42
|
+
// child under the retries already running against it.
|
|
43
|
+
let resetEpoch = 0
|
|
44
|
+
let serviceReset: Promise<void> | undefined
|
|
45
|
+
|
|
46
|
+
async function withServiceRetry<T>(run: () => Promise<T>): Promise<T> {
|
|
47
|
+
const epoch = resetEpoch
|
|
48
|
+
try {
|
|
49
|
+
return await track(run())
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!serviceDied(error)) throw error
|
|
52
|
+
if (epoch === resetEpoch) {
|
|
53
|
+
serviceReset ??= Promise.resolve(esbuild?.stop())
|
|
54
|
+
.catch(() => undefined)
|
|
55
|
+
.then(() => {
|
|
56
|
+
resetEpoch += 1
|
|
57
|
+
serviceReset = undefined
|
|
58
|
+
})
|
|
59
|
+
await serviceReset
|
|
60
|
+
}
|
|
61
|
+
return track(run())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const build: typeof Esbuild.build = options => withServiceRetry(() => load().build(options))
|
|
31
66
|
|
|
32
67
|
export const transform: typeof Esbuild.transform = (input, options) =>
|
|
33
|
-
|
|
68
|
+
withServiceRetry(() => load().transform(input, options))
|
|
34
69
|
|
|
35
70
|
/**
|
|
36
71
|
* Spawn the resident service child ahead of the first real compile. Fire-and-forget: the spawn and
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PNEXT_TRACE=<comma-separated scopes>` - the single umbrella for pnext's diagnostic output.
|
|
3
|
+
* The variable is parsed once, on first use; every later check is a map lookup on a boolean test,
|
|
4
|
+
* so a build with tracing off pays nothing.
|
|
5
|
+
*
|
|
6
|
+
* Grammar: entries are `scope` or `scope=value`, comma separated. `1` is an alias for `boot`
|
|
7
|
+
* (`PNEXT_TRACE=1` keeps its old meaning) and `all` turns on every scope.
|
|
8
|
+
* Example: `PNEXT_TRACE=boot,server=/tmp/modules.json`.
|
|
9
|
+
*
|
|
10
|
+
* A scope is all-or-nothing: turning one on turns on everything it covers.
|
|
11
|
+
*
|
|
12
|
+
* | scope | prints / does |
|
|
13
|
+
* | -------- | --------------------------------------------------------------------------------- |
|
|
14
|
+
* | `boot` | boot-phase timings and memory readings |
|
|
15
|
+
* | `server` | dev request / render / build timings, per-module compile attribution, route-facts and global-css hit/miss, cache-invalidation reasons, load-plugin registrations |
|
|
16
|
+
* | `vendor` | vendor saturation spans, esbuild plugin-callback attribution, preplan/native report |
|
|
17
|
+
* | `client` | client-build phase timings, and the esbuild metafile written to the output dir |
|
|
18
|
+
* | `all` | the four above |
|
|
19
|
+
*
|
|
20
|
+
* Two scopes take a `=<path>` value:
|
|
21
|
+
* - `server=<path>` dumps the per-module compile list as JSON; a `.jsonl` path instead collects the
|
|
22
|
+
* heavy-package facade profile (they are never wanted at once, and the suffix picks between them).
|
|
23
|
+
* - `vendor=<path>` appends the JSONL workload trace bench/tools/vendor-analyze.ts reads.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Parse one raw `PNEXT_TRACE` value into scope -> value (`''` when the entry carried none). */
|
|
27
|
+
export function parseTraceFlags(raw: string | undefined): Map<string, string> {
|
|
28
|
+
const scopes = new Map<string, string>()
|
|
29
|
+
for (const entry of raw?.split(',') ?? []) {
|
|
30
|
+
const trimmed = entry.trim()
|
|
31
|
+
if (!trimmed) continue
|
|
32
|
+
const eq = trimmed.indexOf('=')
|
|
33
|
+
const name = eq === -1 ? trimmed : trimmed.slice(0, eq).trim()
|
|
34
|
+
if (!name) continue
|
|
35
|
+
scopes.set(name === '1' ? 'boot' : name, eq === -1 ? '' : trimmed.slice(eq + 1).trim())
|
|
36
|
+
}
|
|
37
|
+
return scopes
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let scopes: Map<string, string> | undefined
|
|
41
|
+
let all = false
|
|
42
|
+
|
|
43
|
+
function flags() {
|
|
44
|
+
if (!scopes) {
|
|
45
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
46
|
+
scopes = parseTraceFlags(process.env.PNEXT_TRACE)
|
|
47
|
+
all = scopes.has('all')
|
|
48
|
+
}
|
|
49
|
+
return scopes
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Drop the memoized parse - for tests and benches that flip PNEXT_TRACE in-process. */
|
|
53
|
+
export function resetTraceFlags() {
|
|
54
|
+
scopes = undefined
|
|
55
|
+
all = false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True when `scope` (or `all`) is listed in PNEXT_TRACE, with or without a value. */
|
|
59
|
+
export function traceEnabled(scope: string): boolean {
|
|
60
|
+
return flags().has(scope) || all
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The `scope=<value>` payload, or undefined when the scope is absent or valueless. */
|
|
64
|
+
export function traceValue(scope: string): string | undefined {
|
|
65
|
+
return flags().get(scope) || undefined
|
|
66
|
+
}
|