@wular/pnext 0.0.24 → 0.1.0
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/bin/pnext +1 -1
- package/package.json +2 -2
- package/reference/data/bench.json +398 -223
- package/reference/getting-started.md +1 -1
- package/src/api/client-navigation.ts +7 -5
- package/src/cli/adapters/vercel-warm.ts +120 -28
- package/src/cli/build.ts +56 -15
- package/src/cli/create.ts +1 -1
- package/src/cli/dev.ts +2 -1
- package/src/cli/index.ts +146 -10
- package/src/cli/migrate/report.ts +1 -1
- package/src/cli/serve/pipeline.ts +23 -6
- package/src/cli/serve/ui.ts +1 -1
- package/src/cli/start.ts +2 -1
- package/src/cli/typegen.ts +5 -2
- package/src/client/build.ts +217 -58
- package/src/client/chunk-fold.ts +7 -1
- package/src/client/entry.ts +44 -7
- package/src/client/router/runtime.ts +1016 -230
- package/src/client/router/types.ts +15 -0
- package/src/compat/actions/client-plugin.ts +10 -0
- package/src/compat/actions/rewrite.ts +1 -0
- package/src/compat/bundler/optimize-package-imports.ts +375 -28
- package/src/compat/client/navigation-scroll.ts +29 -3
- package/src/compat/client/optimistic-routing.ts +2 -2
- package/src/compat/client/segment-cache-policy.ts +4 -18
- package/src/compat/client/segment-cache.ts +104 -0
- package/src/compat/client/segment-prefetch.ts +6 -6
- package/src/compat/next/config-loader.ts +6 -4
- package/src/compat/next/navigation.ts +16 -2
- package/src/compat/pages/router.ts +102 -9
- package/src/compat/protocol.ts +10 -1
- package/src/compat/register/actions.ts +2 -0
- package/src/compat/register/bundler.ts +4 -6
- package/src/compat/register/routing.ts +3 -4
- package/src/compat/tsconfig-defaults.ts +3 -2
- package/src/config.ts +3 -3
- package/src/css/postcss.ts +450 -17
- package/src/dev/client-actions.ts +2 -0
- package/src/dev/server.ts +87 -14
- package/src/render/island-context.ts +3 -0
- package/src/render/ppr.ts +14 -0
- package/src/render/renderer.ts +268 -42
- package/src/request/context.ts +32 -11
- package/src/resolve/scan-facts.ts +23 -4
- package/src/routing/proxy.ts +29 -25
- package/src/routing/routes.ts +1 -1
- package/src/runtime/loader.ts +47 -1
- package/src/runtime/modules.ts +546 -65
- package/src/runtime/vendor-build.ts +109 -22
- package/src/runtime/vendor.ts +7 -3
- package/src/utils/ansi.ts +3 -1
- package/src/utils/fs.ts +17 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
pnext is a Preact framework for file-routed apps. It renders on the server by default and ships client JavaScript only where a component opts in.
|
|
4
4
|
|
|
5
|
-
> pnext requires [Bun](https://bun.sh/get) 1.3.
|
|
5
|
+
> pnext requires [Bun](https://bun.sh/get) 1.3.10 or newer (1.4.0 recommended) - make sure it is installed first.
|
|
6
6
|
|
|
7
7
|
<!-- tabs:start -->
|
|
8
8
|
|
|
@@ -176,15 +176,17 @@ function useLocation() {
|
|
|
176
176
|
return previous === next ? previous : next
|
|
177
177
|
})
|
|
178
178
|
locationListeners().add(listener)
|
|
179
|
-
window.addEventListener('popstate', listener)
|
|
180
179
|
// Re-read once the listener is attached: the URL can move between the
|
|
181
180
|
// render that captured it and this effect (a history traversal landing
|
|
182
181
|
// while a swapped-in tree mounts), and nothing was listening for that one.
|
|
182
|
+
// Raw popstate is deliberately NOT a subscription source: on a full
|
|
183
|
+
// traversal the address bar moves before the cached document commits, so
|
|
184
|
+
// rendering that URL into the departing island can consume the only route
|
|
185
|
+
// change before the persistent island is grafted into the committed tree.
|
|
186
|
+
// The router broadcasts shallow traversals immediately and full traversals
|
|
187
|
+
// after their DOM commit.
|
|
183
188
|
listener()
|
|
184
|
-
return () =>
|
|
185
|
-
locationListeners().delete(listener)
|
|
186
|
-
window.removeEventListener('popstate', listener)
|
|
187
|
-
}
|
|
189
|
+
return () => locationListeners().delete(listener)
|
|
188
190
|
}, [])
|
|
189
191
|
|
|
190
192
|
return location
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module warm-up for
|
|
2
|
+
* Module warm-up for a production build - run in its own process.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Every build compiles the modules its production server can need (`compile` mode): a built app that
|
|
5
|
+
* still compiles on its first request pays a whole cold pipeline - esbuild service included - inside
|
|
6
|
+
* the response, which is neither a build output nor something a served request should do.
|
|
7
|
+
*
|
|
8
|
+
* The vercel adapter needs strictly more (`full` mode). Request time on Vercel is read-only, so
|
|
9
|
+
* everything a production start would compile or *vendor* lazily has to be on disk already: that
|
|
10
|
+
* mode also imports the app's route handlers and page modules once, so their on-demand vendor
|
|
11
|
+
* bundles get written now.
|
|
8
12
|
*
|
|
9
13
|
* Importing handlers runs the app's own server code, which can crash the runtime rather than throw,
|
|
10
14
|
* so the whole pass runs in a child that writes into the same on-disk build cache the parent reads
|
|
@@ -35,7 +39,13 @@ import {
|
|
|
35
39
|
import { getCompatModeExtensions } from '../../extensions'
|
|
36
40
|
import { getFontExtensions } from '../../render/hooks'
|
|
37
41
|
import { findConventionFiles, findLayouts } from '../../routing/routes'
|
|
38
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
prewarmServerTransforms,
|
|
44
|
+
registerServerRuntime,
|
|
45
|
+
serverBundleTargetForRuntime,
|
|
46
|
+
} from '../../runtime/loader'
|
|
47
|
+
import { cacheRoot } from '../../runtime/module-cache'
|
|
48
|
+
import { listFiles } from '../../utils/fs'
|
|
39
49
|
import { namedBunBinary } from '../boot/named-bin'
|
|
40
50
|
import type { VerboseLogger } from '../../utils/verbose'
|
|
41
51
|
import type { BuildManifest } from '../../types'
|
|
@@ -73,6 +83,15 @@ const ATTEMPT_TIMEOUT_MS = 10 * 60_000
|
|
|
73
83
|
// before concluding the app cannot be warmed.
|
|
74
84
|
const MAX_ATTEMPTS = 3
|
|
75
85
|
|
|
86
|
+
/**
|
|
87
|
+
* `compile` writes every artifact the production server would otherwise build inside a request;
|
|
88
|
+
* `full` additionally imports the app's own modules so their vendor bundles land too (see above).
|
|
89
|
+
*/
|
|
90
|
+
export type WarmMode = 'compile' | 'full'
|
|
91
|
+
|
|
92
|
+
/** argv flag rather than a stdin message: the child picks its mode before the first prewarm batch. */
|
|
93
|
+
const FULL_MODE_FLAG = '--full'
|
|
94
|
+
|
|
76
95
|
export interface WarmChild {
|
|
77
96
|
/**
|
|
78
97
|
* Hand the child the page sources from the route scan so it compiles them
|
|
@@ -87,17 +106,38 @@ export interface WarmChild {
|
|
|
87
106
|
* child runs handlers concurrently.
|
|
88
107
|
*/
|
|
89
108
|
finish(log: VerboseLogger, onCompiled?: (modules: string[]) => void): Promise<string[]>
|
|
109
|
+
/** Stop every attempt owned by this build. Safe to call after `finish` or more than once. */
|
|
110
|
+
kill(): void
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
type WarmProcess = Bun.Subprocess<'pipe', 'pipe', '
|
|
113
|
+
type WarmProcess = Bun.Subprocess<'pipe', 'pipe', 'pipe'>
|
|
93
114
|
|
|
94
115
|
/**
|
|
95
116
|
* Spawn the warm child. Call as early in the build as possible: it boots while the build runs and
|
|
96
117
|
* does no work until `finish`, which must be called only after the build manifest is on disk. Never
|
|
97
118
|
* throws - a warm-up that cannot start degrades to "no warmed modules", which `finish` reports.
|
|
98
119
|
*/
|
|
99
|
-
export function startWarmChild(config: ResolvedConfig): WarmChild {
|
|
100
|
-
const
|
|
120
|
+
export function startWarmChild(config: ResolvedConfig, mode: WarmMode = 'full'): WarmChild {
|
|
121
|
+
const children = new Set<WarmProcess>()
|
|
122
|
+
let disposed = false
|
|
123
|
+
const spawn = () => {
|
|
124
|
+
if (disposed) return undefined
|
|
125
|
+
const child = spawnWarmProcess(config, mode)
|
|
126
|
+
if (!child) return undefined
|
|
127
|
+
children.add(child)
|
|
128
|
+
void child.exited.finally(() => children.delete(child))
|
|
129
|
+
void forwardWarmStderr(child)
|
|
130
|
+
return child
|
|
131
|
+
}
|
|
132
|
+
const killOnExit = () => kill()
|
|
133
|
+
const kill = () => {
|
|
134
|
+
if (disposed) return
|
|
135
|
+
disposed = true
|
|
136
|
+
process.off('exit', killOnExit)
|
|
137
|
+
for (const child of children) child.kill()
|
|
138
|
+
children.clear()
|
|
139
|
+
}
|
|
140
|
+
process.once('exit', killOnExit)
|
|
101
141
|
const first = spawn()
|
|
102
142
|
return {
|
|
103
143
|
prewarm(files) {
|
|
@@ -109,17 +149,37 @@ export function startWarmChild(config: ResolvedConfig): WarmChild {
|
|
|
109
149
|
// broken pipe: the child is gone; `finish` reports why
|
|
110
150
|
}
|
|
111
151
|
},
|
|
112
|
-
finish: (log, onCompiled) =>
|
|
152
|
+
finish: (log, onCompiled) =>
|
|
153
|
+
warmWithRestarts(first, spawn, mode, log, onCompiled).finally(kill),
|
|
154
|
+
kill,
|
|
113
155
|
}
|
|
114
156
|
}
|
|
115
157
|
|
|
116
|
-
|
|
158
|
+
/** Forward diagnostics without giving the warm child the caller's stderr descriptor. */
|
|
159
|
+
async function forwardWarmStderr(child: WarmProcess) {
|
|
160
|
+
try {
|
|
161
|
+
const reader = child.stderr.getReader()
|
|
162
|
+
for (;;) {
|
|
163
|
+
const { done, value } = await reader.read()
|
|
164
|
+
if (done) return
|
|
165
|
+
process.stderr.write(value)
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// The build shutting the child down also closes this pipe.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function spawnWarmProcess(config: ResolvedConfig, mode: WarmMode): WarmProcess | undefined {
|
|
117
173
|
const entry = fileURLToPath(new URL(import.meta.url))
|
|
174
|
+
const argv = mode === 'full' ? [config.root, FULL_MODE_FLAG] : [config.root]
|
|
118
175
|
try {
|
|
119
|
-
return Bun.spawn(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
176
|
+
return Bun.spawn([namedBunBinary('pnext-warm'), '--conditions=react-server', entry, ...argv], {
|
|
177
|
+
cwd: config.root,
|
|
178
|
+
stdin: 'pipe',
|
|
179
|
+
stdout: 'pipe',
|
|
180
|
+
stderr: 'pipe',
|
|
181
|
+
env: process.env,
|
|
182
|
+
})
|
|
123
183
|
} catch {
|
|
124
184
|
return undefined // no child (fork limit, unsupported platform)
|
|
125
185
|
}
|
|
@@ -128,6 +188,7 @@ function spawnWarmProcess(config: ResolvedConfig): WarmProcess | undefined {
|
|
|
128
188
|
async function warmWithRestarts(
|
|
129
189
|
first: WarmProcess | undefined,
|
|
130
190
|
spawn: () => WarmProcess | undefined,
|
|
191
|
+
mode: WarmMode,
|
|
131
192
|
log: VerboseLogger,
|
|
132
193
|
onCompiled?: (modules: string[]) => void,
|
|
133
194
|
) {
|
|
@@ -162,7 +223,7 @@ async function warmWithRestarts(
|
|
|
162
223
|
// thing it would redo is what killed it.
|
|
163
224
|
child = finished.size > before && attempt < MAX_ATTEMPTS ? spawn() : undefined
|
|
164
225
|
if (child) log.log(`warm-up ${failure} after ${finished.size} files; restarting past them`)
|
|
165
|
-
else warmIncomplete(failure, finished.size)
|
|
226
|
+
else warmIncomplete(failure, finished.size, mode)
|
|
166
227
|
}
|
|
167
228
|
fireOnCompiled?.()
|
|
168
229
|
return compiled()
|
|
@@ -222,11 +283,13 @@ async function runAttempt(
|
|
|
222
283
|
return child.signalCode ? `was killed by ${child.signalCode}` : `exited with code ${code}`
|
|
223
284
|
}
|
|
224
285
|
|
|
225
|
-
function warmIncomplete(failure: string, done: number) {
|
|
286
|
+
function warmIncomplete(failure: string, done: number, mode: WarmMode) {
|
|
226
287
|
console.warn(
|
|
227
|
-
`
|
|
228
|
-
|
|
229
|
-
|
|
288
|
+
`pnext build: module warm-up ${failure} after warming ${done} file(s). ` +
|
|
289
|
+
(mode === 'full'
|
|
290
|
+
? 'The function ships without the rest of its module cache and may fail to render those ' +
|
|
291
|
+
"routes at request time, where Vercel's filesystem is read-only."
|
|
292
|
+
: 'The production server compiles the rest on the first request that needs them.'),
|
|
230
293
|
)
|
|
231
294
|
}
|
|
232
295
|
|
|
@@ -279,6 +342,7 @@ export async function warmRouteModules(
|
|
|
279
342
|
config: ResolvedConfig,
|
|
280
343
|
manifest: BuildManifest,
|
|
281
344
|
skip: ReadonlySet<string> = new Set(),
|
|
345
|
+
mode: WarmMode = 'full',
|
|
282
346
|
) {
|
|
283
347
|
const clientFiles = new Set<string>()
|
|
284
348
|
const handlers: BuildManifest['routes'] = []
|
|
@@ -313,9 +377,31 @@ export async function warmRouteModules(
|
|
|
313
377
|
report(file, fileURLToPath(href)),
|
|
314
378
|
),
|
|
315
379
|
),
|
|
380
|
+
// Handler artifacts are otherwise only written by the import below, which
|
|
381
|
+
// `compile` mode does not run: compiling them here is what keeps a handler
|
|
382
|
+
// route's first served request off the compile pipeline too.
|
|
383
|
+
...(mode === 'compile'
|
|
384
|
+
? handlers.map(async route => {
|
|
385
|
+
registerServerRuntime(config, route.sourceFiles)
|
|
386
|
+
report(route.file, fileURLToPath(await handlerModuleHref(config, route)))
|
|
387
|
+
})
|
|
388
|
+
: []),
|
|
316
389
|
])
|
|
317
390
|
}
|
|
318
391
|
console.log(COMPILED_MARKER)
|
|
392
|
+
if (mode === 'compile') {
|
|
393
|
+
// Transform is the tail of the compile pipeline: an artifact is transformed when it is first
|
|
394
|
+
// imported, so a server that skipped every compile would still start esbuild for this alone.
|
|
395
|
+
// The import pass below covers it in `full` mode; here it runs standalone, without app code.
|
|
396
|
+
for (const route of [...pages, ...handlers]) registerServerRuntime(config, route.sourceFiles)
|
|
397
|
+
const modules = cacheRoot(config.outPath)
|
|
398
|
+
await prewarmServerTransforms(modules, await listFiles(modules))
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
// Everything below runs the app's own code to capture what only an import
|
|
402
|
+
// writes (vendor bundles, next/font bytes). A normal build serves from a
|
|
403
|
+
// writable disk and creates those on demand; only a read-only deployment
|
|
404
|
+
// target needs them now.
|
|
319
405
|
|
|
320
406
|
// Handlers come last, and only once nothing else is in flight. Importing them runs the app's own
|
|
321
407
|
// server code (auth, db pools, telemetry), and that code running *concurrently* with the compile
|
|
@@ -447,18 +533,24 @@ async function readWarmRequests(config: ResolvedConfig): Promise<Partial<WarmReq
|
|
|
447
533
|
|
|
448
534
|
if (import.meta.main) {
|
|
449
535
|
const root = process.argv[2]
|
|
450
|
-
const
|
|
536
|
+
const mode: WarmMode = process.argv.includes(FULL_MODE_FLAG) ? 'full' : 'compile'
|
|
537
|
+
// This helper's stderr is forwarded to the user-facing build. The parent
|
|
538
|
+
// already validated and printed next.config warnings, so avoid duplicates.
|
|
539
|
+
const config = await loadConfig(root, { warnings: false })
|
|
451
540
|
await bootstrapCompat(config)
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
setEmitCompiledSpecifiersManifest(
|
|
541
|
+
// Only the vercel adapter reads the specifier manifests back (its trace step);
|
|
542
|
+
// a plain build would write them for nothing.
|
|
543
|
+
setEmitCompiledSpecifiersManifest(mode === 'full')
|
|
455
544
|
// Blocks until the parent sends the go-ahead, i.e. until the manifest is
|
|
456
545
|
// written; a prewarm batch may arrive and run before that.
|
|
457
546
|
const request = await readWarmRequests(config)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
547
|
+
// A build that fails closes stdin without a go-ahead and never writes the manifest. There is
|
|
548
|
+
// nothing to warm and nothing to report: exiting quietly keeps the failed build's own error the
|
|
549
|
+
// only thing on stderr.
|
|
550
|
+
const manifestFile = path.join(config.outPath, 'manifest.json')
|
|
551
|
+
if (!request.skip || !existsSync(manifestFile)) process.exit(0)
|
|
552
|
+
const manifest = JSON.parse(await readFile(manifestFile, 'utf8')) as BuildManifest
|
|
553
|
+
await warmRouteModules(config, manifest, new Set(request.skip ?? []), mode)
|
|
462
554
|
// App code reached through a route handler can leave the loop alive (a db
|
|
463
555
|
// pool, a stray interval); the warm pass is done either way.
|
|
464
556
|
process.exit(0)
|
package/src/cli/build.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createRequire } from 'node:module'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
import { writeVercelOutput } from './adapters/vercel'
|
|
7
|
-
import { startWarmChild } from './adapters/vercel-warm'
|
|
7
|
+
import { startWarmChild, type WarmChild } from './adapters/vercel-warm'
|
|
8
8
|
import { devOutSegment, loadConfig, pathToFileHref } from '../config'
|
|
9
9
|
import { bootstrapCompat } from '../compat-bootstrap'
|
|
10
10
|
import {
|
|
@@ -67,7 +67,12 @@ import {
|
|
|
67
67
|
} from '../render/ppr'
|
|
68
68
|
import { handleRouteModule, staticRouteParams, type RouteHandlerModule } from '../routing/handler'
|
|
69
69
|
import { runWithCacheScope } from '../request/cache'
|
|
70
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
devServerModuleHref,
|
|
72
|
+
resetModuleGraphFailure,
|
|
73
|
+
setEmitCompiledSpecifiersManifest,
|
|
74
|
+
throwIfModuleGraphFailed,
|
|
75
|
+
} from '../runtime/modules'
|
|
71
76
|
import {
|
|
72
77
|
beginDynamicBailoutProbe,
|
|
73
78
|
endDynamicBailoutProbe,
|
|
@@ -151,34 +156,39 @@ interface BuildOptions {
|
|
|
151
156
|
|
|
152
157
|
export async function buildProject(root?: string, options: BuildOptions = {}) {
|
|
153
158
|
clearBuildParallelPhases()
|
|
159
|
+
resetModuleGraphFailure()
|
|
154
160
|
// One read per source for the whole build (route-fact walk, action discovery,
|
|
155
161
|
// client loader), released here so nothing survives the build.
|
|
156
162
|
beginSourceScope()
|
|
157
163
|
// Only the vercel adapter's trace step reads the per-artifact specifier
|
|
158
164
|
// sidecars; every other build path pays nothing for them.
|
|
159
165
|
const restoreSpecifiersManifest = setEmitCompiledSpecifiersManifest(options.adapter === 'vercel')
|
|
166
|
+
const lifecycle: { warm?: WarmChild } = {}
|
|
160
167
|
try {
|
|
161
|
-
return await runBuild(root, options)
|
|
168
|
+
return await runBuild(root, options, lifecycle)
|
|
162
169
|
} catch (error) {
|
|
163
170
|
// A parallel phase that already failed (a type error) is the root cause of
|
|
164
171
|
// whatever broke downstream; report it instead of the symptom.
|
|
165
172
|
const phaseError = buildParallelPhaseError()
|
|
166
173
|
throw phaseError ?? error
|
|
167
174
|
} finally {
|
|
175
|
+
lifecycle.warm?.kill()
|
|
168
176
|
endSourceScope()
|
|
169
177
|
restoreSpecifiersManifest()
|
|
170
178
|
flushDevModuleCaches()
|
|
171
179
|
}
|
|
172
180
|
}
|
|
173
181
|
|
|
174
|
-
async function runBuild(
|
|
182
|
+
async function runBuild(
|
|
183
|
+
root: string | undefined,
|
|
184
|
+
options: BuildOptions,
|
|
185
|
+
lifecycle: { warm?: WarmChild },
|
|
186
|
+
) {
|
|
175
187
|
const verbose = options.verbose ?? false
|
|
176
188
|
const log = createVerboseLogger(verbose, 'build')
|
|
177
189
|
const startedAt = performance.now()
|
|
178
190
|
|
|
179
|
-
console.log(
|
|
180
|
-
`${cyan('▲')} ${bold('pnext')} ${dim('— Creating an optimized production build ...\n')}`,
|
|
181
|
-
)
|
|
191
|
+
console.log(`⚡ ${bold('pnext')} ${dim('— Creating an optimized production build ...\n')}`)
|
|
182
192
|
const config = await loadConfig(root)
|
|
183
193
|
const buildCache = lookupBuildCache(config, options)
|
|
184
194
|
// Compat plugin loader: the single gated seam that populates the core
|
|
@@ -206,10 +216,13 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
206
216
|
// Load Tailwind on the CSS worker while the steps below run, so the first
|
|
207
217
|
// stylesheet doesn't pay its cold boot.
|
|
208
218
|
warmCssPipeline(config)
|
|
209
|
-
// The
|
|
210
|
-
// its
|
|
211
|
-
// adapter
|
|
212
|
-
|
|
219
|
+
// The warm pass runs in its own process; start it here so its boot overlaps the build and only the
|
|
220
|
+
// warming itself lands on its own step. Every build compiles what the production server would
|
|
221
|
+
// otherwise compile inside its first request; the vercel adapter additionally imports app modules
|
|
222
|
+
// for the bundles only an import writes (see adapters/vercel-warm).
|
|
223
|
+
const warmMode = options.adapter === 'vercel' ? 'full' : 'compile'
|
|
224
|
+
const warm = startWarmChild(config, warmMode)
|
|
225
|
+
lifecycle.warm = warm
|
|
213
226
|
await log.step('prepare output directory', async () => {
|
|
214
227
|
// Build-owned outputs only: `<outRoot>/dev` belongs to a possibly-running
|
|
215
228
|
// dev server and must survive.
|
|
@@ -252,10 +265,10 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
252
265
|
`--debug-build-paths ${options.debugBuildPaths} — ${routes.length} route${routes.length === 1 ? '' : 's'} kept`,
|
|
253
266
|
)
|
|
254
267
|
}
|
|
255
|
-
// The page routes' server modules are the bulk of the
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
warm
|
|
268
|
+
// The page routes' server modules are the bulk of the warm pass and depend on
|
|
269
|
+
// nothing but the scan; hand them over now so the child compiles them under
|
|
270
|
+
// the client stage instead of after it.
|
|
271
|
+
warm.prewarm(routes.filter(route => route.kind === 'page').map(route => route.file))
|
|
259
272
|
// Compat build steps (action discovery/bundling + server-reference manifest)
|
|
260
273
|
// populate their output onto this accumulator. Pure-core registers no steps.
|
|
261
274
|
const buildState: BuildStepState = { actions: [] }
|
|
@@ -842,6 +855,12 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
842
855
|
for (const line of generateRouteLines) console.log(line)
|
|
843
856
|
}
|
|
844
857
|
|
|
858
|
+
// A render may intentionally turn an exception into a 500 response so the
|
|
859
|
+
// dev server can stay alive. During a production build, however, an emitted
|
|
860
|
+
// module that cannot resolve one of its content-addressed dependencies makes
|
|
861
|
+
// the output unusable and must remain fatal even after that recovery path.
|
|
862
|
+
throwIfModuleGraphFailed()
|
|
863
|
+
|
|
845
864
|
// Fail the build after every route has been given the chance to log its
|
|
846
865
|
// prerender error (matching `prerenderEarlyExit: false`).
|
|
847
866
|
if (hadAfterPrerenderError) {
|
|
@@ -926,6 +945,13 @@ async function runBuild(root: string | undefined, options: BuildOptions) {
|
|
|
926
945
|
await log.step('vercel adapter output', () =>
|
|
927
946
|
writeVercelOutput(config, manifest, { verbose, warm }),
|
|
928
947
|
)
|
|
948
|
+
// Plain build: the adapter owns the child in the branch above. Compiling the
|
|
949
|
+
// rest of the app's modules is build work, so it counts toward the build's own
|
|
950
|
+
// duration rather than hiding after it.
|
|
951
|
+
else await log.step('warm module cache', () => warm.finish(log))
|
|
952
|
+
|
|
953
|
+
// Also cover module loads performed by completion hooks or the warm pass.
|
|
954
|
+
throwIfModuleGraphFailed()
|
|
929
955
|
|
|
930
956
|
// Bundling is done; the build metric stops here. Phases that ran alongside it
|
|
931
957
|
// (typecheck) are awaited next and reported on their own lines.
|
|
@@ -1068,6 +1094,7 @@ function printBuildSummary(
|
|
|
1068
1094
|
// Route table: ○ static, ● SSG (params), ◐ partial prerender, ƒ dynamic.
|
|
1069
1095
|
console.log('')
|
|
1070
1096
|
console.log('Route (app)')
|
|
1097
|
+
const usedMarkers = new Set<string>()
|
|
1071
1098
|
for (const route of routes) {
|
|
1072
1099
|
const intercepted = interceptionPrerenders.get(route.id)
|
|
1073
1100
|
const marker =
|
|
@@ -1089,15 +1116,29 @@ function printBuildSummary(
|
|
|
1089
1116
|
const label = route.interception ? interceptionDisplayRoute(route) : route.route || '/'
|
|
1090
1117
|
// Next names a dynamic route by its source pattern (`/[dyn]`), not pnext's
|
|
1091
1118
|
// `:param` form; compat apps' e2e output is matched against Next's.
|
|
1119
|
+
usedMarkers.add(marker)
|
|
1092
1120
|
console.log(` ${marker} ${nextCompatEnabled(config) ? toNextRoutePattern(label) : label}`)
|
|
1093
1121
|
}
|
|
1094
1122
|
for (const file of staticMetadataSummaryFiles(staticFiles)) {
|
|
1123
|
+
usedMarkers.add('○')
|
|
1095
1124
|
console.log(` ${dim('○')} /${file}`)
|
|
1096
1125
|
}
|
|
1097
1126
|
if (hasMiddleware) {
|
|
1127
|
+
usedMarkers.add('ƒ')
|
|
1098
1128
|
console.log('')
|
|
1099
1129
|
console.log(` ${dim('ƒ')} Middleware`)
|
|
1100
1130
|
}
|
|
1131
|
+
const legend: [string, string][] = [
|
|
1132
|
+
['○', 'static'],
|
|
1133
|
+
['●', 'SSG'],
|
|
1134
|
+
['◐', 'partial prerender'],
|
|
1135
|
+
['ƒ', 'dynamic'],
|
|
1136
|
+
]
|
|
1137
|
+
const legendLine = legend
|
|
1138
|
+
.filter(([marker]) => usedMarkers.has(marker))
|
|
1139
|
+
.map(([marker, meaning]) => `${marker} ${meaning}`)
|
|
1140
|
+
.join(' ')
|
|
1141
|
+
if (legendLine) console.log(`\n ${dim(legendLine)}`)
|
|
1101
1142
|
console.log('')
|
|
1102
1143
|
console.log(
|
|
1103
1144
|
`${green('✓')} ${bold('Build complete')} ${dim(`in ${formatBuildDuration(durationMs)}`)}`,
|
package/src/cli/create.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function createApp(dir: string | undefined, options: CreateAppOptio
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const name = sanitizePackageName(path.basename(target))
|
|
28
|
-
console.log(
|
|
28
|
+
console.log(`⚡ ${bold('pnext create')} ${dim(`— scaffolding ${name}`)}\n`)
|
|
29
29
|
|
|
30
30
|
const files = await scaffoldFiles(name, await latestPnextVersionRange('create'))
|
|
31
31
|
await Promise.all(
|
package/src/cli/dev.ts
CHANGED
|
@@ -44,7 +44,8 @@ export async function dev(options: DevOptions = {}) {
|
|
|
44
44
|
markBoot('dev:config')
|
|
45
45
|
warnDuplicateSass(config.root, Boolean(config.compat?.next))
|
|
46
46
|
const port = options.port ?? 3000
|
|
47
|
-
|
|
47
|
+
// Next's documented default. Bun resolves 'localhost' to ::1 only, refusing IPv4 clients.
|
|
48
|
+
const hostname = options.hostname ?? '0.0.0.0'
|
|
48
49
|
// A memory-triggered restart can briefly race the previous listener's
|
|
49
50
|
// shutdown, so give the port a moment to free before failing.
|
|
50
51
|
await ensurePortFree(port, hostname, { attempts: 10 })
|