@gradial/aci 0.1.30 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +4 -4
  2. package/bin/aci-darwin-arm64 +0 -0
  3. package/bin/aci.js +33 -32
  4. package/dist/astro/assets.d.ts +46 -0
  5. package/dist/astro/assets.js +51 -0
  6. package/dist/astro/index.d.ts +2 -0
  7. package/dist/astro/index.js +8 -0
  8. package/dist/astro/page.d.ts +88 -0
  9. package/dist/astro/page.js +117 -0
  10. package/dist/content/provider.d.ts +7 -0
  11. package/dist/dev/content-watch.d.ts +1 -0
  12. package/dist/dev/content-watch.js +1 -1
  13. package/dist/next/config.d.ts +6 -0
  14. package/dist/next/config.js +20 -13
  15. package/dist/next/content-watch.d.ts +1 -0
  16. package/dist/next/content-watch.js +2 -2
  17. package/dist/next/index.d.ts +1 -0
  18. package/dist/next/index.js +1 -0
  19. package/dist/next/middleware.d.ts +3 -2
  20. package/dist/next/middleware.js +40 -7
  21. package/dist/next/page.d.ts +13 -5
  22. package/dist/next/page.js +42 -17
  23. package/dist/next/server.d.ts +1 -1
  24. package/dist/next/server.js +33 -9
  25. package/dist/providers/s3.d.ts +7 -0
  26. package/dist/providers/s3.js +20 -0
  27. package/dist/testing/conformance/checks.d.ts +12 -0
  28. package/dist/testing/conformance/checks.js +483 -0
  29. package/dist/testing/conformance/index.d.ts +3 -0
  30. package/dist/testing/conformance/index.js +2 -0
  31. package/dist/testing/conformance/preset.d.ts +14 -0
  32. package/dist/testing/conformance/preset.js +98 -0
  33. package/dist/testing/conformance/types.d.ts +45 -0
  34. package/dist/testing/conformance/types.js +7 -0
  35. package/package.json +17 -9
  36. package/src/cli/materialize-template.mjs +133 -0
  37. package/src/cli/validate-content.mjs +0 -0
  38. package/src/cli/verify-next-cdn-routes.mjs +10 -19
  39. package/templates/astro/starter-content/src/cms/contracts/components/button.contract.ts +19 -0
  40. package/templates/astro/starter-content/src/cms/contracts/components/container.contract.ts +19 -0
  41. package/templates/astro/starter-content/src/cms/contracts/components/featureCard.contract.ts +15 -0
  42. package/templates/astro/starter-content/src/cms/contracts/components/featureGrid.contract.ts +16 -0
  43. package/templates/astro/starter-content/src/cms/contracts/components/homeHero.contract.ts +36 -0
  44. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +21 -0
  45. package/templates/astro/starter-content/src/cms/contracts/components/statsBar.contract.ts +19 -0
  46. package/templates/astro/starter-content/src/cms/registry.ts +28 -0
  47. package/templates/astro/template/{.aci.yaml → .aci.yaml.tmpl} +1 -1
  48. package/templates/astro/template/README.md.tmpl +53 -0
  49. package/templates/astro/template/gitignore +12 -0
  50. package/templates/astro/template/package.json.tmpl +5 -3
  51. package/templates/astro/template/src/cms/contracts/components/index.ts +3 -0
  52. package/templates/astro/template/src/cms/registry.ts +3 -26
  53. package/templates/astro/template/src/pages/[...slug].astro +13 -35
  54. package/templates/astro/template/tests/conformance.test.ts.tmpl +12 -0
  55. package/templates/common/.agents/AGENTS.md +3 -5
  56. package/templates/common/.agents/skills/aci/SKILL.md +72 -23
  57. package/templates/common/.agents/skills/authoring/SKILL.md +53 -14
  58. package/templates/common/.agents/skills/authoring/references/block-registry.md +109 -15
  59. package/templates/common/.agents/skills/authoring/references/contracts.md +23 -7
  60. package/templates/common/.agents/skills/authoring/references/repo-structure.md +35 -19
  61. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +75 -17
  62. package/templates/common/.agents/skills/migration/SKILL.md +7 -1
  63. package/templates/common/.agents/skills/migration/references/canon-migration.md +471 -0
  64. package/templates/common/.content/config/site.json.tmpl +15 -0
  65. package/templates/common/.content/pages/home/_index.json +4 -103
  66. package/templates/common/.nvmrc +1 -0
  67. package/templates/common/README.md.tmpl +32 -11
  68. package/templates/common/contracts/components/index.ts +1 -19
  69. package/templates/common/design-system/tokens/colors.css +34 -10
  70. package/templates/common/starter-content/.content/config/robots.txt +2 -0
  71. package/templates/common/starter-content/.content/pages/home/_index.json +112 -0
  72. package/templates/common/starter-content/.content/schemas/site.schema.json +138 -0
  73. package/templates/nextjs/starter-content/src/cms/contracts/components/container.contract.ts +19 -0
  74. package/templates/nextjs/starter-content/src/cms/contracts/components/featureCard.contract.ts +15 -0
  75. package/templates/nextjs/starter-content/src/cms/contracts/components/featureGrid.contract.ts +16 -0
  76. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +21 -0
  77. package/templates/nextjs/starter-content/src/cms/contracts/components/siteFooter.contract.ts +45 -0
  78. package/templates/nextjs/starter-content/src/cms/contracts/components/siteNavigation.contract.ts +30 -0
  79. package/templates/nextjs/starter-content/src/cms/contracts/components/statsBar.contract.ts +19 -0
  80. package/templates/nextjs/starter-content/src/cms/registry.ts +28 -0
  81. package/templates/nextjs/template/{.aci.yaml → .aci.yaml.tmpl} +1 -1
  82. package/templates/nextjs/template/README.md.tmpl +52 -0
  83. package/templates/nextjs/template/gitignore +15 -0
  84. package/templates/nextjs/template/next.config.mjs +1 -6
  85. package/templates/nextjs/template/package.json.tmpl +5 -5
  86. package/templates/nextjs/template/src/app/gradial/assets/[...path]/route.ts +1 -4
  87. package/templates/nextjs/template/src/app/layout.tsx +1 -3
  88. package/templates/nextjs/template/src/cms/contracts/components/index.ts +1 -13
  89. package/templates/nextjs/template/src/cms/registry.ts +3 -26
  90. package/templates/nextjs/template/src/design-system/styles.css +346 -0
  91. package/templates/nextjs/template/src/design-system/tokens/colors.css +110 -0
  92. package/templates/nextjs/template/src/design-system/tokens/components.css +86 -0
  93. package/templates/nextjs/template/src/design-system/tokens/index.css +6 -0
  94. package/templates/nextjs/template/src/design-system/tokens/radii.css +5 -0
  95. package/templates/nextjs/template/src/design-system/tokens/shadows.css +9 -0
  96. package/templates/nextjs/template/src/design-system/tokens/spacing.css +26 -0
  97. package/templates/nextjs/template/src/design-system/tokens/typography.css +32 -0
  98. package/templates/nextjs/template/src/middleware.ts.tmpl +12 -0
  99. package/templates/nextjs/template/tests/conformance.test.ts.tmpl +12 -0
  100. package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +0 -132
  101. package/templates/nextjs/template/src/cms/contracts/components/container.contract.ts +0 -20
  102. package/templates/nextjs/template/src/middleware.ts +0 -7
  103. /package/templates/{nextjs/template → astro/starter-content}/src/cms/contracts/components/siteFooter.contract.ts +0 -0
  104. /package/templates/{nextjs/template → astro/starter-content}/src/cms/contracts/components/siteNavigation.contract.ts +0 -0
  105. /package/templates/astro/{template → starter-content}/src/components/chrome/Footer.astro +0 -0
  106. /package/templates/astro/{template → starter-content}/src/components/chrome/Header.astro +0 -0
  107. /package/templates/astro/{template → starter-content}/src/components/elements/AtomicBlock.astro +0 -0
  108. /package/templates/astro/{template → starter-content}/src/components/elements/AtomicBlocks.astro +0 -0
  109. /package/templates/astro/{template → starter-content}/src/components/elements/Button.astro +0 -0
  110. /package/templates/astro/{template → starter-content}/src/components/elements/ButtonBlock.astro +0 -0
  111. /package/templates/astro/{template → starter-content}/src/components/elements/Container.astro +0 -0
  112. /package/templates/astro/{template → starter-content}/src/components/elements/Icon.astro +0 -0
  113. /package/templates/astro/{template → starter-content}/src/components/elements/Logo.astro +0 -0
  114. /package/templates/astro/{template → starter-content}/src/components/elements/RemoteImage.astro +0 -0
  115. /package/templates/astro/{template → starter-content}/src/components/sections/ContainerSection.astro +0 -0
  116. /package/templates/astro/{template → starter-content}/src/components/sections/FeatureCard.astro +0 -0
  117. /package/templates/astro/{template → starter-content}/src/components/sections/FeatureGrid.astro +0 -0
  118. /package/templates/astro/{template → starter-content}/src/components/sections/HomeHero.astro +0 -0
  119. /package/templates/astro/{template → starter-content}/src/components/sections/StatsBar.astro +0 -0
  120. /package/templates/astro/{template → starter-content}/src/fixtures/atomicBlocks.ts +0 -0
  121. /package/templates/astro/{template → starter-content}/src/fixtures/composition.ts +0 -0
  122. /package/templates/astro/{template → starter-content}/src/fixtures/index.ts +0 -0
  123. /package/templates/astro/{template → starter-content}/src/fixtures/sections.ts +0 -0
  124. /package/templates/astro/{template → starter-content}/src/render/RenderPage.astro +0 -0
  125. /package/templates/astro/{template → starter-content}/src/stories/CompositionStoryFrame.astro +0 -0
  126. /package/templates/astro/{template → starter-content}/src/stories/HomeHero.stories.ts +0 -0
  127. /package/templates/astro/{template → starter-content}/src/stories/SectionStoryFrame.astro +0 -0
  128. /package/templates/astro/{template → starter-content}/src/stories/StoryDocFrame.astro +0 -0
  129. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  130. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  131. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  132. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  133. /package/templates/common/{.content → starter-content/.content}/config/site.json +0 -0
  134. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/asset.json +0 -0
  135. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/editorial.json +0 -0
  136. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/governance.json +0 -0
  137. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -0
  138. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/asset.json +0 -0
  139. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/editorial.json +0 -0
  140. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/governance.json +0 -0
  141. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -0
  142. /package/templates/common/{.content → starter-content/.content}/fragments/navbar/_index.json +0 -0
  143. /package/templates/common/{.content → starter-content/.content}/fragments/site-footer/_index.json +0 -0
  144. /package/templates/common/{.content → starter-content/.content}/pages/about/_index.json +0 -0
  145. /package/templates/common/{public → starter-content/public}/assets/Dwire-model.png +0 -0
  146. /package/templates/common/{public → starter-content/public}/assets/seattle.avif +0 -0
  147. /package/templates/nextjs/{template → starter-content}/src/cms/contracts/components/button.contract.ts +0 -0
  148. /package/templates/nextjs/{template → starter-content}/src/cms/contracts/components/homeHero.contract.ts +0 -0
  149. /package/templates/nextjs/{template → starter-content}/src/components/chrome/Footer.tsx +0 -0
  150. /package/templates/nextjs/{template → starter-content}/src/components/chrome/Header.tsx +0 -0
  151. /package/templates/nextjs/{template → starter-content}/src/components/elements/AtomicBlock.tsx +0 -0
  152. /package/templates/nextjs/{template → starter-content}/src/components/elements/Button.tsx +0 -0
  153. /package/templates/nextjs/{template → starter-content}/src/components/elements/ButtonBlock.tsx +0 -0
  154. /package/templates/nextjs/{template → starter-content}/src/components/elements/Container.tsx +0 -0
  155. /package/templates/nextjs/{template → starter-content}/src/components/elements/Icon.tsx +0 -0
  156. /package/templates/nextjs/{template → starter-content}/src/components/elements/Logo.tsx +0 -0
  157. /package/templates/nextjs/{template → starter-content}/src/components/sections/ContainerSection.tsx +0 -0
  158. /package/templates/nextjs/{template → starter-content}/src/components/sections/FeatureCard.tsx +0 -0
  159. /package/templates/nextjs/{template → starter-content}/src/components/sections/FeatureGrid.tsx +0 -0
  160. /package/templates/nextjs/{template → starter-content}/src/components/sections/HomeHero.tsx +0 -0
  161. /package/templates/nextjs/{template → starter-content}/src/components/sections/StatsBar.tsx +0 -0
  162. /package/templates/nextjs/{template → starter-content}/src/stories/Button.stories.tsx +0 -0
  163. /package/templates/nextjs/{template → starter-content}/src/stories/Header.stories.tsx +0 -0
  164. /package/templates/nextjs/{template → starter-content}/src/stories/HomeHero.stories.tsx +0 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Framework-agnostic conformance checks for ACI site projects.
