@gradial/aci 0.1.19 → 0.1.20-preview.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.
- package/README.md +22 -0
- package/bin/aci +0 -0
- package/dist/astro/index.d.ts +2 -2
- package/dist/astro/index.js +5 -1
- package/dist/astro/preview.d.ts +5 -0
- package/dist/astro/preview.js +33 -0
- package/dist/content/cache.test.d.ts +1 -0
- package/dist/content/cache.test.js +86 -0
- package/dist/define-data-model.d.ts +2 -0
- package/dist/define-data-model.js +3 -0
- package/dist/dev/index.d.ts +12 -0
- package/dist/dev/index.js +84 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/next/config.d.ts +1 -1
- package/dist/next/config.js +13 -1
- package/dist/next/content-watch.d.ts +11 -0
- package/dist/next/content-watch.js +106 -0
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +2 -2
- package/dist/next/middleware.js +34 -50
- package/dist/next/page.d.ts +40 -0
- package/dist/next/page.js +59 -0
- package/dist/next/preview-mode.js +4 -8
- package/dist/next/server.js +0 -6
- package/dist/preview/core.d.ts +41 -0
- package/dist/preview/core.js +116 -0
- package/dist/sveltekit/index.d.ts +2 -1
- package/dist/sveltekit/index.js +5 -1
- package/dist/sveltekit/preview.d.ts +5 -0
- package/dist/sveltekit/preview.js +33 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/data.d.ts +19 -0
- package/dist/types/data.js +11 -0
- package/dist/types/image.d.ts +1 -1
- package/dist/types/image.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +24 -1
- package/src/cli/compile-registry.mjs +40 -1
- package/src/types/config.ts +1 -0
- package/src/types/data.ts +47 -0
- package/src/types/image.ts +2 -2
- package/src/types/index.ts +1 -0
- package/dist/render.d.ts +0 -14
- package/dist/render.js +0 -33
package/README.md
CHANGED
|
@@ -5,6 +5,28 @@ installing `@gradial/aci`, Zod 4, and TypeScript tooling. The
|
|
|
5
5
|
SDK exposes component and layout helpers, renderer types, compiler tooling,
|
|
6
6
|
content provider helpers, and framework-specific subpaths from one package.
|
|
7
7
|
|
|
8
|
+
## Getting Started
|
|
9
|
+
|
|
10
|
+
The fastest way to create a new ACI site is via the scaffolding script:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx tsx scripts/create-site.ts --framework astro --name my-site --dir ./my-site
|
|
14
|
+
cd my-site
|
|
15
|
+
npm run dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Available frameworks: `astro`, `nextjs`, `sveltekit`.
|
|
19
|
+
|
|
20
|
+
Generated starters include:
|
|
21
|
+
- `.aci/docs/` — Understanding ACI, running, and testing guides
|
|
22
|
+
- `.aci/guides/` — Component development, design system, content authoring
|
|
23
|
+
- `.aci/migration/` — Migrating existing sites to ACI
|
|
24
|
+
- Design system tokens, ESLint rules, and demo content
|
|
25
|
+
|
|
26
|
+
See the [templates directory](templates/) for the canonical starter source.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
8
30
|
```bash
|
|
9
31
|
npm install @gradial/aci zod@^4
|
|
10
32
|
npm install -D typescript tsx
|
package/bin/aci
CHANGED
|
Binary file
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -33,9 +33,9 @@ export declare function getGradialStaticPaths(): Promise<Array<{
|
|
|
33
33
|
};
|
|
34
34
|
}>>;
|
|
35
35
|
export declare function getPageRuntimeRenderInput(request: Request): RenderInput | null;
|
|
36
|
+
export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, type PreviewContext, type PreviewCookie, } from './preview.js';
|
|
36
37
|
export interface GradialAstroOptions extends GradialContentWatchOptions {
|
|
37
38
|
/** Path to compiled content root. Defaults to ACI_CONTENT_ROOT env or '.aci/compiled' */
|
|
38
39
|
compiledRoot?: string;
|
|
39
40
|
}
|
|
40
|
-
export declare function
|
|
41
|
-
export {};
|
|
41
|
+
export declare function withGradialAci(options?: GradialAstroOptions): AstroIntegrationLike;
|
package/dist/astro/index.js
CHANGED
|
@@ -31,7 +31,11 @@ export async function getGradialStaticPaths() {
|
|
|
31
31
|
export function getPageRuntimeRenderInput(request) {
|
|
32
32
|
return getPendingRenderInputFromHeaders(request.headers);
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Preview support
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, } from './preview.js';
|
|
38
|
+
export function withGradialAci(options = {}) {
|
|
35
39
|
return {
|
|
36
40
|
name: 'gradial-astro',
|
|
37
41
|
hooks: {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type PreviewContext, type PreviewCookie } from '../preview/core.js';
|
|
2
|
+
export declare function getPreviewMode(request: Request, siteId: string): Promise<PreviewContext>;
|
|
3
|
+
export declare function createPreviewBannerHTML(releaseId: string): string;
|
|
4
|
+
export declare function createPreviewCookies(token: string, releaseId: string): PreviewCookie[];
|
|
5
|
+
export type { PreviewContext, PreviewCookie };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { extractPreviewToken, resolvePreviewContext, createPreviewBannerHTML as coreCreatePreviewBannerHTML, createPreviewCookies as coreCreatePreviewCookies, previewSignKey, } from '../preview/core.js';
|
|
2
|
+
export async function getPreviewMode(request, siteId) {
|
|
3
|
+
const url = new URL(request.url);
|
|
4
|
+
const token = extractPreviewToken({
|
|
5
|
+
query: url.searchParams,
|
|
6
|
+
cookies: parseCookieHeader(request.headers.get('cookie') || ''),
|
|
7
|
+
authorization: request.headers.get('authorization') || undefined,
|
|
8
|
+
});
|
|
9
|
+
if (!token)
|
|
10
|
+
return { isPreview: false };
|
|
11
|
+
return resolvePreviewContext({ token, siteId, signKey: previewSignKey() });
|
|
12
|
+
}
|
|
13
|
+
export function createPreviewBannerHTML(releaseId) {
|
|
14
|
+
return coreCreatePreviewBannerHTML(releaseId);
|
|
15
|
+
}
|
|
16
|
+
export function createPreviewCookies(token, releaseId) {
|
|
17
|
+
return coreCreatePreviewCookies(token, releaseId);
|
|
18
|
+
}
|
|
19
|
+
function parseCookieHeader(header) {
|
|
20
|
+
const cookies = new Map();
|
|
21
|
+
for (const part of header.split(';')) {
|
|
22
|
+
const trimmed = part.trim();
|
|
23
|
+
if (!trimmed)
|
|
24
|
+
continue;
|
|
25
|
+
const idx = trimmed.indexOf('=');
|
|
26
|
+
if (idx < 0)
|
|
27
|
+
continue;
|
|
28
|
+
const name = trimmed.slice(0, idx).trim();
|
|
29
|
+
const value = trimmed.slice(idx + 1).trim();
|
|
30
|
+
cookies.set(name, decodeURIComponent(value));
|
|
31
|
+
}
|
|
32
|
+
return cookies;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getCachedSiteConfig, getCachedManifest, clearProviderCache, getCacheStats, resetCacheStats } from './cache.js';
|
|
3
|
+
describe('Content Cache', () => {
|
|
4
|
+
let mockProvider;
|
|
5
|
+
let getSiteConfigCallCount = 0;
|
|
6
|
+
let manifestCallCount = 0;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
getSiteConfigCallCount = 0;
|
|
9
|
+
manifestCallCount = 0;
|
|
10
|
+
resetCacheStats();
|
|
11
|
+
mockProvider = {
|
|
12
|
+
getSiteConfig: async () => {
|
|
13
|
+
getSiteConfigCallCount++;
|
|
14
|
+
return { title: 'Test Site' };
|
|
15
|
+
},
|
|
16
|
+
manifest: async () => {
|
|
17
|
+
manifestCallCount++;
|
|
18
|
+
return {
|
|
19
|
+
routes: {},
|
|
20
|
+
fragments: {},
|
|
21
|
+
siteConfigRef: 'site.json'
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
getPage: async () => ({}),
|
|
25
|
+
getFragment: async () => ({}),
|
|
26
|
+
listRoutes: async () => []
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
it('should cache siteConfig across multiple calls', async () => {
|
|
30
|
+
const result1 = await getCachedSiteConfig(mockProvider);
|
|
31
|
+
const result2 = await getCachedSiteConfig(mockProvider);
|
|
32
|
+
expect(getSiteConfigCallCount).toBe(1); // Only called once
|
|
33
|
+
expect(result1).toEqual(result2);
|
|
34
|
+
const stats = getCacheStats();
|
|
35
|
+
expect(stats.siteConfig.hits).toBe(1);
|
|
36
|
+
expect(stats.siteConfig.misses).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
it('should cache manifest across multiple calls', async () => {
|
|
39
|
+
const result1 = await getCachedManifest(mockProvider);
|
|
40
|
+
const result2 = await getCachedManifest(mockProvider);
|
|
41
|
+
expect(manifestCallCount).toBe(1); // Only called once
|
|
42
|
+
expect(result1).toEqual(result2);
|
|
43
|
+
const stats = getCacheStats();
|
|
44
|
+
expect(stats.manifest.hits).toBe(1);
|
|
45
|
+
expect(stats.manifest.misses).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
it('should clear cache for a provider', async () => {
|
|
48
|
+
await getCachedSiteConfig(mockProvider);
|
|
49
|
+
expect(getSiteConfigCallCount).toBe(1);
|
|
50
|
+
clearProviderCache(mockProvider);
|
|
51
|
+
await getCachedSiteConfig(mockProvider);
|
|
52
|
+
expect(getSiteConfigCallCount).toBe(2); // Called again after clear
|
|
53
|
+
const stats = getCacheStats();
|
|
54
|
+
expect(stats.siteConfig.misses).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
it('should not cache rejected promises', async () => {
|
|
57
|
+
const errorProvider = {
|
|
58
|
+
getSiteConfig: async () => {
|
|
59
|
+
getSiteConfigCallCount++;
|
|
60
|
+
throw new Error('S3 timeout');
|
|
61
|
+
},
|
|
62
|
+
getPage: async () => ({}),
|
|
63
|
+
getFragment: async () => ({}),
|
|
64
|
+
listRoutes: async () => []
|
|
65
|
+
};
|
|
66
|
+
await expect(getCachedSiteConfig(errorProvider)).rejects.toThrow('S3 timeout');
|
|
67
|
+
expect(getSiteConfigCallCount).toBe(1);
|
|
68
|
+
// Second call should retry (not cached)
|
|
69
|
+
await expect(getCachedSiteConfig(errorProvider)).rejects.toThrow('S3 timeout');
|
|
70
|
+
expect(getSiteConfigCallCount).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
it('should cache separately per provider instance', async () => {
|
|
73
|
+
const provider2 = {
|
|
74
|
+
getSiteConfig: async () => ({ title: 'Site 2' }),
|
|
75
|
+
getPage: async () => ({}),
|
|
76
|
+
getFragment: async () => ({}),
|
|
77
|
+
listRoutes: async () => []
|
|
78
|
+
};
|
|
79
|
+
const result1 = await getCachedSiteConfig(mockProvider);
|
|
80
|
+
const result2 = await getCachedSiteConfig(provider2);
|
|
81
|
+
expect(result1).toEqual({ title: 'Test Site' });
|
|
82
|
+
expect(result2).toEqual({ title: 'Site 2' });
|
|
83
|
+
const stats = getCacheStats();
|
|
84
|
+
expect(stats.siteConfig.misses).toBe(2); // Both are cache misses
|
|
85
|
+
});
|
|
86
|
+
});
|
package/dist/dev/index.d.ts
CHANGED
|
@@ -7,10 +7,21 @@ declare global {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
export interface GradialContentWatchOptions extends DevRefreshOptions {
|
|
10
|
+
/** Source content root to watch. Defaults to ACI_CONTENT_SOURCE env or '.content' */
|
|
10
11
|
contentRoot?: string;
|
|
12
|
+
/** Compiled content output root. Defaults to ACI_CONTENT_ROOT env or '.aci/compiled' */
|
|
11
13
|
compiledRoot?: string;
|
|
14
|
+
/** Path to the ACI CLI binary. Defaults to ACI_BIN env or 'aci' */
|
|
15
|
+
aciBin?: string;
|
|
16
|
+
/** Disable content watching entirely. Defaults to ACI_DISABLE_CONTENT_WATCH env */
|
|
12
17
|
enabled?: boolean;
|
|
13
18
|
}
|
|
19
|
+
interface ViteModuleGraph {
|
|
20
|
+
idToModuleMap: Map<unknown, {
|
|
21
|
+
id: string | null;
|
|
22
|
+
}>;
|
|
23
|
+
invalidateModule(mod: unknown): void;
|
|
24
|
+
}
|
|
14
25
|
interface ViteDevServerLike {
|
|
15
26
|
middlewares?: {
|
|
16
27
|
use(path: string, handler: (req: IncomingMessage, res: ServerResponse, next: () => void) => void): void;
|
|
@@ -22,6 +33,7 @@ interface ViteDevServerLike {
|
|
|
22
33
|
ws: {
|
|
23
34
|
send(payload: unknown): void;
|
|
24
35
|
};
|
|
36
|
+
moduleGraph?: ViteModuleGraph;
|
|
25
37
|
}
|
|
26
38
|
export interface VitePluginLike {
|
|
27
39
|
name: string;
|
package/dist/dev/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { compiledContentRoot } from '../content/routes.js';
|
|
4
5
|
import { DEFAULT_DEV_REFRESH_PATH, DEFAULT_DEV_REFRESH_PORT, } from './browser.js';
|
|
@@ -48,23 +49,97 @@ export function devRefreshScript(options = {}) {
|
|
|
48
49
|
export function devRefreshScriptTag(options = {}) {
|
|
49
50
|
return `<script>${devRefreshScript(options)}</script>`;
|
|
50
51
|
}
|
|
52
|
+
function sourceContentRoot(options) {
|
|
53
|
+
return path.resolve(options.contentRoot ||
|
|
54
|
+
process.env.ACI_CONTENT_SOURCE ||
|
|
55
|
+
'./.content');
|
|
56
|
+
}
|
|
57
|
+
function resolveAciBin(options) {
|
|
58
|
+
return options.aciBin || process.env.ACI_BIN || 'aci';
|
|
59
|
+
}
|
|
60
|
+
function recompileContent(options, reason) {
|
|
61
|
+
const siteDir = process.cwd();
|
|
62
|
+
const src = sourceContentRoot(options);
|
|
63
|
+
const out = path.resolve(options.compiledRoot ||
|
|
64
|
+
process.env.ACI_CONTENT_ROOT ||
|
|
65
|
+
'.aci/compiled');
|
|
66
|
+
const bin = resolveAciBin(options);
|
|
67
|
+
console.log(`[aci] compiling content (${reason})`);
|
|
68
|
+
const result = spawnSync(bin, [
|
|
69
|
+
'-s', siteDir,
|
|
70
|
+
'build',
|
|
71
|
+
'--content', src,
|
|
72
|
+
'--out', out,
|
|
73
|
+
'--skip-code',
|
|
74
|
+
], {
|
|
75
|
+
cwd: siteDir,
|
|
76
|
+
stdio: 'inherit',
|
|
77
|
+
env: process.env,
|
|
78
|
+
});
|
|
79
|
+
if (result.status !== 0) {
|
|
80
|
+
console.error(`[aci] content compile failed with exit code ${result.status ?? 'unknown'}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
51
85
|
export function gradialContentWatchPlugin(options = {}) {
|
|
52
86
|
const enabled = options.enabled ?? process.env.ACI_DISABLE_CONTENT_WATCH !== '1';
|
|
87
|
+
if (!enabled) {
|
|
88
|
+
return {
|
|
89
|
+
name: 'gradial-content-watch-disabled',
|
|
90
|
+
apply: 'serve',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
let timer;
|
|
94
|
+
let running = false;
|
|
95
|
+
let queued = false;
|
|
96
|
+
function rebuild(server, reason) {
|
|
97
|
+
if (running) {
|
|
98
|
+
queued = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
running = true;
|
|
102
|
+
const success = recompileContent(options, reason);
|
|
103
|
+
running = false;
|
|
104
|
+
if (success) {
|
|
105
|
+
if (server.moduleGraph) {
|
|
106
|
+
for (const mod of server.moduleGraph.idToModuleMap.values()) {
|
|
107
|
+
server.moduleGraph.invalidateModule(mod);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
server.ws.send({ type: 'full-reload' });
|
|
111
|
+
}
|
|
112
|
+
if (queued) {
|
|
113
|
+
queued = false;
|
|
114
|
+
rebuild(server, 'queued change');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
53
117
|
return {
|
|
54
118
|
name: 'gradial-content-watch',
|
|
55
119
|
apply: 'serve',
|
|
56
120
|
configureServer(server) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
server.watcher.add(
|
|
62
|
-
server.watcher.
|
|
121
|
+
const src = sourceContentRoot(options);
|
|
122
|
+
const compiled = path.resolve(options.compiledRoot ||
|
|
123
|
+
process.env.ACI_CONTENT_ROOT ||
|
|
124
|
+
compiledContentRoot());
|
|
125
|
+
server.watcher.add(src);
|
|
126
|
+
server.watcher.add(compiled);
|
|
127
|
+
rebuild(server, 'startup');
|
|
128
|
+
server.watcher.on('all', (_event, filePath) => {
|
|
63
129
|
const changed = path.resolve(filePath);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
130
|
+
const isSource = changed.startsWith(src + path.sep);
|
|
131
|
+
const isCompiled = changed.startsWith(compiled + path.sep);
|
|
132
|
+
if (!isSource && !isCompiled)
|
|
133
|
+
return;
|
|
134
|
+
if (isCompiled) {
|
|
135
|
+
server.ws.send({ type: 'full-reload' });
|
|
136
|
+
return;
|
|
67
137
|
}
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
timer = setTimeout(() => {
|
|
140
|
+
const relative = path.relative(src, changed);
|
|
141
|
+
rebuild(server, relative);
|
|
142
|
+
}, 100);
|
|
68
143
|
});
|
|
69
144
|
},
|
|
70
145
|
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/next/config.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export interface WithGradialOptions {
|
|
|
3
3
|
/** Asset rewrite prefix. Defaults to '/.gradial-dam' */
|
|
4
4
|
assetPrefix?: string;
|
|
5
5
|
}
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function withGradialAci(nextConfig?: NextConfig, options?: WithGradialOptions): NextConfig;
|
package/dist/next/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { createContentWatchWebpackPlugin } from './content-watch.js';
|
|
2
|
+
export function withGradialAci(nextConfig = {}, options = {}) {
|
|
2
3
|
const assetPrefix = options.assetPrefix || '/.gradial-dam';
|
|
3
4
|
const userRewrites = nextConfig.rewrites;
|
|
5
|
+
const userWebpack = nextConfig.webpack;
|
|
4
6
|
return {
|
|
5
7
|
...nextConfig,
|
|
6
8
|
rewrites: async () => {
|
|
@@ -21,5 +23,15 @@ export function withGradial(nextConfig = {}, options = {}) {
|
|
|
21
23
|
beforeFiles: [...gradialRewrites, ...(userResult.beforeFiles ?? [])],
|
|
22
24
|
};
|
|
23
25
|
},
|
|
26
|
+
webpack: (config, context) => {
|
|
27
|
+
if (context?.isServer && process.env.NODE_ENV !== 'production') {
|
|
28
|
+
if (!config.plugins)
|
|
29
|
+
config.plugins = [];
|
|
30
|
+
config.plugins.push(createContentWatchWebpackPlugin());
|
|
31
|
+
}
|
|
32
|
+
return typeof userWebpack === 'function'
|
|
33
|
+
? userWebpack(config, context)
|
|
34
|
+
: config;
|
|
35
|
+
},
|
|
24
36
|
};
|
|
25
37
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface ContentWatchWebpackPlugin {
|
|
2
|
+
apply(compiler: {
|
|
3
|
+
hooks: {
|
|
4
|
+
[key: string]: {
|
|
5
|
+
tap: (name: string, fn: () => void) => void;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
}): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function createContentWatchWebpackPlugin(): ContentWatchWebpackPlugin;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { WebSocketServer } from 'ws';
|
|
6
|
+
import { DEFAULT_DEV_REFRESH_PATH, DEFAULT_DEV_REFRESH_PORT, } from '../dev/browser.js';
|
|
7
|
+
function sourceContentRoot() {
|
|
8
|
+
return path.resolve(process.env.ACI_CONTENT_SOURCE || './.content');
|
|
9
|
+
}
|
|
10
|
+
function compiledContentRoot() {
|
|
11
|
+
return path.resolve(process.env.ACI_CONTENT_ROOT || '.aci/compiled');
|
|
12
|
+
}
|
|
13
|
+
function resolveAciBin() {
|
|
14
|
+
return process.env.ACI_BIN || 'aci';
|
|
15
|
+
}
|
|
16
|
+
function recompileContent(reason) {
|
|
17
|
+
const siteDir = process.cwd();
|
|
18
|
+
const src = sourceContentRoot();
|
|
19
|
+
const out = compiledContentRoot();
|
|
20
|
+
const bin = resolveAciBin();
|
|
21
|
+
console.log(`[aci] compiling content (${reason})`);
|
|
22
|
+
const result = spawnSync(bin, [
|
|
23
|
+
'-s', siteDir,
|
|
24
|
+
'build',
|
|
25
|
+
'--content', src,
|
|
26
|
+
'--out', out,
|
|
27
|
+
'--skip-code',
|
|
28
|
+
], {
|
|
29
|
+
cwd: siteDir,
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
env: process.env,
|
|
32
|
+
});
|
|
33
|
+
if (result.status !== 0) {
|
|
34
|
+
console.error(`[aci] content compile failed with exit code ${result.status ?? 'unknown'}`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
let watcherStarted = false;
|
|
40
|
+
function startContentWatcher() {
|
|
41
|
+
if (watcherStarted)
|
|
42
|
+
return;
|
|
43
|
+
watcherStarted = true;
|
|
44
|
+
const enabled = process.env.ACI_DISABLE_CONTENT_WATCH !== '1';
|
|
45
|
+
if (!enabled)
|
|
46
|
+
return;
|
|
47
|
+
const src = sourceContentRoot();
|
|
48
|
+
const wsPort = Number(process.env.ACI_DEV_WS_PORT || process.env.NEXT_PUBLIC_ACI_DEV_WS_PORT || DEFAULT_DEV_REFRESH_PORT);
|
|
49
|
+
const wsPath = process.env.ACI_DEV_WS_PATH || DEFAULT_DEV_REFRESH_PATH;
|
|
50
|
+
let timer;
|
|
51
|
+
let running = false;
|
|
52
|
+
let queued = false;
|
|
53
|
+
const server = http.createServer();
|
|
54
|
+
const wss = new WebSocketServer({ server, path: wsPath });
|
|
55
|
+
server.listen(wsPort, () => {
|
|
56
|
+
console.log(`[aci] dev refresh WebSocket on port ${wsPort}`);
|
|
57
|
+
});
|
|
58
|
+
function notifyClients() {
|
|
59
|
+
for (const client of wss.clients) {
|
|
60
|
+
if (client.readyState === 1) {
|
|
61
|
+
client.send(JSON.stringify({ type: 'content-updated' }));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function rebuild(reason) {
|
|
66
|
+
if (running) {
|
|
67
|
+
queued = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
running = true;
|
|
71
|
+
const success = recompileContent(reason);
|
|
72
|
+
running = false;
|
|
73
|
+
if (success) {
|
|
74
|
+
notifyClients();
|
|
75
|
+
}
|
|
76
|
+
if (queued) {
|
|
77
|
+
queued = false;
|
|
78
|
+
rebuild('queued change');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
rebuild('startup');
|
|
82
|
+
try {
|
|
83
|
+
fs.watch(src, { recursive: true }, (_event, filename) => {
|
|
84
|
+
if (!filename)
|
|
85
|
+
return;
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
timer = setTimeout(() => {
|
|
88
|
+
rebuild(path.relative(src, path.resolve(src, filename)));
|
|
89
|
+
}, 100);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
console.warn('[aci] fs.watch not supported on this platform, content watch disabled');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function createContentWatchWebpackPlugin() {
|
|
97
|
+
return {
|
|
98
|
+
apply(compiler) {
|
|
99
|
+
compiler.hooks['beforeCompile']?.tap('gradial-content-watch', () => {
|
|
100
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
101
|
+
startContentWatcher();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
|
|
2
|
-
export {
|
|
3
|
-
export { PreviewBanner, type PreviewBannerProps } from './preview-banner.js';
|
|
2
|
+
export { withGradialAci, type WithGradialOptions } from './config.js';
|
|
4
3
|
export { isPreviewMode } from './preview-mode.js';
|
|
4
|
+
export { createPreviewBannerHTML, createPreviewCookies, extractPreviewToken, resolvePreviewContext, PREVIEW_HEADER, RELEASE_HEADER, PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, type PreviewContext, type PreviewClaims, type PreviewCookie, } from '../preview/core.js';
|
package/dist/next/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { DevRefresh } from './dev-refresh.js';
|
|
2
|
-
export {
|
|
3
|
-
export { PreviewBanner } from './preview-banner.js';
|
|
2
|
+
export { withGradialAci } from './config.js';
|
|
4
3
|
export { isPreviewMode } from './preview-mode.js';
|
|
4
|
+
export { createPreviewBannerHTML, createPreviewCookies, extractPreviewToken, resolvePreviewContext, PREVIEW_HEADER, RELEASE_HEADER, PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, } from '../preview/core.js';
|
package/dist/next/middleware.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server.js';
|
|
2
2
|
import { getUncachedEdgeConfigValue } from './edge-config.js';
|
|
3
|
-
import {
|
|
4
|
-
const RELEASE_HEADER = 'x-gradial-release-id';
|
|
3
|
+
import { PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, PREVIEW_HEADER, RELEASE_HEADER, extractPreviewToken, resolvePreviewContext, createPreviewCookies, previewSignKey, } from '../preview/core.js';
|
|
5
4
|
const ROUTE_HEADER = 'x-gradial-route';
|
|
6
5
|
const DISPATCH_HEADER = 'x-gradial-dispatch';
|
|
7
6
|
export function createGradialMiddleware(config) {
|
|
@@ -11,40 +10,30 @@ export function createGradialMiddleware(config) {
|
|
|
11
10
|
if (url.searchParams.get('leave_preview') === '1') {
|
|
12
11
|
const cleanUrl = new URL(url.pathname, url.origin);
|
|
13
12
|
const response = NextResponse.redirect(cleanUrl);
|
|
14
|
-
response.cookies.delete(
|
|
15
|
-
response.cookies.delete(
|
|
13
|
+
response.cookies.delete(PREVIEW_TOKEN_COOKIE);
|
|
14
|
+
response.cookies.delete(PREVIEW_RELEASE_COOKIE);
|
|
16
15
|
return response;
|
|
17
16
|
}
|
|
18
17
|
const requestHeaders = gradialHeaders(request.headers);
|
|
19
18
|
// Try preview token first; if valid, use that release.
|
|
20
19
|
// If no token or invalid, fall through to active release from edge config.
|
|
21
|
-
const
|
|
22
|
-
const releaseId =
|
|
20
|
+
const preview = await resolvePreviewFromRequest(request, config);
|
|
21
|
+
const releaseId = preview?.releaseId || await activeReleaseFromEdgeConfig(config);
|
|
23
22
|
if (!releaseId) {
|
|
24
23
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
25
24
|
}
|
|
26
25
|
requestHeaders.set(RELEASE_HEADER, releaseId);
|
|
27
26
|
requestHeaders.set(ROUTE_HEADER, url.pathname);
|
|
28
27
|
requestHeaders.set(DISPATCH_HEADER, 'ssr-page');
|
|
29
|
-
if (
|
|
30
|
-
requestHeaders.set(
|
|
28
|
+
if (preview?.isPreview) {
|
|
29
|
+
requestHeaders.set(PREVIEW_HEADER, '1');
|
|
31
30
|
}
|
|
32
31
|
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
33
32
|
// Persist preview token as cookie so navigation stays in preview
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
sameSite: 'lax',
|
|
39
|
-
maxAge: 60 * 60,
|
|
40
|
-
path: '/',
|
|
41
|
-
});
|
|
42
|
-
response.cookies.set('aci_preview_release', previewReleaseId, {
|
|
43
|
-
httpOnly: false,
|
|
44
|
-
sameSite: 'lax',
|
|
45
|
-
maxAge: 60 * 60,
|
|
46
|
-
path: '/',
|
|
47
|
-
});
|
|
33
|
+
if (preview?.token && preview.isPreview) {
|
|
34
|
+
for (const cookie of createPreviewCookies(preview.token, releaseId)) {
|
|
35
|
+
response.cookies.set(cookie.name, cookie.value, cookie.options);
|
|
36
|
+
}
|
|
48
37
|
}
|
|
49
38
|
return response;
|
|
50
39
|
};
|
|
@@ -54,8 +43,31 @@ function gradialHeaders(headers) {
|
|
|
54
43
|
next.delete(RELEASE_HEADER);
|
|
55
44
|
next.delete(ROUTE_HEADER);
|
|
56
45
|
next.delete(DISPATCH_HEADER);
|
|
46
|
+
next.delete(PREVIEW_HEADER);
|
|
57
47
|
return next;
|
|
58
48
|
}
|
|
49
|
+
async function resolvePreviewFromRequest(request, config) {
|
|
50
|
+
const token = extractPreviewToken({
|
|
51
|
+
query: request.nextUrl.searchParams,
|
|
52
|
+
cookies: cookieMap(request.cookies),
|
|
53
|
+
authorization: request.headers.get('authorization') || undefined,
|
|
54
|
+
});
|
|
55
|
+
if (!token)
|
|
56
|
+
return null;
|
|
57
|
+
const signKey = previewSignKey();
|
|
58
|
+
if (!signKey) {
|
|
59
|
+
console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return await resolvePreviewContext({ token, siteId: config.siteId, signKey });
|
|
63
|
+
}
|
|
64
|
+
function cookieMap(cookies) {
|
|
65
|
+
const map = new Map();
|
|
66
|
+
for (const cookie of cookies.getAll()) {
|
|
67
|
+
map.set(cookie.name, cookie.value);
|
|
68
|
+
}
|
|
69
|
+
return map;
|
|
70
|
+
}
|
|
59
71
|
async function activeReleaseFromEdgeConfig(config) {
|
|
60
72
|
const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
|
|
61
73
|
if (!edgeConfig || !config.siteId)
|
|
@@ -126,31 +138,3 @@ function clampRelease(activeReleaseId, activeSequence, compatibility) {
|
|
|
126
138
|
function edgeKeySegment(value) {
|
|
127
139
|
return value.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
128
140
|
}
|
|
129
|
-
async function releaseIdFromPreviewToken(request, config) {
|
|
130
|
-
const token = previewTokenFromRequest(request);
|
|
131
|
-
if (!token)
|
|
132
|
-
return '';
|
|
133
|
-
const signKey = process.env.GRADIAL_PREVIEW_SIGN_KEY || '';
|
|
134
|
-
if (!signKey) {
|
|
135
|
-
console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
|
|
136
|
-
return '';
|
|
137
|
-
}
|
|
138
|
-
const claims = await validatePreviewToken(token, signKey);
|
|
139
|
-
if (!claims || claims.scope !== 'preview' || !claims.releaseId)
|
|
140
|
-
return '';
|
|
141
|
-
if (claims.siteId && config.siteId && claims.siteId !== config.siteId)
|
|
142
|
-
return '';
|
|
143
|
-
if (!claims.expiresAt || Date.parse(claims.expiresAt) <= Date.now())
|
|
144
|
-
return '';
|
|
145
|
-
return claims.releaseId;
|
|
146
|
-
}
|
|
147
|
-
function previewTokenFromRequest(request) {
|
|
148
|
-
const queryToken = request.nextUrl.searchParams.get('preview_token');
|
|
149
|
-
if (queryToken)
|
|
150
|
-
return queryToken;
|
|
151
|
-
const cookieToken = request.cookies.get('aci_preview')?.value || '';
|
|
152
|
-
if (cookieToken)
|
|
153
|
-
return cookieToken;
|
|
154
|
-
const auth = request.headers.get('authorization') || '';
|
|
155
|
-
return auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : '';
|
|
156
|
-
}
|