@webjsdev/server 0.8.38 → 0.8.40

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/src/vendor.js CHANGED
@@ -47,7 +47,6 @@ 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';
51
50
 
52
51
  /**
53
52
  * Set of package names whose importmap entries are populated by the
@@ -70,8 +69,8 @@ const BUILTIN = new Set(['@webjsdev/core', '@webjsdev/core/']);
70
69
  /**
71
70
  * Server-only framework packages that must NEVER be vendored to the browser.
72
71
  * 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
72
+ * these are pure server packages: the CLI, the SSR runtime, the MCP server. A
73
+ * stray browser-graph scan
75
74
  * that surfaces one of them must not push it onto the jspm path (#713). Matched
76
75
  * on the extracted package name, so subpaths (`@webjsdev/cli/bin/webjs.js`) are
77
76
  * covered. `@webjsdev/ui` is intentionally absent: its components ARE
@@ -158,10 +157,6 @@ function isServerOnlyFile(name) {
158
157
  if (/\.server\.(js|ts|mjs|mts)$/.test(name)) return true;
159
158
  if (/^route\.(js|ts|mjs|mts)$/.test(name)) return true;
160
159
  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;
165
160
  return false;
166
161
  }
167
162
 
@@ -284,26 +279,6 @@ export function getPackageVersion(pkgName, appDir) {
284
279
  }
285
280
  }
286
281
 
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
-
307
282
  /**
308
283
  * Read the installed package's declared `dependencies` + `peerDependencies`
309
284
  * from its `package.json`, hoist-aware (same resolution as `getPackageVersion`,
@@ -679,18 +654,11 @@ function mergePerInstall(fragments) {
679
654
  */
680
655
  export async function vendorImportMapEntries(bareImports, appDir) {
681
656
  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);
689
657
  for (const spec of bareImports) {
690
658
  if (BUILTIN.has(spec)) continue;
691
659
  const pkg = extractPackageName(spec);
692
660
  if (!pkg || BUILTIN.has(pkg) || FRAMEWORK_SERVER_ONLY.has(pkg)) continue;
693
- const version = getPackageVersion(pkg, appDir) || declared[pkg];
661
+ const version = getPackageVersion(pkg, appDir);
694
662
  if (!version) continue;
695
663
  // Splice the version into the specifier: 'dayjs/plugin/utc' with
696
664
  // version 1.11.13 becomes 'dayjs@1.11.13/plugin/utc'. jspm.io's
@@ -17,11 +17,6 @@
17
17
  "type": "boolean",
18
18
  "default": true
19
19
  },
