@gradial/aci 0.1.53-rc.2 → 0.1.53-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +26 -4
- package/dist/astro/index.js +2 -5
- package/dist/cli/emit-error-pages.d.mts +2 -1
- package/dist/cli/emit-error-pages.mjs +13 -270
- package/dist/content/component-block.d.ts +17 -0
- package/dist/content/component-block.js +40 -0
- package/dist/content/resolve-slots.js +2 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/internal/aci-root.d.ts +1 -0
- package/dist/internal/aci-root.js +49 -0
- package/dist/next/config.js +6 -13
- package/dist/next/error-seat-payload.d.ts +1 -1
- package/dist/next/error-seat-payload.js +1 -2
- package/dist/next/error-seat.js +3 -4
- package/dist/next/render.js +2 -8
- package/dist/platform-tests/adapters/astro.d.ts +2 -0
- package/dist/platform-tests/adapters/astro.js +101 -0
- package/dist/platform-tests/adapters/next.d.ts +9 -0
- package/dist/platform-tests/adapters/next.js +112 -0
- package/dist/platform-tests/adapters/site.d.ts +29 -0
- package/dist/platform-tests/adapters/site.js +92 -0
- package/dist/platform-tests/adapters/types.d.ts +27 -0
- package/dist/platform-tests/adapters/types.js +1 -0
- package/dist/platform-tests/index.d.ts +47 -0
- package/dist/platform-tests/index.js +50 -0
- package/dist/platform-tests/sample-props.d.ts +2 -0
- package/dist/platform-tests/sample-props.js +82 -0
- package/dist/platform-tests/site-check.d.ts +51 -0
- package/dist/platform-tests/site-check.js +193 -0
- package/dist/registry.d.ts +1 -1
- package/dist/testing/conformance/index.d.ts +2 -1
- package/dist/testing/conformance/index.js +2 -1
- package/dist/testing/conformance/next-stubs.d.ts +13 -4
- package/dist/testing/conformance/next-stubs.js +76 -4
- package/dist/testing/conformance/preset.js +1 -2
- package/dist/testing/conformance/root-identity.d.ts +0 -29
- package/dist/testing/conformance/root-identity.js +36 -344
- package/dist/testing/conformance/types.d.ts +1 -1
- package/dist/types/component.d.ts +11 -3
- package/dist/types/component.js +10 -1
- package/package.json +9 -8
- package/src/cli/emit-error-pages.mjs +13 -273
- package/src/types/component.test.ts +17 -0
- package/src/types/component.ts +18 -3
- package/templates/astro/starter-content/src/components/chrome/Footer.astro +3 -4
- package/templates/astro/starter-content/src/components/chrome/Header.astro +3 -4
- package/templates/astro/starter-content/src/components/elements/Button.astro +4 -4
- package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +3 -4
- package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/HomeHero.astro +3 -4
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -4
- package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -4
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +6 -0
- package/templates/common/.agents/skills/authoring/references/block-registry.md +41 -7
- package/templates/common/.agents/skills/authoring/references/sdk-migrations.md +87 -0
- package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +9 -9
- package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +9 -9
- package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +5 -4
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +10 -10
- package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +5 -4
- package/templates/nextjs/template/package.json.tmpl +1 -1
|
@@ -1,361 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Conformance check: rootIdentity (ADR
|
|
2
|
+
* Conformance check: rootIdentity (sdk I-31, ADR 0063).
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
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 {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
result = await checkSiteRootIdentity(ctx.framework, ctx.rootDir, {
|
|
28
|
+
registryFiles: explicit ? [explicit] : [],
|
|
29
|
+
});
|
|
166
30
|
}
|
|
167
31
|
catch (err) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
/**
|
|
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-
|
|
24
|
-
* element
|
|
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
|
|
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;
|
package/dist/types/component.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
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.
|
|
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.
|
|
167
|
-
"@gradial/aci-cli-darwin-x64": "0.1.53-rc.
|
|
168
|
-
"@gradial/aci-cli-linux-arm64": "0.1.53-rc.
|
|
169
|
-
"@gradial/aci-cli-linux-x64": "0.1.53-rc.
|
|
170
|
-
"@gradial/aci-cli-win32-arm64": "0.1.53-rc.
|
|
171
|
-
"@gradial/aci-cli-win32-x64": "0.1.53-rc.
|
|
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
|
}
|