@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/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)
|
|
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
|
|
918
|
-
*
|
|
919
|
-
*
|
|
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
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
|
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
|
-
|
|
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
|