@webjsdev/server 0.8.35 → 0.8.36
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 +1 -1
- package/src/action-seed-bun.js +45 -11
- package/src/action-seed.js +52 -11
- package/src/bun-pin-rewrite.js +152 -0
- package/src/dev.js +31 -1
- package/webjs-config.schema.json +5 -0
package/package.json
CHANGED
package/src/action-seed-bun.js
CHANGED
|
@@ -26,39 +26,73 @@
|
|
|
26
26
|
* same module Bun would have loaded.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
/** Bun's global install cache + node_modules: a dep, never an app file to rewrite. */
|
|
30
|
+
const DEP_PATH_RE = /[\\/](?:node_modules|install[\\/]cache|\.bun)[\\/]/;
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
|
-
* Install the `Bun.plugin`
|
|
31
|
-
*
|
|
33
|
+
* Install the webjs `Bun.plugin` `onLoad`. It carries two independent transforms
|
|
34
|
+
* on ONE handler (Bun `onLoad` is first-match-wins, so a second overlapping
|
|
35
|
+
* plugin would starve one of them):
|
|
36
|
+
*
|
|
37
|
+
* - **Pin rewrite (#685)**, when `pinTransform` is supplied: rewrite bare
|
|
38
|
+
* specifiers of declared deps to inline-versioned ones so Bun zero-install
|
|
39
|
+
* fetches the pinned version, not latest. Applies to APP files only (a dep
|
|
40
|
+
* in `node_modules` / the global cache is returned raw). This broadens the
|
|
41
|
+
* filter to all JS/TS; without it the filter stays the cheap `*.server.*`
|
|
42
|
+
* pre-screen.
|
|
43
|
+
* - **Seed facade (#472)**, when `seedEnabled`: facet a `'use server'`
|
|
44
|
+
* candidate into the SSR action-result seed facade, built from the
|
|
45
|
+
* (already pin-rewritten) source.
|
|
46
|
+
*
|
|
47
|
+
* Idempotency is the caller's responsibility (`registerSeedHooks` guards on
|
|
48
|
+
* `_registered`).
|
|
32
49
|
*
|
|
33
50
|
* @param {{
|
|
34
51
|
* isSeedCandidate: (specifier: string) => boolean,
|
|
35
52
|
* buildSeedFacade: (origSpec: string, absPath: string, src: string) => (string | null),
|
|
36
53
|
* serverFileRe: RegExp,
|
|
54
|
+
* seedEnabled?: boolean,
|
|
55
|
+
* pinTransform?: ((src: string, loader: 'ts' | 'js') => string) | null,
|
|
37
56
|
* }} helpers
|
|
38
57
|
*/
|
|
39
|
-
export function installBunSeedPlugin({ isSeedCandidate, buildSeedFacade, serverFileRe }) {
|
|
58
|
+
export function installBunSeedPlugin({ isSeedCandidate, buildSeedFacade, serverFileRe, seedEnabled = true, pinTransform = null }) {
|
|
59
|
+
// Pinning needs to see every app module's imports, so broaden the filter when
|
|
60
|
+
// it is active; otherwise keep the narrow `*.server.*` seed pre-screen.
|
|
61
|
+
const filter = pinTransform ? /\.m?[jt]s(\?|$)/ : serverFileRe;
|
|
40
62
|
Bun.plugin({
|
|
41
|
-
name: 'webjs-
|
|
63
|
+
name: 'webjs-onload',
|
|
42
64
|
setup(build) {
|
|
43
|
-
|
|
44
|
-
build.onLoad({ filter: serverFileRe }, async (args) => {
|
|
65
|
+
build.onLoad({ filter }, async (args) => {
|
|
45
66
|
const absPath = args.path.split('?')[0];
|
|
46
67
|
// `.ts` / `.mts` strip via the `ts` loader; `.js` / `.mjs` via `js`.
|
|
47
68
|
const loader = /\.m?ts$/.test(absPath) ? 'ts' : 'js';
|
|
69
|
+
// A dependency (node_modules or Bun's global cache): never rewrite it
|
|
70
|
+
// (the app's package.json does not pin transitive deps; they follow from
|
|
71
|
+
// the pinned direct deps' own manifests). Return the raw source unchanged.
|
|
72
|
+
if (pinTransform && DEP_PATH_RE.test(absPath)) {
|
|
73
|
+
return { contents: await Bun.file(absPath).text(), loader };
|
|
74
|
+
}
|
|
48
75
|
// Read the real source. A genuine read failure (missing file) propagates,
|
|
49
76
|
// which Bun reports as a load error exactly as it would without the plugin.
|
|
50
|
-
|
|
77
|
+
let src = await Bun.file(absPath).text();
|
|
78
|
+
// Pin rewrite. For a NON-`.server` module this is where its specifiers get
|
|
79
|
+
// pinned. A `.server` candidate's REAL module is pinned on its
|
|
80
|
+
// `?webjs-seed-orig` passthrough load (which re-enters this onLoad); the
|
|
81
|
+
// facade itself only reads export names, unaffected by the rewrite.
|
|
82
|
+
if (pinTransform) {
|
|
83
|
+
try { src = pinTransform(src, loader); } catch { /* fail-open: raw source */ }
|
|
84
|
+
}
|
|
51
85
|
try {
|
|
52
86
|
// Facet only a `'use server'` candidate (not the `?webjs-seed-orig`
|
|
53
87
|
// passthrough); a non-candidate or a passthrough falls through to the
|
|
54
|
-
//
|
|
55
|
-
if (isSeedCandidate(args.path)) {
|
|
88
|
+
// (possibly pin-rewritten) source below.
|
|
89
|
+
if (seedEnabled && isSeedCandidate(args.path)) {
|
|
56
90
|
const source = buildSeedFacade(args.path, absPath, src);
|
|
57
91
|
if (source != null) return { contents: source, loader: 'js' };
|
|
58
92
|
}
|
|
59
93
|
} catch {
|
|
60
|
-
// Fail-open: any faceting error serves the
|
|
61
|
-
//
|
|
94
|
+
// Fail-open: any faceting error serves the source (no seeding for this
|
|
95
|
+
// module), the Bun analog of the Node hook's `nextLoad` fallback.
|
|
62
96
|
}
|
|
63
97
|
return { contents: src, loader };
|
|
64
98
|
});
|
package/src/action-seed.js
CHANGED
|
@@ -51,11 +51,13 @@
|
|
|
51
51
|
import * as nodeModule from 'node:module';
|
|
52
52
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
53
53
|
import { readFileSync } from 'node:fs';
|
|
54
|
+
import { join } from 'node:path';
|
|
54
55
|
import { fileURLToPath } from 'node:url';
|
|
55
56
|
import { stringify } from '@webjsdev/core';
|
|
56
57
|
import { hashFile } from './actions.js';
|
|
57
58
|
import { isStreamable } from './action-stream.js';
|
|
58
59
|
import { serverRuntime } from './listener-core.js';
|
|
60
|
+
import { resolveDepVersions, rewriteDepSpecifiers } from './bun-pin-rewrite.js';
|
|
59
61
|
|
|
60
62
|
/** Ambient per-render seed collector. `Map<key, value>` or undefined. */
|
|
61
63
|
const als = new AsyncLocalStorage();
|
|
@@ -296,27 +298,66 @@ function seedLoadHook(url, context, nextLoad) {
|
|
|
296
298
|
}
|
|
297
299
|
|
|
298
300
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* Bun
|
|
305
|
-
*
|
|
301
|
+
* Build the per-file pin transform for Bun (#685): read the app's declared dep
|
|
302
|
+
* versions (package.json, with `bun.lock` exact pins preferred) and return a
|
|
303
|
+
* function that rewrites a module's bare dep specifiers to inline-versioned ones
|
|
304
|
+
* so Bun zero-install fetches the pinned version, not latest. Returns null when
|
|
305
|
+
* there is nothing to pin (no package.json, or no declared deps). Bun-only: it
|
|
306
|
+
* references `Bun.Transpiler`, so it is invoked only on the Bun branch.
|
|
307
|
+
* @param {string} appDir
|
|
308
|
+
* @returns {((src: string, loader: 'ts' | 'js') => string) | null}
|
|
309
|
+
*/
|
|
310
|
+
function buildBunPinTransform(appDir) {
|
|
311
|
+
let pkgText;
|
|
312
|
+
try { pkgText = readFileSync(join(appDir, 'package.json'), 'utf8'); } catch { return null; }
|
|
313
|
+
let lockText = null;
|
|
314
|
+
try { lockText = readFileSync(join(appDir, 'bun.lock'), 'utf8'); } catch { /* optional */ }
|
|
315
|
+
const versions = resolveDepVersions(pkgText, lockText);
|
|
316
|
+
if (Object.keys(versions).length === 0) return null;
|
|
317
|
+
return (src, loader) => {
|
|
318
|
+
const imports = new Bun.Transpiler({ loader }).scanImports(src);
|
|
319
|
+
return rewriteDepSpecifiers(src, imports, versions);
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Install the boot-time load hook(s) (idempotent). Called once from `dev.js`,
|
|
325
|
+
* BEFORE any app module is imported (a module loaded before the hook would
|
|
326
|
+
* already be cached untransformed). Two concerns ride here:
|
|
327
|
+
*
|
|
328
|
+
* - **Seeding (#472)** when `seedEnabled`: facet `'use server'` modules into
|
|
329
|
+
* the SSR action-result seed facade. Node uses `module.registerHooks`; Bun a
|
|
330
|
+
* `Bun.plugin` `onLoad`.
|
|
331
|
+
* - **Pinning (#685)** when `pinEnabled` (Bun only): rewrite bare dep
|
|
332
|
+
* specifiers to the package.json/bun.lock version so zero-install fetches the
|
|
333
|
+
* pinned version, not latest. Independent of seeding, so the Bun plugin is
|
|
334
|
+
* installed when EITHER is on (they share the one first-match-wins onLoad).
|
|
335
|
+
*
|
|
336
|
+
* No-op on a second call. Async because the Bun path dynamically imports
|
|
337
|
+
* `action-seed-bun.js` (so the `Bun.*` global is never referenced on Node).
|
|
338
|
+
*
|
|
339
|
+
* @param {{ appDir?: string | null, seedEnabled?: boolean, pinEnabled?: boolean }} [opts]
|
|
306
340
|
* @returns {Promise<void>}
|
|
307
341
|
*/
|
|
308
|
-
export async function registerSeedHooks() {
|
|
342
|
+
export async function registerSeedHooks({ appDir = null, seedEnabled = true, pinEnabled = false } = {}) {
|
|
309
343
|
if (_registered) return;
|
|
310
344
|
_registered = true;
|
|
311
345
|
|
|
312
346
|
if (serverRuntime() === 'bun') {
|
|
313
|
-
// Bun has no module.registerHooks; install the
|
|
347
|
+
// Bun has no module.registerHooks; install the facade + pin rewrite via one
|
|
348
|
+
// Bun.plugin onLoad (first-match-wins, so the two transforms must share it).
|
|
314
349
|
const { installBunSeedPlugin } = await import('./action-seed-bun.js');
|
|
315
|
-
|
|
316
|
-
|
|
350
|
+
const pinTransform = (pinEnabled && appDir) ? buildBunPinTransform(appDir) : null;
|
|
351
|
+
installBunSeedPlugin({ isSeedCandidate, buildSeedFacade, serverFileRe: SERVER_FILE_RE, seedEnabled, pinTransform });
|
|
352
|
+
if (seedEnabled) _enabled = true;
|
|
317
353
|
return;
|
|
318
354
|
}
|
|
319
355
|
|
|
356
|
+
// Node: seeding only (Bun-specific inline-version pinning does not apply; the
|
|
357
|
+
// Node zero-install resolve-hook is #669's separate concern). Nothing to
|
|
358
|
+
// install when seeding is off.
|
|
359
|
+
if (!seedEnabled) return;
|
|
360
|
+
|
|
320
361
|
// A runtime that is neither Node-with-registerHooks nor Bun: seeding stays
|
|
321
362
|
// OFF (fail-open). `seedingEnabled()` is false, ssr.js emits no seed block,
|
|
322
363
|
// and the client RPC stub falls back to a normal fetch. Never wrong data.
|
|
@@ -0,0 +1,152 @@
|
|
|
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), 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).
|
|
29
|
+
*
|
|
30
|
+
* Runtime-neutral: takes the two file contents (the Bun glue reads them via
|
|
31
|
+
* `Bun.file`), so this stays unit-testable on Node.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} pkgJsonText package.json contents
|
|
34
|
+
* @param {string | null} [bunLockText] bun.lock contents, when present
|
|
35
|
+
* @returns {Record<string, string>} package name -> version
|
|
36
|
+
*/
|
|
37
|
+
export function resolveDepVersions(pkgJsonText, bunLockText) {
|
|
38
|
+
/** @type {Record<string, string>} */
|
|
39
|
+
const out = {};
|
|
40
|
+
let pkg;
|
|
41
|
+
try { pkg = JSON.parse(pkgJsonText); } catch { return out; }
|
|
42
|
+
const declared = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
43
|
+
|
|
44
|
+
// bun.lock pins each package as `"name": ["name@<exact>", ...]`. Extract the
|
|
45
|
+
// exact version for each DECLARED dep, anchored on its name so a substring
|
|
46
|
+
// match cannot cross to another package. This is the precise source.
|
|
47
|
+
/** @type {Record<string, string>} */
|
|
48
|
+
const lockExact = {};
|
|
49
|
+
if (bunLockText) {
|
|
50
|
+
for (const name of Object.keys(declared)) {
|
|
51
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
const m = bunLockText.match(new RegExp('"' + esc + '"\\s*:\\s*\\[\\s*"' + esc + '@([^"]+)"'));
|
|
53
|
+
if (m && isExactVersion(m[1])) lockExact[name] = m[1];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
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).
|
|
64
|
+
if (lockExact[name]) out[name] = lockExact[name];
|
|
65
|
+
else if (isExactVersion(range)) out[name] = range;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether a version string is an EXACT semver (the only form valid as a Bun
|
|
72
|
+
* inline specifier): `1.2.3`, with an optional prerelease / build suffix
|
|
73
|
+
* (`1.2.3-rc.1`, `1.2.3+build`). Rejects any range operator (`^ ~ > < = * x | -`
|
|
74
|
+
* space), a dist-tag (`latest`), and a protocol range (`workspace:` etc.).
|
|
75
|
+
* @param {unknown} v
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function isExactVersion(v) {
|
|
79
|
+
return typeof v === 'string' && /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(v);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The npm package name a specifier belongs to: `@scope/name[/sub]` or
|
|
84
|
+
* `name[/sub]`. Returns null for a bare scope with no name.
|
|
85
|
+
* @param {string} p
|
|
86
|
+
* @returns {string | null}
|
|
87
|
+
*/
|
|
88
|
+
export function packageNameOf(p) {
|
|
89
|
+
if (p.startsWith('@')) {
|
|
90
|
+
const parts = p.split('/');
|
|
91
|
+
return parts.length >= 2 && parts[0] && parts[1] ? parts[0] + '/' + parts[1] : null;
|
|
92
|
+
}
|
|
93
|
+
return p.split('/')[0] || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether a specifier should be left untouched: relative, the `#` app alias, a
|
|
98
|
+
* protocol/builtin (`node:`, `bun:`, `http:`...), or already version-pinned.
|
|
99
|
+
* @param {string} p
|
|
100
|
+
* @param {string} name
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
function skipSpecifier(p, name) {
|
|
104
|
+
if (!p || p[0] === '.' || p[0] === '#') return true;
|
|
105
|
+
// A protocol/builtin has a colon before any slash (node:fs, bun:sqlite, http:).
|
|
106
|
+
// A scope `@scope/x` has no leading colon, so it is not caught here.
|
|
107
|
+
const firstSlash = p.indexOf('/');
|
|
108
|
+
const head = firstSlash === -1 ? p : p.slice(0, firstSlash);
|
|
109
|
+
if (head.includes(':')) return true;
|
|
110
|
+
// Already versioned: the name is immediately followed by `@<something>`.
|
|
111
|
+
if (p.slice(name.length).startsWith('@')) return true;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Rewrite bare specifiers of DECLARED deps to `name@version` (keeping any
|
|
117
|
+
* subpath). `name` -> `name@v`, `name/sub` -> `name@v/sub`,
|
|
118
|
+
* `@scope/name/sub` -> `@scope/name@v/sub`.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} src module source
|
|
121
|
+
* @param {Array<{ kind: string, path: string }>} imports Bun.Transpiler.scanImports output
|
|
122
|
+
* @param {Record<string, string>} depVersions package name -> version (exact preferred, e.g. from bun.lock)
|
|
123
|
+
* @returns {string} the rewritten source (unchanged when nothing matched)
|
|
124
|
+
*/
|
|
125
|
+
export function rewriteDepSpecifiers(src, imports, depVersions) {
|
|
126
|
+
/** @type {Map<string, string>} specifier -> versioned specifier */
|
|
127
|
+
const remap = new Map();
|
|
128
|
+
for (const imp of imports) {
|
|
129
|
+
const p = imp && imp.path;
|
|
130
|
+
if (!p || remap.has(p)) continue;
|
|
131
|
+
const name = packageNameOf(p);
|
|
132
|
+
if (!name || skipSpecifier(p, name)) continue;
|
|
133
|
+
const ver = depVersions[name];
|
|
134
|
+
if (!ver) continue;
|
|
135
|
+
remap.set(p, name + '@' + ver + p.slice(name.length));
|
|
136
|
+
}
|
|
137
|
+
if (remap.size === 0) return src;
|
|
138
|
+
|
|
139
|
+
let out = src;
|
|
140
|
+
for (const [from, to] of remap) {
|
|
141
|
+
const q = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
|
+
// Anchor on the import/export/require form so an identical non-import
|
|
143
|
+
// string literal is not touched: `from 'x'`, `import 'x'`, `import('x')`,
|
|
144
|
+
// `require('x')`. The optional `(` covers dynamic import / require. The
|
|
145
|
+
// leading `(^|[^\w.$])` boundary keeps a method call (`db.select().from('x')`,
|
|
146
|
+
// a `.import(...)` member) or a keyword-suffixed identifier (`xfrom 'x'`)
|
|
147
|
+
// from matching, which an unanchored keyword would wrongly rewrite.
|
|
148
|
+
const re = new RegExp("(^|[^\\w.$])((?:from|import|require)\\s*\\(?\\s*)(['\"])" + q + "\\3", 'g');
|
|
149
|
+
out = out.replace(re, (_m, pre, lead, quote) => pre + lead + quote + to + quote);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
package/src/dev.js
CHANGED
|
@@ -268,6 +268,32 @@ export async function readSeedEnabled(appDir) {
|
|
|
268
268
|
return true;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Read the Bun zero-install version-pinning switch (#685). The `WEBJS_PIN` env
|
|
273
|
+
* override wins (same grammar as `WEBJS_SEED`), otherwise
|
|
274
|
+
* `{ "webjs": { "pin": false } }` disables it. Default ON (opt-out). Consulted
|
|
275
|
+
* only on Bun: the rewrite injects inline-version specifiers that Bun's
|
|
276
|
+
* auto-install honors so zero-install fetches the pinned version, not latest
|
|
277
|
+
* (#684). Node zero-install pinning is #669's separate concern.
|
|
278
|
+
* @param {string} appDir
|
|
279
|
+
* @returns {Promise<boolean>}
|
|
280
|
+
*/
|
|
281
|
+
export async function readPinEnabled(appDir) {
|
|
282
|
+
const raw = process.env.WEBJS_PIN;
|
|
283
|
+
if (raw != null && raw !== '') {
|
|
284
|
+
const v = String(raw).trim().toLowerCase();
|
|
285
|
+
if (v === '0' || v === 'false' || v === 'off' || v === 'no') return false;
|
|
286
|
+
if (v === '1' || v === 'true' || v === 'on' || v === 'yes') return true;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const pkg = JSON.parse(await readFile(join(appDir, 'package.json'), 'utf8'));
|
|
290
|
+
if (pkg && pkg.webjs && pkg.webjs.pin === false) return false;
|
|
291
|
+
} catch {
|
|
292
|
+
// No package.json, malformed JSON, or unreadable. Keep the default.
|
|
293
|
+
}
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
271
297
|
/**
|
|
272
298
|
* Read the client-router switch (`webjs.clientRouter`) from the app's
|
|
273
299
|
* package.json (#629). Default `true`: the client router auto-enables in the
|
|
@@ -568,7 +594,11 @@ export async function createRequestHandler(opts) {
|
|
|
568
594
|
// Read once (not per-rebuild): the hook is global and cannot be cleanly
|
|
569
595
|
// un-installed, so toggling needs a restart. Disabled -> no hook, no ambient
|
|
570
596
|
// collector wrap in ssr.js, and module loading stays byte-identical.
|
|
571
|
-
|
|
597
|
+
// Pinning (#685) is Bun-only and independent of seeding, so install the hook
|
|
598
|
+
// when EITHER is on (on Bun they share one onLoad).
|
|
599
|
+
const seedEnabled = await readSeedEnabled(appDir);
|
|
600
|
+
const pinEnabled = serverRuntime() === 'bun' && await readPinEnabled(appDir);
|
|
601
|
+
if (seedEnabled || pinEnabled) await registerSeedHooks({ appDir, seedEnabled, pinEnabled });
|
|
572
602
|
|
|
573
603
|
// When an app commits a vendor pin (.webjs/vendor/importmap.json) it carries a
|
|
574
604
|
// deterministic vendor map that is cheap to read (one file, no analysis, no
|
package/webjs-config.schema.json
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
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
|
+
},
|
|
20
25
|
"clientRouter": {
|
|
21
26
|
"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.",
|
|
22
27
|
"type": "boolean",
|