@wular/pnext 0.0.6 → 0.0.7

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/src/config.ts CHANGED
@@ -3,7 +3,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
3
3
  import path from 'node:path'
4
4
  import { loadEnv } from './env'
5
5
  import { findWorkspaceRoot } from './resolve/imports'
6
- import type { PNextConfig } from './types'
6
+ import { PREFETCH_MODES, type PNextConfig } from './types'
7
7
  import { setCacheComponents } from './render/ppr'
8
8
 
9
9
  export type ResolvedConfig = Required<Pick<PNextConfig, 'outDir' | 'basePath'>> &
@@ -86,6 +86,7 @@ export async function loadConfig(
86
86
  ? { compat: { ...(loadedConfig.default?.compat ?? {}), next: true } }
87
87
  : {}),
88
88
  }
89
+ validateConfig(config)
89
90
  // cacheComponents is a per-app flag on a process-global cell: clear it before
90
91
  // the next.config source re-derives it, so a pure-core (or flag-off) app
91
92
  // loaded after a cacheComponents app never inherits the previous build's flag.
@@ -128,6 +129,22 @@ export async function loadConfig(
128
129
  }
129
130
  }
130
131
 
132
+ // One row per enum-valued config field; validateConfig checks them all.
133
+ const enumFields: readonly [keyof PNextConfig, readonly unknown[]][] = [
134
+ ['prefetch', PREFETCH_MODES],
135
+ ]
136
+
137
+ function validateConfig(config: PNextConfig) {
138
+ for (const [field, allowed] of enumFields) {
139
+ const value = config[field]
140
+ if (value === undefined || allowed.includes(value)) continue
141
+ const list = allowed.map(v => (typeof v === 'string' ? `'${v}'` : String(v))).join(', ')
142
+ throw new Error(
143
+ `Invalid pnext config: '${field}' must be one of ${list} (received ${JSON.stringify(value)}).`,
144
+ )
145
+ }
146
+ }
147
+
131
148
  /** Dev's private subtree under the out root. Build never touches it. */
132
149
  export const devOutSegment = 'dev'
133
150
 
package/src/dev/server.ts CHANGED
@@ -964,10 +964,10 @@ async function handleRoute(
964
964
  }
965
965
 
966
966
  function routeClientCacheKey(config: ResolvedConfig, route: RouteManifestEntry) {
967
- const existing = routeCacheKeys.get(route.id)
967
+ const existing = routeCacheKeys.get(appKey(config.outPath, route.id))
968
968
  if (existing) return existing
969
969
  const key = devClientCacheKey(config, route, Boolean(config.compat?.next))
970
- routeCacheKeys.set(route.id, key)
970
+ routeCacheKeys.set(appKey(config.outPath, route.id), key)
971
971
  return key
972
972
  }
973
973
 
@@ -3655,6 +3655,7 @@ async function renderTree(
3655
3655
  renderCollectedHeadScripts(),
3656
3656
  bootstrapScripts,
3657
3657
  compatBuildManifestScript,
3658
+ prefetchModeScript(options.config),
3658
3659
  ]
3659
3660
  .filter(Boolean)
3660
3661
  .join('\n'),
@@ -3791,6 +3792,16 @@ function shouldRenderRuntimeMetadataInBody(options: RenderOptions, runtimeMetada
3791
3792
  )
3792
3793
  }
3793
3794
 
