@webjsdev/server 0.8.8 → 0.8.10
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/index.js +3 -0
- package/package.json +1 -1
- package/src/actions.js +37 -5
- package/src/api.js +16 -1
- package/src/auth.js +18 -3
- package/src/body-limit.js +291 -0
- package/src/check.js +41 -350
- package/src/context.js +66 -16
- package/src/cors.js +245 -0
- package/src/csp.js +218 -0
- package/src/dev.js +215 -10
- package/src/env-schema.js +250 -0
- package/src/headers.js +213 -0
- package/src/json.js +11 -2
- package/src/node-version.js +102 -0
- package/src/page-action.js +225 -0
- package/src/ssr.js +41 -23
package/src/ssr.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { renderToString, isNotFound, isRedirect, lookupModuleUrl, isLazy } from '@webjsdev/core';
|
|
3
|
+
import { renderToString, isNotFound, isRedirect, lookupModuleUrl, isLazy, cspNonce } from '@webjsdev/core';
|
|
4
4
|
import { importMapTag, vendorIntegrityFor, publishedBuildId } from './importmap.js';
|
|
5
5
|
import { jsonForScriptTag } from './script-tag-json.js';
|
|
6
6
|
import { readToken, newToken, cookieHeader } from './csrf.js';
|
|
@@ -20,7 +20,7 @@ import { transitiveDeps } from './module-graph.js';
|
|
|
20
20
|
* @param {import('./router.js').PageRoute} route
|
|
21
21
|
* @param {Record<string,string>} params
|
|
22
22
|
* @param {URL} url
|
|
23
|
-
* @param {{ dev: boolean, appDir: string, req?: Request, moduleGraph?: import('./module-graph.js').ModuleGraph, serverFiles?: Map<string,string> | Set<string> }} opts
|
|
23
|
+
* @param {{ dev: boolean, appDir: string, req?: Request, moduleGraph?: import('./module-graph.js').ModuleGraph, serverFiles?: Map<string,string> | Set<string>, actionData?: unknown, status?: number, pageModule?: Record<string, unknown> }} opts
|
|
24
24
|
* @returns {Promise<Response>}
|
|
25
25
|
*/
|
|
26
26
|
export async function ssrPage(route, params, url, opts) {
|
|
@@ -28,6 +28,12 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
28
28
|
params,
|
|
29
29
|
searchParams: Object.fromEntries(url.searchParams.entries()),
|
|
30
30
|
url: url.toString(),
|
|
31
|
+
// Populated only when this render is the re-render after a failed page
|
|
32
|
+
// `action` submission (#244). The page function and every layout receive
|
|
33
|
+
// it so they can surface field errors and repopulate inputs from the
|
|
34
|
+
// user's submitted values. Undefined on a normal GET render, so GET output
|
|
35
|
+
// is byte-identical to before this feature.
|
|
36
|
+
actionData: opts.actionData,
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
// Collect metadata across layouts (outermost first) then page.
|
|
@@ -46,7 +52,7 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
46
52
|
const have = haveHeader
|
|
47
53
|
? new Set(haveHeader.split(',').map((s) => s.trim()).filter(Boolean))
|
|
48
54
|
: null;
|
|
49
|
-
const body = await renderChain(route, ctx, opts.dev, suspenseCtx, have);
|
|
55
|
+
const body = await renderChain(route, ctx, opts.dev, suspenseCtx, have, opts.pageModule);
|
|
50
56
|
// Module URLs for the page + every layout in its chain. These ride
|
|
51
57
|
// the importmap; the browser fetches each file as it walks the
|
|
52
58
|
// import graph. Combined with the modulepreload hints below, this
|
|
@@ -102,7 +108,10 @@ export async function ssrPage(route, params, url, opts) {
|
|
|
102
108
|
streamBody,
|
|
103
109
|
closer,
|
|
104
110
|
suspenseCtx,
|
|
105
|
-
200
|
|
111
|
+
// Normally 200. After a failed page `action` submission the caller passes
|
|
112
|
+
// 422 (or another 4xx) so the re-rendered page with field errors carries
|
|
113
|
+
// the right status for both the no-JS reload and the enhanced swap (#244).
|
|
114
|
+
opts.status || 200,
|
|
106
115
|
opts.req,
|
|
107
116
|
url,
|
|
108
117
|
metadata,
|
|
@@ -209,8 +218,12 @@ async function ssrNotFoundHtml(notFoundFile, opts) {
|
|
|
209
218
|
});
|
|
210
219
|
}
|
|
211
220
|
|
|
212
|
-
async function renderChain(route, ctx, dev, suspenseCtx, have) {
|
|
213
|
-
|
|
221
|
+
async function renderChain(route, ctx, dev, suspenseCtx, have, pageModule) {
|
|
222
|
+
// Reuse a caller-supplied page module when present (the page-action
|
|
223
|
+
// re-render passes the exact module whose `action` just ran, so the
|
|
224
|
+
// failure re-render shares that single evaluation instead of re-importing
|
|
225
|
+
// and re-running the module's top-level side effects).
|
|
226
|
+
const page = pageModule || await loadModule(route.file, dev);
|
|
214
227
|
if (!page.default) throw new Error(`Page ${route.file} must have a default export`);
|
|
215
228
|
let tree = await page.default(ctx);
|
|
216
229
|
|
|
@@ -1254,10 +1267,17 @@ function streamingHtmlResponse(prefix, bodyHtml, closer, ctx, status, req, url,
|
|
|
1254
1267
|
}
|
|
1255
1268
|
|
|
1256
1269
|
/**
|
|
1270
|
+
* Import a route module. In prod the URL is stable so Node's module cache
|
|
1271
|
+
* serves a single evaluation; in dev a cache-bust query forces a fresh
|
|
1272
|
+
* evaluation so source edits take effect (which also re-runs the module's
|
|
1273
|
+
* top-level side effects, the reason pages/layouts must keep their top level
|
|
1274
|
+
* side-effect-free). Exported so page-action.js loads the page module the same
|
|
1275
|
+
* way the SSR re-render does.
|
|
1276
|
+
*
|
|
1257
1277
|
* @param {string} file
|
|
1258
1278
|
* @param {boolean} dev
|
|
1259
1279
|
*/
|
|
1260
|
-
async function loadModule(file, dev) {
|
|
1280
|
+
export async function loadModule(file, dev) {
|
|
1261
1281
|
const url = pathToFileURL(file).toString();
|
|
1262
1282
|
const bust = dev ? `?t=${Date.now()}-${Math.random().toString(36).slice(2)}` : '';
|
|
1263
1283
|
return import(url + bust);
|
|
@@ -1275,26 +1295,24 @@ function toUrlPath(file, appDir) {
|
|
|
1275
1295
|
}
|
|
1276
1296
|
|
|
1277
1297
|
/**
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1298
|
+
* The CSP nonce for the in-flight request, or undefined if none is in
|
|
1299
|
+
* scope. Delegates to `cspNonce()`, which returns the per-request nonce
|
|
1300
|
+
* the handler MINTED when CSP is enabled (issue #233), or, as a fallback,
|
|
1301
|
+
* the nonce parsed from an inbound `Content-Security-Policy` request
|
|
1302
|
+
* header (the legacy consume-only path). Using the same source as the
|
|
1303
|
+
* `Content-Security-Policy` response header is what guarantees the inline
|
|
1304
|
+
* boot script, the importmap, the modulepreload hints, and the header all
|
|
1305
|
+
* carry the EXACT same nonce: one minted value, no drift.
|
|
1280
1306
|
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
* nonce across `script-src` and `style-src` (a per-request
|
|
1285
|
-
* single-nonce model), and webjs only emits `<script>` /
|
|
1286
|
-
* `<link rel="modulepreload">` tags, so reading the first match
|
|
1287
|
-
* is the right behaviour. A future caller that emits styled
|
|
1288
|
-
* inline content under a separate style nonce would need to
|
|
1289
|
-
* extend this to be directive-scoped.
|
|
1307
|
+
* `req` is accepted (and ignored) so existing call sites stay unchanged;
|
|
1308
|
+
* the value comes from the request-scoped AsyncLocalStorage store, not
|
|
1309
|
+
* the argument.
|
|
1290
1310
|
*
|
|
1291
|
-
* @param {Request}
|
|
1311
|
+
* @param {Request} [_req]
|
|
1292
1312
|
* @returns {string | undefined}
|
|
1293
1313
|
*/
|
|
1294
|
-
function getNonce(
|
|
1295
|
-
|
|
1296
|
-
const match = /\bnonce-([A-Za-z0-9+/=]+)/.exec(csp);
|
|
1297
|
-
return match ? match[1] : undefined;
|
|
1314
|
+
function getNonce(_req) {
|
|
1315
|
+
return cspNonce() || undefined;
|
|
1298
1316
|
}
|
|
1299
1317
|
|
|
1300
1318
|
/** @param {string} s */
|