@gradial/aci 0.1.53-rc.2 → 0.1.53-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +26 -4
  3. package/dist/astro/index.js +2 -5
  4. package/dist/cli/emit-error-pages.d.mts +2 -1
  5. package/dist/cli/emit-error-pages.mjs +13 -270
  6. package/dist/content/component-block.d.ts +17 -0
  7. package/dist/content/component-block.js +40 -0
  8. package/dist/content/resolve-slots.js +2 -4
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +1 -0
  11. package/dist/internal/aci-root.d.ts +1 -0
  12. package/dist/internal/aci-root.js +49 -0
  13. package/dist/next/config.js +6 -13
  14. package/dist/next/error-seat-payload.d.ts +1 -1
  15. package/dist/next/error-seat-payload.js +1 -2
  16. package/dist/next/error-seat.js +3 -4
  17. package/dist/next/render.js +2 -8
  18. package/dist/platform-tests/adapters/astro.d.ts +2 -0
  19. package/dist/platform-tests/adapters/astro.js +101 -0
  20. package/dist/platform-tests/adapters/next.d.ts +9 -0
  21. package/dist/platform-tests/adapters/next.js +112 -0
  22. package/dist/platform-tests/adapters/site.d.ts +29 -0
  23. package/dist/platform-tests/adapters/site.js +92 -0
  24. package/dist/platform-tests/adapters/types.d.ts +27 -0
  25. package/dist/platform-tests/adapters/types.js +1 -0
  26. package/dist/platform-tests/index.d.ts +47 -0
  27. package/dist/platform-tests/index.js +50 -0
  28. package/dist/platform-tests/sample-props.d.ts +2 -0
  29. package/dist/platform-tests/sample-props.js +82 -0
  30. package/dist/platform-tests/site-check.d.ts +51 -0
  31. package/dist/platform-tests/site-check.js +193 -0
  32. package/dist/registry.d.ts +1 -1
  33. package/dist/testing/conformance/index.d.ts +2 -1
  34. package/dist/testing/conformance/index.js +2 -1
  35. package/dist/testing/conformance/next-stubs.d.ts +13 -4
  36. package/dist/testing/conformance/next-stubs.js +76 -4
  37. package/dist/testing/conformance/preset.js +1 -2
  38. package/dist/testing/conformance/root-identity.d.ts +0 -29
  39. package/dist/testing/conformance/root-identity.js +36 -344
  40. package/dist/testing/conformance/types.d.ts +1 -1
  41. package/dist/types/component.d.ts +11 -3
  42. package/dist/types/component.js +10 -1
  43. package/package.json +9 -8
  44. package/src/cli/emit-error-pages.mjs +13 -273
  45. package/src/types/component.test.ts +17 -0
  46. package/src/types/component.ts +18 -3
  47. package/templates/astro/starter-content/src/components/chrome/Footer.astro +3 -4
  48. package/templates/astro/starter-content/src/components/chrome/Header.astro +3 -4
  49. package/templates/astro/starter-content/src/components/elements/Button.astro +4 -4
  50. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +3 -4
  51. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -4
  52. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -4
  53. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -4
  54. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +3 -4
  55. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -4
  56. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -4
  57. package/templates/astro/template/package.json.tmpl +1 -1
  58. package/templates/common/.agents/skills/authoring/SKILL.md +6 -0
  59. package/templates/common/.agents/skills/authoring/references/block-registry.md +41 -7
  60. package/templates/common/.agents/skills/authoring/references/sdk-migrations.md +87 -0
  61. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +5 -4
  62. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +5 -4
  63. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +5 -4
  64. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +9 -9
  65. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +5 -4
  66. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +9 -9
  67. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +5 -4
  68. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +10 -10
  69. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +5 -4
  70. package/templates/nextjs/template/package.json.tmpl +1 -1
