@gradial/aci 0.1.53-canary.694 → 0.1.53-canary.700
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 +63 -0
- package/README.md +4 -4
- package/dist/astro/index.js +2 -5
- 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/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/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/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/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,51 @@
|
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
|
+
- `AciRootProps` (the new name of `RootIdentityProps`, which stays as an
|
|
8
|
+
alias) and `aciRootAttrs(props)`, exported from `@gradial/aci`.
|
|
9
|
+
- The `rootIdentity` check covers every file that calls `createRegistry(`,
|
|
10
|
+
renders a component no content places with props derived from its
|
|
11
|
+
contract's schema, and prints its coverage: components checked, and each
|
|
12
|
+
one not run with the reason.
|
|
7
13
|
- Page `metadata.robots` accepts boolean `index` and `follow` directives.
|
|
8
14
|
Next and Astro emit the corresponding HTML tag. Publish excludes pages with
|
|
9
15
|
`index: false` from the release sitemap.
|
|
10
16
|
|
|
11
17
|
### Migration required
|
|
12
18
|
|
|
19
|
+
- **Block root identity: render the root on every render** (ADR 0051) — each
|
|
20
|
+
registered component must put `aciRootAttrs(props)` (or its rest props) on
|
|
21
|
+
its own root element, and must render that root even when its content is
|
|
22
|
+
empty; the `rootIdentity` conformance check now fails a component that
|
|
23
|
+
renders nothing. Runbook: `.agents/skills/authoring/references/sdk-migrations.md`,
|
|
24
|
+
section "Root identity (0.1.52-rc.24)".
|
|
25
|
+
- **Migration required: test loader** — the `rootIdentity` check in
|
|
26
|
+
`@gradial/aci/testing` imports your component registry, so your test
|
|
27
|
+
process must be able to load TypeScript, JSX, and your `tsconfig` path
|
|
28
|
+
aliases. Run your test suite with the `tsx` loader, as the starters do:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
"test": "node --import tsx --test tests/**/*.test.ts"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A test process without the loader (for example `node --test` or
|
|
35
|
+
`node --test --experimental-strip-types`) now fails `npm test` with:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
rootIdentity: cannot load src/cms/registry.ts: this test process cannot resolve the path alias "@/cms".
|
|
39
|
+
Run tests with: node --import tsx --test
|
|
40
|
+
See the @gradial/aci CHANGELOG, "Migration required: test loader".
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If you turned the check off with `checks: { rootIdentity: false }` because
|
|
44
|
+
it could not load CSS modules or path aliases, remove that option: the
|
|
45
|
+
check now loads stylesheets, images, fonts, `next/image`, `next/link`, and
|
|
46
|
+
`next/font` as inert stubs, and reads every registry file in the site.
|
|
47
|
+
|
|
48
|
+
There is no build gate. `next build` and `astro build` do not run the
|
|
49
|
+
check. The local signal is `npm test`, where the conformance check renders
|
|
50
|
+
every registered component in isolation. A component it cannot load or
|
|
51
|
+
render is reported as not run, never as a failure.
|
|
13
52
|
- Existing Astro sites must install `@gradial/aci/astro/Head` and pass resolved
|
|
14
53
|
metadata as one object through their route and layout. Next sites must upgrade
|
|
15
54
|
the SDK and rebuild. See [page metadata migration](./migrations/page-metadata-head.md).
|
|
@@ -18,8 +57,32 @@
|
|
|
18
57
|
before telling an author that `noindex` is present. No platform test currently
|
|
19
58
|
enforces that check on older site builds.
|
|
20
59
|
|
|
60
|
+
### Removed
|
|
61
|
+
|
|
62
|
+
- `astroAuthoredBlocks` is no longer exported from `@gradial/aci/testing`:
|
|
63
|
+
the Astro `rootIdentity` check renders components instead of scanning
|
|
64
|
+
`dist/`. `findRootIdentityFailures` and `RootIdentityFailure` are no longer
|
|
65
|
+
exported either; `checkRootIdentity` and `formatRootIdentityFailures` stay.
|
|
66
|
+
|
|
21
67
|
### Changed
|
|
22
68
|
|
|
69
|
+
- **`rootIdentity` reads every registry file again.** The `registry` option
|
|
70
|
+
introduced in 0.1.52-rc.33 is still accepted, but it is no longer required:
|
|
71
|
+
the check covers every file that calls `createRegistry(` (a main registry
|
|
72
|
+
and an error-page registry are both checked). Sites that added the option
|
|
73
|
+
for rc.33 need no change.
|
|
74
|
+
- **Nested blocks carry their root identity in their props.** The renderers
|
|
75
|
+
now put `data-aci-block` and `data-aci-component` into the `props` of
|
|
76
|
+
every component block nested in a block's props, at any depth (a block
|
|
77
|
+
slot, or a list inside an item such as `tabs[].blocks`). A container that
|
|
78
|
+
renders child blocks from plain data with `<Component {...block.props} />`
|
|
79
|
+
now emits each child's identity on the child's root, so `rootIdentity`
|
|
80
|
+
passes for it without a site change. Containers that call
|
|
81
|
+
`renderChildren` are unchanged: the same values are applied once. A
|
|
82
|
+
container that rebuilds a child's props from selected fields (for example
|
|
83
|
+
`<Component text={block.props.text} />`) still drops the identity and is
|
|
84
|
+
still reported. The `props` of nested blocks now contain the two identity
|
|
85
|
+
keys at render time; the compiled content does not change.
|
|
23
86
|
- Immutable code builds (`ACI_CODE_ARTIFACT_BUILD=1`) no longer read or compile
|
|
24
87
|
authored error-page content. Native client error seats use the shipped
|
|
25
88
|
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-canary.
|
|
3
|
+
`@gradial/aci` `0.1.53-canary.700` 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-canary.
|
|
11
|
+
npm install @gradial/aci@0.1.53-canary.700 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-canary.
|
|
140
|
+
npx @gradial/aci@0.1.53-canary.700 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-canary.
|
|
146
|
+
npx @gradial/aci@0.1.53-canary.700 init --list
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
## Personalization: The One-Tag Install
|
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
|
}
|
|
@@ -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 {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isComponentBlock } from '../content/component-block.js';
|
|
2
|
+
/**
|
|
3
|
+
* The one place a renderer seam applies the block root identity (sdk I-31,
|
|
4
|
+
* ADR 0063 §One verifier). The identity props go last, so neither content
|
|
5
|
+
* props nor tenant slotProps can overwrite them.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function withAciRootProps(props, block) {
|
|
10
|
+
return {
|
|
11
|
+
...props,
|
|
12
|
+
'data-aci-block': block.id,
|
|
13
|
+
'data-aci-component': block.component,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A copy of a block's content props in which every nested component block
|
|
18
|
+
* (the `rootIdentity` predicate, at any depth: a block slot, or a list inside
|
|
19
|
+
* an item such as `tabs[].blocks`) carries its own identity in its `props`
|
|
20
|
+
* (sdk I-31). A container that renders a child with `{...child.props}` then
|
|
21
|
+
* emits the child's identity; one that calls `renderChildren` re-applies the
|
|
22
|
+
* same values, so nothing doubles. Walk only compiled content, never tenant
|
|
23
|
+
* slotProps: those can hold React elements.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function withNestedAciRootProps(props) {
|
|
28
|
+
return stampNested(props);
|
|
29
|
+
}
|
|
30
|
+
function stampNested(value) {
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return value.map(stampNested);
|
|
33
|
+
if (!isPlainObject(value))
|
|
34
|
+
return value;
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [key, child] of Object.entries(value))
|
|
37
|
+
out[key] = stampNested(child);
|
|
38
|
+
if (isComponentBlock(out) && typeof out.id === 'string' && out.id !== ''
|
|
39
|
+
&& typeof out.component === 'string' && isPlainObject(out.props)) {
|
|
40
|
+
out.props = withAciRootProps(out.props, { id: out.id, component: out.component });
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function isPlainObject(value) {
|
|
45
|
+
if (typeof value !== 'object' || value === null)
|
|
46
|
+
return false;
|
|
47
|
+
const proto = Object.getPrototypeOf(value);
|
|
48
|
+
return proto === Object.prototype || proto === null;
|
|
49
|
+
}
|
package/dist/next/error-seat.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Component, Fragment } from 'react';
|
|
4
|
+
import { withAciRootProps, withNestedAciRootProps } from '../internal/aci-root.js';
|
|
4
5
|
import { ErrorPageInstrumentation } from './error-page-instrumentation.js';
|
|
5
6
|
import { ErrorPageMarker } from './error-page-marker.js';
|
|
6
7
|
/** The build-time serialized payload, or null when the define is absent. */
|
|
@@ -74,13 +75,11 @@ function renderClientBlocks(blocks, registry) {
|
|
|
74
75
|
if (!entry)
|
|
75
76
|
return null;
|
|
76
77
|
const Component = entry.component;
|
|
77
|
-
|
|
78
|
+
// sdk I-31 (ADR 0051): the error seat is an injection seam too.
|
|
79
|
+
const componentProps = withAciRootProps(withNestedAciRootProps(block.props), block);
|
|
78
80
|
if (entry.contract.blockSlots) {
|
|
79
81
|
componentProps.renderChildren = (children) => renderClientBlocks(children, registry);
|
|
80
82
|
}
|
|
81
|
-
// sdk I-19 (ADR 0051): the error seat is an injection seam too.
|
|
82
|
-
componentProps['data-aci-block'] = block.id;
|
|
83
|
-
componentProps['data-aci-component'] = block.component;
|
|
84
83
|
// Index+id composite: a repeated fragment placement repeats its authored
|
|
85
84
|
// id, and sibling keys must stay unique for React reconciliation.
|
|
86
85
|
return _jsx(Component, { ...componentProps }, `${index}:${block.id}`);
|
package/dist/next/render.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { withAciRootProps, withNestedAciRootProps } from '../internal/aci-root.js';
|
|
2
3
|
export async function renderBlock(block, registry, key, extraProps) {
|
|
3
4
|
const entry = registry.find((r) => r.id === block.component);
|
|
4
5
|
if (!entry) {
|
|
@@ -6,14 +7,7 @@ export async function renderBlock(block, registry, key, extraProps) {
|
|
|
6
7
|
}
|
|
7
8
|
const Component = entry.component;
|
|
8
9
|
const hasBlockSlots = !!entry.contract.blockSlots;
|
|
9
|
-
|
|
10
|
-
// content props nor tenant slotProps can overwrite them.
|
|
11
|
-
const componentProps = {
|
|
12
|
-
...block.props,
|
|
13
|
-
...extraProps,
|
|
14
|
-
'data-aci-block': block.id,
|
|
15
|
-
'data-aci-component': block.component,
|
|
16
|
-
};
|
|
10
|
+
const componentProps = withAciRootProps({ ...withNestedAciRootProps(block.props), ...extraProps }, block);
|
|
17
11
|
if (hasBlockSlots) {
|
|
18
12
|
componentProps.renderChildren = (blocks) => renderBlocks(blocks, registry, extraProps);
|
|
19
13
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astro adapter: renders one registered component with the Astro Container
|
|
3
|
+
* API over a Vite server that Astro configures for the site
|
|
4
|
+
* (`getViteConfig`), the way Astro documents testing components. No site
|
|
5
|
+
* build and no emitted HTML, so static and server output are covered alike.
|
|
6
|
+
* astro, its container, and vite resolve from the site, never from the SDK.
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { join, relative, sep } from 'node:path';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
import { createRenderChildren } from '../../astro/index.js';
|
|
12
|
+
import { classifyLoadError, findRegistryFiles, mergeRegistries, registryExport } from './site.js';
|
|
13
|
+
export async function openAstroSite(siteDir, explicit = []) {
|
|
14
|
+
// Every file that calls createRegistry(, plus any registry the site names explicitly.
|
|
15
|
+
const registryFiles = [...new Set([...findRegistryFiles(siteDir), ...explicit])].sort();
|
|
16
|
+
if (registryFiles.length === 0)
|
|
17
|
+
throw new Error('no registry file imports createRegistry from @gradial/aci');
|
|
18
|
+
const fromSite = createRequire(join(siteDir, 'package.json'));
|
|
19
|
+
const importFrom = async (resolve, specifier) => (await import(pathToFileURL(resolve(specifier)).href));
|
|
20
|
+
const { getViteConfig } = await importFrom(fromSite.resolve, 'astro/config');
|
|
21
|
+
const { experimental_AstroContainer } = await importFrom(fromSite.resolve, 'astro/container');
|
|
22
|
+
const fromAstro = createRequire(fromSite.resolve('astro/package.json'));
|
|
23
|
+
const { createServer } = await importFrom(fromAstro.resolve, 'vite');
|
|
24
|
+
const vite = await withoutContentWatch(async () => {
|
|
25
|
+
// An isolated SSR render needs no client dependency optimizer and no dev
|
|
26
|
+
// toolbar; both would otherwise run in the background past close().
|
|
27
|
+
const config = await getViteConfig({
|
|
28
|
+
logLevel: 'silent',
|
|
29
|
+
optimizeDeps: { noDiscovery: true, include: [] },
|
|
30
|
+
}, { root: siteDir, logLevel: 'silent', devToolbar: { enabled: false } })({ mode: 'development', command: 'serve' });
|
|
31
|
+
const server = (config.server ?? {});
|
|
32
|
+
return createServer({
|
|
33
|
+
...config,
|
|
34
|
+
server: { ...server, middlewareMode: true, hmr: false, ws: false, watch: null },
|
|
35
|
+
appType: 'custom',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
// Every registry file is loaded through Astro's own Vite pipeline; one
|
|
40
|
+
// that cannot load is reported and does not hide the others.
|
|
41
|
+
const registries = [];
|
|
42
|
+
const unloadable = [];
|
|
43
|
+
for (const registryFile of registryFiles) {
|
|
44
|
+
const rel = relative(siteDir, registryFile);
|
|
45
|
+
try {
|
|
46
|
+
const registry = registryExport(await vite.ssrLoadModule('/' + rel.split(sep).join('/')));
|
|
47
|
+
if (registry)
|
|
48
|
+
registries.push(registry);
|
|
49
|
+
else
|
|
50
|
+
unloadable.push({ file: rel, reason: 'does not export the createRegistry result' });
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
unloadable.push({ file: rel, reason: classifyLoadError(err).reason });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const registry = mergeRegistries(registries);
|
|
57
|
+
const container = await experimental_AstroContainer.create();
|
|
58
|
+
return {
|
|
59
|
+
registry,
|
|
60
|
+
adapter: createAstroAdapter(registry, container, siteDir),
|
|
61
|
+
unloadable,
|
|
62
|
+
close: () => vite.close(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
await vite.close();
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function createAstroAdapter(registry, container, siteDir) {
|
|
71
|
+
const renderChildren = createRenderChildren(registry);
|
|
72
|
+
return {
|
|
73
|
+
id: 'astro',
|
|
74
|
+
async renderBlock(block) {
|
|
75
|
+
const [{ Component, props }] = renderChildren([block]);
|
|
76
|
+
if (!Component)
|
|
77
|
+
throw new Error(`no registered component "${block.component}"`);
|
|
78
|
+
return container.renderToString(Component, { props });
|
|
79
|
+
},
|
|
80
|
+
sourceFileFor(componentId) {
|
|
81
|
+
// Astro's compiler stamps every component factory with its source path.
|
|
82
|
+
const component = registry.find((entry) => entry.id === componentId)?.component;
|
|
83
|
+
return typeof component?.moduleId === 'string' ? relative(siteDir, component.moduleId) : undefined;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// withAci() recompiles content when a dev server starts; an isolated render
|
|
88
|
+
// needs neither the recompile nor the watcher.
|
|
89
|
+
async function withoutContentWatch(fn) {
|
|
90
|
+
const previous = process.env.ACI_DISABLE_CONTENT_WATCH;
|
|
91
|
+
process.env.ACI_DISABLE_CONTENT_WATCH = '1';
|
|
92
|
+
try {
|
|
93
|
+
return await fn();
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (previous === undefined)
|
|
97
|
+
delete process.env.ACI_DISABLE_CONTENT_WATCH;
|
|
98
|
+
else
|
|
99
|
+
process.env.ACI_DISABLE_CONTENT_WATCH = previous;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Registry } from '../../registry.js';
|
|
2
|
+
import type { FrameworkAdapter, LoadedSite } from './types.js';
|
|
3
|
+
/** The react-dom/server surface the adapter needs. */
|
|
4
|
+
interface ReactRuntime {
|
|
5
|
+
renderToStaticMarkup(element: unknown): string;
|
|
6
|
+
}
|
|
7
|
+
export declare function openNextSite(siteDir: string, explicit?: readonly string[]): Promise<LoadedSite>;
|
|
8
|
+
export declare function createNextAdapter(registry: Registry, react: ReactRuntime, sourceFileFor: (componentId: string) => string | undefined): FrameworkAdapter;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next adapter: renders one registered component with react-dom/server from
|
|
3
|
+
* the site's own React, through the SDK's `renderBlock`. `next/link` and
|
|
4
|
+
* `next/image` are stubbed (testing/conformance/next-stubs.ts) because their
|
|
5
|
+
* CommonJS interop objects cannot render outside Next's bundler.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { join, relative } from 'node:path';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
import { renderBlock } from '../../next/render.js';
|
|
12
|
+
import { classifyLoadError, findRegistryFiles, LoaderMissingError, mergeRegistries, registryExport } from './site.js';
|
|
13
|
+
export async function openNextSite(siteDir, explicit = []) {
|
|
14
|
+
// Every file that calls createRegistry(, plus any registry the site names explicitly.
|
|
15
|
+
const registryFiles = [...new Set([...findRegistryFiles(siteDir), ...explicit])].sort();
|
|
16
|
+
if (registryFiles.length === 0)
|
|
17
|
+
throw new Error('no registry file imports createRegistry from @gradial/aci');
|
|
18
|
+
const fromSite = createRequire(join(siteDir, 'package.json'));
|
|
19
|
+
const react = fromSite('react-dom/server');
|
|
20
|
+
// The site's tsconfig may keep `jsx: preserve` for Next, in which case the
|
|
21
|
+
// test transform emits classic React.createElement calls.
|
|
22
|
+
globalThis.React = fromSite('react');
|
|
23
|
+
const { register } = await import('node:module');
|
|
24
|
+
register(stubHookURL());
|
|
25
|
+
// Every registry file is loaded; one that cannot load does not hide the
|
|
26
|
+
// others. A process with no TypeScript/JSX loader is a different failure:
|
|
27
|
+
// nothing can be checked and the fix is one line, so it throws.
|
|
28
|
+
const registries = [];
|
|
29
|
+
const unloadable = [];
|
|
30
|
+
const fileFor = new Map();
|
|
31
|
+
for (const registryFile of registryFiles) {
|
|
32
|
+
const rel = relative(siteDir, registryFile);
|
|
33
|
+
let mod;
|
|
34
|
+
try {
|
|
35
|
+
mod = (await import(pathToFileURL(registryFile).href));
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const failure = classifyLoadError(err);
|
|
39
|
+
if (failure.kind === 'loader-missing')
|
|
40
|
+
throw new LoaderMissingError(rel, failure.reason);
|
|
41
|
+
unloadable.push({ file: rel, reason: failure.reason });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const registry = registryExport(mod);
|
|
45
|
+
if (!registry) {
|
|
46
|
+
unloadable.push({ file: rel, reason: 'does not export the createRegistry result' });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
registries.push(registry);
|
|
50
|
+
const resolve = componentFileResolver(readFileSync(registryFile, 'utf8'), registry);
|
|
51
|
+
for (const entry of registry) {
|
|
52
|
+
const file = resolve(entry.id);
|
|
53
|
+
if (file && !fileFor.has(entry.id))
|
|
54
|
+
fileFor.set(entry.id, file);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const registry = mergeRegistries(registries);
|
|
58
|
+
return {
|
|
59
|
+
registry,
|
|
60
|
+
adapter: createNextAdapter(registry, react, (id) => fileFor.get(id)),
|
|
61
|
+
unloadable,
|
|
62
|
+
close: async () => { },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function createNextAdapter(registry, react, sourceFileFor) {
|
|
66
|
+
return {
|
|
67
|
+
id: 'next',
|
|
68
|
+
async renderBlock(block) {
|
|
69
|
+
if (!registry.some((entry) => entry.id === block.component)) {
|
|
70
|
+
throw new Error(`no registered component "${block.component}"`);
|
|
71
|
+
}
|
|
72
|
+
return markupFor(await renderBlock(block, registry, `probe:${block.id}`), react);
|
|
73
|
+
},
|
|
74
|
+
sourceFileFor,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Slot components are async server components in Next; react-dom/server
|
|
79
|
+
* cannot render those, so the element the SDK produced is unwrapped by
|
|
80
|
+
* awaiting the component function directly.
|
|
81
|
+
*/
|
|
82
|
+
async function markupFor(element, react) {
|
|
83
|
+
const el = element;
|
|
84
|
+
if (typeof el.type === 'function' && el.type.constructor?.name === 'AsyncFunction') {
|
|
85
|
+
return react.renderToStaticMarkup(await el.type(el.props));
|
|
86
|
+
}
|
|
87
|
+
return react.renderToStaticMarkup(element);
|
|
88
|
+
}
|
|
89
|
+
/** The stub hook: compiled `.js` in dist, `.ts` when run from source. */
|
|
90
|
+
function stubHookURL() {
|
|
91
|
+
const js = new URL('../../testing/conformance/next-stubs.js', import.meta.url);
|
|
92
|
+
if (existsSync(fileURLToPath(js)))
|
|
93
|
+
return js;
|
|
94
|
+
return new URL('../../testing/conformance/next-stubs.ts', import.meta.url);
|
|
95
|
+
}
|
|
96
|
+
/** Map a registered component id to the import path the registry file names for it. */
|
|
97
|
+
function componentFileResolver(registrySource, registry) {
|
|
98
|
+
const imports = new Map();
|
|
99
|
+
for (const m of registrySource.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
100
|
+
const names = (m[1] ?? m[2]).split(',').map((n) => n.trim().split(/\s+as\s+/).pop()).filter(Boolean);
|
|
101
|
+
for (const name of names)
|
|
102
|
+
imports.set(name, m[3]);
|
|
103
|
+
}
|
|
104
|
+
const byId = new Map();
|
|
105
|
+
for (const entry of registry) {
|
|
106
|
+
const fn = entry.component;
|
|
107
|
+
const file = typeof fn === 'function' && fn.name ? imports.get(fn.name) : undefined;
|
|
108
|
+
if (file)
|
|
109
|
+
byId.set(entry.id, file);
|
|
110
|
+
}
|
|
111
|
+
return (componentId) => byId.get(componentId);
|
|
112
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Registry } from '../../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Every source file that calls `createRegistry(`, in a stable order: under
|
|
4
|
+
* `src/` when the site has one, so a build output directory (whose bundled
|
|
5
|
+
* chunks also contain the call) is never mistaken for a registry. A site may
|
|
6
|
+
* keep several (a main registry, an error-page registry); the check covers
|
|
7
|
+
* the union, so "passed" means every registered component was looked at.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findRegistryFiles(siteDir: string): string[];
|
|
10
|
+
/** Merge registries by component id; the first file that registers an id wins. */
|
|
11
|
+
export declare function mergeRegistries(registries: readonly Registry[]): Registry;
|
|
12
|
+
/**
|
|
13
|
+
* Why a registry file could not be loaded. `loader-missing` means the test
|
|
14
|
+
* process itself cannot load TypeScript, JSX, or the site's path aliases:
|
|
15
|
+
* a site configuration defect with a one-line fix, and the check verified
|
|
16
|
+
* nothing. Anything else is a limit of the isolated loader, reported as
|
|
17
|
+
* not-run and left to publish.
|
|
18
|
+
*/
|
|
19
|
+
export type LoadFailure = {
|
|
20
|
+
kind: 'loader-missing' | 'unloadable';
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function classifyLoadError(err: unknown): LoadFailure;
|
|
24
|
+
/** Thrown when the test process has no TypeScript/JSX loader at all. */
|
|
25
|
+
export declare class LoaderMissingError extends Error {
|
|
26
|
+
constructor(file: string, reason: string);
|
|
27
|
+
}
|
|
28
|
+
/** The `createRegistry` result among a module's exports. */
|
|
29
|
+
export declare function registryExport(mod: Record<string, unknown>): Registry | undefined;
|