@affectively/aeon-pages 1.3.0
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/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { mkdir, writeFile, rm, readFile, readdir } from 'fs/promises';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { build } from './build';
|
|
5
|
+
|
|
6
|
+
describe('aeon build', () => {
|
|
7
|
+
const originalCwd = process.cwd();
|
|
8
|
+
const testDir = resolve(originalCwd, '.aeon-test-build');
|
|
9
|
+
const pagesDir = join(testDir, 'pages');
|
|
10
|
+
const outputDir = join(testDir, '.aeon');
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Clean up any existing test dir
|
|
14
|
+
await rm(testDir, { recursive: true, force: true });
|
|
15
|
+
|
|
16
|
+
// Create test directory structure
|
|
17
|
+
await mkdir(pagesDir, { recursive: true });
|
|
18
|
+
await mkdir(join(pagesDir, 'blog', '[slug]'), { recursive: true });
|
|
19
|
+
await mkdir(join(pagesDir, 'docs', '[[...path]]'), { recursive: true });
|
|
20
|
+
await mkdir(join(pagesDir, 'api', '[...catchall]'), { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Create test pages
|
|
23
|
+
await writeFile(
|
|
24
|
+
join(pagesDir, 'page.tsx'),
|
|
25
|
+
`'use aeon';
|
|
26
|
+
|
|
27
|
+
export default function Home() {
|
|
28
|
+
return (
|
|
29
|
+
<div className="container">
|
|
30
|
+
<h1>Welcome to Aeon Flux</h1>
|
|
31
|
+
<p>The CMS is the website.</p>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await writeFile(
|
|
38
|
+
join(pagesDir, 'blog', '[slug]', 'page.tsx'),
|
|
39
|
+
`'use aeon';
|
|
40
|
+
|
|
41
|
+
export default function BlogPost({ params }) {
|
|
42
|
+
return (
|
|
43
|
+
<article>
|
|
44
|
+
<h1>Blog Post</h1>
|
|
45
|
+
</article>
|
|
46
|
+
);
|
|
47
|
+
}`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await writeFile(
|
|
51
|
+
join(pagesDir, 'blog', '[slug]', 'layout.tsx'),
|
|
52
|
+
`export default function BlogLayout({ children }) {
|
|
53
|
+
return <div className="blog-layout">{children}</div>;
|
|
54
|
+
}`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await writeFile(
|
|
58
|
+
join(pagesDir, 'docs', '[[...path]]', 'page.tsx'),
|
|
59
|
+
`"use aeon";
|
|
60
|
+
|
|
61
|
+
export default function Docs() {
|
|
62
|
+
return (
|
|
63
|
+
<section>
|
|
64
|
+
<h1>Documentation</h1>
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await writeFile(
|
|
71
|
+
join(pagesDir, 'api', '[...catchall]', 'page.ts'),
|
|
72
|
+
`// Static page - no directive
|
|
73
|
+
export default function ApiDocs() {
|
|
74
|
+
return { type: 'div', children: ['API Docs'] };
|
|
75
|
+
}`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Create config
|
|
79
|
+
await writeFile(
|
|
80
|
+
join(testDir, 'aeon.config.ts'),
|
|
81
|
+
`export default {
|
|
82
|
+
pagesDir: './pages',
|
|
83
|
+
runtime: 'cloudflare',
|
|
84
|
+
output: { dir: '.aeon' },
|
|
85
|
+
};`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Change to test directory
|
|
89
|
+
process.chdir(testDir);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(async () => {
|
|
93
|
+
process.chdir(originalCwd);
|
|
94
|
+
await rm(testDir, { recursive: true, force: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('generates manifest.json with all routes', async () => {
|
|
98
|
+
await build({});
|
|
99
|
+
|
|
100
|
+
const manifest = JSON.parse(
|
|
101
|
+
await readFile(join(outputDir, 'manifest.json'), 'utf-8'),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(manifest.version).toBe('1.0.0');
|
|
105
|
+
expect(manifest.routes.length).toBeGreaterThanOrEqual(4);
|
|
106
|
+
|
|
107
|
+
// Check root route
|
|
108
|
+
const rootRoute = manifest.routes.find((r: any) => r.pattern === '/');
|
|
109
|
+
expect(rootRoute).toBeDefined();
|
|
110
|
+
expect(rootRoute.isAeon).toBe(true);
|
|
111
|
+
expect(rootRoute.sessionId).toBe('index');
|
|
112
|
+
|
|
113
|
+
// Check dynamic route
|
|
114
|
+
const blogRoute = manifest.routes.find((r: any) =>
|
|
115
|
+
r.pattern.includes('[slug]'),
|
|
116
|
+
);
|
|
117
|
+
expect(blogRoute).toBeDefined();
|
|
118
|
+
expect(blogRoute.isAeon).toBe(true);
|
|
119
|
+
|
|
120
|
+
// Check optional catch-all
|
|
121
|
+
const docsRoute = manifest.routes.find((r: any) =>
|
|
122
|
+
r.pattern.includes('[[...path]]'),
|
|
123
|
+
);
|
|
124
|
+
expect(docsRoute).toBeDefined();
|
|
125
|
+
|
|
126
|
+
// Check non-aeon route
|
|
127
|
+
const apiRoute = manifest.routes.find((r: any) =>
|
|
128
|
+
r.pattern.includes('[...catchall]'),
|
|
129
|
+
);
|
|
130
|
+
expect(apiRoute).toBeDefined();
|
|
131
|
+
expect(apiRoute.isAeon).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('generates D1 migration SQL', async () => {
|
|
135
|
+
await build({});
|
|
136
|
+
|
|
137
|
+
const migration = await readFile(
|
|
138
|
+
join(outputDir, 'migrations', '0001_initial.sql'),
|
|
139
|
+
'utf-8',
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Check for tables
|
|
143
|
+
expect(migration).toContain('CREATE TABLE IF NOT EXISTS routes');
|
|
144
|
+
expect(migration).toContain('CREATE TABLE IF NOT EXISTS sessions');
|
|
145
|
+
expect(migration).toContain('CREATE TABLE IF NOT EXISTS presence');
|
|
146
|
+
|
|
147
|
+
// Check for indexes
|
|
148
|
+
expect(migration).toContain(
|
|
149
|
+
'CREATE INDEX IF NOT EXISTS idx_routes_pattern',
|
|
150
|
+
);
|
|
151
|
+
expect(migration).toContain(
|
|
152
|
+
'CREATE INDEX IF NOT EXISTS idx_sessions_route',
|
|
153
|
+
);
|
|
154
|
+
expect(migration).toContain(
|
|
155
|
+
'CREATE INDEX IF NOT EXISTS idx_presence_session',
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Check columns
|
|
159
|
+
expect(migration).toContain('path TEXT PRIMARY KEY');
|
|
160
|
+
expect(migration).toContain('session_id TEXT');
|
|
161
|
+
expect(migration).toContain('tree TEXT NOT NULL');
|
|
162
|
+
expect(migration).toContain('schema_version TEXT');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('generates seed.sql with route and session data', async () => {
|
|
166
|
+
await build({});
|
|
167
|
+
|
|
168
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
169
|
+
|
|
170
|
+
// Check for route insertions
|
|
171
|
+
expect(seed).toContain('INSERT OR REPLACE INTO routes');
|
|
172
|
+
expect(seed).toContain('pattern, session_id, component_id');
|
|
173
|
+
|
|
174
|
+
// Check for session insertions
|
|
175
|
+
expect(seed).toContain('INSERT OR REPLACE INTO sessions');
|
|
176
|
+
expect(seed).toContain('tree');
|
|
177
|
+
expect(seed).toContain('schema_version');
|
|
178
|
+
|
|
179
|
+
// Check that component tree is serialized
|
|
180
|
+
expect(seed).toContain('"type"');
|
|
181
|
+
expect(seed).toContain('"children"');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('generates Cloudflare Worker', async () => {
|
|
185
|
+
await build({});
|
|
186
|
+
|
|
187
|
+
const worker = await readFile(
|
|
188
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
189
|
+
'utf-8',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Check for route matching
|
|
193
|
+
expect(worker).toContain('const ROUTES =');
|
|
194
|
+
expect(worker).toContain('matchRoute');
|
|
195
|
+
expect(worker).toContain('matchPattern');
|
|
196
|
+
|
|
197
|
+
// Check for D1 integration
|
|
198
|
+
expect(worker).toContain('getSession');
|
|
199
|
+
expect(worker).toContain('env.DB');
|
|
200
|
+
expect(worker).toContain('.prepare(');
|
|
201
|
+
|
|
202
|
+
// Check for WebSocket handling
|
|
203
|
+
expect(worker).toContain('handleWebSocket');
|
|
204
|
+
expect(worker).toContain('AEON_SESSIONS');
|
|
205
|
+
|
|
206
|
+
// Check for rendering
|
|
207
|
+
expect(worker).toContain('renderPage');
|
|
208
|
+
expect(worker).toContain('renderTree');
|
|
209
|
+
|
|
210
|
+
// Check for dynamic segment handling
|
|
211
|
+
expect(worker).toContain('[[...');
|
|
212
|
+
expect(worker).toContain('[...');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('generates wrangler.toml', async () => {
|
|
216
|
+
await build({});
|
|
217
|
+
|
|
218
|
+
const wrangler = await readFile(join(outputDir, 'wrangler.toml'), 'utf-8');
|
|
219
|
+
|
|
220
|
+
expect(wrangler).toContain('name = "aeon-flux"');
|
|
221
|
+
expect(wrangler).toContain('main = "dist/worker.js"');
|
|
222
|
+
expect(wrangler).toContain('[[d1_databases]]');
|
|
223
|
+
expect(wrangler).toContain('binding = "DB"');
|
|
224
|
+
expect(wrangler).toContain('[durable_objects]');
|
|
225
|
+
expect(wrangler).toContain('AeonPageSession');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('parses JSX and extracts component tree', async () => {
|
|
229
|
+
await build({});
|
|
230
|
+
|
|
231
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
232
|
+
|
|
233
|
+
// Check that JSX was parsed into tree structure
|
|
234
|
+
// The home page has a div with className "container"
|
|
235
|
+
expect(seed).toContain('"type"');
|
|
236
|
+
|
|
237
|
+
// Check for text content extraction
|
|
238
|
+
expect(seed.toLowerCase()).toContain('welcome');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('detects use aeon directive with single quotes', async () => {
|
|
242
|
+
await build({});
|
|
243
|
+
|
|
244
|
+
const manifest = JSON.parse(
|
|
245
|
+
await readFile(join(outputDir, 'manifest.json'), 'utf-8'),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const homeRoute = manifest.routes.find((r: any) => r.pattern === '/');
|
|
249
|
+
expect(homeRoute.isAeon).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('detects use aeon directive with double quotes', async () => {
|
|
253
|
+
await build({});
|
|
254
|
+
|
|
255
|
+
const manifest = JSON.parse(
|
|
256
|
+
await readFile(join(outputDir, 'manifest.json'), 'utf-8'),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const docsRoute = manifest.routes.find((r: any) =>
|
|
260
|
+
r.pattern.includes('docs'),
|
|
261
|
+
);
|
|
262
|
+
expect(docsRoute.isAeon).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('detects non-aeon pages', async () => {
|
|
266
|
+
await build({});
|
|
267
|
+
|
|
268
|
+
const manifest = JSON.parse(
|
|
269
|
+
await readFile(join(outputDir, 'manifest.json'), 'utf-8'),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const apiRoute = manifest.routes.find((r: any) =>
|
|
273
|
+
r.pattern.includes('api'),
|
|
274
|
+
);
|
|
275
|
+
expect(apiRoute.isAeon).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('handles layout files', async () => {
|
|
279
|
+
await build({});
|
|
280
|
+
|
|
281
|
+
const manifest = JSON.parse(
|
|
282
|
+
await readFile(join(outputDir, 'manifest.json'), 'utf-8'),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const blogRoute = manifest.routes.find((r: any) =>
|
|
286
|
+
r.pattern.includes('[slug]'),
|
|
287
|
+
);
|
|
288
|
+
expect(blogRoute.layout).toBeDefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('creates correct output directory structure', async () => {
|
|
292
|
+
await build({});
|
|
293
|
+
|
|
294
|
+
const distFiles = await readdir(join(outputDir, 'dist'));
|
|
295
|
+
expect(distFiles).toContain('worker.js');
|
|
296
|
+
|
|
297
|
+
const migrationFiles = await readdir(join(outputDir, 'migrations'));
|
|
298
|
+
expect(migrationFiles).toContain('0001_initial.sql');
|
|
299
|
+
|
|
300
|
+
const rootFiles = await readdir(outputDir);
|
|
301
|
+
expect(rootFiles).toContain('manifest.json');
|
|
302
|
+
expect(rootFiles).toContain('seed.sql');
|
|
303
|
+
expect(rootFiles).toContain('wrangler.toml');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('generates valid SQL for seed data', async () => {
|
|
307
|
+
await build({});
|
|
308
|
+
|
|
309
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
310
|
+
|
|
311
|
+
// Check SQL syntax
|
|
312
|
+
const statements = seed.split(';').filter((s) => s.trim());
|
|
313
|
+
for (const stmt of statements) {
|
|
314
|
+
if (stmt.includes('INSERT')) {
|
|
315
|
+
expect(stmt).toMatch(/INSERT OR REPLACE INTO \w+ \([^)]+\) VALUES/);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check for proper escaping
|
|
320
|
+
expect(seed).not.toContain("''"); // No double single quotes unless escaping
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('handles empty pages directory gracefully', async () => {
|
|
324
|
+
// Remove all pages
|
|
325
|
+
await rm(pagesDir, { recursive: true });
|
|
326
|
+
await mkdir(pagesDir, { recursive: true });
|
|
327
|
+
|
|
328
|
+
await build({});
|
|
329
|
+
|
|
330
|
+
const manifest = JSON.parse(
|
|
331
|
+
await readFile(join(outputDir, 'manifest.json'), 'utf-8'),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(manifest.routes).toEqual([]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('uses default config when config file missing', async () => {
|
|
338
|
+
// Remove config
|
|
339
|
+
await rm(join(testDir, 'aeon.config.ts'));
|
|
340
|
+
|
|
341
|
+
// Create pages in default location
|
|
342
|
+
await mkdir(join(testDir, 'pages'), { recursive: true });
|
|
343
|
+
await writeFile(
|
|
344
|
+
join(testDir, 'pages', 'page.tsx'),
|
|
345
|
+
`'use aeon';\nexport default function Page() { return <div>Test</div>; }`,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
await build({ config: 'nonexistent.config.ts' });
|
|
349
|
+
|
|
350
|
+
// Should use defaults and still work
|
|
351
|
+
const manifest = JSON.parse(
|
|
352
|
+
await readFile(join(testDir, '.aeon', 'manifest.json'), 'utf-8'),
|
|
353
|
+
);
|
|
354
|
+
expect(manifest.version).toBe('1.0.0');
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('AST to D1 sync', () => {
|
|
359
|
+
const originalCwd = process.cwd();
|
|
360
|
+
const testDir = resolve(originalCwd, '.aeon-test-ast-sync');
|
|
361
|
+
const pagesDir = join(testDir, 'pages');
|
|
362
|
+
const outputDir = join(testDir, '.aeon');
|
|
363
|
+
|
|
364
|
+
beforeEach(async () => {
|
|
365
|
+
await rm(testDir, { recursive: true, force: true });
|
|
366
|
+
await mkdir(pagesDir, { recursive: true });
|
|
367
|
+
await writeFile(
|
|
368
|
+
join(testDir, 'aeon.config.ts'),
|
|
369
|
+
`export default { pagesDir: './pages', runtime: 'cloudflare' };`,
|
|
370
|
+
);
|
|
371
|
+
process.chdir(testDir);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
afterEach(async () => {
|
|
375
|
+
process.chdir(originalCwd);
|
|
376
|
+
await rm(testDir, { recursive: true, force: true });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('extracts component type from JSX', async () => {
|
|
380
|
+
await writeFile(
|
|
381
|
+
join(pagesDir, 'page.tsx'),
|
|
382
|
+
`'use aeon';
|
|
383
|
+
export default function Test() {
|
|
384
|
+
return (
|
|
385
|
+
<main className="test">
|
|
386
|
+
<h1>Title</h1>
|
|
387
|
+
</main>
|
|
388
|
+
);
|
|
389
|
+
}`,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
await build({});
|
|
393
|
+
|
|
394
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
395
|
+
expect(seed).toContain('"type":"main"');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('extracts text content from JSX', async () => {
|
|
399
|
+
await writeFile(
|
|
400
|
+
join(pagesDir, 'page.tsx'),
|
|
401
|
+
`'use aeon';
|
|
402
|
+
export default function Test() {
|
|
403
|
+
return (
|
|
404
|
+
<div>
|
|
405
|
+
Hello World Content Here
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}`,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
await build({});
|
|
412
|
+
|
|
413
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
414
|
+
expect(seed.toLowerCase()).toContain('hello world');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('handles component with props', async () => {
|
|
418
|
+
await writeFile(
|
|
419
|
+
join(pagesDir, 'page.tsx'),
|
|
420
|
+
`'use aeon';
|
|
421
|
+
export default function Test() {
|
|
422
|
+
return (
|
|
423
|
+
<section id="main" data-testid="container">
|
|
424
|
+
Content
|
|
425
|
+
</section>
|
|
426
|
+
);
|
|
427
|
+
}`,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
await build({});
|
|
431
|
+
|
|
432
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
433
|
+
expect(seed).toContain('"type":"section"');
|
|
434
|
+
expect(seed).toContain('"props"');
|
|
435
|
+
expect(seed).toContain('"className":"aeon-page"');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('handles nested components', async () => {
|
|
439
|
+
await writeFile(
|
|
440
|
+
join(pagesDir, 'page.tsx'),
|
|
441
|
+
`'use aeon';
|
|
442
|
+
export default function Test() {
|
|
443
|
+
return (
|
|
444
|
+
<div>
|
|
445
|
+
<header>
|
|
446
|
+
<nav>Navigation</nav>
|
|
447
|
+
</header>
|
|
448
|
+
<main>Content</main>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}`,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
await build({});
|
|
455
|
+
|
|
456
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
457
|
+
expect(seed).toContain('"type":"div"');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('syncs route pattern to session ID', async () => {
|
|
461
|
+
await mkdir(join(pagesDir, 'users', '[id]', 'posts', '[postId]'), {
|
|
462
|
+
recursive: true,
|
|
463
|
+
});
|
|
464
|
+
await writeFile(
|
|
465
|
+
join(pagesDir, 'users', '[id]', 'posts', '[postId]', 'page.tsx'),
|
|
466
|
+
`'use aeon';
|
|
467
|
+
export default function Post() {
|
|
468
|
+
return <article>Post</article>;
|
|
469
|
+
}`,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
await build({});
|
|
473
|
+
|
|
474
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
475
|
+
|
|
476
|
+
// Session ID should be derived from route
|
|
477
|
+
expect(seed).toContain('users-[id]-posts-[postId]');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('handles special characters in content', async () => {
|
|
481
|
+
await writeFile(
|
|
482
|
+
join(pagesDir, 'page.tsx'),
|
|
483
|
+
`'use aeon';
|
|
484
|
+
export default function Test() {
|
|
485
|
+
return (
|
|
486
|
+
<div>
|
|
487
|
+
It's a "test" with <special> & characters
|
|
488
|
+
</div>
|
|
489
|
+
);
|
|
490
|
+
}`,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
await build({});
|
|
494
|
+
|
|
495
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
496
|
+
// Should be properly escaped for SQL - single quotes are doubled
|
|
497
|
+
expect(seed).toContain("It''s"); // Single quote escaped as ''
|
|
498
|
+
expect(seed).toContain('test'); // Double quotes should be preserved in JSON
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('generates consistent session IDs', async () => {
|
|
502
|
+
await writeFile(
|
|
503
|
+
join(pagesDir, 'page.tsx'),
|
|
504
|
+
`'use aeon';\nexport default function Home() { return <div>Home</div>; }`,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
await build({});
|
|
508
|
+
|
|
509
|
+
const manifest = JSON.parse(
|
|
510
|
+
await readFile(join(outputDir, 'manifest.json'), 'utf-8'),
|
|
511
|
+
);
|
|
512
|
+
const seed = await readFile(join(outputDir, 'seed.sql'), 'utf-8');
|
|
513
|
+
|
|
514
|
+
const homeRoute = manifest.routes.find((r: any) => r.pattern === '/');
|
|
515
|
+
expect(seed).toContain(`'${homeRoute.sessionId}'`);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('Multi-layer caching', () => {
|
|
520
|
+
const originalCwd = process.cwd();
|
|
521
|
+
const testDir = resolve(originalCwd, '.aeon-test-cache');
|
|
522
|
+
const pagesDir = join(testDir, 'pages');
|
|
523
|
+
const outputDir = join(testDir, '.aeon');
|
|
524
|
+
|
|
525
|
+
beforeEach(async () => {
|
|
526
|
+
await rm(testDir, { recursive: true, force: true });
|
|
527
|
+
await mkdir(pagesDir, { recursive: true });
|
|
528
|
+
await writeFile(
|
|
529
|
+
join(pagesDir, 'page.tsx'),
|
|
530
|
+
`'use aeon';\nexport default function Home() { return <div>Home</div>; }`,
|
|
531
|
+
);
|
|
532
|
+
await writeFile(
|
|
533
|
+
join(testDir, 'aeon.config.ts'),
|
|
534
|
+
`export default { pagesDir: './pages', runtime: 'cloudflare' };`,
|
|
535
|
+
);
|
|
536
|
+
process.chdir(testDir);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
afterEach(async () => {
|
|
540
|
+
process.chdir(originalCwd);
|
|
541
|
+
await rm(testDir, { recursive: true, force: true });
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('wrangler.toml includes KV namespace for page cache', async () => {
|
|
545
|
+
await build({});
|
|
546
|
+
|
|
547
|
+
const wrangler = await readFile(join(outputDir, 'wrangler.toml'), 'utf-8');
|
|
548
|
+
|
|
549
|
+
expect(wrangler).toContain('[[kv_namespaces]]');
|
|
550
|
+
expect(wrangler).toContain('binding = "PAGES_CACHE"');
|
|
551
|
+
expect(wrangler).toContain('# KV Namespace - edge page cache');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('worker includes KV cache layer logic', async () => {
|
|
555
|
+
await build({});
|
|
556
|
+
|
|
557
|
+
const worker = await readFile(
|
|
558
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
559
|
+
'utf-8',
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// Check for KV cache first check
|
|
563
|
+
expect(worker).toContain('env.PAGES_CACHE');
|
|
564
|
+
expect(worker).toContain('getFromKV');
|
|
565
|
+
expect(worker).toContain("'X-Aeon-Cache': 'HIT-KV'");
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test('worker includes D1 pre-rendered page layer', async () => {
|
|
569
|
+
await build({});
|
|
570
|
+
|
|
571
|
+
const worker = await readFile(
|
|
572
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
573
|
+
'utf-8',
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
expect(worker).toContain('getPreRenderedPage');
|
|
577
|
+
expect(worker).toContain("'X-Aeon-Cache': 'HIT-D1'");
|
|
578
|
+
expect(worker).toContain('rendered_pages');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('worker includes session-based fallback layer', async () => {
|
|
582
|
+
await build({});
|
|
583
|
+
|
|
584
|
+
const worker = await readFile(
|
|
585
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
586
|
+
'utf-8',
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
expect(worker).toContain('getSession');
|
|
590
|
+
expect(worker).toContain("'X-Aeon-Cache': 'MISS'");
|
|
591
|
+
expect(worker).toContain('renderPage');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('worker includes build version for cache invalidation', async () => {
|
|
595
|
+
await build({});
|
|
596
|
+
|
|
597
|
+
const worker = await readFile(
|
|
598
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
599
|
+
'utf-8',
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
expect(worker).toContain('BUILD_VERSION');
|
|
603
|
+
expect(worker).toContain('cached.version === BUILD_VERSION');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test('worker caches KV-miss to KV after D1 hit', async () => {
|
|
607
|
+
await build({});
|
|
608
|
+
|
|
609
|
+
const worker = await readFile(
|
|
610
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
611
|
+
'utf-8',
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// After D1 hit, should cache in KV
|
|
615
|
+
expect(worker).toContain('ctx.waitUntil');
|
|
616
|
+
expect(worker).toContain('env.PAGES_CACHE.put');
|
|
617
|
+
expect(worker).toContain('expirationTtl: CACHE_TTL');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('worker caches session-rendered pages to KV', async () => {
|
|
621
|
+
await build({});
|
|
622
|
+
|
|
623
|
+
const worker = await readFile(
|
|
624
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
625
|
+
'utf-8',
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// After session render, should cache in KV
|
|
629
|
+
expect(worker).toContain('JSON.stringify(cacheData)');
|
|
630
|
+
expect(worker).toContain('html, version: BUILD_VERSION');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('worker returns proper cache headers', async () => {
|
|
634
|
+
await build({});
|
|
635
|
+
|
|
636
|
+
const worker = await readFile(
|
|
637
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
638
|
+
'utf-8',
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// Check for cache control headers
|
|
642
|
+
expect(worker).toContain('Cache-Control');
|
|
643
|
+
expect(worker).toContain('max-age=3600');
|
|
644
|
+
expect(worker).toContain('stale-while-revalidate');
|
|
645
|
+
expect(worker).toContain('X-Aeon-Version');
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('getFromKV helper parses cached JSON correctly', async () => {
|
|
649
|
+
await build({});
|
|
650
|
+
|
|
651
|
+
const worker = await readFile(
|
|
652
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
653
|
+
'utf-8',
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
expect(worker).toContain('async function getFromKV');
|
|
657
|
+
expect(worker).toContain('JSON.parse(value)');
|
|
658
|
+
expect(worker).toContain('return null'); // Returns null on miss or parse error
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test('cache key uses route pattern', async () => {
|
|
662
|
+
await build({});
|
|
663
|
+
|
|
664
|
+
const worker = await readFile(
|
|
665
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
666
|
+
'utf-8',
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
expect(worker).toContain('const cacheKey = `page:${match.pattern}`');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('CACHE_TTL is configurable', async () => {
|
|
673
|
+
await build({});
|
|
674
|
+
|
|
675
|
+
const worker = await readFile(
|
|
676
|
+
join(outputDir, 'dist', 'worker.js'),
|
|
677
|
+
'utf-8',
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
expect(worker).toContain('const CACHE_TTL = 3600');
|
|
681
|
+
});
|
|
682
|
+
});
|