@@ -1,361 +1,53 @@
1
1
  /**
2
- * Conformance check: rootIdentity (ADR 0051).
2
+ * Conformance check: rootIdentity (sdk I-31, ADR 0063).
3
3
  *
4
- * The ACI renderer passes every block `data-aci-block` and
5
- * `data-aci-component` as props; a component must spread them onto the
6
- * element it returns. The registry type constraint proves a component
7
- * declares the props. This check proves it renders them, by rendering every
8
- * registered component through the SDK's own seam and reading the root of
9
- * the markup.
10
- *
11
- * Next: components are rendered with react-dom/server from the site's own
12
- * node_modules; next/link and next/image are stubbed (see next-stubs.ts)
13
- * because their CommonJS interop objects cannot render outside Next.
14
- * Astro: components cannot be rendered outside Astro, so the built site's
15
- * HTML is scanned for every authored block id instead (building first when
16
- * `dist/` is absent).
4
+ * The renderer passes every block `data-aci-block` and `data-aci-component`
5
+ * as props; a component must place them on the element it returns. The
6
+ * registry type constraint proves a component declares the props. This
7
+ * check proves it renders them: it renders every registered component in
8
+ * isolation through the framework adapter (Next: react-dom/server; Astro:
9
+ * the Container API over a Vite server rooted at the site) and hands each
10
+ * render to the same rootIdentity test publish runs. No site build.
17
11
  */
18
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
19
- import { createRequire } from 'node:module';
20
- import { join, relative, resolve } from 'node:path';
21
- import { fileURLToPath, pathToFileURL } from 'node:url';
22
- import { execSync } from 'node:child_process';
12
+ import { LoaderMissingError } from '../../platform-tests/adapters/site.js';
13
+ import { checkSiteRootIdentity, formatRootIdentityCoverage, formatRootIdentityFailures, } from '../../platform-tests/site-check.js';
14
+ import { existsSync, statSync } from 'node:fs';
15
+ import { resolve } from 'node:path';
23
16
  const CHECK = 'rootIdentity';
24
17
  const LABEL = 'components render data-aci-block on their root element';
