@webjsdev/server 0.8.28 → 0.8.30
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 +67 -0
- package/src/action-seed.js +88 -35
- package/src/actions.js +2 -2
- package/src/cache-fn.js +2 -2
- package/src/cache-tags.js +1 -1
- package/src/dev.js +8 -6
- package/src/env-schema.js +1 -1
- package/src/json.js +1 -1
- package/src/vendor.js +1 -1
- package/webjs-config.schema.json +31 -2
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun install for SSR action-result seeding (#472, #529).
|
|
3
|
+
*
|
|
4
|
+
* Bun has no `module.registerHooks`, so the seed facade is installed via a
|
|
5
|
+
* `Bun.plugin` `onLoad`, the Bun analog of the Node load hook in
|
|
6
|
+
* `action-seed.js`. This module is dynamically imported by `registerSeedHooks`
|
|
7
|
+
* ONLY when `serverRuntime()` is `'bun'`, so the `Bun.*` global is never
|
|
8
|
+
* referenced on Node (the same isolation as `listener-bun.js`).
|
|
9
|
+
*
|
|
10
|
+
* The faceting DECISION (`isSeedCandidate`) and the facade SOURCE
|
|
11
|
+
* (`buildSeedFacade`) are the shared, runtime-neutral helpers passed in, so Node
|
|
12
|
+
* and Bun emit byte-identical facades that feed the same `AsyncLocalStorage`
|
|
13
|
+
* collector. The only Bun-specific glue here is the plugin shell + reading the
|
|
14
|
+
* source via `Bun.file`.
|
|
15
|
+
*
|
|
16
|
+
* Bun's `onLoad` `args.path` includes any query (verified), so the facade's
|
|
17
|
+
* `?webjs-seed-orig` passthrough and a dev `?t=` re-import are both seen, exactly
|
|
18
|
+
* like the Node hook's URL.
|
|
19
|
+
*
|
|
20
|
+
* UNLIKE Node's hook, a Bun `onLoad` registered for a filter MUST return a
|
|
21
|
+
* `{ contents, loader }` object for EVERY match; returning `undefined` to defer
|
|
22
|
+
* to the default loader is an error ("onLoad() expects an object returned"). So
|
|
23
|
+
* the non-facet cases (the `?webjs-seed-orig` passthrough, a `.server.*` with no
|
|
24
|
+
* `'use server'`, an unenumerable `export *`) are handled by reading and
|
|
25
|
+
* RETURNING the real source ourselves with the extension's loader, which is the
|
|
26
|
+
* same module Bun would have loaded.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Install the `Bun.plugin` seed `onLoad`. Idempotency is the caller's
|
|
31
|
+
* responsibility (`registerSeedHooks` guards on `_registered`).
|
|
32
|
+
*
|
|
33
|
+
* @param {{
|
|
34
|
+
* isSeedCandidate: (specifier: string) => boolean,
|
|
35
|
+
* buildSeedFacade: (origSpec: string, absPath: string, src: string) => (string | null),
|
|
36
|
+
* serverFileRe: RegExp,
|
|
37
|
+
* }} helpers
|
|
38
|
+
*/
|
|
39
|
+
export function installBunSeedPlugin({ isSeedCandidate, buildSeedFacade, serverFileRe }) {
|
|
40
|
+
Bun.plugin({
|
|
41
|
+
name: 'webjs-action-seed',
|
|
42
|
+
setup(build) {
|
|
43
|
+
// The filter is a cheap path pre-screen (`*.server.*`, optional query).
|
|
44
|
+
build.onLoad({ filter: serverFileRe }, async (args) => {
|
|
45
|
+
const absPath = args.path.split('?')[0];
|
|
46
|
+
// `.ts` / `.mts` strip via the `ts` loader; `.js` / `.mjs` via `js`.
|
|
47
|
+
const loader = /\.m?ts$/.test(absPath) ? 'ts' : 'js';
|
|
48
|
+
// Read the real source. A genuine read failure (missing file) propagates,
|
|
49
|
+
// which Bun reports as a load error exactly as it would without the plugin.
|
|
50
|
+
const src = await Bun.file(absPath).text();
|
|
51
|
+
try {
|
|
52
|
+
// Facet only a `'use server'` candidate (not the `?webjs-seed-orig`
|
|
53
|
+
// passthrough); a non-candidate or a passthrough falls through to the
|
|
54
|
+
// raw source below.
|
|
55
|
+
if (isSeedCandidate(args.path)) {
|
|
56
|
+
const source = buildSeedFacade(args.path, absPath, src);
|
|
57
|
+
if (source != null) return { contents: source, loader: 'js' };
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Fail-open: any faceting error serves the raw source (no seeding for
|
|
61
|
+
// this module), the Bun analog of the Node hook's `nextLoad` fallback.
|
|
62
|
+
}
|
|
63
|
+
return { contents: src, loader };
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
package/src/action-seed.js
CHANGED
|
@@ -18,11 +18,18 @@
|
|
|
18
18
|
* The framework promise is "what you write is what you see in the browser source
|
|
19
19
|
* tab", not "what you write is the exact function object that runs server-side"
|
|
20
20
|
* (the RPC stub already replaces the action on the client). So we install a
|
|
21
|
-
* SERVER-SIDE transparent facade
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* SERVER-SIDE transparent facade at module load: for a `'use server'`
|
|
22
|
+
* `*.server.*` module, the load hook returns a facade that re-exports each
|
|
23
|
+
* function wrapped in a `Proxy`. The Proxy records `(file, fn, args) -> result`
|
|
24
|
+
* into an ambient `AsyncLocalStorage` collector WHENEVER a collector is active,
|
|
25
|
+
* and is a pure passthrough otherwise.
|
|
26
|
+
*
|
|
27
|
+
* The facade SOURCE and the wrapping (`__seedWrap`, `buildSeedFacade`) are
|
|
28
|
+
* runtime-neutral; only the INSTALL mechanism differs by runtime (`#529`), chosen
|
|
29
|
+
* by `serverRuntime()`: Node uses the synchronous `module.registerHooks` load
|
|
30
|
+
* hook (Node 24+, main-thread); Bun uses a `Bun.plugin` `onLoad` (Bun has no
|
|
31
|
+
* `module.registerHooks`). Both feed the same `AsyncLocalStorage` collector and
|
|
32
|
+
* emit the same seed wire, so a page seeds identically on either runtime.
|
|
26
33
|
*
|
|
27
34
|
* Recording is gated entirely by the ALS collector, which is established ONLY
|
|
28
35
|
* around the SSR page render (`collectSeeds`). The RPC endpoint path runs with
|
|
@@ -48,6 +55,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
48
55
|
import { stringify } from '@webjsdev/core';
|
|
49
56
|
import { hashFile } from './actions.js';
|
|
50
57
|
import { isStreamable } from './action-stream.js';
|
|
58
|
+
import { serverRuntime } from './listener-core.js';
|
|
51
59
|
|
|
52
60
|
/** Ambient per-render seed collector. `Map<key, value>` or undefined. */
|
|
53
61
|
const als = new AsyncLocalStorage();
|
|
@@ -205,6 +213,17 @@ function buildFacade(origUrl, absPath, exports) {
|
|
|
205
213
|
const file = JSON.stringify(absPath);
|
|
206
214
|
let out = `import * as __orig from ${origSpec};\n`;
|
|
207
215
|
out += `import { __seedWrap as __w } from ${JSON.stringify(SELF_URL)};\n`;
|
|
216
|
+
// Fail-open catch-all (#535). Re-export every named binding of the real module.
|
|
217
|
+
// An explicit `export const NAME = __w(...)` below SHADOWS the matching star
|
|
218
|
+
// binding (an explicit export wins over a star re-export of the same name, with
|
|
219
|
+
// no SyntaxError), so an enumerated export is still wrapped and seeded. A named
|
|
220
|
+
// export the `extractExportNames` regex MISSED (exotic syntax, an unusual
|
|
221
|
+
// re-export form, a codegen-produced export) is NOT enumerated below, so it
|
|
222
|
+
// flows through this star unwrapped: it resolves and works over a normal RPC,
|
|
223
|
+
// just is not seeded, instead of being dropped and crashing the importer with
|
|
224
|
+
// `undefined`. `export *` does not carry `default`, which the explicit default
|
|
225
|
+
// line below still handles.
|
|
226
|
+
out += `export * from ${origSpec};\n`;
|
|
208
227
|
for (const n of exports.names) {
|
|
209
228
|
const k = JSON.stringify(n);
|
|
210
229
|
out += `export const ${n} = __w(${file}, ${k}, __orig[${k}]);\n`;
|
|
@@ -215,13 +234,46 @@ function buildFacade(origUrl, absPath, exports) {
|
|
|
215
234
|
return out;
|
|
216
235
|
}
|
|
217
236
|
|
|
218
|
-
/** Match `*.server.{js,ts,mjs,mts}` (optionally with a query). */
|
|
219
|
-
const SERVER_FILE_RE = /\.server\.m?[jt]s(\?|$)/;
|
|
237
|
+
/** Match `*.server.{js,ts,mjs,mts}` (optionally with a query). Also the Bun plugin filter. */
|
|
238
|
+
export const SERVER_FILE_RE = /\.server\.m?[jt]s(\?|$)/;
|
|
220
239
|
/** The `'use server'` directive in the file head. */
|
|
221
240
|
const USE_SERVER_RE = /^\s*(['"])use server\1\s*;?\s*$/m;
|
|
222
241
|
|
|
223
242
|
/**
|
|
224
|
-
*
|
|
243
|
+
* Whether a load specifier (a Node file URL or a Bun file path, with an optional
|
|
244
|
+
* query) is a faceting candidate: a `*.server.*` module that is NOT the facade's
|
|
245
|
+
* own `?webjs-seed-orig` passthrough of the real module. Runtime-neutral.
|
|
246
|
+
* @param {string} specifier
|
|
247
|
+
* @returns {boolean}
|
|
248
|
+
*/
|
|
249
|
+
export function isSeedCandidate(specifier) {
|
|
250
|
+
return SERVER_FILE_RE.test(specifier) && !specifier.includes('webjs-seed-orig');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build the wrapping facade for a candidate module's source, or null to pass it
|
|
255
|
+
* through unwrapped (no `'use server'`, a non-enumerable `export *`, or no
|
|
256
|
+
* exports). Runtime-neutral: the caller passes the load specifier (URL on Node,
|
|
257
|
+
* path on Bun) used as the facade's `?webjs-seed-orig` import base, plus the
|
|
258
|
+
* absolute file path (the hash basis) and the already-read source.
|
|
259
|
+
* @param {string} origSpec the real module's load specifier (URL or path), no seed query
|
|
260
|
+
* @param {string} absPath the real module's absolute file path
|
|
261
|
+
* @param {string} src the module source
|
|
262
|
+
* @returns {string | null} facade source, or null for passthrough
|
|
263
|
+
*/
|
|
264
|
+
export function buildSeedFacade(origSpec, absPath, src) {
|
|
265
|
+
const head = src.split('\n').slice(0, 5).join('\n');
|
|
266
|
+
if (!USE_SERVER_RE.test(head)) return null;
|
|
267
|
+
const exports = extractExportNames(src);
|
|
268
|
+
// A `export *` re-export cannot be enumerated; skip faceting (passthrough)
|
|
269
|
+
// so we never emit a facade that silently drops re-exported bindings.
|
|
270
|
+
if (exports.hasStar) return null;
|
|
271
|
+
if (exports.names.length === 0 && !exports.hasDefault) return null;
|
|
272
|
+
return buildFacade(origSpec, absPath, exports);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The synchronous `module.registerHooks` load hook (Node). For a `'use server'`
|
|
225
277
|
* `*.server.*` module it returns a wrapping facade; for everything else
|
|
226
278
|
* (including the `?webjs-seed-orig` passthrough of the real module) it defers to
|
|
227
279
|
* `nextLoad`. Fail-open: any error defers to `nextLoad`, so a load that the hook
|
|
@@ -232,19 +284,11 @@ const USE_SERVER_RE = /^\s*(['"])use server\1\s*;?\s*$/m;
|
|
|
232
284
|
*/
|
|
233
285
|
function seedLoadHook(url, context, nextLoad) {
|
|
234
286
|
try {
|
|
235
|
-
if (!
|
|
236
|
-
// The facade's own `?webjs-seed-orig` import must load the REAL module.
|
|
237
|
-
if (url.includes('webjs-seed-orig')) return nextLoad(url, context);
|
|
287
|
+
if (!isSeedCandidate(url)) return nextLoad(url, context);
|
|
238
288
|
const absPath = fileURLToPath(url.split('?')[0]);
|
|
239
289
|
const src = readFileSync(absPath, 'utf8');
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
const exports = extractExportNames(src);
|
|
243
|
-
// A `export *` re-export cannot be enumerated; skip faceting (passthrough)
|
|
244
|
-
// so we never emit a facade that silently drops re-exported bindings.
|
|
245
|
-
if (exports.hasStar) return nextLoad(url, context);
|
|
246
|
-
if (exports.names.length === 0 && !exports.hasDefault) return nextLoad(url, context);
|
|
247
|
-
const source = buildFacade(url, absPath, exports);
|
|
290
|
+
const source = buildSeedFacade(url, absPath, src);
|
|
291
|
+
if (source == null) return nextLoad(url, context);
|
|
248
292
|
return { source, format: 'module', shortCircuit: true };
|
|
249
293
|
} catch {
|
|
250
294
|
return nextLoad(url, context);
|
|
@@ -253,27 +297,36 @@ function seedLoadHook(url, context, nextLoad) {
|
|
|
253
297
|
|
|
254
298
|
/**
|
|
255
299
|
* Install the seed load hook (idempotent). Called once at boot from `dev.js`
|
|
256
|
-
* when seeding is enabled, BEFORE any action module is imported (
|
|
257
|
-
*
|
|
258
|
-
*
|
|
300
|
+
* when seeding is enabled, BEFORE any action module is imported (a module loaded
|
|
301
|
+
* before the hook would already be cached unwrapped). The install mechanism is
|
|
302
|
+
* chosen by `serverRuntime()` (#529): Node's synchronous `module.registerHooks`,
|
|
303
|
+
* or a `Bun.plugin` `onLoad` on Bun. A no-op on a second call. Async because the
|
|
304
|
+
* Bun path dynamically imports `action-seed-bun.js` (so the `Bun.*` global is
|
|
305
|
+
* never referenced on Node); the Node path resolves synchronously.
|
|
306
|
+
* @returns {Promise<void>}
|
|
259
307
|
*/
|
|
260
|
-
export function registerSeedHooks() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
308
|
+
export async function registerSeedHooks() {
|
|
309
|
+
if (_registered) return;
|
|
310
|
+
_registered = true;
|
|
311
|
+
|
|
312
|
+
if (serverRuntime() === 'bun') {
|
|
313
|
+
// Bun has no module.registerHooks; install the same facade via Bun.plugin.
|
|
314
|
+
const { installBunSeedPlugin } = await import('./action-seed-bun.js');
|
|
315
|
+
installBunSeedPlugin({ isSeedCandidate, buildSeedFacade, serverFileRe: SERVER_FILE_RE });
|
|
316
|
+
_enabled = true;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// A runtime that is neither Node-with-registerHooks nor Bun: seeding stays
|
|
321
|
+
// OFF (fail-open). `seedingEnabled()` is false, ssr.js emits no seed block,
|
|
322
|
+
// and the client RPC stub falls back to a normal fetch. Never wrong data.
|
|
266
323
|
if (typeof nodeModule.registerHooks !== 'function') {
|
|
267
|
-
|
|
268
|
-
_registered = true;
|
|
269
|
-
console.warn('[webjs] SSR action-result seeding (#472) is disabled: module.registerHooks is unavailable on this runtime (e.g. Bun). Async-render components will re-fetch on hydration; no correctness impact.');
|
|
270
|
-
}
|
|
324
|
+
console.warn('[webjs] SSR action-result seeding (#472) is disabled: this runtime has neither module.registerHooks nor Bun.plugin. Async-render components re-fetch on hydration; no correctness impact.');
|
|
271
325
|
return;
|
|
272
326
|
}
|
|
273
|
-
|
|
274
|
-
if (_registered) return;
|
|
275
|
-
_registered = true;
|
|
327
|
+
|
|
276
328
|
nodeModule.registerHooks({ load: seedLoadHook });
|
|
329
|
+
_enabled = true;
|
|
277
330
|
}
|
|
278
331
|
|
|
279
332
|
/**
|
package/src/actions.js
CHANGED
|
@@ -150,7 +150,7 @@ async function rpcResponse(payload, init = {}) {
|
|
|
150
150
|
* module (the hash index is all the analysis needs; a module is imported on
|
|
151
151
|
* demand by `invokeAction` / `serveActionStub`), so it is safe for a read-only
|
|
152
152
|
* introspection caller (the MCP `list_actions` tool, #262) with no top-level
|
|
153
|
-
* side effects (
|
|
153
|
+
* side effects (DB driver init, DB connect).
|
|
154
154
|
*
|
|
155
155
|
* @param {string} appDir
|
|
156
156
|
* @param {boolean} dev
|
|
@@ -181,7 +181,7 @@ export async function buildActionIndex(appDir, dev) {
|
|
|
181
181
|
// Pure-RPC actions are NOT executed at boot: invokeAction and
|
|
182
182
|
// serveActionStub import the module on demand (first RPC call / first stub
|
|
183
183
|
// fetch), so the hash index above is all the analysis needs. Eagerly
|
|
184
|
-
// running every server module (and its transitive
|
|
184
|
+
// running every server module (and its transitive DB driver init, DB
|
|
185
185
|
// connects, etc.) would be wasted work.
|
|
186
186
|
}
|
|
187
187
|
|
package/src/cache-fn.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { cache } from '@webjsdev/server';
|
|
9
9
|
*
|
|
10
10
|
* export const listPosts = cache(
|
|
11
|
-
* async () =>
|
|
11
|
+
* async () => db.query.posts.findMany({ orderBy: { createdAt: 'desc' } }),
|
|
12
12
|
* { key: 'posts', ttl: 60 }
|
|
13
13
|
* );
|
|
14
14
|
*
|
|
@@ -107,7 +107,7 @@ export function cache(fn, opts) {
|
|
|
107
107
|
*
|
|
108
108
|
* ```js
|
|
109
109
|
* export async function createPost(input) {
|
|
110
|
-
* await
|
|
110
|
+
* await db.insert(posts).values(input);
|
|
111
111
|
* await listPosts.invalidate();
|
|
112
112
|
* }
|
|
113
113
|
* ```
|
package/src/cache-tags.js
CHANGED
|
@@ -104,7 +104,7 @@ export async function addKeyToTags(tags, cacheKey, ttlMs) {
|
|
|
104
104
|
* 'use server';
|
|
105
105
|
* import { revalidateTag } from '@webjsdev/server';
|
|
106
106
|
* export async function createComment(input) {
|
|
107
|
-
* await
|
|
107
|
+
* await db.insert(comments).values(input);
|
|
108
108
|
* await revalidateTag('post:' + input.postId); // postById(postId) recomputes
|
|
109
109
|
* return { success: true };
|
|
110
110
|
* }
|
package/src/dev.js
CHANGED
|
@@ -508,7 +508,7 @@ export async function createRequestHandler(opts) {
|
|
|
508
508
|
// Read once (not per-rebuild): the hook is global and cannot be cleanly
|
|
509
509
|
// un-installed, so toggling needs a restart. Disabled -> no hook, no ambient
|
|
510
510
|
// collector wrap in ssr.js, and module loading stays byte-identical.
|
|
511
|
-
if (await readSeedEnabled(appDir)) registerSeedHooks();
|
|
511
|
+
if (await readSeedEnabled(appDir)) await registerSeedHooks();
|
|
512
512
|
|
|
513
513
|
// When an app commits a vendor pin (.webjs/vendor/importmap.json) it carries a
|
|
514
514
|
// deterministic vendor map that is cheap to read (one file, no analysis, no
|
|
@@ -1353,16 +1353,18 @@ export async function createRequestHandler(opts) {
|
|
|
1353
1353
|
* artefact dir (the #258 routes.d.ts and the vendor pin) that the dev server
|
|
1354
1354
|
* itself writes on startup and on every rebuild, so without this skip the
|
|
1355
1355
|
* write fires a watch event, triggers a rebuild, re-writes the file, and loops
|
|
1356
|
-
* forever. `
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1356
|
+
* forever. `db/dev.db*` (the SQLite file + sidecars) and `db/migrations`
|
|
1357
|
+
* (drizzle-kit output) churn during db:migrate. The `db/dev.db` branch is
|
|
1358
|
+
* prefix-only (no trailing separator) so the `-journal` / `-wal` sidecars match
|
|
1359
|
+
* too, while staying anchored to `db/dev.db` so a SOURCE file like
|
|
1360
|
+
* `db/schema.server.ts` still triggers a reload. The others stay
|
|
1359
1361
|
* separator-anchored so an unrelated name like `node_modules.bak/foo` does not.
|
|
1360
1362
|
*
|
|
1361
1363
|
* @param {string} filename relative path from an fs.watch `event.filename`
|
|
1362
1364
|
* @returns {boolean} true when the change should be ignored
|
|
1363
1365
|
*/
|
|
1364
1366
|
export function shouldIgnoreWatchPath(filename) {
|
|
1365
|
-
return /(?:^|[\\/])(?:node_modules|\.git|\.webjs)(?:[\\/]|$)|(?:^|[\\/])
|
|
1367
|
+
return /(?:^|[\\/])(?:node_modules|\.git|\.webjs)(?:[\\/]|$)|(?:^|[\\/])db[\\/](?:dev\.db|migrations)/.test(filename || '');
|
|
1366
1368
|
}
|
|
1367
1369
|
|
|
1368
1370
|
/**
|
|
@@ -1414,7 +1416,7 @@ export async function startServer(opts) {
|
|
|
1414
1416
|
//
|
|
1415
1417
|
// fs.watch returns relative paths in event.filename. `shouldIgnoreWatchPath`
|
|
1416
1418
|
// (module-level, exported for tests) skips node_modules, .git, .webjs/, and
|
|
1417
|
-
//
|
|
1419
|
+
// the SQLite dev DB (db/dev.db) + db/migrations so a file the dev server itself writes never loops.
|
|
1418
1420
|
const rebuild = debounce(() => app.rebuild(), 80);
|
|
1419
1421
|
watcherAbort = new AbortController();
|
|
1420
1422
|
(async () => {
|
package/src/env-schema.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* webjs auto-loads `<appDir>/.env` into `process.env` at boot, but does no
|
|
5
5
|
* validation, so a missing or misconfigured required var (DATABASE_URL,
|
|
6
|
-
* AUTH_SECRET, ...) fails late and cryptically (a
|
|
6
|
+
* AUTH_SECRET, ...) fails late and cryptically (a database connect error
|
|
7
7
|
* mid-request, an undefined secret signing a token). This module adds an
|
|
8
8
|
* optional boot-time validation hook that fails fast with one clear message
|
|
9
9
|
* listing EVERY missing or invalid var at once, before the app serves a
|
package/src/json.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readTextBounded, BodyLimitError, DEFAULT_MAX_BODY_BYTES } from './body-
|
|
|
9
9
|
* // GET /api/posts
|
|
10
10
|
* import { json } from '@webjsdev/server';
|
|
11
11
|
* export async function GET() {
|
|
12
|
-
* return json(await listPosts()); // plain
|
|
12
|
+
* return json(await listPosts()); // plain Drizzle rows with Date columns
|
|
13
13
|
* }
|
|
14
14
|
*
|
|
15
15
|
* The helper reads the in-flight Request from the AsyncLocalStorage
|
package/src/vendor.js
CHANGED
|
@@ -409,7 +409,7 @@ async function jspmCall(installs, provider) {
|
|
|
409
409
|
// A 5xx/429 is a transient jspm problem worth retrying. A 401/4xx
|
|
410
410
|
// means at least one install is genuinely unresolvable (jspm uses
|
|
411
411
|
// 401 for that): a private / workspace / server-only package (e.g.
|
|
412
|
-
// @webjsdev/server,
|
|
412
|
+
// @webjsdev/server, better-sqlite3) the browser never fetches
|
|
413
413
|
// anyway. Permanent failures must NOT block readiness.
|
|
414
414
|
const transient = response.status >= 500 || response.status === 429;
|
|
415
415
|
return { ok: false, imports: {}, transient };
|
package/webjs-config.schema.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://webjs.dev/schemas/webjs-config.schema.json",
|
|
4
4
|
"title": "webjs config block",
|
|
5
|
-
"description": "Schema for the `webjs` object in a webjs app's package.json. Each key maps to a documented server reader. An unknown key is flagged (additionalProperties is false) so a typo no longer silently falls back to the default.",
|
|
6
|
-
"$comment": "Single source of truth lives in THREE co-located places that must stay in lockstep: this schema, the TS type at packages/core/src/webjs-config.d.ts, and the
|
|
5
|
+
"description": "Schema for the `webjs` object in a webjs app's package.json. Each key maps to a documented reader (a server reader, or for `dev`/`start` the CLI's readAppTasks). An unknown key is flagged (additionalProperties is false) so a typo no longer silently falls back to the default.",
|
|
6
|
+
"$comment": "Single source of truth lives in THREE co-located places that must stay in lockstep: this schema, the TS type at packages/core/src/webjs-config.d.ts, and the reader functions (readElideEnabled in dev.js, readSeedEnabled in dev.js, compileHeaderRules in headers.js, compileRedirectRules / readTrailingSlashPolicy in redirects.js, readBasePath in base-path.js, readCspConfig in csp.js, readBodyLimits / computeServerTimeouts in body-limit.js, and the CLI's readAppTasks in packages/cli/lib/app-tasks.js for dev/start, #550). Adding a webjs.* key means updating all three. See packages/server/AGENTS.md for the one documented procedure.",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
9
9
|
"properties": {
|
|
@@ -147,6 +147,35 @@
|
|
|
147
147
|
"type": "integer",
|
|
148
148
|
"minimum": 0,
|
|
149
149
|
"default": 5000
|
|
150
|
+
},
|
|
151
|
+
"dev": {
|
|
152
|
+
"description": "Dev task orchestration (#550). Read by the CLI (readAppTasks in packages/cli/lib/app-tasks.js), NOT the server. webjs dev runs `before` (one-shot, e.g. webjs db migrate) to completion, then `parallel` (long-lived watchers, e.g. tailwindcss --watch) alongside the server. Replaces the old predev hook + concurrently watchers.",
|
|
153
|
+
"type": "object",
|
|
154
|
+
"additionalProperties": false,
|
|
155
|
+
"properties": {
|
|
156
|
+
"before": {
|
|
157
|
+
"description": "One-shot commands run sequentially to completion before the dev server boots. A non-zero exit aborts the boot.",
|
|
158
|
+
"type": "array",
|
|
159
|
+
"items": { "type": "string" }
|
|
160
|
+
},
|
|
161
|
+
"parallel": {
|
|
162
|
+
"description": "Long-lived commands run as child processes alongside the dev server, torn down on exit so a watcher cannot leak past it.",
|
|
163
|
+
"type": "array",
|
|
164
|
+
"items": { "type": "string" }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"start": {
|
|
169
|
+
"description": "Start task orchestration (#550). Read by the CLI (readAppTasks in packages/cli/lib/app-tasks.js), NOT the server. webjs start runs `before` (one-shot, e.g. webjs db migrate) to completion before serving. Replaces the old prestart hook.",
|
|
170
|
+
"type": "object",
|
|
171
|
+
"additionalProperties": false,
|
|
172
|
+
"properties": {
|
|
173
|
+
"before": {
|
|
174
|
+
"description": "One-shot commands run sequentially to completion before the prod server boots. A non-zero exit aborts the boot.",
|
|
175
|
+
"type": "array",
|
|
176
|
+
"items": { "type": "string" }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
150
179
|
}
|
|
151
180
|
}
|
|
152
181
|
}
|