3795
+ /**
3796
+ * Inline script exposing the configured default prefetch mode to the client router, which applies it
3797
+ * to links carrying no `data-prefetch` of their own. Emitted only when `prefetch` is configured - an
3798
+ * app that leaves it unset keeps the global undefined and the built-in 'visible' default.
3799
+ */
3800
+ function prefetchModeScript(config: ResolvedConfig): string {
3801
+ if (config.prefetch === undefined) return ''
3802
+ return `<script>window.__PNEXT_PREFETCH__=${JSON.stringify(config.prefetch)};</script>`
3803
+ }
3804
+
3794
3805
  /**
3795
3806
  * Embeds the render's navigation state (children source path + each slot's
3796
3807
  * resolved source path). The client echoes it on soft-navigation fetches so
@@ -1,4 +1,4 @@
1
- import type { RouteParamValue } from '../types'
1
+ import type { PrefetchMode, RouteParamValue } from '../types'
2
2
 
3
3
  export type SearchValue = string | number | boolean | null | undefined
4
4
  export type SearchInput = URLSearchParams | Record<string, SearchValue | SearchValue[]>
@@ -19,8 +19,10 @@ export function routeHref(route: string, options: HrefParts = {}) {
19
19
  declare global {
20
20
  interface Window {
21
21
  __PNEXT_TRAILING_SLASH__?: boolean
22
+ __PNEXT_PREFETCH__?: PrefetchMode
22
23
  }
23
24
  var __PNEXT_TRAILING_SLASH__: boolean | undefined
25
+ var __PNEXT_PREFETCH__: PrefetchMode | undefined
24
26
  }
25
27
 
26
28
  // With `trailingSlash: true` the server 308s bare page URLs to their slashed
@@ -44,6 +46,22 @@ export function getTrailingSlashUrls() {
44
46
  return isTrailingSlashEnabled()
45
47
  }
46
48
 
49
+ // The app-wide default prefetch mode (config `prefetch`), for links that set
50
+ // none of their own. Isomorphic like the trailing-slash seam: the server sets it
51
+ // from the resolved config, the client reads the injected window global
52
+ // (renderer's prefetchModeScript).
53
+ export function setDefaultPrefetchMode(mode: PrefetchMode | undefined) {
54
+ globalThis.__PNEXT_PREFETCH__ = mode
55
+ }
56
+
57
+ export function getDefaultPrefetchMode(): PrefetchMode {
58
+ const mode =
59
+ process.browser || typeof window !== 'undefined'
60
+ ? window.__PNEXT_PREFETCH__
61
+ : globalThis.__PNEXT_PREFETCH__
62
+ return mode === undefined ? 'visible' : mode
63
+ }
64
+
47
65
  // The configured basePath ('' when unset). Set once per process by the server
48
66
  // runtime from the resolved config (registerServerRuntime), mirroring the
49
67
  // trailing-slash seam. Core render code (metadata) reads it through
@@ -41,7 +41,7 @@ import {
41
41
  } from '../resolve/dynamic'
42
42
  import { cacheRoot } from './module-cache'
43
43
  import { readNodeModuleBundle, writeNodeModuleBundle } from '../dev/restart/node-modules'
44
- import { setBasePathPrefix, setTrailingSlashUrls } from '../routing/href'
44
+ import { setBasePathPrefix, setDefaultPrefetchMode, setTrailingSlashUrls } from '../routing/href'
45
45
  import { resolveExternalLoadTarget, resolveImport, workspacePackageRoots } from '../resolve/imports'
46
46
  import { escapeRegex } from '../utils/code'
47
47
  import { writeFileAtomic } from '../utils/fs'
@@ -169,6 +169,8 @@ export function registerServerRuntime(config: ResolvedConfig, sourceFiles: strin
169
169
  // File-convention metadata asset hrefs (og-image, manifest) carry the
170
170
  // basePath prefix; core render reads it through the href seam.
171
171
  setBasePathPrefix(typeof config.basePath === 'string' ? config.basePath : '')
172
+ // Server-rendered Links bake the configured default into `data-prefetch`.
173
+ setDefaultPrefetchMode(config.prefetch)
172
174
  if (typeof Bun === 'undefined') return
173
175
  const sourceRoots = [...new Set(sourceFiles.map(file => sourceRootForFile(config, file)))].sort()
174
176
  const signature = sourceRoots.join('\0')
@@ -422,13 +422,14 @@ async function runVendorDemand(plan: VendorBuildPlan, nested: boolean, ticket: V
422
422
  *
423
423
  * Every demand pays it before it can even learn it has no siblings, and it is a timer on the one JS thread
424
424
  * the concurrent bundlers already saturate with plugin IPC, so the real wait is bounded below by this rather
425
- * than by it. `PNEXT_VENDOR_GROUP_WINDOW_MS` is the bisect seam.
425
+ * than by it. `PNEXT_VENDOR_GROUP_WINDOW_MS` is the bisect seam, read per round so
426
+ * tests can widen it after module load.
426
427
  */
