@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/dev/server.ts CHANGED
@@ -3,12 +3,15 @@ import { readFile, readdir, rm, stat, watch } from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
  import {
5
5
  DEFERRED_DYNAMIC_SIDECAR,
6
+ buildClientEntries,
6
7
  buildClientEntry,
8
+ clientSourceHash,
7
9
  deferredDynamicRefById,
8
10
  prebuiltRuntimeDir,
9
11
  registerDeferredDynamicRef,
10
12
  } from '../client/build'
11
13
  import { drainPreplanBuilds } from '../runtime/vendor'
14
+ import { getClientActionBundler } from './client-actions'
12
15
  import { devClientCacheKey } from './restart/client-key'
13
16
  import { clientChunkStoreDir, contentAddressDir, sweepClientChunkStore } from './client-chunk-store'
14
17
  import { restartCacheEnabled } from './restart/enabled'
@@ -98,9 +101,12 @@ import type { RouteManifestEntry, RouteParamValue } from '../types'
98
101
  import {
99
102
  clearDevRouteBundleKeys,
100
103
  devClientModuleHref,
104
+ devClientReferenceGroups,
105
+ devClientReferenceModules,
101
106
  devModuleGraph,
102
107
  devRouteModuleLoaders,
103
108
  devServerModuleHref,
109
+ importDevModule,
104
110
  warmDevModulePipeline,
105
111
  } from '../runtime/modules'
106
112
  import { ssrClientReference } from '../client/reference'
