@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.
- package/README.md +12 -6
- package/bin/pnext +5 -0
- package/package.json +1 -1
- package/reference/compat.md +86 -21
- package/reference/config.md +40 -93
- package/reference/css.md +40 -35
- package/reference/dev.md +25 -32
- package/reference/env.md +18 -20
- package/reference/getting-started.md +132 -0
- package/reference/metadata.md +26 -41
- package/reference/navigation.md +58 -105
- package/reference/performance.md +21 -142
- package/reference/rendering.md +73 -70
- package/reference/routing.md +87 -97
- package/reference/typegen.md +21 -27
- package/src/api/client-navigation.ts +11 -8
- package/src/api/link.tsx +5 -3
- package/src/cli/analyze.ts +103 -48
- package/src/cli/create.ts +1 -1
- package/src/cli/index.ts +6 -0
- package/src/client/build.ts +10 -2
- package/src/client/entry.ts +13 -9
- package/src/client/router/events.ts +15 -0
- package/src/client/router/runtime.ts +40 -9
- package/src/client/router/types.ts +5 -0
- package/src/config.ts +18 -1
- package/src/dev/server.ts +2 -2
- package/src/render/renderer.ts +11 -0
- package/src/render/static-slots-revive.ts +65 -0
- package/src/render/static-slots.ts +3 -54
- package/src/routing/href.ts +19 -1
- package/src/runtime/loader.ts +3 -1
- package/src/runtime/vendor.ts +5 -4
- package/src/types.ts +5 -1
- package/reference/overview.md +0 -35
|
@@ -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
|
-
|
|
5
|
-
export
|
|
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
|
-
}
|
package/src/routing/href.ts
CHANGED
|
@@ -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
|
package/src/runtime/loader.ts
CHANGED
|
@@ -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')
|
package/src/runtime/vendor.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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
|
package/reference/overview.md
DELETED
|
@@ -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)
|