@gradial/aci 0.1.34 → 0.1.35

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 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 { basename, dirname, join, relative, resolve } from 'node:path';
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
- if (argv[0] === 'init') {
35
- await runInit(argv.slice(1));
36
- process.exit(0);
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
- }
@@ -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);
@@ -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
@@ -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>;
@@ -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 === '.'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
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.34",
139
- "@gradial/aci-cli-darwin-x64": "0.1.34",
140
- "@gradial/aci-cli-linux-arm64": "0.1.34",
141
- "@gradial/aci-cli-linux-x64": "0.1.34",
142
- "@gradial/aci-cli-win32-arm64": "0.1.34",
143
- "@gradial/aci-cli-win32-x64": "0.1.34"
130
+ "@gradial/aci-cli-darwin-arm64": "0.1.35",
131
+ "@gradial/aci-cli-darwin-x64": "0.1.35",
132
+ "@gradial/aci-cli-linux-arm64": "0.1.35",
133
+ "@gradial/aci-cli-linux-x64": "0.1.35",
134
+ "@gradial/aci-cli-win32-arm64": "0.1.35",
135
+ "@gradial/aci-cli-win32-x64": "0.1.35"
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
- // Single assembly path for scaffolding a site from the SDK templates.
1
+ // TS reference engine for scaffolding a site from the SDK templates.
2
2
  //
3
- // Both `scripts/generate-starters.ts` (repo codegen for starters/*) and
4
- // `aci init` (bin/aci.js, customer scaffolding from the published package)
5
- // call materializeTemplate so the two outputs can never drift.
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 contracts and
110
- // design tokens can only come from templates/common.
111
- copyDirSync(
112
- path.join(commonDir, 'contracts', 'components'),
113
- path.join(destDir, 'src', 'cms', 'contracts', 'components'),
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