@gradial/aci 0.1.53-rc.2 → 0.1.53-rc.4
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 +77 -0
- package/README.md +26 -4
- package/dist/astro/index.js +2 -5
- package/dist/cli/emit-error-pages.d.mts +2 -1
- package/dist/cli/emit-error-pages.mjs +13 -270
- package/dist/content/component-block.d.ts +17 -0
- package/dist/content/component-block.js +40 -0
- package/dist/content/resolve-slots.js +2 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/internal/aci-root.d.ts +1 -0
- package/dist/internal/aci-root.js +49 -0
- package/dist/next/config.js +6 -13
- package/dist/next/error-seat-payload.d.ts +1 -1
- package/dist/next/error-seat-payload.js +1 -2
- package/dist/next/error-seat.js +3 -4
- package/dist/next/render.js +2 -8
- package/dist/platform-tests/adapters/astro.d.ts +2 -0
- package/dist/platform-tests/adapters/astro.js +101 -0
- package/dist/platform-tests/adapters/next.d.ts +9 -0
- package/dist/platform-tests/adapters/next.js +112 -0
- package/dist/platform-tests/adapters/site.d.ts +29 -0
- package/dist/platform-tests/adapters/site.js +92 -0
- package/dist/platform-tests/adapters/types.d.ts +27 -0
- package/dist/platform-tests/adapters/types.js +1 -0
- package/dist/platform-tests/index.d.ts +47 -0
- package/dist/platform-tests/index.js +50 -0
- package/dist/platform-tests/sample-props.d.ts +2 -0
- package/dist/platform-tests/sample-props.js +82 -0
- package/dist/platform-tests/site-check.d.ts +51 -0
- package/dist/platform-tests/site-check.js +193 -0
- package/dist/registry.d.ts +1 -1
- package/dist/testing/conformance/index.d.ts +2 -1
- package/dist/testing/conformance/index.js +2 -1
- package/dist/testing/conformance/next-stubs.d.ts +13 -4
- package/dist/testing/conformance/next-stubs.js +76 -4
- package/dist/testing/conformance/preset.js +1 -2
- package/dist/testing/conformance/root-identity.d.ts +0 -29
- package/dist/testing/conformance/root-identity.js +36 -344
- package/dist/testing/conformance/types.d.ts +1 -1
- package/dist/types/component.d.ts +11 -3
- package/dist/types/component.js +10 -1
- package/package.json +9 -8
- package/src/cli/emit-error-pages.mjs +13 -273
- package/src/types/component.test.ts +17 -0
- package/src/types/component.ts +18 -3
- package/templates/astro/starter-content/src/components/chrome/Footer.astro +3 -4
- package/templates/astro/starter-content/src/components/chrome/Header.astro +3 -4
- package/templates/astro/starter-content/src/components/elements/Button.astro +4 -4
- package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +3 -4
- package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/HomeHero.astro +3 -4
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -4
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +6 -0
- package/templates/common/.agents/skills/authoring/references/block-registry.md +41 -7
- package/templates/common/.agents/skills/authoring/references/sdk-migrations.md +87 -0
- package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +9 -9
- package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +9 -9
- package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +10 -10
- package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +5 -4
- package/templates/nextjs/template/package.json.tmpl +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,59 @@
|
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
|
+
- Public `aci schemas compile` and `aci error-pages prepare|emit` commands let
|
|
8
|
+
site build scripts compile code-owned artifacts without invoking SDK internals.
|
|
9
|
+
|
|
10
|
+
- `AciRootProps` (the new name of `RootIdentityProps`, which stays as an
|
|
11
|
+
alias) and `aciRootAttrs(props)`, exported from `@gradial/aci`.
|
|
12
|
+
- The `rootIdentity` check covers every file that calls `createRegistry(`,
|
|
13
|
+
renders a component no content places with props derived from its
|
|
14
|
+
contract's schema, and prints its coverage: components checked, and each
|
|
15
|
+
one not run with the reason.
|
|
7
16
|
- Page `metadata.robots` accepts boolean `index` and `follow` directives.
|
|
8
17
|
Next and Astro emit the corresponding HTML tag. Publish excludes pages with
|
|
9
18
|
`index: false` from the release sitemap.
|
|
10
19
|
|
|
11
20
|
### Migration required
|
|
12
21
|
|
|
22
|
+
- Error-artifact `prepare` no longer compiles authored content. Sites relying
|
|
23
|
+
on that side effect must run `content:compile` during local development or
|
|
24
|
+
explicit content validation. Production Next builds require contracts and
|
|
25
|
+
code only; authored error-page content is resolved during ACI preview/publish.
|
|
26
|
+
|
|
27
|
+
- **Block root identity: render the root on every render** (ADR 0051) — each
|
|
28
|
+
registered component must put `aciRootAttrs(props)` (or its rest props) on
|
|
29
|
+
its own root element, and must render that root even when its content is
|
|
30
|
+
empty; the `rootIdentity` conformance check now fails a component that
|
|
31
|
+
renders nothing. Runbook: `.agents/skills/authoring/references/sdk-migrations.md`,
|
|
32
|
+
section "Root identity (0.1.52-rc.24)".
|
|
33
|
+
- **Migration required: test loader** — the `rootIdentity` check in
|
|
34
|
+
`@gradial/aci/testing` imports your component registry, so your test
|
|
35
|
+
process must be able to load TypeScript, JSX, and your `tsconfig` path
|
|
36
|
+
aliases. Run your test suite with the `tsx` loader, as the starters do:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
"test": "node --import tsx --test tests/**/*.test.ts"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A test process without the loader (for example `node --test` or
|
|
43
|
+
`node --test --experimental-strip-types`) now fails `npm test` with:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
rootIdentity: cannot load src/cms/registry.ts: this test process cannot resolve the path alias "@/cms".
|
|
47
|
+
Run tests with: node --import tsx --test
|
|
48
|
+
See the @gradial/aci CHANGELOG, "Migration required: test loader".
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If you turned the check off with `checks: { rootIdentity: false }` because
|
|
52
|
+
it could not load CSS modules or path aliases, remove that option: the
|
|
53
|
+
check now loads stylesheets, images, fonts, `next/image`, `next/link`, and
|
|
54
|
+
`next/font` as inert stubs, and reads every registry file in the site.
|
|
55
|
+
|
|
56
|
+
There is no build gate. `next build` and `astro build` do not run the
|
|
57
|
+
check. The local signal is `npm test`, where the conformance check renders
|
|
58
|
+
every registered component in isolation. A component it cannot load or
|
|
59
|
+
render is reported as not run, never as a failure.
|
|
13
60
|
- Existing Astro sites must install `@gradial/aci/astro/Head` and pass resolved
|
|
14
61
|
metadata as one object through their route and layout. Next sites must upgrade
|
|
15
62
|
the SDK and rebuild. See [page metadata migration](./migrations/page-metadata-head.md).
|
|
@@ -18,8 +65,38 @@
|
|
|
18
65
|
before telling an author that `noindex` is present. No platform test currently
|
|
19
66
|
enforces that check on older site builds.
|
|
20
67
|
|
|
68
|
+
### Removed
|
|
69
|
+
|
|
70
|
+
- `astroAuthoredBlocks` is no longer exported from `@gradial/aci/testing`:
|
|
71
|
+
the Astro `rootIdentity` check renders components instead of scanning
|
|
72
|
+
`dist/`. `findRootIdentityFailures` and `RootIdentityFailure` are no longer
|
|
73
|
+
exported either; `checkRootIdentity` and `formatRootIdentityFailures` stay.
|
|
74
|
+
|
|
21
75
|
### Changed
|
|
22
76
|
|
|
77
|
+
- Next production builds and error-artifact preparation are content-independent
|
|
78
|
+
without `ACI_CODE_ARTIFACT_BUILD`. Local Next development still validates and
|
|
79
|
+
previews authored error seats; production authored error pages belong to
|
|
80
|
+
preview/publish. Sites should compile contracts in `prebuild`, validate content
|
|
81
|
+
explicitly, and remove build-phase switches from their scripts.
|
|
82
|
+
|
|
83
|
+
- **`rootIdentity` reads every registry file again.** The `registry` option
|
|
84
|
+
introduced in 0.1.52-rc.33 is still accepted, but it is no longer required:
|
|
85
|
+
the check covers every file that calls `createRegistry(` (a main registry
|
|
86
|
+
and an error-page registry are both checked). Sites that added the option
|
|
87
|
+
for rc.33 need no change.
|
|
88
|
+
- **Nested blocks carry their root identity in their props.** The renderers
|
|
89
|
+
now put `data-aci-block` and `data-aci-component` into the `props` of
|
|
90
|
+
every component block nested in a block's props, at any depth (a block
|
|
91
|
+
slot, or a list inside an item such as `tabs[].blocks`). A container that
|
|
92
|
+
renders child blocks from plain data with `<Component {...block.props} />`
|
|
93
|
+
now emits each child's identity on the child's root, so `rootIdentity`
|
|
94
|
+
passes for it without a site change. Containers that call
|
|
95
|
+
`renderChildren` are unchanged: the same values are applied once. A
|
|
96
|
+
container that rebuilds a child's props from selected fields (for example
|
|
97
|
+
`<Component text={block.props.text} />`) still drops the identity and is
|
|
98
|
+
still reported. The `props` of nested blocks now contain the two identity
|
|
99
|
+
keys at render time; the compiled content does not change.
|
|
23
100
|
- Immutable code builds (`ACI_CODE_ARTIFACT_BUILD=1`) no longer read or compile
|
|
24
101
|
authored error-page content. Native client error seats use the shipped
|
|
25
102
|
default; publish resolves authored CDN error pages independently. Error
|
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.4` 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.4 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.4 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.4 init --list
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
## Personalization: The One-Tag Install
|
|
@@ -223,3 +223,25 @@ From `packages/sdk`:
|
|
|
223
223
|
pnpm run build
|
|
224
224
|
pnpm run generate-starters
|
|
225
225
|
```
|
|
226
|
+
|
|
227
|
+
## Public code-build commands
|
|
228
|
+
|
|
229
|
+
Use the installed `aci` binary from npm scripts, not files beneath the package's
|
|
230
|
+
`src`, `dist`, or `bin` directories:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"scripts": {
|
|
235
|
+
"schemas:compile": "aci schemas compile",
|
|
236
|
+
"prebuild": "npm run schemas:compile",
|
|
237
|
+
"build": "aci error-pages prepare && next build && aci error-pages emit",
|
|
238
|
+
"content:compile": "npm run schemas:compile && aci content compile --content ./.content --out ./.aci/compiled"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`schemas compile` reads the component/data registry paths from `.aci.yaml` and
|
|
244
|
+
recompiles contracts without reading authored content. The error commands prepare
|
|
245
|
+
code-owned analytics policy and emit the default hosting fallback (preserving a
|
|
246
|
+
site-owned `public/500.html`). All three honor `--site-dir`; helper failures exit
|
|
247
|
+
nonzero. Local development/content validation must explicitly compile content.
|
package/dist/astro/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { contentWatchPlugin, damAssetPlugin, devRefreshPort, } from '../dev/content-watch.js';
|
|
4
4
|
import { createDevRefreshBrowserScript } from '../dev/index.js';
|
|
5
|
+
import { withAciRootProps, withNestedAciRootProps } from '../internal/aci-root.js';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Static paths helper — for Astro SSG
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -57,14 +58,10 @@ export function createRenderChildren(registry) {
|
|
|
57
58
|
return blocks.map((block) => {
|
|
58
59
|
const entry = registry.find((r) => r.id === block.component);
|
|
59
60
|
const hasBlockSlots = !!entry?.contract.blockSlots;
|
|
60
|
-
const props =
|
|
61
|
+
const props = withAciRootProps(withNestedAciRootProps(block.props), block);
|
|
61
62
|
if (hasBlockSlots) {
|
|
62
63
|
props.renderChildren = renderChildren;
|
|
63
64
|
}
|
|
64
|
-
// sdk I-19 (ADR 0051): root identity props are applied last so content
|
|
65
|
-
// props cannot overwrite them. The template spreads them onto the root.
|
|
66
|
-
props['data-aci-block'] = block.id;
|
|
67
|
-
props['data-aci-component'] = block.component;
|
|
68
65
|
return { Component: entry?.component, props };
|
|
69
66
|
});
|
|
70
67
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export function
|
|
2
|
+
export function prepare(root: string | undefined, content: any): Promise<string>;
|
|
3
3
|
/**
|
|
4
4
|
* D-15 policy resolution for the 500.html instrumentation tag (page_type
|
|
5
5
|
* "error"; self-contained inline delivery — this copy is a real static
|
|
@@ -38,3 +38,4 @@ export function writeInstrumentationArtifact(root: string | undefined, content:
|
|
|
38
38
|
* loudly instead.
|
|
39
39
|
*/
|
|
40
40
|
export function stampInstrumentation(html: any, instrumentationTag: any): any;
|
|
41
|
+
export function emit(root: string | undefined, content: any): Promise<void>;
|
|
@@ -1,80 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// ACI_CODE_ARTIFACT_BUILD=1 makes both phases content-independent: prepare
|
|
7
|
-
// serializes only the code-owned policy at its existing .aci/compiled path.
|
|
8
|
-
// Emit uses the shipped default without resolving authored error-page bindings.
|
|
9
|
-
//
|
|
10
|
-
// Two phases. The Next template wires both around `next build`; the Astro
|
|
11
|
-
// template runs only `prepare` before `astro build` (its 404/500 seats
|
|
12
|
-
// render through the framework, but the D-16 instrumentation artifact must
|
|
13
|
-
// still reach the compiled content root for the AWS lane):
|
|
14
|
-
//
|
|
15
|
-
// prepare (before `next build`)
|
|
16
|
-
// Ensures compiled content exists at .aci/compiled so withAci() can
|
|
17
|
-
// serialize the bound catchAll page for the client-only error seat
|
|
18
|
-
// (D-9). Compiles ./.content via the platform CLI when needed; when
|
|
19
|
-
// neither compiled content nor authored content exists, it warns and
|
|
20
|
-
// leaves the shipped default in play (REQ-4, warn posture). Then writes
|
|
21
|
-
// error-pages/instrumentation.json (both kinds' serialized tags) into
|
|
22
|
-
// the compiled content root so the tag crosses the JS→Go boundary as
|
|
23
|
-
// data: builders retain the instrumentation file at its existing capsule
|
|
24
|
-
// path, and the Go publish materializer stamps the kind-matching tag into the CDN
|
|
25
|
-
// error artifacts (D-16). An unserializable tenant push fails the build
|
|
26
|
-
// loudly here (delta FIND-006).
|
|
27
|
-
//
|
|
28
|
-
// emit (after `next build`)
|
|
29
|
-
// Materializes `public/500.html` from the page bound to
|
|
30
|
-
// site.json#errorPages.catchAll by booting the standalone build,
|
|
31
|
-
// fetching the bound route, and post-processing the document. Boot-and-
|
|
32
|
-
// fetch beats re-rendering the page here because the standalone server
|
|
33
|
-
// is the only renderer that produces the document exactly as served
|
|
34
|
-
// (layout shell, resolved fragments, RSC-injected head and styles).
|
|
35
|
-
// Post-processing:
|
|
36
|
-
// - all script tags and preload/prefetch links stripped — this
|
|
37
|
-
// removes the collect and assignment tags (REQ-11) along with the
|
|
38
|
-
// framework runtime, which is useless in a statically served copy;
|
|
39
|
-
// - same-origin stylesheets inlined from .next/static (REQ-14 spirit:
|
|
40
|
-
// inline styles; note @font-face/image URLs inside the CSS keep
|
|
41
|
-
// their same-origin paths — this copy is served by the Vercel
|
|
42
|
-
// platform on the deployment that owns those assets);
|
|
43
|
-
// - one self-contained inline script appended pushing the D-14
|
|
44
|
-
// `page_view` (page_type "error") — a static document executes
|
|
45
|
-
// inline scripts, unlike the flight-rendered not-found seat. The
|
|
46
|
-
// push body is site policy (D-15): the tenant-owned
|
|
47
|
-
// src/aci/error-page-instrumentation.mjs when present, the SDK
|
|
48
|
-
// default otherwise;
|
|
49
|
-
// - hard size budget: ≤150 KB raw (REQ-14) — exceeding it fails the
|
|
50
|
-
// build; trimming the design system or falling back to system
|
|
51
|
-
// fonts is the sanctioned escape hatch.
|
|
52
|
-
// Skips when the site already provides its own public/500.html (a file
|
|
53
|
-
// without the generation marker); regenerates its own prior output.
|
|
54
|
-
// Absent bindings emit the shipped branded-neutral default (REQ-4).
|
|
55
|
-
// Vercel picks static per-status files out of the build output
|
|
56
|
-
// ("file presence = configuration"); on the current Pro plan the file
|
|
57
|
-
// serves app-level 500s only and lights up for platform 5xx on a plan
|
|
58
|
-
// upgrade (REQ-10, P-1).
|
|
59
|
-
//
|
|
60
|
-
// Usage: node ./node_modules/@gradial/aci/src/cli/emit-error-pages.mjs <prepare|emit>
|
|
61
|
-
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
// Code-build error artifacts. prepare transports code-owned analytics policy;
|
|
3
|
+
// emit writes a content-independent static 500. Authored error-page bindings
|
|
4
|
+
// and rendering belong to content preview/publish, never the framework build.
|
|
5
|
+
// Usage: emit-error-pages.mjs <prepare|emit>
|
|
62
6
|
import fs from 'node:fs';
|
|
63
|
-
import net from 'node:net';
|
|
64
7
|
import path from 'node:path';
|
|
65
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
66
9
|
const MARKER = '<!-- aci:error-pages generated -->';
|
|
67
10
|
const SIZE_BUDGET = 150 * 1024;
|
|
68
|
-
const BOOT_DEADLINE_MS = 60_000;
|
|
69
11
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
70
12
|
const siteRoot = process.cwd();
|
|
71
13
|
const contentRoot = path.resolve(siteRoot, process.env.ACI_CONTENT_ROOT || '.aci/compiled');
|
|
72
14
|
function log(message) {
|
|
73
15
|
console.log(`[aci error-pages] ${message}`);
|
|
74
16
|
}
|
|
75
|
-
function warn(message) {
|
|
76
|
-
console.warn(`\n[aci error-pages] WARNING: ${message}\n`);
|
|
77
|
-
}
|
|
78
17
|
function fail(message) {
|
|
79
18
|
console.error(`\n[aci error-pages] ERROR: ${message}\n`);
|
|
80
19
|
process.exit(1);
|
|
@@ -82,175 +21,11 @@ function fail(message) {
|
|
|
82
21
|
async function sdkContent() {
|
|
83
22
|
return await import(pathToFileURL(path.join(packageRoot, 'dist', 'content', 'index.js')).href);
|
|
84
23
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// prepare
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// Retain the deployed transport path for SDK-first upgrades. This file is
|
|
92
|
-
// executable policy, not a compiled page snapshot; do not compile content here.
|
|
93
|
-
export async function prepareCodeArtifact(root = siteRoot, content) {
|
|
24
|
+
// Keep the existing capsule transport path for SDK-first upgrades. This file
|
|
25
|
+
// is executable policy, not a compiled content snapshot.
|
|
26
|
+
export async function prepare(root = siteRoot, content) {
|
|
94
27
|
return writeInstrumentationArtifact(root, content, path.join(root, '.aci', 'compiled'));
|
|
95
28
|
}
|
|
96
|
-
async function prepare() {
|
|
97
|
-
if (process.env.ACI_CODE_ARTIFACT_BUILD === '1') {
|
|
98
|
-
// Code artifacts carry executable policy only. The selected content and
|
|
99
|
-
// its error-page bindings belong to preview/publish, never this build.
|
|
100
|
-
await prepareCodeArtifact();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (hasCompiledContent()) {
|
|
104
|
-
log(`compiled content present at ${contentRoot}`);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
const authored = path.join(siteRoot, '.content');
|
|
108
|
-
if (!fs.existsSync(authored)) {
|
|
109
|
-
warn(`neither compiled content (${contentRoot}) nor authored content (./.content) exists; `
|
|
110
|
-
+ 'the error seat and 500.html will serve the shipped default page (REQ-4)');
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
log('compiling ./.content for the error-page build steps');
|
|
114
|
-
execFileSync('node', [path.join(packageRoot, 'bin', 'aci.js'), 'content', 'compile', '--content', authored, '--out', contentRoot], { cwd: siteRoot, stdio: 'inherit' });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// D-16: the instrumentation artifact rides the compiled content root into
|
|
118
|
-
// the AWS-lane code capsule (source/.aci/compiled/**). Written even when
|
|
119
|
-
// content is absent — the tenant policy module is independent of authored
|
|
120
|
-
// content, and an artifact the Go materializer never receives silently
|
|
121
|
-
// degrades to the default tags (the exact gap D-16 closes).
|
|
122
|
-
try {
|
|
123
|
-
await writeInstrumentationArtifact();
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
fail(error.message);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// emit
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
async function resolveCatchAllRoute() {
|
|
133
|
-
const { resolveErrorPageBindings, defaultErrorPageHtml, errorPagePostureFromEnv } = await sdkContent();
|
|
134
|
-
// D-5 posture: the one shared config surface (ACI_ERROR_PAGES_POSTURE);
|
|
135
|
-
// unknown values fail closed here just like the Go stack.
|
|
136
|
-
let posture;
|
|
137
|
-
try {
|
|
138
|
-
posture = errorPagePostureFromEnv();
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
fail(error.message);
|
|
142
|
-
}
|
|
143
|
-
if (!hasCompiledContent()) {
|
|
144
|
-
if (posture === 'enforce') {
|
|
145
|
-
fail(`no compiled content at ${contentRoot}; the enforce posture fails closed (D-5)`);
|
|
146
|
-
}
|
|
147
|
-
warn(`no compiled content at ${contentRoot}; emitting the shipped default 500.html (REQ-4)`);
|
|
148
|
-
return { route: null, defaultHtml: defaultErrorPageHtml() };
|
|
149
|
-
}
|
|
150
|
-
const { FileContentProvider } = await import(pathToFileURL(path.join(packageRoot, 'dist', 'providers', 'file.js')).href);
|
|
151
|
-
const provider = new FileContentProvider(contentRoot);
|
|
152
|
-
const siteConfig = await provider.getSiteConfig();
|
|
153
|
-
const resolution = await resolveErrorPageBindings(siteConfig, { source: provider, posture });
|
|
154
|
-
if (resolution.errors.length > 0) {
|
|
155
|
-
fail(`errorPages bindings are invalid (REQ-5, fail closed):\n- ${resolution.errors.join('\n- ')}`);
|
|
156
|
-
}
|
|
157
|
-
for (const warning of resolution.warnings)
|
|
158
|
-
warn(warning);
|
|
159
|
-
if (resolution.defaulted || !resolution.bindings) {
|
|
160
|
-
return { route: null, defaultHtml: defaultErrorPageHtml() };
|
|
161
|
-
}
|
|
162
|
-
return { route: resolution.bindings.catchAll, defaultHtml: defaultErrorPageHtml() };
|
|
163
|
-
}
|
|
164
|
-
async function freePort() {
|
|
165
|
-
return new Promise((resolve, reject) => {
|
|
166
|
-
const server = net.createServer();
|
|
167
|
-
server.unref();
|
|
168
|
-
server.on('error', reject);
|
|
169
|
-
server.listen(0, '127.0.0.1', () => {
|
|
170
|
-
const { port } = server.address();
|
|
171
|
-
server.close(() => resolve(port));
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
async function fetchRenderedRoute(route) {
|
|
176
|
-
const serverPath = path.join(siteRoot, '.next', 'standalone', 'server.js');
|
|
177
|
-
if (!fs.existsSync(serverPath)) {
|
|
178
|
-
fail(`${serverPath} missing — the emit phase must run after \`next build\` (withAci uses standalone output)`);
|
|
179
|
-
}
|
|
180
|
-
const port = await freePort();
|
|
181
|
-
const logChunks = [];
|
|
182
|
-
const child = spawn('node', [serverPath], {
|
|
183
|
-
cwd: siteRoot,
|
|
184
|
-
detached: true,
|
|
185
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
186
|
-
env: {
|
|
187
|
-
...process.env,
|
|
188
|
-
NODE_ENV: 'production',
|
|
189
|
-
PORT: String(port),
|
|
190
|
-
HOSTNAME: '127.0.0.1',
|
|
191
|
-
ACI_CONTENT_PROVIDER: 'file',
|
|
192
|
-
ACI_CONTENT_ROOT: contentRoot,
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
child.stdout.on('data', (c) => logChunks.push(String(c)));
|
|
196
|
-
child.stderr.on('data', (c) => logChunks.push(String(c)));
|
|
197
|
-
const exited = new Promise((resolve) => {
|
|
198
|
-
child.on('exit', resolve);
|
|
199
|
-
child.on('error', (error) => {
|
|
200
|
-
logChunks.push(`spawn failed: ${error.message}\n`);
|
|
201
|
-
resolve(null);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
try {
|
|
205
|
-
const url = `http://127.0.0.1:${port}${route}`;
|
|
206
|
-
const start = Date.now();
|
|
207
|
-
let last = 'no attempt made';
|
|
208
|
-
while (Date.now() - start < BOOT_DEADLINE_MS) {
|
|
209
|
-
try {
|
|
210
|
-
const response = await fetch(url, { redirect: 'follow' });
|
|
211
|
-
if (response.status === 200)
|
|
212
|
-
return await response.text();
|
|
213
|
-
last = `status ${response.status}`;
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
last = error.message;
|
|
217
|
-
}
|
|
218
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
219
|
-
}
|
|
220
|
-
fail(`GET ${url} did not render within ${BOOT_DEADLINE_MS}ms (last: ${last})\n--- server output ---\n${logChunks.join('').slice(-4000)}`);
|
|
221
|
-
return '';
|
|
222
|
-
}
|
|
223
|
-
finally {
|
|
224
|
-
const killGroup = (signal) => {
|
|
225
|
-
if (!child.pid)
|
|
226
|
-
return;
|
|
227
|
-
try {
|
|
228
|
-
process.kill(-child.pid, signal);
|
|
229
|
-
}
|
|
230
|
-
catch { /* gone */ }
|
|
231
|
-
};
|
|
232
|
-
killGroup('SIGTERM');
|
|
233
|
-
const killTimer = setTimeout(() => killGroup('SIGKILL'), 10_000);
|
|
234
|
-
await exited;
|
|
235
|
-
clearTimeout(killTimer);
|
|
236
|
-
killGroup('SIGKILL');
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function inlineStylesheets(html) {
|
|
240
|
-
return html.replace(/<link\b[^>]*rel="stylesheet"[^>]*>/gi, (tag) => {
|
|
241
|
-
const href = tag.match(/href="([^"]+)"/i)?.[1];
|
|
242
|
-
if (!href || !href.startsWith('/_next/')) {
|
|
243
|
-
warn(`dropping non-inlinable stylesheet ${href ?? '<unknown>'} from 500.html (self-containment, REQ-14)`);
|
|
244
|
-
return '';
|
|
245
|
-
}
|
|
246
|
-
const cssPath = path.join(siteRoot, '.next', href.replace(/^\/_next\//, ''));
|
|
247
|
-
if (!fs.existsSync(cssPath)) {
|
|
248
|
-
warn(`stylesheet ${href} not found in .next output; dropping it from 500.html`);
|
|
249
|
-
return '';
|
|
250
|
-
}
|
|
251
|
-
return `<style>${fs.readFileSync(cssPath, 'utf8')}</style>`;
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
29
|
// Relative convention path of the tenant-owned instrumentation policy
|
|
255
30
|
// module (D-15, code-organization R-14).
|
|
256
31
|
const INSTRUMENTATION_MODULE = path.join('src', 'aci', 'error-page-instrumentation.mjs');
|
|
@@ -351,48 +126,16 @@ export function stampInstrumentation(html, instrumentationTag) {
|
|
|
351
126
|
}
|
|
352
127
|
return stamped;
|
|
353
128
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
// Strip every script tag: framework runtime chunks are dead weight in a
|
|
357
|
-
// statically served copy, and this removes the collect script and the
|
|
358
|
-
// data-aci-assignments tag (REQ-11) in one sweep.
|
|
359
|
-
out = out.replace(/<script\b[\s\S]*?<\/script>/gi, '');
|
|
360
|
-
out = out.replace(/<link\b[^>]*rel="(?:preload|modulepreload|prefetch)"[^>]*>/gi, '');
|
|
361
|
-
out = inlineStylesheets(out);
|
|
362
|
-
return stampInstrumentation(out, instrumentationTag);
|
|
363
|
-
}
|
|
364
|
-
async function emit() {
|
|
365
|
-
const publicDir = path.join(siteRoot, 'public');
|
|
129
|
+
export async function emit(root = siteRoot, content) {
|
|
130
|
+
const publicDir = path.join(root, 'public');
|
|
366
131
|
const target = path.join(publicDir, '500.html');
|
|
367
132
|
if (fs.existsSync(target) && !fs.readFileSync(target, 'utf8').includes(MARKER)) {
|
|
368
133
|
log('public/500.html already provided by the site; leaving it untouched (skip-if-present)');
|
|
369
134
|
return;
|
|
370
135
|
}
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
let instrumentationTag;
|
|
375
|
-
try {
|
|
376
|
-
instrumentationTag = await resolveInstrumentationTag();
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
fail(error.message);
|
|
380
|
-
}
|
|
381
|
-
let html;
|
|
382
|
-
try {
|
|
383
|
-
if (!route) {
|
|
384
|
-
// The shipped default is already self-contained; it gets the same
|
|
385
|
-
// instrumentation-plus-marker append used for rendered pages.
|
|
386
|
-
html = stampInstrumentation(defaultHtml, instrumentationTag);
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
log(`rendering bound catchAll page ${route} for public/500.html`);
|
|
390
|
-
html = postProcess(await fetchRenderedRoute(route), instrumentationTag);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
fail(error.message);
|
|
395
|
-
}
|
|
136
|
+
const sdk = content ?? (await sdkContent());
|
|
137
|
+
const instrumentationTag = await resolveInstrumentationTag(root, sdk);
|
|
138
|
+
const html = stampInstrumentation(sdk.defaultErrorPageHtml(), instrumentationTag);
|
|
396
139
|
const size = Buffer.byteLength(html);
|
|
397
140
|
if (size > SIZE_BUDGET) {
|
|
398
141
|
fail(`public/500.html is ${size} bytes, over the ${SIZE_BUDGET}-byte budget (REQ-14). `
|
|
@@ -400,7 +143,7 @@ async function emit() {
|
|
|
400
143
|
}
|
|
401
144
|
fs.mkdirSync(publicDir, { recursive: true });
|
|
402
145
|
fs.writeFileSync(target, html);
|
|
403
|
-
log(`wrote public/500.html (${size} bytes
|
|
146
|
+
log(`wrote public/500.html (${size} bytes, shipped default)`);
|
|
404
147
|
}
|
|
405
148
|
// Run only when executed as a CLI — the module is also imported by unit
|
|
406
149
|
// tests for stampInstrumentation.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ComponentBlock {
|
|
2
|
+
id: string;
|
|
3
|
+
component: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The content compiler's block predicate (`compiler.IsComponentBlock`): an
|
|
7
|
+
* explicit `$type: "component"` marker, or a non-empty string `component`
|
|
8
|
+
* with an object `props`. The shared Go/TS vector
|
|
9
|
+
* `internal/domain/content/compiler/testdata/component-block-predicate.v1.json`
|
|
10
|
+
* pins both sides.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isComponentBlock(node: Record<string, unknown>): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Every component block in a compiled page payload that carries an id,
|
|
15
|
+
* parents before children, nested slots included.
|
|
16
|
+
*/
|
|
17
|
+
export declare function collectComponentBlocks(payload: unknown): ComponentBlock[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The content compiler's component-block rule, shared by rendering (root
|
|
2
|
+
// identity, slot resolution) and the rootIdentity conformance check.
|
|
3
|
+
/**
|
|
4
|
+
* The content compiler's block predicate (`compiler.IsComponentBlock`): an
|
|
5
|
+
* explicit `$type: "component"` marker, or a non-empty string `component`
|
|
6
|
+
* with an object `props`. The shared Go/TS vector
|
|
7
|
+
* `internal/domain/content/compiler/testdata/component-block-predicate.v1.json`
|
|
8
|
+
* pins both sides.
|
|
9
|
+
*/
|
|
10
|
+
export function isComponentBlock(node) {
|
|
11
|
+
if (node.$type === 'component')
|
|
12
|
+
return true;
|
|
13
|
+
const props = node.props;
|
|
14
|
+
return typeof node.component === 'string' && node.component !== ''
|
|
15
|
+
&& typeof props === 'object' && props !== null && !Array.isArray(props);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Every component block in a compiled page payload that carries an id,
|
|
19
|
+
* parents before children, nested slots included.
|
|
20
|
+
*/
|
|
21
|
+
export function collectComponentBlocks(payload) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const walk = (node) => {
|
|
24
|
+
if (Array.isArray(node)) {
|
|
25
|
+
for (const item of node)
|
|
26
|
+
walk(item);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (typeof node !== 'object' || node === null)
|
|
30
|
+
return;
|
|
31
|
+
const record = node;
|
|
32
|
+
if (isComponentBlock(record) && typeof record.id === 'string' && record.id !== '') {
|
|
33
|
+
out.push({ id: record.id, component: typeof record.component === 'string' ? record.component : '' });
|
|
34
|
+
}
|
|
35
|
+
for (const value of Object.values(record))
|
|
36
|
+
walk(value);
|
|
37
|
+
};
|
|
38
|
+
walk(payload);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isComponentBlock } from './component-block.js';
|
|
1
2
|
/**
|
|
2
3
|
* Shared Go/TS limit contract (sdk I-21). These values are pinned against
|
|
3
4
|
* internal/domain/content/compiler/testdata/limit-conformance.v1.json by
|
|
@@ -105,10 +106,7 @@ function* walkValue(value, ancestry, state) {
|
|
|
105
106
|
const block = (yield* walkValue(fragmentToBlock(fragment, id), chain, state));
|
|
106
107
|
return Object.defineProperty(block, fragmentRootScope, { value: true });
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
-
|| (typeof node.component === 'string' && node.component.length > 0
|
|
110
|
-
&& node.props !== null && typeof node.props === 'object' && !Array.isArray(node.props));
|
|
111
|
-
if (isBlock && ++state.blocks > MAX_EXPANDED_BLOCKS) {
|
|
109
|
+
if (isComponentBlock(node) && ++state.blocks > MAX_EXPANDED_BLOCKS) {
|
|
112
110
|
throw new FragmentResolutionError(`ACI rendered page exceeds ${MAX_EXPANDED_BLOCKS} component blocks`);
|
|
113
111
|
}
|
|
114
112
|
const result = {};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export { createRegistry, registryLookup } from './registry.js';
|
|
|
3
3
|
export type { Registry, RegistryEntry, AnyComponent } from './registry.js';
|
|
4
4
|
export { blockRef, blockRefArray } from './types/block-ref.js';
|
|
5
5
|
export type { BlockRef, BlockRefEntry, BlockRefMeta, BlockRefUnion } from './types/block-ref.js';
|
|
6
|
-
export type { ComponentContract, ComponentContractID, ComponentContractValidator, ComponentRegistry, ComponentValidationContext, ComponentValidationIssue, InferComponentProps, RootIdentityProps, } from './types/component.js';
|
|
6
|
+
export type { ComponentContract, ComponentContractID, ComponentContractValidator, ComponentRegistry, ComponentValidationContext, ComponentValidationIssue, InferComponentProps, AciRootProps, RootIdentityProps, } from './types/component.js';
|
|
7
|
+
export { aciRootAttrs } from './types/component.js';
|
|
7
8
|
export { ImageSchema, imageAttrs, primaryImageSource, type Image, type ImageSource, type ImageAttrs, type ImageSlotContract, type SlotOutput, } from './types/image.js';
|
|
8
9
|
export { LinkSchema, type Link, type LinkTarget, type LinkResolverContext, resolveLink, resolveHref, assertSafeHref, normalizeLinkRel, } from './types/link.js';
|
|
9
10
|
export { HeadingSchema, InlineContentSchema, RichTextValueSchema, type Heading, type InlineContent, type RichTextValue, } from './types/rich-text.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { defineComponentContract } from './define-component.js';
|
|
2
2
|
export { createRegistry, registryLookup } from './registry.js';
|
|
3
3
|
export { blockRef, blockRefArray } from './types/block-ref.js';
|
|
4
|
+
export { aciRootAttrs } from './types/component.js';
|
|
4
5
|
export { ImageSchema, imageAttrs, primaryImageSource, } from './types/image.js';
|
|
5
6
|
export { LinkSchema, resolveLink, resolveHref, assertSafeHref, normalizeLinkRel, } from './types/link.js';
|
|
6
7
|
export { HeadingSchema, InlineContentSchema, RichTextValueSchema, } from './types/rich-text.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|