@wular/pnext 0.0.10 → 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/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/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 +2 -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/trace-flags.ts +66 -0
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
|
*/
|
|
@@ -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
|
+
}
|