@@ -280,6 +286,7 @@ export async function startDevServer(options: DevServerOptions) {
280
286
  installGlobalCssCache(config)
281
287
  markBoot('boot:registry')
282
288
  let routes = await scanRoutes(config.appPath)
289
+ setDevClientRoutes(config, routes)
283
290
  markBoot('boot:scanRoutes')
284
291
  await validateProxyFiles(config)
285
292
  let proxyRunner = createProxyRunner(config)
@@ -351,7 +358,10 @@ export async function startDevServer(options: DevServerOptions) {
351
358
  }
352
359
  // The route table is built from file paths only, so only an added, removed
353
360
  // or renamed file can change it — a plain save never does.
354
- if (structural) routes = await scanRoutes(config.appPath)
361
+ if (structural) {
362
+ routes = await scanRoutes(config.appPath)
363
+ setDevClientRoutes(config, routes)
364
+ }
355
365
  await validateProxyFiles(config)
356
366
  proxyRunner = createProxyRunner(config)
357
367
  devImportVersion = `${Date.now()}`
@@ -914,22 +924,43 @@ async function warmDevPageModule(config: ResolvedConfig, route: RouteManifestEnt
914
924
 
915
925
  /**
916
926
  * The client-SSR half of the route: `markClientReferences` imports every SSR-able reference through
917
- * the CLIENT layer, a different artifact from the browser bundle `buildDevClient` warms, and naming
918
- * those artifacts walks the reference's whole source graph. Resolving the href here is the same
919
- * memoized call the render makes, so a request that beats us joins the work instead of repeating it.
927
+ * the CLIENT layer, a different artifact from the browser bundle `buildDevClient` warms. Import the
928
+ * content-addressed artifacts here, alongside the route bundle, so a restart evaluates them once
929
+ * before the render reaches the same module-registry entries.
930
+ *
931
+ * The batchable references are one esbuild pass over the whole set rather than one walk per file
932
+ * (`devClientReferenceModules`); the render joins that same pass. What the pass cannot own - package
933
+ * references, pages-router sources - keeps the per-file naming below.
920
934
  */
921
935
  async function warmDevClientReferences(config: ResolvedConfig, route: RouteManifestEntry) {
922
936
  // eslint-disable-next-line turbo/no-undeclared-env-vars
923
937
  if (process.env.PNEXT_DEV_WARM_CLIENT_REFS === '0') return
938
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
939
+ const evaluate = process.env.PNEXT_DEV_PRELOAD_REF_IMPORTS !== '0'
924
940
  const target = serverBundleTargetForRuntime(route.segmentConfig?.runtime)
925
- const files = new Set(
926
- route.clientReferences.filter(ssrClientReference).map(reference => reference.file),
927
- )
928
- await Promise.allSettled(
929
- // The version argument is unused by devClientModuleHref (artifacts are named
930
- // by content), so there is nothing to keep in sync with the render's call.
931
- [...files].map(file => devClientModuleHref(config, file, undefined, target)),
932
- )
941
+ const { batched, perModule } = devClientReferenceGroups(config, route)
942
+ await Promise.allSettled([
943
+ batched.length > 0
944
+ ? devClientReferenceModules(config, route, { evaluate }).catch(error => {
945
+ console.debug(
946
+ `PNext dev warm failed for the client references of ${route.route}; falling back to per-module loading.`,
947
+ error,
948
+ )
949
+ throw error
950
+ })
951
+ : Promise.resolve(),
952
+ ...perModule.map(async file => {
953
+ const href = await devClientModuleHref(config, file, undefined, target)
954
+ if (evaluate) {
955
+ try {
956
+ await importDevModule(href, { warm: true })
957
+ } catch (error) {
958
+ console.debug(`PNext dev warm evaluation failed for client reference ${file}.`, error)
959
+ throw error
960
+ }
961
+ }
962
+ }),
963
+ ])
933
964
  }
934
965
 
935
966
  const HISTORY_LIMIT = 8
@@ -1203,8 +1234,30 @@ async function loadDeferredDynamicSidecar(outDir: string) {
1203
1234
  }
1204
1235
  }
1205
1236
 
1237
+ // The current scan's routes, per app: the client build must span EVERY client route in one
1238
+ // esbuild graph (like prod) so shared modules keep one identity — per-route graphs shipped
1239
+ // per-route copies of shared libraries, breaking context identity across navigations and
1240
+ // remounting the app shell (splash) on every soft nav.
1241
+ const devClientRouteSets = new Map<string, RouteManifestEntry[]>()
1242
+ function setDevClientRoutes(config: ResolvedConfig, routes: RouteManifestEntry[]) {
1243
+ devClientRouteSets.set(config.outPath, routes)
1244
+ }
1245
+ function devClientBatchRoutes(config: ResolvedConfig, route: RouteManifestEntry) {
1246
+ const all = devClientRouteSets.get(config.outPath)
1247
+ if (!all?.some(candidate => candidate.id === route.id)) return [route]
1248
+ return all.filter(
1249
+ candidate =>
1250
+ candidate.client || candidate.clientReferences.length > 0 || candidate.needsRouterEntry,
1251
+ )
1252
+ }
1253
+
1206
1254
  async function buildDevClient(config: ResolvedConfig, route: RouteManifestEntry) {
1207
- const cacheKey = await routeClientCacheKey(config, route)
1255
+ const batchRoutes = devClientBatchRoutes(config, route)
1256
+ const cacheKeys = await Promise.all(batchRoutes.map(entry => routeClientCacheKey(config, entry)))
1257
+ const cacheKey =
1258
+ batchRoutes.length === 1
1259
+ ? cacheKeys[0]!
1260
+ : `batch-${clientSourceHash([...cacheKeys].sort().join('\n'))}`
1208
1261
  const outDir = path.join(config.outPath, 'cache', 'client', cacheKey)
1209
1262
  const outFile = path.join(outDir, `${clientEntryName(route)}.js`)
1210
1263
 
@@ -1231,7 +1284,27 @@ async function buildDevClient(config: ResolvedConfig, route: RouteManifestEntry)
1231
1284
  // set) so concurrent callers can't both slip past the dedup check.
1232
1285
  const inner = (async () => {
1233
1286
  await ensureDir(outDir)
1234
- const file = await buildClientEntry({ config, route, outDir, dev: true })
1287
+ // The action stub plugin builds its filter from the DISCOVERED action modules, so discovery has
1288
+ // to have run before the build is constructed. A route-module compile arms it, but a batch spans
1289
+ // routes no request has reached: unarmed, their `'use server'` imports bundle the real module and
1290
+ // drag its server-only dependencies (node:crypto, http, tls) into the client graph — which fails
1291
+ // the whole batch, not just that route.
1292
+ if (nextCompatEnabled(config)) await getClientActionBundler()?.ensureArmed()
1293
+ const file =
1294
+ batchRoutes.length === 1
1295
+ ? await buildClientEntry({ config, route, outDir, dev: true })
1296
+ : await (async () => {
1297
+ // Action usage is per-request knowledge in dev; force the action runtime like the
1298
+ // single-route dev build always did.
1299
+ await buildClientEntries({
1300
+ config,
1301
+ routes: batchRoutes,
1302
+ outDir,
1303
+ dev: true,
1304
+ hasServerActions: true,
1305
+ })
1306
+ return outFile
1307
+ })()
1235
1308
  // Dedup this generation's chunk bytes against the shared store before serving
1236
1309
  //: identical chunks recur across generations, and this is
1237
1310
  // the one place all of a generation's output exists before requests read it.
@@ -63,3 +63,6 @@ export const RouteParamsContext = sharedContext<RouteParamsSnapshot | null>(
63
63
  'pnext.routeParamsContext',
64
64
  null,
65
65
  )
66
+
67
+ /** Marks a client island whose nearest Suspense boundary owns static-generation fallback UI. */
68
+ export const CsrBailoutContext = sharedContext('pnext.csrBailoutContext', false)
package/src/render/ppr.ts CHANGED
@@ -65,6 +65,10 @@ const PRERENDER_STORAGE = Symbol.for('pnext.prerenderStorage')
65
65
  const prerenderStorage = ((globalThis as Record<PropertyKey, unknown>)[PRERENDER_STORAGE] ??=
66
66
  new AsyncLocalStorage<PrerenderScope>()) as AsyncLocalStorage<PrerenderScope>
67
67
 
68
+ const CSR_BAILOUT_STORAGE = Symbol.for('pnext.csrBailoutStorage')
69
+ const csrBailoutStorage = ((globalThis as Record<PropertyKey, unknown>)[CSR_BAILOUT_STORAGE] ??=
70
+ new AsyncLocalStorage<boolean>()) as AsyncLocalStorage<boolean>
71
+
68
72
  interface PrerenderDynamicTracker {
69
73
  dynamic: boolean
70
74
  }
@@ -189,6 +193,16 @@ export function isPrerendering() {
189
193
  return prerenderStorage.getStore() !== undefined
190
194
  }
191
195
 
196
+ /** Run a legacy static render where client URL hooks defer to Suspense. */
197
+ export function runWithCsrBailout<T>(callback: () => T): T {
198
+ return csrBailoutStorage.run(true, callback)
199
+ }
200
+
201
+ /** True while a legacy static render must defer URL-dependent client islands. */
202
+ export function isCsrBailoutPrerender(): boolean {
203
+ return csrBailoutStorage.getStore() === true
204
+ }
205
+
192
206
  /** True inside a runtime-prefetch prerender (see PrerenderScope.runtimePrefetch). */
193
207
  export function isRuntimePrefetchPrerender(): boolean {
194
208
  return prerenderStorage.getStore()?.runtimePrefetch === true