@gradial/aci 0.1.1 → 0.1.2

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 (47) hide show
  1. package/README.md +46 -1
  2. package/bin/aci +0 -0
  3. package/bin/aci.js +139 -27
  4. package/dist/assets/index.d.ts +2 -0
  5. package/dist/assets/index.js +2 -0
  6. package/dist/astro/index.js +4 -4
  7. package/dist/block-ref.d.ts +34 -0
  8. package/dist/block-ref.js +34 -0
  9. package/dist/define-component.d.ts +1 -0
  10. package/dist/define-component.js +1 -0
  11. package/dist/define-layout.d.ts +1 -0
  12. package/dist/define-layout.js +1 -0
  13. package/dist/dev/index.d.ts +6 -0
  14. package/dist/dev/index.js +66 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +4 -1
  17. package/dist/next/asset-route.d.ts +9 -0
  18. package/dist/next/asset-route.js +15 -0
  19. package/dist/next/config.d.ts +2 -10
  20. package/dist/next/config.js +5 -2
  21. package/dist/next/server.d.ts +4 -0
  22. package/dist/next/server.js +53 -0
  23. package/dist/providers/s3.d.ts +2 -0
  24. package/dist/providers/s3.js +13 -1
  25. package/dist/react/GradialMedia.d.ts +24 -0
  26. package/dist/react/GradialMedia.js +31 -0
  27. package/dist/react/GradialPicture.d.ts +14 -0
  28. package/dist/react/GradialPicture.js +30 -0
  29. package/dist/react/GradialVideoPlayer.d.ts +13 -0
  30. package/dist/react/GradialVideoPlayer.js +28 -0
  31. package/dist/react/index.d.ts +3 -0
  32. package/dist/react/index.js +3 -0
  33. package/dist/sveltekit/index.js +6 -1
  34. package/dist/types/component.d.ts +8 -3
  35. package/dist/types/image.d.ts +2 -2
  36. package/dist/types/image.js +1 -1
  37. package/dist/types/index.d.ts +2 -0
  38. package/dist/types/index.js +2 -0
  39. package/dist/types/media.d.ts +69 -0
  40. package/dist/types/media.js +86 -0
  41. package/dist/types/video.d.ts +70 -0
  42. package/dist/types/video.js +22 -0
  43. package/package.json +13 -9
  44. package/src/cli/compile-registry.mjs +142 -1
  45. package/src/cli/validate-content.mjs +98 -0
  46. package/src/cli/css-stub-loader.mjs +0 -27
  47. package/src/cli/generate-registry.mjs +0 -72
package/README.md CHANGED
@@ -13,7 +13,7 @@ npm install -D typescript tsx
13
13
  Common imports:
14
14
 
15
15
  ```ts
16
- import { defineComponent, defineLayout, slot } from '@gradial/aci';
16
+ import { defineComponentContract, defineLayoutContract, slot } from '@gradial/aci';
17
17
  import type { GradialRenderer } from '@gradial/aci';
18
18
  import type { KernelPage, KernelSiteConfig } from '@gradial/aci/content';
19
19
  import { createContentSchemas, stringField } from '@gradial/aci/content';
@@ -28,6 +28,51 @@ Framework helpers live under `@gradial/aci/astro`,
28
28
  `@gradial/aci/next/server`, `@gradial/aci/next/middleware`,
29
29
  `@gradial/aci/next/dev-refresh`, and `@gradial/aci/sveltekit`.
30
30
 
31
+ ## Component Contracts vs Runtime Components
32
+
33
+ Keep compile-time CMS contracts separate from runtime component implementation.
34
+ The ACI compiler imports only contract files, so those files must not import
35
+ React, framework components, CSS, or browser-only code.
36
+
37
+ Recommended shape:
38
+
39
+ ```txt
40
+ src/cms/contracts/components/hero.contract.ts
41
+ src/cms/contracts/components/index.ts
42
+ src/cms/contracts/layouts/index.ts
43
+ src/components/Hero.tsx
44
+ src/cms/renderBlock.tsx
45
+ ```
46
+
47
+ Contract files own names, schemas, render modes, image slots, and other metadata:
48
+
49
+ ```ts
50
+ import { defineComponentContract, GradialImageSchema } from '@gradial/aci';
51
+ import { z } from 'zod';
52
+
53
+ export const heroContract = defineComponentContract({
54
+ name: 'hero',
55
+ schema: z.object({
56
+ headline: z.string(),
57
+ image: GradialImageSchema,
58
+ }),
59
+ renderModes: { canStatic: true, canSSR: true, canClientIsland: false },
60
+ });
61
+
62
+ export type HeroProps = z.infer<typeof heroContract.schema>;
63
+ ```
64
+
65
+ Runtime components may import the contract for types, but contract files should
66
+ never import runtime components:
67
+
68
+ ```tsx
69
+ import type { HeroProps } from '../cms/contracts/components/hero.contract';
70
+
71
+ export function Hero(props: HeroProps) {
72
+ return <section>{props.headline}</section>;
73
+ }
74
+ ```
75
+
31
76
  Testing helpers live under `@gradial/aci/testing`:
32
77
 
33
78
  ```ts
