@gradial/aci 0.1.53-rc.7 → 0.2.0-canary.715
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 +93 -5
- package/README.md +19 -15
- package/dist/astro/index.d.ts +4 -1
- package/dist/cli/compile-registry.mjs +8 -1
- package/dist/cli/materialize-template.mjs +1 -1
- package/dist/content/component-block.js +1 -1
- package/dist/internal/release-marker.d.ts +1 -0
- package/dist/internal/release-marker.js +11 -0
- package/dist/next/config.js +2 -0
- package/dist/next/page.js +2 -1
- package/dist/next/platform-tracing.d.ts +10 -0
- package/dist/next/platform-tracing.js +24 -0
- package/dist/platform-tests/index.d.ts +2 -1
- package/dist/platform-tests/index.js +2 -1
- package/dist/render/astro.d.ts +2 -0
- package/dist/render/astro.js +88 -0
- package/dist/render/errors.d.ts +23 -0
- package/dist/render/errors.js +31 -0
- package/dist/render/index.d.ts +22 -0
- package/dist/render/index.js +73 -0
- package/dist/render/input.d.ts +27 -0
- package/dist/render/input.js +99 -0
- package/dist/render/mintlify.d.ts +2 -0
- package/dist/render/mintlify.js +170 -0
- package/dist/render/next.d.ts +2 -0
- package/dist/render/next.js +142 -0
- package/dist/render/types.d.ts +62 -0
- package/dist/render/types.js +1 -0
- package/dist/types/component.d.ts +1 -1
- package/dist/verify/bounds.d.ts +11 -0
- package/dist/verify/bounds.js +80 -0
- package/dist/verify/catalog.d.ts +17 -0
- package/dist/verify/catalog.js +168 -0
- package/dist/verify/check.d.ts +75 -0
- package/dist/verify/check.js +12 -0
- package/dist/verify/checks/assets.d.ts +2 -0
- package/dist/verify/checks/assets.js +22 -0
- package/dist/verify/checks/blocks.d.ts +2 -0
- package/dist/verify/checks/blocks.js +31 -0
- package/dist/verify/checks/document.d.ts +2 -0
- package/dist/verify/checks/document.js +75 -0
- package/dist/verify/checks/fetch.d.ts +2 -0
- package/dist/verify/checks/fetch.js +27 -0
- package/dist/verify/checks/links.d.ts +14 -0
- package/dist/verify/checks/links.js +70 -0
- package/dist/verify/checks/resource.d.ts +15 -0
- package/dist/verify/checks/resource.js +38 -0
- package/dist/verify/checks/status.d.ts +2 -0
- package/dist/verify/checks/status.js +43 -0
- package/dist/verify/concurrency.d.ts +4 -0
- package/dist/verify/concurrency.js +32 -0
- package/dist/verify/fetcher.d.ts +19 -0
- package/dist/verify/fetcher.js +75 -0
- package/dist/verify/html/refs.d.ts +14 -0
- package/dist/verify/html/refs.js +81 -0
- package/dist/verify/html/scan.d.ts +17 -0
- package/dist/verify/html/scan.js +139 -0
- package/dist/verify/html/srcset.d.ts +2 -0
- package/dist/verify/html/srcset.js +42 -0
- package/dist/verify/index.d.ts +47 -0
- package/dist/verify/index.js +39 -0
- package/dist/verify/judged.d.ts +21 -0
- package/dist/verify/judged.js +47 -0
- package/dist/verify/local/cli.d.ts +1 -0
- package/dist/verify/local/cli.js +60 -0
- package/dist/verify/local/index.d.ts +12 -0
- package/dist/verify/local/index.js +10 -0
- package/dist/verify/local/run.d.ts +17 -0
- package/dist/verify/local/run.js +179 -0
- package/dist/verify/local/types.d.ts +44 -0
- package/dist/verify/local/types.js +6 -0
- package/dist/verify/paths.d.ts +47 -0
- package/dist/verify/paths.js +82 -0
- package/dist/verify/reduce.d.ts +48 -0
- package/dist/verify/reduce.js +147 -0
- package/dist/verify/registry.d.ts +9 -0
- package/dist/verify/registry.js +21 -0
- package/dist/verify/source.d.ts +88 -0
- package/dist/verify/source.js +44 -0
- package/dist/verify/sources/candidate.d.ts +70 -0
- package/dist/verify/sources/candidate.js +272 -0
- package/dist/verify/sources/network.d.ts +39 -0
- package/dist/verify/sources/network.js +68 -0
- package/dist/verify/sources/url.d.ts +28 -0
- package/dist/verify/sources/url.js +108 -0
- package/dist/verify/suite.d.ts +26 -0
- package/dist/verify/suite.js +42 -0
- package/dist/verify/types.d.ts +19 -0
- package/dist/verify/types.js +4 -0
- package/dist/verify/unit.d.ts +33 -0
- package/dist/verify/unit.js +136 -0
- package/dist/verify/version.d.ts +2 -0
- package/dist/verify/version.js +7 -0
- package/package.json +22 -7
- package/src/cli/compile-registry.mjs +9 -1
- package/src/cli/materialize-template.mjs +1 -1
- package/src/types/component.ts +1 -1
- package/templates/astro/template/README.md.tmpl +20 -1
- package/templates/astro/template/package.json.tmpl +2 -1
- package/templates/common/.agents/AGENTS.md +2 -1
- package/templates/common/.agents/skills/aci/SKILL.md +28 -5
- package/templates/common/.agents/skills/authoring/references/block-registry.md +1 -1
- package/templates/common/.agents/skills/authoring/references/sdk-migrations.md +6 -3
- package/templates/common/.content/fragments/navbar/_index.json +1 -10
- package/templates/common/.content/fragments/site-footer/_index.json +1 -43
- package/templates/common/.content/pages/_index.json +2 -27
- package/templates/common/.github/workflows/aci-verify.yml +162 -0
- package/templates/common/README.md.tmpl +2 -0
- package/templates/common/starter-content/.content/config/site.json +0 -124
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +0 -8
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +1 -35
- package/templates/common/starter-content/.content/pages/_index.json +4 -16
- package/templates/nextjs/template/README.md.tmpl +20 -1
- package/templates/nextjs/template/package.json.tmpl +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
### Fixed
|
|
6
6
|
|
|
7
|
+
- The staged and live checks fetch the stable collect script
|
|
8
|
+
(`/_aci/aci.js`) at the judged release's own path,
|
|
9
|
+
`/__r/<release>/_aci/aci.js`. The stable URL answers the active
|
|
10
|
+
release's SDK, so on a Vercel staged preview (a deployment the active
|
|
11
|
+
pointer does not target yet) it answered 404 and every SSR publish with a
|
|
12
|
+
new code deployment failed `assets.notFound`.
|
|
13
|
+
- Page verification treats redirects as site logic. A page a redirect
|
|
14
|
+
answers (next.config redirects, vercel.json, a host rule) is judged on
|
|
15
|
+
where it lands: a 2xx passes with the warning `route.redirected` (the
|
|
16
|
+
published page is unreachable at its own URL; the hop chain is in the
|
|
17
|
+
message), and only the status check runs on it. A redirect that lands on
|
|
18
|
+
a 4xx or 5xx, loops, or still redirects after 10 hops is `status.notOk`.
|
|
19
|
+
The release marker is read on the page a redirect lands on, and not at
|
|
20
|
+
all when it lands on another origin. A link to a route the release does
|
|
21
|
+
not publish passes when the server redirects it to a 2xx. The candidate
|
|
22
|
+
server keeps an upstream redirect on its own origin, and answers the
|
|
23
|
+
site's own `next.config` redirects (`siteRedirects`, from the build's
|
|
24
|
+
routes manifest) for a path no page holds, so a link to a redirected
|
|
25
|
+
route passes before staging too.
|
|
26
|
+
|
|
7
27
|
- Astro root-identity conformance checks now load the site-configured UI renderers,
|
|
8
28
|
allowing nested React and other integration components to render in the isolated
|
|
9
29
|
container.
|
|
@@ -32,6 +52,55 @@
|
|
|
32
52
|
- Page `metadata.robots` accepts boolean `index` and `follow` directives.
|
|
33
53
|
Next and Astro emit the corresponding HTML tag. Publish excludes pages with
|
|
34
54
|
`index: false` from the release sitemap.
|
|
55
|
+
- Page verification fetches the images an inline `style` attribute
|
|
56
|
+
references (`url()`), so a missing hero background is `fetch.notFound`,
|
|
57
|
+
and warns with `document.unrenderedValue` when page text shows
|
|
58
|
+
`undefined`, `NaN` or `[object Object]`. The warning never blocks.
|
|
59
|
+
- `.aci/registry.json` records `sdkVersion`, the SDK that compiled it.
|
|
60
|
+
- `@gradial/aci/verify`: page verification, a map and a reduce whose loop the
|
|
61
|
+
caller owns. `testUnit({ ref, source, profile })` verifies one route ×
|
|
62
|
+
variant and returns a plain-JSON `UnitResult` (its findings and the facts
|
|
63
|
+
the release checks need), so units can run in other processes or CI jobs;
|
|
64
|
+
`reduce(unitResults, { routes, profile })` judges every unit's facts
|
|
65
|
+
against the release's routes and returns
|
|
66
|
+
`{ errors, warnings, sdkVersion, stats }`. `withFetchCache(source)` shares
|
|
67
|
+
one fetch cache across the units you run. `@gradial/aci/verify/local`
|
|
68
|
+
holds the local loop, `runSuite({ source, profile })`, and adds
|
|
69
|
+
`listLocalUnits`, `runLocalShard` (one `--shard i/n`) and
|
|
70
|
+
`reduceLocalResults` for a sharded CI run. Profiles: `health` (each page
|
|
71
|
+
answers 2xx, and an error page at `/_errors/404/` answers 404:
|
|
72
|
+
`status.*`), `smoke` (plus the page's own scripts, stylesheets and images
|
|
73
|
+
load: `assets.*`) and `full` (plus each page is a full HTML document,
|
|
74
|
+
`document.*`, and error pages need exactly one
|
|
75
|
+
`<script data-aci-error-page>` tag; every component block has its
|
|
76
|
+
`data-aci-block` root, `blocks.missingRoot`; every internal link goes to
|
|
77
|
+
a published route, `links.unpublishedTarget`; every script, stylesheet,
|
|
78
|
+
image, srcset candidate and file link answers, `fetch.notFound` and
|
|
79
|
+
`fetch.failed`). A failed same-origin or DAM URL is an error; a failed
|
|
80
|
+
third-party URL is a warning. One source kind:
|
|
81
|
+
`urlSource({ baseUrl, pages, auth?, address? })` reads pages over HTTP from
|
|
82
|
+
a dev server, a CI server, a candidate server, a staged preview or live.
|
|
83
|
+
`serveCandidate({ pages?, renderer?, units?, mounts? })` is the candidate
|
|
84
|
+
server: pre-rendered pages, or compiled pages rendered on request through
|
|
85
|
+
`@gradial/aci/render`, plus your build's assets (`siteAssetMounts`). `auth` headers only go to the server's
|
|
86
|
+
own origin. Pass `pages` to verify a subset; links are still checked
|
|
87
|
+
against every route. A page that cannot be rendered or reached is an
|
|
88
|
+
error with its `render.*` or `page.unreachable` code. A check that throws
|
|
89
|
+
is a `<check>.crashed` error on the page it was judging; the rest of the
|
|
90
|
+
run still judges. A page without a compiled payload is listed under
|
|
91
|
+
`stats.unjudged` for the checks that need one (`blocks`). The base render
|
|
92
|
+
has no `variantKey`.
|
|
93
|
+
- Next: `withAci()` traces `@gradial/aci/render` and `@gradial/aci/verify`
|
|
94
|
+
(their whole import closure) into the standalone output, where the
|
|
95
|
+
platform loads them.
|
|
96
|
+
- `@gradial/aci/render`: `createRenderer({ siteRoot, contentRoot })` starts
|
|
97
|
+
your built Next, Astro or Mintlify site once and `render(page)` returns one
|
|
98
|
+
compiled page's HTML, rejecting with a typed `RenderError` (`code`, `route`,
|
|
99
|
+
`variantKey`). The platform renders publish-time pages through it when your
|
|
100
|
+
installed SDK has it; older SDKs keep the platform's own harness. The base
|
|
101
|
+
render omits `variantKey` (`"base"` and `""` are refused as `render.input`),
|
|
102
|
+
and a page without a site config, domain or locale is `render.input`
|
|
103
|
+
instead of a guessed default.
|
|
35
104
|
|
|
36
105
|
### Migration required
|
|
37
106
|
|
|
@@ -46,6 +115,23 @@
|
|
|
46
115
|
empty; the `rootIdentity` conformance check now fails a component that
|
|
47
116
|
renders nothing. Runbook: `.agents/skills/authoring/references/sdk-migrations.md`,
|
|
48
117
|
section "Root identity (0.1.52-rc.24)".
|
|
118
|
+
- **Publish verifies every page** (sdk I-33) — before a release is staged,
|
|
119
|
+
publish renders every page and variant and runs the `@gradial/aci/verify`
|
|
120
|
+
`full` profile over them; any error refuses the publish with
|
|
121
|
+
`publish.verification_failed` and the findings (route, code, what to fix).
|
|
122
|
+
The block-root rule above is its `blocks.missingRoot` check. A check that
|
|
123
|
+
crashes is a `<check>.crashed` error on that page, which an owner can force
|
|
124
|
+
past. Run the same checks first: `aci verify` (below) locally, in
|
|
125
|
+
`npm test`, and in the `aci-verify.yml` CI workflow.
|
|
126
|
+
- **`npm test` runs `aci verify`; the site-stage preset is gone** — the
|
|
127
|
+
scaffold's `npm test` now ends with `npm run verify`, which is
|
|
128
|
+
`aci verify --build`. To adopt it in
|
|
129
|
+
an existing site, add `"verify": "aci verify --build"` and append
|
|
130
|
+
`&& npm run verify` to your `test` script. `siteStagePreset`,
|
|
131
|
+
`runLocalSiteStage`, `siteStageProblem` and `testUnit` are no longer
|
|
132
|
+
exported from `@gradial/aci/testing`: delete `tests/aci-platform.test.mjs`
|
|
133
|
+
and the `playwright` devDependency if you added them. The browser-driven
|
|
134
|
+
`livePage` and `rootIdentity` platform tests no longer run anywhere.
|
|
49
135
|
- **Migration required: test loader** — the `rootIdentity` check in
|
|
50
136
|
`@gradial/aci/testing` imports your component registry, so your test
|
|
51
137
|
process must be able to load TypeScript, JSX, and your `tsconfig` path
|
|
@@ -69,10 +155,13 @@
|
|
|
69
155
|
check now loads stylesheets, images, fonts, `next/image`, `next/link`, and
|
|
70
156
|
`next/font` as inert stubs, and reads every registry file in the site.
|
|
71
157
|
|
|
72
|
-
There is no build gate. `next build` and `astro build` do not run the
|
|
73
|
-
|
|
74
|
-
every registered component in isolation
|
|
75
|
-
|
|
158
|
+
There is no build gate. `next build` and `astro build` do not run the check.
|
|
159
|
+
The signals are `npm test` in your repository, where the conformance check
|
|
160
|
+
renders every registered component in isolation and `aci verify` renders
|
|
161
|
+
every page, and publish, which rejects a release when a rendered block has no
|
|
162
|
+
root identity. A component the conformance check cannot load or render is
|
|
163
|
+
reported as not run, never as a failure; `aci verify` and publish still check
|
|
164
|
+
the pages that use it.
|
|
76
165
|
- Existing Astro sites must install `@gradial/aci/astro/Head` and pass resolved
|
|
77
166
|
metadata as one object through their route and layout. Next sites must upgrade
|
|
78
167
|
the SDK and rebuild. See [page metadata migration](./migrations/page-metadata-head.md).
|
|
@@ -245,7 +334,6 @@ below) — delete it from any imports.
|
|
|
245
334
|
- `visitorProfileDataModel`: the schema had no server-side document contract
|
|
246
335
|
and no consumers. Drop it from imports; `audienceDataModels` no longer
|
|
247
336
|
includes it.
|
|
248
|
-
|
|
249
337
|
## 0.1.52-rc.7 - 2026-08-19
|
|
250
338
|
|
|
251
339
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agentic Content Infrastructure SDK
|
|
2
2
|
|
|
3
|
-
`@gradial/aci` `0.
|
|
3
|
+
`@gradial/aci` `0.2.0-canary.715` 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.
|
|
11
|
+
npm install @gradial/aci@0.2.0-canary.715 zod@^4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Root Import
|
|
@@ -72,23 +72,27 @@ including the `blog_body` contract pattern for page-specific container children.
|
|
|
72
72
|
## Block Root Identity
|
|
73
73
|
|
|
74
74
|
The renderer passes every block two props, `data-aci-block` (the authored
|
|
75
|
-
block id) and `data-aci-component`. A component extends `
|
|
76
|
-
|
|
77
|
-
events to content by (page path, block id):
|
|
75
|
+
block id) and `data-aci-component`. A component extends `AciRootProps` and
|
|
76
|
+
places exactly those props on its root element with `aciRootAttrs(props)`, so
|
|
77
|
+
analytics can join events to content by (page path, block id):
|
|
78
78
|
|
|
79
79
|
```tsx
|
|
80
|
-
import type
|
|
80
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
81
81
|
|
|
82
|
-
export function StatsBar(
|
|
83
|
-
|
|
82
|
+
export function StatsBar(props: ContentProps & AciRootProps) {
|
|
83
|
+
const { stats = [] } = props;
|
|
84
|
+
return <section {...aciRootAttrs(props)} className="…">…</section>;
|
|
84
85
|
}
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
lacks the
|
|
91
|
-
|
|
88
|
+
`RootIdentityProps` is the former name of `AciRootProps`, and a rest-spread
|
|
89
|
+
(`{ stats, ...root }`, then `{...root}` on the root) still works. Three checks
|
|
90
|
+
make this required: `createRegistry` fails typecheck for a component whose
|
|
91
|
+
props type lacks the keys; the default `rootIdentity` conformance check (and,
|
|
92
|
+
on Astro, `astro build` through `withAci()`) renders every registered
|
|
93
|
+
component in isolation and fails naming its source file; and `aci verify` and
|
|
94
|
+
publish run the `@gradial/aci/verify` `blocks` check over every rendered page.
|
|
95
|
+
`runConformanceChecks` is async; `await` it before reading
|
|
92
96
|
`.passed`.
|
|
93
97
|
|
|
94
98
|
## Mintlify
|
|
@@ -137,13 +141,13 @@ host bundles select and serve immutable release artifacts.
|
|
|
137
141
|
The first RC scaffold path is Next:
|
|
138
142
|
|
|
139
143
|
```bash
|
|
140
|
-
npx @gradial/aci@0.
|
|
144
|
+
npx @gradial/aci@0.2.0-canary.715 init my-site --template next-starter
|
|
141
145
|
```
|
|
142
146
|
|
|
143
147
|
List available templates:
|
|
144
148
|
|
|
145
149
|
```bash
|
|
146
|
-
npx @gradial/aci@0.
|
|
150
|
+
npx @gradial/aci@0.2.0-canary.715 init --list
|
|
147
151
|
```
|
|
148
152
|
|
|
149
153
|
## Personalization: The One-Tag Install
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -11,12 +11,15 @@ interface AstroConfigSetupParams {
|
|
|
11
11
|
}
|
|
12
12
|
interface AstroBuildDoneParams {
|
|
13
13
|
dir: URL;
|
|
14
|
+
logger?: {
|
|
15
|
+
info(message: string): void;
|
|
16
|
+
};
|
|
14
17
|
}
|
|
15
18
|
export interface AstroIntegrationLike {
|
|
16
19
|
name: string;
|
|
17
20
|
hooks: {
|
|
18
21
|
'astro:config:setup'(params: AstroConfigSetupParams): void;
|
|
19
|
-
'astro:build:done'?(params: AstroBuildDoneParams): void
|
|
22
|
+
'astro:build:done'?(params: AstroBuildDoneParams): void | Promise<void>;
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -96,6 +96,9 @@ async function compileRegistry(options) {
|
|
|
96
96
|
}
|
|
97
97
|
const registry = {
|
|
98
98
|
manifestVersion: '1',
|
|
99
|
+
// The installed SDK that built this artifact (ADR 0063): publish asks only
|
|
100
|
+
// whether it is recent enough to have produced a platform test's outcome.
|
|
101
|
+
sdkVersion: installedSDKVersion(),
|
|
99
102
|
generatedAt: new Date().toISOString(),
|
|
100
103
|
components: componentEntries,
|
|
101
104
|
dataModels: dataModelEntries,
|
|
@@ -106,6 +109,10 @@ async function compileRegistry(options) {
|
|
|
106
109
|
schemaFiles.push(pageSchemaPath);
|
|
107
110
|
return { registry: registryPath, schemas: schemaFiles };
|
|
108
111
|
}
|
|
112
|
+
function installedSDKVersion() {
|
|
113
|
+
const manifest = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
114
|
+
return manifest.version;
|
|
115
|
+
}
|
|
109
116
|
async function loadDefaultArray(modulePath, label) {
|
|
110
117
|
const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
|
|
111
118
|
const value = mod.default ?? mod.registry ?? mod.components ?? mod.dataModels;
|
|
@@ -17,7 +17,7 @@ const EXCLUDE_PATTERNS = [
|
|
|
17
17
|
/storybook-static/,
|
|
18
18
|
/starter-content/,
|
|
19
19
|
];
|
|
20
|
-
const COMMON_DIRS = ['.aci', '.agents', '.content', 'eslint-rules'];
|
|
20
|
+
const COMMON_DIRS = ['.aci', '.agents', '.content', 'eslint-rules', '.github'];
|
|
21
21
|
const COMMON_ROOT_FILES = ['.nvmrc'];
|
|
22
22
|
// Per-framework values rendered into shared templates (e.g. README.md.tmpl).
|
|
23
23
|
// Derived here so `aci init` and `generate-starters` can never disagree.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The content compiler's component-block rule, shared by rendering (root
|
|
2
|
-
// identity, slot resolution)
|
|
2
|
+
// identity, slot resolution), the verify `blocks` check and conformance.
|
|
3
3
|
/**
|
|
4
4
|
* The content compiler's block predicate (`compiler.IsComponentBlock`): an
|
|
5
5
|
* explicit `$type: "component"` marker, or a non-empty string `component`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release marker: `<meta name="aci-release" content="<releaseId>">`,
|
|
3
|
+
* written by the platform on every page of a release, never by the site.
|
|
4
|
+
* Publish injects it into every HTML page of an SSG release; the SDK's SSR
|
|
5
|
+
* page writes it per request from the release the router dispatched to. The
|
|
6
|
+
* platform's checks read it to know which release produced the HTML they
|
|
7
|
+
* judged, whatever an edge cached (hosting I-27). Pinned with the platform by
|
|
8
|
+
* internal/domain/publish/pageverify/testdata/platform-paths.v1.json.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export const RELEASE_MARKER = 'aci-release';
|
package/dist/next/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, relative } from 'path';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { createContentWatchWebpackPlugin } from './content-watch.js';
|
|
5
5
|
import { buildErrorSeatPayload } from './error-seat-payload.js';
|
|
6
|
+
import { withPlatformTraced } from './platform-tracing.js';
|
|
6
7
|
import { defaultErrorPageFragmentHtml } from '../content/default-error-page.js';
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
export function withAci(nextConfig = {}, options = {}) {
|
|
@@ -35,6 +36,7 @@ export function withAci(nextConfig = {}, options = {}) {
|
|
|
35
36
|
...nextConfig,
|
|
36
37
|
output: 'standalone',
|
|
37
38
|
outputFileTracingRoot: nextConfig.outputFileTracingRoot ?? process.cwd(),
|
|
39
|
+
outputFileTracingIncludes: withPlatformTraced(nextConfig.outputFileTracingIncludes),
|
|
38
40
|
headers: async () => {
|
|
39
41
|
const gradialHeaders = [
|
|
40
42
|
{
|
package/dist/next/page.js
CHANGED
|
@@ -7,6 +7,7 @@ import { renderBlocks } from './render.js';
|
|
|
7
7
|
import { AssignmentsScript } from './assignments-script.js';
|
|
8
8
|
import { CollectScript } from './collect-script.js';
|
|
9
9
|
import { renderProofContentFor } from '../internal/render-proof.js';
|
|
10
|
+
import { RELEASE_MARKER } from '../internal/release-marker.js';
|
|
10
11
|
import { parseHostedPageTarget } from './hosted-page-target.js';
|
|
11
12
|
/**
|
|
12
13
|
* Creates the catch-all route handler for a Next.js ACI site.
|
|
@@ -88,7 +89,7 @@ export function createPage(registry, options) {
|
|
|
88
89
|
}));
|
|
89
90
|
return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig }), renderProofContent
|
|
90
91
|
? _jsx("meta", { name: "aci-render-proof", content: renderProofContent })
|
|
91
|
-
: null] }));
|
|
92
|
+
: null, releaseId ? _jsx("meta", { name: RELEASE_MARKER, content: releaseId }) : null] }));
|
|
92
93
|
}
|
|
93
94
|
catch (error) {
|
|
94
95
|
if (error instanceof PageNotFoundError)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NextConfig } from 'next';
|
|
2
|
+
/**
|
|
3
|
+
* The SDK entries the platform loads from a Next site's standalone tree: the
|
|
4
|
+
* capsule's page harness imports `./render` (page_sdk.mjs) and the publish
|
|
5
|
+
* verify host imports `./verify` (verify_suite.mjs). No server code imports
|
|
6
|
+
* either, so Next's file tracing would prune them.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PLATFORM_ENTRIES: readonly ["./render", "./verify"];
|
|
9
|
+
export declare const PLATFORM_TRACED_FILES: string[];
|
|
10
|
+
export declare function withPlatformTraced(includes: NextConfig['outputFileTracingIncludes']): NonNullable<NextConfig['outputFileTracingIncludes']>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK entries the platform loads from a Next site's standalone tree: the
|
|
3
|
+
* capsule's page harness imports `./render` (page_sdk.mjs) and the publish
|
|
4
|
+
* verify host imports `./verify` (verify_suite.mjs). No server code imports
|
|
5
|
+
* either, so Next's file tracing would prune them.
|
|
6
|
+
*/
|
|
7
|
+
export const PLATFORM_ENTRIES = ['./render', './verify'];
|
|
8
|
+
// The import closure of PLATFORM_ENTRIES, as dist globs, plus the package
|
|
9
|
+
// manifest that resolves the exports (and that sdkVersion reads).
|
|
10
|
+
// platform-tracing.test.ts walks the closure from the sources and fails on
|
|
11
|
+
// any file these globs miss.
|
|
12
|
+
export const PLATFORM_TRACED_FILES = [
|
|
13
|
+
'./node_modules/@gradial/aci/package.json',
|
|
14
|
+
'./node_modules/@gradial/aci/dist/render/**/*.js',
|
|
15
|
+
'./node_modules/@gradial/aci/dist/runtime/**/*.js',
|
|
16
|
+
'./node_modules/@gradial/aci/dist/verify/**/*.js',
|
|
17
|
+
'./node_modules/@gradial/aci/dist/content/component-block.js',
|
|
18
|
+
'./node_modules/@gradial/aci/dist/collect/aci.js',
|
|
19
|
+
'./node_modules/@gradial/aci/dist/internal/release-marker.js',
|
|
20
|
+
];
|
|
21
|
+
export function withPlatformTraced(includes) {
|
|
22
|
+
const all = includes ?? {};
|
|
23
|
+
return { ...all, '/**': [...(all['/**'] ?? []), ...PLATFORM_TRACED_FILES] };
|
|
24
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The root-identity judgement conformance runs over a site's renders
|
|
3
|
-
* (`./testing`, site-check.ts).
|
|
3
|
+
* (`./testing`, site-check.ts). Publish and `aci verify` judge root identity
|
|
4
|
+
* through the verify `blocks` check instead, which reads parsed tags.
|
|
4
5
|
*/
|
|
5
6
|
export { collectComponentBlocks, isComponentBlock, type ComponentBlock } from '../content/component-block.js';
|
|
6
7
|
export type TestVerdict = 'pass' | 'fail' | 'not-run';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The root-identity judgement conformance runs over a site's renders
|
|
3
|
-
* (`./testing`, site-check.ts).
|
|
3
|
+
* (`./testing`, site-check.ts). Publish and `aci verify` judge root identity
|
|
4
|
+
* through the verify `blocks` check instead, which reads parsed tags.
|
|
4
5
|
*/
|
|
5
6
|
import { collectComponentBlocks } from '../content/component-block.js';
|
|
6
7
|
export { collectComponentBlocks, isComponentBlock } from '../content/component-block.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { RenderError } from './errors.js';
|
|
6
|
+
import { GRADIAL_RENDER_INPUT_HEADER, astroRoute, renderInputFor, withPendingInput } from './input.js';
|
|
7
|
+
const SERVER_ENTRIES = ['dist/server/entry.mjs', 'dist-astro/server/entry.mjs'];
|
|
8
|
+
// The handler's own no-match fallback body; never an authored page.
|
|
9
|
+
const NOT_FOUND_BODY = 'not found';
|
|
10
|
+
function astroServerEntry(siteRoot) {
|
|
11
|
+
const entry = SERVER_ENTRIES.map((rel) => path.join(siteRoot, rel)).find((file) => existsSync(file));
|
|
12
|
+
if (!entry) {
|
|
13
|
+
throw new Error(`Astro server entry was not found under ${siteRoot} (looked for ${SERVER_ENTRIES.join(', ')})`);
|
|
14
|
+
}
|
|
15
|
+
return entry;
|
|
16
|
+
}
|
|
17
|
+
async function loadHandler(entryPath) {
|
|
18
|
+
const entry = (await import(pathToFileURL(entryPath).href));
|
|
19
|
+
const handler = entry.handler ?? entry.default ?? entry.start;
|
|
20
|
+
if (typeof handler !== 'function') {
|
|
21
|
+
throw new Error(`Astro server entry ${entryPath} must export handler`);
|
|
22
|
+
}
|
|
23
|
+
return handler;
|
|
24
|
+
}
|
|
25
|
+
async function listen(handler) {
|
|
26
|
+
const server = http.createServer((req, res) => {
|
|
27
|
+
const next = (error) => {
|
|
28
|
+
res.statusCode = error ? 500 : 404;
|
|
29
|
+
res.end(error ? (error instanceof Error ? error.message : String(error)) : NOT_FOUND_BODY);
|
|
30
|
+
};
|
|
31
|
+
const pending = handler(req, res, next, {});
|
|
32
|
+
if (pending && typeof pending.catch === 'function')
|
|
33
|
+
pending.catch(next);
|
|
34
|
+
});
|
|
35
|
+
await new Promise((resolve, reject) => {
|
|
36
|
+
server.once('error', reject);
|
|
37
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
38
|
+
});
|
|
39
|
+
const { port } = server.address();
|
|
40
|
+
return { server, baseURL: `http://127.0.0.1:${port}` };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Binding `/404/` collides with Astro's native 404 seat (D-11): the server
|
|
44
|
+
* answers the direct route with the authored page and status 404. That is a
|
|
45
|
+
* successful render of a page served with 200 (the static artifact), so the
|
|
46
|
+
* render answers 200, as the Next renderer does for the same page.
|
|
47
|
+
*/
|
|
48
|
+
function isAuthoredNotFoundPage(route, status, html) {
|
|
49
|
+
const body = html.trim();
|
|
50
|
+
return status === 404 && route === '/404/' && body !== '' && body !== NOT_FOUND_BODY;
|
|
51
|
+
}
|
|
52
|
+
async function start(options) {
|
|
53
|
+
process.chdir(options.siteRoot);
|
|
54
|
+
Object.assign(process.env, { NODE_ENV: 'production', ACI_PAGE_RUNTIME: '1' });
|
|
55
|
+
const { server, baseURL } = await listen(await loadHandler(astroServerEntry(options.siteRoot)));
|
|
56
|
+
async function render(page) {
|
|
57
|
+
const route = astroRoute(page.route);
|
|
58
|
+
const input = renderInputFor(page, route);
|
|
59
|
+
return withPendingInput(input, async (inputID) => {
|
|
60
|
+
const response = await fetch(new URL(route, baseURL), { headers: { [GRADIAL_RENDER_INPUT_HEADER]: inputID } });
|
|
61
|
+
const html = await response.text();
|
|
62
|
+
if (!response.ok && !isAuthoredNotFoundPage(route, response.status, html)) {
|
|
63
|
+
throw new RenderError('render.status', route, `astro page runtime ${route} returned ${response.status}: ${html.slice(0, 500)}`, {
|
|
64
|
+
variantKey: page.variantKey,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!html.trim()) {
|
|
68
|
+
throw new RenderError('render.empty', route, `astro page runtime ${route} returned empty HTML`, { variantKey: page.variantKey });
|
|
69
|
+
}
|
|
70
|
+
return { html, status: isAuthoredNotFoundPage(route, response.status, html) ? 200 : response.status };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
render,
|
|
75
|
+
close: () => new Promise((resolve) => server.close(() => resolve())),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Importing the entry makes Node resolve the bundle's whole import graph. */
|
|
79
|
+
async function check(options) {
|
|
80
|
+
try {
|
|
81
|
+
await import(pathToFileURL(astroServerEntry(options.siteRoot)).href);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return [`import astro server entry: ${error instanceof Error ? error.message : String(error)}`];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export const astroAdapter = { start, check };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why one page failed to render. Codes are stable "render.<reason>" strings:
|
|
3
|
+
*
|
|
4
|
+
* - render.input the compiled payload cannot drive this framework's render
|
|
5
|
+
* - render.threw the framework handler threw
|
|
6
|
+
* - render.status the framework answered with a non-2xx status (a redirect
|
|
7
|
+
* still pending after the allowed hops counts)
|
|
8
|
+
* - render.empty the framework answered 2xx with an empty document
|
|
9
|
+
* - render.timeout the framework did not answer in time
|
|
10
|
+
*/
|
|
11
|
+
export declare const RENDER_ERROR_CODES: readonly ["render.input", "render.threw", "render.status", "render.empty", "render.timeout"];
|
|
12
|
+
export type RenderErrorCode = (typeof RENDER_ERROR_CODES)[number];
|
|
13
|
+
export declare class RenderError extends Error {
|
|
14
|
+
readonly code: RenderErrorCode;
|
|
15
|
+
readonly route: string;
|
|
16
|
+
readonly variantKey?: string;
|
|
17
|
+
constructor(code: RenderErrorCode, route: string, message: string, options?: {
|
|
18
|
+
variantKey?: string;
|
|
19
|
+
cause?: unknown;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Wraps anything a framework threw as render.threw, keeping RenderErrors as they are. */
|
|
23
|
+
export declare function asRenderError(error: unknown, route: string, variantKey: string | undefined): RenderError;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why one page failed to render. Codes are stable "render.<reason>" strings:
|
|
3
|
+
*
|
|
4
|
+
* - render.input the compiled payload cannot drive this framework's render
|
|
5
|
+
* - render.threw the framework handler threw
|
|
6
|
+
* - render.status the framework answered with a non-2xx status (a redirect
|
|
7
|
+
* still pending after the allowed hops counts)
|
|
8
|
+
* - render.empty the framework answered 2xx with an empty document
|
|
9
|
+
* - render.timeout the framework did not answer in time
|
|
10
|
+
*/
|
|
11
|
+
export const RENDER_ERROR_CODES = ['render.input', 'render.threw', 'render.status', 'render.empty', 'render.timeout'];
|
|
12
|
+
export class RenderError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
route;
|
|
15
|
+
variantKey;
|
|
16
|
+
constructor(code, route, message, options = {}) {
|
|
17
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
18
|
+
this.name = 'RenderError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.route = route;
|
|
21
|
+
if (options.variantKey !== undefined)
|
|
22
|
+
this.variantKey = options.variantKey;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Wraps anything a framework threw as render.threw, keeping RenderErrors as they are. */
|
|
26
|
+
export function asRenderError(error, route, variantKey) {
|
|
27
|
+
if (error instanceof RenderError)
|
|
28
|
+
return error;
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return new RenderError('render.threw', route, message, { variantKey, cause: error });
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RenderFramework, Renderer, RendererOptions } from './types.js';
|
|
2
|
+
export { RenderError } from './errors.js';
|
|
3
|
+
export type { RenderErrorCode } from './errors.js';
|
|
4
|
+
export type { RenderFramework, RenderPage, RenderPayload, RenderResult, Renderer, RendererOptions } from './types.js';
|
|
5
|
+
/** One page render may not outlast this; the framework is left running. */
|
|
6
|
+
export declare const RENDER_TIMEOUT_MS = 60000;
|
|
7
|
+
/**
|
|
8
|
+
* Names the framework whose production build sits in siteRoot. The checks
|
|
9
|
+
* are each framework's own build marker, so exactly one matches a real build.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectFramework(options: Pick<RendererOptions, 'siteRoot' | 'contentRoot'>): RenderFramework;
|
|
12
|
+
/**
|
|
13
|
+
* Starts the site's framework once and renders compiled pages through it.
|
|
14
|
+
* Every failure of one page rejects with a RenderError; startup failures
|
|
15
|
+
* reject createRenderer itself.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createRenderer(options: RendererOptions): Promise<Renderer>;
|
|
18
|
+
/**
|
|
19
|
+
* Proves the framework's runtime dependencies resolve from siteRoot without
|
|
20
|
+
* starting it. Returns one message per problem; empty means ready.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkRenderer(options: RendererOptions): Promise<string[]>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RenderError, asRenderError } from './errors.js';
|
|
4
|
+
import { assertRenderPage } from './input.js';
|
|
5
|
+
export { RenderError } from './errors.js';
|
|
6
|
+
/** One page render may not outlast this; the framework is left running. */
|
|
7
|
+
export const RENDER_TIMEOUT_MS = 60_000;
|
|
8
|
+
// Loaded on demand: a site's runtime carries only its own framework's
|
|
9
|
+
// dependencies (a Next standalone tree has no Mintlify projector).
|
|
10
|
+
const ADAPTERS = {
|
|
11
|
+
next: async () => (await import('./next.js')).nextAdapter,
|
|
12
|
+
astro: async () => (await import('./astro.js')).astroAdapter,
|
|
13
|
+
mintlify: async () => (await import('./mintlify.js')).mintlifyAdapter,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Names the framework whose production build sits in siteRoot. The checks
|
|
17
|
+
* are each framework's own build marker, so exactly one matches a real build.
|
|
18
|
+
*/
|
|
19
|
+
export function detectFramework(options) {
|
|
20
|
+
const markers = [
|
|
21
|
+
['mintlify', path.join(options.contentRoot, 'renderers', 'mintlify', 'docs.json')],
|
|
22
|
+
['next', path.join(options.siteRoot, '.next', 'BUILD_ID')],
|
|
23
|
+
['astro', path.join(options.siteRoot, 'dist', 'server', 'entry.mjs')],
|
|
24
|
+
['astro', path.join(options.siteRoot, 'dist-astro', 'server', 'entry.mjs')],
|
|
25
|
+
];
|
|
26
|
+
const found = markers.find(([, marker]) => existsSync(marker));
|
|
27
|
+
if (!found) {
|
|
28
|
+
throw new Error(`no Next, Astro or Mintlify production build found in ${options.siteRoot}; build the site first`);
|
|
29
|
+
}
|
|
30
|
+
return found[0];
|
|
31
|
+
}
|
|
32
|
+
function withDeadline(page, rendering) {
|
|
33
|
+
let timer;
|
|
34
|
+
const deadline = new Promise((_, reject) => {
|
|
35
|
+
timer = setTimeout(() => {
|
|
36
|
+
reject(new RenderError('render.timeout', page.route, `render of ${page.route} exceeded ${RENDER_TIMEOUT_MS / 1000}s`, {
|
|
37
|
+
variantKey: page.variantKey,
|
|
38
|
+
}));
|
|
39
|
+
}, RENDER_TIMEOUT_MS);
|
|
40
|
+
});
|
|
41
|
+
return Promise.race([rendering, deadline]).finally(() => clearTimeout(timer));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Starts the site's framework once and renders compiled pages through it.
|
|
45
|
+
* Every failure of one page rejects with a RenderError; startup failures
|
|
46
|
+
* reject createRenderer itself.
|
|
47
|
+
*/
|
|
48
|
+
export async function createRenderer(options) {
|
|
49
|
+
const adapter = await adapterFor(options);
|
|
50
|
+
const renderer = await adapter.start(options);
|
|
51
|
+
return {
|
|
52
|
+
async render(page) {
|
|
53
|
+
try {
|
|
54
|
+
assertRenderPage(page);
|
|
55
|
+
return await withDeadline(page, renderer.render(page));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw asRenderError(error, page.route, page.variantKey);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
close: () => renderer.close(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Proves the framework's runtime dependencies resolve from siteRoot without
|
|
66
|
+
* starting it. Returns one message per problem; empty means ready.
|
|
67
|
+
*/
|
|
68
|
+
export async function checkRenderer(options) {
|
|
69
|
+
return (await adapterFor(options)).check(options);
|
|
70
|
+
}
|
|
71
|
+
function adapterFor(options) {
|
|
72
|
+
return ADAPTERS[options.framework ?? detectFramework(options)]();
|
|
73
|
+
}
|