@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 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.15",
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). Note: bun1.x functions have
385
- // a 150 MiB uncompressed size limit vs 250 MiB for nodejs.
386
- runtime: 'bun1.x',
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('pnext requires Bun. Install it from https://bun.sh/get')
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 = (await import(href)) as Parameters<typeof handleRouteModule>[0]
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 = (await import(href)) as RouteHandlerModule &
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, () =>
@@ -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 { devClientModuleHref, devServerModuleHref, importDevModule } from '../runtime/modules'
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
- // In dev, re-throw a module's FIRST evaluation error on every later request
667
- // rather than the downstream symptom a half-evaluated module produces.
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 = (await import(href)) as Record<string, unknown>
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 import(href) as Promise<Record<string, unknown>>
695
+ return importModuleOnce<Record<string, unknown>>(href)
691
696
  }
692
697
 
693
698
  function clientScriptPath(route: RouteManifestEntry, dev?: boolean) {
@@ -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
- /** Import a compiled dev module, re-throwing its first evaluation error on later imports. */
189
- export async function importDevModule<T>(href: string): Promise<T> {
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
- const module = (await import(href)) as T
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',