@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 CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  ## Getting started
10
10
 
11
+ > pnext requires [Bun](https://bun.sh/get) - make sure it is installed first.
12
+
11
13
  A new app:
12
14
 
13
15
  ```sh
@@ -22,11 +24,13 @@ bunx @wular/pnext migrate
22
24
 
23
25
  Or by hand: `bun add -d @wular/pnext`, then `pnext dev`.
24
26
 
27
+ [Getting Started](./reference/getting-started.md) walks through all of it, from first page to build.
28
+
25
29
  ## Incremental by design
26
30
 
27
- Server-rendered pages ship **0 KB** of JavaScript, or **~1 KB gzip** if you want client-side navigation and prefetching. Interactive pages hydrate on Preact for **~7.5 KB** of framework, **~12.5 KB** with React compatibility. And everything is instant: the first page in dev renders **1113.5× faster** than Next's on **3.4–3.8× less memory**, and production builds run **7–10.5× faster**. See [Performance](./reference/performance.md).
31
+ Server-rendered pages ship **0 KB** of JavaScript, or **~1 KB gzip** if you want client-side navigation and prefetching. Interactive pages hydrate on Preact for **~7.5 KB** of framework, **~12.5 KB** with React compatibility. Everything is instant, the first page in dev renders **1012× faster** than Next.js on **3.5–4× less memory**, and production builds run **7–9× faster**. See [Performance](./reference/performance.md).
28
32
 
29
- Core pnext is pure Preact. `compat.react` runs React components and libraries on it, and `compat.next` runs a whole Next.js App Router app unchanged. Start anywhere on that ladder and move when it suits you. The App Router compatibility is validated against Next's own test suite (4,400+ assertions passing). The `pages/` folder is not supported, and neither are private internal utilities of Next.js or React. See [Compatibility](./reference/compat.md).
33
+ Core pnext is pure Preact. `compat.react` runs React components and libraries on it, and `compat.next` runs a whole Next.js App Router app unchanged. Start anywhere on that ladder and move when it suits you. The App Router compatibility is validated against Next's own test suite (4,400+ assertions passing). The `pages/` folder or private internal utilities of Next.js or React are mostly not supported. See [Compatibility](./reference/compat.md).
30
34
 
31
35
  ## A quick tour
32
36
 
@@ -63,7 +67,9 @@ import { useState } from 'preact/hooks'
63
67
 
64
68
  export function Counter({ initial }: { initial: number }) {
65
69
  const [count, setCount] = useState(initial)
66
- return <button onClick={() => setCount(count + 1)}>Count {count}</button>
70
+ return (
71
+ <button onClick={() => setCount(count + 1)}>Count {count}</button>
72
+ )
67
73
  }
68
74
  ```
69
75
 
@@ -98,14 +104,14 @@ export async function GET(request: NextRequest) {
98
104
 
99
105
  - `proxy.ts` runs before route matching.
100
106
  - `loading.tsx`, `error.tsx`, and `not-found.tsx` define per-segment fallbacks.
101
- - `pnext build` emits static HTML for routes that never read the request, and a server for the ones that do. `pnext start` serves it.
107
+ - `pnext build` makes the production build and `pnext start` serves it. Routes that never read the request are prerendered to static HTML.
102
108
 
103
109
  ## Learn more
104
110
 
105
111
  Apps are file-routed from `app/`: `page.tsx` and `layout.tsx` are Server Components, `route.ts` files are HTTP handlers, `public/` is served from `/`. The reference covers the rest:
106
112
 
107
- - [Overview](./reference/overview.md)
108
- - [Dev server](./reference/dev.md)
113
+ - [Getting Started](./reference/getting-started.md)
114
+ - [Development](./reference/dev.md)
109
115
  - [Routing](./reference/routing.md)
110
116
  - [Navigation](./reference/navigation.md)
111
117
  - [Rendering](./reference/rendering.md)
package/bin/pnext CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/bin/sh
2
2
 
3
+ if ! command -v bun >/dev/null 2>&1; then
4
+ echo "pnext requires Bun. Install it from https://bun.sh/get" >&2
5
+ exit 1
6
+ fi
7
+
3
8
  script=$0
4
9
 
5
10
  # `dirname` is an external binary and `$(...)` forks a subshell, so spelling this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wular/pnext",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "A fast little framework for server-first React apps, fully compatible with Next.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  # Compatibility
2
2
 
3
- pnext is Next.js-shaped. With `compat.next` on, App Router apps behave the way they do in Next: the `next/*` module surface, `next.config.js`, server actions, metadata, `proxy`/`middleware`, `'use cache'`, the `/_next/image` optimizer, and the `next/navigation` router. That is validated by running Next.js's own test suite against pnext rather than by hand-written parity claims.
3
+ pnext is Next.js-shaped. With `compat.next` on, App Router apps behave the way they do in Next: the `next/*` module surface, `next.config.js`, server actions, metadata, proxy and middleware, `'use cache'`, the `/_next/image` optimizer, and the `next/navigation` router. That is validated by running Next.js's own test suite against pnext, with 4,400+ assertions passing, rather than by hand-written parity claims.
4
4
 
5
5
  ```ts
6
6
  export default {
@@ -12,52 +12,117 @@ export default {
12
12
  }
13
13
  ```
14
14
 
15
- `compat.react` aliases `react`, `react-dom`, `react-dom/client`, `react-dom/server`, `react/jsx-runtime`, `react/jsx-dev-runtime`, and `react/compiler-runtime` onto Preact-backed shims. `react`'s `cache()` maps to pnext's request cache on the server.
15
+ `compat.react` aliases `react`, `react-dom`, `react-dom/client`, `react-dom/server`, `react/jsx-runtime`, `react/jsx-dev-runtime`, and `react/compiler-runtime` onto Preact-backed shims. React's `cache()` maps to pnext's request cache on the server. Importing `next/*` or `react` without the matching mode is a build error that names the flag to enable.
16
16
 
17
- Importing `next/*` or `react` without the matching compat mode is a build error naming the flag to enable.
17
+ To turn Next compatibility on without a config file, set `PNEXT_COMPAT=next`. See [Environment Variables](./env.md).
18
18
 
19
- Everything below is a place pnext deliberately does not match Next. It is the whole list.
19
+ ## Migrating a Next.js app
20
20
 
21
- ## Preact, not React
21
+ ```sh
22
+ bunx @wular/pnext migrate --dry-run
23
+ bunx @wular/pnext migrate
24
+ ```
25
+
26
+ Migration rewrites `package.json` and `tsconfig.json`, and creates a `pnext.config.ts` with `compat.next` enabled. It reports on your app source but never edits it.
27
+
28
+ ## What differs from Next
29
+
30
+ These are the places pnext deliberately does not match Next. Read them before migrating. Together with the caveats in the feature notes below, this is the whole list.
22
31
 
23
- `react-dom/server`'s streaming renderers (`renderToReadableStream`, `renderToPipeableStream`) throw; the synchronous `renderToString`/`renderToStaticMarkup` work. Client components run on Preact's reconciler, so code reaching into React internals is out of scope.
32
+ ### Preact, not React
33
+
34
+ The streaming renderers from `react-dom/server`, `renderToReadableStream` and `renderToPipeableStream`, throw. The synchronous `renderToString` and `renderToStaticMarkup` work. Client components run on Preact's reconciler, so code reaching into React internals is out of scope.
24
35
 
25
36
  Direct Preact imports and Preact Signals need no compat and produce smaller bundles than `preact/compat`.
26
37
 
27
- ## No Flight payload
38
+ ### No Flight payload
39
+
40
+ Soft navigation and server-action responses carry HTML, not a React Flight stream. Streaming, refresh, and revalidation-driven updates all work. Consuming the RSC payload as a wire format does not.
28
41
 
29
- Soft navigation and server-action responses carry HTML, not a React Flight stream. Streaming, `refresh()`, and revalidation-driven updates all work; consuming the RSC payload as a wire format does not.
42
+ Server-rendered JSX can cross into a Client Component as `children` or through any other prop, including nested in arrays and plain objects. Both stay server-rendered static HTML and ship no code, as in Next. Elements inside `Map` and `Set` props are the one shape that throws.
30
43
 
31
- Server-rendered JSX crosses into a Client Component as `children` or through any other prop (including nested in arrays and plain objects) — both stay server-rendered static HTML and ship no code, like in Next. Elements inside `Map`/`Set` props are the one shape that throws.
44
+ ### Pages Router is emulated
32
45
 
33
- ## Pages Router is emulated
46
+ A `pages/` directory is materialized onto App Router routes, so `getStaticProps` and `getServerSideProps` pages and `pages/api` handlers run. `_app`, `_document`, and `_error` are ignored. `next/head` renders nothing, so use the metadata exports instead. `next/router` maps onto the app router where the concepts line up and no-ops elsewhere.
34
47
 
35
- A `pages/` directory is materialized onto App Router routes, so `getStaticProps`/`getServerSideProps` pages and `pages/api` handlers run. `_app`, `_document`, and `_error` are ignored. `next/head` renders nothing — use the metadata exports. `next/router` maps onto the app router where the concepts line up and no-ops elsewhere.
48
+ ### No webpack or Turbopack
36
49
 
37
- ## No webpack or Turbopack
50
+ esbuild is the only bundler. A `webpack(config)` function in `next.config.js` is not executed, and pnext warns once at config load when one is present. Loader chains from `turbopack.rules`, plus `turbopack.resolveAlias`, `transpilePackages`, `modularizeImports`, and `optimizePackageImports`, are re-implemented on esbuild directly.
38
51
 
39
- esbuild is the only bundler, and a `webpack(config)` function in `next.config.js` is not executed (pnext warns once at config load when one is present). `turbopack.rules` loader chains, `turbopack.resolveAlias`, `transpilePackages`, `modularizeImports`, and `optimizePackageImports` are re-implemented on esbuild directly.
52
+ A `webpack()` function that references `@svgr/webpack`, by far the most common custom-loader use, is auto-detected. pnext then compiles `.svg` imports to inline Preact components, matching that loader's default output. Root SVG attributes are spread first, so props such as `className`, `width`, and `height` override them. Without that reference, `.svg` imports keep the normal static-asset URL behavior.
40
53
 
41
- If `webpack()` references `@svgr/webpack` the overwhelmingly common custom-loader use — pnext auto-detects it and compiles `.svg` imports to inline Preact components (root SVG attributes spread first, so `className`/`width`/`height`/etc. props override them), matching `@svgr/webpack`'s default output. Without that reference, `.svg` imports keep the normal static-asset URL behavior.
54
+ ### Cache state is per process
42
55
 
43
- ## Optional native dependencies
56
+ `revalidatePath`, `revalidateTag`, `unstable_cache`, and `'use cache'` entries live in the server process by default, so a multi-instance deployment revalidates one instance. Configure `cacheHandler` in `next.config.js` for a shared store.
44
57
 
45
- All three ship as `optionalDependencies`, so a normal install has them — same as Next. The graceful paths below only matter when the optional install fails.
58
+ ### Optional native dependencies
59
+
60
+ All three ship as `optionalDependencies`, so a normal install has them, the same as Next. These paths only matter when the optional install fails.
46
61
 
47
62
  - `next/image`'s `/_next/image` optimizer needs `sharp`. The component, `images` config validation, and static imports work without it.
48
63
  - `next/og`'s `ImageResponse` needs `satori` and `@resvg/resvg-js`. Without them, or without a usable font, it answers with a valid placeholder PNG instead of failing the request.
49
64
  - `next/font/google` resolves the catalog through `next-font`. If it cannot, the build fails rather than falling back to a hosted font.
50
65
 
51
- ## Smaller surfaces
66
+ ### Smaller surfaces
52
67
 
53
- - `userAgent()` uses an in-house parser covering mainstream browsers, engines, CPUs, and devices not the full ua-parser-js database.
54
- - Only these `next/dist/*` paths are shimmed; any other deep import fails with an error naming this list.
68
+ - `userAgent()` uses an in-house parser covering mainstream browsers, engines, CPUs, and devices, not the full ua-parser-js database.
69
+ - React's taint functions do not exist under Preact, so pnext does not export them. What it implements is the guarantee `experimental.taint` exists for: with the flag on, `process.env` is registered as tainted, and passing that object as a client-component prop at any depth throws. Development shows the message in the nearest error boundary and production shows React's redacted error text, matching Next. Tainting your own objects or values is not available.
70
+ - `ViewTransition` and `addTransitionType` are exported from the `react` entry so pages importing them render instead of throwing, and `next/link` accepts `transitionTypes`. Support stops there: the component is a passthrough that renders no DOM, and pnext does not drive `document.startViewTransition`, so declared names and types are recorded but no browser transition is played.
71
+ - Only these `next/dist/*` paths are shimmed. Any other deep import fails with an error naming this list.
55
72
  - `next/dist/client/components/app-router-headers`
56
73
  - `next/dist/server/web/spec-extension/unstable-cache`
57
74
  - `next/dist/server/web/spec-extension/unstable-no-store`
58
75
  - `next/dist/server/web/spec-extension/revalidate`
59
76
  - `next/dist/server/app-render/work-unit-async-storage.external` (server only)
60
77
 
61
- ## Cache state is per process
78
+ ## Feature notes
79
+
80
+ The rest of the surface ships as well. Each note says where support stops short.
81
+
82
+ ### redirects and rewrites
83
+
84
+ `redirects()` and `rewrites()` in `next.config.js` are both honored. Sources support the `:param`, `:param*`, `:param+`, `:param?`, and `:param(regex)` tokens, plus `has` and `missing` conditions on host, header, query, and cookie. Named capture groups feed their values into destination parameters.
85
+
86
+ Rewrites accept the array form and the object form with `beforeFiles`, `afterFiles`, and `fallback`, and entries setting `basePath: false` match the raw path. An external `http` or `https` destination is proxied through a server-side fetch. After a rewrite fires, `usePathname()` and `useSearchParams()` still report the URL the browser asked for.
87
+
88
+ Redirects use 308 for `permanent: true`, 307 for `permanent: false`, or an explicit `statusCode`, and they keep external destinations as redirects.
89
+
90
+ One partial: fallback rewrites apply only to requests that would otherwise 404, and in development that check consults the route table alone, since there is no built output to look at.
91
+
92
+ ### after()
93
+
94
+ `after()` from `next/server` runs work once the response is fully sent. Each callback runs exactly once, when the response closes, on every path: stream end, a redirect, a not-found, a thrown error, or a client abort. Calls nested inside an `after()` task run too. When the host platform supplies the Vercel request context, each task is also handed to its `waitUntil` so a serverless invocation stays alive until the task settles. An `after()` task that throws during a build prerender fails the build rather than quietly degrading the route.
95
+
96
+ ### next/form
97
+
98
+ `Form` renders a GET form and intercepts submission into a client-side navigation, building the destination URL from the form's fields. String actions get the basePath applied and are prefetched like a link, including a re-prefetch when a revalidation invalidates them. Function actions pass straight through as React form actions with no interception. A submitter that overrides the encoding, method, or target falls back to the browser's native submit, and file inputs are not submitted with a string action. Both cases warn in development.
99
+
100
+ ### instrumentation and instrumentation-client
101
+
102
+ An `instrumentation` file at the project root or in `src/` is bundled and imported once at server start. Its `register()` is awaited before the first request is served, and its `onRequestError` export is wired into the error funnel. When the app contains any edge entity, meaning a proxy or middleware, or a route declaring an edge runtime, a second freshly loaded instance is registered with `NEXT_RUNTIME` set to `edge`, mirroring Next's separate edge boot.
103
+
104
+ An `instrumentation-client` file is bundled with any `instrumentationClientInject` entries ahead of it, in configured order, and loaded from the document head so it runs before hydration. Each module's `onRouterTransitionStart` export is called at the start of every soft navigation. Apps without such a file get no extra bundle and no extra head tag.
105
+
106
+ ### OpenTelemetry
107
+
108
+ pnext emits Next's span taxonomy through the global `@opentelemetry/api` that your instrumentation file registers: a root request span carrying `next.route` and `http.status_code`, plus child spans for rendering, route handlers, `fetch`, middleware and proxy, and Pages Router data and API handlers. Incoming `traceparent` headers are extracted, and errors caught by the request funnel mark the root span.
109
+
110
+ `@opentelemetry/api` is an optional dependency resolved from your own `node_modules`, so pnext and your SDK share one API singleton. When the package is absent, every tracing helper is inert. Keys listed in `experimental.clientTraceMetadata` are injected into the document head as meta tags.
111
+
112
+ ### MDX
113
+
114
+ `.mdx` and `.md` modules compile through `@mdx-js/mdx`, an optional dependency loaded on the first MDX compile, so an app that never imports MDX does not need it installed. `createMDX()` from `@next/mdx` is understood at config load, so the remark, rehype, and recma plugins you configure there run. MDX files become routes only when `pageExtensions` lists the extension, matching Next. An `mdx-components` file supplies the component provider, and without one MDX emits plain host elements. One caveat: MDX currently compiles in the client graph, so treat an MDX module as client code.
115
+
116
+ ### Edge runtime
117
+
118
+ A route or proxy declaring an edge runtime, and a Pages Router handler configured for `edge` or `experimental-edge`, runs with `process.env.NEXT_RUNTIME` set to `edge`, the `EdgeRuntime` global defined, and a `process` object that hides `version` and `versions` so code branching on those detects the edge environment. This is an emulation inside the same Bun process rather than a separate isolate, so the Edge API subset is not enforced: Node built-ins stay reachable, and code that only works because of that will still fail on a real edge platform.
119
+
120
+ ### Root params
121
+
122
+ `next/root-params` resolves parameters from the root dynamic segment. It works in layouts, pages, and `'use cache'` functions, and inside `generateStaticParams` when a parent `generateStaticParams` already provided the parameter. Calling it inside a server action, inside `unstable_cache`, or from a route handler throws with the same diagnostics Next produces. Reading one marks every segment of the response as varying.
123
+
124
+ ### output: 'export' and output: 'standalone'
125
+
126
+ `output: 'export'` writes a static tree to `out/`, or to `distDir` when the app configures a custom one. The tree carries the HTML and the flat per-page artifacts the client router fetches when no pnext server is present, the client runtime under `_next/static/chunks/`, the build manifests, the `public/` tree, and the not-found page. Dynamic routes without `generateStaticParams`, pages forcing dynamic rendering, and route handlers with disallowed segment config are build errors, as they are in Next.
62
127
 
63
- `revalidatePath`/`revalidateTag`, `unstable_cache`, and `'use cache'` entries live in the server process by default, so a multi-instance deployment revalidates one instance. Configure `cacheHandler` in `next.config.js` for a shared store.
128
+ `output: 'standalone'` writes `.next/standalone/` with a `server.js` that boots on `PORT` and `HOSTNAME`, along with a `.nft.json` trace beside each page entry and a middleware manifest. Because pnext's production server runs under Bun, that launcher is a thin Node script that re-executes the real pnext server pointed back at the original build directory. The folder is therefore not a self-contained bundle you can ship on its own, because the build tree has to travel with it.
@@ -1,9 +1,9 @@
1
1
  # Config
2
2
 
3
- pnext reads `pnext.config.ts` from the project root passed to the CLI. The file is optional.
3
+ An optional `pnext.config.ts` in the project root, meaning the directory passed to the CLI.
4
4
 
5
5
  ```ts
6
- import type { pnextConfig } from '@wular/pnext'
6
+ import type { PNextConfig } from '@wular/pnext'
7
7
 
8
8
  export default {
9
9
  outDir: '.pnext',
@@ -12,109 +12,56 @@ export default {
12
12
  compat: {
13
13
  next: true,
14
14
  },
15
- } satisfies pnextConfig
15
+ } satisfies PNextConfig
16
16
  ```
17
17
 
18
- With `compat.next`, `next.config.js` is loaded too, and it wins on the options both files can set (`basePath`, `assetPrefix`, `outDir`, `trailingSlash`, `skipTrailingSlashRedirect`, `productionBrowserSourceMaps`).
18
+ This is the form `pnext create` scaffolds. With `compat.next`, pnext also loads `next.config.js`, and that file wins for the options both can set: `basePath`, `assetPrefix`, `outDir`, `trailingSlash`, `skipTrailingSlashRedirect`, and `productionBrowserSourceMaps`.
19
19
 
20
20
  ## Fields
21
21
 
22
- ### `outDir`
22
+ | Field | Default | What it does |
23
+ | ----------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
24
+ | `outDir` | `'.pnext'` | Build output for HTML, client assets, cache files, and `manifest.json`. |
25
+ | `basePath` | `''` | Path prefix for an app served below the domain root. |
26
+ | `assetPrefix` | `basePath` | URL prefix for emitted assets. Set it when assets come from a CDN. |
27
+ | `trailingSlash` | `false` | Canonicalizes URLs to a trailing slash and redirects the form without one. |
28
+ | `skipTrailingSlashRedirect` | `false` | Serves both slash forms with no canonical redirect, preserving authored hrefs. |
29
+ | `productionBrowserSourceMaps` | `false` | Emits external `.js.map` files next to production client chunks. |
30
+ | `prefetch` | `'visible'` | Default prefetch mode for links that set none of their own. See [Navigation](./navigation.md#prefetching). |
31
+ | `workspaceRoot` | inferred | Monorepo root for resolving and reloading local workspace package imports. |
32
+ | `htmlLimitedBots` | see below | User agents that get blocking metadata in the head. Applies with `compat.next`. |
33
+ | `adapter` | unset | Narrows what a deployment adapter packs into its server function. |
34
+ | `compat` | all `false` | Turns on the React, Next, and React Compiler compatibility layers. |
23
35
 
24
- Default: `'.pnext'`
36
+ Worth knowing:
25
37
 
26
- Build output directory for HTML, client assets, cache files, and `manifest.json`.
38
+ - Development client output is already unminified, so it never emits sourcemaps.
39
+ - `workspaceRoot` is inferred from `package.json#workspaces` or `pnpm-workspace.yaml`, otherwise the app root. It does not change the app root used for `public/`, `.pnext/`, or `pnext.config.ts`. Set it only to override the inference.
40
+ - The `htmlLimitedBots` default is a regex covering Twitterbot, Slackbot, Bingbot, Discordbot, LinkedInBot, and the Google and Lighthouse renderers. Matching user agents receive metadata blocked in the head instead of streamed into the body.
27
41
 
28
- ### `basePath`
42
+ ## Compat modes
29
43
 
30
- Default: `''`
44
+ | Flag | Effect |
45
+ | ---------------------- | -------------------------------------------------------------------------- |
46
+ | `compat.react` | Aliases the React and React DOM entry points to Preact-backed shims. |
47
+ | `compat.next` | Adds the `next/*` surface, `next.config.js`, and Next App Router behavior. |
48
+ | `compat.reactCompiler` | Experimental React Compiler support for React-style Client Components. |
31
49
 
32
- Path prefix for apps served below the domain root.
50
+ Both `compat.next` and `compat.reactCompiler` imply `compat.react`. Direct Preact imports stay smaller than any of them, so reach for compat when you are running React-style components. React Compiler is not intended for components built around Preact Signals. See [Compatibility](./compat.md).
33
51
 
34
- ### `assetPrefix`
52
+ ## Deployment adapter
35
53
 
36
- Default: `basePath`
54
+ `exclude` and `keep` are string lists that adjust which directories and file suffixes an adapter packs. The Vercel adapter consumes them.
37
55
 
38
- URL prefix for emitted assets. Set it to serve `/assets/*` from a CDN.
39
-
40
- ### `trailingSlash`
41
-
42
- Default: `false`
43
-
44
- Canonicalize URLs to a trailing slash, redirecting the unslashed form.
45
-
46
- ### `skipTrailingSlashRedirect`
47
-
48
- Default: `false`
49
-
50
- Serve both the slashed and unslashed URL without a canonical redirect, and leave
51
- `<Link>` hrefs exactly as authored.
52
-
53
- ### `productionBrowserSourceMaps`
54
-
55
- Default: `false`
56
-
57
- Emit external `.js.map` browser sourcemaps next to each production client chunk,
58
- linked by a `//# sourceMappingURL=` comment. Off by default (as in Next): maps
59
- publish your first-party source to every visitor and cost real build time.
60
- Development never emits them — dev client output is un-minified already.
61
-
62
- ### `htmlLimitedBots`
63
-
64
- Default: a regex covering Twitterbot, Slackbot, Bingbot, Discordbot, LinkedInBot,
65
- and the Google/Lighthouse renderers.
66
-
67
- User agents matched by this pattern get metadata blocked in `<head>` rather than
68
- streamed into the body. `compat.next` only.
69
-
70
- ### `adapter`
71
-
72
- Default: unset
73
-
74
- `exclude` and `keep` string lists overriding what a deployment adapter packs into
75
- the server function. Entries are directory names (`storybook-static`) or file
76
- suffixes (`.map`).
77
-
78
- ### `workspaceRoot`
79
-
80
- Default: inferred from `package.json#workspaces` or `pnpm-workspace.yaml`, otherwise the app root.
81
-
82
- Monorepo root used to resolve and reload local workspace package imports. This does not change where pnext reads `public/`, writes `.pnext/`, or loads `pnext.config.ts`.
83
-
84
- ### `compat.react`
85
-
86
- Default: `false`
87
-
88
- When `true`, pnext aliases `react`, `react-dom`, `react-dom/client`, `react-dom/server`, `react/jsx-runtime`, `react/jsx-dev-runtime`, and `react/compiler-runtime` onto Preact-backed shims.
89
-
90
- Use this when moving React-style components into a pnext app. Direct Preact imports are still smaller; `preact/compat` adds compatibility code only when your app imports React APIs.
91
-
92
- ### `compat.next`
93
-
94
- Default: `false`
95
-
96
- Enables the `next/*` module surface, `next.config.js`, and the rest of the Next App Router behaviors for existing apps and shared packages. This implies `compat.react`.
97
-
98
- See [Compatibility](./compat.md).
99
-
100
- ### `compat.reactCompiler`
101
-
102
- Default: `false`
103
-
104
- Experimental React Compiler support for React-style Client Components. This implies `compat.react`.
105
-
106
- Use this for components written with React hooks and props-heavy render paths where compiler memoization can reduce rerenders. Direct Preact code still gives the smallest bundles, and React Compiler compat is not recommended for components built around Preact Signals.
107
-
108
- ## File Conventions
109
-
110
- Routes are loaded from `app/` or `src/app/`. If both exist, `app/` wins. Under
111
- `compat.next`, a `pages/` directory is materialized onto App Router routes; a
112
- hybrid app keeps its native `app/` routes.
113
-
114
- Static assets are loaded from `public/` at the project root.
115
-
116
- ## Workspace Root
56
+ ```ts
57
+ export default {
58
+ adapter: {
59
+ exclude: ['storybook-static', '.map'],
60
+ keep: ['runtime-assets', '.wasm'],
61
+ },
62
+ } satisfies PNextConfig
63
+ ```
117
64
 
118
- pnext keeps the CLI root as the app root for `public/`, `.pnext/`, and `pnext.config.ts`.
65
+ ## Where pnext looks for files
119
66
 
120
- For monorepos, pnext also infers a workspace root by walking upward to `package.json#workspaces` or `pnpm-workspace.yaml`. Set `workspaceRoot` only when that inference should be overridden.
67
+ Routes come from `app/` or `src/app/`, and `app/` wins if both exist. Static assets come from `public/` at the project root. Under `compat.next`, a `pages/` directory is materialized onto App Router routes, and a hybrid app keeps its native `app/` routes.
package/reference/css.md CHANGED
@@ -1,55 +1,31 @@
1
1
  # CSS
2
2
 
3
- ## Global CSS
4
-
5
- Import global styles from the root layout. pnext compiles global CSS once and links it from every page as `/assets/global.css`.
3
+ Global styles, per-route styles, CSS Modules, and the compat-only preprocessors.
6
4
 
7
- Global CSS can use local workspace package imports, font files, image assets, and the app's PostCSS/Tailwind setup.
8
-
9
- ## PostCSS
5
+ ## Global CSS
10
6
 
11
- When the app root has a `postcss.config.{cjs,js,mjs}`, pnext runs its plugins on every emitted stylesheet, in a worker thread. Tailwind v4 (`@tailwindcss/postcss`) stays warm there, so dev rebuilds are incremental; config-file edits need a dev-server restart.
7
+ Imported from the root layout. pnext compiles it once and links it from every page as `/assets/global.css`.
12
8
 
13
9
  ```tsx
14
- import type { ComponentChildren } from 'preact'
10
+ // app/layout.tsx
15
11
  import './globals.css'
16
-
17
- export default function Layout({
18
- children,
19
- }: {
20
- children: ComponentChildren
21
- }) {
22
- return (
23
- <html>
24
- <body>{children}</body>
25
- </html>
26
- )
27
- }
28
12
  ```
29
13
 
30
- ## Route CSS
14
+ Global CSS can import local workspace packages, font files, image assets, and the app's PostCSS or Tailwind setup.
15
+
16
+ ## Route and component CSS
31
17
 
32
- Import plain CSS from pages, layouts, or components:
18
+ Any page, layout, or component can import a stylesheet.
33
19
 
34
20
  ```tsx
35
21
  import './page.css'
36
-
37
- export default function Page() {
38
- return <h1 className="title">Hello</h1>
39
- }
40
22
  ```
41
23
 
42
- pnext emits route CSS only for routes that import CSS. A route with CSS imports gets a linked `/assets/<route>.css` chunk.
24
+ pnext emits CSS only for routes that import it. A route stylesheet is named from the route id, as `/assets/<route-id>.css`. When a compat build splits one into chunks, each name gains an index suffix.
43
25
 
44
26
  ## CSS Modules
45
27
 
46
- Use `.module.css` for scoped classes:
47
-
48
- ```css
49
- .title {
50
- color: rebeccapurple;
51
- }
52
- ```
28
+ A `.module.css` file gives scoped class names that match the server-rendered HTML.
53
29
 
54
30
  ```tsx
55
31
  import styles from './page.module.css'
@@ -59,4 +35,33 @@ export default function Page() {
59
35
  }
60
36
  ```
61
37
 
62
- CSS module class names are scoped in the emitted CSS and match the server-rendered HTML.
38
+ Under `compat.next`, a `:global(...)` wrapper leaves the enclosed selector unscoped.
39
+
40
+ ## Tailwind and PostCSS
41
+
42
+ A `postcss.config.{cjs,js,mjs}` file at the app root makes pnext run its plugins on every emitted stylesheet. Tailwind v4 stays warm in development. Editing the config file requires a dev-server restart.
43
+
44
+ ## Sass
45
+
46
+ Requires `compat.next` and the optional `sass` dependency. `.scss`, `.sass`, `.module.scss`, and `.module.sass` imports compile, and `sassOptions.includePaths` from `next.config.js` is honored. If `sass` is unavailable, Sass imports produce no CSS rather than breaking a non-Sass app.
47
+
48
+ ## styled-jsx
49
+
50
+ Requires `compat.next`. Style blocks and `styled-jsx/css` are transformed to pnext's own runtime, which collects server styles and mirrors client styles into the document head. Apps do not need to install `styled-jsx`.
51
+
52
+ ## Lightning CSS
53
+
54
+ Requires `compat.next` and the app's own `lightningcss` package.
55
+
56
+ - `experimental.useLightningcss` transforms CSS with it.
57
+ - `experimental.lightningCssFeatures` controls which features are included or excluded.
58
+
59
+ CSS passes through untransformed when the package is absent or the transform fails.
60
+
61
+ ## Nonces and inline CSS
62
+
63
+ pnext reads a CSP nonce from the request `Content-Security-Policy` header and applies it to generated styles. With `experimental.inlineCss`, production documents inline route and global CSS in nonce-bearing style tags, while development keeps stylesheet links.
64
+
65
+ ## Chunking and order
66
+
67
+ `experimental.cssChunking` groups compatible route stylesheets into fewer files while preserving each route's stylesheet order. It splits them instead when sharing a chunk would change cascade order or make a chunk too large.
package/reference/dev.md CHANGED
@@ -1,20 +1,18 @@
1
- # Dev Server
1
+ # Development
2
2
 
3
- `pnext dev` starts a Bun HTTP server for local development.
3
+ The `pnext` CLI carries the whole flow: `dev` while you build, `analyze` to inspect what ships, `build` and `start` for production.
4
4
 
5
- ## CLI
6
-
7
- ```txt
5
+ ```sh
8
6
  pnext dev [directory] [--port 3000] [--hostname 127.0.0.1]
9
7
  pnext build [directory] [--adapter vercel] [--verbose]
10
8
  pnext start [directory] [--port 3000] [--hostname 127.0.0.1]
11
9
  pnext analyze [route] [directory] [--brotli] [--files] [--json]
12
10
  pnext typegen [directory]
11
+ pnext create <directory> [--no-install]
12
+ pnext migrate [directory] [--dry-run]
13
13
  ```
14
14
 
15
- `analyze` takes a route (`/users/[id]` or a concrete path like `/users/ada`) to report on that route only.
16
-
17
- Add `@wular/pnext` as a package dependency and put `pnext` in package scripts:
15
+ `create` scaffolds a new app with everything set up. `migrate` converts a Next.js project in place: it rewrites `package.json` and `tsconfig.json` and creates a `pnext.config.ts` with `compat.next` enabled, without editing app source; `--dry-run` previews. To set a project up by hand, add the package and scripts:
18
16
 
19
17
  ```json
20
18
  {
@@ -30,40 +28,35 @@ Add `@wular/pnext` as a package dependency and put `pnext` in package scripts:
30
28
  }
31
29
  ```
32
30
 
33
- The package exposes a `pnext` binary. Package managers make it available inside script `PATH`.
31
+ ## Dev server
34
32
 
35
- ## Processes
33
+ `pnext dev` starts a Bun HTTP server that scans the route tree, renders matching pages on request, and serves static assets from `public/`. Client route entries are built on demand and cached in `.pnext/cache`. Browser pages reload on route-tree changes over a server-sent events stream at `/__pnext/events`; the other `/__pnext/*` endpoints are implementation details, not application routes.
36
34
 
37
- pnext processes are labelled in Activity Monitor and `ps`: `pnext dev` shows
38
- up as a single `pnext-dev` server process, `pnext build` as `pnext-build`, and
39
- esbuild's service process as `pnext-esbuild`. Tailwind and PostCSS run in a
40
- worker thread inside the server process, not as child processes.
35
+ The dev server does not typecheck in the request path; run your package's lint and typecheck scripts separately. `pnext typegen` regenerates the route types on demand; see [Type Safety](./typegen.md).
41
36
 
42
- The dev server re-execs itself same pid when its memory passes
43
- `PNEXT_DEV_MAX_RSS_MB` (default 2048). The esbuild service is stopped and
44
- respawned past `PNEXT_DEV_MAX_ESBUILD_RSS_MB` (default 1024).
37
+ In Activity Monitor and `ps`, the dev server runs as `pnext-dev`, builds as `pnext-build`, and the bundler service as `pnext-esbuild`. The dev server re-execs itself when its memory passes `PNEXT_DEV_MAX_RSS_MB` (default 2048), and restarts the bundler past `PNEXT_DEV_MAX_ESBUILD_RSS_MB` (default 1024).
45
38
 
46
- ## Responsibilities
39
+ ## Analyze
47
40
 
48
- - Scan the route tree.
49
- - Render matching pages on request.
50
- - Serve static assets from `public/`.
51
- - Build client route entries on demand with esbuild.
52
- - Cache generated client entries in `.pnext/cache`.
53
- - Notify the browser of route-tree changes through `/__pnext/events`.
41
+ `pnext analyze` reports the client JavaScript behind each route. Optionally pass a route, either a template like `/users/[id]` or a concrete path, to report on that route only. `--files` breaks the report into files, `--brotli` measures with brotli instead of gzip, and `--json` emits machine-readable output.
54
42
 
55
- ## Cache
43
+ ## Build and run
56
44
 
57
- Client entry cache keys include the route id and source content hash. Cached entries are stored below `.pnext/cache/client`.
45
+ `pnext build` makes the production build and `pnext start` serves it. Routes render on the server per request; ones that never read the request are prerendered to static HTML at build time. A `compat.next` build typechecks off-thread alongside bundling.
58
46
 
59
- ## Reload Events
47
+ Debug flags: `--experimental-build-mode compile|generate` splits the build into its two phases, `--debug-build-paths <paths>` narrows diagnostics to matching paths, and `--debug-prerender` prints prerender diagnostics.
60
48
 
61
- The dev server exposes a server-sent events stream at `/__pnext/events`. Browser pages reload when app files change.
49
+ ## Deploy
62
50
 
63
- ## Typechecking
51
+ pnext deploys anywhere Bun runs: a VPS, a container, or any host you control. Run `pnext build` on the machine or in CI, then `pnext start` serves the app on your port.
64
52
 
65
- The dev server does not typecheck in the request path. Run package lint or typecheck commands separately. `pnext build` under `compat.next` typechecks off-thread, in parallel with bundling, and reports it on its own line.
53
+ Vercel has a dedicated adapter: `pnext build --adapter vercel` writes Build Output to `.vercel/output`. Static pages and static route-handler responses are emitted as files; everything dynamic runs in a single `_pnext` function on Vercel's Bun runtime.
66
54
 
67
- ## Server Adapter
55
+ ## Environment variables
68
56
 
69
- Use `pnext build --adapter vercel` to write Vercel Build Output at `.vercel/output`. Static pages and static `GET` route-handler outputs are emitted as files. Everything dynamic — pages, route handlers, and `proxy.ts` is served by a single `_pnext` function on Vercel's Bun runtime; the proxy's `config.matcher` patterns become routes into it.
57
+ - `PNEXT_COMPAT=next`: Next compatibility without a config file. See [Compatibility](./compat.md).
58
+ - `PNEXT_TYPECHECK=classic`: in-process TypeScript checker instead of the native one.
59
+ - `PNEXT_CLIENT_METAFILE=1`: write the client esbuild metafile to the output directory.
60
+ - `PNEXT_CLIENT_PROFILE=1`: print client-build phase timings.
61
+ - `PNEXT_DEV_PROFILE=1`: print dev request and build timings.
62
+ - `PNEXT_BOOT_TRACE=1`: print boot-phase timings and memory readings.