@gradial/aci 0.1.0 → 0.1.1

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 (50) hide show
  1. package/README.md +1 -1
  2. package/bin/aci.js +45 -0
  3. package/dist/assets/index.d.ts +1 -0
  4. package/dist/assets/index.js +1 -0
  5. package/dist/astro/index.d.ts +24 -2
  6. package/dist/astro/index.js +42 -4
  7. package/dist/content/index.d.ts +0 -3
  8. package/dist/content/index.js +0 -3
  9. package/dist/content/provider.d.ts +32 -8
  10. package/dist/content/provider.js +26 -16
  11. package/dist/content/routes.d.ts +6 -12
  12. package/dist/content/routes.js +9 -55
  13. package/dist/content/validation.js +1 -1
  14. package/dist/define-layout.js +5 -1
  15. package/dist/dev/browser.d.ts +1 -1
  16. package/dist/dev/browser.js +1 -1
  17. package/dist/dev/index.d.ts +3 -3
  18. package/dist/dev/index.js +8 -8
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/next/config.d.ts +14 -0
  22. package/dist/next/config.js +22 -0
  23. package/dist/next/dev-refresh.js +4 -4
  24. package/dist/next/edge-config.d.ts +1 -0
  25. package/dist/next/edge-config.js +92 -0
  26. package/dist/next/index.d.ts +2 -0
  27. package/dist/next/index.js +2 -0
  28. package/dist/next/middleware.js +4 -6
  29. package/dist/next/server.d.ts +5 -24
  30. package/dist/next/server.js +47 -152
  31. package/dist/providers/file.d.ts +11 -17
  32. package/dist/providers/file.js +44 -78
  33. package/dist/providers/s3.d.ts +24 -0
  34. package/dist/providers/s3.js +162 -0
  35. package/dist/sveltekit/index.d.ts +18 -2
  36. package/dist/sveltekit/index.js +35 -4
  37. package/dist/testing/index.d.ts +14 -12
  38. package/dist/testing/index.js +41 -28
  39. package/dist/types/component.d.ts +19 -2
  40. package/dist/types/config.d.ts +4 -0
  41. package/dist/types/image.d.ts +51 -0
  42. package/dist/types/image.js +58 -0
  43. package/dist/types/index.d.ts +1 -0
  44. package/dist/types/index.js +1 -0
  45. package/dist/types/layout.d.ts +12 -0
  46. package/package.json +26 -2
  47. package/src/cli/compile-registry.mjs +162 -0
  48. package/src/cli/css-stub-loader.mjs +27 -0
  49. package/src/cli/generate-registry.mjs +72 -0
  50. package/src/cli/validate-content.mjs +391 -0
