@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +4 -4
  3. package/dist/astro/index.js +2 -5
  4. package/dist/content/component-block.d.ts +17 -0
  5. package/dist/content/component-block.js +40 -0
  6. package/dist/content/resolve-slots.js +2 -4
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +1 -0
  9. package/dist/internal/aci-root.d.ts +1 -0
  10. package/dist/internal/aci-root.js +49 -0
  11. package/dist/next/error-seat.js +3 -4
  12. package/dist/next/render.js +2 -8
  13. package/dist/platform-tests/adapters/astro.d.ts +2 -0
  14. package/dist/platform-tests/adapters/astro.js +101 -0
  15. package/dist/platform-tests/adapters/next.d.ts +9 -0
  16. package/dist/platform-tests/adapters/next.js +112 -0
  17. package/dist/platform-tests/adapters/site.d.ts +29 -0
  18. package/dist/platform-tests/adapters/site.js +92 -0
  19. package/dist/platform-tests/adapters/types.d.ts +27 -0
  20. package/dist/platform-tests/adapters/types.js +1 -0
  21. package/dist/platform-tests/index.d.ts +47 -0
  22. package/dist/platform-tests/index.js +50 -0
  23. package/dist/platform-tests/sample-props.d.ts +2 -0
  24. package/dist/platform-tests/sample-props.js +82 -0
  25. package/dist/platform-tests/site-check.d.ts +51 -0
  26. package/dist/platform-tests/site-check.js +193 -0
  27. package/dist/registry.d.ts +1 -1
  28. package/dist/testing/conformance/index.d.ts +2 -1
  29. package/dist/testing/conformance/index.js +2 -1
  30. package/dist/testing/conformance/next-stubs.d.ts +13 -4
  31. package/dist/testing/conformance/next-stubs.js +76 -4
  32. package/dist/testing/conformance/preset.js +1 -2
  33. package/dist/testing/conformance/root-identity.d.ts +0 -29
  34. package/dist/testing/conformance/root-identity.js +36 -344
  35. package/dist/testing/conformance/types.d.ts +1 -1
  36. package/dist/types/component.d.ts +11 -3
  37. package/dist/types/component.js +10 -1
  38. package/package.json +9 -8
  39. package/src/types/component.test.ts +17 -0
  40. package/src/types/component.ts +18 -3
  41. package/templates/astro/starter-content/src/components/chrome/Footer.astro +3 -4
  42. package/templates/astro/starter-content/src/components/chrome/Header.astro +3 -4
  43. package/templates/astro/starter-content/src/components/elements/Button.astro +4 -4
  44. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +3 -4
  45. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -4
  46. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -4
  47. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -4
  48. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +3 -4
  49. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -4
  50. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -4
  51. package/templates/common/.agents/skills/authoring/SKILL.md +6 -0
  52. package/templates/common/.agents/skills/authoring/references/block-registry.md +41 -7
  53. package/templates/common/.agents/skills/authoring/references/sdk-migrations.md +87 -0
  54. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +5 -4
  55. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +5 -4
  56. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +5 -4
  57. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +9 -9
  58. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +5 -4
  59. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +9 -9
  60. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +5 -4
  61. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +10 -10
  62. 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.694` is a pre-1.0 public-surface reset for the ACI
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.694 zod@^4
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.694 init my-site --template next-starter
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.694 init --list
146
+ npx @gradial/aci@0.1.53-canary.700 init --list
147
147
  ```
148
148
 
149
149
  ## Personalization: The One-Tag Install
@@ -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 = { ...block.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
- const isBlock = node.$type === 'component'
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
+ }
@@ -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
- const componentProps = { ...block.props };
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}`);
@@ -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
- // sdk I-19 (ADR 0051): root identity props are applied last so neither
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,2 @@
1
+ import type { LoadedSite } from './types.js';
2
+ export declare function openAstroSite(siteDir: string, explicit?: readonly string[]): Promise<LoadedSite>;
@@ -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;