20
- "pin": {
21
- "description": "Bun zero-install dependency version-pinning switch (#685). Default true, Bun only. When on, an onLoad transform rewrites a declared dep's bare import specifier to its package.json / bun.lock version so Bun auto-install fetches the pinned version instead of latest (#684). Set to false to leave specifiers bare (zero-install resolves to latest). Read by readPinEnabled in dev.js; the WEBJS_PIN env override wins over this. No effect on Node.",
22
- "type": "boolean",
23
- "default": true
24
- },
25
20
  "clientRouter": {
26
21
  "description": "Client-router switch (#629). Default true: the client router auto-enables in the browser whenever @webjsdev/core loads, so any page with a component gets SPA-style navigation with no import. Set to false to opt the whole app out (pure MPA, full-page navigation). Read by readClientRouterEnabled in dev.js.",
27
22
  "type": "boolean",
@@ -1,192 +0,0 @@
1
- /**
2
- * Pin Bun zero-install dependency versions by rewriting bare import specifiers
3
- * to inline-versioned ones (#685).
4
- *
5
- * Under Bun zero-install (`bun run dev` / `start` via the `webjs-bun.mjs`
6
- * bootstrap, no `node_modules`), Bun's auto-install fetches `latest` for a bare
7
- * `import 'zod'`, ignoring package.json and `bun.lock` (#684). But Bun honors an
8
- * INLINE version in the specifier (`import 'zod@1.0.0'`), and that survives the
9
- * `onResolve` bypass because it is part of the specifier the loader receives. So
10
- * an `onLoad` transform that rewrites `import 'zod'` to `import 'zod@<pinned>'`
11
- * (the version from package.json / `bun.lock`) makes auto-install fetch the
12
- * pinned version. This module is the runtime-neutral, unit-testable core; the
13
- * Bun `onLoad` glue (which supplies `Bun.Transpiler().scanImports` and the
14
- * resolved dep versions) lives in the Bun-side plugin.
15
- *
16
- * The specifier set comes from `Bun.Transpiler.scanImports`, which is
17
- * AST-accurate (a plain string that merely looks like a specifier is NOT
18
- * listed), so we never rewrite a non-import string. The in-source replacement is
19
- * additionally anchored on the `from` / `import` / `require` keyword so an
20
- * identical non-import string literal elsewhere is left alone.
21
- */
22
-
23
- /**
24
- * Resolve the version to pin each DECLARED dependency to: the exact version from
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).
38
- *
39
- * Runtime-neutral: takes the two file contents (the Bun glue reads them via
40
- * `Bun.file`), so this stays unit-testable on Node.
41
- *
42
- * @param {string} pkgJsonText package.json contents
43
- * @param {string | null} [bunLockText] bun.lock contents, when present
44
- * @returns {Record<string, string>} package name -> version
45
- */
46
- export function resolveDepVersions(pkgJsonText, bunLockText) {
47
- /** @type {Record<string, string>} */
48
- const out = {};
49
- let pkg;
50
- try { pkg = JSON.parse(pkgJsonText); } catch { return out; }
51
- const declared = { ...pkg.dependencies, ...pkg.devDependencies };
52
-
53
- // bun.lock pins each package as `"name": ["name@<exact>", ...]`. Extract the
54
- // exact version for each DECLARED dep, anchored on its name so a substring
55
- // match cannot cross to another package. This is the precise source.
56
- /** @type {Record<string, string>} */
57
- const lockExact = {};
58
- if (bunLockText) {
59
- for (const name of Object.keys(declared)) {
60
- const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
61
- const m = bunLockText.match(new RegExp('"' + esc + '"\\s*:\\s*\\[\\s*"' + esc + '@([^"]+)"'));
62
- if (m && isExactVersion(m[1])) lockExact[name] = m[1];
63
- }
64
- }
65
-
66
- for (const [name, range] of Object.entries(declared)) {
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).
73
- if (lockExact[name]) out[name] = lockExact[name];
74
- else if (isInlineableVersion(range)) out[name] = range;
75
- }
76
- return out;
77
- }
78
-
79
- /**
80
- * Whether a version string is an EXACT semver (the only form valid as a Bun
81
- * inline specifier): `1.2.3`, with an optional prerelease / build suffix
82
- * (`1.2.3-rc.1`, `1.2.3+build`). Rejects any range operator (`^ ~ > < = * x | -`
83
- * space), a dist-tag (`latest`), and a protocol range (`workspace:` etc.).
84
- * @param {unknown} v
85
- * @returns {boolean}
86
- */
87
- function isExactVersion(v) {
88
- return typeof v === 'string' && /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(v);
89
- }
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
-
122
- /**
123
- * The npm package name a specifier belongs to: `@scope/name[/sub]` or
124
- * `name[/sub]`. Returns null for a bare scope with no name.
125
- * @param {string} p
126
- * @returns {string | null}
127
- */
128
- export function packageNameOf(p) {
129
- if (p.startsWith('@')) {
130
- const parts = p.split('/');
131
- return parts.length >= 2 && parts[0] && parts[1] ? parts[0] + '/' + parts[1] : null;
132
- }
133
- return p.split('/')[0] || null;
134
- }
135
-
136
- /**
137
- * Whether a specifier should be left untouched: relative, the `#` app alias, a
138
- * protocol/builtin (`node:`, `bun:`, `http:`...), or already version-pinned.
139
- * @param {string} p
140
- * @param {string} name
141
- * @returns {boolean}
142
- */
143
- function skipSpecifier(p, name) {
144
- if (!p || p[0] === '.' || p[0] === '#') return true;
145
- // A protocol/builtin has a colon before any slash (node:fs, bun:sqlite, http:).
146
- // A scope `@scope/x` has no leading colon, so it is not caught here.
147
- const firstSlash = p.indexOf('/');
148
- const head = firstSlash === -1 ? p : p.slice(0, firstSlash);
149
- if (head.includes(':')) return true;
150
- // Already versioned: the name is immediately followed by `@<something>`.
151
- if (p.slice(name.length).startsWith('@')) return true;
152
- return false;
153
- }
154
-
155
- /**
156
- * Rewrite bare specifiers of DECLARED deps to `name@version` (keeping any
157
- * subpath). `name` -> `name@v`, `name/sub` -> `name@v/sub`,
158
- * `@scope/name/sub` -> `@scope/name@v/sub`.
159
- *
160
- * @param {string} src module source
161
- * @param {Array<{ kind: string, path: string }>} imports Bun.Transpiler.scanImports output
162
- * @param {Record<string, string>} depVersions package name -> version (exact preferred, e.g. from bun.lock)
163
- * @returns {string} the rewritten source (unchanged when nothing matched)
164
- */
165
- export function rewriteDepSpecifiers(src, imports, depVersions) {
166
- /** @type {Map<string, string>} specifier -> versioned specifier */
167
- const remap = new Map();
168
- for (const imp of imports) {
169
- const p = imp && imp.path;
170
- if (!p || remap.has(p)) continue;
171
- const name = packageNameOf(p);
172
- if (!name || skipSpecifier(p, name)) continue;
173
- const ver = depVersions[name];
174
- if (!ver) continue;
175
- remap.set(p, name + '@' + ver + p.slice(name.length));
176
- }
177
- if (remap.size === 0) return src;
178
-
179
- let out = src;
180
- for (const [from, to] of remap) {
181
- const q = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
182
- // Anchor on the import/export/require form so an identical non-import
183
- // string literal is not touched: `from 'x'`, `import 'x'`, `import('x')`,
184
- // `require('x')`. The optional `(` covers dynamic import / require. The
185
- // leading `(^|[^\w.$])` boundary keeps a method call (`db.select().from('x')`,
186
- // a `.import(...)` member) or a keyword-suffixed identifier (`xfrom 'x'`)
187
- // from matching, which an unanchored keyword would wrongly rewrite.
188
- const re = new RegExp("(^|[^\\w.$])((?:from|import|require)\\s*\\(?\\s*)(['\"])" + q + "\\3", 'g');
189
- out = out.replace(re, (_m, pre, lead, quote) => pre + lead + quote + to + quote);
190
- }
191
- return out;
192
- }