@wular/pnext 0.0.15 → 0.0.16
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 +3 -1
- package/package.json +4 -1
- package/reference/getting-started.md +1 -1
- package/src/cli/adapters/vercel.ts +6 -3
- package/src/cli/index.ts +21 -1
- package/src/cli/serve/pipeline.ts +5 -3
- package/src/render/renderer.ts +10 -5
- package/src/runtime/modules.ts +21 -8
package/bin/pnext
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
|
|
3
|
+
# Presence only. The version floor is enforced in src/cli/index.ts, where
|
|
4
|
+
# Bun.version costs nothing — asking `bun --version` here would fork per command.
|
|
3
5
|
if ! command -v bun >/dev/null 2>&1; then
|
|
4
|
-
echo "pnext requires Bun. Install it from https://bun.sh/get" >&2
|
|
6
|
+
echo "pnext requires Bun >=1.3.6. Install it from https://bun.sh/get" >&2
|
|
5
7
|
exit 1
|
|
6
8
|
fi
|
|
7
9
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wular/pnext",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "A fast little framework for server-first React apps, fully compatible with Next.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"bun": ">=1.3.6"
|
|
38
|
+
},
|
|
36
39
|
"exports": {
|
|
37
40
|
".": "./src/index.ts",
|
|
38
41
|
"./internal": "./src/internal.ts",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
pnext is a Preact framework for file-routed apps. It renders on the server by default and ships client JavaScript only where a component opts in.
|
|
4
4
|
|
|
5
|
-
> pnext requires [Bun](https://bun.sh/get) - make sure it is installed first.
|
|
5
|
+
> pnext requires [Bun](https://bun.sh/get) 1.3.6 or newer - make sure it is installed first.
|
|
6
6
|
|
|
7
7
|
<!-- tabs:start -->
|
|
8
8
|
|
|
@@ -381,9 +381,12 @@ export default Object.assign(handler, { fetch: handler });
|
|
|
381
381
|
`${JSON.stringify(
|
|
382
382
|
{
|
|
383
383
|
// The Bun function runtime (project-level `bunVersion` does not apply
|
|
384
|
-
// to prebuilt Build Output API functions
|
|
385
|
-
//
|
|
386
|
-
|
|
384
|
+
// to prebuilt Build Output API functions, so the version is pinned
|
|
385
|
+
// here). Not `bun1.x`: that resolves to Bun 1.3.14, which segfaults a
|
|
386
|
+
// second into serving and takes the whole function down with SIGABRT.
|
|
387
|
+
// Note: bun functions have a 150 MiB uncompressed size limit vs 250
|
|
388
|
+
// MiB for nodejs.
|
|
389
|
+
runtime: 'bun1.4.x',
|
|
387
390
|
handler: 'index.mjs',
|
|
388
391
|
launcherType: 'Nodejs',
|
|
389
392
|
architecture: FUNCTION_PLATFORM.architecture,
|
package/src/cli/index.ts
CHANGED
|
@@ -4,9 +4,29 @@ import path from 'node:path'
|
|
|
4
4
|
import { markBoot } from './boot/trace'
|
|
5
5
|
import { commandBinaryName, nameEsbuildProcess, namedBunBinary } from './boot/named-bin'
|
|
6
6
|
|
|
7
|
+
const MIN_BUN = '1.3.6'
|
|
8
|
+
// Not a floor but a hole in one: 1.3.14 segfaults about a second into serving,
|
|
9
|
+
// so a pnext server dies before its first response with no error of its own to
|
|
10
|
+
// show. 1.3.13 and 1.4.x are both fine.
|
|
11
|
+
const BROKEN_BUN = new Set(['1.3.14'])
|
|
12
|
+
|
|
7
13
|
// Reached only if something bypassed bin/pnext (which guards the same way).
|
|
8
14
|
if (typeof Bun === 'undefined') {
|
|
9
|
-
console.error(
|
|
15
|
+
console.error(`pnext requires Bun >=${MIN_BUN}. Install it from https://bun.sh/get`)
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
// A prerelease sorts below its own release, so compare the release: a 1.4.0-canary is a 1.4.0 here.
|
|
19
|
+
const bunVersion = Bun.version.split('-')[0] ?? Bun.version
|
|
20
|
+
if (BROKEN_BUN.has(bunVersion)) {
|
|
21
|
+
console.error(
|
|
22
|
+
`pnext does not support Bun ${Bun.version}: it crashes the process at runtime. ` +
|
|
23
|
+
'Upgrade to latest Bun with `bun upgrade`.',
|
|
24
|
+
)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
} else if (!Bun.semver.satisfies(bunVersion, `>=${MIN_BUN}`)) {
|
|
27
|
+
console.error(
|
|
28
|
+
`pnext requires Bun >=${MIN_BUN}, found ${Bun.version}. Upgrade with \`bun upgrade\`.`,
|
|
29
|
+
)
|
|
10
30
|
process.exit(1)
|
|
11
31
|
}
|
|
12
32
|
|
|
@@ -14,6 +14,7 @@ import { loadConfig, pathToFileHref } from '../../config'
|
|
|
14
14
|
import type { ResolvedConfig } from '../../config'
|
|
15
15
|
import { bootstrapCompat } from '../../compat-bootstrap'
|
|
16
16
|
import { registerServerRuntime, serverBundleTargetForRuntime } from '../../runtime/loader'
|
|
17
|
+
import { importModuleOnce } from '../../runtime/modules'
|
|
17
18
|
import { applyProxyResponse, createProxyRunner } from '../../routing/proxy'
|
|
18
19
|
import {
|
|
19
20
|
isPprShellUpgradeEligible,
|
|
@@ -146,7 +147,7 @@ export async function createRequestHandler(
|
|
|
146
147
|
const href = compatModuleMode(config)
|
|
147
148
|
? await moduleHrefForRoute(config, route)
|
|
148
149
|
: pathToFileHref(route.file)
|
|
149
|
-
const module =
|
|
150
|
+
const module = await importModuleOnce<Parameters<typeof handleRouteModule>[0]>(href)
|
|
150
151
|
const rendered = await getRenderExtensions().collectRenderMeta(
|
|
151
152
|
() =>
|
|
152
153
|
withRouteRuntime(route.segmentConfig?.runtime, () =>
|
|
@@ -980,8 +981,9 @@ async function handleRoute(
|
|
|
980
981
|
const href = compatModuleMode(config)
|
|
981
982
|
? await moduleHrefForRoute(config, route)
|
|
982
983
|
: new URL(`file://${route.file}`).href
|
|
983
|
-
const imported =
|
|
984
|
-
Parameters<typeof metadataRouteHandlerModule>[0]
|
|
984
|
+
const imported = await importModuleOnce<
|
|
985
|
+
RouteHandlerModule & Parameters<typeof metadataRouteHandlerModule>[0]
|
|
986
|
+
>(href)
|
|
985
987
|
return (metadataRouteHandlerModule(imported, route) ?? imported) as RouteHandlerModule
|
|
986
988
|
})
|
|
987
989
|
return spans.withRouteHandlerSpan(route.route, () =>
|
package/src/render/renderer.ts
CHANGED
|
@@ -26,7 +26,12 @@ import {
|
|
|
26
26
|
import { clientEntryName } from '../client/chunk-name'
|
|
27
27
|
import { pathToFileHref, type ResolvedConfig } from '../config'
|
|
28
28
|
import { markErrorLogged } from '../utils/error-log'
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
devClientModuleHref,
|
|
31
|
+
devServerModuleHref,
|
|
32
|
+
importDevModule,
|
|
33
|
+
importModuleOnce,
|
|
34
|
+
} from '../runtime/modules'
|
|
30
35
|
import { serverBundleTargetForRuntime } from '../runtime/loader'
|
|
31
36
|
import {
|
|
32
37
|
getActionModuleExtensions,
|
|
@@ -663,8 +668,8 @@ function clientModuleHref(file: string, options: ModuleLoadOptions) {
|
|
|
663
668
|
}
|
|
664
669
|
|
|
665
670
|
async function importModule(file: string, options: ModuleLoadOptions) {
|
|
666
|
-
//
|
|
667
|
-
//
|
|
671
|
+
// Re-throw a module's FIRST evaluation error on every later request rather
|
|
672
|
+
// than the downstream symptom a half-evaluated module produces.
|
|
668
673
|
let module: Record<string, unknown>
|
|
669
674
|
if (options.moduleLoader) module = await options.moduleLoader(file)
|
|
670
675
|
else if (options.dev) {
|
|
@@ -672,7 +677,7 @@ async function importModule(file: string, options: ModuleLoadOptions) {
|
|
|
672
677
|
} else {
|
|
673
678
|
const href = await moduleHref(file, options)
|
|
674
679
|
await drainPreplanBuilds()
|
|
675
|
-
module =
|
|
680
|
+
module = await importModuleOnce<Record<string, unknown>>(href)
|
|
676
681
|
}
|
|
677
682
|
// Tag any 'use server' action module's exports with their wire id so
|
|
678
683
|
// <form action={fn}> can progressively enhance. Compat-owned; a no-op for a
|
|
@@ -687,7 +692,7 @@ async function importClientModule(file: string, options: ModuleLoadOptions) {
|
|
|
687
692
|
const href = await clientModuleHref(file, options)
|
|
688
693
|
if (options.dev) return importDevModule<Record<string, unknown>>(href)
|
|
689
694
|
await drainPreplanBuilds()
|
|
690
|
-
return
|
|
695
|
+
return importModuleOnce<Record<string, unknown>>(href)
|
|
691
696
|
}
|
|
692
697
|
|
|
693
698
|
function clientScriptPath(route: RouteManifestEntry, dev?: boolean) {
|
package/src/runtime/modules.ts
CHANGED
|
@@ -185,21 +185,34 @@ const routeModuleLoaders = new Map<string, Promise<DevRouteModuleLoaders>>()
|
|
|
185
185
|
// and the entry is never consulted again.
|
|
186
186
|
const moduleEvalErrors = new Map<string, unknown>()
|
|
187
187
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Import a compiled module, re-throwing its first evaluation error on later imports.
|
|
190
|
+
*
|
|
191
|
+
* Production needs this as much as dev: a re-import resolves with a half-evaluated namespace whose
|
|
192
|
+
* `export const` bindings are still in TDZ, so the render's next read (`module.metadata`) throws
|
|
193
|
+
* `Cannot access 'metadata' before initialization` and that replaces the real error in the response.
|
|
194
|
+
* Prod hrefs are content-addressed too, so the entry keys the same way dev's does.
|
|
195
|
+
*/
|
|
196
|
+
export async function importModuleOnce<T>(href: string): Promise<T> {
|
|
190
197
|
if (moduleEvalErrors.has(href)) throw moduleEvalErrors.get(href)
|
|
191
|
-
// Pre-planned vendor builds are enqueued without an awaiter; nothing
|
|
192
|
-
// evaluates until they all settle (no-op when the pipeline is empty).
|
|
193
|
-
await drainPreplanBuilds()
|
|
194
198
|
try {
|
|
195
|
-
|
|
196
|
-
noteModuleImported(href)
|
|
197
|
-
return module
|
|
199
|
+
return (await import(href)) as T
|
|
198
200
|
} catch (error) {
|
|
199
201
|
moduleEvalErrors.set(href, error)
|
|
200
202
|
throw error
|
|
201
203
|
}
|
|
202
204
|
}
|
|
205
|
+
|
|
206
|
+
/** `importModuleOnce` plus dev's registry-generation accounting. */
|
|
207
|
+
export async function importDevModule<T>(href: string): Promise<T> {
|
|
208
|
+
if (moduleEvalErrors.has(href)) throw moduleEvalErrors.get(href)
|
|
209
|
+
// Pre-planned vendor builds are enqueued without an awaiter; nothing
|
|
210
|
+
// evaluates until they all settle (no-op when the pipeline is empty).
|
|
211
|
+
await drainPreplanBuilds()
|
|
212
|
+
const module = await importModuleOnce<T>(href)
|
|
213
|
+
noteModuleImported(href)
|
|
214
|
+
return module
|
|
215
|
+
}
|
|
203
216
|
const extensionlessDynamicImportExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
|
|
204
217
|
const externalLoadNamespaces = {
|
|
205
218
|
server: 'pnext-route-external-server',
|