@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.
Files changed (53) hide show
  1. package/bin/pnext +1 -1
  2. package/package.json +2 -2
  3. package/reference/data/bench.json +398 -223
  4. package/reference/getting-started.md +1 -1
  5. package/src/api/client-navigation.ts +7 -5
  6. package/src/cli/adapters/vercel-warm.ts +120 -28
  7. package/src/cli/build.ts +56 -15
  8. package/src/cli/create.ts +1 -1
  9. package/src/cli/dev.ts +2 -1
  10. package/src/cli/index.ts +146 -10
  11. package/src/cli/migrate/report.ts +1 -1
  12. package/src/cli/serve/pipeline.ts +23 -6
  13. package/src/cli/serve/ui.ts +1 -1
  14. package/src/cli/start.ts +2 -1
  15. package/src/cli/typegen.ts +5 -2
  16. package/src/client/build.ts +217 -58
  17. package/src/client/chunk-fold.ts +7 -1
  18. package/src/client/entry.ts +44 -7
  19. package/src/client/router/runtime.ts +1016 -230
  20. package/src/client/router/types.ts +15 -0
  21. package/src/compat/actions/client-plugin.ts +10 -0
  22. package/src/compat/actions/rewrite.ts +1 -0
  23. package/src/compat/bundler/optimize-package-imports.ts +375 -28
  24. package/src/compat/client/navigation-scroll.ts +29 -3
  25. package/src/compat/client/optimistic-routing.ts +2 -2
  26. package/src/compat/client/segment-cache-policy.ts +4 -18
  27. package/src/compat/client/segment-cache.ts +104 -0
  28. package/src/compat/client/segment-prefetch.ts +6 -6
  29. package/src/compat/next/config-loader.ts +6 -4
  30. package/src/compat/next/navigation.ts +16 -2
  31. package/src/compat/pages/router.ts +102 -9
  32. package/src/compat/protocol.ts +10 -1
  33. package/src/compat/register/actions.ts +2 -0
  34. package/src/compat/register/bundler.ts +4 -6
  35. package/src/compat/register/routing.ts +3 -4
  36. package/src/compat/tsconfig-defaults.ts +3 -2
  37. package/src/config.ts +3 -3
  38. package/src/css/postcss.ts +450 -17
  39. package/src/dev/client-actions.ts +2 -0
  40. package/src/dev/server.ts +87 -14
  41. package/src/render/island-context.ts +3 -0
  42. package/src/render/ppr.ts +14 -0
  43. package/src/render/renderer.ts +268 -42
  44. package/src/request/context.ts +32 -11
  45. package/src/resolve/scan-facts.ts +23 -4
  46. package/src/routing/proxy.ts +29 -25
  47. package/src/routing/routes.ts +1 -1
  48. package/src/runtime/loader.ts +47 -1
  49. package/src/runtime/modules.ts +546 -65
  50. package/src/runtime/vendor-build.ts +109 -22
  51. package/src/runtime/vendor.ts +7 -3
  52. package/src/utils/ansi.ts +3 -1
  53. 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.6'
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 === 'dev') {
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
- console.log(`pnext dev [directory] [--port 3000] [--hostname 127.0.0.1]
243
- pnext build [directory] [--adapter vercel] [--verbose]
244
- pnext start [directory] [--port 3000] [--hostname 127.0.0.1]
245
- pnext analyze [route] [directory] [--brotli] [--files] [--json]
246
- pnext typegen [directory]
247
- pnext create <directory> [--no-install]
248
- pnext migrate [directory] [--dry-run]`)
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${cyan('▲')} ${bold('pnext migrate')} ${dim(`— ${suffix}`)}`)
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(
@@ -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(`${cyan('▲')} ${bold('pnext')} ${dim(mode)}`)
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
- const hostname = options.hostname ?? 'localhost'
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)
@@ -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
- routes.map(route => {
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 routes.length
77
+ return checked.length
75
78
  }
76
79
 
77
80
  async function writeRouteAliases(config: ResolvedConfig, routes: RouteManifestEntry[]) {