427
- const VENDOR_GROUP_WINDOW_MS = (() => {
428
+ function vendorGroupWindowMs() {
428
429
  // eslint-disable-next-line turbo/no-undeclared-env-vars
429
430
  const override = Number(process.env.PNEXT_VENDOR_GROUP_WINDOW_MS)
430
431
  return Number.isFinite(override) && override >= 0 ? override : 5
431
- })()
432
+ }
432
433
 
433
434
  /**
434
435
  * A group re-parses the package graph once per round, so an unbounded trickle
@@ -515,7 +516,7 @@ function runVendorGroupRound(
515
516
  }
516
517
 
517
518
  async function vendorGroupRound(group: VendorGroupPlan, state: VendorGroupState, nested: boolean) {
518
- await new Promise(resolve => setTimeout(resolve, VENDOR_GROUP_WINDOW_MS))
519
+ await new Promise(resolve => setTimeout(resolve, vendorGroupWindowMs()))
519
520
  const members = [...state.pending.values()]
520
521
  if (members.length === 0) return
521
522
  // A lone subpath with no sibling to share a graph with is the single-bundle
package/src/types.ts CHANGED
@@ -2,7 +2,8 @@ import type { ComponentChildren } from 'preact'
2
2
 
3
3
  export type RouteMode = 'static' | 'dynamic'
4
4
 
5
- export type PrefetchMode = false | 'intent' | 'visible' | 'load'
5
+ export const PREFETCH_MODES = ['visible', 'intent', 'load', false] as const
6
+ export type PrefetchMode = (typeof PREFETCH_MODES)[number]
6
7
 
7
8
  export interface PNextRoutes {
8
9
  readonly __pnext_internal_route_brand?: never
@@ -30,6 +31,9 @@ export interface PNextConfig {
30
31
  // Link hrefs / client navigation (the raw path is preserved end to end).
31
32
  skipTrailingSlashRedirect?: boolean
32
33
  htmlLimitedBots?: RegExp
34
+ // App-wide default prefetch mode for client-router links that set none of
35
+ // their own. `false` means links never prefetch unless they opt in.
36
+ prefetch?: PrefetchMode
33
37
  // Emit browser sourcemaps from the production client build. Off by default,
34
38
  // matching Next: shipping maps publishes your source to every visitor, and
35
39
  // generating them costs real build time. Dev never emits them (the
@@ -1,35 +0,0 @@
1
- # pnext
2
-
3
- pnext is a Preact framework for file-routed applications. It keeps familiar Next-style routing and rendering semantics where they help, and diverges where pnext needs a simpler or smaller model.
4
-
5
- It renders on the server by default, passes request data through explicit props, provides strong generated route type safety, and ships client JavaScript only for components that opt into running in the browser.
6
-
7
- ## Core Model
8
-
9
- - Routes live in an `app/` directory.
10
- - `page.tsx` files define UI routes.
11
- - `layout.tsx` files export metadata, viewport, and can wrap descendant pages.
12
- - `route.ts` files define HTTP handlers.
13
- - `proxy.ts` or `middleware.ts` can run before route matching.
14
- - `loading.tsx`, `error.tsx`, and `not-found.tsx` define route fallbacks.
15
- - Pages and layouts are pnext Server Components by default.
16
- - Server components can be async.
17
- - Pages without client components emit HTML without pnext client JavaScript.
18
- - Client Components can be loaded with `dynamic({ load: 'visible' })` and deferred until visible.
19
- - React compatibility mode supported via `compat.react`.
20
- - Next compatibility mode supported via `compat.next`.
21
- - Experimental React Compiler support for React-style Client Components via `compat.reactCompiler`.
22
-
23
- ## References
24
-
25
- - [Config](./config.md)
26
- - [Routing](./routing.md)
27
- - [Metadata](./metadata.md)
28
- - [Navigation](./navigation.md)
29
- - [Rendering](./rendering.md)
30
- - [CSS](./css.md)
31
- - [Environment Variables](./env.md)
32
- - [Compatibility](./compat.md)
33
- - [Typegen](./typegen.md)
34
- - [Dev Server](./dev.md)
35
- - [Performance](./performance.md)