@gradial/aci 0.1.52-rc.21 → 0.1.52-rc.24
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/README.md +79 -5
- package/dist/astro/index.js +4 -0
- package/dist/audience-data-models.d.ts +12 -0
- package/dist/audience-data-models.js +3 -0
- package/dist/cli/mintlify-render.d.mts +1 -0
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +3 -0
- package/dist/collect/api.js +19 -0
- package/dist/collect/assignments.d.ts +7 -9
- package/dist/collect/assignments.js +23 -32
- package/dist/collect/batcher.d.ts +3 -2
- package/dist/collect/consent/envelope.d.ts +7 -1
- package/dist/collect/consent/envelope.js +3 -1
- package/dist/collect/consent/mapping.d.ts +2 -0
- package/dist/collect/consent/mapping.js +1 -0
- package/dist/collect/consenthold.d.ts +38 -0
- package/dist/collect/consenthold.js +82 -0
- package/dist/collect/declarative.js +7 -5
- package/dist/collect/gapbuffer.d.ts +13 -4
- package/dist/collect/gapbuffer.js +10 -4
- package/dist/collect/index.js +66 -19
- package/dist/collect/pageviews.d.ts +11 -11
- package/dist/collect/pageviews.js +27 -22
- package/dist/consent-banner/index.d.ts +17 -0
- package/dist/consent-banner/index.js +23 -0
- package/dist/content/provider.d.ts +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/domain/edge/identity.d.ts +5 -0
- package/dist/internal/domain/edge/identity.js +49 -13
- package/dist/internal/domain/edge/mimic_token.d.ts +1 -0
- package/dist/internal/domain/edge/mimic_token.js +74 -7
- package/dist/internal/domain/edge/personalization-core.d.ts +8 -33
- package/dist/internal/domain/edge/personalization-core.js +151 -195
- package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
- package/dist/internal/domain/edge/personalization_index.js +56 -0
- package/dist/internal/domain/edge/record/codec.d.ts +1 -30
- package/dist/internal/domain/edge/record/codec.js +1 -156
- package/dist/internal/domain/edge/resolve.js +55 -38
- package/dist/internal/domain/edge/types.d.ts +41 -32
- package/dist/internal/domain/edge/variant_key.d.ts +3 -0
- package/dist/internal/domain/edge/variant_key.js +44 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +13 -0
- package/dist/internal/platform/collectionstore/errors.js +28 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +198 -0
- package/dist/mintlify/config.d.ts +3 -0
- package/dist/mintlify/config.js +77 -0
- package/dist/mintlify/index.d.ts +3 -0
- package/dist/mintlify/index.js +2 -0
- package/dist/mintlify/json.d.ts +8 -0
- package/dist/mintlify/json.js +37 -0
- package/dist/mintlify/materialize.d.ts +12 -0
- package/dist/mintlify/materialize.js +159 -0
- package/dist/mintlify/types.d.ts +69 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +252 -0
- package/dist/next/assignments-navigation.d.ts +4 -0
- package/dist/next/assignments-navigation.js +18 -0
- package/dist/next/assignments-script.d.ts +3 -2
- package/dist/next/assignments-script.js +5 -5
- package/dist/next/error-seat.js +3 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +3 -0
- package/dist/next/render.js +8 -1
- package/dist/react/VideoPlayer.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/registry.d.ts +4 -2
- package/dist/testing/conformance/checks.d.ts +2 -2
- package/dist/testing/conformance/checks.js +2 -0
- package/dist/testing/conformance/index.d.ts +1 -0
- package/dist/testing/conformance/index.js +1 -0
- package/dist/testing/conformance/next-stubs.d.ts +6 -0
- package/dist/testing/conformance/next-stubs.js +9 -0
- package/dist/testing/conformance/preset.d.ts +3 -2
- package/dist/testing/conformance/preset.js +12 -8
- package/dist/testing/conformance/root-identity.d.ts +31 -0
- package/dist/testing/conformance/root-identity.js +380 -0
- package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-image.js +5 -0
- package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-link.js +4 -0
- package/dist/testing/conformance/types.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/types/component.d.ts +9 -0
- package/package.json +11 -7
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/types/component.ts +10 -0
- package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
- package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
- package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
- package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
- package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
- package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
- package/templates/astro/template/package.json.tmpl +4 -4
- package/templates/common/.agents/skills/aci/SKILL.md +50 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
- package/templates/common/.agents/skills/authoring/references/block-registry.md +15 -0
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/.content/schemas/site.schema.json +11 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
- package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
- package/templates/common/starter-content/.content/patterns/layouts/default.json +2 -2
- package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
- package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
- package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
- package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance check: rootIdentity (ADR 0051).
|
|
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).
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { join, relative } from 'node:path';
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
|
+
import { execSync } from 'node:child_process';
|
|
23
|
+
const CHECK = 'rootIdentity';
|
|
24
|
+
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);
|
|
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
|
+
export async function checkRootIdentity(ctx) {
|
|
140
|
+
return ctx.framework === 'next' ? checkNextRootIdentity(ctx) : checkAstroRootIdentity(ctx);
|
|
141
|
+
}
|
|
142
|
+
async function checkNextRootIdentity(ctx) {
|
|
143
|
+
const registryFile = findRegistryFile(ctx.rootDir);
|
|
144
|
+
if (!registryFile) {
|
|
145
|
+
return [fail(`${CHECK}: no registry file imports createRegistry from @gradial/aci`)];
|
|
146
|
+
}
|
|
147
|
+
const siteRequire = createRequire(join(ctx.rootDir, 'package.json'));
|
|
148
|
+
let react;
|
|
149
|
+
try {
|
|
150
|
+
react = siteRequire('react-dom/server');
|
|
151
|
+
// The site's tsconfig may keep `jsx: preserve` for Next, in which case
|
|
152
|
+
// the test transform emits classic React.createElement calls.
|
|
153
|
+
globalThis.React = siteRequire('react');
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
return [fail(`${CHECK}: cannot load react-dom/server from the site (${err instanceof Error ? err.message : String(err)})`)];
|
|
157
|
+
}
|
|
158
|
+
const { register } = await import('node:module');
|
|
159
|
+
register(stubHookURL());
|
|
160
|
+
let mod;
|
|
161
|
+
try {
|
|
162
|
+
mod = (await import(pathToFileURL(registryFile).href));
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return [fail(`${CHECK}: cannot load ${relative(ctx.rootDir, registryFile)} (${err instanceof Error ? err.message : String(err)})`)];
|
|
166
|
+
}
|
|
167
|
+
const registry = Object.values(mod).find(isRegistry);
|
|
168
|
+
if (!registry) {
|
|
169
|
+
return [fail(`${CHECK}: ${relative(ctx.rootDir, registryFile)} does not export the createRegistry result`)];
|
|
170
|
+
}
|
|
171
|
+
const source = readFileSync(registryFile, 'utf8');
|
|
172
|
+
const failures = await findRootIdentityFailures(registry, compiledBlocks(ctx.rootDir), react, componentFileResolver(source, registry));
|
|
173
|
+
if (failures.length === 0) {
|
|
174
|
+
return [ok(`${registry.length} registered components`)];
|
|
175
|
+
}
|
|
176
|
+
return [fail(formatRootIdentityFailures(failures, 'next'))];
|
|
177
|
+
}
|
|
178
|
+
async function checkAstroRootIdentity(ctx) {
|
|
179
|
+
const distDir = join(ctx.rootDir, 'dist');
|
|
180
|
+
if (!existsSync(distDir) || distIsStale(distDir, [join(ctx.rootDir, 'src'), join(ctx.rootDir, '.content')])) {
|
|
181
|
+
try {
|
|
182
|
+
execSync('npm run build', { cwd: ctx.rootDir, stdio: 'pipe', timeout: 300_000, env: { ...process.env, NODE_ENV: 'production' } });
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const stderr = err instanceof Error && 'stderr' in err ? String(err.stderr).slice(0, 500) : String(err);
|
|
186
|
+
return [fail(`${CHECK}: npm run build failed, cannot scan dist/ (${stderr})`)];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const authored = astroAuthoredBlocks(join(ctx.rootDir, '.content'));
|
|
190
|
+
if (authored.length === 0) {
|
|
191
|
+
return [fail(`${CHECK}: no authored blocks found under .content/pages`)];
|
|
192
|
+
}
|
|
193
|
+
const html = walkHTML(distDir).map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
194
|
+
const missing = authored.filter(({ id, component }) => {
|
|
195
|
+
const pattern = new RegExp(`<[a-z][^>]*(?:data-aci-block="${escapeRegExp(id)}"[^>]*data-aci-component="${escapeRegExp(component)}"` +
|
|
196
|
+
`|data-aci-component="${escapeRegExp(component)}"[^>]*data-aci-block="${escapeRegExp(id)}")`);
|
|
197
|
+
return !pattern.test(html);
|
|
198
|
+
});
|
|
199
|
+
if (missing.length === 0) {
|
|
200
|
+
return [ok(`${authored.length} authored blocks found on component roots in dist/`)];
|
|
201
|
+
}
|
|
202
|
+
const byComponent = new Map();
|
|
203
|
+
for (const m of missing) {
|
|
204
|
+
if (!byComponent.has(m.component))
|
|
205
|
+
byComponent.set(m.component, { component: m.component, missing: ['data-aci-block'] });
|
|
206
|
+
}
|
|
207
|
+
return [fail(formatRootIdentityFailures([...byComponent.values()], 'astro'))];
|
|
208
|
+
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Helpers
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
/** The stub hook next to this module: compiled `.js` in dist, `.ts` when run from source. */
|
|
213
|
+
function stubHookURL() {
|
|
214
|
+
const js = new URL('./next-stubs.js', import.meta.url);
|
|
215
|
+
if (existsSync(fileURLToPath(js)))
|
|
216
|
+
return js;
|
|
217
|
+
return new URL('./next-stubs.ts', import.meta.url);
|
|
218
|
+
}
|
|
219
|
+
function ok(detail) {
|
|
220
|
+
return { name: CHECK, label: LABEL, ok: true, detail };
|
|
221
|
+
}
|
|
222
|
+
// A failure's label is the message itself so the test runner prints it
|
|
223
|
+
// without a generic prefix; the check name is the first word of the message.
|
|
224
|
+
function fail(message) {
|
|
225
|
+
return { name: CHECK, label: message, ok: false };
|
|
226
|
+
}
|
|
227
|
+
function isRegistry(value) {
|
|
228
|
+
return Array.isArray(value) && value.length > 0
|
|
229
|
+
&& value.every((e) => e && typeof e === 'object' && typeof e.id === 'string' && 'component' in e && 'contract' in e);
|
|
230
|
+
}
|
|
231
|
+
function findRegistryFile(rootDir) {
|
|
232
|
+
const skip = new Set(['node_modules', '.next', '.aci', 'dist', '.git', 'tests']);
|
|
233
|
+
const walk = (dir) => {
|
|
234
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
235
|
+
if (entry.name.startsWith('.') && entry.name !== '.')
|
|
236
|
+
continue;
|
|
237
|
+
const full = join(dir, entry.name);
|
|
238
|
+
if (entry.isDirectory()) {
|
|
239
|
+
if (skip.has(entry.name))
|
|
240
|
+
continue;
|
|
241
|
+
const found = walk(full);
|
|
242
|
+
if (found)
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
else if (/\.(ts|tsx|js|mjs)$/.test(entry.name) && readFileSync(full, 'utf8').includes('createRegistry(')) {
|
|
246
|
+
return full;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
};
|
|
251
|
+
return walk(rootDir);
|
|
252
|
+
}
|
|
253
|
+
/** Map a registered component id to the import path the registry file names for it. */
|
|
254
|
+
function componentFileResolver(registrySource, registry) {
|
|
255
|
+
const imports = new Map();
|
|
256
|
+
for (const m of registrySource.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
257
|
+
const names = (m[1] ?? m[2]).split(',').map((n) => n.trim().split(/\s+as\s+/).pop()).filter(Boolean);
|
|
258
|
+
for (const name of names)
|
|
259
|
+
imports.set(name, m[3]);
|
|
260
|
+
}
|
|
261
|
+
const byId = new Map();
|
|
262
|
+
for (const entry of registry) {
|
|
263
|
+
const fn = entry.component;
|
|
264
|
+
const file = typeof fn === 'function' && fn.name ? imports.get(fn.name) : undefined;
|
|
265
|
+
if (file)
|
|
266
|
+
byId.set(entry.id, file);
|
|
267
|
+
}
|
|
268
|
+
return (component) => byId.get(component);
|
|
269
|
+
}
|
|
270
|
+
/** Blocks from the site's compiled content, when `npm run content:compile` has run. */
|
|
271
|
+
function compiledBlocks(rootDir) {
|
|
272
|
+
const compiled = join(rootDir, '.aci', 'compiled');
|
|
273
|
+
if (!existsSync(compiled))
|
|
274
|
+
return [];
|
|
275
|
+
const blocks = [];
|
|
276
|
+
const routes = join(compiled, 'routes');
|
|
277
|
+
if (existsSync(routes)) {
|
|
278
|
+
for (const file of walkJSON(routes)) {
|
|
279
|
+
const doc = JSON.parse(readFileSync(file, 'utf8'));
|
|
280
|
+
for (const region of Object.values(doc.regions ?? {}))
|
|
281
|
+
blocks.push(...region);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const fragments = join(compiled, 'fragments');
|
|
285
|
+
if (existsSync(fragments)) {
|
|
286
|
+
for (const file of walkJSON(fragments)) {
|
|
287
|
+
const doc = JSON.parse(readFileSync(file, 'utf8'));
|
|
288
|
+
if (doc.block) {
|
|
289
|
+
const id = file.slice(fragments.length + 1).replace(/\.json$/, '');
|
|
290
|
+
blocks.push({ $type: 'component', id, component: doc.block.component, props: doc.block.props });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return blocks;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Every block the Astro scan must find in dist/: page blocks by their own id,
|
|
298
|
+
* plus fragments (navbar, footer) by fragment id, which is the block id the
|
|
299
|
+
* renderer synthesizes when a layout slot default places them.
|
|
300
|
+
*/
|
|
301
|
+
export function astroAuthoredBlocks(contentDir) {
|
|
302
|
+
const out = authoredBlocks(join(contentDir, 'pages'));
|
|
303
|
+
const fragmentsDir = join(contentDir, 'fragments');
|
|
304
|
+
if (existsSync(fragmentsDir)) {
|
|
305
|
+
for (const entry of readdirSync(fragmentsDir, { withFileTypes: true })) {
|
|
306
|
+
if (!entry.isDirectory())
|
|
307
|
+
continue;
|
|
308
|
+
const file = join(fragmentsDir, entry.name, '_index.json');
|
|
309
|
+
if (!existsSync(file))
|
|
310
|
+
continue;
|
|
311
|
+
const doc = JSON.parse(readFileSync(file, 'utf8'));
|
|
312
|
+
const component = doc.block?.component;
|
|
313
|
+
if (typeof component === 'string' && component !== '') {
|
|
314
|
+
out.push({ id: doc.fragmentId ?? entry.name, component });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
/** dist/ is stale when any source or content file is newer than its newest output. */
|
|
321
|
+
function distIsStale(distDir, inputs) {
|
|
322
|
+
const newest = (dir) => {
|
|
323
|
+
if (!existsSync(dir))
|
|
324
|
+
return 0;
|
|
325
|
+
let max = 0;
|
|
326
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
327
|
+
const full = join(dir, entry.name);
|
|
328
|
+
max = Math.max(max, entry.isDirectory() ? newest(full) : statSync(full).mtimeMs);
|
|
329
|
+
}
|
|
330
|
+
return max;
|
|
331
|
+
};
|
|
332
|
+
const built = newest(distDir);
|
|
333
|
+
return inputs.some((dir) => newest(dir) > built);
|
|
334
|
+
}
|
|
335
|
+
function authoredBlocks(pagesDir) {
|
|
336
|
+
if (!existsSync(pagesDir))
|
|
337
|
+
return [];
|
|
338
|
+
const out = [];
|
|
339
|
+
const collect = (value) => {
|
|
340
|
+
if (Array.isArray(value)) {
|
|
341
|
+
value.forEach(collect);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!value || typeof value !== 'object')
|
|
345
|
+
return;
|
|
346
|
+
const node = value;
|
|
347
|
+
if (typeof node.component === 'string' && node.props && typeof node.props === 'object' && typeof node.id === 'string' && node.id !== '') {
|
|
348
|
+
out.push({ id: node.id, component: node.component });
|
|
349
|
+
}
|
|
350
|
+
Object.values(node).forEach(collect);
|
|
351
|
+
};
|
|
352
|
+
for (const file of walkJSON(pagesDir)) {
|
|
353
|
+
if (!file.endsWith('_index.json'))
|
|
354
|
+
continue;
|
|
355
|
+
const doc = JSON.parse(readFileSync(file, 'utf8'));
|
|
356
|
+
if (doc.$type === 'page')
|
|
357
|
+
collect(doc.regions);
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
function walkJSON(dir) {
|
|
362
|
+
return walkExt(dir, '.json');
|
|
363
|
+
}
|
|
364
|
+
function walkHTML(dir) {
|
|
365
|
+
return walkExt(dir, '.html');
|
|
366
|
+
}
|
|
367
|
+
function walkExt(dir, ext) {
|
|
368
|
+
const out = [];
|
|
369
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
370
|
+
const full = join(dir, entry.name);
|
|
371
|
+
if (entry.isDirectory())
|
|
372
|
+
out.push(...walkExt(full, ext));
|
|
373
|
+
else if (entry.name.endsWith(ext))
|
|
374
|
+
out.push(full);
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
function escapeRegExp(value) {
|
|
379
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
380
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
export default function Image({ src, alt, ...rest }) {
|
|
3
|
+
const source = typeof src === 'object' && src !== null && 'src' in src ? String(src.src) : String(src);
|
|
4
|
+
return createElement('img', { src: source, alt: alt ?? '', ...rest });
|
|
5
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export default function Link({ href, children, ...rest }: {
|
|
3
|
+
href: unknown;
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
} & Record<string, unknown>): import("react").ReactElement<{
|
|
6
|
+
href: string;
|
|
7
|
+
}, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/** Supported framework identifiers. */
|
|
8
8
|
export type ConformanceFramework = 'next' | 'astro';
|
|
9
9
|
/** Individual check names that can be enabled/disabled. */
|
|
10
|
-
export type ConformanceCheckName = 'config' | 'registry' | 'pageEntry' | 'middleware' | 'noFileContentProvider' | 'noCustomContent' | 'noRequestTimeAssetRoute' | 'buildSucceeds' | 'contentCompile' | 'sdkVersion';
|
|
10
|
+
export type ConformanceCheckName = 'config' | 'registry' | 'pageEntry' | 'middleware' | 'noFileContentProvider' | 'noCustomContent' | 'noRequestTimeAssetRoute' | 'buildSucceeds' | 'contentCompile' | 'sdkVersion' | 'rootIdentity';
|
|
11
11
|
/**
|
|
12
12
|
* Options for the conformance preset.
|
|
13
13
|
*
|
|
@@ -51,7 +51,7 @@ export interface ConformanceResult {
|
|
|
51
51
|
failures: ConformanceCheckResult[];
|
|
52
52
|
}
|
|
53
53
|
/** A conformance check function. */
|
|
54
|
-
export type ConformanceCheckFn = (ctx: ConformanceContext) => ConformanceCheckResult[]
|
|
54
|
+
export type ConformanceCheckFn = (ctx: ConformanceContext) => ConformanceCheckResult[] | Promise<ConformanceCheckResult[]>;
|
|
55
55
|
/** Context passed to each check function. */
|
|
56
56
|
export interface ConformanceContext {
|
|
57
57
|
framework: ConformanceFramework;
|
package/dist/testing/index.js
CHANGED
|
@@ -53,9 +53,9 @@ export class FixtureContentProvider {
|
|
|
53
53
|
return [...this.pages.entries()]
|
|
54
54
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
55
55
|
.map(([routePath, page]) => ({
|
|
56
|
-
slug: routePath === '/' ? '
|
|
56
|
+
slug: routePath === '/' ? '' : routePath.replace(/^\//, ''),
|
|
57
57
|
path: routePath,
|
|
58
|
-
payloadRef:
|
|
58
|
+
payloadRef: routePath === '/' ? 'routes/index.json' : `routes/${routePath.replace(/^\//, '')}.json`,
|
|
59
59
|
digest: '',
|
|
60
60
|
}));
|
|
61
61
|
}
|
|
@@ -18,6 +18,15 @@ export interface ComponentValidationIssue {
|
|
|
18
18
|
message: string;
|
|
19
19
|
severity?: 'error' | 'warning';
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
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.
|
|
25
|
+
*/
|
|
26
|
+
export interface RootIdentityProps {
|
|
27
|
+
'data-aci-block'?: string;
|
|
28
|
+
'data-aci-component'?: string;
|
|
29
|
+
}
|
|
21
30
|
export interface ComponentDefinition<TId extends string = string, TSchema = unknown> {
|
|
22
31
|
id: TId;
|
|
23
32
|
displayName?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.52-rc.
|
|
3
|
+
"version": "0.1.52-rc.24",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/gradial/bare-metal-go.git",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"types": "./dist/edge/expression-capabilities.v1.d.ts",
|
|
46
46
|
"import": "./dist/edge/expression-capabilities.v1.js"
|
|
47
47
|
},
|
|
48
|
+
"./mintlify": {
|
|
49
|
+
"types": "./dist/mintlify/index.d.ts",
|
|
50
|
+
"import": "./dist/mintlify/index.js"
|
|
51
|
+
},
|
|
48
52
|
"./providers/file": {
|
|
49
53
|
"types": "./dist/providers/file.d.ts",
|
|
50
54
|
"import": "./dist/providers/file.js"
|
|
@@ -155,11 +159,11 @@
|
|
|
155
159
|
"zod": "^4.0.0"
|
|
156
160
|
},
|
|
157
161
|
"optionalDependencies": {
|
|
158
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.
|
|
159
|
-
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.
|
|
160
|
-
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.
|
|
161
|
-
"@gradial/aci-cli-linux-x64": "0.1.52-rc.
|
|
162
|
-
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.
|
|
163
|
-
"@gradial/aci-cli-win32-x64": "0.1.52-rc.
|
|
162
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.24",
|
|
163
|
+
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.24",
|
|
164
|
+
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.24",
|
|
165
|
+
"@gradial/aci-cli-linux-x64": "0.1.52-rc.24",
|
|
166
|
+
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.24",
|
|
167
|
+
"@gradial/aci-cli-win32-x64": "0.1.52-rc.24"
|
|
164
168
|
}
|
|
165
169
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadCompiledMintlifySource, materializeMintlifySite } from '../../dist/mintlify/index.js';
|
|
3
|
+
|
|
4
|
+
const [sourceArg, compiledArg, outputArg] = process.argv.slice(2);
|
|
5
|
+
if (!sourceArg || !compiledArg || !outputArg) {
|
|
6
|
+
throw new Error('Mintlify renderer requires content, compiled, and output directories');
|
|
7
|
+
}
|
|
8
|
+
const source = await loadCompiledMintlifySource(path.resolve(sourceArg), path.resolve(compiledArg));
|
|
9
|
+
await materializeMintlifySite({ ...source, outputDir: path.resolve(outputArg) });
|
package/src/types/component.ts
CHANGED
|
@@ -23,6 +23,16 @@ export interface ComponentValidationIssue {
|
|
|
23
23
|
severity?: 'error' | 'warning';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Root identity props the SDK renderers inject into every page and fragment
|
|
28
|
+
* block (sdk I-19, ADR 0051). A component spreads them onto its own root
|
|
29
|
+
* element; the starter conformance test fails a component that drops them.
|
|
30
|
+
*/
|
|
31
|
+
export interface RootIdentityProps {
|
|
32
|
+
'data-aci-block'?: string;
|
|
33
|
+
'data-aci-component'?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
export interface ComponentDefinition<TId extends string = string, TSchema = unknown> {
|
|
27
37
|
id: TId;
|
|
28
38
|
displayName?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Container from '../elements/Container.astro';
|
|
3
|
+
import type { RootIdentityProps } from '@gradial/aci';
|
|
3
4
|
import Logo from '../elements/Logo.astro';
|
|
4
5
|
|
|
5
6
|
interface FooterLink {
|
|
@@ -21,7 +22,7 @@ interface FooterColumn {
|
|
|
21
22
|
links?: FooterLink[];
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
interface Props {
|
|
25
|
+
interface Props extends RootIdentityProps {
|
|
25
26
|
brandHref: string;
|
|
26
27
|
brandLabel?: string;
|
|
27
28
|
brandLogo?: { src: string; alt?: string } | null;
|
|
@@ -38,7 +39,8 @@ const {
|
|
|
38
39
|
social = [],
|
|
39
40
|
columns = [],
|
|
40
41
|
legalLinks = [],
|
|
41
|
-
copyright = ''
|
|
42
|
+
copyright = '',
|
|
43
|
+
...root
|
|
42
44
|
} = Astro.props;
|
|
43
45
|
|
|
44
46
|
function columnGroups(column: FooterColumn): FooterGroup[] {
|
|
@@ -54,7 +56,7 @@ function columnGroups(column: FooterColumn): FooterGroup[] {
|
|
|
54
56
|
}
|
|
55
57
|
---
|
|
56
58
|
|
|
57
|
-
<footer class="bm-footer [font-family:var(--font-family-base)] text-fg" id="footer">
|
|
59
|
+
<footer {...root} class="bm-footer [font-family:var(--font-family-base)] text-fg" id="footer">
|
|
58
60
|
<Container className="py-[var(--spacing-footer-y)]">
|
|
59
61
|
<div class="grid gap-12 pb-24 md:grid-cols-[var(--footer-grid-columns)] lg:gap-24">
|
|
60
62
|
<a href={brandHref} class="self-start">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Button from '../elements/Button.astro';
|
|
3
|
+
import type { RootIdentityProps } from '@gradial/aci';
|
|
3
4
|
import Container from '../elements/Container.astro';
|
|
4
5
|
import Icon from '../elements/Icon.astro';
|
|
5
6
|
import Logo from '../elements/Logo.astro';
|
|
@@ -14,7 +15,7 @@ interface UtilityItem extends NavItem {
|
|
|
14
15
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
interface Props {
|
|
18
|
+
interface Props extends RootIdentityProps {
|
|
18
19
|
brandHref: string;
|
|
19
20
|
brandLabel?: string;
|
|
20
21
|
brandLogo?: { src: string; alt?: string } | null;
|
|
@@ -27,11 +28,12 @@ const {
|
|
|
27
28
|
brandLabel = 'Gradial',
|
|
28
29
|
brandLogo = null,
|
|
29
30
|
navigation = [],
|
|
30
|
-
utilities = []
|
|
31
|
+
utilities = [],
|
|
32
|
+
...root
|
|
31
33
|
} = Astro.props;
|
|
32
34
|
---
|
|
33
35
|
|
|
34
|
-
<header class="bm-header sticky top-0 z-[var(--z-header)] [font-family:var(--font-family-base)] text-fg">
|
|
36
|
+
<header {...root} class="bm-header sticky top-0 z-[var(--z-header)] [font-family:var(--font-family-base)] text-fg">
|
|
35
37
|
<a
|
|
36
38
|
href="#content-main"
|
|
37
39
|
class="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-10 focus:rounded-button focus:bg-white focus:px-4 focus:py-2 focus:text-black"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
import type { RootIdentityProps } from '@gradial/aci';
|
|
3
|
+
|
|
4
|
+
interface Props extends RootIdentityProps {
|
|
3
5
|
href: string;
|
|
4
6
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost';
|
|
5
7
|
className?: string;
|
|
@@ -7,7 +9,7 @@ interface Props {
|
|
|
7
9
|
rel?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
const { href, variant = 'primary', className = '', target, rel } = Astro.props;
|
|
12
|
+
const { href, variant = 'primary', className = '', target, rel, ...root } = Astro.props;
|
|
11
13
|
|
|
12
14
|
const variantTokens = {
|
|
13
15
|
primary: {
|
|
@@ -41,6 +43,7 @@ const tokenStyle = `background-color: ${variantTokens.background}; border-color:
|
|
|
41
43
|
---
|
|
42
44
|
|
|
43
45
|
<a
|
|
46
|
+
{...root}
|
|
44
47
|
class={`inline-flex min-h-10 items-center justify-center rounded-button border px-[var(--spacing-button-x)] py-[var(--spacing-button-y)] [font-family:var(--font-family-base)] text-[length:var(--text-button)] font-[var(--font-weight-semibold)] leading-button transition duration-200 hover:opacity-85 ${className}`}
|
|
45
48
|
href={href}
|
|
46
49
|
target={target}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Button from '../elements/Button.astro';
|
|
3
3
|
import Icon from '../elements/Icon.astro';
|
|
4
|
+
import type { RootIdentityProps } from '@gradial/aci';
|
|
4
5
|
import type { ContentProps } from '../../cms/contracts/components/button.contract';
|
|
5
6
|
|
|
6
|
-
type Props = ContentProps;
|
|
7
|
+
type Props = ContentProps & RootIdentityProps;
|
|
7
8
|
|
|
8
9
|
const {
|
|
9
10
|
label,
|
|
@@ -12,13 +13,14 @@ const {
|
|
|
12
13
|
newWindow = false,
|
|
13
14
|
icon = '',
|
|
14
15
|
iconPosition = 'before',
|
|
15
|
-
className = ''
|
|
16
|
+
className = '',
|
|
17
|
+
...root
|
|
16
18
|
} = Astro.props;
|
|
17
19
|
const target = newWindow ? '_blank' : undefined;
|
|
18
20
|
const rel = newWindow ? 'noreferrer' : undefined;
|
|
19
21
|
---
|
|
20
22
|
|
|
21
|
-
<Button {href} {variant} {target} {rel} {className}>
|
|
23
|
+
<Button {href} {variant} {target} {rel} {className} {...root}>
|
|
22
24
|
{icon && iconPosition === 'before' ? <Icon name={icon} className="mr-2 size-4" /> : null}
|
|
23
25
|
<span>{label}</span>
|
|
24
26
|
{icon && iconPosition === 'after' ? <Icon name={icon} className="ml-2 size-4" /> : null}
|