@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
@@ -33,6 +33,8 @@ export interface ScanEdge {
33
33
  star?: boolean
34
34
  /** `export … from` rather than `import`. */
35
35
  reexport?: boolean
36
+ /** Public export name -> name imported from the target. */
37
+ exportMap?: Record<string, string>
36
38
  }
37
39
 
38
40
  /** One module specifier literal, with the span of its quoted text. */
@@ -192,6 +194,12 @@ export function rewriteFacts(file: string, source: string): RewriteFacts {
192
194
  return parsed(file, source).rewrite
193
195
  }
194
196
 
197
+ /** Every specifier literal with the syntax that determines when it is resolved. */
198
+ export function moduleSpecifierEdges(source: string, file = 'source'): SpecifierEdge[] {
199
+ const facts = rewriteFacts(file, source)
200
+ return facts.unreliable ? textualEdges(source) : facts.edges
201
+ }
202
+
195
203
  function parsed(file: string, source: string, lang = langForFile(file)) {
196
204
  const key = `${lang}\0${source.length}\0${bun.hash(source).toString()}`
197
205
  const cached = memo.get(key)
@@ -216,10 +224,9 @@ export function rewriteSpecifierLiterals(
216
224
  file: string,
217
225
  replace: (specifier: string, kind: SpecifierEdge['kind']) => string | undefined,
218
226
  ): string {
219
- const facts = rewriteFacts(file, source)
220
227
  // A partially recovered parse has a partial record; the textual scan the
221
228
  // folded passes replaced is the safe answer there.
222
- const edges = facts.unreliable ? textualEdges(source) : facts.edges
229
+ const edges = moduleSpecifierEdges(source, file)
223
230
  const edits: { start: number; end: number; value: string }[] = []
224
231
  for (const edge of edges) {
225
232
  const value = replace(edge.specifier, edge.kind)
@@ -341,13 +348,25 @@ function parseFacts(file: string, source: string, lang: ParserLang) {
341
348
  if (star) exportStars.push(specifier)
342
349
  let edge = edges.get(specifier)
343
350
  if (!edge) {
344
- edge = { specifier, exports: [], index: statement.start, reexport: true }
351
+ edge = {
352
+ specifier,
353
+ exports: [],
354
+ index: statement.start,
355
+ reexport: true,
356
+ exportMap: {},
357
+ }
345
358
  edges.set(specifier, edge)
346
359
  imports.push(edge)
347
360
  }
348
361
  if (star) edge.star = true
349
362
  else if (importKind === 'All') edge.exports.push('*')
350
- else if (entry.importName.name) edge.exports.push(entry.importName.name)
363
+ else {
364
+ const imported = importKind === 'Default' ? 'default' : entry.importName.name
365
+ if (!imported) continue
366
+ edge.exports.push(imported)
367
+ const exported = exportKind === 'Default' ? 'default' : entry.exportName.name
368
+ if (exported) edge.exportMap![exported] = imported
369
+ }
351
370
  }
352
371
  }
353
372
 
@@ -231,10 +231,9 @@ export function createProxyRunner(
231
231
 
232
232
  if (!response) return { request }
233
233
 
234
- // Same-origin middleware redirects follow the app's trailing-slash rule,
235
- // exactly like a direct navigation would (Next normalizes the Location).
236
- // skipTrailingSlashRedirect and cross-origin targets are left untouched.
237
- response = normalizeRedirectLocation(response, url, config)
234
+ // Redirects use the router-server envelope: relative same-origin target in
235
+ // both Location and body, with trailing-slash normalization when enabled.
236
+ response = await normalizeRedirectResponse(response, url, request.method, config)
238
237
 
239
238
  const meta = nextResponseMeta(response)
240
239
  if (!meta) return response
@@ -299,12 +298,13 @@ export function createProxyRunner(
299
298
  return runner
300
299
  }
301
300
 
302
- // Apply the app's trailing-slash rule to a same-origin redirect emitted by the
303
- // middleware (e.g. `NextResponse.redirect(new URL('/somewhere', req.url))` under
304
- // `trailingSlash: true` must land on `/somewhere/`). Cross-origin targets, the
305
- // skipTrailingSlashRedirect opt-out, and non-redirect responses pass through.
306
- function normalizeRedirectLocation(response: Response, requestUrl: URL, config: ResolvedConfig) {
307
- if (config.skipTrailingSlashRedirect || !config.trailingSlash) return response
301
+ // Normalize the redirect emitted by middleware into the router-server envelope.
302
+ async function normalizeRedirectResponse(
303
+ response: Response,
304
+ requestUrl: URL,
305
+ method: string,
306
+ config: ResolvedConfig,
307
+ ) {
308
308
  if (response.status < 300 || response.status >= 400) return response
309
309
  const location = response.headers.get('location')
310
310
  if (!location) return response
@@ -314,22 +314,26 @@ function normalizeRedirectLocation(response: Response, requestUrl: URL, config:
314
314
  } catch {
315
315
  return response
316
316
  }
317
- if (target.origin !== requestUrl.origin) return response
318
- const normalized = canonicalTrailingSlashPath(target.pathname, true)
319
- if (normalized === target.pathname) return response
320
- target.pathname = normalized
321
- try {
322
- response.headers.set('location', target.href)
323
- return response
324
- } catch {
325
- const headers = new Headers(response.headers)
326
- headers.set('location', target.href)
327
- return new Response(response.body, {
328
- status: response.status,
329
- statusText: response.statusText,
330
- headers,
331
- })
317
+ if (target.origin === requestUrl.origin) {
318
+ if (!config.skipTrailingSlashRedirect && config.trailingSlash) {
319
+ target.pathname = canonicalTrailingSlashPath(target.pathname, true)
320
+ }
332
321
  }
322
+ const destination =
323
+ target.origin === requestUrl.origin
324
+ ? `${target.pathname}${target.search}${target.hash}`
325
+ : target.href
326
+ const headers = new Headers(response.headers)
327
+ headers.set('location', destination)
328
+ const body =
329
+ method.toUpperCase() === 'HEAD' || response.body === null ? null : await response.arrayBuffer()
330
+ const synthesizeDestination = method.toUpperCase() !== 'HEAD' && (!body || body.byteLength === 0)
331
+ if (synthesizeDestination) headers.delete('content-length')
332
+ return new Response(synthesizeDestination ? destination : body, {
333
+ status: response.status,
334
+ statusText: response.statusText,
335
+ headers,
336
+ })
333
337
  }
334
338
 
335
339
  // Human label for the truncation warning ("10MB", "500KB") — whole units only,
@@ -2342,7 +2342,7 @@ function publishedClientExports(
2342
2342
  const found: ModuleEdge[] = []
2343
2343
  for (const edge of facts.imports) {
2344
2344
  if (!edge.reexport) continue
2345
- const map = esmReexportMap(source, edge.specifier)
2345
+ const map = { ...esmReexportMap(source, edge.specifier), ...edge.exportMap }
2346
2346
  const next = edge.star
2347
2347
  ? requested.filter(name => name !== 'default')
2348
2348
  : requested.map(name => map[name]).filter((name): name is string => Boolean(name))
@@ -131,6 +131,7 @@ const rootPathCache = new Map<string, string[]>()
131
131
  const runtimeConfigLookupCache = new Map<string, RuntimeConfig | null>()
132
132
  const firstAliasCache = new Map<string, string | null>()
133
133
  const transformCache = new Map<string, TransformCacheEntry>()
134
+ const pendingTransforms = new Map<string, Promise<string>>()
134
135
  const nodeModuleTransformCache = new Map<string, TransformCacheEntry>()
135
136
  // Freshness of generated `.pnext-require.cjs` sidecars (see inlineModuleScopeRequires).
136
137
  const requireSidecarCache = new Map<string, { mtimeMs: number; size: number }>()
@@ -427,6 +428,19 @@ async function transformSource(file: string) {
427
428
  if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
428
429
  return cached.code
429
430
  }
431
+ const pendingKey = `${file}\0${fileStat.mtimeMs}\0${fileStat.size}`
432
+ const pending = pendingTransforms.get(pendingKey)
433
+ if (pending) return pending
434
+ const started = transformSourceUncached(file, fileStat)
435
+ pendingTransforms.set(pendingKey, started)
436
+ try {
437
+ return await started
438
+ } finally {
439
+ if (pendingTransforms.get(pendingKey) === started) pendingTransforms.delete(pendingKey)
440
+ }
441
+ }
442
+
443
+ async function transformSourceUncached(file: string, fileStat: { mtimeMs: number; size: number }) {
430
444
  // The in-memory cache is empty on every restart, so a restarted server
431
445
  // re-transforms the whole materialized module set on the request path.
432
446
  const diskCache = transformCacheFile(file, fileStat)
@@ -469,11 +483,43 @@ async function transformSource(file: string) {
469
483
  })
470
484
  if (diskCache) {
471
485
  await mkdir(path.dirname(diskCache), { recursive: true }).catch(() => undefined)
472
- void writeFileAtomic(diskCache, result.code).catch(() => undefined)
486
+ // Off the caller's path: a render never waits on the cache it is filling.
487
+ // Tracked so a process whose whole job IS filling it can await them.
488
+ const write = writeFileAtomic(diskCache, result.code).catch(() => undefined)
489
+ transformWrites.add(write)
490
+ void write.finally(() => transformWrites.delete(write))
473
491
  }
474
492
  return result.code
475
493
  }
476
494
 
495
+ // Transform writes in flight; see prewarmServerTransforms.
496
+ const transformWrites = new Set<Promise<unknown>>()
497
+
498
+ /**
499
+ * Fill the on-disk transform cache for compiled artifacts a build just wrote.
500
+ *
501
+ * Importing a materialized module transforms it first, and that transform is the last piece of the
502
+ * compile pipeline a built app would otherwise reach on its first served request - esbuild service
503
+ * child included. Running it here is pure build work on build output: no app module is evaluated,
504
+ * unlike the import pass the vercel adapter needs for its vendor bundles.
505
+ */
506
+ export async function prewarmServerTransforms(root: string, files: Iterable<string>) {
507
+ // The load hook's own filter, so this warms exactly the files an import would
508
+ // route through it — never a `.css` shim or the natively-loaded vendor tree.
509
+ const loadable = rootFilter(root)
510
+ await Promise.all(
511
+ [...files].map(async file => {
512
+ if (!loadable.test(file)) return
513
+ const stats = await stat(file).catch(() => undefined)
514
+ if (!stats || !transformCacheFile(file, stats)) return
515
+ await transformSource(file).catch(() => undefined)
516
+ }),
517
+ )
518
+ // Settle the writes above, or this process exits with entries in flight and
519
+ // the next boot recompiles exactly what this pass was for.
520
+ while (transformWrites.size > 0) await Promise.all([...transformWrites])
521
+ }
522
+
477
523
  // eslint-disable-next-line turbo/no-undeclared-env-vars
478
524
  const transformDiskCacheDisabled = process.env.PNEXT_TRANSFORM_DISK_CACHE === '0'
479
525
  const materializedSegment = `${path.sep}cache${path.sep}server${path.sep}`