@gradial/aci 0.1.46 → 0.1.47
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/README.md +4 -4
- package/dist/astro/error-page.d.ts +56 -0
- package/dist/astro/error-page.js +79 -0
- package/dist/astro/index.d.ts +4 -0
- package/dist/astro/index.js +9 -0
- package/dist/cli/emit-error-pages.d.mts +39 -0
- package/dist/cli/emit-error-pages.mjs +402 -0
- package/dist/content/default-error-page-html.d.ts +2 -0
- package/dist/content/default-error-page-html.js +6 -0
- package/dist/content/default-error-page.d.ts +23 -0
- package/dist/content/default-error-page.js +34 -0
- package/dist/content/error-page-kind.d.ts +57 -0
- package/dist/content/error-page-kind.js +106 -0
- package/dist/content/error-pages.d.ts +88 -0
- package/dist/content/error-pages.js +120 -0
- package/dist/content/index.d.ts +2 -0
- package/dist/content/index.js +2 -0
- package/dist/content/types.d.ts +12 -0
- package/dist/next/config.js +17 -0
- package/dist/next/error-page-instrumentation.d.ts +23 -0
- package/dist/next/error-page-instrumentation.js +36 -0
- package/dist/next/error-page-marker.d.ts +11 -0
- package/dist/next/error-page-marker.js +20 -0
- package/dist/next/error-pages.d.ts +52 -0
- package/dist/next/error-pages.js +94 -0
- package/dist/next/error-seat-payload.d.ts +33 -0
- package/dist/next/error-seat-payload.js +143 -0
- package/dist/next/error-seat.d.ts +57 -0
- package/dist/next/error-seat.js +83 -0
- package/dist/next/index.d.ts +4 -0
- package/dist/next/index.js +8 -0
- package/package.json +14 -8
- package/src/cli/emit-error-pages.mjs +424 -0
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/astro/template/src/aci/error-page-instrumentation.mjs +33 -0
- package/templates/astro/template/src/pages/404.astro +54 -0
- package/templates/astro/template/src/pages/500.astro +53 -0
- package/templates/astro/template/src/pages/[...slug].astro +15 -2
- package/templates/common/.agents/skills/authoring/SKILL.md +71 -0
- package/templates/common/.content/config/robots.txt +4 -0
- package/templates/common/.content/config/site.json.tmpl +4 -0
- package/templates/common/.content/pages/404/_index.json +31 -0
- package/templates/common/.content/pages/error/_index.json +31 -0
- package/templates/common/.content/schemas/site.schema.json +9 -0
- package/templates/common/starter-content/.content/config/robots.txt +2 -0
- package/templates/common/starter-content/.content/config/site.json +4 -0
- package/templates/common/starter-content/.content/pages/404/_index.json +31 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -6
- package/templates/common/starter-content/.content/pages/error/_index.json +31 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +10 -19
- package/templates/common/starter-content/.content/schemas/site.schema.json +9 -0
- package/templates/nextjs/template/package.json.tmpl +1 -1
- package/templates/nextjs/template/src/aci/error-page-instrumentation.d.mts +4 -0
- package/templates/nextjs/template/src/aci/error-page-instrumentation.mjs +38 -0
- package/templates/nextjs/template/src/app/error.tsx +18 -0
- package/templates/nextjs/template/src/app/not-found.tsx +13 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped branded-neutral default error page (authorable-error-pages
|
|
3
|
+
* REQ-4/D-5). Materialized with a loud warning when a site has no
|
|
4
|
+
* `errorPages` binding under the warn-then-enforce posture. Self-contained
|
|
5
|
+
* by construction: inline styles only, no external references (REQ-14).
|
|
6
|
+
*
|
|
7
|
+
* The human-editable source of truth is `assets/error-pages/default.html`;
|
|
8
|
+
* it is embedded here as a generated constant (default-error-page-html.ts)
|
|
9
|
+
* because bundled serverless functions cannot read package assets at
|
|
10
|
+
* runtime — webpack rewrites module-URL resolution and output file tracing
|
|
11
|
+
* does not carry the .html into the function bundle, so a runtime file read
|
|
12
|
+
* crashes the very error surface this page exists to protect.
|
|
13
|
+
*/
|
|
14
|
+
/** Returns the shipped branded-neutral default error page HTML. */
|
|
15
|
+
export declare function defaultErrorPageHtml(): string;
|
|
16
|
+
/**
|
|
17
|
+
* The shipped default page's `<body>` content as an embeddable fragment: the
|
|
18
|
+
* body element renamed to a `<div>` (its inline styles preserved) so
|
|
19
|
+
* framework error seats can inline the default INSIDE an existing document —
|
|
20
|
+
* the not-found seat and the client error seat render within the app's root
|
|
21
|
+
* layout, where a nested `<body>`/`<html>` would be invalid.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defaultErrorPageFragmentHtml(): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DEFAULT_ERROR_PAGE_HTML } from './default-error-page-html.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shipped branded-neutral default error page (authorable-error-pages
|
|
4
|
+
* REQ-4/D-5). Materialized with a loud warning when a site has no
|
|
5
|
+
* `errorPages` binding under the warn-then-enforce posture. Self-contained
|
|
6
|
+
* by construction: inline styles only, no external references (REQ-14).
|
|
7
|
+
*
|
|
8
|
+
* The human-editable source of truth is `assets/error-pages/default.html`;
|
|
9
|
+
* it is embedded here as a generated constant (default-error-page-html.ts)
|
|
10
|
+
* because bundled serverless functions cannot read package assets at
|
|
11
|
+
* runtime — webpack rewrites module-URL resolution and output file tracing
|
|
12
|
+
* does not carry the .html into the function bundle, so a runtime file read
|
|
13
|
+
* crashes the very error surface this page exists to protect.
|
|
14
|
+
*/
|
|
15
|
+
/** Returns the shipped branded-neutral default error page HTML. */
|
|
16
|
+
export function defaultErrorPageHtml() {
|
|
17
|
+
return DEFAULT_ERROR_PAGE_HTML;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The shipped default page's `<body>` content as an embeddable fragment: the
|
|
21
|
+
* body element renamed to a `<div>` (its inline styles preserved) so
|
|
22
|
+
* framework error seats can inline the default INSIDE an existing document —
|
|
23
|
+
* the not-found seat and the client error seat render within the app's root
|
|
24
|
+
* layout, where a nested `<body>`/`<html>` would be invalid.
|
|
25
|
+
*/
|
|
26
|
+
export function defaultErrorPageFragmentHtml() {
|
|
27
|
+
const match = defaultErrorPageHtml().match(/<body([^>]*)>([\s\S]*?)<\/body>/i);
|
|
28
|
+
if (!match) {
|
|
29
|
+
// The shipped asset is package-controlled; this is a packaging bug, but
|
|
30
|
+
// error surfaces must never crash — fall back to the raw document body.
|
|
31
|
+
return defaultErrorPageHtml();
|
|
32
|
+
}
|
|
33
|
+
return `<div${match[1]}>${match[2]}</div>`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ErrorPageKind = 'notFound' | 'catchAll';
|
|
2
|
+
/** D-14 page_type values for the error-surface `page_view` event. */
|
|
3
|
+
export type ErrorPageType = 'not_found' | 'error';
|
|
4
|
+
/** Maps a binding kind to its D-14 page_type. */
|
|
5
|
+
export declare function errorPageType(kind: ErrorPageKind): ErrorPageType;
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
dataLayer?: Array<Record<string, unknown>>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The D-15 policy shape: the dataLayer push for one error-surface view.
|
|
13
|
+
* Site policy lives in the tenant-owned scaffolded module
|
|
14
|
+
* `src/aci/error-page-instrumentation.mjs` (`pushErrorPageView`); the SDK
|
|
15
|
+
* ships `defaultPushErrorPageView` as the scaffolded default's twin. A push
|
|
16
|
+
* MUST be self-contained (browser globals only — no imports, no closed-over
|
|
17
|
+
* identifiers): the Astro seats and the 500.html emitter execute it
|
|
18
|
+
* STRINGIFIED via `serializeErrorPageInstrumentation`.
|
|
19
|
+
*/
|
|
20
|
+
export type ErrorPagePush = (pageType: ErrorPageType) => void;
|
|
21
|
+
/**
|
|
22
|
+
* The SDK's copy of the D-14 default payload push — the same body the
|
|
23
|
+
* scaffolded policy module ships with. Used when no tenant module is in
|
|
24
|
+
* play: the Next `ErrorPageInstrumentation` component's default, the Astro
|
|
25
|
+
* seats' fallback, and the emitter's fallback tag. Self-contained by the
|
|
26
|
+
* ErrorPagePush contract (it serializes via toString()).
|
|
27
|
+
*/
|
|
28
|
+
export declare function defaultPushErrorPageView(pageType: ErrorPageType): void;
|
|
29
|
+
/**
|
|
30
|
+
* SDK MECHANISM (D-15, code-organization R-14): serializes a self-contained
|
|
31
|
+
* push function into the inline analytics `<script>` tag the Astro seats
|
|
32
|
+
* inject via `<Fragment set:html>` and the Next emit-error-pages CLI stamps
|
|
33
|
+
* into `public/500.html`. The marker attribute and IIFE wrapper are
|
|
34
|
+
* SDK-owned platform contract (D-14: `data-aci-error-page` is the STABLE
|
|
35
|
+
* GTM suppression signal); the function body is site policy. `</script`,
|
|
36
|
+
* `<script` and `<!--` occurrences in the body are escaped (the script-data
|
|
37
|
+
* escaping table) so a push can neither break out of the tag nor walk the
|
|
38
|
+
* HTML parser into the escaped-script-data states (FIND-007).
|
|
39
|
+
*
|
|
40
|
+
* A non-function push degrades to the default tag with a warning
|
|
41
|
+
* (FIND-002): this is the runtime mechanism boundary and error surfaces
|
|
42
|
+
* must not crash over a broken policy value — the emitter keeps its own
|
|
43
|
+
* fail-loud stance at build time.
|
|
44
|
+
*/
|
|
45
|
+
export declare function serializeErrorPageInstrumentation(push: ErrorPagePush, kind: ErrorPageKind): string;
|
|
46
|
+
/**
|
|
47
|
+
* The SDK DEFAULT analytics tag (D-15 amended D-14): the serialized
|
|
48
|
+
* `defaultPushErrorPageView`. Consumers fall back to it when no tenant
|
|
49
|
+
* policy module is provided — the Astro seats without
|
|
50
|
+
* `options.pushErrorPageView` and the emitter when
|
|
51
|
+
* `src/aci/error-page-instrumentation.mjs` is absent. The Next 'use client'
|
|
52
|
+
* component (`next/error-page-instrumentation.tsx`) cannot consume a script
|
|
53
|
+
* tag (RSC skips inline scripts); it calls the same default function
|
|
54
|
+
* directly, and the parity test beside it holds both deliveries in
|
|
55
|
+
* lockstep.
|
|
56
|
+
*/
|
|
57
|
+
export declare function errorPageInstrumentationTag(kind: ErrorPageKind): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Authorable error pages: the kind/page_type vocabulary (REQ-12 / D-14).
|
|
2
|
+
//
|
|
3
|
+
// This is a deliberately dependency-free CLIENT-SAFE leaf module: the Next
|
|
4
|
+
// error-surface components ('use client' — error-page-marker,
|
|
5
|
+
// error-page-instrumentation, error-seat) bundle it into the browser, so it
|
|
6
|
+
// must not import anything that reaches node builtins (content/error-pages.ts
|
|
7
|
+
// pulls content/routes.ts → node:path, which webpack rejects in client
|
|
8
|
+
// bundles). It is the SINGLE definition (quorum SIMPLE-005) — content/
|
|
9
|
+
// error-pages.ts re-exports it for server-side callers, and the next/astro
|
|
10
|
+
// barrels re-export from there.
|
|
11
|
+
/** Maps a binding kind to its D-14 page_type. */
|
|
12
|
+
export function errorPageType(kind) {
|
|
13
|
+
return kind === 'notFound' ? 'not_found' : 'error';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The SDK's copy of the D-14 default payload push — the same body the
|
|
17
|
+
* scaffolded policy module ships with. Used when no tenant module is in
|
|
18
|
+
* play: the Next `ErrorPageInstrumentation` component's default, the Astro
|
|
19
|
+
* seats' fallback, and the emitter's fallback tag. Self-contained by the
|
|
20
|
+
* ErrorPagePush contract (it serializes via toString()).
|
|
21
|
+
*/
|
|
22
|
+
export function defaultPushErrorPageView(pageType) {
|
|
23
|
+
window.dataLayer = window.dataLayer || [];
|
|
24
|
+
window.dataLayer.push({
|
|
25
|
+
event: 'page_view',
|
|
26
|
+
page_type: pageType,
|
|
27
|
+
page_path: window.location.pathname,
|
|
28
|
+
page_location: window.location.href,
|
|
29
|
+
page_referrer: document.referrer || '(direct)',
|
|
30
|
+
page_title: document.title,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* SDK MECHANISM (D-15, code-organization R-14): serializes a self-contained
|
|
35
|
+
* push function into the inline analytics `<script>` tag the Astro seats
|
|
36
|
+
* inject via `<Fragment set:html>` and the Next emit-error-pages CLI stamps
|
|
37
|
+
* into `public/500.html`. The marker attribute and IIFE wrapper are
|
|
38
|
+
* SDK-owned platform contract (D-14: `data-aci-error-page` is the STABLE
|
|
39
|
+
* GTM suppression signal); the function body is site policy. `</script`,
|
|
40
|
+
* `<script` and `<!--` occurrences in the body are escaped (the script-data
|
|
41
|
+
* escaping table) so a push can neither break out of the tag nor walk the
|
|
42
|
+
* HTML parser into the escaped-script-data states (FIND-007).
|
|
43
|
+
*
|
|
44
|
+
* A non-function push degrades to the default tag with a warning
|
|
45
|
+
* (FIND-002): this is the runtime mechanism boundary and error surfaces
|
|
46
|
+
* must not crash over a broken policy value — the emitter keeps its own
|
|
47
|
+
* fail-loud stance at build time.
|
|
48
|
+
*/
|
|
49
|
+
export function serializeErrorPageInstrumentation(push, kind) {
|
|
50
|
+
const pageType = errorPageType(kind);
|
|
51
|
+
let effectivePush = push;
|
|
52
|
+
if (typeof effectivePush !== 'function') {
|
|
53
|
+
console.warn('[aci error-pages] pushErrorPageView is not a function; serializing the SDK default push instead (D-15)');
|
|
54
|
+
effectivePush = defaultPushErrorPageView;
|
|
55
|
+
}
|
|
56
|
+
const source = escapeInlineScriptSource(effectivePush.toString());
|
|
57
|
+
assertSerializablePushSource(source);
|
|
58
|
+
return `<script data-aci-error-page="${pageType}">(${source})(${JSON.stringify(pageType)});</script>`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Script-data escaping (FIND-007): `</script` would close the inline tag,
|
|
62
|
+
* and `<!--`/`<script` open the escaped-script-data parser states where the
|
|
63
|
+
* real closing tag is consumed as text. All three are defused with a
|
|
64
|
+
* case-PRESERVING backslash after `<` — inside JS string literals `\/`,
|
|
65
|
+
* `\s` and `\!` decode to the original characters, so payload values
|
|
66
|
+
* survive byte-for-byte on every surface.
|
|
67
|
+
*/
|
|
68
|
+
function escapeInlineScriptSource(source) {
|
|
69
|
+
return source.replace(/<(\/script|script|!--)/gi, (match) => `<\\${match.slice(1)}`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Serialization smoke-eval (D-16, delta FIND-006): the tag ships the push
|
|
73
|
+
* STRINGIFIED, so its toString() must parse as a standalone function
|
|
74
|
+
* expression. Bound functions (`fn.bind(...)` stringifies to
|
|
75
|
+
* `function () { [native code] }`) and method-shorthand definitions
|
|
76
|
+
* (`{ pushErrorPageView(t) {…} }` stringifies without the `function`
|
|
77
|
+
* keyword) both produce source that is not an expression — the build must
|
|
78
|
+
* fail loudly here instead of shipping a tag that throws in every visitor's
|
|
79
|
+
* browser.
|
|
80
|
+
*/
|
|
81
|
+
function assertSerializablePushSource(source) {
|
|
82
|
+
try {
|
|
83
|
+
// Parse-only: the function body never executes.
|
|
84
|
+
new Function(`return (${source});`);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw new Error('pushErrorPageView cannot be serialized: its toString() output does not parse as a '
|
|
88
|
+
+ `function expression (${String(error)}). Likely causes: exporting a bound function `
|
|
89
|
+
+ '(.bind() loses the source) or a method-shorthand definition. Export a plain '
|
|
90
|
+
+ 'self-contained `function pushErrorPageView(pageType) {…}` instead (D-15 contract).');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The SDK DEFAULT analytics tag (D-15 amended D-14): the serialized
|
|
95
|
+
* `defaultPushErrorPageView`. Consumers fall back to it when no tenant
|
|
96
|
+
* policy module is provided — the Astro seats without
|
|
97
|
+
* `options.pushErrorPageView` and the emitter when
|
|
98
|
+
* `src/aci/error-page-instrumentation.mjs` is absent. The Next 'use client'
|
|
99
|
+
* component (`next/error-page-instrumentation.tsx`) cannot consume a script
|
|
100
|
+
* tag (RSC skips inline scripts); it calls the same default function
|
|
101
|
+
* directly, and the parity test beside it holds both deliveries in
|
|
102
|
+
* lockstep.
|
|
103
|
+
*/
|
|
104
|
+
export function errorPageInstrumentationTag(kind) {
|
|
105
|
+
return serializeErrorPageInstrumentation(defaultPushErrorPageView, kind);
|
|
106
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { RouteEntry } from './provider.js';
|
|
2
|
+
import type { KernelSiteConfig, SiteErrorPages } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Error-page binding validation (authorable-error-pages REQ-4/REQ-5/D-5),
|
|
5
|
+
* semantically paired with the Go compiler's
|
|
6
|
+
* `internal/domain/contentbuild/error_pages.go`. Under the warn-then-enforce
|
|
7
|
+
* migration posture, absent bindings are legal: they produce a loud warning
|
|
8
|
+
* and a `defaulted: true` result so callers fall back to the shipped
|
|
9
|
+
* branded-neutral default page (`defaultErrorPageHtml()`). Present bindings
|
|
10
|
+
* are strict in every posture: malformed routes, bindings resolving to no
|
|
11
|
+
* page, unpublished bound pages, and audience/experiment overlays on bound
|
|
12
|
+
* pages all fail closed.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: `src/next/error-seat-payload.ts` carries a SYNCHRONOUS twin of the
|
|
15
|
+
* binding validation below (it runs inside next.config evaluation where these
|
|
16
|
+
* async providers cannot be awaited). Keep the REQ-5 semantics of the two in
|
|
17
|
+
* lockstep when editing either; extraction into one shared core is deferred.
|
|
18
|
+
*/
|
|
19
|
+
/** D-5 migration posture for absent bindings; default is 'warn' today. */
|
|
20
|
+
export type ErrorPagePosture = 'warn' | 'enforce';
|
|
21
|
+
export { errorPageType, errorPageInstrumentationTag, defaultPushErrorPageView, serializeErrorPageInstrumentation, } from './error-page-kind.js';
|
|
22
|
+
export type { ErrorPageKind, ErrorPageType, ErrorPagePush } from './error-page-kind.js';
|
|
23
|
+
import type { ErrorPageKind } from './error-page-kind.js';
|
|
24
|
+
/**
|
|
25
|
+
* The one D-5 posture config surface, shared by every stack: the same
|
|
26
|
+
* variable the Go content build and publish materializer read.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ERROR_PAGES_POSTURE_ENV = "ACI_ERROR_PAGES_POSTURE";
|
|
29
|
+
/** Reads the D-5 posture from ACI_ERROR_PAGES_POSTURE; throws on unknown values (fail closed). */
|
|
30
|
+
export declare function errorPagePostureFromEnv(env?: Record<string, string | undefined>): ErrorPagePosture;
|
|
31
|
+
/** Mirrors the site.schema.json binding pattern: "/<segment>/". */
|
|
32
|
+
export declare const ERROR_PAGE_ROUTE_PATTERN: RegExp;
|
|
33
|
+
/**
|
|
34
|
+
* The slice of a content provider binding resolution needs. Both
|
|
35
|
+
* FileContentProvider and S3ContentProvider satisfy it.
|
|
36
|
+
*/
|
|
37
|
+
export interface ErrorPageContentSource {
|
|
38
|
+
listRoutes(): Promise<RouteEntry[]>;
|
|
39
|
+
getPage<T = unknown>(route: string): Promise<T>;
|
|
40
|
+
}
|
|
41
|
+
export interface ResolveErrorPageBindingsOptions {
|
|
42
|
+
/** Absent-binding posture; defaults to 'warn' (D-5). */
|
|
43
|
+
posture?: ErrorPagePosture;
|
|
44
|
+
/**
|
|
45
|
+
* Compiled content to resolve bindings against. When omitted, only shape
|
|
46
|
+
* validation runs (the serve-time seam: publish status was already
|
|
47
|
+
* enforced at build time).
|
|
48
|
+
*/
|
|
49
|
+
source?: ErrorPageContentSource;
|
|
50
|
+
}
|
|
51
|
+
export interface ErrorPageBindingsResolution {
|
|
52
|
+
/** Validated bindings; null when defaulted or invalid. */
|
|
53
|
+
bindings: SiteErrorPages | null;
|
|
54
|
+
/**
|
|
55
|
+
* True when no bindings are authored under the warn posture: callers must
|
|
56
|
+
* serve the shipped default page and surface `warnings` loudly.
|
|
57
|
+
*/
|
|
58
|
+
defaulted: boolean;
|
|
59
|
+
warnings: string[];
|
|
60
|
+
errors: string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validates the site's error-page bindings under the given posture.
|
|
64
|
+
* Never throws for validation outcomes: callers fail their build closed on a
|
|
65
|
+
* non-empty `errors` array.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveErrorPageBindings(siteConfig: Pick<KernelSiteConfig, 'errorPages'> | null | undefined, options?: ResolveErrorPageBindingsOptions): Promise<ErrorPageBindingsResolution>;
|
|
68
|
+
/**
|
|
69
|
+
* One kind's resolution for the serve-time seams (02-design "SDK core"):
|
|
70
|
+
* `route: null` + `defaulted: true` means "serve the shipped branded-neutral
|
|
71
|
+
* default page" (`defaultErrorPageHtml()` / `defaultErrorPageFragmentHtml()`).
|
|
72
|
+
*/
|
|
73
|
+
export interface ResolvedErrorPage {
|
|
74
|
+
kind: ErrorPageKind;
|
|
75
|
+
/** Bound public route path, or null when the shipped default applies. */
|
|
76
|
+
route: string | null;
|
|
77
|
+
defaulted: boolean;
|
|
78
|
+
/** Loud-warning material (REQ-4) — callers surface these, e.g. console.warn. */
|
|
79
|
+
warnings: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolves one error-page kind under the warn posture (default). Unlike the
|
|
83
|
+
* build-time `resolveErrorPageBindings`, this NEVER fails: error surfaces
|
|
84
|
+
* must not crash and must not re-enter the failing system (ANTI-2), so a
|
|
85
|
+
* broken binding — which the build already failed closed on (REQ-5) —
|
|
86
|
+
* degrades to the shipped default with the problem surfaced in `warnings`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveErrorPage(kind: ErrorPageKind, siteConfig: Pick<KernelSiteConfig, 'errorPages'> | null | undefined, options?: ResolveErrorPageBindingsOptions): Promise<ResolvedErrorPage>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { normalizeRoute } from './routes.js';
|
|
2
|
+
// The kind/page_type vocabulary is defined once (quorum SIMPLE-005) in the
|
|
3
|
+
// client-safe leaf module ./error-page-kind.ts — THIS module reaches
|
|
4
|
+
// node:path via routes.js, so 'use client' components must import the leaf
|
|
5
|
+
// directly. Re-exported here as the canonical server-side import site.
|
|
6
|
+
export { errorPageType, errorPageInstrumentationTag, defaultPushErrorPageView, serializeErrorPageInstrumentation, } from './error-page-kind.js';
|
|
7
|
+
const ERROR_PAGE_KINDS = ['notFound', 'catchAll'];
|
|
8
|
+
/**
|
|
9
|
+
* The one D-5 posture config surface, shared by every stack: the same
|
|
10
|
+
* variable the Go content build and publish materializer read.
|
|
11
|
+
*/
|
|
12
|
+
export const ERROR_PAGES_POSTURE_ENV = 'ACI_ERROR_PAGES_POSTURE';
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes a raw posture value exactly like the Go side
|
|
15
|
+
* (`contentbuild.normalizeErrorPagePosture`): absent/empty means warn, and an
|
|
16
|
+
* unknown value throws so a config typo cannot silently disable enforcement.
|
|
17
|
+
*/
|
|
18
|
+
function normalizeErrorPagePosture(raw) {
|
|
19
|
+
const value = raw ?? '';
|
|
20
|
+
if (value === '' || value === 'warn')
|
|
21
|
+
return 'warn';
|
|
22
|
+
if (value === 'enforce')
|
|
23
|
+
return 'enforce';
|
|
24
|
+
throw new Error(`unknown errorPages validation posture ${JSON.stringify(value)} (want "warn" or "enforce")`);
|
|
25
|
+
}
|
|
26
|
+
/** Reads the D-5 posture from ACI_ERROR_PAGES_POSTURE; throws on unknown values (fail closed). */
|
|
27
|
+
export function errorPagePostureFromEnv(env = process.env) {
|
|
28
|
+
return normalizeErrorPagePosture(env[ERROR_PAGES_POSTURE_ENV]);
|
|
29
|
+
}
|
|
30
|
+
/** Mirrors the site.schema.json binding pattern: "/<segment>/". */
|
|
31
|
+
export const ERROR_PAGE_ROUTE_PATTERN = /^\/(?:[A-Za-z0-9._~-]+\/)+$/;
|
|
32
|
+
const ERROR_PAGES_ABSENT_WARNING = 'site config has no errorPages bindings; the shipped branded-neutral default error page will be served for 404/5xx — author /404/ and /error/ pages and bind them via site.json#errorPages before the enforce posture lands (authorable-error-pages REQ-4/D-5)';
|
|
33
|
+
/**
|
|
34
|
+
* Validates the site's error-page bindings under the given posture.
|
|
35
|
+
* Never throws for validation outcomes: callers fail their build closed on a
|
|
36
|
+
* non-empty `errors` array.
|
|
37
|
+
*/
|
|
38
|
+
export async function resolveErrorPageBindings(siteConfig, options = {}) {
|
|
39
|
+
const warnings = [];
|
|
40
|
+
const errors = [];
|
|
41
|
+
// Unknown posture values fail closed (quorum BUG-002, Go parity): a typo
|
|
42
|
+
// in ACI_ERROR_PAGES_POSTURE must never silently mean "warn".
|
|
43
|
+
let posture;
|
|
44
|
+
try {
|
|
45
|
+
posture = normalizeErrorPagePosture(options.posture);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
49
|
+
return { bindings: null, defaulted: false, warnings, errors };
|
|
50
|
+
}
|
|
51
|
+
const raw = siteConfig?.errorPages;
|
|
52
|
+
if (raw == null) {
|
|
53
|
+
if (posture === 'enforce') {
|
|
54
|
+
errors.push('site config requires errorPages.notFound and errorPages.catchAll bindings (posture "enforce")');
|
|
55
|
+
return { bindings: null, defaulted: false, warnings, errors };
|
|
56
|
+
}
|
|
57
|
+
warnings.push(ERROR_PAGES_ABSENT_WARNING);
|
|
58
|
+
return { bindings: null, defaulted: true, warnings, errors };
|
|
59
|
+
}
|
|
60
|
+
// Present bindings are strict in every posture (REQ-5).
|
|
61
|
+
for (const kind of ERROR_PAGE_KINDS) {
|
|
62
|
+
const route = raw[kind];
|
|
63
|
+
if (typeof route !== 'string' || route === '') {
|
|
64
|
+
errors.push(`errorPages.${kind} binding is required`);
|
|
65
|
+
}
|
|
66
|
+
else if (!ERROR_PAGE_ROUTE_PATTERN.test(route)) {
|
|
67
|
+
errors.push(`errorPages.${kind} "${route}" is not a route path (want "/<segment>/")`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (errors.length > 0) {
|
|
71
|
+
return { bindings: null, defaulted: false, warnings, errors };
|
|
72
|
+
}
|
|
73
|
+
const bindings = { notFound: raw.notFound, catchAll: raw.catchAll };
|
|
74
|
+
if (options.source) {
|
|
75
|
+
const routes = await options.source.listRoutes();
|
|
76
|
+
const byPath = new Map(routes.map((entry) => [entry.path, entry]));
|
|
77
|
+
for (const kind of ERROR_PAGE_KINDS) {
|
|
78
|
+
const route = bindings[kind];
|
|
79
|
+
const normalized = normalizeRoute(route);
|
|
80
|
+
const entry = byPath.get(normalized);
|
|
81
|
+
if (!entry) {
|
|
82
|
+
errors.push(`errorPages.${kind} "${route}" does not resolve to any page`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entry.variants && Object.keys(entry.variants).length > 0) {
|
|
86
|
+
errors.push(`errorPages.${kind} "${route}" must not have audience or experiment overlays`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const page = await options.source
|
|
90
|
+
.getPage(normalized)
|
|
91
|
+
.catch(() => null);
|
|
92
|
+
if (!page || page.status !== 'published') {
|
|
93
|
+
errors.push(`errorPages.${kind} "${route}" must be a published page (status "${page?.status ?? 'unknown'}")`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (errors.length > 0) {
|
|
98
|
+
return { bindings: null, defaulted: false, warnings, errors };
|
|
99
|
+
}
|
|
100
|
+
return { bindings, defaulted: false, warnings, errors };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolves one error-page kind under the warn posture (default). Unlike the
|
|
104
|
+
* build-time `resolveErrorPageBindings`, this NEVER fails: error surfaces
|
|
105
|
+
* must not crash and must not re-enter the failing system (ANTI-2), so a
|
|
106
|
+
* broken binding — which the build already failed closed on (REQ-5) —
|
|
107
|
+
* degrades to the shipped default with the problem surfaced in `warnings`.
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveErrorPage(kind, siteConfig, options = {}) {
|
|
110
|
+
const resolution = await resolveErrorPageBindings(siteConfig, options);
|
|
111
|
+
if (resolution.errors.length > 0 || resolution.defaulted || !resolution.bindings) {
|
|
112
|
+
return {
|
|
113
|
+
kind,
|
|
114
|
+
route: null,
|
|
115
|
+
defaulted: true,
|
|
116
|
+
warnings: [...resolution.warnings, ...resolution.errors]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { kind, route: resolution.bindings[kind], defaulted: false, warnings: resolution.warnings };
|
|
120
|
+
}
|
package/dist/content/index.d.ts
CHANGED
package/dist/content/index.js
CHANGED
package/dist/content/types.d.ts
CHANGED
|
@@ -85,9 +85,21 @@ export interface KernelSiteConfig {
|
|
|
85
85
|
description?: string;
|
|
86
86
|
siteName?: string;
|
|
87
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* Error-page route bindings (authorable-error-pages REQ-1). Both values
|
|
90
|
+
* are public route paths (`/<segment>/`) resolving to ordinary published
|
|
91
|
+
* pages. Optional under the warn-then-enforce posture (REQ-4/D-5):
|
|
92
|
+
* absence is legal and produces a loud build/publish warning plus the
|
|
93
|
+
* shipped branded-neutral default page.
|
|
94
|
+
*/
|
|
95
|
+
errorPages?: SiteErrorPages;
|
|
88
96
|
/** Personalization collect-tag config (design/04); absent = disabled. */
|
|
89
97
|
personalization?: SitePersonalizationConfig;
|
|
90
98
|
}
|
|
99
|
+
export interface SiteErrorPages {
|
|
100
|
+
notFound: string;
|
|
101
|
+
catchAll: string;
|
|
102
|
+
}
|
|
91
103
|
export interface KernelRouteMetadata {
|
|
92
104
|
title: string;
|
|
93
105
|
description: string;
|
package/dist/next/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readdirSync, existsSync } from 'fs';
|
|
|
2
2
|
import { join, relative } from 'path';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { createContentWatchWebpackPlugin } from './content-watch.js';
|
|
5
|
+
import { buildErrorSeatPayload } from './error-seat-payload.js';
|
|
5
6
|
const require = createRequire(import.meta.url);
|
|
6
7
|
export function withAci(nextConfig = {}, options = {}) {
|
|
7
8
|
if (process.env.TURBOPACK) {
|
|
@@ -11,6 +12,21 @@ export function withAci(nextConfig = {}, options = {}) {
|
|
|
11
12
|
'`next dev` / `next build` without --turbopack.');
|
|
12
13
|
}
|
|
13
14
|
const publicPaths = collectPublicPaths(join(process.cwd(), 'public'));
|
|
15
|
+
// Authorable error pages (D-9): the Next error seat is client-only, so the
|
|
16
|
+
// bound catchAll page's content is serialized here — while next.config is
|
|
17
|
+
// evaluated — and injected into the bundle via DefinePlugin below. The D-5
|
|
18
|
+
// posture rides ACI_ERROR_PAGES_POSTURE (the same variable the Go stack
|
|
19
|
+
// reads): under warn (default), absent bindings/content and non-REQ-5
|
|
20
|
+
// serialization failures warn and serialize the shipped default (REQ-4);
|
|
21
|
+
// under enforce they fail the build closed, and unknown posture values
|
|
22
|
+
// fail closed too. Present-but-broken bindings fail closed always (REQ-5).
|
|
23
|
+
const errorSeat = buildErrorSeatPayload(undefined, process.env.ACI_ERROR_PAGES_POSTURE);
|
|
24
|
+
if (errorSeat.errors.length > 0) {
|
|
25
|
+
throw new Error(`ACI errorPages bindings are invalid (site.json#errorPages):\n- ${errorSeat.errors.join('\n- ')}`);
|
|
26
|
+
}
|
|
27
|
+
for (const warning of errorSeat.warnings) {
|
|
28
|
+
console.warn(`\n[aci error-pages] WARNING: ${warning}\n`);
|
|
29
|
+
}
|
|
14
30
|
const passthroughRoutes = normalizePassthroughRoutes(options.passthroughRoutes ?? []);
|
|
15
31
|
const assetPrefix = options.assetPrefix || '/.gradial-dam';
|
|
16
32
|
const assetOrigin = options.assetOrigin || '';
|
|
@@ -82,6 +98,7 @@ export function withAci(nextConfig = {}, options = {}) {
|
|
|
82
98
|
'__ACI_PUBLIC_PATHS__': JSON.stringify(publicPaths),
|
|
83
99
|
'__ACI_PASSTHROUGH_ROUTES__': JSON.stringify(passthroughRoutes),
|
|
84
100
|
'__ACI_ASSET_PREFIX__': JSON.stringify(assetPrefix),
|
|
101
|
+
'__ACI_ERROR_PAGE__': JSON.stringify(errorSeat.payload),
|
|
85
102
|
}));
|
|
86
103
|
return typeof userWebpack === 'function'
|
|
87
104
|
? userWebpack(config, context)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ErrorPageKind, type ErrorPagePush } from '../content/error-page-kind.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error-surface analytics MECHANISM (authorable-error-pages REQ-12 / D-14 /
|
|
4
|
+
* D-15): calls the site's push function exactly ONCE per error view with
|
|
5
|
+
* the D-14 page_type. The push BODY is site policy — the seats forward the
|
|
6
|
+
* tenant-owned scaffolded module's `pushErrorPageView` via `push`; without
|
|
7
|
+
* it, `defaultPushErrorPageView` (the scaffolded default's twin) runs.
|
|
8
|
+
*
|
|
9
|
+
* Delivery MUST be a 'use client' useEffect: the not-found seat is
|
|
10
|
+
* client-rendered from the RSC flight payload, where React skips execution
|
|
11
|
+
* of inline script elements (spike A learning 3). The SDK error seats mount
|
|
12
|
+
* this only with error render context; ordinary routes — including direct
|
|
13
|
+
* GET /404/ and /error/ previews — never render it (REQ-12).
|
|
14
|
+
*
|
|
15
|
+
* Renders nothing. The seats (createNotFound, ErrorSeat) render the inert
|
|
16
|
+
* `data-aci-error-page` marker themselves — server-side where possible, so
|
|
17
|
+
* gates can assert it in fetched documents (a client component's output
|
|
18
|
+
* never appears in the serialized flight text).
|
|
19
|
+
*/
|
|
20
|
+
export declare function ErrorPageInstrumentation({ kind, push, }: {
|
|
21
|
+
kind: ErrorPageKind;
|
|
22
|
+
push?: ErrorPagePush;
|
|
23
|
+
}): null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
// Client-safe leaf import — see error-page-marker.tsx.
|
|
4
|
+
import { defaultPushErrorPageView, errorPageType, } from '../content/error-page-kind.js';
|
|
5
|
+
/**
|
|
6
|
+
* Error-surface analytics MECHANISM (authorable-error-pages REQ-12 / D-14 /
|
|
7
|
+
* D-15): calls the site's push function exactly ONCE per error view with
|
|
8
|
+
* the D-14 page_type. The push BODY is site policy — the seats forward the
|
|
9
|
+
* tenant-owned scaffolded module's `pushErrorPageView` via `push`; without
|
|
10
|
+
* it, `defaultPushErrorPageView` (the scaffolded default's twin) runs.
|
|
11
|
+
*
|
|
12
|
+
* Delivery MUST be a 'use client' useEffect: the not-found seat is
|
|
13
|
+
* client-rendered from the RSC flight payload, where React skips execution
|
|
14
|
+
* of inline script elements (spike A learning 3). The SDK error seats mount
|
|
15
|
+
* this only with error render context; ordinary routes — including direct
|
|
16
|
+
* GET /404/ and /error/ previews — never render it (REQ-12).
|
|
17
|
+
*
|
|
18
|
+
* Renders nothing. The seats (createNotFound, ErrorSeat) render the inert
|
|
19
|
+
* `data-aci-error-page` marker themselves — server-side where possible, so
|
|
20
|
+
* gates can assert it in fetched documents (a client component's output
|
|
21
|
+
* never appears in the serialized flight text).
|
|
22
|
+
*/
|
|
23
|
+
export function ErrorPageInstrumentation({ kind, push, }) {
|
|
24
|
+
const pageType = errorPageType(kind);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let effectivePush = push ?? defaultPushErrorPageView;
|
|
27
|
+
if (typeof effectivePush !== 'function') {
|
|
28
|
+
// FIND-002: a non-function push (broken config plumbing) must degrade
|
|
29
|
+
// to the default, never crash the error surface itself.
|
|
30
|
+
console.warn('[aci error-pages] pushErrorPageView is not a function; using the SDK default push (D-15)');
|
|
31
|
+
effectivePush = defaultPushErrorPageView;
|
|
32
|
+
}
|
|
33
|
+
effectivePush(pageType);
|
|
34
|
+
}, [pageType, push]);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ErrorPageKind } from '../content/error-page-kind.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inert, invisible marker naming the error surface
|
|
4
|
+
* (`data-aci-error-page="not_found"|"error"`). Rendered by the seats next to
|
|
5
|
+
* ErrorPageInstrumentation as the fetch-level assertion hook for build and
|
|
6
|
+
* serve gates; its absence on ordinary routes proves direct /404/ and
|
|
7
|
+
* /error/ visits carry no error-typed instrumentation (REQ-12).
|
|
8
|
+
*/
|
|
9
|
+
export declare function ErrorPageMarker({ kind }: {
|
|
10
|
+
kind: ErrorPageKind;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Deliberately NO 'use client' directive: this module is shared by the
|
|
3
|
+
// server-rendered not-found seat and the client-only error seat. At the
|
|
4
|
+
// not-found seat the marker must be a SERVER-rendered element so it lands in
|
|
5
|
+
// the RSC flight payload — a client component's rendered output never
|
|
6
|
+
// appears in the serialized document, which is exactly why the build/serve
|
|
7
|
+
// gates could not assert a marker owned by ErrorPageInstrumentation.
|
|
8
|
+
// Import the client-safe leaf directly: content/error-pages.js reaches
|
|
9
|
+
// node:path via routes.js, which webpack rejects in browser bundles.
|
|
10
|
+
import { errorPageType } from '../content/error-page-kind.js';
|
|
11
|
+
/**
|
|
12
|
+
* Inert, invisible marker naming the error surface
|
|
13
|
+
* (`data-aci-error-page="not_found"|"error"`). Rendered by the seats next to
|
|
14
|
+
* ErrorPageInstrumentation as the fetch-level assertion hook for build and
|
|
15
|
+
* serve gates; its absence on ordinary routes proves direct /404/ and
|
|
16
|
+
* /error/ visits carry no error-typed instrumentation (REQ-12).
|
|
17
|
+
*/
|
|
18
|
+
export function ErrorPageMarker({ kind }) {
|
|
19
|
+
return _jsx("span", { hidden: true, "data-aci-error-page": errorPageType(kind) });
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { type FetchConfig } from './server.js';
|
|
3
|
+
import { type ErrorPageKind, type ErrorPagePush, type ResolvedErrorPage } from '../content/error-pages.js';
|
|
4
|
+
import type { KernelSiteConfig } from '../content/types.js';
|
|
5
|
+
import type { Registry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Next runtime seams for authorable error pages (REQ-6, 02-design "SDK
|
|
8
|
+
* core"): `resolveErrorPage(kind)` + `renderPageForRoute(route)` — the pair
|
|
9
|
+
* both spike wrappers hand-rolled — plus `createNotFound(registry)`, which
|
|
10
|
+
* composes them so the starter's `not-found.tsx` stays thin.
|
|
11
|
+
*/
|
|
12
|
+
export interface NextResolvedErrorPage extends ResolvedErrorPage {
|
|
13
|
+
siteConfig: KernelSiteConfig | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolves one error-page binding from the active site config under the
|
|
17
|
+
* warn posture (D-5). Never throws: an unreachable site config degrades to
|
|
18
|
+
* the shipped default — an error surface must not re-enter the failing
|
|
19
|
+
* system (ANTI-2).
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveErrorPage(kind: ErrorPageKind, config?: FetchConfig): Promise<NextResolvedErrorPage>;
|
|
22
|
+
/**
|
|
23
|
+
* Renders the content page bound to `route` through the normal page
|
|
24
|
+
* pipeline (createPage: layout slots, fragments, registry blocks, collect/
|
|
25
|
+
* assignments tags), resolving to the same tree the catch-all route would
|
|
26
|
+
* serve. Throws whatever the pipeline throws — including the notFound()
|
|
27
|
+
* control error for missing content — so callers at error seats must wrap
|
|
28
|
+
* it (createNotFound does).
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderPageForRoute(registry: Registry, route: string): Promise<ReactElement>;
|
|
31
|
+
/**
|
|
32
|
+
* The shipped branded-neutral default error page (REQ-4/D-5) as an inline
|
|
33
|
+
* fragment — used when no binding exists or the bound content cannot be
|
|
34
|
+
* rendered. Self-contained by construction (inline styles only, REQ-14).
|
|
35
|
+
*/
|
|
36
|
+
export declare function DefaultErrorPageContent(): import("react").JSX.Element;
|
|
37
|
+
/**
|
|
38
|
+
* Builds the component for Next's native not-found seat
|
|
39
|
+
* (`src/app/not-found.tsx`). The SDK catch-all invokes `notFound()` on
|
|
40
|
+
* missing content, so this renders with a real HTTP 404 (REQ-3/REQ-6).
|
|
41
|
+
*
|
|
42
|
+
* Fallback contract: if the bound notFound content is itself missing or
|
|
43
|
+
* fails to render at request time, the shipped default renders INLINE —
|
|
44
|
+
* the seat never re-invokes notFound() (a recursive seat was the spike's
|
|
45
|
+
* kill-list item) and never throws.
|
|
46
|
+
*
|
|
47
|
+
* `pushErrorPageView` (D-15): the tenant-owned policy module's push,
|
|
48
|
+
* forwarded to ErrorPageInstrumentation; omitted → the SDK default.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createNotFound(registry: Registry, config?: FetchConfig & {
|
|
51
|
+
pushErrorPageView?: ErrorPagePush;
|
|
52
|
+
}): () => Promise<ReactElement>;
|