@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,73 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// ACI_CODE_ARTIFACT_BUILD=1 makes both phases content-independent: prepare
|
|
7
|
-
// serializes only the code-owned policy at its existing .aci/compiled path.
|
|
8
|
-
// Emit uses the shipped default without resolving authored error-page bindings.
|
|
9
|
-
//
|
|
10
|
-
// Two phases. The Next template wires both around `next build`; the Astro
|
|
11
|
-
// template runs only `prepare` before `astro build` (its 404/500 seats
|
|
12
|
-
// render through the framework, but the D-16 instrumentation artifact must
|
|
13
|
-
// still reach the compiled content root for the AWS lane):
|
|
14
|
-
//
|
|
15
|
-
// prepare (before `next build`)
|
|
16
|
-
// Ensures compiled content exists at .aci/compiled so withAci() can
|
|
17
|
-
// serialize the bound catchAll page for the client-only error seat
|
|
18
|
-
// (D-9). Compiles ./.content via the platform CLI when needed; when
|
|
19
|
-
// neither compiled content nor authored content exists, it warns and
|
|
20
|
-
// leaves the shipped default in play (REQ-4, warn posture). Then writes
|
|
21
|
-
// error-pages/instrumentation.json (both kinds' serialized tags) into
|
|
22
|
-
// the compiled content root so the tag crosses the JS→Go boundary as
|
|
23
|
-
// data: builders retain the instrumentation file at its existing capsule
|
|
24
|
-
// path, and the Go publish materializer stamps the kind-matching tag into the CDN
|
|
25
|
-
// error artifacts (D-16). An unserializable tenant push fails the build
|
|
26
|
-
// loudly here (delta FIND-006).
|
|
27
|
-
//
|
|
28
|
-
// emit (after `next build`)
|
|
29
|
-
// Materializes `public/500.html` from the page bound to
|
|
30
|
-
// site.json#errorPages.catchAll by booting the standalone build,
|
|
31
|
-
// fetching the bound route, and post-processing the document. Boot-and-
|
|
32
|
-
// fetch beats re-rendering the page here because the standalone server
|
|
33
|
-
// is the only renderer that produces the document exactly as served
|
|
34
|
-
// (layout shell, resolved fragments, RSC-injected head and styles).
|
|
35
|
-
// Post-processing:
|
|
36
|
-
// - all script tags and preload/prefetch links stripped — this
|
|
37
|
-
// removes the collect and assignment tags (REQ-11) along with the
|
|
38
|
-
// framework runtime, which is useless in a statically served copy;
|
|
39
|
-
// - same-origin stylesheets inlined from .next/static (REQ-14 spirit:
|
|
40
|
-
// inline styles; note @font-face/image URLs inside the CSS keep
|
|
41
|
-
// their same-origin paths — this copy is served by the Vercel
|
|
42
|
-
// platform on the deployment that owns those assets);
|
|
43
|
-
// - one self-contained inline script appended pushing the D-14
|
|
44
|
-
// `page_view` (page_type "error") — a static document executes
|
|
45
|
-
// inline scripts, unlike the flight-rendered not-found seat. The
|
|
46
|
-
// push body is site policy (D-15): the tenant-owned
|
|
47
|
-
// src/aci/error-page-instrumentation.mjs when present, the SDK
|
|
48
|
-
// default otherwise;
|
|
49
|
-
// - hard size budget: ≤150 KB raw (REQ-14) — exceeding it fails the
|
|
50
|
-
// build; trimming the design system or falling back to system
|
|
51
|
-
// fonts is the sanctioned escape hatch.
|
|
52
|
-
// Skips when the site already provides its own public/500.html (a file
|
|
53
|
-
// without the generation marker); regenerates its own prior output.
|
|
54
|
-
// Absent bindings emit the shipped branded-neutral default (REQ-4).
|
|
55
|
-
// Vercel picks static per-status files out of the build output
|
|
56
|
-
// ("file presence = configuration"); on the current Pro plan the file
|
|
57
|
-
// serves app-level 500s only and lights up for platform 5xx on a plan
|
|
58
|
-
// upgrade (REQ-10, P-1).
|
|
59
|
-
//
|
|
60
|
-
// Usage: node ./node_modules/@gradial/aci/src/cli/emit-error-pages.mjs <prepare|emit>
|
|
2
|
+
// Code-build error artifacts. prepare transports code-owned analytics policy;
|
|
3
|
+
// emit writes a content-independent static 500. Authored error-page bindings
|
|
4
|
+
// and rendering belong to content preview/publish, never the framework build.
|
|
5
|
+
// Usage: emit-error-pages.mjs <prepare|emit>
|
|
61
6
|
|
|
62
|
-
import { execFileSync, spawn } from 'node:child_process';
|
|
63
7
|
import fs from 'node:fs';
|
|
64
|
-
import net from 'node:net';
|
|
65
8
|
import path from 'node:path';
|
|
66
9
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
67
10
|
|
|
68
11
|
const MARKER = '<!-- aci:error-pages generated -->';
|
|
69
12
|
const SIZE_BUDGET = 150 * 1024;
|
|
70
|
-
const BOOT_DEADLINE_MS = 60_000;
|
|
71
13
|
|
|
72
14
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
73
15
|
const siteRoot = process.cwd();
|
|
@@ -76,9 +18,6 @@ const contentRoot = path.resolve(siteRoot, process.env.ACI_CONTENT_ROOT || '.aci
|
|
|
76
18
|
function log(message) {
|
|
77
19
|
console.log(`[aci error-pages] ${message}`);
|
|
78
20
|
}
|
|
79
|
-
function warn(message) {
|
|
80
|
-
console.warn(`\n[aci error-pages] WARNING: ${message}\n`);
|
|
81
|
-
}
|
|
82
21
|
function fail(message) {
|
|
83
22
|
console.error(`\n[aci error-pages] ERROR: ${message}\n`);
|
|
84
23
|
process.exit(1);
|
|
@@ -88,181 +27,12 @@ async function sdkContent() {
|
|
|
88
27
|
return await import(pathToFileURL(path.join(packageRoot, 'dist', 'content', 'index.js')).href);
|
|
89
28
|
}
|
|
90
29
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// prepare
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
// Retain the deployed transport path for SDK-first upgrades. This file is
|
|
100
|
-
// executable policy, not a compiled page snapshot; do not compile content here.
|
|
101
|
-
export async function prepareCodeArtifact(root = siteRoot, content) {
|
|
30
|
+
// Keep the existing capsule transport path for SDK-first upgrades. This file
|
|
31
|
+
// is executable policy, not a compiled content snapshot.
|
|
32
|
+
export async function prepare(root = siteRoot, content) {
|
|
102
33
|
return writeInstrumentationArtifact(root, content, path.join(root, '.aci', 'compiled'));
|
|
103
34
|
}
|
|
104
35
|
|
|
105
|
-
async function prepare() {
|
|
106
|
-
if (process.env.ACI_CODE_ARTIFACT_BUILD === '1') {
|
|
107
|
-
// Code artifacts carry executable policy only. The selected content and
|
|
108
|
-
// its error-page bindings belong to preview/publish, never this build.
|
|
109
|
-
await prepareCodeArtifact();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (hasCompiledContent()) {
|
|
113
|
-
log(`compiled content present at ${contentRoot}`);
|
|
114
|
-
} else {
|
|
115
|
-
const authored = path.join(siteRoot, '.content');
|
|
116
|
-
if (!fs.existsSync(authored)) {
|
|
117
|
-
warn(
|
|
118
|
-
`neither compiled content (${contentRoot}) nor authored content (./.content) exists; `
|
|
119
|
-
+ 'the error seat and 500.html will serve the shipped default page (REQ-4)',
|
|
120
|
-
);
|
|
121
|
-
} else {
|
|
122
|
-
log('compiling ./.content for the error-page build steps');
|
|
123
|
-
execFileSync(
|
|
124
|
-
'node',
|
|
125
|
-
[path.join(packageRoot, 'bin', 'aci.js'), 'content', 'compile', '--content', authored, '--out', contentRoot],
|
|
126
|
-
{ cwd: siteRoot, stdio: 'inherit' },
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// D-16: the instrumentation artifact rides the compiled content root into
|
|
131
|
-
// the AWS-lane code capsule (source/.aci/compiled/**). Written even when
|
|
132
|
-
// content is absent — the tenant policy module is independent of authored
|
|
133
|
-
// content, and an artifact the Go materializer never receives silently
|
|
134
|
-
// degrades to the default tags (the exact gap D-16 closes).
|
|
135
|
-
try {
|
|
136
|
-
await writeInstrumentationArtifact();
|
|
137
|
-
} catch (error) {
|
|
138
|
-
fail(error.message);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// emit
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
async function resolveCatchAllRoute() {
|
|
147
|
-
const { resolveErrorPageBindings, defaultErrorPageHtml, errorPagePostureFromEnv } = await sdkContent();
|
|
148
|
-
// D-5 posture: the one shared config surface (ACI_ERROR_PAGES_POSTURE);
|
|
149
|
-
// unknown values fail closed here just like the Go stack.
|
|
150
|
-
let posture;
|
|
151
|
-
try {
|
|
152
|
-
posture = errorPagePostureFromEnv();
|
|
153
|
-
} catch (error) {
|
|
154
|
-
fail(error.message);
|
|
155
|
-
}
|
|
156
|
-
if (!hasCompiledContent()) {
|
|
157
|
-
if (posture === 'enforce') {
|
|
158
|
-
fail(`no compiled content at ${contentRoot}; the enforce posture fails closed (D-5)`);
|
|
159
|
-
}
|
|
160
|
-
warn(`no compiled content at ${contentRoot}; emitting the shipped default 500.html (REQ-4)`);
|
|
161
|
-
return { route: null, defaultHtml: defaultErrorPageHtml() };
|
|
162
|
-
}
|
|
163
|
-
const { FileContentProvider } = await import(
|
|
164
|
-
pathToFileURL(path.join(packageRoot, 'dist', 'providers', 'file.js')).href
|
|
165
|
-
);
|
|
166
|
-
const provider = new FileContentProvider(contentRoot);
|
|
167
|
-
const siteConfig = await provider.getSiteConfig();
|
|
168
|
-
const resolution = await resolveErrorPageBindings(siteConfig, { source: provider, posture });
|
|
169
|
-
if (resolution.errors.length > 0) {
|
|
170
|
-
fail(`errorPages bindings are invalid (REQ-5, fail closed):\n- ${resolution.errors.join('\n- ')}`);
|
|
171
|
-
}
|
|
172
|
-
for (const warning of resolution.warnings) warn(warning);
|
|
173
|
-
if (resolution.defaulted || !resolution.bindings) {
|
|
174
|
-
return { route: null, defaultHtml: defaultErrorPageHtml() };
|
|
175
|
-
}
|
|
176
|
-
return { route: resolution.bindings.catchAll, defaultHtml: defaultErrorPageHtml() };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function freePort() {
|
|
180
|
-
return new Promise((resolve, reject) => {
|
|
181
|
-
const server = net.createServer();
|
|
182
|
-
server.unref();
|
|
183
|
-
server.on('error', reject);
|
|
184
|
-
server.listen(0, '127.0.0.1', () => {
|
|
185
|
-
const { port } = server.address();
|
|
186
|
-
server.close(() => resolve(port));
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function fetchRenderedRoute(route) {
|
|
192
|
-
const serverPath = path.join(siteRoot, '.next', 'standalone', 'server.js');
|
|
193
|
-
if (!fs.existsSync(serverPath)) {
|
|
194
|
-
fail(`${serverPath} missing — the emit phase must run after \`next build\` (withAci uses standalone output)`);
|
|
195
|
-
}
|
|
196
|
-
const port = await freePort();
|
|
197
|
-
const logChunks = [];
|
|
198
|
-
const child = spawn('node', [serverPath], {
|
|
199
|
-
cwd: siteRoot,
|
|
200
|
-
detached: true,
|
|
201
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
202
|
-
env: {
|
|
203
|
-
...process.env,
|
|
204
|
-
NODE_ENV: 'production',
|
|
205
|
-
PORT: String(port),
|
|
206
|
-
HOSTNAME: '127.0.0.1',
|
|
207
|
-
ACI_CONTENT_PROVIDER: 'file',
|
|
208
|
-
ACI_CONTENT_ROOT: contentRoot,
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
child.stdout.on('data', (c) => logChunks.push(String(c)));
|
|
212
|
-
child.stderr.on('data', (c) => logChunks.push(String(c)));
|
|
213
|
-
const exited = new Promise((resolve) => {
|
|
214
|
-
child.on('exit', resolve);
|
|
215
|
-
child.on('error', (error) => {
|
|
216
|
-
logChunks.push(`spawn failed: ${error.message}\n`);
|
|
217
|
-
resolve(null);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
const url = `http://127.0.0.1:${port}${route}`;
|
|
223
|
-
const start = Date.now();
|
|
224
|
-
let last = 'no attempt made';
|
|
225
|
-
while (Date.now() - start < BOOT_DEADLINE_MS) {
|
|
226
|
-
try {
|
|
227
|
-
const response = await fetch(url, { redirect: 'follow' });
|
|
228
|
-
if (response.status === 200) return await response.text();
|
|
229
|
-
last = `status ${response.status}`;
|
|
230
|
-
} catch (error) {
|
|
231
|
-
last = error.message;
|
|
232
|
-
}
|
|
233
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
234
|
-
}
|
|
235
|
-
fail(`GET ${url} did not render within ${BOOT_DEADLINE_MS}ms (last: ${last})\n--- server output ---\n${logChunks.join('').slice(-4000)}`);
|
|
236
|
-
return '';
|
|
237
|
-
} finally {
|
|
238
|
-
const killGroup = (signal) => {
|
|
239
|
-
if (!child.pid) return;
|
|
240
|
-
try { process.kill(-child.pid, signal); } catch { /* gone */ }
|
|
241
|
-
};
|
|
242
|
-
killGroup('SIGTERM');
|
|
243
|
-
const killTimer = setTimeout(() => killGroup('SIGKILL'), 10_000);
|
|
244
|
-
await exited;
|
|
245
|
-
clearTimeout(killTimer);
|
|
246
|
-
killGroup('SIGKILL');
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function inlineStylesheets(html) {
|
|
251
|
-
return html.replace(/<link\b[^>]*rel="stylesheet"[^>]*>/gi, (tag) => {
|
|
252
|
-
const href = tag.match(/href="([^"]+)"/i)?.[1];
|
|
253
|
-
if (!href || !href.startsWith('/_next/')) {
|
|
254
|
-
warn(`dropping non-inlinable stylesheet ${href ?? '<unknown>'} from 500.html (self-containment, REQ-14)`);
|
|
255
|
-
return '';
|
|
256
|
-
}
|
|
257
|
-
const cssPath = path.join(siteRoot, '.next', href.replace(/^\/_next\//, ''));
|
|
258
|
-
if (!fs.existsSync(cssPath)) {
|
|
259
|
-
warn(`stylesheet ${href} not found in .next output; dropping it from 500.html`);
|
|
260
|
-
return '';
|
|
261
|
-
}
|
|
262
|
-
return `<style>${fs.readFileSync(cssPath, 'utf8')}</style>`;
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
36
|
// Relative convention path of the tenant-owned instrumentation policy
|
|
267
37
|
// module (D-15, code-organization R-14).
|
|
268
38
|
const INSTRUMENTATION_MODULE = path.join('src', 'aci', 'error-page-instrumentation.mjs');
|
|
@@ -372,47 +142,17 @@ export function stampInstrumentation(html, instrumentationTag) {
|
|
|
372
142
|
return stamped;
|
|
373
143
|
}
|
|
374
144
|
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
// Strip every script tag: framework runtime chunks are dead weight in a
|
|
378
|
-
// statically served copy, and this removes the collect script and the
|
|
379
|
-
// data-aci-assignments tag (REQ-11) in one sweep.
|
|
380
|
-
out = out.replace(/<script\b[\s\S]*?<\/script>/gi, '');
|
|
381
|
-
out = out.replace(/<link\b[^>]*rel="(?:preload|modulepreload|prefetch)"[^>]*>/gi, '');
|
|
382
|
-
out = inlineStylesheets(out);
|
|
383
|
-
return stampInstrumentation(out, instrumentationTag);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async function emit() {
|
|
387
|
-
const publicDir = path.join(siteRoot, 'public');
|
|
145
|
+
export async function emit(root = siteRoot, content) {
|
|
146
|
+
const publicDir = path.join(root, 'public');
|
|
388
147
|
const target = path.join(publicDir, '500.html');
|
|
389
148
|
if (fs.existsSync(target) && !fs.readFileSync(target, 'utf8').includes(MARKER)) {
|
|
390
149
|
log('public/500.html already provided by the site; leaving it untouched (skip-if-present)');
|
|
391
150
|
return;
|
|
392
151
|
}
|
|
393
152
|
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
let instrumentationTag;
|
|
398
|
-
try {
|
|
399
|
-
instrumentationTag = await resolveInstrumentationTag();
|
|
400
|
-
} catch (error) {
|
|
401
|
-
fail(error.message);
|
|
402
|
-
}
|
|
403
|
-
let html;
|
|
404
|
-
try {
|
|
405
|
-
if (!route) {
|
|
406
|
-
// The shipped default is already self-contained; it gets the same
|
|
407
|
-
// instrumentation-plus-marker append used for rendered pages.
|
|
408
|
-
html = stampInstrumentation(defaultHtml, instrumentationTag);
|
|
409
|
-
} else {
|
|
410
|
-
log(`rendering bound catchAll page ${route} for public/500.html`);
|
|
411
|
-
html = postProcess(await fetchRenderedRoute(route), instrumentationTag);
|
|
412
|
-
}
|
|
413
|
-
} catch (error) {
|
|
414
|
-
fail(error.message);
|
|
415
|
-
}
|
|
153
|
+
const sdk = content ?? (await sdkContent());
|
|
154
|
+
const instrumentationTag = await resolveInstrumentationTag(root, sdk);
|
|
155
|
+
const html = stampInstrumentation(sdk.defaultErrorPageHtml(), instrumentationTag);
|
|
416
156
|
|
|
417
157
|
const size = Buffer.byteLength(html);
|
|
418
158
|
if (size > SIZE_BUDGET) {
|
|
@@ -424,7 +164,7 @@ async function emit() {
|
|
|
424
164
|
|
|
425
165
|
fs.mkdirSync(publicDir, { recursive: true });
|
|
426
166
|
fs.writeFileSync(target, html);
|
|
427
|
-
log(`wrote public/500.html (${size} bytes
|
|
167
|
+
log(`wrote public/500.html (${size} bytes, shipped default)`);
|
|
428
168
|
}
|
|
429
169
|
|
|
430
170
|
// Run only when executed as a CLI — the module is also imported by unit
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { aciRootAttrs } from './component.js';
|
|
5
|
+
|
|
6
|
+
test('aciRootAttrs returns exactly the two root identity keys (sdk D-9)', () => {
|
|
7
|
+
const props = {
|
|
8
|
+
'data-aci-block': 'hero-1',
|
|
9
|
+
'data-aci-component': 'hero',
|
|
10
|
+
title: 'Ship pages faster',
|
|
11
|
+
className: 'leak',
|
|
12
|
+
};
|
|
13
|
+
assert.deepEqual(aciRootAttrs(props), {
|
|
14
|
+
'data-aci-block': 'hero-1',
|
|
15
|
+
'data-aci-component': 'hero',
|
|
16
|
+
});
|
|
17
|
+
});
|
package/src/types/component.ts
CHANGED
|
@@ -25,14 +25,29 @@ export interface ComponentValidationIssue {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Root identity props the SDK renderers inject into every page and fragment
|
|
28
|
-
* block (sdk I-
|
|
29
|
-
* element
|
|
28
|
+
* block (sdk I-31, ADR 0051). A component places them on its own root
|
|
29
|
+
* element with `{...aciRootAttrs(props)}` or a rest-spread; the conformance
|
|
30
|
+
* check fails a component that drops them (ADR 0063).
|
|
30
31
|
*/
|
|
31
|
-
export interface
|
|
32
|
+
export interface AciRootProps {
|
|
32
33
|
'data-aci-block'?: string;
|
|
33
34
|
'data-aci-component'?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** The former name of {@link AciRootProps} (sdk D-9). */
|
|
38
|
+
export type RootIdentityProps = AciRootProps;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Exactly the {@link AciRootProps} keys of `props`, for the root element:
|
|
42
|
+
* `<section {...aciRootAttrs(props)}>`. Nothing else in `props` leaks.
|
|
43
|
+
*/
|
|
44
|
+
export function aciRootAttrs(props: AciRootProps): AciRootProps {
|
|
45
|
+
return {
|
|
46
|
+
'data-aci-block': props['data-aci-block'],
|
|
47
|
+
'data-aci-component': props['data-aci-component'],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export interface ComponentDefinition<TId extends string = string, TSchema = unknown> {
|
|
37
52
|
id: TId;
|
|
38
53
|
displayName?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Container from '../elements/Container.astro';
|
|
3
|
-
import type
|
|
3
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
4
4
|
import Logo from '../elements/Logo.astro';
|
|
5
5
|
|
|
6
6
|
interface FooterLink {
|
|
@@ -22,7 +22,7 @@ interface FooterColumn {
|
|
|
22
22
|
links?: FooterLink[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
interface Props extends
|
|
25
|
+
interface Props extends AciRootProps {
|
|
26
26
|
brandHref: string;
|
|
27
27
|
brandLabel?: string;
|
|
28
28
|
brandLogo?: { src: string; alt?: string } | null;
|
|
@@ -40,7 +40,6 @@ const {
|
|
|
40
40
|
columns = [],
|
|
41
41
|
legalLinks = [],
|
|
42
42
|
copyright = '',
|
|
43
|
-
...root
|
|
44
43
|
} = Astro.props;
|
|
45
44
|
|
|
46
45
|
function columnGroups(column: FooterColumn): FooterGroup[] {
|
|
@@ -56,7 +55,7 @@ function columnGroups(column: FooterColumn): FooterGroup[] {
|
|
|
56
55
|
}
|
|
57
56
|
---
|
|
58
57
|
|
|
59
|
-
<footer {...
|
|
58
|
+
<footer {...aciRootAttrs(Astro.props)} class="bm-footer [font-family:var(--font-family-base)] text-fg" id="footer">
|
|
60
59
|
<Container className="py-[var(--spacing-footer-y)]">
|
|
61
60
|
<div class="grid gap-12 pb-24 md:grid-cols-[var(--footer-grid-columns)] lg:gap-24">
|
|
62
61
|
<a href={brandHref} class="self-start">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Button from '../elements/Button.astro';
|
|
3
|
-
import type
|
|
3
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
4
4
|
import Container from '../elements/Container.astro';
|
|
5
5
|
import Icon from '../elements/Icon.astro';
|
|
6
6
|
import Logo from '../elements/Logo.astro';
|
|
@@ -15,7 +15,7 @@ interface UtilityItem extends NavItem {
|
|
|
15
15
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
interface Props extends
|
|
18
|
+
interface Props extends AciRootProps {
|
|
19
19
|
brandHref: string;
|
|
20
20
|
brandLabel?: string;
|
|
21
21
|
brandLogo?: { src: string; alt?: string } | null;
|
|
@@ -29,11 +29,10 @@ const {
|
|
|
29
29
|
brandLogo = null,
|
|
30
30
|
navigation = [],
|
|
31
31
|
utilities = [],
|
|
32
|
-
...root
|
|
33
32
|
} = Astro.props;
|
|
34
33
|
---
|
|
35
34
|
|
|
36
|
-
<header {...
|
|
35
|
+
<header {...aciRootAttrs(Astro.props)} class="bm-header sticky top-0 z-[var(--z-header)] [font-family:var(--font-family-base)] text-fg">
|
|
37
36
|
<a
|
|
38
37
|
href="#content-main"
|
|
39
38
|
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,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type
|
|
2
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
3
3
|
|
|
4
|
-
interface Props extends
|
|
4
|
+
interface Props extends AciRootProps {
|
|
5
5
|
href: string;
|
|
6
6
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost';
|
|
7
7
|
className?: string;
|
|
@@ -9,7 +9,7 @@ interface Props extends RootIdentityProps {
|
|
|
9
9
|
rel?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const { href, variant = 'primary', className = '', target, rel
|
|
12
|
+
const { href, variant = 'primary', className = '', target, rel } = Astro.props;
|
|
13
13
|
|
|
14
14
|
const variantTokens = {
|
|
15
15
|
primary: {
|
|
@@ -43,7 +43,7 @@ const tokenStyle = `background-color: ${variantTokens.background}; border-color:
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
45
|
<a
|
|
46
|
-
{...
|
|
46
|
+
{...aciRootAttrs(Astro.props)}
|
|
47
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}`}
|
|
48
48
|
href={href}
|
|
49
49
|
target={target}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Button from '../elements/Button.astro';
|
|
3
3
|
import Icon from '../elements/Icon.astro';
|
|
4
|
-
import type
|
|
4
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
5
5
|
import type { ContentProps } from '../../cms/contracts/components/button.contract';
|
|
6
6
|
|
|
7
|
-
type Props = ContentProps &
|
|
7
|
+
type Props = ContentProps & AciRootProps;
|
|
8
8
|
|
|
9
9
|
const {
|
|
10
10
|
label,
|
|
@@ -14,13 +14,12 @@ const {
|
|
|
14
14
|
icon = '',
|
|
15
15
|
iconPosition = 'before',
|
|
16
16
|
className = '',
|
|
17
|
-
...root
|
|
18
17
|
} = Astro.props;
|
|
19
18
|
const target = newWindow ? '_blank' : undefined;
|
|
20
19
|
const rel = newWindow ? 'noreferrer' : undefined;
|
|
21
20
|
---
|
|
22
21
|
|
|
23
|
-
<Button {href} {variant} {target} {rel} {className} {...
|
|
22
|
+
<Button {href} {variant} {target} {rel} {className} {...aciRootAttrs(Astro.props)}>
|
|
24
23
|
{icon && iconPosition === 'before' ? <Icon name={icon} className="mr-2 size-4" /> : null}
|
|
25
24
|
<span>{label}</span>
|
|
26
25
|
{icon && iconPosition === 'after' ? <Icon name={icon} className="ml-2 size-4" /> : null}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Container from '../elements/Container.astro';
|
|
3
|
-
import type
|
|
3
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
4
4
|
import type { RenderChildren, RenderableBlock } from '@gradial/aci/astro';
|
|
5
5
|
|
|
6
|
-
interface Props extends
|
|
6
|
+
interface Props extends AciRootProps {
|
|
7
7
|
theme?: 'auto' | 'light' | 'dark';
|
|
8
8
|
className?: string;
|
|
9
9
|
content?: any[];
|
|
10
10
|
renderChildren?: RenderChildren;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const { theme = 'auto', className = '', content, renderChildren
|
|
13
|
+
const { theme = 'auto', className = '', content, renderChildren } = Astro.props;
|
|
14
14
|
const children: RenderableBlock[] = content && renderChildren ? renderChildren(content) : [];
|
|
15
15
|
const buttonCount = content?.filter((b: any) => b.component === 'button').length ?? 0;
|
|
16
16
|
const layout = buttonCount === 1
|
|
@@ -18,7 +18,7 @@ const layout = buttonCount === 1
|
|
|
18
18
|
: 'flex-col items-center sm:flex-row sm:justify-center sm:gap-4';
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
<section {...
|
|
21
|
+
<section {...aciRootAttrs(Astro.props)} class={`theme-${theme} relative overflow-hidden py-[var(--spacing-section-y)] ${className}`}>
|
|
22
22
|
{theme === 'dark' ? (
|
|
23
23
|
<div class="pointer-events-none absolute inset-0 bg-[image:var(--gradient-hero-base)] opacity-30"></div>
|
|
24
24
|
) : null}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Icon from '../elements/Icon.astro';
|
|
3
|
-
import type
|
|
3
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
4
4
|
|
|
5
|
-
interface Props extends
|
|
5
|
+
interface Props extends AciRootProps {
|
|
6
6
|
icon?: string;
|
|
7
7
|
title: string;
|
|
8
8
|
description?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const { icon = '', title = '', description = ''
|
|
11
|
+
const { icon = '', title = '', description = '' } = Astro.props;
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
<div {...
|
|
14
|
+
<div {...aciRootAttrs(Astro.props)} class="rounded-card border border-[var(--color-edge-space)] bg-[var(--color-space-surface)] p-[var(--spacing-card)] transition-all duration-[var(--transition-duration-fast)] hover:border-[var(--color-edge-space-strong)] hover:bg-[var(--color-space-raised)] hover:shadow-[var(--shadow-card-default)]">
|
|
15
15
|
{icon ? (
|
|
16
16
|
<div class="mb-6 flex h-12 w-12 items-center justify-center rounded-button bg-[color-mix(in_srgb,var(--color-brand-blue)_12%,transparent)] text-[var(--color-brand-blue-light)]">
|
|
17
17
|
<Icon name={icon} className="h-6 w-6" />
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Container from '../elements/Container.astro';
|
|
3
|
-
import type
|
|
3
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
4
4
|
import type { RenderChildren, RenderableBlock } from '@gradial/aci/astro';
|
|
5
5
|
|
|
6
|
-
interface Props extends
|
|
6
|
+
interface Props extends AciRootProps {
|
|
7
7
|
title?: string;
|
|
8
8
|
subtitle?: string;
|
|
9
9
|
features?: any[];
|
|
10
10
|
renderChildren?: RenderChildren;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const { title = '', subtitle = '', features, renderChildren
|
|
13
|
+
const { title = '', subtitle = '', features, renderChildren } = Astro.props;
|
|
14
14
|
const children: RenderableBlock[] = features && renderChildren ? renderChildren(features) : [];
|
|
15
15
|
const count = features?.length ?? 0;
|
|
16
16
|
const cols = count <= 2 ? 'sm:grid-cols-2' : 'sm:grid-cols-2 lg:grid-cols-3';
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
<section {...
|
|
19
|
+
<section {...aciRootAttrs(Astro.props)} class="theme-dark py-[var(--spacing-section-y)] bg-[var(--color-space-deep)]">
|
|
20
20
|
<Container>
|
|
21
21
|
<div class="mb-12 max-w-[var(--max-width-container-narrow)]">
|
|
22
22
|
{title ? (
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Button from '../elements/Button.astro';
|
|
3
3
|
import Container from '../elements/Container.astro';
|
|
4
|
-
import { imageAttrs, type Image, type
|
|
4
|
+
import { aciRootAttrs, imageAttrs, type Image, type AciRootProps } from '@gradial/aci';
|
|
5
5
|
|
|
6
|
-
interface Props extends
|
|
6
|
+
interface Props extends AciRootProps {
|
|
7
7
|
eyebrow?: string;
|
|
8
8
|
headline: string;
|
|
9
9
|
description?: string;
|
|
@@ -19,7 +19,6 @@ const {
|
|
|
19
19
|
ctaLabel,
|
|
20
20
|
ctaHref,
|
|
21
21
|
image,
|
|
22
|
-
...root
|
|
23
22
|
} = Astro.props;
|
|
24
23
|
|
|
25
24
|
const imgAttrs = image ? imageAttrs(image, {
|
|
@@ -30,7 +29,7 @@ const imgAttrs = image ? imageAttrs(image, {
|
|
|
30
29
|
---
|
|
31
30
|
|
|
32
31
|
<section
|
|
33
|
-
{...
|
|
32
|
+
{...aciRootAttrs(Astro.props)}
|
|
34
33
|
class="theme-dark relative min-h-[var(--height-hero-min)] overflow-hidden bg-black text-fg"
|
|
35
34
|
aria-label={image?.alt || headline}
|
|
36
35
|
>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type
|
|
2
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
3
3
|
import type { ContentProps } from '../../cms/contracts/components/personalizationChoice.contract';
|
|
4
4
|
|
|
5
|
-
type Props = ContentProps &
|
|
5
|
+
type Props = ContentProps & AciRootProps;
|
|
6
6
|
|
|
7
|
-
const { title, description, redLabel, blueLabel, refreshLabel
|
|
7
|
+
const { title, description, redLabel, blueLabel, refreshLabel } = Astro.props;
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
<section {...
|
|
10
|
+
<section {...aciRootAttrs(Astro.props)} class="border-y border-[var(--color-border)] bg-[var(--color-surface)] py-[var(--spacing-section-y)]">
|
|
11
11
|
<div class="mx-auto max-w-[var(--layout-content-max)] px-[var(--layout-gutter)] text-center">
|
|
12
12
|
<h2 class="text-[length:var(--text-h2)] font-[var(--font-weight-semibold)] text-[var(--color-fg)]">{title}</h2>
|
|
13
13
|
{description ? <p class="mx-auto mt-3 max-w-2xl text-[var(--color-fg-muted)]">{description}</p> : null}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Container from '../elements/Container.astro';
|
|
3
|
-
import type
|
|
3
|
+
import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
|
|
4
4
|
|
|
5
5
|
interface Stat {
|
|
6
6
|
label: string;
|
|
@@ -8,14 +8,14 @@ interface Stat {
|
|
|
8
8
|
suffix?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
interface Props extends
|
|
11
|
+
interface Props extends AciRootProps {
|
|
12
12
|
stats?: Stat[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const { stats = []
|
|
15
|
+
const { stats = [] } = Astro.props;
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
<section {...
|
|
18
|
+
<section {...aciRootAttrs(Astro.props)} class="theme-dark border-y border-[var(--color-edge-space)] bg-[var(--color-space-surface)] py-[var(--spacing-section-y)]">
|
|
19
19
|
<Container>
|
|
20
20
|
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
|
21
21
|
{stats.map((stat) => (
|