25
- // ---------------------------------------------------------------------------
26
- // Pure core (unit-tested with in-memory registries)
27
- // ---------------------------------------------------------------------------
28
- /**
29
- * Render each block through the SDK renderer and report every component
30
- * whose root element lacks the identity attributes. `blocks` are the blocks
31
- * to exercise; registry entries not covered by a block are probed with empty
32
- * props so an unused component cannot hide a forgotten spread.
33
- */
34
- export async function findRootIdentityFailures(registry, blocks, react, fileFor = () => undefined) {
35
- const { renderBlock } = await import('../../next/render.js');
36
- const failures = new Map();
37
- const covered = new Set();
38
- const probe = async (block, contentBacked) => {
39
- if (failures.has(block.component))
40
- return;
41
- const element = await renderBlock(block, registry, `probe:${block.id}`);
42
- let html;
43
- try {
44
- html = await markupFor(element, react);
45
- }
46
- catch (err) {
47
- // A component the content never uses is probed with empty props; if it
48
- // needs real props to render, that proves nothing about identity, so
49
- // it is skipped rather than reported. Content-backed renders that
50
- // throw are real failures.
51
- if (!contentBacked)
52
- return;
53
- failures.set(block.component, {
54
- component: block.component,
55
- file: fileFor(block.component),
56
- missing: [`render threw: ${err instanceof Error ? err.message : String(err)}`],
57
- });
58
- return;
59
- }
60
- const root = rootAttributes(html);
61
- const missing = [];
62
- if (root.block !== block.id)
63
- missing.push('data-aci-block');
64
- if (root.component !== block.component)
65
- missing.push('data-aci-component');
66
- if (missing.length > 0) {
67
- failures.set(block.component, { component: block.component, file: fileFor(block.component), missing });
68
- }
69
- };
70
- for (const block of blocks) {
71
- if (!registry.some((entry) => entry.id === block.component))
72
- continue;
73
- covered.add(block.component);
74
- await probe(block, true);
75
- }
76
- for (const entry of registry) {
77
- if (covered.has(entry.id))
78
- continue;
79
- await probe({ $type: 'component', id: `probe-${entry.id}`, component: entry.id, props: {} }, false);
80
- }
81
- return [...failures.values()];
82
- }
83
- /** The failure message: one line of what, the list, one line of fix. */
84
- export function formatRootIdentityFailures(failures, framework) {
85
- const width = Math.max(...failures.map((f) => f.component.length), 0);
86
- const lines = failures.map((f) => {
87
- const note = f.missing.length === 1 && f.missing[0].startsWith('data-aci-') && f.missing[0] !== 'data-aci-block'
88
- ? `(missing ${f.missing[0]})`
89
- : f.missing[0]?.startsWith('render threw') ? `(${f.missing[0]})` : '';
90
- return [' ' + f.component.padEnd(width), f.file, note].filter(Boolean).join(' ');
91
- });
92
- const fix = framework === 'next'
93
- ? [
94
- 'Fix: extend RootIdentityProps and spread the rest props onto the root:',
95
- ' function StatsBar({ stats, ...root }: StatsBarProps) { return <section {...root} …',
96
- ]
97
- : [
98
- 'Fix: extend RootIdentityProps and spread the rest props onto the root:',
99
- ' const { stats, ...root } = Astro.props; <section {...root} …',
100
- ];
101
- return [
102
- `${CHECK}: ${failures.length} component${failures.length === 1 ? '' : 's'} do${failures.length === 1 ? 'es' : ''} not render data-aci-block on their root element`,
103
- '',
104
- ...lines,
105
- '',
106
- ...fix,
107
- ].join('\n');
108
- }
109
- /**
110
- * Slot components are async server components in Next; react-dom/server
111
- * cannot render those, so the element the SDK produced is unwrapped by
112
- * awaiting the component function directly.
113
- */
114
- async function markupFor(element, react) {
115
- const el = element;
116
- const type = el.type;
117
- if (typeof type === 'function' && type.constructor?.name === 'AsyncFunction') {
118
- const resolved = await type(el.props);
119
- return react.renderToStaticMarkup(resolved);
120
- }
121
- return react.renderToStaticMarkup(element);
122
- }
123
- function rootAttributes(html) {
124
- // react-dom hoists resource hints (<link rel="preload">, <meta>) ahead of
125
- // the component's markup; they are not the root element.
126
- const body = html.trim().replace(/^(?:<(?:link|meta)\b[^>]*\/?>\s*)+/i, '');
127
- const root = /^<([a-z][a-z0-9-]*)([^>]*)>/i.exec(body);
128
- if (!root)
129
- return {};
130
- const attrs = root[2];
131
- return {
132
- block: /\sdata-aci-block="([^"]*)"/.exec(attrs)?.[1],
133
- component: /\sdata-aci-component="([^"]*)"/.exec(attrs)?.[1],
134
- };
135
- }
136
- // ---------------------------------------------------------------------------
137
- // Site integration
138
- // ---------------------------------------------------------------------------
139
18
  export async function checkRootIdentity(ctx) {
140
- return ctx.framework === 'next' ? checkNextRootIdentity(ctx) : checkAstroRootIdentity(ctx);
141
- }
142
- async function checkNextRootIdentity(ctx) {
143
- if (!ctx.registry?.trim()) {
144
- return [fail(`${CHECK}: set the conformance registry option to the render registry module used by the app`)];
145
- }
146
- const registryFile = resolve(ctx.rootDir, ctx.registry);
147
- if (!existsSync(registryFile) || !statSync(registryFile).isFile()) {
19
+ // A registry the site names explicitly must exist: a wrong option is a
20
+ // configuration error, not a limit of the isolated check.
21
+ const explicit = ctx.registry?.trim() ? resolve(ctx.rootDir, ctx.registry) : undefined;
22
+ if (explicit && !(existsSync(explicit) && statSync(explicit).isFile())) {
148
23
  return [fail(`${CHECK}: registry module does not exist: ${ctx.registry}`)];
149
24
  }
150
- const siteRequire = createRequire(join(ctx.rootDir, 'package.json'));
151
- let react;
152
- try {
153
- react = siteRequire('react-dom/server');
154
- // The site's tsconfig may keep `jsx: preserve` for Next, in which case
155
- // the test transform emits classic React.createElement calls.
156
- globalThis.React = siteRequire('react');
157
- }
158
- catch (err) {
159
- return [fail(`${CHECK}: cannot load react-dom/server from the site (${err instanceof Error ? err.message : String(err)})`)];
160
- }
161
- const { register } = await import('node:module');
162
- register(stubHookURL());
163
- let mod;
25
+ let result;
164
26
  try {
165
- mod = (await import(pathToFileURL(registryFile).href));
27
+ result = await checkSiteRootIdentity(ctx.framework, ctx.rootDir, {
28
+ registryFiles: explicit ? [explicit] : [],
29
+ });
166
30
  }
167
31
  catch (err) {
168
- return [fail(`${CHECK}: cannot load ${relative(ctx.rootDir, registryFile)} (${err instanceof Error ? err.message : String(err)})`)];
169
- }
170
- const registry = Object.values(mod).find(isRegistry);
171
- if (!registry) {
172
- return [fail(`${CHECK}: ${relative(ctx.rootDir, registryFile)} does not export the createRegistry result`)];
173
- }
174
- const source = readFileSync(registryFile, 'utf8');
175
- const failures = await findRootIdentityFailures(registry, compiledBlocks(ctx.rootDir), react, componentFileResolver(source, registry));
176
- if (failures.length === 0) {
177
- return [ok(`${registry.length} registered components`)];
178
- }
179
- return [fail(formatRootIdentityFailures(failures, 'next'))];
180
- }
181
- async function checkAstroRootIdentity(ctx) {
182
- const distDir = join(ctx.rootDir, 'dist');
183
- if (!existsSync(distDir) || distIsStale(distDir, [join(ctx.rootDir, 'src'), join(ctx.rootDir, '.content')])) {
184
- try {
185
- execSync('npm run build', { cwd: ctx.rootDir, stdio: 'pipe', timeout: 300_000, env: { ...process.env, NODE_ENV: 'production' } });
186
- }
187
- catch (err) {
188
- const stderr = err instanceof Error && 'stderr' in err ? String(err.stderr).slice(0, 500) : String(err);
189
- return [fail(`${CHECK}: npm run build failed, cannot scan dist/ (${stderr})`)];
190
- }
191
- }
192
- const authored = astroAuthoredBlocks(join(ctx.rootDir, '.content'));
193
- if (authored.length === 0) {
194
- return [fail(`${CHECK}: no authored blocks found under .content/pages`)];
195
- }
196
- const html = walkHTML(distDir).map((file) => readFileSync(file, 'utf8')).join('\n');
197
- const missing = authored.filter(({ id, component }) => {
198
- const pattern = new RegExp(`<[a-z][^>]*(?:data-aci-block="${escapeRegExp(id)}"[^>]*data-aci-component="${escapeRegExp(component)}"` +
199
- `|data-aci-component="${escapeRegExp(component)}"[^>]*data-aci-block="${escapeRegExp(id)}")`);
200
- return !pattern.test(html);
201
- });
202
- if (missing.length === 0) {
203
- return [ok(`${authored.length} authored blocks found on component roots in dist/`)];
204
- }
205
- const byComponent = new Map();
206
- for (const m of missing) {
207
- if (!byComponent.has(m.component))
208
- byComponent.set(m.component, { component: m.component, missing: ['data-aci-block'] });
209
- }
210
- return [fail(formatRootIdentityFailures([...byComponent.values()], 'astro'))];
211
- }
212
- // ---------------------------------------------------------------------------
213
- // Helpers
214
- // ---------------------------------------------------------------------------
215
- /** The stub hook next to this module: compiled `.js` in dist, `.ts` when run from source. */
216
- function stubHookURL() {
217
- const js = new URL('./next-stubs.js', import.meta.url);
218
- if (existsSync(fileURLToPath(js)))
219
- return js;
220
- return new URL('./next-stubs.ts', import.meta.url);
221
- }
222
- function ok(detail) {
223
- return { name: CHECK, label: LABEL, ok: true, detail };
32
+ // No TypeScript/JSX loader in this test process: nothing was checked and
33
+ // the fix is one line, so this fails rather than passing quietly.
34
+ if (err instanceof LoaderMissingError)
35
+ return [fail(`${CHECK}: ${err.message}`)];
36
+ // Any other failure to open the site is a limit of the isolated check,
37
+ // not a finding about the site; publish checks it in the real renderer.
38
+ const reason = (err instanceof Error ? err.message : String(err)).split('\n')[0];
39
+ return [{ name: CHECK, label: LABEL, ok: true, detail: `${CHECK}: not run (${reason}); publish checks every placed block` }];
40
+ }
41
+ const coverage = formatRootIdentityCoverage(result);
42
+ if (result.notRun.length > 0)
43
+ console.warn(`[aci] ${coverage}`);
44
+ if (result.failures.length === 0) {
45
+ return [{ name: CHECK, label: LABEL, ok: true, detail: coverage }];
46
+ }
47
+ return [fail(`${formatRootIdentityFailures(result.failures)}\n\n${coverage}`)];
224
48
  }
