@webjsdev/server 0.8.36 → 0.8.38

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.38",
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
@@ -66,6 +67,18 @@ import { BUFFERED_MARKER } from './conditional-get.js';
66
67
  */
67
68
  const BUILTIN = new Set(['@webjsdev/core', '@webjsdev/core/']);
68
69
 
70
+ /**
71
+ * Server-only framework packages that must NEVER be vendored to the browser.
72
+ * Unlike `@webjsdev/core` (browser-bound, served locally via `/__webjs/core/*`),
73
+ * these are pure server packages: the CLI (and its `webjs-bun.mjs` bootstrap
74
+ * import, #675), the SSR runtime, the MCP server. A stray browser-graph scan
75
+ * that surfaces one of them must not push it onto the jspm path (#713). Matched
76
+ * on the extracted package name, so subpaths (`@webjsdev/cli/bin/webjs.js`) are
77
+ * covered. `@webjsdev/ui` is intentionally absent: its components ARE
78
+ * browser-bound, so it stays vendorable.
79
+ */
80
+ const FRAMEWORK_SERVER_ONLY = new Set(['@webjsdev/cli', '@webjsdev/server', '@webjsdev/mcp']);
81
+
69
82
  /**
70
83
  * Scan source files under `dir` for bare import specifiers reachable
71
84
  * from the browser. Returns a Set of package names.
@@ -90,6 +103,12 @@ export async function scanBareImports(dir, skipFiles) {
90
103
  const found = new Set();
91
104
  await walk(dir, found, skipFiles);
92
105
  for (const b of BUILTIN) found.delete(b);
106
+ // Drop server-only framework packages (and their subpaths) so they never
107
+ // reach the importmap / jspm path (#713).
108
+ for (const spec of found) {
109
+ const p = extractPackageName(spec);
110
+ if (p && FRAMEWORK_SERVER_ONLY.has(p)) found.delete(spec);
111
+ }
93
112
  return found;
94
113
  }
95
114
 
@@ -139,6 +158,10 @@ function isServerOnlyFile(name) {
139
158
  if (/\.server\.(js|ts|mjs|mts)$/.test(name)) return true;
140
159
  if (/^route\.(js|ts|mjs|mts)$/.test(name)) return true;
141
160
  if (/^middleware\.(js|ts|mjs|mts)$/.test(name)) return true;
161
+ // The zero-install Bun bootstrap (#675): a server-only entry that imports
162
+ // `@webjsdev/cli`. It never loads in the browser, so keep it out of the
163
+ // vendor scan, else the CLI would be pushed onto the jspm path (#713).
164
+ if (name === 'webjs-bun.mjs') return true;
142
165
  return false;
143
166
  }
144
167
 
@@ -261,6 +284,26 @@ export function getPackageVersion(pkgName, appDir) {
261
284
  }
262
285
  }
263
286
 
287
+ /**
288
+ * Declared dep versions for the zero-install importmap fallback (#699): the
289
+ * SAME source the Bun server pin uses (`resolveDepVersions`), so the browser
290
+ * importmap and the server resolve a vendor from one source. Returns a map of
291
+ * package name to the bun.lock exact (when present) else the package.json
292
+ * declared semver (an exact or an inline-safe range). Covers `dependencies` and
293
+ * `devDependencies` (what `resolveDepVersions` reads), NOT `peerDependencies`.
294
+ * Empty when there is no readable package.json. Used only as a fallback when
295
+ * `getPackageVersion` (require.resolve against node_modules) finds nothing.
296
+ * @param {string} appDir
297
+ * @returns {Record<string, string>}
298
+ */
299
+ export function declaredVendorVersions(appDir) {
300
+ let pkgText;
301
+ try { pkgText = readFileSync(join(appDir, 'package.json'), 'utf8'); } catch { return {}; }
302
+ let lockText = null;
303
+ try { lockText = readFileSync(join(appDir, 'bun.lock'), 'utf8'); } catch { /* optional */ }
304
+ return resolveDepVersions(pkgText, lockText);
305
+ }
306
+
264
307
  /**
265
308
  * Read the installed package's declared `dependencies` + `peerDependencies`
266
309
  * from its `package.json`, hoist-aware (same resolution as `getPackageVersion`,
@@ -636,11 +679,18 @@ function mergePerInstall(fragments) {
636
679
  */
637
680
  export async function vendorImportMapEntries(bareImports, appDir) {
638
681
  const installs = [];
682
+ // Zero-install fallback (#699). When node_modules is absent (Bun
683
+ // zero-install), `getPackageVersion` (require.resolve) finds nothing, so the
684
+ // entry would be dropped and the browser bare import would 404. Fall back to
685
+ // the SAME source the Bun server pin uses, so the importmap and the server
686
+ // resolve a vendor from one source: bun.lock exact when present, else the
687
+ // package.json declared semver (jspm resolves a range, so a range works).
688
+ const declared = declaredVendorVersions(appDir);
639
689
  for (const spec of bareImports) {
640
690
  if (BUILTIN.has(spec)) continue;
641
691
  const pkg = extractPackageName(spec);
642
- if (!pkg || BUILTIN.has(pkg)) continue;
643
- const version = getPackageVersion(pkg, appDir);
692
+ if (!pkg || BUILTIN.has(pkg) || FRAMEWORK_SERVER_ONLY.has(pkg)) continue;
693
+ const version = getPackageVersion(pkg, appDir) || declared[pkg];
644
694
  if (!version) continue;
645
695
  // Splice the version into the specifier: 'dayjs/plugin/utc' with
646
696
  // version 1.11.13 becomes 'dayjs@1.11.13/plugin/utc'. jspm.io's
@@ -1208,7 +1258,7 @@ export async function pinAll(appDir, opts = {}) {
1208
1258
  for (const spec of bare) {
1209
1259
  if (BUILTIN.has(spec)) continue;
1210
1260
  const pkg = extractPackageName(spec);
1211
- if (!pkg || BUILTIN.has(pkg)) continue;
1261
+ if (!pkg || BUILTIN.has(pkg) || FRAMEWORK_SERVER_ONLY.has(pkg)) continue;
1212
1262
  const version = getPackageVersion(pkg, appDir);
1213
1263
  if (!version) continue;
1214
1264
  const subpath = spec.slice(pkg.length);