package/bin/aci ADDED
Binary file
package/bin/aci.js CHANGED
@@ -1,45 +1,157 @@
1
1
  #!/usr/bin/env node
2
- import { execFileSync } from 'node:child_process';
2
+ import { spawn } from 'node:child_process';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
- import path from 'node:path';
5
+ import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
8
8
  const require = createRequire(import.meta.url);
9
+ const argv = process.argv.slice(2);
10
+ const callerCwd = process.cwd();
9
11
 
10
- const PLATFORMS = {
11
- 'darwin-arm64': '@gradial/cli-darwin-arm64',
12
- 'darwin-x64': '@gradial/cli-darwin-x64',
13
- 'linux-x64': '@gradial/cli-linux-x64',
14
- 'linux-arm64': '@gradial/cli-linux-arm64',
15
- 'win32-x64': '@gradial/cli-win32-x64',
12
+ const platformPackages = {
13
+ 'darwin-arm64': '@gradial/aci-cli-darwin-arm64',
14
+ 'darwin-x64': '@gradial/aci-cli-darwin-x64',
15
+ 'linux-arm64': '@gradial/aci-cli-linux-arm64',
16
+ 'linux-x64': '@gradial/aci-cli-linux-x64',
17
+ 'win32-x64': '@gradial/aci-cli-win32-x64',
16
18
  };
17
19
 
18
- const key = `${process.platform}-${process.arch}`;
19
- const pkg = PLATFORMS[key];
20
- if (!pkg) {
21
- console.error(`ACI: unsupported platform ${key}`);
20
+ const platformKey = `${process.platform}-${process.arch}`;
21
+ const packageName = platformPackages[platformKey];
22
+
23
+ if (!packageName) {
24
+ console.error(`Unsupported platform for aci: ${platformKey}`);
22
25
  process.exit(1);
23
26
  }
24
27
 
25
- const ext = process.platform === 'win32' ? '.exe' : '';
26
- let bin;
27
- const localBin = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'bin', `aci${ext}`);
28
- if (process.env.ACI_BIN) {
29
- bin = process.env.ACI_BIN;
30
- } else if (existsSync(localBin)) {
31
- bin = localBin;
28
+ const binName = process.platform === 'win32' ? 'aci.exe' : 'aci';
29
+ const shimDir = dirname(fileURLToPath(import.meta.url));
30
+ const repoRoot = resolve(shimDir, '../../..');
31
+
32
+ function resolveBundledBinary() {
33
+ const binaryPath = join(shimDir, binName);
34
+ return existsSync(binaryPath) ? binaryPath : null;
35
+ }
36
+
37
+ function resolvePackagedBinary() {
38
+ try {
39
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
40
+ const binaryPath = join(dirname(packageJsonPath), 'bin', binName);
41
+
42
+ return existsSync(binaryPath) ? binaryPath : null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function resolveWorkspaceBinary() {
49
+ const binaryPath = resolve(shimDir, '..', '..', 'packages', `cli-${platformKey}`, 'bin', binName);
50
+
51
+ return existsSync(binaryPath) ? binaryPath : null;
52
+ }
53
+
54
+ const binaryPath = resolveBundledBinary() ?? resolvePackagedBinary() ?? resolveWorkspaceBinary();
55
+ const fallback = binaryPath ? null : resolveGoFallback();
56
+
57
+ if (!binaryPath && !fallback) {
58
+ console.error(
59
+ [
60
+ `Could not find the aci binary for ${platformKey}.`,
61
+ `Install ${packageName} or build the local CLI package with ./scripts/build-cli.sh.`,
62
+ ].join(' '),
63
+ );
64
+ process.exit(1);
32
65
  }
33
66
 
34
- try {
35
- bin ||= require.resolve(`${pkg}/bin/aci${ext}`);
36
- } catch {
37
- console.error('ACI: binary not installed. Run: npm install');
67
+ const child = binaryPath
68
+ ? spawn(binaryPath, argv, { stdio: 'inherit' })
69
+ : spawn(fallback.command, fallback.args, {
70
+ cwd: fallback.cwd,
71
+ stdio: 'inherit',
72
+ });
73
+
74
+ child.on('error', (error) => {
75
+ console.error(`Failed to run aci: ${error.message}`);
38
76
  process.exit(1);
77
+ });
78
+
79
+ child.on('exit', (code, signal) => {
80
+ if (signal) {
81
+ process.kill(process.pid, signal);
82
+ return;
83
+ }
84
+
85
+ process.exit(code ?? 1);
86
+ });
87
+
88
+ function resolveGoFallback() {
89
+ const goModPath = join(repoRoot, 'go.mod');
90
+ const commandDir = join(repoRoot, 'cmd', 'aci');
91
+
92
+ if (!existsSync(goModPath) || !existsSync(commandDir)) {
93
+ return null;
94
+ }
95
+
96
+ return {
97
+ command: 'go',
98
+ args: ['run', './cmd/aci', ...normalizeArgsForGoFallback(argv)],
99
+ cwd: repoRoot,
100
+ };
101
+ }
102
+
103
+ function normalizeArgsForGoFallback(args) {
104
+ const pathFlags = new Set(['-s', '--site-dir', '--content', '--out']);
105
+ const normalized = [];
106
+ let hasSiteDir = false;
107
+
108
+ for (let index = 0; index < args.length; index += 1) {
109
+ const arg = args[index];
110
+
111
+ if (pathFlags.has(arg)) {
112
+ normalized.push(arg);
113
+ if (arg === '-s' || arg === '--site-dir') {
114
+ hasSiteDir = true;
115
+ }
116
+
117
+ const value = args[index + 1];
118
+ if (value !== undefined) {
119
+ normalized.push(normalizePathArg(value));
120
+ index += 1;
121
+ }
122
+ continue;
123
+ }
124
+
125
+ if (arg.startsWith('--site-dir=')) {
126
+ hasSiteDir = true;
127
+ normalized.push(normalizePathFlagValue(arg));
128
+ continue;
129
+ }
130
+
131
+ if (arg.startsWith('--content=') || arg.startsWith('--out=')) {
132
+ normalized.push(normalizePathFlagValue(arg));
133
+ continue;
134
+ }
135
+
136
+ normalized.push(arg);
137
+ }
138
+
139
+ return hasSiteDir ? normalized : ['--site-dir', callerCwd, ...normalized];
140
+ }
141
+
142
+ function normalizePathFlagValue(arg) {
143
+ const separator = arg.indexOf('=');
144
+ return `${arg.slice(0, separator + 1)}${normalizePathArg(arg.slice(separator + 1))}`;
145
+ }
146
+
147
+ function normalizePathArg(value) {
148
+ if (!value || value.startsWith('-') || isAbsolutePath(value)) {
149
+ return value;
150
+ }
151
+
152
+ return resolve(callerCwd, value);
39
153
  }
40
154
 
41
- try {
42
- execFileSync(bin, process.argv.slice(2), { stdio: 'inherit' });
43
- } catch (error) {
44
- process.exit(error.status ?? 1);
155
+ function isAbsolutePath(value) {
156
+ return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
45
157
  }
@@ -1 +1,3 @@
1
1
  export { type GradialImage, type ImageSource, type PictureSource, type ImageSlotContract, type SlotOutput, type ImageHTMLAttributes, GradialImageSchema, renderImageHTML, } from '../types/image.js';
2
+ export { type GradialVideo, type VideoSource, GradialVideoSchema, VideoSourceSchema, } from '../types/video.js';
3
+ export { type GradialAsset, GradialAssetSchema, isGradialImage, isGradialVideo, isGradialAsset, renderVideoHTML, renderAssetHTML, } from '../types/media.js';
@@ -1 +1,3 @@
1
1
  export { GradialImageSchema, renderImageHTML, } from '../types/image.js';
2
+ export { GradialVideoSchema, VideoSourceSchema, } from '../types/video.js';
3
+ export { GradialAssetSchema, isGradialImage, isGradialVideo, isGradialAsset, renderVideoHTML, renderAssetHTML, } from '../types/media.js';
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { gradialContentWatchPlugin, devRefreshPort, devRefreshScript, } from '../dev/index.js';
3
+ import { gradialContentWatchPlugin, gradialDamAssetPlugin, devRefreshPort, devRefreshScript, } from '../dev/index.js';
4
4
  // ---------------------------------------------------------------------------
5
5
  // Static paths helper — for Astro SSG
6
6
  // ---------------------------------------------------------------------------
@@ -40,17 +40,17 @@ export function gradialAstro(options = {}) {
40
40
  }
41
41
  updateConfig({
42
42
  vite: {
43
- plugins: [gradialContentWatchPlugin(options)],
43
+ plugins: [gradialDamAssetPlugin(options), gradialContentWatchPlugin(options)],
44
44
  },
45
45
  });
46
46
  },
47
47
  'astro:build:done'({ dir }) {
48
48
  const siteDir = process.cwd();
49
49
  const compiledRoot = path.resolve(siteDir, options.compiledRoot || process.env.ACI_CONTENT_ROOT || '.aci/compiled');
50
- const source = path.join(compiledRoot, '.aci-dam');
50
+ const source = path.join(compiledRoot, '.gradial-dam');
51
51
  if (!fs.existsSync(source))
52
52
  return;
53
- const target = path.join(dir.pathname, '.aci-dam');
53
+ const target = path.join(dir.pathname, '.gradial-dam');
54
54
  fs.rmSync(target, { recursive: true, force: true });
55
55
  fs.cpSync(source, target, { recursive: true });
56
56
  },
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export interface BlockRefOptions {
3
+ /** Component names allowed in this slot. Omit to allow any registered component. */
4
+ allow?: string[];
5
+ }
6
+ /**
7
+ * Schema helper for nested component references within a parent component.
8
+ *
9
+ * Produces a Zod schema for a block entry: `{ id, component, props }`.
10
+ * When `allow` is provided, `component` is constrained to an enum of the
11
+ * listed names. This flows into the generated JSON schema, so the content
12
+ * validator enforces the allowlist at build time.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * defineComponent({
17
+ * name: 'tabbed_panels',
18
+ * schema: z.object({
19
+ * tabs: z.array(z.object({
20
+ * id: z.string().min(1),
21
+ * label: z.string().min(1),
22
+ * blocks: z.array(blockRef({ allow: ['checklist', 'text_media'] })).min(1),
23
+ * })),
24
+ * }),
25
+ * })
26
+ * ```
27
+ */
28
+ export declare function blockRef(options?: BlockRefOptions): z.ZodObject<{
29
+ id: z.ZodString;
30
+ component: z.ZodEnum<{
31
+ [x: string]: string;
32
+ }> | z.ZodString;
33
+ props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
34
+ }, z.core.$strip>;
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Schema helper for nested component references within a parent component.
4
+ *
5
+ * Produces a Zod schema for a block entry: `{ id, component, props }`.
6
+ * When `allow` is provided, `component` is constrained to an enum of the
7
+ * listed names. This flows into the generated JSON schema, so the content
8
+ * validator enforces the allowlist at build time.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * defineComponent({
13
+ * name: 'tabbed_panels',
14
+ * schema: z.object({
15
+ * tabs: z.array(z.object({
16
+ * id: z.string().min(1),
17
+ * label: z.string().min(1),
18
+ * blocks: z.array(blockRef({ allow: ['checklist', 'text_media'] })).min(1),
19
+ * })),
20
+ * }),
21
+ * })
22
+ * ```
23
+ */
24
+ export function blockRef(options) {
25
+ const allow = options?.allow;
26
+ const componentSchema = allow && allow.length > 0
27
+ ? z.enum(allow)
28
+ : z.string().min(1);
29
+ return z.object({
30
+ id: z.string().min(1),
31
+ component: componentSchema,
32
+ props: z.record(z.string(), z.unknown()),
33
+ });
34
+ }
@@ -1,2 +1,3 @@
1
1
  import type { ComponentContract, ComponentDefinition } from './types/component.js';
2
2
  export declare function defineComponent<TSchema = unknown>(definition: ComponentDefinition<TSchema>): ComponentContract<TSchema>;
3
+ export declare const defineComponentContract: typeof defineComponent;
@@ -11,3 +11,4 @@ export function defineComponent(definition) {
11
11
  vary: definition.vary ?? definition.varyDimensions,
12
12
  };
13
13
  }
14
+ export const defineComponentContract = defineComponent;
@@ -1,3 +1,4 @@
1
1
  import type { LayoutContract, LayoutDefinition, LayoutSlot } from './types/layout.js';
2
2
  export declare function defineLayout(definition: LayoutDefinition): LayoutContract;
3
+ export declare const defineLayoutContract: typeof defineLayout;
3
4
  export declare function slot(name: string, required?: boolean): LayoutSlot;
@@ -5,6 +5,7 @@ export function defineLayout(definition) {
5
5
  defaults: definition.defaults,
6
6
  };
7
7
  }
8
+ export const defineLayoutContract = defineLayout;
8
9
  export function slot(name, required = false) {
9
10
  return { name, required };
10
11
  }
@@ -1,3 +1,4 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
1
2
  import { type DevRefreshOptions } from './browser.js';
2
3
  export { DEFAULT_DEV_REFRESH_PATH, DEFAULT_DEV_REFRESH_PORT, type DevRefreshOptions } from './browser.js';
3
4
  declare global {
@@ -7,9 +8,13 @@ declare global {
7
8
  }
8
9
  export interface GradialContentWatchOptions extends DevRefreshOptions {
9
10
  contentRoot?: string;
11
+ compiledRoot?: string;
10
12
  enabled?: boolean;
11
13
  }
12
14
  interface ViteDevServerLike {
15
+ middlewares?: {
16
+ use(path: string, handler: (req: IncomingMessage, res: ServerResponse, next: () => void) => void): void;
17
+ };
13
18
  watcher: {
14
19
  add(path: string): void;
15
20
  on(event: string, callback: (eventName: string, filePath: string) => void): void;
@@ -28,3 +33,4 @@ export declare function devRefreshPort(options?: DevRefreshOptions): number;
28
33
  export declare function devRefreshScript(options?: DevRefreshOptions): string;
29
34
  export declare function devRefreshScriptTag(options?: DevRefreshOptions): string;
30
35
  export declare function gradialContentWatchPlugin(options?: GradialContentWatchOptions): VitePluginLike;
36
+ export declare function gradialDamAssetPlugin(options?: GradialContentWatchOptions): VitePluginLike;
package/dist/dev/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs/promises';
1
2
  import path from 'node:path';
2
3
  import { compiledContentRoot } from '../content/routes.js';
3
4
  import { DEFAULT_DEV_REFRESH_PATH, DEFAULT_DEV_REFRESH_PORT, } from './browser.js';
@@ -68,3 +69,68 @@ export function gradialContentWatchPlugin(options = {}) {
68
69
  },
69
70
  };
70
71
  }
72
+ export function gradialDamAssetPlugin(options = {}) {
73
+ return {
74
+ name: 'gradial-dam-assets',
75
+ apply: 'serve',
76
+ configureServer(server) {
77
+ if (!server.middlewares) {
78
+ return;
79
+ }
80
+ server.middlewares.use('/.gradial-dam', async (req, res, next) => {
81
+ const root = path.resolve(options.compiledRoot || compiledContentRoot());
82
+ const requestPath = safeRequestPath(req.url || '');
83
+ if (!requestPath) {
84
+ res.statusCode = 404;
85
+ res.end('asset not found');
86
+ return;
87
+ }
88
+ const filePath = path.resolve(root, '.gradial-dam', requestPath.replace(/^\/+/, ''));
89
+ if (!isInside(root, filePath)) {
90
+ res.statusCode = 404;
91
+ res.end('asset not found');
92
+ return;
93
+ }
94
+ try {
95
+ const body = await fs.readFile(filePath);
96
+ res.statusCode = 200;
97
+ res.setHeader('Content-Type', contentType(filePath));
98
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
99
+ res.end(body);
100
+ }
101
+ catch {
102
+ next();
103
+ }
104
+ });
105
+ },
106
+ };
107
+ }
108
+ function safeRequestPath(url) {
109
+ const rawPath = url.split('?')[0] || '';
110
+ try {
111
+ const decoded = decodeURIComponent(rawPath);
112
+ return decoded.startsWith('/.gradial-dam/') ? decoded.slice('/.gradial-dam/'.length) : decoded;
113
+ }
114
+ catch {
115
+ return '';
116
+ }
117
+ }
118
+ function isInside(root, filePath) {
119
+ const rel = path.relative(root, filePath);
120
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
121
+ }
122
+ function contentType(filePath) {
123
+ switch (path.extname(filePath).toLowerCase()) {
124
+ case '.webp':
125
+ return 'image/webp';
126
+ case '.svg':
127
+ return 'image/svg+xml';
128
+ case '.jpg':
129
+ case '.jpeg':
130
+ return 'image/jpeg';
131
+ case '.png':
132
+ return 'image/png';
133
+ default:
134
+ return 'application/octet-stream';
135
+ }
136
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
+ export * from './block-ref.js';
1
2
  export * from './define-component.js';
2
3
  export * from './define-layout.js';
3
- export * from './content/index.js';
4
4
  export * from './types/index.js';
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
+ export * from './block-ref.js';
1
2
  export * from './define-component.js';
2
3
  export * from './define-layout.js';
3
- export * from './content/index.js';
4
4
  export * from './types/index.js';
5
+ // Note: content/ is NOT re-exported here because it imports node:path,
6
+ // making it incompatible with client-side bundlers. Import server-only
7
+ // content utilities from '@gradial/aci/content' explicitly.
@@ -0,0 +1,9 @@
1
+ type AssetRouteContext = {
2
+ params: Promise<AssetRouteParams>;
3
+ };
4
+ type AssetRouteParams = {
5
+ releaseId?: string;
6
+ path?: string[];
7
+ };
8
+ export declare function GET(_request: Request, { params }: AssetRouteContext): Promise<Response>;
9
+ export {};
@@ -0,0 +1,15 @@
1
+ import { getReleaseAssetResponse } from './server.js';
2
+ export async function GET(_request, { params }) {
3
+ const resolvedParams = await params;
4
+ const releaseId = resolvedParams.releaseId || '';
5
+ const assetPath = (resolvedParams.path || []).join('/');
6
+ if (!releaseId || !assetPath) {
7
+ return new Response('asset not found', { status: 404 });
8
+ }
9
+ try {
10
+ return await getReleaseAssetResponse(releaseId, assetPath);
11
+ }
12
+ catch {
13
+ return new Response('asset not found', { status: 404 });
14
+ }
15
+ }
@@ -1,14 +1,6 @@
1
- interface NextRewrite {
2
- source: string;
3
- destination: string;
4
- }
5
- interface NextConfig {
6
- rewrites?: () => Promise<NextRewrite[]> | NextRewrite[];
7
- [key: string]: unknown;
8
- }
1
+ import type { NextConfig } from 'next';
9
2
  export interface WithGradialOptions {
10
- /** Asset rewrite prefix. Defaults to '/.aci-dam' */
3
+ /** Asset rewrite prefix. Defaults to '/.gradial-dam' */
11
4
  assetPrefix?: string;
12
5
  }
13
6
  export declare function withGradial(nextConfig?: NextConfig, options?: WithGradialOptions): NextConfig;
14
- export {};
@@ -1,5 +1,5 @@
1
1
  export function withGradial(nextConfig = {}, options = {}) {
2
- const assetPrefix = options.assetPrefix || '/.aci-dam';
2
+ const assetPrefix = options.assetPrefix || '/.gradial-dam';
3
3
  const userRewrites = nextConfig.rewrites;
4
4
  return {
5
5
  ...nextConfig,
@@ -16,7 +16,10 @@ export function withGradial(nextConfig = {}, options = {}) {
16
16
  if (Array.isArray(userResult)) {
17
17
  return [...gradialRewrites, ...userResult];
18
18
  }
19
- return gradialRewrites;
19
+ return {
20
+ ...userResult,
21
+ beforeFiles: [...gradialRewrites, ...(userResult.beforeFiles ?? [])],
22
+ };
20
23
  },
21
24
  };
22
25
  }
@@ -36,4 +36,8 @@ export declare function routeFromNextParams(params?: Promise<{
36
36
  export declare function generateGradialStaticParams(): Promise<Array<{
37
37
  slug?: string[];
38
38
  }>>;
39
+ export declare class ReleaseAssetNotFoundError extends Error {
40
+ constructor(releaseId: string, assetPath: string, cause?: unknown);
41
+ }
42
+ export declare function getReleaseAssetResponse(releaseId: string, assetPath: string, config?: GradialFetchConfig): Promise<Response>;
39
43
  export declare function resolveReleaseId(config?: GradialFetchConfig): Promise<string>;
@@ -47,6 +47,59 @@ export async function generateGradialStaticParams() {
47
47
  slug: entry.path === '/' ? undefined : entry.path.replace(/^\/|\/$/g, '').split('/'),
48
48
  }));
49
49
  }
50
+ export class ReleaseAssetNotFoundError extends Error {
51
+ constructor(releaseId, assetPath, cause) {
52
+ super(`ACI release asset not found: ${releaseId}/${assetPath}`);
53
+ this.name = 'ReleaseAssetNotFoundError';
54
+ this.cause = cause;
55
+ }
56
+ }
57
+ export async function getReleaseAssetResponse(releaseId, assetPath, config = {}) {
58
+ const clean = cleanAssetPath(assetPath);
59
+ if (!releaseId || !clean) {
60
+ throw new ReleaseAssetNotFoundError(releaseId, assetPath);
61
+ }
62
+ const provider = new S3ContentProvider({
63
+ bucket: config.bucket,
64
+ keyPrefix: releaseKey(config, releaseId),
65
+ region: config.region,
66
+ accessKeyId: config.accessKeyId,
67
+ secretAccessKey: config.secretAccessKey,
68
+ sessionToken: config.sessionToken,
69
+ endpoint: config.endpoint,
70
+ });
71
+ const key = joinKey(provider.config.keyPrefix, 'assets', clean);
72
+ const res = await provider.fetchRaw(key);
73
+ if (!res.ok || !res.body) {
74
+ throw new ReleaseAssetNotFoundError(releaseId, clean, await safeResponseText(res));
75
+ }
76
+ const headers = new Headers();
77
+ copyHeader(res.headers, headers, 'content-type');
78
+ copyHeader(res.headers, headers, 'content-length');
79
+ copyHeader(res.headers, headers, 'etag');
80
+ copyHeader(res.headers, headers, 'last-modified');
81
+ headers.set('cache-control', 'public, max-age=31536000, immutable');
82
+ return new Response(res.body, { status: 200, headers });
83
+ }
84
+ function cleanAssetPath(value) {
85
+ const clean = value.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
86
+ if (!clean || clean.split('/').some((s) => s === '..' || s === '.'))
87
+ return '';
88
+ return clean;
89
+ }
90
+ function copyHeader(from, to, name) {
91
+ const value = from.get(name);
92
+ if (value)
93
+ to.set(name, value);
94
+ }
95
+ async function safeResponseText(res) {
96
+ try {
97
+ return await res.text();
98
+ }
99
+ catch {
100
+ return '';
101
+ }
102
+ }
50
103
  export async function resolveReleaseId(config = {}) {
51
104
  if (config.releaseId)
52
105
  return config.releaseId;
@@ -18,7 +18,9 @@ export declare class S3ContentProvider implements ContentProvider {
18
18
  getPage<T = unknown>(route: string): Promise<T>;
19
19
  getFragment<T = unknown>(id: string): Promise<T>;
20
20
  manifest(): Promise<CompiledManifest>;
21
+ fetchRaw(key: string): Promise<Response>;
21
22
  private getJSON;
22
23
  }
23
24
  export declare function getS3JSON<T>(key: string, config: ResolvedS3Config): Promise<T>;
25
+ export declare function getS3Raw(key: string, config: ResolvedS3Config): Promise<Response>;
24
26
  export {};