225
49
  // A failure's label is the message itself so the test runner prints it
226
50
  // without a generic prefix; the check name is the first word of the message.
227
51
  function fail(message) {
228
52
  return { name: CHECK, label: message, ok: false };
229
53
  }
230
- function isRegistry(value) {
231
- return Array.isArray(value) && value.length > 0
232
- && value.every((e) => e && typeof e === 'object' && typeof e.id === 'string' && 'component' in e && 'contract' in e);
233
- }
234
- /** Map a registered component id to the import path the registry file names for it. */
235
- function componentFileResolver(registrySource, registry) {
236
- const imports = new Map();
237
- for (const m of registrySource.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g)) {
238
- const names = (m[1] ?? m[2]).split(',').map((n) => n.trim().split(/\s+as\s+/).pop()).filter(Boolean);
239
- for (const name of names)
240
- imports.set(name, m[3]);
241
- }
242
- const byId = new Map();
243
- for (const entry of registry) {
244
- const fn = entry.component;
245
- const file = typeof fn === 'function' && fn.name ? imports.get(fn.name) : undefined;
246
- if (file)
247
- byId.set(entry.id, file);
248
- }
249
- return (component) => byId.get(component);
250
- }
251
- /** Blocks from the site's compiled content, when `npm run content:compile` has run. */
252
- function compiledBlocks(rootDir) {
253
- const compiled = join(rootDir, '.aci', 'compiled');
254
- if (!existsSync(compiled))
255
- return [];
256
- const blocks = [];
257
- const routes = join(compiled, 'routes');
258
- if (existsSync(routes)) {
259
- for (const file of walkJSON(routes)) {
260
- const doc = JSON.parse(readFileSync(file, 'utf8'));
261
- for (const region of Object.values(doc.regions ?? {}))
262
- blocks.push(...region);
263
- }
264
- }
265
- const fragments = join(compiled, 'fragments');
266
- if (existsSync(fragments)) {
267
- for (const file of walkJSON(fragments)) {
268
- const doc = JSON.parse(readFileSync(file, 'utf8'));
269
- if (doc.block) {
270
- const id = file.slice(fragments.length + 1).replace(/\.json$/, '');
271
- blocks.push({ $type: 'component', id, component: doc.block.component, props: doc.block.props });
272
- }
273
- }
274
- }
275
- return blocks;
276
- }
277
- /**
278
- * Every block the Astro scan must find in dist/: page blocks by their own id,
279
- * plus fragments (navbar, footer) by fragment id, which is the block id the
280
- * renderer synthesizes when a layout slot default places them.
281
- */
282
- export function astroAuthoredBlocks(contentDir) {
283
- const out = authoredBlocks(join(contentDir, 'pages'));
284
- const fragmentsDir = join(contentDir, 'fragments');
285
- if (existsSync(fragmentsDir)) {
286
- for (const entry of readdirSync(fragmentsDir, { withFileTypes: true })) {
287
- if (!entry.isDirectory())
288
- continue;
289
- const file = join(fragmentsDir, entry.name, '_index.json');
290
- if (!existsSync(file))
291
- continue;
292
- const doc = JSON.parse(readFileSync(file, 'utf8'));
293
- const component = doc.block?.component;
294
- if (typeof component === 'string' && component !== '') {
295
- out.push({ id: doc.fragmentId ?? entry.name, component });
296
- }
297
- }
298
- }
299
- return out;
300
- }
301
- /** dist/ is stale when any source or content file is newer than its newest output. */
302
- function distIsStale(distDir, inputs) {
303
- const newest = (dir) => {
304
- if (!existsSync(dir))
305
- return 0;
306
- let max = 0;
307
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
308
- const full = join(dir, entry.name);
309
- max = Math.max(max, entry.isDirectory() ? newest(full) : statSync(full).mtimeMs);
310
- }
311
- return max;
312
- };
313
- const built = newest(distDir);
314
- return inputs.some((dir) => newest(dir) > built);
315
- }
316
- function authoredBlocks(pagesDir) {
317
- if (!existsSync(pagesDir))
318
- return [];
319
- const out = [];
320
- const collect = (value) => {
321
- if (Array.isArray(value)) {
322
- value.forEach(collect);
323
- return;
324
- }
325
- if (!value || typeof value !== 'object')
326
- return;
327
- const node = value;
328
- if (typeof node.component === 'string' && node.props && typeof node.props === 'object' && typeof node.id === 'string' && node.id !== '') {
329
- out.push({ id: node.id, component: node.component });
330
- }
331
- Object.values(node).forEach(collect);
332
- };
333
- for (const file of walkJSON(pagesDir)) {
334
- if (!file.endsWith('_index.json'))
335
- continue;
336
- const doc = JSON.parse(readFileSync(file, 'utf8'));
337
- if (doc.$type === 'page')
338
- collect(doc.regions);
339
- }
340
- return out;
341
- }
342
- function walkJSON(dir) {
343
- return walkExt(dir, '.json');
344
- }
345
- function walkHTML(dir) {
346
- return walkExt(dir, '.html');
347
- }
348
- function walkExt(dir, ext) {
349
- const out = [];
350
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
351
- const full = join(dir, entry.name);
352
- if (entry.isDirectory())
353
- out.push(...walkExt(full, ext));
354
- else if (entry.name.endsWith(ext))
355
- out.push(full);
356
- }
357
- return out;
358
- }
359
- function escapeRegExp(value) {
360
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
361
- }
@@ -29,7 +29,7 @@ export interface ConformancePresetOptions {
29
29
  * check verifies .aci.yaml does NOT declare a siteId.
30
30
  */
31
31
  siteId?: string;
32
- /** Render registry module, relative to rootDir (or absolute). Required for Next rootIdentity. */
32
+ /** A registry module to check in addition to every file that calls createRegistry(, relative to rootDir (or absolute). Optional. */
33
33
  registry?: string;
34
34
  /** Root directory of the site project. Defaults to process.cwd(). */
35
35
  rootDir?: string;
@@ -20,13 +20,21 @@ export interface ComponentValidationIssue {
20
20
  }
21
21
  /**
22
22
  * Root identity props the SDK renderers inject into every page and fragment
23
- * block (sdk I-19, ADR 0051). A component spreads them onto its own root
24
- * element; the starter conformance test fails a component that drops them.
23
+ * block (sdk I-31, ADR 0051). A component places them on its own root
24
+ * element with `{...aciRootAttrs(props)}` or a rest-spread; the conformance
25
+ * check fails a component that drops them (ADR 0063).
25
26
  */
26
- export interface RootIdentityProps {
27
+ export interface AciRootProps {
27
28
  'data-aci-block'?: string;
28
29
  'data-aci-component'?: string;
29
30
  }
31
+ /** The former name of {@link AciRootProps} (sdk D-9). */
32
+ export type RootIdentityProps = AciRootProps;
33
+ /**
34
+ * Exactly the {@link AciRootProps} keys of `props`, for the root element:
35
+ * `<section {...aciRootAttrs(props)}>`. Nothing else in `props` leaks.
36
+ */
37
+ export declare function aciRootAttrs(props: AciRootProps): AciRootProps;
30
38
  export interface ComponentDefinition<TId extends string = string, TSchema = unknown> {
31
39
  id: TId;
32
40
  displayName?: string;
@@ -1 +1,10 @@
1
- export {};
1
+ /**
2
+ * Exactly the {@link AciRootProps} keys of `props`, for the root element:
3
+ * `<section {...aciRootAttrs(props)}>`. Nothing else in `props` leaks.
4
+ */
5
+ export function aciRootAttrs(props) {
6
+ return {
7
+ 'data-aci-block': props['data-aci-block'],
8
+ 'data-aci-component': props['data-aci-component'],
9
+ };
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.53-rc.2",
3
+ "version": "0.1.53-rc.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/gradial/bare-metal-go.git",
@@ -160,14 +160,15 @@
160
160
  "next": "^15.5.19",
161
161
  "tsx": "^4.20.0",
162
162
  "typescript": "^5.9.3",
163
- "zod": "^4.0.0"
163
+ "zod": "^4.0.0",
164
+ "astro": "^6.4.8"
164
165
  },
165
166
  "optionalDependencies": {
166
- "@gradial/aci-cli-darwin-arm64": "0.1.53-rc.2",
167
- "@gradial/aci-cli-darwin-x64": "0.1.53-rc.2",
168
- "@gradial/aci-cli-linux-arm64": "0.1.53-rc.2",
169
- "@gradial/aci-cli-linux-x64": "0.1.53-rc.2",
170
- "@gradial/aci-cli-win32-arm64": "0.1.53-rc.2",
171
- "@gradial/aci-cli-win32-x64": "0.1.53-rc.2"
167
+ "@gradial/aci-cli-darwin-arm64": "0.1.53-rc.4",
168
+ "@gradial/aci-cli-darwin-x64": "0.1.53-rc.4",
169
+ "@gradial/aci-cli-linux-arm64": "0.1.53-rc.4",
170
+ "@gradial/aci-cli-linux-x64": "0.1.53-rc.4",
171
+ "@gradial/aci-cli-win32-arm64": "0.1.53-rc.4",
172
+ "@gradial/aci-cli-win32-x64": "0.1.53-rc.4"
172
173
  }
173
174
  }