3
+ *
4
+ * Each check function receives a ConformanceContext and returns an array of
5
+ * ConformanceCheckResult items. Checks are designed to be deterministic,
6
+ * file-system-only inspections (no network, no process spawning for the
7
+ * fast checks; build/compile checks shell out).
8
+ */
9
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
+ import { join, relative } from 'node:path';
11
+ import { execSync } from 'node:child_process';
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ function ok(name, label, detail) {
16
+ return { name, label, ok: true, detail };
17
+ }
18
+ function fail(name, label, detail) {
19
+ return { name, label, ok: false, detail };
20
+ }
21
+ function readTextFile(path) {
22
+ try {
23
+ return readFileSync(path, 'utf-8');
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function readJSON(path) {
30
+ const text = readTextFile(path);
31
+ if (text === null)
32
+ return null;
33
+ try {
34
+ return JSON.parse(text);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function findFirstExisting(root, candidates) {
41
+ for (const rel of candidates) {
42
+ const full = join(root, rel);
43
+ const content = readTextFile(full);
44
+ if (content !== null) {
45
+ return { path: full, content };
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ /**
51
+ * Walk a directory recursively and return all file paths matching a predicate.
52
+ * Skips node_modules, .next, .aci, dist, and hidden directories.
53
+ */
54
+ function walkFiles(dir, predicate) {
55
+ const results = [];
56
+ const skipDirs = new Set(['node_modules', '.next', '.aci', 'dist', '.git', '.svn']);
57
+ function walk(current) {
58
+ let names;
59
+ try {
60
+ names = readdirSync(current);
61
+ }
62
+ catch {
63
+ return;
64
+ }
65
+ for (const name of names) {
66
+ const full = join(current, name);
67
+ let stat = null;
68
+ try {
69
+ stat = statSync(full);
70
+ }
71
+ catch {
72
+ continue;
73
+ }
74
+ if (stat.isDirectory()) {
75
+ if (!skipDirs.has(name) && !name.startsWith('.')) {
76
+ walk(full);
77
+ }
78
+ }
79
+ else if (predicate(full)) {
80
+ results.push(full);
81
+ }
82
+ }
83
+ }
84
+ walk(dir);
85
+ return results;
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Check: config — withAci present in framework config
89
+ // ---------------------------------------------------------------------------
90
+ export function checkConfig(ctx) {
91
+ const results = [];
92
+ if (ctx.framework === 'next') {
93
+ const configFile = findFirstExisting(ctx.rootDir, [
94
+ 'next.config.ts',
95
+ 'next.config.mts',
96
+ 'next.config.js',
97
+ 'next.config.mjs',
98
+ 'next.config.cjs',
99
+ ]);
100
+ if (!configFile) {
101
+ results.push(fail('config', 'next config file exists'));
102
+ return results;
103
+ }
104
+ results.push(ok('config', 'next config file exists', relative(ctx.rootDir, configFile.path)));
105
+ const hasWithAci = configFile.content.includes('withAci');
106
+ results.push(hasWithAci
107
+ ? ok('config', 'next config uses withAci', relative(ctx.rootDir, configFile.path))
108
+ : fail('config', 'next config uses withAci', 'withAci() wrapper not found in ' + relative(ctx.rootDir, configFile.path)));
109
+ const hasStaticExport = /output\s*:\s*['"]export['"]/.test(configFile.content.replace(/\/\/.*/g, ''));
110
+ results.push(!hasStaticExport
111
+ ? ok('config', 'next config does not use static export')
112
+ : fail('config', 'next config does not use static export', 'output: "export" found'));
113
+ }
114
+ if (ctx.framework === 'astro') {
115
+ const configFile = findFirstExisting(ctx.rootDir, [
116
+ 'astro.config.ts',
117
+ 'astro.config.mts',
118
+ 'astro.config.js',
119
+ 'astro.config.mjs',
120
+ ]);
121
+ if (!configFile) {
122
+ results.push(fail('config', 'astro config file exists'));
123
+ return results;
124
+ }
125
+ results.push(ok('config', 'astro config file exists', relative(ctx.rootDir, configFile.path)));
126
+ const hasWithAci = configFile.content.includes('withAci');
127
+ results.push(hasWithAci
128
+ ? ok('config', 'astro config uses withAci', relative(ctx.rootDir, configFile.path))
129
+ : fail('config', 'astro config uses withAci', 'withAci() not found in ' + relative(ctx.rootDir, configFile.path)));
130
+ }
131
+ return results;
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Check: registry — createRegistry used with typed contracts
135
+ // ---------------------------------------------------------------------------
136
+ export function checkRegistry(ctx) {
137
+ const results = [];
138
+ // Find registry file(s) — look for files that import createRegistry
139
+ const sourceFiles = walkFiles(ctx.rootDir, (p) => /\.(ts|tsx|js|jsx)$/.test(p));
140
+ const registryFiles = sourceFiles.filter((f) => {
141
+ const content = readTextFile(f);
142
+ return content !== null && content.includes('createRegistry');
143
+ });
144
+ if (registryFiles.length === 0) {
145
+ results.push(fail('registry', 'createRegistry usage found', 'no file imports createRegistry from @gradial/aci'));
146
+ return results;
147
+ }
148
+ for (const file of registryFiles) {
149
+ const content = readTextFile(file);
150
+ const rel = relative(ctx.rootDir, file);
151
+ results.push(ok('registry', 'createRegistry usage found', rel));
152
+ // Check that the registry imports from @gradial/aci (not a local copy)
153
+ const importsFromSDK = content.includes("from '@gradial/aci'") ||
154
+ content.includes('from "@gradial/aci"') ||
155
+ content.includes("from '@gradial/aci/") ||
156
+ content.includes('from "@gradial/aci/');
157
+ results.push(importsFromSDK
158
+ ? ok('registry', 'registry imports from @gradial/aci', rel)
159
+ : fail('registry', 'registry imports from @gradial/aci', rel + ' should import createRegistry from @gradial/aci'));
160
+ // Check that contracts are used (tuple pattern [contract, Component])
161
+ const hasTuplePattern = /createRegistry\s*\(\s*\[/.test(content);
162
+ results.push(hasTuplePattern
163
+ ? ok('registry', 'registry uses typed contract tuples', rel)
164
+ : fail('registry', 'registry uses typed contract tuples', rel + ' should pass [contract, Component] tuples to createRegistry'));
165
+ }
166
+ return results;
167
+ }
168
+ // ---------------------------------------------------------------------------
169
+ // Check: pageEntry — catch-all page correctness
170
+ // ---------------------------------------------------------------------------
171
+ export function checkPageEntry(ctx) {
172
+ const results = [];
173
+ if (ctx.framework !== 'next') {
174
+ // Astro pages have a different pattern; skip for now
175
+ return results;
176
+ }
177
+ // Find the app root (src/app or app)
178
+ const appRoot = existsSync(join(ctx.rootDir, 'src', 'app'))
179
+ ? join(ctx.rootDir, 'src', 'app')
180
+ : join(ctx.rootDir, 'app');
181
+ const pageFile = findFirstExisting(appRoot, [
182
+ '[[...slug]]/page.tsx',
183
+ '[[...slug]]/page.ts',
184
+ '[[...slug]]/page.jsx',
185
+ '[[...slug]]/page.js',
186
+ '[...slug]/page.tsx',
187
+ '[...slug]/page.ts',
188
+ '[...slug]/page.jsx',
189
+ '[...slug]/page.js',
190
+ ]);
191
+ if (!pageFile) {
192
+ results.push(fail('pageEntry', 'next CMS catch-all page exists', 'expected [[...slug]]/page.tsx or [...slug]/page.tsx'));
193
+ return results;
194
+ }
195
+ const rel = relative(ctx.rootDir, pageFile.path);
196
+ results.push(ok('pageEntry', 'next CMS catch-all page exists', rel));
197
+ const usesSDK = pageFile.content.includes("@gradial/aci/next/server") ||
198
+ pageFile.content.includes("@gradial/aci/next'") ||
199
+ pageFile.content.includes('@gradial/aci/next"');
200
+ results.push(usesSDK
201
+ ? ok('pageEntry', 'page uses @gradial/aci SDK', rel)
202
+ : fail('pageEntry', 'page uses @gradial/aci SDK', rel + ' should import from @gradial/aci/next'));
203
+ const usesCreatePage = pageFile.content.includes('createPage') ||
204
+ pageFile.content.includes('routeFromNextParams');
205
+ results.push(usesCreatePage
206
+ ? ok('pageEntry', 'page uses createPage or routeFromNextParams', rel)
207
+ : fail('pageEntry', 'page uses createPage or routeFromNextParams', rel));
208
+ const normalized = pageFile.content.replace(/\s+/g, ' ');
209
+ const hasForceDynamic = normalized.includes("dynamic = 'force-dynamic'") ||
210
+ normalized.includes('dynamic = "force-dynamic"');
211
+ results.push(hasForceDynamic
212
+ ? ok('pageEntry', 'page exports force-dynamic', rel)
213
+ : fail('pageEntry', 'page exports force-dynamic', rel + ' should export const dynamic = "force-dynamic"'));
214
+ const hasGenerateStaticParams = pageFile.content.includes('generateStaticParams');
215
+ results.push(!hasGenerateStaticParams
216
+ ? ok('pageEntry', 'page does not use generateStaticParams', rel)
217
+ : fail('pageEntry', 'page does not use generateStaticParams', rel + ' should not export generateStaticParams'));
218
+ return results;
219
+ }
220
+ // ---------------------------------------------------------------------------
221
+ // Check: middleware — presence and siteId (Next)
222
+ // ---------------------------------------------------------------------------
223
+ export function checkMiddleware(ctx) {
224
+ const results = [];
225
+ if (ctx.framework !== 'next') {
226
+ return results;
227
+ }
228
+ const middlewareFile = findFirstExisting(ctx.rootDir, [
229
+ 'src/middleware.ts',
230
+ 'src/middleware.js',
231
+ 'middleware.ts',
232
+ 'middleware.js',
233
+ ]);
234
+ if (!middlewareFile) {
235
+ results.push(fail('middleware', 'next middleware.ts exists', 'expected src/middleware.ts or middleware.ts'));
236
+ return results;
237
+ }
238
+ const rel = relative(ctx.rootDir, middlewareFile.path);
239
+ results.push(ok('middleware', 'next middleware.ts exists', rel));
240
+ const usesSDKMiddleware = middlewareFile.content.includes('createGradialMiddleware') ||
241
+ middlewareFile.content.includes('@gradial/aci/next/middleware');
242
+ results.push(usesSDKMiddleware
243
+ ? ok('middleware', 'middleware uses createGradialMiddleware', rel)
244
+ : fail('middleware', 'middleware uses createGradialMiddleware', rel + ' should use createGradialMiddleware from @gradial/aci/next/middleware'));
245
+ // Check if siteId is configured as a literal (no env var indirection)
246
+ if (ctx.siteId) {
247
+ const hasSiteIdLiteral = middlewareFile.content.includes(`siteId`) &&
248
+ !middlewareFile.content.includes('process.env');
249
+ results.push(hasSiteIdLiteral
250
+ ? ok('middleware', 'middleware configures siteId (literal)', rel)
251
+ : fail('middleware', 'middleware configures siteId (literal)', rel + ' should pass a literal siteId string to createGradialMiddleware (no env var indirection)'));
252
+ }
253
+ // Check that the middleware matcher is inlined (not imported) and matches
254
+ // the SDK's canonical value. Next.js requires config.matcher to be
255
+ // statically analyzable, so importing the matcher from the SDK breaks the
256
+ // build. Instead the template inlines the value and this check detects drift.
257
+ const canonicalMatcher = "['/((?!_next(?:/|$)|api(?:/|$)|favicon\\\\.ico$|\\\\.gradial-dam(?:/|$)).*)']";
258
+ // Check for actual import/usage of aciMiddlewareMatcher (not just a mention
259
+ // in a comment). Look for `matcher: aciMiddlewareMatcher` or
260
+ // `import { ... aciMiddlewareMatcher ... }`.
261
+ const hasImportedMatcher = /matcher\s*:\s*aciMiddlewareMatcher/.test(middlewareFile.content) ||
262
+ /import\s+.*aciMiddlewareMatcher.*from/.test(middlewareFile.content);
263
+ if (hasImportedMatcher) {
264
+ results.push(fail('middleware', 'middleware matcher is inlined (not imported)', rel + ' should inline the matcher value for Next.js static analysis; see @gradial/aci/next/middleware for the canonical value'));
265
+ }
266
+ else {
267
+ // Verify the inlined value matches the SDK canonical
268
+ const matcherMatch = middlewareFile.content.match(/matcher\s*:\s*(\[.*?\])/s);
269
+ if (matcherMatch) {
270
+ const normalizedInlined = matcherMatch[1].replace(/\s+/g, '');
271
+ const normalizedCanonical = canonicalMatcher.replace(/\s+/g, '');
272
+ results.push(normalizedInlined === normalizedCanonical
273
+ ? ok('middleware', 'middleware matcher matches SDK canonical', rel)
274
+ : fail('middleware', 'middleware matcher matches SDK canonical', `${rel}: inlined matcher does not match SDK's aciMiddlewareMatcher; expected ${canonicalMatcher}`));
275
+ }
276
+ else {
277
+ results.push(fail('middleware', 'middleware matcher is defined', rel + ' should define config.matcher'));
278
+ }
279
+ }
280
+ return results;
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // Check: noFileContentProvider — no direct FileContentProvider imports in app code
284
+ // ---------------------------------------------------------------------------
285
+ export function checkNoFileContentProvider(ctx) {
286
+ const results = [];
287
+ // Scan source files for FileContentProvider imports (which should only be
288
+ // used internally by the SDK, not by customer app code)
289
+ const sourceFiles = walkFiles(ctx.rootDir, (p) => /\.(ts|tsx|js|jsx)$/.test(p));
290
+ const violations = [];
291
+ for (const file of sourceFiles) {
292
+ const content = readTextFile(file);
293
+ if (content === null)
294
+ continue;
295
+ // Skip SDK source itself and test files
296
+ if (file.includes('node_modules'))
297
+ continue;
298
+ if (file.endsWith('.test.ts') || file.endsWith('.test.tsx'))
299
+ continue;
300
+ if (file.endsWith('.test.js') || file.endsWith('.test.jsx'))
301
+ continue;
302
+ if (content.includes('FileContentProvider')) {
303
+ violations.push(relative(ctx.rootDir, file));
304
+ }
305
+ }
306
+ if (violations.length === 0) {
307
+ results.push(ok('noFileContentProvider', 'no FileContentProvider imports in app code'));
308
+ }
309
+ else {
310
+ results.push(fail('noFileContentProvider', 'no FileContentProvider imports in app code', 'found in: ' + violations.join(', ')));
311
+ }
312
+ return results;
313
+ }
314
+ // ---------------------------------------------------------------------------
315
+ // Check: noCustomContent — no custom content.ts that bypasses SDK
316
+ // ---------------------------------------------------------------------------
317
+ export function checkNoCustomContent(ctx) {
318
+ const results = [];
319
+ // Check for a content.ts file in the source root that manually constructs
320
+ // a content provider instead of relying on the SDK's built-in wiring.
321
+ const srcRoot = existsSync(join(ctx.rootDir, 'src'))
322
+ ? join(ctx.rootDir, 'src')
323
+ : ctx.rootDir;
324
+ const customContentFile = findFirstExisting(srcRoot, [
325
+ 'content.ts',
326
+ 'content.js',
327
+ 'lib/content.ts',
328
+ 'lib/content.js',
329
+ 'utils/content.ts',
330
+ 'utils/content.js',
331
+ ]);
332
+ if (!customContentFile) {
333
+ results.push(ok('noCustomContent', 'no custom content.ts bypassing SDK'));
334
+ return results;
335
+ }
336
+ // If the file exists, check if it manually constructs providers
337
+ const hasProviderConstruction = customContentFile.content.includes('FileContentProvider') ||
338
+ customContentFile.content.includes('S3ContentProvider') ||
339
+ customContentFile.content.includes('new ') && customContentFile.content.includes('Provider');
340
+ if (hasProviderConstruction) {
341
+ results.push(fail('noCustomContent', 'no custom content.ts bypassing SDK', 'found manual provider construction in ' + relative(ctx.rootDir, customContentFile.path)));
342
+ }
343
+ else {
344
+ results.push(ok('noCustomContent', 'no custom content.ts bypassing SDK'));
345
+ }
346
+ return results;
347
+ }
348
+ // ---------------------------------------------------------------------------
349
+ // Check: noLegacyAssetRoute — no legacy asset route pattern
350
+ // ---------------------------------------------------------------------------
351
+ export function checkNoLegacyAssetRoute(ctx) {
352
+ const results = [];
353
+ if (ctx.framework !== 'next') {
354
+ return results;
355
+ }
356
+ const appRoot = existsSync(join(ctx.rootDir, 'src', 'app'))
357
+ ? join(ctx.rootDir, 'src', 'app')
358
+ : join(ctx.rootDir, 'app');
359
+ // The legacy pattern uses [releaseId] segment; the canonical pattern uses just [...path]
360
+ const legacyRoute = findFirstExisting(appRoot, [
361
+ 'gradial/assets/[releaseId]/[...path]/route.ts',
362
+ 'gradial/assets/[releaseId]/[...path]/route.tsx',
363
+ 'gradial/assets/[releaseId]/[...path]/route.js',
364
+ 'gradial/assets/[releaseId]/[...path]/route.jsx',
365
+ ]);
366
+ if (legacyRoute) {
367
+ results.push(fail('noLegacyAssetRoute', 'no legacy asset route with [releaseId] segment', 'found legacy route at ' + relative(ctx.rootDir, legacyRoute.path) + '; use gradial/assets/[...path]/route.ts instead'));
368
+ }
369
+ else {
370
+ results.push(ok('noLegacyAssetRoute', 'no legacy asset route with [releaseId] segment'));
371
+ }
372
+ // Verify the canonical asset route exists
373
+ const canonicalRoute = findFirstExisting(appRoot, [
374
+ 'gradial/assets/[...path]/route.ts',
375
+ 'gradial/assets/[...path]/route.tsx',
376
+ 'gradial/assets/[...path]/route.js',
377
+ 'gradial/assets/[...path]/route.jsx',
378
+ ]);
379
+ if (canonicalRoute) {
380
+ results.push(ok('noLegacyAssetRoute', 'canonical asset route exists', relative(ctx.rootDir, canonicalRoute.path)));
381
+ const usesSDKHandler = canonicalRoute.content.includes('@gradial/aci/next/asset-route');
382
+ results.push(usesSDKHandler
383
+ ? ok('noLegacyAssetRoute', 'asset route uses SDK handler', relative(ctx.rootDir, canonicalRoute.path))
384
+ : fail('noLegacyAssetRoute', 'asset route uses SDK handler', relative(ctx.rootDir, canonicalRoute.path) + ' should import from @gradial/aci/next/asset-route'));
385
+ }
386
+ else {
387
+ results.push(fail('noLegacyAssetRoute', 'canonical asset route exists', 'expected gradial/assets/[...path]/route.ts'));
388
+ }
389
+ return results;
390
+ }
391
+ // ---------------------------------------------------------------------------
392
+ // Check: buildSucceeds — npm build exits cleanly
393
+ // ---------------------------------------------------------------------------
394
+ export function checkBuildSucceeds(ctx) {
395
+ const results = [];
396
+ try {
397
+ execSync('npm run build', {
398
+ cwd: ctx.rootDir,
399
+ stdio: 'pipe',
400
+ timeout: 300_000, // 5 min
401
+ env: { ...process.env, NODE_ENV: 'production' },
402
+ });
403
+ results.push(ok('buildSucceeds', 'npm run build succeeds'));
404
+ }
405
+ catch (err) {
406
+ const stderr = err instanceof Error && 'stderr' in err
407
+ ? String(err.stderr).slice(0, 500)
408
+ : 'unknown error';
409
+ results.push(fail('buildSucceeds', 'npm run build succeeds', stderr));
410
+ }
411
+ return results;
412
+ }
413
+ // ---------------------------------------------------------------------------
414
+ // Check: contentCompile — aci compile-registry succeeds
415
+ // ---------------------------------------------------------------------------
416
+ export function checkContentCompile(ctx) {
417
+ const results = [];
418
+ // Look for the compile-registry script in the SDK
419
+ const compileScript = findFirstExisting(ctx.rootDir, [
420
+ 'node_modules/@gradial/aci/src/cli/compile-registry.mjs',
421
+ 'node_modules/@gradial/aci/dist/cli/compile-registry.mjs',
422
+ ]);
423
+ if (!compileScript) {
424
+ results.push(fail('contentCompile', 'content compile script found', '@gradial/aci compile-registry not found in node_modules'));
425
+ return results;
426
+ }
427
+ try {
428
+ execSync('npx aci compile', {
429
+ cwd: ctx.rootDir,
430
+ stdio: 'pipe',
431
+ timeout: 60_000,
432
+ });
433
+ results.push(ok('contentCompile', 'content compilation succeeds'));
434
+ }
435
+ catch (err) {
436
+ const stderr = err instanceof Error && 'stderr' in err
437
+ ? String(err.stderr).slice(0, 500)
438
+ : 'unknown error';
439
+ results.push(fail('contentCompile', 'content compilation succeeds', stderr));
440
+ }
441
+ return results;
442
+ }
443
+ // ---------------------------------------------------------------------------
444
+ // Check: sdkVersion — SDK version is current
445
+ // ---------------------------------------------------------------------------
446
+ export function checkSdkVersion(ctx) {
447
+ const results = [];
448
+ // Read the installed SDK version
449
+ const installedPkg = readJSON(join(ctx.rootDir, 'node_modules', '@gradial', 'aci', 'package.json'));
450
+ if (!installedPkg || !installedPkg.version) {
451
+ results.push(fail('sdkVersion', '@gradial/aci is installed', 'package not found in node_modules'));
452
+ return results;
453
+ }
454
+ results.push(ok('sdkVersion', '@gradial/aci is installed', `v${installedPkg.version}`));
455
+ // Read the declared version in site's package.json
456
+ const sitePkg = readJSON(join(ctx.rootDir, 'package.json'));
457
+ if (sitePkg) {
458
+ const declaredVersion = sitePkg.dependencies?.['@gradial/aci'] ??
459
+ sitePkg.devDependencies?.['@gradial/aci'];
460
+ if (declaredVersion) {
461
+ results.push(ok('sdkVersion', '@gradial/aci declared in package.json', declaredVersion));
462
+ }
463
+ else {
464
+ results.push(fail('sdkVersion', '@gradial/aci declared in package.json', 'missing from dependencies and devDependencies'));
465
+ }
466
+ }
467
+ return results;
468
+ }
469
+ // ---------------------------------------------------------------------------
470
+ // Check registry: maps check names to functions
471
+ // ---------------------------------------------------------------------------
472
+ export const CHECK_REGISTRY = {
473
+ config: checkConfig,
474
+ registry: checkRegistry,
475
+ pageEntry: checkPageEntry,
476
+ middleware: checkMiddleware,
477
+ noFileContentProvider: checkNoFileContentProvider,
478
+ noCustomContent: checkNoCustomContent,
479
+ noLegacyAssetRoute: checkNoLegacyAssetRoute,
480
+ buildSucceeds: checkBuildSucceeds,
481
+ contentCompile: checkContentCompile,
482
+ sdkVersion: checkSdkVersion,
483
+ };
@@ -0,0 +1,3 @@
1
+ export { conformancePreset, runConformanceChecks } from './preset.js';
2
+ export { checkConfig, checkRegistry, checkPageEntry, checkMiddleware, checkNoFileContentProvider, checkNoCustomContent, checkNoLegacyAssetRoute, checkBuildSucceeds, checkContentCompile, checkSdkVersion, CHECK_REGISTRY, } from './checks.js';
3
+ export type { ConformanceFramework, ConformanceCheckName, ConformancePresetOptions, ConformanceCheckResult, ConformanceResult, ConformanceContext, ConformanceCheckFn, } from './types.js';
@@ -0,0 +1,2 @@
1
+ export { conformancePreset, runConformanceChecks } from './preset.js';
2
+ export { checkConfig, checkRegistry, checkPageEntry, checkMiddleware, checkNoFileContentProvider, checkNoCustomContent, checkNoLegacyAssetRoute, checkBuildSucceeds, checkContentCompile, checkSdkVersion, CHECK_REGISTRY, } from './checks.js';
@@ -0,0 +1,14 @@
1
+ import type { ConformancePresetOptions, ConformanceResult } from './types.js';
2
+ /**
3
+ * Run all resolved checks and return an aggregate result.
4
+ * This is the programmatic API — call it directly if you don't want the
5
+ * node:test integration.
6
+ */
7
+ export declare function runConformanceChecks(options: ConformancePresetOptions): ConformanceResult;
8
+ /**
9
+ * Register a conformance test suite using node:test.
10
+ *
11
+ * Each check runs as a subtest within a "ACI conformance" suite.
12
+ * Failures are reported as test assertion failures.
13
+ */
14
+ export declare function conformancePreset(options: ConformancePresetOptions): void;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Conformance preset for @gradial/aci sites.
3
+ *
4
+ * Usage in a customer repo's test suite:
5
+ *
6
+ * ```ts
7
+ * import test from 'node:test';
8
+ * import { conformancePreset } from '@gradial/aci/testing';
9
+ *
10
+ * conformancePreset({
11
+ * framework: 'next',
12
+ * siteId: 'site_mysite',
13
+ * });
14
+ * ```
15
+ *
16
+ * This registers a test suite using node:test that verifies the site's ACI
17
+ * integration conforms to the canonical patterns.
18
+ */
19
+ import test from 'node:test';
20
+ import assert from 'node:assert/strict';
21
+ import { CHECK_REGISTRY } from './checks.js';
22
+ /** All check names in default execution order. */
23
+ const ALL_CHECKS = [
24
+ 'config',
25
+ 'registry',
26
+ 'pageEntry',
27
+ 'middleware',
28
+ 'noFileContentProvider',
29
+ 'noCustomContent',
30
+ 'noLegacyAssetRoute',
31
+ 'sdkVersion',
32
+ // Expensive checks last — these shell out
33
+ 'contentCompile',
34
+ 'buildSucceeds',
35
+ ];
36
+ /** Default checks exclude the expensive build/compile checks. */
37
+ const DEFAULT_CHECKS = ALL_CHECKS.filter((c) => c !== 'buildSucceeds' && c !== 'contentCompile');
38
+ function resolveChecks(input) {
39
+ if (!input)
40
+ return DEFAULT_CHECKS;
41
+ if (Array.isArray(input)) {
42
+ return input.filter((c) => c in CHECK_REGISTRY);
43
+ }
44
+ // Object form: start with defaults, apply overrides
45
+ return ALL_CHECKS.filter((name) => {
46
+ if (name in input)
47
+ return input[name];
48
+ return DEFAULT_CHECKS.includes(name);
49
+ });
50
+ }
51
+ /**
52
+ * Run all resolved checks and return an aggregate result.
53
+ * This is the programmatic API — call it directly if you don't want the
54
+ * node:test integration.
55
+ */
56
+ export function runConformanceChecks(options) {
57
+ const ctx = {
58
+ framework: options.framework,
59
+ siteId: options.siteId,
60
+ rootDir: options.rootDir || process.cwd(),
61
+ };
62
+ const checks = resolveChecks(options.checks);
63
+ const allResults = checks.flatMap((name) => {
64
+ const checkFn = CHECK_REGISTRY[name];
65
+ return checkFn(ctx);
66
+ });
67
+ const failures = allResults.filter((r) => !r.ok);
68
+ return {
69
+ passed: failures.length === 0,
70
+ checks: allResults,
71
+ failures,
72
+ };
73
+ }
74
+ /**
75
+ * Register a conformance test suite using node:test.
76
+ *
77
+ * Each check runs as a subtest within a "ACI conformance" suite.
78
+ * Failures are reported as test assertion failures.
79
+ */
80
+ export function conformancePreset(options) {
81
+ const checks = resolveChecks(options.checks);
82
+ test('ACI conformance', async (t) => {
83
+ const ctx = {
84
+ framework: options.framework,
85
+ siteId: options.siteId,
86
+ rootDir: options.rootDir || process.cwd(),
87
+ };
88
+ for (const checkName of checks) {
89
+ await t.test(checkName, () => {
90
+ const checkFn = CHECK_REGISTRY[checkName];
91
+ const results = checkFn(ctx);
92
+ for (const result of results) {
93
+ assert.ok(result.ok, `${result.label}${result.detail ? ` (${result.detail})` : ''}`);
94
+ }
95
+ });
96
+ }
97
+ });
98
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Conformance preset types for @gradial/aci/testing.
3
+ *
4
+ * These types define the contract for conformance checks that customer repos
5
+ * run in their own CI to verify their ACI integration stays canonical.
6
+ */
7
+ /** Supported framework identifiers. */
8
+ export type ConformanceFramework = 'next' | 'astro';
9
+ /** Individual check names that can be enabled/disabled. */
10
+ export type ConformanceCheckName = 'config' | 'registry' | 'pageEntry' | 'middleware' | 'noFileContentProvider' | 'noCustomContent' | 'noLegacyAssetRoute' | 'buildSucceeds' | 'contentCompile' | 'sdkVersion';
11
+ /** Options for the conformance preset. */
12
+ export interface ConformancePresetOptions {
13
+ /** Framework the site uses. */
14
+ framework: ConformanceFramework;
15
+ /** Expected siteId value (verified in config and middleware). */
16
+ siteId: string;
17
+ /** Root directory of the site project. Defaults to process.cwd(). */
18
+ rootDir?: string;
19
+ /**
20
+ * Which checks to run. Defaults to all checks.
21
+ * Pass an array to run only those checks, or an object to enable/disable individually.
22
+ */
23
+ checks?: ConformanceCheckName[] | Partial<Record<ConformanceCheckName, boolean>>;
24
+ }
25
+ /** Result of a single conformance check. */
26
+ export interface ConformanceCheckResult {
27
+ name: ConformanceCheckName;
28
+ label: string;
29
+ ok: boolean;
30
+ detail?: string;
31
+ }
32
+ /** Aggregated result of all conformance checks. */
33
+ export interface ConformanceResult {
34
+ passed: boolean;
35
+ checks: ConformanceCheckResult[];
36
+ failures: ConformanceCheckResult[];
37
+ }
38
+ /** A conformance check function. */
39
+ export type ConformanceCheckFn = (ctx: ConformanceContext) => ConformanceCheckResult[];
40
+ /** Context passed to each check function. */
41
+ export interface ConformanceContext {
42
+ framework: ConformanceFramework;
43
+ siteId: string;
44
+ rootDir: string;
45
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Conformance preset types for @gradial/aci/testing.
3
+ *
4
+ * These types define the contract for conformance checks that customer repos
5
+ * run in their own CI to verify their ACI integration stays canonical.
6
+ */
7
+ export {};