@wular/pnext 0.0.6 → 0.0.8

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.
@@ -0,0 +1,65 @@
1
+ import type { h, VNode } from 'preact'
2
+
3
+ // Client half of the static-slot protocol, split out of ./static-slots so it stays PREACT-FREE:
4
+ // entries import it eagerly, and a visible-dynamic entry must not pull preact into its static graph
5
+ // (preact declares no `sideEffects: false`, so even an unused binding keeps the chunk import alive).
6
+ // `createElement` is therefore threaded in from the island mount, where preact is lazily imported.
7
+
8
+ export const ISLAND_STATIC_SLOT_ATTRIBUTE = 'data-pnext-static-slot'
9
+ export const ISLAND_STATIC_SLOT_MARKER = '$$pnext_slot'
10
+
11
+ type Props = Record<string, unknown>
12
+
13
+ /** Cheap gate on the raw props attribute so islands with no element props skip the revive walk. */
14
+ export function hasIslandStaticSlots(raw: string) {
15
+ return raw.includes(ISLAND_STATIC_SLOT_MARKER)
16
+ }
17
+
18
+ /**
19
+ * Client mount: swap each `$$pnext_slot` marker for a `pnext-static-slot` host holding the matching
20
+ * server-rendered DOM, converted to vnodes by the entry's DOM walker (so nested islands inside the
21
+ * adopted subtree become real island vnodes and hydrate on their own). The content is static server
22
+ * markup - it never re-renders, same as element children.
23
+ */
24
+ export async function reviveIslandStaticSlots(
25
+ props: Props,
26
+ root: ParentNode,
27
+ toChildren: (node: ParentNode) => unknown,
28
+ createElement: typeof h,
29
+ ): Promise<Props> {
30
+ return (await reviveSlots(props, root, toChildren, createElement, new Set())) as Props
31
+ }
32
+
33
+ async function reviveSlots(
34
+ value: unknown,
35
+ root: ParentNode,
36
+ toChildren: (node: ParentNode) => unknown,
37
+ createElement: typeof h,
38
+ seen: Set<object>,
39
+ ): Promise<unknown> {
40
+ if (value === null || typeof value !== 'object' || seen.has(value)) return value
41
+ const marker = (value as Props)[ISLAND_STATIC_SLOT_MARKER]
42
+ if (typeof marker === 'string') {
43
+ const node = root.querySelector(`[${ISLAND_STATIC_SLOT_ATTRIBUTE}="${cssEscape(marker)}"]`)
44
+ // No server markup for this slot (the island never rendered the prop, or it was skipped for
45
+ // SSR): nothing to adopt, so the prop arrives null rather than as an empty host.
46
+ if (!node) return null
47
+ return createElement(
48
+ 'pnext-static-slot',
49
+ { [ISLAND_STATIC_SLOT_ATTRIBUTE]: marker, style: { display: 'contents' } },
50
+ (await toChildren(node)) as VNode,
51
+ )
52
+ }
53
+ const proto = Object.getPrototypeOf(value) as object | null
54
+ if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return value
55
+ seen.add(value)
56
+ const target = value as Props
57
+ for (const key of Object.keys(target)) {
58
+ target[key] = await reviveSlots(target[key], root, toChildren, createElement, seen)
59
+ }
60
+ return value
61
+ }
62
+
63
+ function cssEscape(value: string) {
64
+ return value.replace(/["\\]/g, '\\$&')
65
+ }
@@ -1,8 +1,9 @@
1
1
  import { h, type VNode } from 'preact'
2
2
  import { isElementLike } from '../utils/serialize'
3
+ import { ISLAND_STATIC_SLOT_ATTRIBUTE, ISLAND_STATIC_SLOT_MARKER } from './static-slots-revive'
3
4
 
4
- export const ISLAND_STATIC_SLOT_ATTRIBUTE = 'data-pnext-static-slot'
5
- export const ISLAND_STATIC_SLOT_MARKER = '$$pnext_slot'
5
+ // The client half lives in ./static-slots-revive (preact-free); re-exported so importers keep one entry point.
6
+ export * from './static-slots-revive'
6
7
 
7
8
  type Props = Record<string, unknown>
8
9
 
@@ -52,55 +53,3 @@ function mapSlots(
52
53
  if (Array.isArray(value)) return mapped.map(([, item]) => item)
53
54
  return Object.fromEntries(mapped)
54
55
  }
55
-
56
- /** Cheap gate on the raw props attribute so islands with no element props skip the revive walk. */
57
- export function hasIslandStaticSlots(raw: string) {
58
- return raw.includes(ISLAND_STATIC_SLOT_MARKER)
59
- }
60
-
61
- /**
62
- * Client mount: swap each `$$pnext_slot` marker for a `pnext-static-slot` host holding the matching
63
- * server-rendered DOM, converted to vnodes by the entry's DOM walker (so nested islands inside the
64
- * adopted subtree become real island vnodes and hydrate on their own). The content is static server
65
- * markup - it never re-renders, same as element children.
66
- */
67
- export async function reviveIslandStaticSlots(
68
- props: Props,
69
- root: ParentNode,
70
- toChildren: (node: ParentNode) => unknown,
71
- ): Promise<Props> {
72
- return (await reviveSlots(props, root, toChildren, new Set())) as Props
73
- }
74
-
75
- async function reviveSlots(
76
- value: unknown,
77
- root: ParentNode,
78
- toChildren: (node: ParentNode) => unknown,
79
- seen: Set<object>,
80
- ): Promise<unknown> {
81
- if (value === null || typeof value !== 'object' || seen.has(value)) return value
82
- const marker = (value as Props)[ISLAND_STATIC_SLOT_MARKER]
83
- if (typeof marker === 'string') {
84
- const node = root.querySelector(`[${ISLAND_STATIC_SLOT_ATTRIBUTE}="${cssEscape(marker)}"]`)
85
- // No server markup for this slot (the island never rendered the prop, or it was skipped for
86
- // SSR): nothing to adopt, so the prop arrives null rather than as an empty host.
87
- if (!node) return null
88
- return h(
89
- 'pnext-static-slot',
90
- { [ISLAND_STATIC_SLOT_ATTRIBUTE]: marker, style: { display: 'contents' } },
91
- (await toChildren(node)) as VNode,
92
- )
93
- }
94
- const proto = Object.getPrototypeOf(value) as object | null
95
- if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return value
96
- seen.add(value)
97
- const target = value as Props
98
- for (const key of Object.keys(target)) {
99
- target[key] = await reviveSlots(target[key], root, toChildren, seen)
100
- }
101
- return value
102
- }
103
-
104
- function cssEscape(value: string) {
105
- return value.replace(/["\\]/g, '\\$&')
106
- }
@@ -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)