@bascik/adapter-cloudflare 1.0.0-rc.1 → 1.0.0-rc.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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * The generated public/_headers on Cloudflare.
3
+ *
4
+ * Static files are served by the asset layer without the Worker, so the
5
+ * headers the Worker sends on what it answers have to be written here for
6
+ * them: the security headers (`security.headers`, `security.hsts`) as one
7
+ * site-wide `/*` rule, and the opt-in `contentSecurityPolicy` as one
8
+ * exact-path rule per static page (from the same pkg function the Node
9
+ * servers and the Worker use, with the build's hashes). Responses the Worker
10
+ * makes itself never get `_headers` rules, and an asset it forwards gets them
11
+ * once, from the asset layer, so no header is sent twice.
12
+ *
13
+ * How Cloudflare reads the file (workers-shared, which miniflare runs): rules
14
+ * are a record keyed by path, so a second rule with a path the author used
15
+ * replaces the author's, at the place of the first; the rules that match a
16
+ * URL apply in file order; a header two of them set is joined with a comma;
17
+ * `! Name` in a rule removes what earlier rules set, before that rule's own
18
+ * values; a line over 2000 characters is skipped; and it stops reading at the
19
+ * 101st path line.
20
+ *
21
+ * So, whenever Bascik writes anything into the file:
22
+ * 1. the site-wide rule comes first (Bascik's, or the author's own `/*`
23
+ * moved to the top with Bascik's headers it does not set added);
24
+ * 2. the static pages' CSP rules come next, never as a second rule for a
25
+ * path the author has (the line goes into the author's rule), and not at
26
+ * all for a page an author rule already sets or removes the CSP header
27
+ * on: the author's value wins there;
28
+ * 3. the author's other rules follow, and each one that sets a security
29
+ * header (any Bascik can write, whatever the toggles say) or a header
30
+ * the author's `/*` sets gets a `! Name` line first: a later, more
31
+ * specific rule replaces the value instead of being joined to it, the
32
+ * same way under every security setting.
33
+ * When Bascik writes nothing (headers, HSTS and CSP all off), the author's
34
+ * file ships as written.
35
+ */
36
+ import { contentSecurityPolicyHeader, DEFAULT_RESPONSE_SECURITY, SECURITY_HEADERS, } from "@bascik/bascik/runtime";
37
+ /** Cloudflare reads at most this many rules (path lines) from _headers. */
38
+ export const CLOUDFLARE_HEADERS_RULE_LIMIT = 100;
39
+ /** Cloudflare skips any _headers line longer than this, with no error. */
40
+ export const CLOUDFLARE_HEADERS_LINE_LIMIT = 2000;
41
+ const SITE_WIDE_PATH = "/*";
42
+ /** How Cloudflare tells a path line from a header line (its own pattern). */
43
+ const PATH_LINE = /^([^\s]+:\/\/|\/)/;
44
+ const UNSET_PREFIX = "! ";
45
+ const CSP_NAMES = [
46
+ "content-security-policy",
47
+ "content-security-policy-report-only",
48
+ ];
49
+ /**
50
+ * Every header Bascik can write here, whatever `security` turns on or off:
51
+ * an author's rule that sets one replaces the earlier value under every
52
+ * setting, so a toggle never changes how the author's own rules combine.
53
+ */
54
+ const MANAGED_HEADERS = new Set([
55
+ ...Object.keys(SECURITY_HEADERS),
56
+ "strict-transport-security",
57
+ ...CSP_NAMES,
58
+ ]);
59
+ /** `x-frame-options` -> `X-Frame-Options`. Cloudflare matches names in any case. */
60
+ const headerName = (lower) => lower.replace(/(^|-)([a-z])/g, (_, dash, c) => `${dash}${c.toUpperCase()}`);
61
+ /** The rules of a _headers text, as Cloudflare reads them (indentation is not significant). */
62
+ const parseRules = (lines) => {
63
+ const rules = [];
64
+ let rule;
65
+ lines.forEach((raw, index) => {
66
+ const line = raw.trim();
67
+ if (line === "" || line.startsWith("#"))
68
+ return;
69
+ if (PATH_LINE.test(line)) {
70
+ if (rule)
71
+ rule.end = index;
72
+ rule = {
73
+ path: line,
74
+ line: index,
75
+ end: lines.length,
76
+ sets: new Set(),
77
+ unsets: new Set(),
78
+ };
79
+ rules.push(rule);
80
+ return;
81
+ }
82
+ if (!rule)
83
+ return;
84
+ if (line.startsWith(UNSET_PREFIX))
85
+ rule.unsets.add(line.slice(UNSET_PREFIX.length).trim().toLowerCase());
86
+ else if (line.includes(":"))
87
+ rule.sets.add(line.slice(0, line.indexOf(":")).trim().toLowerCase());
88
+ });
89
+ return rules;
90
+ };
91
+ const linesOf = (text) => text === "" ? [] : text.replace(/\s*$/, "").split(/\r?\n/);
92
+ /**
93
+ * How many rules Cloudflare reads from a _headers text: one per path line
94
+ * that has a header or a `! Name` under it. A path written twice counts
95
+ * twice, because Cloudflare stops at a count of path lines, not of paths.
96
+ */
97
+ export const countHeadersRules = (text) => parseRules(linesOf(text)).filter((rule) => rule.sets.size + rule.unsets.size)
98
+ .length;
99
+ /** Cloudflare's own pattern match (workers-shared generateRuleRegExp), for path rules. */
100
+ const ruleMatches = (rulePath, path) => {
101
+ if (!rulePath.startsWith("/"))
102
+ return false;
103
+ const source = rulePath
104
+ .split("*")
105
+ .map((part) => part.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
106
+ .join(".*")
107
+ .replace(/:[A-Za-z]\w*/g, "[^/]+");
108
+ try {
109
+ return new RegExp(`^${source}$`).test(path);
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ };
115
+ /** What to do when the static headers do not fit in _headers, for this variant. */
116
+ const remedy = (variant) => variant === "workers"
117
+ ? `Let the Worker send the headers on every response instead: in your wrangler.jsonc (or wrangler.json or wrangler.toml) set ` +
118
+ `"assets": { "run_worker_first": true }, and Bascik writes no _headers rules for them.`
119
+ : `Cloudflare Pages cannot run the Worker before static files, so build with --target cloudflare-workers and set ` +
120
+ `"assets": { "run_worker_first": true } in your wrangler.jsonc; the Worker then sends the headers on every response.`;
121
+ /**
122
+ * The whole public/_headers text: the site-wide security rule, the static
123
+ * pages' CSP rules, and the author's rules. The author's text unchanged when
124
+ * Bascik has nothing to write; "" when there is no file at all.
125
+ */
126
+ export const buildHeadersFile = ({ authored, security = DEFAULT_RESPONSE_SECURITY, base, csp, variant = "workers", }) => {
127
+ const lines = linesOf(authored);
128
+ const rules = parseRules(lines);
129
+ // Cloudflare keeps the last rule written for a path, at the place of the first.
130
+ const effective = new Map();
131
+ for (const rule of rules)
132
+ effective.set(rule.path, rule);
133
+ const authorSiteWide = effective.get(SITE_WIDE_PATH);
134
+ // name (lowercase) -> value, in the order Node sends them.
135
+ const siteWide = new Map(Object.entries(security.headers));
136
+ if (security.hsts !== null)
137
+ siteWide.set("strict-transport-security", security.hsts);
138
+ /** Lines to add right after a rule's path line, by that line's index. */
139
+ const additions = new Map();
140
+ const add = (rule, line) => {
141
+ const list = additions.get(rule.line) ?? [];
142
+ list.push(line);
143
+ additions.set(rule.line, list);
144
+ };
145
+ // The static pages' CSP.
146
+ const prefix = base === "/" ? "" : base.replace(/\/$/, "");
147
+ const cspRules = [];
148
+ const cspInAuthorRules = [];
149
+ const cspRoutes = new Map();
150
+ if (csp) {
151
+ for (const route of csp.staticRoutes) {
152
+ const { name, value } = contentSecurityPolicyHeader(csp.policy, csp.hashes[route]);
153
+ const path = `${prefix}${route}`;
154
+ const line = ` ${headerName(name)}: ${value}`;
155
+ // An author's rule that sets or removes this header on the page wins.
156
+ const authorDecides = [...effective.values()].some((rule) => (rule.sets.has(name) || rule.unsets.has(name)) &&
157
+ ruleMatches(rule.path, path));
158
+ if (authorDecides)
159
+ continue;
160
+ if (line.trim().length > CLOUDFLARE_HEADERS_LINE_LIMIT)
161
+ throw new Error(`[bascik] --target cloudflare: the ${headerName(name)} line for the static page ${path} is ` +
162
+ `${line.trim().length} characters (one hash for each of its inline scripts and styles), but Cloudflare ` +
163
+ `skips any _headers line over ${CLOUDFLARE_HEADERS_LINE_LIMIT} characters, so the page would ship with no ` +
164
+ `policy and no error. ${remedy(variant)} Or move some of the page's inline scripts and styles into files.`);
165
+ cspRoutes.set(path, route);
166
+ const own = effective.get(path);
167
+ // Never a second rule for the author's path: it would replace theirs.
168
+ if (own)
169
+ cspInAuthorRules.push([own, line]);
170
+ else
171
+ cspRules.push(path, line);
172
+ }
173
+ }
174
+ if (siteWide.size === 0 && cspRoutes.size === 0)
175
+ return authored;
176
+ // A later rule replaces a security header (or one the author's `/*` sets)
177
+ // instead of joining it.
178
+ const replaced = new Set([
179
+ ...MANAGED_HEADERS,
180
+ ...siteWide.keys(),
181
+ ...(authorSiteWide?.sets ?? []),
182
+ ]);
183
+ for (const rule of effective.values()) {
184
+ if (rule === authorSiteWide) {
185
+ // The author's own `/*`: their values and removals stand.
186
+ for (const [name, value] of siteWide)
187
+ if (!rule.sets.has(name) && !rule.unsets.has(name))
188
+ add(rule, ` ${headerName(name)}: ${value}`);
189
+ continue;
190
+ }
191
+ for (const name of rule.sets)
192
+ if (replaced.has(name) && !rule.unsets.has(name))
193
+ add(rule, ` ${UNSET_PREFIX}${headerName(name)}`);
194
+ }
195
+ for (const [rule, line] of cspInAuthorRules)
196
+ add(rule, line);
197
+ const skip = new Set();
198
+ const rendered = (from, to) => {
199
+ const out = [];
200
+ for (let i = from; i < to; i++) {
201
+ if (skip.has(i))
202
+ continue;
203
+ out.push(lines[i]);
204
+ const extra = additions.get(i);
205
+ if (extra)
206
+ out.push(...extra);
207
+ }
208
+ while (out.length && out[out.length - 1].trim() === "")
209
+ out.pop();
210
+ return out;
211
+ };
212
+ const blocks = [];
213
+ if (authorSiteWide) {
214
+ // First: the author's `/*` (with the comments above it when it is their
215
+ // first rule). An earlier `/*` rule Cloudflare already ignores (the last
216
+ // one written wins) is left out, or it would take the moved rule's place.
217
+ const from = authorSiteWide === rules[0] ? 0 : authorSiteWide.line;
218
+ blocks.push(rendered(from, authorSiteWide.end));
219
+ for (let i = from; i < authorSiteWide.end; i++)
220
+ skip.add(i);
221
+ for (const rule of rules)
222
+ if (rule.path === SITE_WIDE_PATH)
223
+ for (let i = rule.line; i < rule.end; i++)
224
+ skip.add(i);
225
+ }
226
+ else if (siteWide.size > 0) {
227
+ blocks.push([
228
+ SITE_WIDE_PATH,
229
+ ...[...siteWide].map(([name, value]) => ` ${headerName(name)}: ${value}`),
230
+ ]);
231
+ }
232
+ if (cspRules.length)
233
+ blocks.push(cspRules);
234
+ const rest = rendered(0, lines.length);
235
+ if (rest.length)
236
+ blocks.push(rest);
237
+ const text = `${blocks.map((b) => b.join("\n")).join("\n\n")}\n`;
238
+ const tooLong = text
239
+ .split("\n")
240
+ .findIndex((line) => line.trim().length > CLOUDFLARE_HEADERS_LINE_LIMIT);
241
+ if (tooLong !== -1) {
242
+ const line = text.split("\n")[tooLong].trim();
243
+ throw new Error(`[bascik] --target cloudflare: a line in your _headers is ${line.length} characters ` +
244
+ `(${line.slice(0, 60)}...), but Cloudflare skips any _headers line over ${CLOUDFLARE_HEADERS_LINE_LIMIT} ` +
245
+ `characters with no error. Shorten it.`);
246
+ }
247
+ const total = countHeadersRules(text);
248
+ if (total > CLOUDFLARE_HEADERS_RULE_LIMIT) {
249
+ const newSiteWide = !authorSiteWide && siteWide.size > 0 ? 1 : 0;
250
+ const cspPages = cspRules.length / 2;
251
+ const authoredRules = total - newSiteWide - cspPages;
252
+ const parts = [
253
+ ...(cspPages
254
+ ? [`${cspPages} for contentSecurityPolicy, one for each static page`]
255
+ : []),
256
+ ...(newSiteWide
257
+ ? ["1 site-wide rule for security.headers and security.hsts"]
258
+ : []),
259
+ ...(authoredRules ? [`${authoredRules} already in your _headers`] : []),
260
+ ];
261
+ throw new Error(`[bascik] --target cloudflare: public/_headers needs ${total} rules (${parts.join(", ")}), but Cloudflare ` +
262
+ `reads at most ${CLOUDFLARE_HEADERS_RULE_LIMIT} and drops the rest with no error. ${remedy(variant)} ` +
263
+ `Or remove rules from your _headers (a path written twice counts twice).`);
264
+ }
265
+ return text;
266
+ };
267
+ /**
268
+ * The author's _headers rules that set or remove a security header on a page
269
+ * the Worker renders. `_headers` reaches static files only, so on those pages
270
+ * the Worker keeps `security.headers`' value: one warning per rule, naming a
271
+ * page, so the author can move the value into the config.
272
+ */
273
+ export const workerPageHeaderOverrides = ({ authored, security = DEFAULT_RESPONSE_SECURITY, base, workerPages, }) => {
274
+ const prefix = base === "/" ? "" : base.replace(/\/$/, "");
275
+ const watched = new Set([
276
+ ...MANAGED_HEADERS,
277
+ ...Object.keys(security.headers),
278
+ ]);
279
+ const warnings = [];
280
+ const effective = new Map();
281
+ for (const rule of parseRules(linesOf(authored)))
282
+ effective.set(rule.path, rule);
283
+ for (const rule of effective.values()) {
284
+ const names = [...rule.sets, ...rule.unsets].filter((n) => watched.has(n));
285
+ if (!names.length)
286
+ continue;
287
+ const page = workerPages
288
+ .map((p) => `${prefix}${p}`)
289
+ .find((p) => ruleMatches(rule.path, p));
290
+ if (!page)
291
+ continue;
292
+ warnings.push(`[bascik] --target cloudflare: your _headers rule ${rule.path} sets ${[...new Set(names)].map(headerName).join(", ")}, ` +
293
+ `but ${page} is rendered by the Worker and _headers rules reach static files only, so that page keeps ` +
294
+ `Bascik's value. To change the header on every page, set it in security.headers.`);
295
+ }
296
+ return warnings;
297
+ };
298
+ //# sourceMappingURL=headers-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers-file.js","sourceRoot":"","sources":["../src/headers-file.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Default export of @bascik/adapter-cloudflare.
5
5
  */
6
- export { CLOUDFLARE_COMPATIBILITY_DATE, CLOUDFLARE_COMPATIBILITY_FLAGS } from "./compat.js";
6
+ export { CLOUDFLARE_COMPATIBILITY_DATE, CLOUDFLARE_COMPATIBILITY_FLAGS, } from "./compat.js";
7
7
  export { buildInvocationRoutes } from "./build.js";
8
8
  declare const _default: import("@bascik/bascik/adapter").HostingAdapter;
9
9
  export default _default;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { defineAdapter } from "@bascik/bascik/adapter";
7
7
  import { build } from "./build.js";
8
- export { CLOUDFLARE_COMPATIBILITY_DATE, CLOUDFLARE_COMPATIBILITY_FLAGS } from "./compat.js";
8
+ export { CLOUDFLARE_COMPATIBILITY_DATE, CLOUDFLARE_COMPATIBILITY_FLAGS, } from "./compat.js";
9
9
  export { buildInvocationRoutes } from "./build.js";
10
10
  export default defineAdapter({
11
11
  name: "cloudflare",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,6BAA6B,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAe,aAAa,CAAC;IAC3B,IAAI,EAAE,YAAY;IAClB,KAAK;CACN,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,GAC/B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAe,aAAa,CAAC;IAC3B,IAAI,EAAE,YAAY;IAClB,KAAK;CACN,CAAC,CAAC"}
package/dist/runtime.d.ts CHANGED
@@ -4,10 +4,12 @@
4
4
  * The request handler that runs inside a Cloudflare Worker (prompt 134).
5
5
  * ─────────────────────────────────────────────────────────────────────────────
6
6
  *
7
- * Portable: no Node imports. It is bundled into the generated `_worker.js`
8
- * together with the site graph emitted by `serverless-artifacts.ts`, and it
9
- * drives the same execution core the Node server uses
10
- * (`lib/request-execution.ts`).
7
+ * Portable: no Node imports (the request-time component host,
8
+ * component-host.ts, uses `node:crypto` under `nodejs_compat` and is bundled
9
+ * only for sites with components, prompt 157). It is bundled into the
10
+ * generated `_worker.js` together with the site graph emitted by
11
+ * `serverless-artifacts.ts`, and it drives the same execution core the Node
12
+ * server uses (`lib/request-execution.ts`).
11
13
  *
12
14
  * Request flow inside the Worker:
13
15
  *
@@ -28,16 +30,41 @@
28
30
  * (`{ name: "cloudflare", env, waitUntil }`). Nothing is copied onto globals
29
31
  * and `process.env` is never touched.
30
32
  */
31
- import { type ApiHandlerModule, type PlatformContext, type ScriptJobRunner, type ApiRouteDefinition } from "@bascik/bascik/runtime";
33
+ import { type ApiHandlerModule, type ExecutionPlan, type PlatformContext, type RenderHost, type ScriptJobRunner, type ApiRouteDefinition, type ContentSecurityPolicyConfig, type CspRouteHashes, type RequestRenderLimits, type ResponseSecurity } from "@bascik/bascik/runtime";
32
34
  /** A request-time script job inside a dynamic page template. */
33
35
  export interface GraphScriptJob {
34
36
  id: string;
35
37
  mode: "server" | "stream";
38
+ /**
39
+ * The component's element classes for this job's output (prompt 157):
40
+ * set on component scripts, and on a page job a component placed
41
+ * statically, so their returned markup is styled like the component's own.
42
+ */
43
+ outputScope?: string;
44
+ /**
45
+ * Where a component script is authored (`server script in
46
+ * src/components/x.html:12`), set on component jobs (prompt 157): the
47
+ * bundled stack names only the Worker file, so a failure is located from
48
+ * this instead.
49
+ */
50
+ owner?: string;
36
51
  /** Loader for the compiled module; `default` is the handler. */
37
52
  load: () => Promise<{
38
53
  default: unknown;
39
54
  }>;
40
55
  }
56
+ /** The build's component manifest, as the portable renderer reads it. */
57
+ export type ComponentManifest = NonNullable<RenderHost["manifest"]>;
58
+ /**
59
+ * Request-time components (prompt 157): the manifest, emitted by build.ts
60
+ * only when it lists a component, and the component scripts it names, keyed
61
+ * by job id. They belong to no page: any page's script may return any
62
+ * component.
63
+ */
64
+ export interface GraphComponents {
65
+ manifest: ComponentManifest;
66
+ jobs: Record<string, GraphScriptJob>;
67
+ }
41
68
  export interface GraphPageSegment {
42
69
  kind: "static" | "script";
43
70
  /** UTF-8 text for static segments. */
@@ -67,12 +94,46 @@ export interface SiteGraph {
67
94
  onServerScriptError: "error" | "warn" | "ignore";
68
95
  /** Dynamic pages by canonical path. */
69
96
  pages: Record<string, GraphPage>;
70
- /** Sorted API routes (static before dynamic). */
97
+ /** Sorted API routes (static, then `[param]`, then catch-all). */
71
98
  apiRoutes: GraphApiRoute[];
72
99
  /** Optional authored 500 page body. */
73
100
  custom500?: string;
101
+ /** A static `404.html` is deployed; its own URLs answer 404, as on Node. */
102
+ staticNotFoundPage?: boolean;
74
103
  /** Release identity for diagnostics headers. */
75
104
  release: string;
105
+ /** Present when the site renders component tags in script output. */
106
+ components?: GraphComponents;
107
+ /**
108
+ * Opt-in `contentSecurityPolicy`: the policy and the build hashes of each
109
+ * page the Worker renders (static pages carry theirs in _headers).
110
+ */
111
+ contentSecurityPolicy?: {
112
+ policy: ContentSecurityPolicyConfig;
113
+ hashes: Record<string, CspRouteHashes>;
114
+ };
115
+ /**
116
+ * The build's `security.headers`, `security.hsts` and
117
+ * `security.privateDynamicPages`, resolved. Absent: the defaults.
118
+ */
119
+ responseSecurity?: ResponseSecurity;
120
+ /**
121
+ * Set when the Worker runs before every static file (`run_worker_first:
122
+ * true` in the authored wrangler config): the build writes no `_headers`
123
+ * rules, and the Worker adds the security headers, and each listed static
124
+ * page's CSP, to what the asset layer returns. A header the response
125
+ * already has (from the author's own `_headers`) is kept.
126
+ */
127
+ assetHeaders?: {
128
+ /** Static pages (paths without the base) whose CSP the Worker sends. */
129
+ cspRoutes: string[];
130
+ };
131
+ /** The build's `security.hideDotfiles`. Absent: true. */
132
+ hideDotfiles?: boolean;
133
+ /** The build's `scripts.stripAnsi`. Absent: true. */
134
+ stripAnsi?: boolean;
135
+ /** The build's `scripts.renderLimits`. Absent: the defaults. */
136
+ renderLimits?: Partial<RequestRenderLimits>;
76
137
  }
77
138
  export interface CloudflareAssetsBinding {
78
139
  fetch(request: Request): Promise<Response>;
@@ -93,15 +154,28 @@ export declare const stripBase: (pathname: string, base: string) => string | nul
93
154
  /** Derive the client IP from Cloudflare's trusted header; never from X-Forwarded-For. */
94
155
  export declare const cloudflareRemoteIp: (request: Request) => string;
95
156
  /**
96
- * Build a `ScriptJobRunner` for one request. Output handling mirrors
97
- * `runServerScriptJob`: a string or stringifiable value is the fragment;
98
- * failure under `onServerScriptError: "error"` throws (500 before commit,
99
- * empty after), `warn` logs and yields empty, `ignore` yields empty.
157
+ * Build a `ScriptJobRunner` for one request: the page's own jobs, then the
158
+ * component scripts of `graph.components` (prompt 157), under the graph's
159
+ * script deadline and error policy (see `runGraphJob`).
100
160
  */
101
161
  export declare const createGraphJobRunner: (page: GraphPage, graph: SiteGraph, request: Request, context: {
102
162
  remoteIp: string;
103
163
  platform: PlatformContext;
104
164
  }, log: (message: string) => void) => ScriptJobRunner;
165
+ /**
166
+ * The page's plan and runner for one request. With a component manifest
167
+ * that has something to render (prompt 157), the runner is wrapped so
168
+ * component tags in job output are rendered (component-host.ts), and the
169
+ * component scripts run like page jobs with the parent's remaining budget.
170
+ * Without one, plan and runner are exactly what they always were.
171
+ */
172
+ export declare const createPageExecution: (page: GraphPage, graph: SiteGraph, request: Request, context: {
173
+ remoteIp: string;
174
+ platform: PlatformContext;
175
+ }, log: (message: string) => void) => {
176
+ plan: ExecutionPlan;
177
+ run: ScriptJobRunner;
178
+ };
105
179
  export interface CreateWorkerOptions {
106
180
  /** Diagnostics sink; defaults to `console.error`. Never reaches the client. */
107
181
  log?: (message: string) => void;