@gradial/aci 0.1.53-rc.4 → 0.1.53-rc.6
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/CHANGELOG.md +12 -0
- package/README.md +38 -4
- package/dist/astro/error-page.d.ts +3 -2
- package/dist/astro/error-page.js +13 -27
- package/dist/astro/page.d.ts +3 -0
- package/dist/astro/page.js +27 -24
- package/dist/cli/emit-error-pages.mjs +46 -5
- package/dist/content/error-page-kind.d.ts +6 -0
- package/dist/content/error-page-kind.js +2 -1
- package/dist/content/error-pages.d.ts +1 -1
- package/package.json +7 -7
- package/src/cli/emit-error-pages.mjs +40 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Astro authored 404/500 helpers use the shared `getPageData` pipeline and
|
|
8
|
+
consume the selected render input during ACI
|
|
9
|
+
preview/publish, preserving authored content and styling in code artifacts
|
|
10
|
+
without compiled content snapshots. Unrelated or personalized inputs cannot
|
|
11
|
+
replace the bound base error page.
|
|
12
|
+
|
|
5
13
|
### Added
|
|
6
14
|
|
|
15
|
+
- Optional Node-only `src/aci/hosting-error-page.mjs` policy adds site-specific
|
|
16
|
+
browser bootstrap code to the SDK-owned static 500 fallback, with syntax
|
|
17
|
+
validation, script-data escaping, and the shared output-size budget.
|
|
18
|
+
|
|
7
19
|
- Public `aci schemas compile` and `aci error-pages prepare|emit` commands let
|
|
8
20
|
site build scripts compile code-owned artifacts without invoking SDK internals.
|
|
9
21
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agentic Content Infrastructure SDK
|
|
2
2
|
|
|
3
|
-
`@gradial/aci` `0.1.53-rc.
|
|
3
|
+
`@gradial/aci` `0.1.53-rc.6` is a pre-1.0 public-surface reset for the ACI
|
|
4
4
|
client SDK. Breaking changes from the old `0.1.x` accidental surface are
|
|
5
5
|
intentional: the root import is framework-neutral, framework runtime behavior is
|
|
6
6
|
behind explicit subpaths, and the first required customer path is React/Next.
|
|
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm install @gradial/aci@0.1.53-rc.
|
|
11
|
+
npm install @gradial/aci@0.1.53-rc.6 zod@^4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Root Import
|
|
@@ -137,13 +137,13 @@ host bundles select and serve immutable release artifacts.
|
|
|
137
137
|
The first RC scaffold path is Next:
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
npx @gradial/aci@0.1.53-rc.
|
|
140
|
+
npx @gradial/aci@0.1.53-rc.6 init my-site --template next-starter
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
List available templates:
|
|
144
144
|
|
|
145
145
|
```bash
|
|
146
|
-
npx @gradial/aci@0.1.53-rc.
|
|
146
|
+
npx @gradial/aci@0.1.53-rc.6 init --list
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
## Personalization: The One-Tag Install
|
|
@@ -245,3 +245,37 @@ recompiles contracts without reading authored content. The error commands prepar
|
|
|
245
245
|
code-owned analytics policy and emit the default hosting fallback (preserving a
|
|
246
246
|
site-owned `public/500.html`). All three honor `--site-dir`; helper failures exit
|
|
247
247
|
nonzero. Local development/content validation must explicitly compile content.
|
|
248
|
+
|
|
249
|
+
## Hosting error-page policy
|
|
250
|
+
|
|
251
|
+
`aci error-pages emit` owns the static `public/500.html` fallback. To add a
|
|
252
|
+
site-specific analytics/consent bootstrap without recreating its HTML emitter,
|
|
253
|
+
create a **Node-only build module** at `src/aci/hosting-error-page.mjs`:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
/** @type {import('@gradial/aci/content').HostingErrorPageScript} */
|
|
257
|
+
export const hostingErrorPageScript = async ({ kind, pageType }) => {
|
|
258
|
+
// Return bundled browser JavaScript, or an empty string for no bootstrap.
|
|
259
|
+
return '';
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The hook receives `{ kind: 'catchAll', pageType: 'error' }`. It runs at build
|
|
264
|
+
time, may import build helpers, and must return self-contained classic browser
|
|
265
|
+
JavaScript. It must not read authored content or select an authored error page.
|
|
266
|
+
Keep consent decisions and vendor-specific behavior in the site policy. It
|
|
267
|
+
must not repeat the error-view push: the SDK emits that first from the existing
|
|
268
|
+
client-safe `src/aci/error-page-instrumentation.mjs` policy.
|
|
269
|
+
|
|
270
|
+
The SDK checks script syntax, escapes HTML script-data delimiters, appends a
|
|
271
|
+
separate `data-aci-error-page-bootstrap="error"` script, and enforces a combined
|
|
272
|
+
150 KiB HTML budget. Invalid present policies fail the build before replacing
|
|
273
|
+
the previous generated fallback. A missing module adds no bootstrap. An existing
|
|
274
|
+
unmarked site-owned `public/500.html` is left untouched. This hook affects the
|
|
275
|
+
native hosting fallback only; it does not add loaders to authored CDN errors or
|
|
276
|
+
change the instrumentation capsule consumed by publish.
|
|
277
|
+
|
|
278
|
+
When replacing a site-local generator, delete its old ignored generated
|
|
279
|
+
`public/500.html` once so it can become SDK-managed. Remove the generator from
|
|
280
|
+
build scripts, retain any shared consent-bundle preparation needed by other
|
|
281
|
+
static routes, and run `aci error-pages emit` as the final build step.
|
|
@@ -48,8 +48,9 @@ export interface AstroErrorPageData {
|
|
|
48
48
|
* error seats (`404.astro` / `500.astro`). `astro` is the seat's Astro
|
|
49
49
|
* global; the bound route's content is loaded as if it were the requested
|
|
50
50
|
* route, so static builds emit it into `dist/404.html`/`dist/500.html` and
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* ACI renders use the already resolved input for the bound base page;
|
|
52
|
+
* local static builds use compiled authoring content. Neither path fetches
|
|
53
|
+
* CMS content on a failure (ANTI-2).
|
|
53
54
|
*/
|
|
54
55
|
export declare function getErrorPageData(astro: AstroGlobal, kind: ErrorPageKind, options?: {
|
|
55
56
|
pushErrorPageView?: ErrorPagePush;
|
package/dist/astro/error-page.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* returned page data always carries empty collect/assignment tags.
|
|
18
18
|
*/
|
|
19
19
|
import { getPageData } from './page.js';
|
|
20
|
-
import { defaultPushErrorPageView, errorPageType,
|
|
20
|
+
import { defaultPushErrorPageView, errorPageType, serializeErrorPageInstrumentation, } from '../content/error-pages.js';
|
|
21
21
|
import { defaultErrorPageFragmentHtml } from '../content/default-error-page.js';
|
|
22
22
|
const FALLBACK_META = {
|
|
23
23
|
notFound: {
|
|
@@ -34,37 +34,23 @@ const FALLBACK_META = {
|
|
|
34
34
|
* error seats (`404.astro` / `500.astro`). `astro` is the seat's Astro
|
|
35
35
|
* global; the bound route's content is loaded as if it were the requested
|
|
36
36
|
* route, so static builds emit it into `dist/404.html`/`dist/500.html` and
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* ACI renders use the already resolved input for the bound base page;
|
|
38
|
+
* local static builds use compiled authoring content. Neither path fetches
|
|
39
|
+
* CMS content on a failure (ANTI-2).
|
|
39
40
|
*/
|
|
40
41
|
export async function getErrorPageData(astro, kind, options = {}) {
|
|
41
42
|
const pageType = errorPageType(kind);
|
|
42
43
|
const instrumentationTag = serializeErrorPageInstrumentation(options.pushErrorPageView ?? defaultPushErrorPageView, kind);
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
isPrerendered: astro.isPrerendered,
|
|
54
|
-
params: { slug },
|
|
55
|
-
request: astro.request,
|
|
56
|
-
url: astro.url,
|
|
57
|
-
});
|
|
58
|
-
return {
|
|
59
|
-
kind,
|
|
60
|
-
pageType,
|
|
61
|
-
instrumentationTag,
|
|
62
|
-
// Base audience, no personalization on error surfaces (REQ-11).
|
|
63
|
-
data: { ...data, collectTag: '', assignmentsTag: '' },
|
|
64
|
-
fallbackHtml: null,
|
|
65
|
-
meta: data.meta,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
45
|
+
const data = await getPageData(astro, { errorPageKind: kind });
|
|
46
|
+
return {
|
|
47
|
+
kind,
|
|
48
|
+
pageType,
|
|
49
|
+
instrumentationTag,
|
|
50
|
+
data,
|
|
51
|
+
fallbackHtml: null,
|
|
52
|
+
meta: data.meta,
|
|
53
|
+
};
|
|
68
54
|
}
|
|
69
55
|
catch (error) {
|
|
70
56
|
console.warn(`[aci error-pages] bound ${kind} page failed to load; serving the shipped default: ${String(error)}`);
|
package/dist/astro/page.d.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import type { KernelPage, KernelSiteConfig, KernelRouteMetadata, KernelPageBlock } from '../content/types.js';
|
|
34
34
|
import { type LayoutEntry } from '../content/provider.js';
|
|
35
|
+
import { type ErrorPageKind } from '../content/error-pages.js';
|
|
35
36
|
/**
|
|
36
37
|
* Minimal Astro global shape — only the properties getPageData reads.
|
|
37
38
|
* This avoids depending on Astro's own types at the SDK level.
|
|
@@ -77,6 +78,8 @@ export interface PageData {
|
|
|
77
78
|
assignmentsTag: string;
|
|
78
79
|
}
|
|
79
80
|
export interface GetPageDataOptions {
|
|
81
|
+
/** Resolve this authored error binding as an unpersonalized base page. */
|
|
82
|
+
errorPageKind?: ErrorPageKind;
|
|
80
83
|
/** Path to compiled content root. Defaults to ACI_CONTENT_ROOT env or '.aci/compiled'. */
|
|
81
84
|
compiledRoot?: string;
|
|
82
85
|
}
|
package/dist/astro/page.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { FragmentNotFoundError } from '../content/provider.js';
|
|
34
34
|
import { normalizeRoute } from '../content/routes.js';
|
|
35
|
+
import { resolveErrorPage } from '../content/error-pages.js';
|
|
35
36
|
import { routeMetadataForContent } from '../content/provider.js';
|
|
36
37
|
import { resolveSlots } from '../content/resolve-slots.js';
|
|
37
38
|
import { assignmentsScriptTagHTML, collectScriptTagHTML } from '../content/personalization.js';
|
|
@@ -54,7 +55,7 @@ import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInputFromHeaders } from '.
|
|
|
54
55
|
* artifacts in S3, no runtime content fetching).
|
|
55
56
|
*/
|
|
56
57
|
export async function getPageData(astro, options) {
|
|
57
|
-
|
|
58
|
+
let route = normalizeRoute(astro.params.slug ? `/${astro.params.slug}` : '/');
|
|
58
59
|
// Static Astro generation is explicit; unexpected header access errors and
|
|
59
60
|
// expired render-input IDs must not turn a selected variant into local base.
|
|
60
61
|
let runtimeInput = null;
|
|
@@ -65,28 +66,29 @@ export async function getPageData(astro, options) {
|
|
|
65
66
|
throw new Error(`ACI Astro render input is unavailable for route ${route}; refusing to render local base content`);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]);
|
|
87
|
-
const entry = (await provider.listRoutes()).find((entry) => normalizeRoute(entry.path) === route);
|
|
88
|
-
variantKey = requireVariantKey(entry?.variantKey, route);
|
|
69
|
+
// Source selection is shared by ordinary pages and authored error seats.
|
|
70
|
+
// A supplied renderer input never consults local authoring files.
|
|
71
|
+
const provider = runtimeInput
|
|
72
|
+
? null
|
|
73
|
+
: new (await import('../providers/file.js')).FileContentProvider(options?.compiledRoot);
|
|
74
|
+
const siteConfig = runtimeInput
|
|
75
|
+
? runtimeInput.siteConfig
|
|
76
|
+
: await provider.getSiteConfig();
|
|
77
|
+
if (options?.errorPageKind) {
|
|
78
|
+
const resolved = await resolveErrorPage(options.errorPageKind, siteConfig);
|
|
79
|
+
for (const warning of resolved.warnings)
|
|
80
|
+
console.warn(`[aci error-pages] ${warning}`);
|
|
81
|
+
if (!resolved.route)
|
|
82
|
+
throw new Error(`No authored ${options.errorPageKind} page is bound`);
|
|
83
|
+
route = normalizeRoute(resolved.route);
|
|
84
|
+
if (runtimeInput && (normalizeRoute(runtimeInput.route) !== route || runtimeInput.variantKey !== 'base')) {
|
|
85
|
+
throw new Error(`ACI Astro ${options.errorPageKind} input must supply the bound base page ${resolved.route}`);
|
|
86
|
+
}
|
|
89
87
|
}
|
|
88
|
+
const page = runtimeInput ? runtimeInput.page : await provider.getPage(route);
|
|
89
|
+
const variantKey = runtimeInput
|
|
90
|
+
? requireVariantKey(runtimeInput.variantKey, route)
|
|
91
|
+
: requireVariantKey((await provider.listRoutes()).find((entry) => normalizeRoute(entry.path) === route)?.variantKey, route);
|
|
90
92
|
const meta = routeMetadataForContent(siteConfig, page);
|
|
91
93
|
// Resolve layout: prefer render input, fall back to provider
|
|
92
94
|
const layout = await resolveLayout(runtimeInput, provider, page.layout, route);
|
|
@@ -102,8 +104,9 @@ export async function getPageData(astro, options) {
|
|
|
102
104
|
});
|
|
103
105
|
// Collect tag: suppress in preview mode (authoring surface, not visitor traffic)
|
|
104
106
|
const isPreview = astro.url.searchParams.has('preview_token');
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
+
const suppressCollection = isPreview || Boolean(options?.errorPageKind);
|
|
108
|
+
const collectTag = suppressCollection ? '' : collectScriptTagHTML(siteConfig);
|
|
109
|
+
const assignmentsTag = suppressCollection ? '' : assignmentsScriptTagHTML(page);
|
|
107
110
|
return { route, variantKey, siteConfig, page, meta, layout, slots, collectTag, assignmentsTag };
|
|
108
111
|
}
|
|
109
112
|
function requireVariantKey(value, route) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Usage: emit-error-pages.mjs <prepare|emit>
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { Script } from 'node:vm';
|
|
8
9
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
10
|
const MARKER = '<!-- aci:error-pages generated -->';
|
|
10
11
|
const SIZE_BUDGET = 150 * 1024;
|
|
@@ -15,11 +16,12 @@ function log(message) {
|
|
|
15
16
|
console.log(`[aci error-pages] ${message}`);
|
|
16
17
|
}
|
|
17
18
|
function fail(message) {
|
|
18
|
-
|
|
19
|
-
process.exit(1);
|
|
19
|
+
throw new Error(`[aci error-pages] ${message}`);
|
|
20
20
|
}
|
|
21
21
|
async function sdkContent() {
|
|
22
|
-
|
|
22
|
+
const content = await import(pathToFileURL(path.join(packageRoot, 'dist', 'content', 'index.js')).href);
|
|
23
|
+
const { escapeInlineScriptSource } = await import(pathToFileURL(path.join(packageRoot, 'dist', 'content', 'error-page-kind.js')).href);
|
|
24
|
+
return { ...content, escapeInlineScriptSource };
|
|
23
25
|
}
|
|
24
26
|
// Keep the existing capsule transport path for SDK-first upgrades. This file
|
|
25
27
|
// is executable policy, not a compiled content snapshot.
|
|
@@ -126,6 +128,43 @@ export function stampInstrumentation(html, instrumentationTag) {
|
|
|
126
128
|
}
|
|
127
129
|
return stamped;
|
|
128
130
|
}
|
|
131
|
+
// Node-only build hook. Keep loaders/bundlers out of the client policy module.
|
|
132
|
+
const HOSTING_POLICY_MODULE = path.join('src', 'aci', 'hosting-error-page.mjs');
|
|
133
|
+
async function hostingBootstrapTag(root, sdk) {
|
|
134
|
+
const modulePath = path.join(root, HOSTING_POLICY_MODULE);
|
|
135
|
+
if (!fs.existsSync(modulePath))
|
|
136
|
+
return '';
|
|
137
|
+
let policy;
|
|
138
|
+
try {
|
|
139
|
+
policy = await import(pathToFileURL(modulePath).href);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw new Error(`${HOSTING_POLICY_MODULE} failed to import: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof policy.hostingErrorPageScript !== 'function') {
|
|
145
|
+
throw new Error(`${HOSTING_POLICY_MODULE} must export a hostingErrorPageScript function`);
|
|
146
|
+
}
|
|
147
|
+
let source;
|
|
148
|
+
try {
|
|
149
|
+
source = await policy.hostingErrorPageScript(Object.freeze({ kind: 'catchAll', pageType: 'error' }));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw new Error(`${HOSTING_POLICY_MODULE}: hostingErrorPageScript failed: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
if (typeof source !== 'string')
|
|
155
|
+
throw new Error(`${HOSTING_POLICY_MODULE}: hostingErrorPageScript must return JavaScript source as a string`);
|
|
156
|
+
if (!source.trim())
|
|
157
|
+
return '';
|
|
158
|
+
const escaped = sdk.escapeInlineScriptSource(source);
|
|
159
|
+
try {
|
|
160
|
+
new Script(escaped);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
throw new Error(`${HOSTING_POLICY_MODULE}: hostingErrorPageScript returned invalid JavaScript: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
log(`hosting bootstrap source: site module ${HOSTING_POLICY_MODULE}`);
|
|
166
|
+
return `<script data-aci-error-page-bootstrap="error">${escaped}</script>`;
|
|
167
|
+
}
|
|
129
168
|
export async function emit(root = siteRoot, content) {
|
|
130
169
|
const publicDir = path.join(root, 'public');
|
|
131
170
|
const target = path.join(publicDir, '500.html');
|
|
@@ -135,11 +174,13 @@ export async function emit(root = siteRoot, content) {
|
|
|
135
174
|
}
|
|
136
175
|
const sdk = content ?? (await sdkContent());
|
|
137
176
|
const instrumentationTag = await resolveInstrumentationTag(root, sdk);
|
|
138
|
-
const
|
|
177
|
+
const bootstrapTag = await hostingBootstrapTag(root, sdk);
|
|
178
|
+
const tags = bootstrapTag ? `${instrumentationTag}\n${bootstrapTag}` : instrumentationTag;
|
|
179
|
+
const html = stampInstrumentation(sdk.defaultErrorPageHtml(), tags);
|
|
139
180
|
const size = Buffer.byteLength(html);
|
|
140
181
|
if (size > SIZE_BUDGET) {
|
|
141
182
|
fail(`public/500.html is ${size} bytes, over the ${SIZE_BUDGET}-byte budget (REQ-14). `
|
|
142
|
-
+ '
|
|
183
|
+
+ 'Reduce the hosting bootstrap policy output or provide a smaller site-owned hosting fallback.');
|
|
143
184
|
}
|
|
144
185
|
fs.mkdirSync(publicDir, { recursive: true });
|
|
145
186
|
fs.writeFileSync(target, html);
|
|
@@ -3,6 +3,12 @@ export type ErrorPageKind = 'notFound' | 'catchAll';
|
|
|
3
3
|
export type ErrorPageType = 'not_found' | 'error';
|
|
4
4
|
/** Maps a binding kind to its D-14 page_type. */
|
|
5
5
|
export declare function errorPageType(kind: ErrorPageKind): ErrorPageType;
|
|
6
|
+
/** The Node-only hook used to augment the SDK-owned native hosting fallback. */
|
|
7
|
+
export interface HostingErrorPageContext {
|
|
8
|
+
readonly kind: 'catchAll';
|
|
9
|
+
readonly pageType: 'error';
|
|
10
|
+
}
|
|
11
|
+
export type HostingErrorPageScript = (context: HostingErrorPageContext) => string | Promise<string>;
|
|
6
12
|
declare global {
|
|
7
13
|
interface Window {
|
|
8
14
|
dataLayer?: Array<Record<string, unknown>>;
|
|
@@ -65,7 +65,8 @@ export function serializeErrorPageInstrumentation(push, kind) {
|
|
|
65
65
|
* `\s` and `\!` decode to the original characters, so payload values
|
|
66
66
|
* survive byte-for-byte on every surface.
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
/** @internal Shared by SDK build-time hosting policy emission. */
|
|
69
|
+
export function escapeInlineScriptSource(source) {
|
|
69
70
|
return source.replace(/<(\/script|script|!--)/gi, (match) => `<\\${match.slice(1)}`);
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
@@ -19,7 +19,7 @@ import type { KernelSiteConfig, SiteErrorPages } from './types.js';
|
|
|
19
19
|
/** D-5 migration posture for absent bindings; default is 'warn' today. */
|
|
20
20
|
export type ErrorPagePosture = 'warn' | 'enforce';
|
|
21
21
|
export { errorPageType, errorPageInstrumentationTag, defaultPushErrorPageView, serializeErrorPageInstrumentation, } from './error-page-kind.js';
|
|
22
|
-
export type { ErrorPageKind, ErrorPageType, ErrorPagePush } from './error-page-kind.js';
|
|
22
|
+
export type { ErrorPageKind, ErrorPageType, ErrorPagePush, HostingErrorPageContext, HostingErrorPageScript } from './error-page-kind.js';
|
|
23
23
|
import type { ErrorPageKind } from './error-page-kind.js';
|
|
24
24
|
/**
|
|
25
25
|
* The one D-5 posture config surface, shared by every stack: the same
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.53-rc.
|
|
3
|
+
"version": "0.1.53-rc.6",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/gradial/bare-metal-go.git",
|
|
@@ -164,11 +164,11 @@
|
|
|
164
164
|
"astro": "^6.4.8"
|
|
165
165
|
},
|
|
166
166
|
"optionalDependencies": {
|
|
167
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.53-rc.
|
|
168
|
-
"@gradial/aci-cli-darwin-x64": "0.1.53-rc.
|
|
169
|
-
"@gradial/aci-cli-linux-arm64": "0.1.53-rc.
|
|
170
|
-
"@gradial/aci-cli-linux-x64": "0.1.53-rc.
|
|
171
|
-
"@gradial/aci-cli-win32-arm64": "0.1.53-rc.
|
|
172
|
-
"@gradial/aci-cli-win32-x64": "0.1.53-rc.
|
|
167
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.53-rc.6",
|
|
168
|
+
"@gradial/aci-cli-darwin-x64": "0.1.53-rc.6",
|
|
169
|
+
"@gradial/aci-cli-linux-arm64": "0.1.53-rc.6",
|
|
170
|
+
"@gradial/aci-cli-linux-x64": "0.1.53-rc.6",
|
|
171
|
+
"@gradial/aci-cli-win32-arm64": "0.1.53-rc.6",
|
|
172
|
+
"@gradial/aci-cli-win32-x64": "0.1.53-rc.6"
|
|
173
173
|
}
|
|
174
174
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { Script } from 'node:vm';
|
|
9
10
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
11
|
|
|
11
12
|
const MARKER = '<!-- aci:error-pages generated -->';
|
|
@@ -19,12 +20,13 @@ function log(message) {
|
|
|
19
20
|
console.log(`[aci error-pages] ${message}`);
|
|
20
21
|
}
|
|
21
22
|
function fail(message) {
|
|
22
|
-
|
|
23
|
-
process.exit(1);
|
|
23
|
+
throw new Error(`[aci error-pages] ${message}`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async function sdkContent() {
|
|
27
|
-
|
|
27
|
+
const content = await import(pathToFileURL(path.join(packageRoot, 'dist', 'content', 'index.js')).href);
|
|
28
|
+
const { escapeInlineScriptSource } = await import(pathToFileURL(path.join(packageRoot, 'dist', 'content', 'error-page-kind.js')).href);
|
|
29
|
+
return { ...content, escapeInlineScriptSource };
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
// Keep the existing capsule transport path for SDK-first upgrades. This file
|
|
@@ -142,6 +144,37 @@ export function stampInstrumentation(html, instrumentationTag) {
|
|
|
142
144
|
return stamped;
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
// Node-only build hook. Keep loaders/bundlers out of the client policy module.
|
|
148
|
+
const HOSTING_POLICY_MODULE = path.join('src', 'aci', 'hosting-error-page.mjs');
|
|
149
|
+
|
|
150
|
+
async function hostingBootstrapTag(root, sdk) {
|
|
151
|
+
const modulePath = path.join(root, HOSTING_POLICY_MODULE);
|
|
152
|
+
if (!fs.existsSync(modulePath)) return '';
|
|
153
|
+
let policy;
|
|
154
|
+
try {
|
|
155
|
+
policy = await import(pathToFileURL(modulePath).href);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error(`${HOSTING_POLICY_MODULE} failed to import: ${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
if (typeof policy.hostingErrorPageScript !== 'function') {
|
|
160
|
+
throw new Error(`${HOSTING_POLICY_MODULE} must export a hostingErrorPageScript function`);
|
|
161
|
+
}
|
|
162
|
+
let source;
|
|
163
|
+
try {
|
|
164
|
+
source = await policy.hostingErrorPageScript(Object.freeze({ kind: 'catchAll', pageType: 'error' }));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(`${HOSTING_POLICY_MODULE}: hostingErrorPageScript failed: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
if (typeof source !== 'string') throw new Error(`${HOSTING_POLICY_MODULE}: hostingErrorPageScript must return JavaScript source as a string`);
|
|
169
|
+
if (!source.trim()) return '';
|
|
170
|
+
const escaped = sdk.escapeInlineScriptSource(source);
|
|
171
|
+
try { new Script(escaped); } catch (error) {
|
|
172
|
+
throw new Error(`${HOSTING_POLICY_MODULE}: hostingErrorPageScript returned invalid JavaScript: ${error.message}`);
|
|
173
|
+
}
|
|
174
|
+
log(`hosting bootstrap source: site module ${HOSTING_POLICY_MODULE}`);
|
|
175
|
+
return `<script data-aci-error-page-bootstrap="error">${escaped}</script>`;
|
|
176
|
+
}
|
|
177
|
+
|
|
145
178
|
export async function emit(root = siteRoot, content) {
|
|
146
179
|
const publicDir = path.join(root, 'public');
|
|
147
180
|
const target = path.join(publicDir, '500.html');
|
|
@@ -152,13 +185,15 @@ export async function emit(root = siteRoot, content) {
|
|
|
152
185
|
|
|
153
186
|
const sdk = content ?? (await sdkContent());
|
|
154
187
|
const instrumentationTag = await resolveInstrumentationTag(root, sdk);
|
|
155
|
-
const
|
|
188
|
+
const bootstrapTag = await hostingBootstrapTag(root, sdk);
|
|
189
|
+
const tags = bootstrapTag ? `${instrumentationTag}\n${bootstrapTag}` : instrumentationTag;
|
|
190
|
+
const html = stampInstrumentation(sdk.defaultErrorPageHtml(), tags);
|
|
156
191
|
|
|
157
192
|
const size = Buffer.byteLength(html);
|
|
158
193
|
if (size > SIZE_BUDGET) {
|
|
159
194
|
fail(
|
|
160
195
|
`public/500.html is ${size} bytes, over the ${SIZE_BUDGET}-byte budget (REQ-14). `
|
|
161
|
-
+ '
|
|
196
|
+
+ 'Reduce the hosting bootstrap policy output or provide a smaller site-owned hosting fallback.',
|
|
162
197
|
);
|
|
163
198
|
}
|
|
164
199
|
|