@@ -0,0 +1,92 @@
1
+ export async function getUncachedEdgeConfigValue(edgeConfig, key) {
2
+ const connection = parseEdgeConfigConnection(edgeConfig);
3
+ if (!connection) {
4
+ throw new Error('Invalid Edge Config connection string');
5
+ }
6
+ const headers = new Headers({
7
+ authorization: `Bearer ${connection.token}`,
8
+ 'x-edge-config-min-updated-at': `${Number.MAX_SAFE_INTEGER}`,
9
+ });
10
+ if (process.env.VERCEL_ENV) {
11
+ headers.set('x-edge-config-vercel-env', process.env.VERCEL_ENV);
12
+ }
13
+ const url = `${connection.baseUrl}/item/${encodeURIComponent(key)}?version=${encodeURIComponent(connection.version)}`;
14
+ const response = await fetch(url, {
15
+ headers,
16
+ cache: 'no-store',
17
+ });
18
+ if (response.ok) {
19
+ return response.json();
20
+ }
21
+ if (response.status === 404 && response.headers.has('x-edge-config-digest')) {
22
+ return undefined;
23
+ }
24
+ const body = await response.text().catch(() => '');
25
+ throw new Error(`Edge Config read ${key} returned ${response.status}: ${body.slice(0, 500)}`);
26
+ }
27
+ function parseEdgeConfigConnection(value) {
28
+ const text = value.trim();
29
+ if (!text)
30
+ return null;
31
+ return parseEdgeConfigQueryConnection(text)
32
+ || parseVercelEdgeConfigURL(text)
33
+ || parseExternalEdgeConfigURL(text);
34
+ }
35
+ function parseEdgeConfigQueryConnection(value) {
36
+ if (!value.startsWith('edge-config:'))
37
+ return null;
38
+ const params = new URLSearchParams(value.slice('edge-config:'.length));
39
+ const id = params.get('id') || '';
40
+ const token = params.get('token') || '';
41
+ if (!id || !token)
42
+ return null;
43
+ return {
44
+ baseUrl: `https://edge-config.vercel.com/${id}`,
45
+ token,
46
+ version: params.get('version') || '1',
47
+ };
48
+ }
49
+ function parseVercelEdgeConfigURL(value) {
50
+ let url;
51
+ try {
52
+ url = new URL(value);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ if (url.protocol !== 'https:' || url.host !== 'edge-config.vercel.com')
58
+ return null;
59
+ const id = url.pathname.split('/').filter(Boolean)[0] || '';
60
+ const token = url.searchParams.get('token') || '';
61
+ if (!id || !id.startsWith('ecfg') || !token)
62
+ return null;
63
+ return {
64
+ baseUrl: `https://edge-config.vercel.com/${id}`,
65
+ token,
66
+ version: url.searchParams.get('version') || '1',
67
+ };
68
+ }
69
+ function parseExternalEdgeConfigURL(value) {
70
+ let url;
71
+ try {
72
+ url = new URL(value);
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ let id = url.searchParams.get('id') || '';
78
+ const token = url.searchParams.get('token') || '';
79
+ const version = url.searchParams.get('version') || '1';
80
+ if (!id || url.pathname.startsWith('/ecfg_')) {
81
+ id = url.pathname.split('/').filter(Boolean)[0] || '';
82
+ }
83
+ if (!id || !token)
84
+ return null;
85
+ url.search = '';
86
+ const base = url.toString().replace(/\/+$/, '');
87
+ return {
88
+ baseUrl: base,
89
+ token,
90
+ version,
91
+ };
92
+ }
@@ -0,0 +1,2 @@
1
+ export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
2
+ export { withGradial, type WithGradialOptions } from './config.js';
@@ -0,0 +1,2 @@
1
+ export { DevRefresh } from './dev-refresh.js';
2
+ export { withGradial } from './config.js';
@@ -1,5 +1,5 @@
1
- import { createClient, get as getEdgeConfigItem } from '@vercel/edge-config';
2
1
  import { NextResponse } from 'next/server';
2
+ import { getUncachedEdgeConfigValue } from './edge-config.js';
3
3
  import { validatePreviewToken } from './preview.js';
4
4
  const RELEASE_HEADER = 'x-gradial-release-id';
5
5
  const ROUTE_HEADER = 'x-gradial-route';
@@ -62,9 +62,7 @@ async function getEdgeConfigJSON(edgeConfig, key) {
62
62
  return value;
63
63
  }
64
64
  async function getEdgeConfigValue(edgeConfig, key) {
65
- return edgeConfig === process.env.EDGE_CONFIG
66
- ? await getEdgeConfigItem(key)
67
- : await createClient(edgeConfig).get(key);
65
+ return getUncachedEdgeConfigValue(edgeConfig, key);
68
66
  }
69
67
  function activeReleaseKey(siteId) {
70
68
  return `activeRelease_${edgeKeySegment(siteId)}`;
@@ -108,7 +106,7 @@ async function releaseFromPreviewToken(request, config) {
108
106
  const token = previewTokenFromRequest(request);
109
107
  if (!token)
110
108
  return { releaseId: '', invalid: false };
111
- const signKey = config.previewSignKey || process.env.GRADIAL_PREVIEW_SIGN_KEY || process.env.HOSTING_PREVIEW_SIGN_KEY || '';
109
+ const signKey = config.previewSignKey || process.env.ACI_PREVIEW_SIGN_KEY || '';
112
110
  if (!signKey)
113
111
  return { releaseId: '', invalid: true };
114
112
  const claims = await validatePreviewToken(token, signKey);
@@ -124,7 +122,7 @@ function previewTokenFromRequest(request) {
124
122
  const queryToken = request.nextUrl.searchParams.get('preview_token');
125
123
  if (queryToken)
126
124
  return queryToken;
127
- const cookieToken = request.cookies.get('bm_preview')?.value || '';
125
+ const cookieToken = request.cookies.get('aci_preview')?.value || '';
128
126
  if (cookieToken)
129
127
  return cookieToken;
130
128
  const auth = request.headers.get('authorization') || '';
@@ -1,9 +1,5 @@
1
- export declare class PageNotFoundError extends Error {
2
- constructor(route: string, cause?: unknown);
3
- }
4
- export declare class FragmentNotFoundError extends Error {
5
- constructor(name: string, cause?: unknown);
6
- }
1
+ import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
2
+ export { FragmentNotFoundError, PageNotFoundError };
7
3
  export interface GradialFetchConfig {
8
4
  siteId?: string;
9
5
  bucket?: string;
@@ -17,24 +13,6 @@ export interface GradialFetchConfig {
17
13
  edgeConfig?: string;
18
14
  deploymentId?: string;
19
15
  }
20
- export interface RouteManifest {
21
- manifestVersion: string;
22
- releaseId: string;
23
- siteId: string;
24
- routes: Record<string, RouteEntry>;
25
- }
26
- export interface RouteEntry {
27
- path: string;
28
- mode: string;
29
- bodyArtifact?: string;
30
- fragments?: Array<{
31
- id: string;
32
- artifactRef?: string;
33
- }>;
34
- cachePolicy?: {
35
- cacheControl?: string;
36
- };
37
- }
38
16
  export interface GradialRenderInput<TPage, TSite extends GradialSiteConfig = GradialSiteConfig> {
39
17
  route: string;
40
18
  domain: string;
@@ -55,4 +33,7 @@ export declare function routeFromNextParams(params?: Promise<{
55
33
  }> | {
56
34
  slug?: string[];
57
35
  }): Promise<string>;
36
+ export declare function generateGradialStaticParams(): Promise<Array<{
37
+ slug?: string[];
38
+ }>>;
58
39
  export declare function resolveReleaseId(config?: GradialFetchConfig): Promise<string>;
@@ -1,55 +1,31 @@
1
- import crypto from 'node:crypto';
2
- import { createClient, get as getEdgeConfigItem } from '@vercel/edge-config';
3
1
  import { headers } from 'next/headers';
2
+ import { FragmentNotFoundError, PageNotFoundError, } from '../content/provider.js';
3
+ import { S3ContentProvider } from '../providers/s3.js';
4
+ import { getUncachedEdgeConfigValue } from './edge-config.js';
4
5
  const RELEASE_HEADER = 'x-gradial-release-id';
5
- export class PageNotFoundError extends Error {
6
- constructor(route, cause) {
7
- super(`Gradial page fragment not found for route ${route}`);
8
- this.name = 'PageNotFoundError';
9
- this.cause = cause;
10
- }
11
- }
12
- export class FragmentNotFoundError extends Error {
13
- constructor(name, cause) {
14
- super(`Gradial fragment not found: ${name}`);
15
- this.name = 'FragmentNotFoundError';
16
- this.cause = cause;
17
- }
18
- }
6
+ export { FragmentNotFoundError, PageNotFoundError };
19
7
  export async function getPage(route = '', config = {}) {
20
- const releaseId = await resolveReleaseId(config);
21
- const key = releaseKey(config, releaseId, 'fragments', 'page', pageRouteSegment(route), 'index.json');
22
- try {
23
- return await getS3JSON(key, config);
24
- }
25
- catch (error) {
26
- throw new PageNotFoundError(route, error);
27
- }
8
+ const provider = await resolveProvider(config);
9
+ return await provider.getPage(route);
28
10
  }
29
11
  export async function getFragment(name, config = {}) {
30
- const releaseId = await resolveReleaseId(config);
31
- const cleanName = cleanPath(name).replace(/\.[^.]+$/, '');
32
- const key = releaseKey(config, releaseId, 'fragments', 'global', `${cleanName}.json`);
33
- try {
34
- return await getS3JSON(key, config);
35
- }
36
- catch (error) {
37
- throw new FragmentNotFoundError(name, error);
38
- }
12
+ const provider = await resolveProvider(config);
13
+ return await provider.getFragment(name);
39
14
  }
40
15
  export async function getRoutes(config = {}) {
41
- const releaseId = await resolveReleaseId(config);
42
- const manifest = await getS3JSON(releaseKey(config, releaseId, 'route-manifest.json'), config);
43
- return Object.keys(manifest.routes || {}).sort();
16
+ const provider = await resolveProvider(config);
17
+ const routes = await provider.listRoutes();
18
+ return routes.map((route) => route.path).sort();
44
19
  }
45
20
  export async function getRenderInput(route = '/', config = {}) {
21
+ const provider = await resolveProvider(config);
46
22
  const [page, siteConfig] = await Promise.all([
47
- getPage(route, config),
48
- getFragment('site', config),
23
+ provider.getPage(route),
24
+ provider.getSiteConfig(),
49
25
  ]);
50
26
  return {
51
27
  route,
52
- domain: siteConfig.domain || 'www.baremetal.local',
28
+ domain: siteConfig.domain || 'www.aci.local',
53
29
  locale: siteConfig.defaultLocale || 'en-us',
54
30
  siteConfig,
55
31
  page,
@@ -63,18 +39,38 @@ export async function routeFromNextParams(params) {
63
39
  return '/';
64
40
  return `/${resolved.slug.join('/')}/`;
65
41
  }
42
+ export async function generateGradialStaticParams() {
43
+ const { FileContentProvider } = await import('../providers/file.js');
44
+ const provider = new FileContentProvider();
45
+ const routes = await provider.listRoutes();
46
+ return routes.map((entry) => ({
47
+ slug: entry.path === '/' ? undefined : entry.path.replace(/^\/|\/$/g, '').split('/'),
48
+ }));
49
+ }
66
50
  export async function resolveReleaseId(config = {}) {
67
51
  if (config.releaseId)
68
52
  return config.releaseId;
69
53
  const headerReleaseId = await releaseIdFromHeaders();
70
54
  if (headerReleaseId)
71
55
  return headerReleaseId;
72
- if (process.env.GRADIAL_RELEASE_ID)
73
- return process.env.GRADIAL_RELEASE_ID;
56
+ if (process.env.ACI_RELEASE_ID)
57
+ return process.env.ACI_RELEASE_ID;
74
58
  const activeReleaseId = await activeReleaseFromEdgeConfig(config);
75
59
  if (activeReleaseId)
76
60
  return activeReleaseId;
77
- throw new Error('Gradial release ID is not available from headers, GRADIAL_RELEASE_ID, or Edge Config');
61
+ throw new Error('ACI release ID is not available from headers, ACI_RELEASE_ID, or Edge Config');
62
+ }
63
+ async function resolveProvider(config) {
64
+ const releaseId = await resolveReleaseId(config);
65
+ return new S3ContentProvider({
66
+ bucket: config.bucket,
67
+ keyPrefix: releaseKey(config, releaseId),
68
+ region: config.region,
69
+ accessKeyId: config.accessKeyId,
70
+ secretAccessKey: config.secretAccessKey,
71
+ sessionToken: config.sessionToken,
72
+ endpoint: config.endpoint,
73
+ });
78
74
  }
79
75
  async function releaseIdFromHeaders() {
80
76
  try {
@@ -87,7 +83,7 @@ async function releaseIdFromHeaders() {
87
83
  }
88
84
  async function activeReleaseFromEdgeConfig(config) {
89
85
  const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
90
- const siteId = config.siteId || process.env.GRADIAL_SITE_ID || '';
86
+ const siteId = config.siteId || process.env.ACI_SITE_ID || '';
91
87
  if (!edgeConfig || !siteId)
92
88
  return '';
93
89
  const active = await activeReleasePointer(edgeConfig, siteId);
@@ -116,106 +112,10 @@ async function getEdgeConfigJSON(edgeConfig, key) {
116
112
  return value;
117
113
  }
118
114
  async function getEdgeConfigValue(edgeConfig, key) {
119
- return edgeConfig === process.env.EDGE_CONFIG
120
- ? await getEdgeConfigItem(key)
121
- : await createClient(edgeConfig).get(key);
122
- }
123
- async function getS3JSON(key, config) {
124
- const resolved = resolveS3Config(config);
125
- const url = s3ObjectURL(resolved, key);
126
- const signedHeaders = signedS3Headers('GET', url, resolved);
127
- const res = await fetch(url, {
128
- method: 'GET',
129
- headers: signedHeaders,
130
- cache: 'no-store',
131
- });
132
- if (!res.ok) {
133
- throw new Error(`S3 GET ${key} failed with status ${res.status}: ${await res.text()}`);
134
- }
135
- return (await res.json());
136
- }
137
- function resolveS3Config(config) {
138
- const resolved = {
139
- siteId: config.siteId || process.env.GRADIAL_SITE_ID || '',
140
- bucket: config.bucket || process.env.GRADIAL_S3_BUCKET || '',
141
- keyPrefix: config.keyPrefix || process.env.GRADIAL_S3_KEY_PREFIX || '',
142
- region: config.region || process.env.AWS_REGION || 'us-east-1',
143
- accessKeyId: config.accessKeyId || process.env.AWS_ACCESS_KEY_ID || '',
144
- secretAccessKey: config.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY || '',
145
- sessionToken: config.sessionToken || process.env.AWS_SESSION_TOKEN || '',
146
- endpoint: trimSlash(config.endpoint || process.env.GRADIAL_S3_ENDPOINT || ''),
147
- };
148
- for (const key of ['bucket', 'accessKeyId', 'secretAccessKey']) {
149
- if (!resolved[key])
150
- throw new Error(`Gradial S3 config missing ${key}`);
151
- }
152
- return resolved;
153
- }
154
- function releaseKey(config, releaseId, ...parts) {
155
- return joinKey(config.keyPrefix || process.env.GRADIAL_S3_KEY_PREFIX || '', 'releases', releaseId, ...parts);
156
- }
157
- function s3ObjectURL(config, key) {
158
- const encodedKey = encodeKey(key);
159
- if (config.endpoint) {
160
- return new URL(`${config.endpoint}/${encodeURIComponent(config.bucket)}${encodedKey}`);
161
- }
162
- return new URL(`https://${config.bucket}.s3.${config.region}.amazonaws.com${encodedKey}`);
163
- }
164
- function signedS3Headers(method, url, config) {
165
- const now = new Date();
166
- const amzDate = amzTimestamp(now);
167
- const dateStamp = amzDate.slice(0, 8);
168
- const payloadHash = sha256Hex('');
169
- const headers = new Headers({
170
- host: url.host,
171
- 'x-amz-content-sha256': payloadHash,
172
- 'x-amz-date': amzDate,
173
- });
174
- if (config.sessionToken)
175
- headers.set('x-amz-security-token', config.sessionToken);
176
- const signedHeaderNames = Array.from(headers.keys()).sort();
177
- const canonicalHeaders = signedHeaderNames.map((key) => `${key}:${headers.get(key)?.trim()}\n`).join('');
178
- const canonicalRequest = [
179
- method,
180
- url.pathname,
181
- url.searchParams.toString(),
182
- canonicalHeaders,
183
- signedHeaderNames.join(';'),
184
- payloadHash,
185
- ].join('\n');
186
- const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`;
187
- const stringToSign = [
188
- 'AWS4-HMAC-SHA256',
189
- amzDate,
190
- credentialScope,
191
- sha256Hex(canonicalRequest),
192
- ].join('\n');
193
- const signingKey = awsSigningKey(config.secretAccessKey, dateStamp, config.region, 's3');
194
- const signature = hmacHex(signingKey, stringToSign);
195
- headers.set('authorization', `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames.join(';')}, Signature=${signature}`);
196
- return headers;
197
- }
198
- function awsSigningKey(secret, dateStamp, region, service) {
199
- const kDate = hmac(Buffer.from(`AWS4${secret}`, 'utf8'), dateStamp);
200
- const kRegion = hmac(kDate, region);
201
- const kService = hmac(kRegion, service);
202
- return hmac(kService, 'aws4_request');
203
- }
204
- function amzTimestamp(date) {
205
- return date.toISOString().replace(/[:-]|\.\d{3}/g, '');
206
- }
207
- function hmac(key, value) {
208
- return crypto.createHmac('sha256', key).update(value, 'utf8').digest();
209
- }
210
- function hmacHex(key, value) {
211
- return crypto.createHmac('sha256', key).update(value, 'utf8').digest('hex');
115
+ return getUncachedEdgeConfigValue(edgeConfig, key);
212
116
  }
213
- function sha256Hex(value) {
214
- return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
215
- }
216
- function pageRouteSegment(route) {
217
- const clean = cleanPath(route);
218
- return clean === '' ? 'index' : clean;
117
+ function releaseKey(config, releaseId) {
118
+ return joinKey(config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || '', 'releases', releaseId);
219
119
  }
220
120
  function activeReleaseKey(siteId) {
221
121
  return `activeRelease_${edgeKeySegment(siteId)}`;
@@ -255,15 +155,10 @@ function clampRelease(activeReleaseId, activeSequence, compatibility) {
255
155
  function edgeKeySegment(value) {
256
156
  return value.replace(/[^A-Za-z0-9_-]/g, '_');
257
157
  }
258
- function cleanPath(value) {
259
- return value.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
260
- }
261
158
  function joinKey(...parts) {
262
- return parts.map(cleanPath).filter(Boolean).join('/');
263
- }
264
- function encodeKey(key) {
265
- return `/${key.split('/').map(encodeURIComponent).join('/')}`;
266
- }
267
- function trimSlash(value) {
268
- return value.replace(/\/+$/, '');
159
+ return parts
160
+ .flatMap((part) => part.split('/'))
161
+ .map((part) => part.trim())
162
+ .filter(Boolean)
163
+ .join('/');
269
164
  }
@@ -1,18 +1,12 @@
1
- import { type ContentPathOptions } from '../content/routes.js';
2
- import { type ContentProvider, type RenderInputOptions } from '../content/provider.js';
3
- import type { KernelPage, KernelRouteMetadata, KernelSiteConfig, RenderInput } from '../content/types.js';
4
- export interface FileContentProviderOptions extends ContentPathOptions {
5
- parseSiteConfig?: (value: unknown, source: string) => KernelSiteConfig;
6
- parsePage?: (value: unknown, source: string) => KernelPage;
1
+ import { type CompiledManifest, type ContentProvider, type RouteEntry } from '../content/provider.js';
2
+ export declare class FileContentProvider implements ContentProvider {
3
+ #private;
4
+ readonly root: string;
5
+ constructor(root?: string);
6
+ listRoutes(): Promise<RouteEntry[]>;
7
+ getSiteConfig<T = unknown>(): Promise<T>;
8
+ getPage<T = unknown>(route: string): Promise<T>;
9
+ getFragment<T = unknown>(id: string): Promise<T>;
10
+ manifest(): Promise<CompiledManifest>;
11
+ private readJSON;
7
12
  }
8
- export declare class FileContentProvider<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig> implements ContentProvider<TPage, TSiteConfig> {
9
- readonly options: FileContentProviderOptions;
10
- constructor(options?: FileContentProviderOptions);
11
- loadSiteConfig(): Promise<TSiteConfig>;
12
- loadPage(route?: string): Promise<TPage | null>;
13
- listRoutes(): Promise<string[]>;
14
- listPublishedRoutes(): Promise<string[]>;
15
- resolveRouteMetadata(route?: string): Promise<KernelRouteMetadata>;
16
- loadRenderInput(route?: string, options?: RenderInputOptions): Promise<RenderInput<TPage, TSiteConfig>>;
17
- }
18
- export declare function createFileContentProvider<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig>(options?: FileContentProviderOptions): FileContentProvider<TPage, TSiteConfig>;
@@ -1,91 +1,57 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { localeRoot, normalizeRoute, routeFromRelativePagePath, routeToPagePath, siteRoot } from '../content/routes.js';
4
- import { loadRenderInput, routeMetadataForContent } from '../content/provider.js';
5
- import { parseKernelPage, parseKernelSiteConfig } from '../content/validation.js';
3
+ import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
4
+ import { normalizeRoute } from '../content/routes.js';
6
5
  export class FileContentProvider {
7
- options;
8
- constructor(options = {}) {
9
- this.options = options;
6
+ root;
7
+ #manifest;
8
+ constructor(root) {
9
+ const resolved = root
10
+ || process.env.ACI_CONTENT_ROOT
11
+ || '.aci/compiled';
12
+ this.root = path.resolve(process.cwd(), resolved);
10
13
  }
11
- async loadSiteConfig() {
12
- const source = path.join(siteRoot(this.options), 'site.json');
13
- const parsed = await readJSON(source);
14
- const value = this.options.parseSiteConfig
15
- ? this.options.parseSiteConfig(parsed, source)
16
- : parseKernelSiteConfig(parsed, source);
17
- return value;
18
- }
19
- async loadPage(route = '/') {
20
- const source = routeToPagePath(route, this.options);
14
+ async listRoutes() {
15
+ const manifest = await this.manifest();
16
+ return Object.values(manifest.routes).sort((a, b) => a.path.localeCompare(b.path));
17
+ }
18
+ async getSiteConfig() {
19
+ const manifest = await this.manifest();
20
+ return await this.readJSON(manifest.siteConfigRef);
21
+ }
22
+ async getPage(route) {
23
+ const manifest = await this.manifest();
24
+ const normalized = normalizeRoute(route);
25
+ const entry = Object.values(manifest.routes).find((candidate) => normalizeRoute(candidate.path) === normalized);
26
+ if (!entry) {
27
+ throw new PageNotFoundError(route);
28
+ }
21
29
  try {
22
- const parsed = await readJSON(source);
23
- const value = this.options.parsePage
24
- ? this.options.parsePage(parsed, source)
25
- : parseKernelPage(parsed, source);
26
- return value;
30
+ return await this.readJSON(entry.payloadRef);
27
31
  }
28
32
  catch (error) {
29
- if (isNotFound(error)) {
30
- return null;
31
- }
32
- throw error;
33
+ throw new PageNotFoundError(route, error);
33
34
  }
34
35
  }
35
- async listRoutes() {
36
- const files = await walk(localeRoot(this.options));
37
- return files
38
- .filter((entry) => entry.endsWith('/_index.json'))
39
- .map((entry) => routeFromRelativePagePath(entry))
40
- .sort((left, right) => left.localeCompare(right));
41
- }
42
- async listPublishedRoutes() {
43
- const routes = await this.listRoutes();
44
- const pages = await Promise.all(routes.map(async (route) => ({
45
- route,
46
- page: await this.loadPage(route)
47
- })));
48
- return pages
49
- .filter((entry) => entry.page?.status === 'published')
50
- .map((entry) => entry.route);
51
- }
52
- async resolveRouteMetadata(route = '/') {
53
- const normalizedRoute = normalizeRoute(route);
54
- const [siteConfig, page] = await Promise.all([
55
- this.loadSiteConfig(),
56
- this.loadPage(normalizedRoute)
57
- ]);
58
- return routeMetadataForContent(siteConfig, page);
59
- }
60
- async loadRenderInput(route = '/', options = {}) {
61
- return loadRenderInput(this, route, options);
62
- }
63
- }
64
- export function createFileContentProvider(options = {}) {
65
- return new FileContentProvider(options);
66
- }
67
- async function readJSON(source) {
68
- const raw = await fs.readFile(source, 'utf8');
69
- return JSON.parse(raw);
70
- }
71
- async function walk(directory, prefix = '') {
72
- const entries = await fs.readdir(directory, { withFileTypes: true });
73
- const files = [];
74
- for (const entry of entries) {
75
- const relative = prefix ? path.join(prefix, entry.name) : entry.name;
76
- const absolute = path.join(directory, entry.name);
77
- if (entry.isDirectory()) {
78
- files.push(...await walk(absolute, relative));
36
+ async getFragment(id) {
37
+ const manifest = await this.manifest();
38
+ const entry = manifest.fragments[id];
39
+ if (!entry) {
40
+ throw new FragmentNotFoundError(id);
41
+ }
42
+ try {
43
+ return await this.readJSON(entry.payloadRef);
79
44
  }
80
- else if (entry.isFile()) {
81
- files.push(relative);
45
+ catch (error) {
46
+ throw new FragmentNotFoundError(id, error);
82
47
  }
83
48
  }
84
- return files;
85
- }
86
- function isNotFound(error) {
87
- return typeof error === 'object'
88
- && error !== null
89
- && 'code' in error
90
- && error.code === 'ENOENT';
49
+ async manifest() {
50
+ this.#manifest ??= this.readJSON('manifest.json');
51
+ return await this.#manifest;
52
+ }
53
+ async readJSON(key) {
54
+ const raw = await fs.readFile(path.join(this.root, key), 'utf8');
55
+ return JSON.parse(raw);
56
+ }
91
57
  }
@@ -0,0 +1,24 @@
1
+ import { type CompiledManifest, type ContentProvider, type RouteEntry } from '../content/provider.js';
2
+ export interface S3ContentProviderConfig {
3
+ bucket?: string;
4
+ keyPrefix?: string;
5
+ region?: string;
6
+ accessKeyId?: string;
7
+ secretAccessKey?: string;
8
+ sessionToken?: string;
9
+ endpoint?: string;
10
+ }
11
+ type ResolvedS3Config = Required<S3ContentProviderConfig>;
12
+ export declare class S3ContentProvider implements ContentProvider {
13
+ #private;
14
+ readonly config: ResolvedS3Config;
15
+ constructor(config?: S3ContentProviderConfig);
16
+ listRoutes(): Promise<RouteEntry[]>;
17
+ getSiteConfig<T = unknown>(): Promise<T>;
18
+ getPage<T = unknown>(route: string): Promise<T>;
19
+ getFragment<T = unknown>(id: string): Promise<T>;
20
+ manifest(): Promise<CompiledManifest>;
21
+ private getJSON;
22
+ }
23
+ export declare function getS3JSON<T>(key: string, config: ResolvedS3Config): Promise<T>;
24
+ export {};