@gradial/aci 0.1.34 → 0.1.36
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/bin/aci.js +5 -77
- package/dist/next/asset-route.js +11 -1
- package/dist/next/config.js +10 -0
- package/dist/next/middleware.d.ts +8 -1
- package/dist/next/middleware.js +1 -1
- package/dist/next/server.d.ts +5 -0
- package/dist/next/server.js +47 -0
- package/dist/testing/conformance/checks.js +72 -7
- package/dist/testing/conformance/preset.js +2 -0
- package/dist/testing/conformance/types.d.ts +17 -0
- package/package.json +15 -16
- package/src/cli/materialize-template.mjs +12 -10
- package/src/cli/validate-content.mjs +0 -0
package/bin/aci.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { chmodSync, existsSync } from 'node:fs';
|
|
4
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
5
4
|
import { createRequire } from 'node:module';
|
|
6
|
-
import {
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
7
|
|
|
9
8
|
const require = createRequire(import.meta.url);
|
|
@@ -31,10 +30,10 @@ const binName = process.platform === 'win32' ? 'aci.exe' : 'aci';
|
|
|
31
30
|
const shimDir = dirname(fileURLToPath(import.meta.url));
|
|
32
31
|
const repoRoot = resolve(shimDir, '../../..');
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// All commands — including `init` — are handled by the Go CLI. The shim only
|
|
34
|
+
// resolves the binary and forwards; template materialization lives in
|
|
35
|
+
// internal/transport/cli/cmd/init.go (parity-tested against the TS engine in
|
|
36
|
+
// scripts/test-starter-dev.mjs).
|
|
38
37
|
|
|
39
38
|
// ── Binary resolution ──────────────────────────────────────────────
|
|
40
39
|
//
|
|
@@ -192,74 +191,3 @@ function normalizePathArg(value) {
|
|
|
192
191
|
function isAbsolutePath(value) {
|
|
193
192
|
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
|
|
194
193
|
}
|
|
195
|
-
|
|
196
|
-
async function runInit(args) {
|
|
197
|
-
// Inside the function because runInit is invoked from module top-level,
|
|
198
|
-
// before module-scope const bindings below it would initialize.
|
|
199
|
-
const initTemplates = {
|
|
200
|
-
'next-starter': 'nextjs',
|
|
201
|
-
'astro-starter': 'astro',
|
|
202
|
-
};
|
|
203
|
-
if (args.includes('--list')) {
|
|
204
|
-
for (const name of Object.keys(initTemplates)) {
|
|
205
|
-
console.log(name);
|
|
206
|
-
}
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const target = args.find((arg) => !arg.startsWith('-'));
|
|
211
|
-
const template = optionValue(args, '--template') || 'next-starter';
|
|
212
|
-
const templateNames = Object.keys(initTemplates).join('|');
|
|
213
|
-
if (!target) {
|
|
214
|
-
throw new Error(`usage: aci init <target> --template ${templateNames}`);
|
|
215
|
-
}
|
|
216
|
-
const framework = initTemplates[template];
|
|
217
|
-
if (!framework) {
|
|
218
|
-
throw new Error(`unknown template "${template}". Available templates: ${Object.keys(initTemplates).join(', ')}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const targetDir = resolve(callerCwd, target);
|
|
222
|
-
await assertEmptyTarget(targetDir);
|
|
223
|
-
|
|
224
|
-
const packageRoot = resolve(shimDir, '..');
|
|
225
|
-
const packageJson = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf-8'));
|
|
226
|
-
const { materializeTemplate } = await import(
|
|
227
|
-
new URL('../src/cli/materialize-template.mjs', import.meta.url)
|
|
228
|
-
);
|
|
229
|
-
materializeTemplate({
|
|
230
|
-
templatesDir: join(packageRoot, 'templates'),
|
|
231
|
-
framework,
|
|
232
|
-
destDir: targetDir,
|
|
233
|
-
vars: {
|
|
234
|
-
siteName: basename(targetDir),
|
|
235
|
-
version: '0.1.0',
|
|
236
|
-
aciVersion: packageJson.version,
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
console.log(`Created ${relative(callerCwd, targetDir) || '.'} from ${template}`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function optionValue(args, name) {
|
|
243
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
244
|
-
const arg = args[index];
|
|
245
|
-
if (arg === name) return args[index + 1] || '';
|
|
246
|
-
if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
|
|
247
|
-
}
|
|
248
|
-
return '';
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function assertEmptyTarget(targetDir) {
|
|
252
|
-
try {
|
|
253
|
-
const info = await stat(targetDir);
|
|
254
|
-
if (!info.isDirectory()) {
|
|
255
|
-
throw new Error(`target exists and is not a directory: ${targetDir}`);
|
|
256
|
-
}
|
|
257
|
-
const entries = await readdir(targetDir);
|
|
258
|
-
if (entries.length > 0) {
|
|
259
|
-
throw new Error(`target directory is not empty: ${targetDir}`);
|
|
260
|
-
}
|
|
261
|
-
} catch (error) {
|
|
262
|
-
if (error?.code === 'ENOENT') return;
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
}
|
package/dist/next/asset-route.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FileContentProvider } from '../providers/file.js';
|
|
2
|
-
import { getReleaseAssetResponse, resolveReleaseId } from './server.js';
|
|
2
|
+
import { getDamAssetResponse, getReleaseAssetResponse, isDamAssetPath, resolveReleaseId, } from './server.js';
|
|
3
3
|
export const runtime = 'nodejs';
|
|
4
4
|
export const dynamic = 'force-dynamic';
|
|
5
5
|
export async function GET(_request, { params }) {
|
|
@@ -12,6 +12,16 @@ export async function GET(_request, { params }) {
|
|
|
12
12
|
if (local) {
|
|
13
13
|
return local;
|
|
14
14
|
}
|
|
15
|
+
// DAM assets are global (not release-scoped): serve them from the hosting
|
|
16
|
+
// bucket root without resolving a release.
|
|
17
|
+
if (isDamAssetPath(assetPath)) {
|
|
18
|
+
try {
|
|
19
|
+
return await getDamAssetResponse(assetPath);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return new Response('dam asset not found', { status: 404 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
15
25
|
try {
|
|
16
26
|
const releaseId = await resolveReleaseId();
|
|
17
27
|
return await getReleaseAssetResponse(releaseId, assetPath);
|
package/dist/next/config.js
CHANGED
|
@@ -45,6 +45,16 @@ export function withAci(nextConfig = {}, options = {}) {
|
|
|
45
45
|
destination: `${assetOrigin}${assetPrefix}/:path*`,
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
+
else {
|
|
49
|
+
// Without an external asset origin (the deliberate CDN override),
|
|
50
|
+
// DAM requests must be served by the app itself: on Vercel nothing
|
|
51
|
+
// else maps `/.gradial-dam/*` to the hosting bucket. Route them to
|
|
52
|
+
// the asset handler, which serves DAM keys from the bucket root.
|
|
53
|
+
rewrites.push({
|
|
54
|
+
source: `${assetPrefix}/:path*`,
|
|
55
|
+
destination: `/gradial/assets/${assetPrefix.replace(/^\/+/, '')}/:path*`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
48
58
|
// CMS document rewrites: robots.txt and sitemap.xml are release-scoped
|
|
49
59
|
// artifacts served via the asset route handler (#40). The middleware
|
|
50
60
|
// passes them through (CMS_DOCUMENT_PATHS) so these rewrites fire and
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type NextRequest } from 'next/server.js';
|
|
2
2
|
export interface GradialMiddlewareConfig {
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Site identity for this deployment. Env-injected identity is valid —
|
|
5
|
+
* the platform sets ACI_SITE_ID per deployment, so passing a bare
|
|
6
|
+
* `process.env.ACI_SITE_ID` (which types as `string | undefined`) is
|
|
7
|
+
* supported. A missing value 500s cleanly at runtime
|
|
8
|
+
* ("ACI site id is not configured").
|
|
9
|
+
*/
|
|
10
|
+
siteId: string | undefined;
|
|
4
11
|
edgeConfig?: string;
|
|
5
12
|
previewSignKey?: string;
|
|
6
13
|
deploymentId?: string;
|
package/dist/next/middleware.js
CHANGED
|
@@ -186,7 +186,7 @@ async function activeReleaseFromEdgeConfig(config) {
|
|
|
186
186
|
return clampRelease(active.releaseId, active.sequence || 0, compatibility);
|
|
187
187
|
}
|
|
188
188
|
function siteIdForConfig(config) {
|
|
189
|
-
return config.siteId;
|
|
189
|
+
return config.siteId ?? '';
|
|
190
190
|
}
|
|
191
191
|
async function getEdgeConfigJSON(edgeConfig, key) {
|
|
192
192
|
const value = await getEdgeConfigValue(edgeConfig, key);
|
package/dist/next/server.d.ts
CHANGED
|
@@ -46,4 +46,9 @@ export declare class ReleaseAssetNotFoundError extends Error {
|
|
|
46
46
|
constructor(releaseId: string, assetPath: string, cause?: unknown);
|
|
47
47
|
}
|
|
48
48
|
export declare function getReleaseAssetResponse(releaseId: string, assetPath: string, config?: FetchConfig): Promise<Response>;
|
|
49
|
+
export declare function isDamAssetPath(assetPath: string): boolean;
|
|
50
|
+
export declare class DamAssetNotFoundError extends Error {
|
|
51
|
+
constructor(assetPath: string, cause?: unknown);
|
|
52
|
+
}
|
|
53
|
+
export declare function getDamAssetResponse(assetPath: string, config?: FetchConfig): Promise<Response>;
|
|
49
54
|
export declare function resolveReleaseId(config?: FetchConfig): Promise<string>;
|
package/dist/next/server.js
CHANGED
|
@@ -150,6 +150,53 @@ export async function getReleaseAssetResponse(releaseId, assetPath, config = {})
|
|
|
150
150
|
headers.set('cache-control', 'public, max-age=300, must-revalidate');
|
|
151
151
|
return new Response(res.body, { status: 200, headers });
|
|
152
152
|
}
|
|
153
|
+
// DAM assets are globally shared and content-addressed: the Go hosting
|
|
154
|
+
// adapter stages them at the bucket ROOT under `.gradial-dam/...`
|
|
155
|
+
// (prefixedKey(damAssetKey(path)) — see internal/domain/hosting/vercel), not
|
|
156
|
+
// under the per-release `releases/{id}/assets/...` prefix. The tenant key
|
|
157
|
+
// prefix (ACI_S3_KEY_PREFIX) still applies.
|
|
158
|
+
const DAM_ASSET_KEY_PREFIX = '.gradial-dam';
|
|
159
|
+
export function isDamAssetPath(assetPath) {
|
|
160
|
+
const clean = cleanAssetPath(assetPath);
|
|
161
|
+
return clean === DAM_ASSET_KEY_PREFIX || clean.startsWith(`${DAM_ASSET_KEY_PREFIX}/`);
|
|
162
|
+
}
|
|
163
|
+
export class DamAssetNotFoundError extends Error {
|
|
164
|
+
constructor(assetPath, cause) {
|
|
165
|
+
super(`ACI DAM asset not found: ${assetPath}`);
|
|
166
|
+
this.name = 'DamAssetNotFoundError';
|
|
167
|
+
this.cause = cause;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export async function getDamAssetResponse(assetPath, config = {}) {
|
|
171
|
+
const clean = cleanAssetPath(assetPath);
|
|
172
|
+
if (!clean.startsWith(`${DAM_ASSET_KEY_PREFIX}/`)) {
|
|
173
|
+
throw new DamAssetNotFoundError(assetPath);
|
|
174
|
+
}
|
|
175
|
+
const provider = new S3ContentProvider({
|
|
176
|
+
bucket: config.bucket,
|
|
177
|
+
keyPrefix: joinKey(config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || ''),
|
|
178
|
+
region: config.region,
|
|
179
|
+
accessKeyId: config.accessKeyId,
|
|
180
|
+
secretAccessKey: config.secretAccessKey,
|
|
181
|
+
sessionToken: config.sessionToken,
|
|
182
|
+
endpoint: config.endpoint,
|
|
183
|
+
});
|
|
184
|
+
const key = joinKey(provider.config.keyPrefix, clean);
|
|
185
|
+
const res = await provider.fetchRaw(key);
|
|
186
|
+
if (!res.ok || !res.body) {
|
|
187
|
+
throw new DamAssetNotFoundError(clean, await safeResponseText(res));
|
|
188
|
+
}
|
|
189
|
+
const headers = new Headers();
|
|
190
|
+
copyHeader(res.headers, headers, 'content-type');
|
|
191
|
+
copyHeader(res.headers, headers, 'content-length');
|
|
192
|
+
copyHeader(res.headers, headers, 'etag');
|
|
193
|
+
copyHeader(res.headers, headers, 'last-modified');
|
|
194
|
+
// DAM keys are content-addressed, so the bytes behind a key never change.
|
|
195
|
+
headers.set('cache-control', 'public, max-age=31536000, immutable');
|
|
196
|
+
headers.set('cdn-cache-control', 'max-age=31536000');
|
|
197
|
+
headers.set('vercel-cdn-cache-control', 'max-age=31536000');
|
|
198
|
+
return new Response(res.body, { status: 200, headers });
|
|
199
|
+
}
|
|
153
200
|
function cleanAssetPath(value) {
|
|
154
201
|
const clean = value.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
|
|
155
202
|
if (!clean || clean.split('/').some((s) => s === '..' || s === '.'))
|
|
@@ -89,6 +89,19 @@ function walkFiles(dir, predicate) {
|
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
export function checkConfig(ctx) {
|
|
91
91
|
const results = [];
|
|
92
|
+
// Env-only site identity: in siteIdMode 'env' the platform owns tenant
|
|
93
|
+
// identity (terraform injects ACI_SITE_ID per deployment), so .aci.yaml
|
|
94
|
+
// must not carry a siteId at all — a leftover label pins the repo to one
|
|
95
|
+
// site and drifts silently from the deployed identity.
|
|
96
|
+
if (ctx.siteIdMode === 'env') {
|
|
97
|
+
const aciYaml = readTextFile(join(ctx.rootDir, '.aci.yaml'));
|
|
98
|
+
if (aciYaml !== null) {
|
|
99
|
+
const pinsSiteId = /^siteId\s*:/m.test(aciYaml);
|
|
100
|
+
results.push(!pinsSiteId
|
|
101
|
+
? ok('config', '.aci.yaml does not pin a siteId (env-only identity)')
|
|
102
|
+
: fail('config', '.aci.yaml does not pin a siteId (env-only identity)', '.aci.yaml declares siteId but this repo uses siteIdMode "env": the platform owns site identity (ACI_SITE_ID is injected per deployment), so the manifest must not pin a site — delete the siteId line'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
92
105
|
if (ctx.framework === 'next') {
|
|
93
106
|
const configFile = findFirstExisting(ctx.rootDir, [
|
|
94
107
|
'next.config.ts',
|
|
@@ -242,13 +255,65 @@ export function checkMiddleware(ctx) {
|
|
|
242
255
|
results.push(usesSDKMiddleware
|
|
243
256
|
? ok('middleware', 'middleware uses createGradialMiddleware', rel)
|
|
244
257
|
: fail('middleware', 'middleware uses createGradialMiddleware', rel + ' should use createGradialMiddleware from @gradial/aci/next/middleware'));
|
|
245
|
-
// Check
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
// Check that the siteId passed to createGradialMiddleware is canonical.
|
|
259
|
+
// Accepted forms depend on ctx.siteIdMode (default 'literal'):
|
|
260
|
+
//
|
|
261
|
+
// 'literal':
|
|
262
|
+
// (a) a string literal exactly equal to ctx.siteId (the tenant's
|
|
263
|
+
// canonical site id) — this is what the template stamps;
|
|
264
|
+
// (b) process.env.ACI_SITE_ID || '<literal>' (or ?? instead of ||)
|
|
265
|
+
// where the literal default is exactly ctx.siteId — the
|
|
266
|
+
// multi-tenant/dev-mirror form: the deployment env supplies the
|
|
267
|
+
// per-tenant identity, the default keeps local dev correct.
|
|
268
|
+
//
|
|
269
|
+
// 'env' (tenant-agnostic repos deployed to multiple sites): the ONLY
|
|
270
|
+
// accepted form is exactly `siteId: process.env.ACI_SITE_ID`. The
|
|
271
|
+
// platform owns tenant identity — terraform sets ACI_SITE_ID on every
|
|
272
|
+
// deployment — and the runtime 500s cleanly if it is missing, so any
|
|
273
|
+
// literal, fallback (|| / ??), other env var name, or computed
|
|
274
|
+
// expression fails.
|
|
275
|
+
//
|
|
276
|
+
// Anything else (placeholder literal, wrong/missing default, other env
|
|
277
|
+
// var names, computed expressions) fails: a wrong siteId means the edge
|
|
278
|
+
// release-pointer key never matches and every live route 503s.
|
|
279
|
+
const siteIdMode = ctx.siteIdMode ?? 'literal';
|
|
280
|
+
if (siteIdMode === 'env') {
|
|
281
|
+
const label = 'middleware configures canonical siteId';
|
|
282
|
+
// Narrow the search to the createGradialMiddleware call when present so
|
|
283
|
+
// an unrelated siteId reference elsewhere in the file can't satisfy us.
|
|
284
|
+
const callIdx = middlewareFile.content.indexOf('createGradialMiddleware(');
|
|
285
|
+
const region = callIdx >= 0 ? middlewareFile.content.slice(callIdx) : middlewareFile.content;
|
|
286
|
+
// Exactly `siteId: process.env.ACI_SITE_ID` followed by the end of the
|
|
287
|
+
// property (`,` or `}`) — same object-property shape the literal-mode
|
|
288
|
+
// regexes target, but with nothing allowed after the env access.
|
|
289
|
+
const bareEnv = /siteId\s*:\s*process\.env\.ACI_SITE_ID\s*(?=[,}])/.test(region);
|
|
290
|
+
results.push(bareEnv
|
|
291
|
+
? ok('middleware', label, `${rel} (env-only siteId)`)
|
|
292
|
+
: fail('middleware', label, `${rel}: middleware must read siteId only from process.env.ACI_SITE_ID (no literal or fallback) — the platform owns tenant identity and injects ACI_SITE_ID per deployment`));
|
|
293
|
+
}
|
|
294
|
+
else if (ctx.siteId) {
|
|
295
|
+
const label = 'middleware configures canonical siteId';
|
|
296
|
+
const expected = ctx.siteId;
|
|
297
|
+
const acceptedForms = `accepted forms: siteId: '${expected}' or ` +
|
|
298
|
+
`siteId: process.env.ACI_SITE_ID || '${expected}' (?? also accepted)`;
|
|
299
|
+
// Narrow the search to the createGradialMiddleware call when present so
|
|
300
|
+
// an unrelated siteId reference elsewhere in the file can't satisfy us.
|
|
301
|
+
const callIdx = middlewareFile.content.indexOf('createGradialMiddleware(');
|
|
302
|
+
const region = callIdx >= 0 ? middlewareFile.content.slice(callIdx) : middlewareFile.content;
|
|
303
|
+
// Form (b): process.env.ACI_SITE_ID || '<default>' (or ??).
|
|
304
|
+
const envMatch = region.match(/siteId\s*:\s*process\.env\.ACI_SITE_ID\s*(?:\|\||\?\?)\s*(['"`])((?:(?!\1).)*)\1/);
|
|
305
|
+
// Form (a): bare string literal (single/double quotes or backticks).
|
|
306
|
+
const literalMatch = region.match(/siteId\s*:\s*(['"`])((?:(?!\1).)*)\1/);
|
|
307
|
+
const configured = envMatch ? envMatch[2] : literalMatch ? literalMatch[2] : null;
|
|
308
|
+
if (configured === expected) {
|
|
309
|
+
results.push(ok('middleware', label, `${rel} (${envMatch ? 'env with canonical default' : 'canonical literal'})`));
|
|
310
|
+
}
|
|
311
|
+
else if (configured !== null) {
|
|
312
|
+
results.push(fail('middleware', label, `${rel}: siteId ${envMatch ? 'default ' : ''}is '${configured}' but the canonical site id is '${expected}'; ${acceptedForms}`));
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
results.push(fail('middleware', label, `${rel}: siteId passed to createGradialMiddleware is not an accepted form (placeholder/computed expressions, other env vars, and process.env.ACI_SITE_ID without a literal default all fail); ${acceptedForms}`));
|
|
316
|
+
}
|
|
252
317
|
}
|
|
253
318
|
// Check that the middleware matcher is inlined (not imported) and matches
|
|
254
319
|
// the SDK's canonical value. Next.js requires config.matcher to be
|
|
@@ -58,6 +58,7 @@ export function runConformanceChecks(options) {
|
|
|
58
58
|
framework: options.framework,
|
|
59
59
|
siteId: options.siteId,
|
|
60
60
|
rootDir: options.rootDir || process.cwd(),
|
|
61
|
+
siteIdMode: options.siteIdMode || 'literal',
|
|
61
62
|
};
|
|
62
63
|
const checks = resolveChecks(options.checks);
|
|
63
64
|
const allResults = checks.flatMap((name) => {
|
|
@@ -84,6 +85,7 @@ export function conformancePreset(options) {
|
|
|
84
85
|
framework: options.framework,
|
|
85
86
|
siteId: options.siteId,
|
|
86
87
|
rootDir: options.rootDir || process.cwd(),
|
|
88
|
+
siteIdMode: options.siteIdMode || 'literal',
|
|
87
89
|
};
|
|
88
90
|
for (const checkName of checks) {
|
|
89
91
|
await t.test(checkName, () => {
|
|
@@ -16,6 +16,21 @@ export interface ConformancePresetOptions {
|
|
|
16
16
|
siteId: string;
|
|
17
17
|
/** Root directory of the site project. Defaults to process.cwd(). */
|
|
18
18
|
rootDir?: string;
|
|
19
|
+
/**
|
|
20
|
+
* How the middleware is expected to obtain its siteId. Defaults to 'literal'.
|
|
21
|
+
*
|
|
22
|
+
* - 'literal': the middleware passes the canonical siteId as a string
|
|
23
|
+
* literal (or `process.env.ACI_SITE_ID || '<canonical>'` / `??` with the
|
|
24
|
+
* canonical literal as the default). This is what the template stamps for
|
|
25
|
+
* single-tenant repos.
|
|
26
|
+
* - 'env': the middleware must read siteId ONLY from the environment —
|
|
27
|
+
* exactly `siteId: process.env.ACI_SITE_ID`, with no literal default, no
|
|
28
|
+
* `||`/`??` fallback, no other env var names, and no computed
|
|
29
|
+
* expressions. Use this for tenant-agnostic repos deployed to multiple
|
|
30
|
+
* sites (the platform owns tenant identity and injects ACI_SITE_ID per
|
|
31
|
+
* deployment; a missing env produces a clean 500 at runtime).
|
|
32
|
+
*/
|
|
33
|
+
siteIdMode?: 'literal' | 'env';
|
|
19
34
|
/**
|
|
20
35
|
* Which checks to run. Defaults to all checks.
|
|
21
36
|
* Pass an array to run only those checks, or an object to enable/disable individually.
|
|
@@ -42,4 +57,6 @@ export interface ConformanceContext {
|
|
|
42
57
|
framework: ConformanceFramework;
|
|
43
58
|
siteId: string;
|
|
44
59
|
rootDir: string;
|
|
60
|
+
/** See ConformancePresetOptions.siteIdMode. Defaults to 'literal'. */
|
|
61
|
+
siteIdMode?: 'literal' | 'env';
|
|
45
62
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -88,14 +88,6 @@
|
|
|
88
88
|
"import": "./dist/testing/conformance/index.js"
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
|
-
"scripts": {
|
|
92
|
-
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
93
|
-
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
94
|
-
"build:cli": "cd ../.. && ./scripts/build-cli.sh",
|
|
95
|
-
"generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
|
|
96
|
-
"compile-registry": "tsx src/cli/compile-registry.mjs",
|
|
97
|
-
"prepack": "npm run generate-starters && npm run build && node scripts/ensure-bin-modes.mjs"
|
|
98
|
-
},
|
|
99
91
|
"dependencies": {
|
|
100
92
|
"markdown-it": "^14.1.0",
|
|
101
93
|
"sanitize-html": "^2.13.1",
|
|
@@ -135,11 +127,18 @@
|
|
|
135
127
|
"zod": "^4.0.0"
|
|
136
128
|
},
|
|
137
129
|
"optionalDependencies": {
|
|
138
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.
|
|
139
|
-
"@gradial/aci-cli-darwin-x64": "0.1.
|
|
140
|
-
"@gradial/aci-cli-linux-arm64": "0.1.
|
|
141
|
-
"@gradial/aci-cli-linux-x64": "0.1.
|
|
142
|
-
"@gradial/aci-cli-win32-arm64": "0.1.
|
|
143
|
-
"@gradial/aci-cli-win32-x64": "0.1.
|
|
130
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.36",
|
|
131
|
+
"@gradial/aci-cli-darwin-x64": "0.1.36",
|
|
132
|
+
"@gradial/aci-cli-linux-arm64": "0.1.36",
|
|
133
|
+
"@gradial/aci-cli-linux-x64": "0.1.36",
|
|
134
|
+
"@gradial/aci-cli-win32-arm64": "0.1.36",
|
|
135
|
+
"@gradial/aci-cli-win32-x64": "0.1.36"
|
|
136
|
+
},
|
|
137
|
+
"scripts": {
|
|
138
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
139
|
+
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
140
|
+
"build:cli": "cd ../.. && ./scripts/build-cli.sh",
|
|
141
|
+
"generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
|
|
142
|
+
"compile-registry": "tsx src/cli/compile-registry.mjs"
|
|
144
143
|
}
|
|
145
|
-
}
|
|
144
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
//
|
|
1
|
+
// TS reference engine for scaffolding a site from the SDK templates.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// `scripts/generate-starters.ts` (repo codegen for starters/*) calls
|
|
4
|
+
// materializeTemplate, then applies the starter-content overlay as an
|
|
5
|
+
// explicit stage on top. Customer scaffolding (`aci init`) is the Go CLI
|
|
6
|
+
// (internal/transport/cli/cmd/init.go), which implements the same engine
|
|
7
|
+
// semantics; scripts/test-starter-dev.mjs asserts the two engines produce
|
|
8
|
+
// byte-identical output so they can never drift.
|
|
6
9
|
// Spec: specs/features/starter-fresh-clone-dx (D-1).
|
|
7
10
|
import * as fs from 'node:fs';
|
|
8
11
|
import * as path from 'node:path';
|
|
@@ -106,12 +109,11 @@ export function materializeTemplate({ templatesDir, framework, destDir, vars })
|
|
|
106
109
|
|
|
107
110
|
copyDirSync(templateDir, destDir);
|
|
108
111
|
|
|
109
|
-
// Shared sources overwrite any framework-template copies so
|
|
110
|
-
// design tokens can only come from templates/common.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
112
|
+
// Shared design-system sources overwrite any framework-template copies so
|
|
113
|
+
// design tokens can only come from templates/common. Contract sources are
|
|
114
|
+
// NOT copied here: a scaffold ships only the empty contracts barrel from the
|
|
115
|
+
// framework template; populated contracts arrive via the starter-content
|
|
116
|
+
// overlay stage (generate-starters.ts / `aci init --with-starter-content`).
|
|
115
117
|
copyDirSync(
|
|
116
118
|
path.join(commonDir, 'design-system', 'tokens'),
|
|
117
119
|
path.join(destDir, 'src', 'design-system', 'tokens'),
|
|
File without changes
|