@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
package/src/cli/index.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync, statSync } from 'node:fs'
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { bold, cyan, dim } from '../utils/ansi'
|
|
4
5
|
import { markBoot } from './boot/trace'
|
|
5
6
|
import { commandBinaryName, nameEsbuildProcess, namedBunBinary } from './boot/named-bin'
|
|
6
7
|
|
|
7
|
-
const MIN_BUN = '1.3.
|
|
8
|
+
const MIN_BUN = '1.3.10'
|
|
8
9
|
// Not a floor but a hole in one: 1.3.14 segfaults about a second into serving,
|
|
9
10
|
// so a pnext server dies before its first response with no error of its own to
|
|
10
11
|
// show. 1.3.13 and 1.4.x are both fine.
|
|
11
12
|
const BROKEN_BUN = new Set(['1.3.14'])
|
|
13
|
+
const VERSION = packageVersion()
|
|
14
|
+
const COMMANDS = new Set([
|
|
15
|
+
'dev',
|
|
16
|
+
'build',
|
|
17
|
+
'start',
|
|
18
|
+
'analyze',
|
|
19
|
+
'typegen',
|
|
20
|
+
'create',
|
|
21
|
+
'migrate',
|
|
22
|
+
'info',
|
|
23
|
+
])
|
|
12
24
|
|
|
13
25
|
// Reached only if something bypassed bin/pnext (which guards the same way).
|
|
14
26
|
if (typeof Bun === 'undefined') {
|
|
@@ -43,7 +55,14 @@ nameEsbuildProcess()
|
|
|
43
55
|
markBoot('cli:naming')
|
|
44
56
|
|
|
45
57
|
try {
|
|
46
|
-
if (command === '
|
|
58
|
+
if (command === '--version' || command === '-v') {
|
|
59
|
+
console.log(VERSION)
|
|
60
|
+
} else if (command === '--help' || command === '-h' || !command) {
|
|
61
|
+
printHelp()
|
|
62
|
+
} else if (COMMANDS.has(command) && (args.includes('--help') || args.includes('-h'))) {
|
|
63
|
+
printCommandHelp(command)
|
|
64
|
+
} else if (command === 'dev') {
|
|
65
|
+
warnUnknownFlags(command, args)
|
|
47
66
|
const { dev } = await import('./dev')
|
|
48
67
|
await dev({
|
|
49
68
|
root: positionalRoot(args),
|
|
@@ -51,6 +70,7 @@ try {
|
|
|
51
70
|
hostname: optionString(args, '--hostname'),
|
|
52
71
|
})
|
|
53
72
|
} else if (command === 'build') {
|
|
73
|
+
warnUnknownFlags(command, args)
|
|
54
74
|
const { buildProject } = await import('./build')
|
|
55
75
|
await buildProject(positionalRoot(args), {
|
|
56
76
|
adapter: adapterOption(args),
|
|
@@ -68,6 +88,7 @@ try {
|
|
|
68
88
|
// terminates once buildProject resolves.
|
|
69
89
|
process.exit(0)
|
|
70
90
|
} else if (command === 'start') {
|
|
91
|
+
warnUnknownFlags(command, args)
|
|
71
92
|
const root = positionalRoot(args)
|
|
72
93
|
// The build emits src/cli/start.ts prebundled into one file; parsing that
|
|
73
94
|
// instead of walking the framework's source graph is most of `start`'s
|
|
@@ -83,6 +104,7 @@ try {
|
|
|
83
104
|
hostname: optionString(args, '--hostname'),
|
|
84
105
|
})
|
|
85
106
|
} else if (command === 'analyze') {
|
|
107
|
+
warnUnknownFlags(command, args)
|
|
86
108
|
const { analyzeProject } = await import('./analyze')
|
|
87
109
|
const { route, root } = analyzePositionals(args)
|
|
88
110
|
const result = await analyzeProject(root, {
|
|
@@ -96,6 +118,7 @@ try {
|
|
|
96
118
|
printAnalyzeResult(result, { route, files: args.includes('--files') })
|
|
97
119
|
}
|
|
98
120
|
} else if (command === 'typegen') {
|
|
121
|
+
warnUnknownFlags(command, args)
|
|
99
122
|
const { typegenProject } = await import('./typegen')
|
|
100
123
|
const result = await typegenProject(positionalRoot(args))
|
|
101
124
|
console.log(
|
|
@@ -105,13 +128,18 @@ try {
|
|
|
105
128
|
// handles (timers/connections) alive — a one-shot command must not hang.
|
|
106
129
|
process.exit(0)
|
|
107
130
|
} else if (command === 'create') {
|
|
131
|
+
warnUnknownFlags(command, args)
|
|
108
132
|
const { createApp } = await import('./create')
|
|
109
133
|
await createApp(positionals(args)[0], { install: !args.includes('--no-install') })
|
|
110
134
|
process.exit(0)
|
|
111
135
|
} else if (command === 'migrate') {
|
|
136
|
+
warnUnknownFlags(command, args)
|
|
112
137
|
const { migrateApp } = await import('./migrate/run')
|
|
113
138
|
const code = await migrateApp(positionalRoot(args), { dryRun: args.includes('--dry-run') })
|
|
114
139
|
process.exit(code)
|
|
140
|
+
} else if (command === 'info') {
|
|
141
|
+
warnUnknownFlags(command, args)
|
|
142
|
+
printInfo()
|
|
115
143
|
} else {
|
|
116
144
|
printHelp()
|
|
117
145
|
process.exit(command ? 1 : 0)
|
|
@@ -238,12 +266,120 @@ function analyzeCompression(args: string[]) {
|
|
|
238
266
|
return args.includes('--brotli') ? 'brotli' : 'gzip'
|
|
239
267
|
}
|
|
240
268
|
|
|
269
|
+
function packageVersion() {
|
|
270
|
+
const packageJson = JSON.parse(
|
|
271
|
+
readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
|
|
272
|
+
) as {
|
|
273
|
+
version?: unknown
|
|
274
|
+
}
|
|
275
|
+
return typeof packageJson.version === 'string' ? packageJson.version : 'unknown'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function warnUnknownFlags(command: string, args: string[]) {
|
|
279
|
+
const flags = commandFlags(command)
|
|
280
|
+
for (const arg of args) {
|
|
281
|
+
if (!arg.startsWith('-') || arg === '--') continue
|
|
282
|
+
const name = arg.startsWith('--') ? arg.split('=', 1)[0]! : arg
|
|
283
|
+
if (!flags.has(name)) console.warn(`Warning: unknown option '${name}' was ignored.`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function commandFlags(command: string) {
|
|
288
|
+
const flags = new Set(['--help', '-h'])
|
|
289
|
+
if (command === 'dev' || command === 'start') {
|
|
290
|
+
flags.add('--port')
|
|
291
|
+
flags.add('--hostname')
|
|
292
|
+
}
|
|
293
|
+
if (command === 'build') {
|
|
294
|
+
for (const flag of [
|
|
295
|
+
'--adapter',
|
|
296
|
+
'--verbose',
|
|
297
|
+
'--experimental-build-mode',
|
|
298
|
+
'--debug-build-paths',
|
|
299
|
+
'--debug-prerender',
|
|
300
|
+
])
|
|
301
|
+
flags.add(flag)
|
|
302
|
+
}
|
|
303
|
+
if (command === 'analyze') for (const flag of ['--json', '--files', '--brotli']) flags.add(flag)
|
|
304
|
+
if (command === 'create') flags.add('--no-install')
|
|
305
|
+
if (command === 'migrate') flags.add('--dry-run')
|
|
306
|
+
return flags
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function printInfo() {
|
|
310
|
+
const config = configPresence(process.cwd())
|
|
311
|
+
console.log(`pnext info
|
|
312
|
+
pnext: ${VERSION}
|
|
313
|
+
bun: ${Bun.version}
|
|
314
|
+
os: ${process.platform} ${process.arch}
|
|
315
|
+
package manager: ${packageManager(process.cwd())}
|
|
316
|
+
pnext.config: ${config.pnext}
|
|
317
|
+
next.config: ${config.next}`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function packageManager(root: string) {
|
|
321
|
+
if (existsSync(path.join(root, 'bun.lock')) || existsSync(path.join(root, 'bun.lockb')))
|
|
322
|
+
return 'bun'
|
|
323
|
+
if (existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
324
|
+
if (existsSync(path.join(root, 'yarn.lock'))) return 'yarn'
|
|
325
|
+
if (existsSync(path.join(root, 'package-lock.json'))) return 'npm'
|
|
326
|
+
return 'unknown'
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function configPresence(root: string) {
|
|
330
|
+
const find = (base: string) => {
|
|
331
|
+
const file = ['js', 'mjs', 'cjs', 'ts']
|
|
332
|
+
.map(extension => `${base}.${extension}`)
|
|
333
|
+
.find(file => existsSync(path.join(root, file)))
|
|
334
|
+
return file ?? 'none'
|
|
335
|
+
}
|
|
336
|
+
return { pnext: find('pnext.config'), next: find('next.config') }
|
|
337
|
+
}
|
|
338
|
+
|
|
241
339
|
function printHelp() {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
340
|
+
const commands: [string, string][] = [
|
|
341
|
+
['dev', 'Start the development server.'],
|
|
342
|
+
['build', 'Create a production build.'],
|
|
343
|
+
['start', 'Start the production server.'],
|
|
344
|
+
['analyze', 'Analyze production bundle output.'],
|
|
345
|
+
['typegen', 'Generate route TypeScript definitions.'],
|
|
346
|
+
['create', 'Create a new pnext application.'],
|
|
347
|
+
['migrate', 'Migrate a Next.js application to pnext.'],
|
|
348
|
+
['info', 'Print environment details for bug reports.'],
|
|
349
|
+
]
|
|
350
|
+
const flags: [string, string][] = [
|
|
351
|
+
['--port <port>', 'Port to listen on (default: 3000).'],
|
|
352
|
+
['--hostname <hostname>', 'Hostname to listen on (default: 0.0.0.0).'],
|
|
353
|
+
['--version, -v', 'Print the pnext version.'],
|
|
354
|
+
['--help, -h', 'Show this help message.'],
|
|
355
|
+
]
|
|
356
|
+
const row = ([name, text]: [string, string], width: number) =>
|
|
357
|
+
` ${cyan(name.padEnd(width))} ${dim(text)}`
|
|
358
|
+
console.log(`⚡ ${bold('pnext')} ${dim(VERSION)}
|
|
359
|
+
|
|
360
|
+
${bold('Usage:')} pnext ${cyan('<command>')} [options]
|
|
361
|
+
|
|
362
|
+
${bold('Commands:')}
|
|
363
|
+
${commands.map(entry => row(entry, 9)).join('\n')}
|
|
364
|
+
|
|
365
|
+
${bold('Common flags:')}
|
|
366
|
+
${flags.map(entry => row(entry, 21)).join('\n')}
|
|
367
|
+
|
|
368
|
+
${bold('Docs:')} ${cyan('https://pnext.dev')}`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function printCommandHelp(command: string) {
|
|
372
|
+
const help: Record<string, string> = {
|
|
373
|
+
dev: `Usage: pnext dev [directory] [options]\n\nFlags:\n --port <port> Port to listen on (default: 3000).\n --hostname <hostname> Hostname to listen on (default: 0.0.0.0).`,
|
|
374
|
+
build: `Usage: pnext build [directory] [options]\n\nFlags:\n --adapter <adapter> Deployment adapter (vercel).\n --verbose Print detailed build output.\n --experimental-build-mode <mode> Build mode: compile or generate.\n --debug-build-paths <patterns> Build matching routes only.\n --debug-prerender Debug prerender failures.`,
|
|
375
|
+
start: `Usage: pnext start [directory] [options]\n\nFlags:\n --port <port> Port to listen on (default: 3000).\n --hostname <hostname> Hostname to listen on (default: 0.0.0.0).`,
|
|
376
|
+
analyze: `Usage: pnext analyze [route] [directory] [options]\n\nFlags:\n --json Print JSON output.\n --files Include individual files.\n --brotli Report Brotli compression.`,
|
|
377
|
+
typegen: 'Usage: pnext typegen [directory]',
|
|
378
|
+
create:
|
|
379
|
+
'Usage: pnext create <directory> [options]\n\nFlags:\n --no-install Skip dependency installation.',
|
|
380
|
+
migrate:
|
|
381
|
+
'Usage: pnext migrate [directory] [options]\n\nFlags:\n --dry-run Preview changes without writing.',
|
|
382
|
+
info: 'Usage: pnext info',
|
|
383
|
+
}
|
|
384
|
+
console.log(help[command])
|
|
249
385
|
}
|
|
@@ -37,7 +37,7 @@ export function emptyResult(): MigrationResult {
|
|
|
37
37
|
|
|
38
38
|
export function printHeader(root: string, dryRun: boolean) {
|
|
39
39
|
const suffix = dryRun ? 'dry run, nothing will be written' : root
|
|
40
|
-
console.log(`\n
|
|
40
|
+
console.log(`\n⚡ ${bold('pnext migrate')} ${dim(`— ${suffix}`)}`)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function printResult(result: MigrationResult, options: { install: string }) {
|
|
@@ -254,6 +254,24 @@ export async function createRequestHandler(
|
|
|
254
254
|
})
|
|
255
255
|
|
|
256
256
|
async function renderNotFound(request: Request): Promise<Response> {
|
|
257
|
+
const built = await maybeBuiltFile(
|
|
258
|
+
config.outPath,
|
|
259
|
+
'/404',
|
|
260
|
+
manifest.staticFiles,
|
|
261
|
+
request.method,
|
|
262
|
+
request.headers,
|
|
263
|
+
nextCompat,
|
|
264
|
+
)
|
|
265
|
+
if (built) {
|
|
266
|
+
setWorkUnitRoute('html', 'static')
|
|
267
|
+
const headers = new Headers(built.headers)
|
|
268
|
+
headers.set('x-nextjs-cache', 'HIT')
|
|
269
|
+
if (!hasBuildCapturedCacheControl(manifest.staticFiles?.['404.html'])) {
|
|
270
|
+
headers.delete('cache-control')
|
|
271
|
+
}
|
|
272
|
+
headers.delete('content-length')
|
|
273
|
+
return compressResponse(new Response(built.body, { status: 404, headers }), request)
|
|
274
|
+
}
|
|
257
275
|
return compressResponse(
|
|
258
276
|
await renderGlobalNotFoundResponse({
|
|
259
277
|
config,
|
|
@@ -514,12 +532,7 @@ export async function createRequestHandler(
|
|
|
514
532
|
// which owns the one rule (Next's static / ISR / dynamic values) for
|
|
515
533
|
// rendered and prebuilt pages alike. maybeBuiltFile's `no-cache` is its
|
|
516
534
|
// asset default, not a claim; a build-captured header is a claim and stays.
|
|
517
|
-
if (
|
|
518
|
-
isHtmlResponse(staticFile) &&
|
|
519
|
-
!(servedBuilt.metadata?.headers ?? []).some(
|
|
520
|
-
([key]) => key.toLowerCase() === 'cache-control',
|
|
521
|
-
)
|
|
522
|
-
) {
|
|
535
|
+
if (isHtmlResponse(staticFile) && !hasBuildCapturedCacheControl(servedBuilt.metadata)) {
|
|
523
536
|
staticFile.headers.delete('cache-control')
|
|
524
537
|
}
|
|
525
538
|
if (servedBuilt.metadata) {
|
|
@@ -1035,6 +1048,10 @@ interface BuiltFileInfo {
|
|
|
1035
1048
|
metadata?: StaticFileMetadata
|
|
1036
1049
|
}
|
|
1037
1050
|
|
|
1051
|
+
function hasBuildCapturedCacheControl(metadata: StaticFileMetadata | undefined): boolean {
|
|
1052
|
+
return (metadata?.headers ?? []).some(([key]) => key.toLowerCase() === 'cache-control')
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1038
1055
|
// The prebuilt output file a GET for `pathname` would serve (page html or a
|
|
1039
1056
|
// static route-handler body), plus its ISR metadata from the build manifest.
|
|
1040
1057
|
async function builtFileInfo(
|
package/src/cli/serve/ui.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function printServerReady({ mode, hostname, port, elapsedMs }: ReadyBanne
|
|
|
40
40
|
|
|
41
41
|
const pad = Math.max(...rows.map(([label]) => label.length))
|
|
42
42
|
console.log('')
|
|
43
|
-
console.log(
|
|
43
|
+
console.log(`⚡ ${bold('pnext')} ${dim(mode)}`)
|
|
44
44
|
for (const [label, value] of rows) {
|
|
45
45
|
console.log(` ${dim('-')} ${dim(label.padEnd(pad))} ${value}`)
|
|
46
46
|
}
|
package/src/cli/start.ts
CHANGED
|
@@ -47,7 +47,8 @@ export async function maybeBuiltFile(
|
|
|
47
47
|
export async function start(options: StartOptions = {}) {
|
|
48
48
|
const startedAt = Date.now()
|
|
49
49
|
const port = options.port ?? 3000
|
|
50
|
-
|
|
50
|
+
// Next's documented default. Bun resolves 'localhost' to ::1 only, refusing IPv4 clients.
|
|
51
|
+
const hostname = options.hostname ?? '0.0.0.0'
|
|
51
52
|
// Export builds are not served; pnext serves its standalone output directly.
|
|
52
53
|
const config = await guardUnservableOutputMode(options.root)
|
|
53
54
|
const manifest = await readProductionManifest(config)
|
package/src/cli/typegen.ts
CHANGED
|
@@ -64,14 +64,17 @@ async function writeChecks(config: ResolvedConfig, routes: RouteManifestEntry[])
|
|
|
64
64
|
await rm(checksDir, { recursive: true, force: true })
|
|
65
65
|
await ensureDir(checksDir)
|
|
66
66
|
|
|
67
|
+
// Metadata code routes (sitemap.ts, opengraph-image.tsx, ...) keep their
|
|
68
|
+
// Next-style signatures, which are not PNextRouteHandlerModule shapes.
|
|
69
|
+
const checked = routes.filter(route => !route.metadataRoute)
|
|
67
70
|
await Promise.all(
|
|
68
|
-
|
|
71
|
+
checked.map(route => {
|
|
69
72
|
const file = path.join(checksDir, `${route.id}.check.ts`)
|
|
70
73
|
return writeIfChanged(file, checkSource(checksDir, route))
|
|
71
74
|
}),
|
|
72
75
|
)
|
|
73
76
|
|
|
74
|
-
return
|
|
77
|
+
return checked.length
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
async function writeRouteAliases(config: ResolvedConfig, routes: RouteManifestEntry[]) {
|