@webjsdev/server 0.8.36 → 0.8.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/server",
3
- "version": "0.8.36",
3
+ "version": "0.8.37",
4
4
  "type": "module",
5
5
  "description": "webjs dev/prod server: SSR, router, API, server actions, live reload",
6
6
  "main": "index.js",
@@ -50,7 +50,7 @@
50
50
 
51
51
  import * as nodeModule from 'node:module';
52
52
  import { AsyncLocalStorage } from 'node:async_hooks';
53
- import { readFileSync } from 'node:fs';
53
+ import { readFileSync, existsSync } from 'node:fs';
54
54
  import { join } from 'node:path';
55
55
  import { fileURLToPath } from 'node:url';
56
56
  import { stringify } from '@webjsdev/core';
@@ -307,7 +307,14 @@ function seedLoadHook(url, context, nextLoad) {
307
307
  * @param {string} appDir
308
308
  * @returns {((src: string, loader: 'ts' | 'js') => string) | null}
309
309
  */
310
- function buildBunPinTransform(appDir) {
310
+ export function buildBunPinTransform(appDir) {
311
+ // Pinning is for TRUE zero-install only. When `node_modules` exists (an
312
+ // installed app, or a workspace member like this repo's own examples), Bun
313
+ // resolves bare specifiers from it, so injecting an inline version would
314
+ // bypass that. Worse, for a workspace-linked dep it would swap the local
315
+ // package for the published one (the #698 blog-on-Bun regression). So skip
316
+ // pinning entirely when node_modules is present; Bun uses the installed copy.
317
+ if (existsSync(join(appDir, 'node_modules'))) return null;
311
318
  let pkgText;
312
319
  try { pkgText = readFileSync(join(appDir, 'package.json'), 'utf8'); } catch { return null; }
313
320
  let lockText = null;
@@ -22,10 +22,19 @@
22
22
 
23
23
  /**
24
24
  * Resolve the version to pin each DECLARED dependency to: the exact version from
25
- * `bun.lock` when present (precise), else the package.json range/value as-is
26
- * (Bun resolves a range in an inline specifier). Only declared deps are
27
- * returned, so the rewrite never pins a transitive dep through an app import
28
- * (those follow from the pinned direct deps' own manifests).
25
+ * `bun.lock` when present (precise and reproducible), else the package.json
26
+ * declared value passed through as-is when it is an inline-safe semver. Bun's
27
+ * inline specifier resolves a range the standard way (`zod@^3.20.0` picks the
28
+ * highest matching `3.x`, verified), so passing the declared range through is
29
+ * the correct semver behaviour, the same a fresh `bun install` would pick. Only
30
+ * declared deps are returned, so the rewrite never pins a transitive dep through
31
+ * an app import (those follow from the pinned direct deps' own manifests).
32
+ *
33
+ * A protocol range (`workspace:`, `file:`, `link:`, git / URL) and a bare
34
+ * wildcard (`*`, `x`, empty) are NOT valid inline specifiers, so they are left
35
+ * BARE (resolving to latest, exactly as before this feature, never to a broken
36
+ * specifier). For reproducibility across machines, commit a `bun.lock` (its
37
+ * exact pin then wins over a floating range).
29
38
  *
30
39
  * Runtime-neutral: takes the two file contents (the Bun glue reads them via
31
40
  * `Bun.file`), so this stays unit-testable on Node.
@@ -55,14 +64,14 @@ export function resolveDepVersions(pkgJsonText, bunLockText) {
55
64
  }
56
65
 
57
66
  for (const [name, range] of Object.entries(declared)) {
58
- // ONLY an EXACT version can be an inline specifier: Bun zero-install
59
- // auto-install resolves `name@1.2.3` but ENOENTs on a range or dist-tag
60
- // (`name@^1.2`, `name@latest`) and on a protocol range (`workspace:`,
61
- // `file:`, ...). So pin to the bun.lock exact when present, else an exact
62
- // package.json pin; a range without a lock is left BARE (it resolves to
63
- // latest, exactly as before this feature, never to a broken specifier).
67
+ // bun.lock exact wins (precise and reproducible). Otherwise pass the
68
+ // declared semver through as an inline specifier: Bun resolves an exact,
69
+ // caret, tilde, or comparator range the standard way (highest match), so
70
+ // `name@^1.2.3` is correct, not broken. A protocol range (`workspace:`,
71
+ // `file:`, git / URL) and a bare wildcard (`*`, `x`, empty) are NOT
72
+ // inline-safe, so they are left BARE (latest, as before).
64
73
  if (lockExact[name]) out[name] = lockExact[name];
65
- else if (isExactVersion(range)) out[name] = range;
74
+ else if (isInlineableVersion(range)) out[name] = range;
66
75
  }
67
76
  return out;
68
77
  }
@@ -79,6 +88,37 @@ function isExactVersion(v) {
79
88
  return typeof v === 'string' && /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(v);
80
89
  }
81
90
 
91
+ /**
92
+ * Whether a declared package.json version is safe to forward verbatim as a Bun
93
+ * inline specifier (`name@<v>`). Accepts a single-token semver: an exact version
94
+ * (with an optional prerelease / build suffix, `1.2.3`, `1.0.0-rc.3`) or a caret
95
+ * / tilde / comparator range over a numeric core WITHOUT a suffix (`^1.2.3`,
96
+ * `~1.2`, `>=1.2.3`, `^3`). Bun resolves these the standard way (highest match)
97
+ * at auto-install time.
98
+ *
99
+ * Rejects, so they are left BARE: a RANGE OPERATOR combined with a prerelease /
100
+ * build suffix (`^1.0.0-rc.3`, `~1.0.0-beta.1`, #703): Bun zero-install ENOENTs
101
+ * on a caret-prerelease inline specifier (verified, `drizzle-orm@^1.0.0-rc.3`
102
+ * errors while the exact `drizzle-orm@1.0.0-rc.3` resolves). Also a protocol
103
+ * range (`workspace:`, `file:`, `link:`, `git+...`, an `http(s)://` URL, any
104
+ * value with a `:`), a bare wildcard (`*`, `x`, `X`, empty), a multi-token range
105
+ * (a space, a `||` union, a hyphen `1 - 2` range, which would break the
106
+ * specifier string), and a dist-tag (`latest`, `next`, which auto-install
107
+ * resolves unreliably). A rejected value resolves to latest, the pre-feature
108
+ * behaviour, never a broken specifier.
109
+ * @param {unknown} v
110
+ * @returns {boolean}
111
+ */
112
+ function isInlineableVersion(v) {
113
+ if (typeof v !== 'string') return false;
114
+ const m = /^(>=|<=|>|<|=|\^|~)?(\d+(?:\.\d+){0,2})([-+][0-9A-Za-z.-]+)?$/.exec(v);
115
+ if (!m) return false;
116
+ // A range operator (group 1) plus a prerelease / build suffix (group 3) is not
117
+ // inline-resolvable under Bun zero-install (#703). An exact version with a
118
+ // suffix, or a range with no suffix, is fine.
119
+ return !(m[1] && m[3]);
120
+ }
121
+
82
122
  /**
83
123
  * The npm package name a specifier belongs to: `@scope/name[/sub]` or
84
124
  * `name[/sub]`. Returns null for a bare scope with no name.
package/src/vendor.js CHANGED
@@ -47,6 +47,7 @@ import { createRequire } from 'node:module';
47
47
  import { spawnSync } from 'node:child_process';
48
48
  import { digestBase64 } from './crypto-utils.js';
49
49
  import { BUFFERED_MARKER } from './conditional-get.js';
50
+ import { resolveDepVersions } from './bun-pin-rewrite.js';
50
51
 
51
52
  /**
52
53
  * Set of package names whose importmap entries are populated by the
@@ -261,6 +262,26 @@ export function getPackageVersion(pkgName, appDir) {
261
262
  }
262
263
  }
263
264
 
265
+ /**
266
+ * Declared dep versions for the zero-install importmap fallback (#699): the
267
+ * SAME source the Bun server pin uses (`resolveDepVersions`), so the browser
268
+ * importmap and the server resolve a vendor from one source. Returns a map of
269
+ * package name to the bun.lock exact (when present) else the package.json
270
+ * declared semver (an exact or an inline-safe range). Covers `dependencies` and
271
+ * `devDependencies` (what `resolveDepVersions` reads), NOT `peerDependencies`.
272
+ * Empty when there is no readable package.json. Used only as a fallback when
273
+ * `getPackageVersion` (require.resolve against node_modules) finds nothing.
274
+ * @param {string} appDir
275
+ * @returns {Record<string, string>}
276
+ */
277
+ export function declaredVendorVersions(appDir) {
278
+ let pkgText;
279
+ try { pkgText = readFileSync(join(appDir, 'package.json'), 'utf8'); } catch { return {}; }
280
+ let lockText = null;
281
+ try { lockText = readFileSync(join(appDir, 'bun.lock'), 'utf8'); } catch { /* optional */ }
282
+ return resolveDepVersions(pkgText, lockText);
283
+ }
284
+
264
285
  /**
265
286
  * Read the installed package's declared `dependencies` + `peerDependencies`
266
287
  * from its `package.json`, hoist-aware (same resolution as `getPackageVersion`,
@@ -636,11 +657,18 @@ function mergePerInstall(fragments) {
636
657
  */
637
658
  export async function vendorImportMapEntries(bareImports, appDir) {
638
659
  const installs = [];
660
+ // Zero-install fallback (#699). When node_modules is absent (Bun
661
+ // zero-install), `getPackageVersion` (require.resolve) finds nothing, so the
662
+ // entry would be dropped and the browser bare import would 404. Fall back to
663
+ // the SAME source the Bun server pin uses, so the importmap and the server
664
+ // resolve a vendor from one source: bun.lock exact when present, else the
665
+ // package.json declared semver (jspm resolves a range, so a range works).
666
+ const declared = declaredVendorVersions(appDir);
639
667
  for (const spec of bareImports) {
640
668
  if (BUILTIN.has(spec)) continue;
641
669
  const pkg = extractPackageName(spec);
642
670
  if (!pkg || BUILTIN.has(pkg)) continue;
643
- const version = getPackageVersion(pkg, appDir);
671
+ const version = getPackageVersion(pkg, appDir) || declared[pkg];
644
672
  if (!version) continue;
645
673
  // Splice the version into the specifier: 'dayjs/plugin/utc' with
646
674
  // version 1.11.13 becomes 'dayjs@1.11.13/plugin/utc'. jspm.io's