@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,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aeon init - Initialize a new Aeon Pages project
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Create new project from template
|
|
6
|
+
* - Import existing Next.js project
|
|
7
|
+
* - Seed D1 database from JSX AST
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdir, writeFile, readdir, readFile, stat } from 'fs/promises';
|
|
11
|
+
import { join, resolve } from 'path';
|
|
12
|
+
|
|
13
|
+
interface ProjectTemplate {
|
|
14
|
+
name: string;
|
|
15
|
+
files: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BASIC_TEMPLATE: ProjectTemplate = {
|
|
19
|
+
name: 'basic',
|
|
20
|
+
files: {
|
|
21
|
+
'package.json': `{
|
|
22
|
+
"name": "my-aeon-app",
|
|
23
|
+
"version": "0.1.0",
|
|
24
|
+
"private": true,
|
|
25
|
+
"type": "module",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "aeon dev",
|
|
28
|
+
"build": "aeon build",
|
|
29
|
+
"start": "aeon start"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@affectively/aeon-pages": "workspace:*",
|
|
33
|
+
"react": "^18.0.0",
|
|
34
|
+
"react-dom": "^18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.7.0",
|
|
38
|
+
"@types/react": "^18.0.0"
|
|
39
|
+
}
|
|
40
|
+
}`,
|
|
41
|
+
|
|
42
|
+
'aeon.config.ts': `import type { AeonConfig } from '@affectively/aeon-pages';
|
|
43
|
+
|
|
44
|
+
const config: AeonConfig = {
|
|
45
|
+
pagesDir: './pages',
|
|
46
|
+
componentsDir: './components',
|
|
47
|
+
runtime: 'bun',
|
|
48
|
+
port: 3000,
|
|
49
|
+
|
|
50
|
+
aeon: {
|
|
51
|
+
sync: {
|
|
52
|
+
mode: 'distributed',
|
|
53
|
+
consistencyLevel: 'strong',
|
|
54
|
+
},
|
|
55
|
+
presence: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
cursorTracking: true,
|
|
58
|
+
inactivityTimeout: 60000,
|
|
59
|
+
},
|
|
60
|
+
versioning: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
autoMigrate: true,
|
|
63
|
+
},
|
|
64
|
+
offline: {
|
|
65
|
+
enabled: true,
|
|
66
|
+
maxQueueSize: 1000,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
components: {
|
|
71
|
+
autoDiscover: true,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
output: {
|
|
75
|
+
dir: '.aeon',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default config;`,
|
|
80
|
+
|
|
81
|
+
'tsconfig.json': `{
|
|
82
|
+
"compilerOptions": {
|
|
83
|
+
"target": "ESNext",
|
|
84
|
+
"module": "ESNext",
|
|
85
|
+
"moduleResolution": "bundler",
|
|
86
|
+
"jsx": "react-jsx",
|
|
87
|
+
"strict": true,
|
|
88
|
+
"esModuleInterop": true,
|
|
89
|
+
"skipLibCheck": true,
|
|
90
|
+
"forceConsistentCasingInFileNames": true,
|
|
91
|
+
"resolveJsonModule": true,
|
|
92
|
+
"declaration": true,
|
|
93
|
+
"declarationMap": true,
|
|
94
|
+
"sourceMap": true,
|
|
95
|
+
"outDir": ".aeon/dist",
|
|
96
|
+
"baseUrl": ".",
|
|
97
|
+
"paths": {
|
|
98
|
+
"@/*": ["./*"]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
102
|
+
"exclude": ["node_modules", ".aeon"]
|
|
103
|
+
}`,
|
|
104
|
+
|
|
105
|
+
'pages/index.tsx': `'use aeon';
|
|
106
|
+
|
|
107
|
+
import { useAeonPage } from '@affectively/aeon-pages/react';
|
|
108
|
+
|
|
109
|
+
export default function HomePage() {
|
|
110
|
+
const { presence, data, updateData } = useAeonPage();
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 py-12 px-4">
|
|
114
|
+
<div className="max-w-3xl mx-auto">
|
|
115
|
+
<header className="text-center mb-12">
|
|
116
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
|
117
|
+
Welcome to Aeon Pages
|
|
118
|
+
</h1>
|
|
119
|
+
<p className="text-xl text-gray-600">
|
|
120
|
+
The CMS IS the website. Click anywhere to edit.
|
|
121
|
+
</p>
|
|
122
|
+
</header>
|
|
123
|
+
|
|
124
|
+
<main className="bg-white rounded-xl shadow-lg p-8">
|
|
125
|
+
<h2 className="text-2xl font-semibold text-gray-800 mb-4">
|
|
126
|
+
Getting Started
|
|
127
|
+
</h2>
|
|
128
|
+
<p className="text-gray-600 mb-6">
|
|
129
|
+
This page is now collaborative. Any edits you make will sync
|
|
130
|
+
in real-time with all connected users.
|
|
131
|
+
</p>
|
|
132
|
+
|
|
133
|
+
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
134
|
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
135
|
+
<span>{presence.length} user(s) online</span>
|
|
136
|
+
</div>
|
|
137
|
+
</main>
|
|
138
|
+
|
|
139
|
+
<footer className="mt-12 text-center text-gray-500 text-sm">
|
|
140
|
+
Built with{' '}
|
|
141
|
+
<a
|
|
142
|
+
href="https://github.com/affectively/aeon-pages"
|
|
143
|
+
className="text-blue-500 hover:underline"
|
|
144
|
+
>
|
|
145
|
+
@affectively/aeon-pages
|
|
146
|
+
</a>
|
|
147
|
+
</footer>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}`,
|
|
152
|
+
|
|
153
|
+
'pages/layout.tsx': `import { type ReactNode } from 'react';
|
|
154
|
+
|
|
155
|
+
interface LayoutProps {
|
|
156
|
+
children: ReactNode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
160
|
+
return (
|
|
161
|
+
<html lang="en">
|
|
162
|
+
<head>
|
|
163
|
+
<meta charSet="UTF-8" />
|
|
164
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
165
|
+
<title>Aeon Pages</title>
|
|
166
|
+
<link
|
|
167
|
+
href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css"
|
|
168
|
+
rel="stylesheet"
|
|
169
|
+
/>
|
|
170
|
+
</head>
|
|
171
|
+
<body>
|
|
172
|
+
{children}
|
|
173
|
+
</body>
|
|
174
|
+
</html>
|
|
175
|
+
);
|
|
176
|
+
}`,
|
|
177
|
+
|
|
178
|
+
'.gitignore': `# Dependencies
|
|
179
|
+
node_modules/
|
|
180
|
+
|
|
181
|
+
# Build output
|
|
182
|
+
.aeon/
|
|
183
|
+
dist/
|
|
184
|
+
|
|
185
|
+
# Environment
|
|
186
|
+
.env
|
|
187
|
+
.env.local
|
|
188
|
+
.env.*.local
|
|
189
|
+
|
|
190
|
+
# IDE
|
|
191
|
+
.vscode/
|
|
192
|
+
.idea/
|
|
193
|
+
*.swp
|
|
194
|
+
*.swo
|
|
195
|
+
|
|
196
|
+
# OS
|
|
197
|
+
.DS_Store
|
|
198
|
+
Thumbs.db
|
|
199
|
+
|
|
200
|
+
# Logs
|
|
201
|
+
*.log
|
|
202
|
+
npm-debug.log*
|
|
203
|
+
`,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export async function init(targetDir?: string): Promise<void> {
|
|
208
|
+
const projectDir = resolve(targetDir || '.');
|
|
209
|
+
const projectName = targetDir || 'aeon-app';
|
|
210
|
+
|
|
211
|
+
console.log(`\n🌟 Initializing Aeon Pages project: ${projectName}\n`);
|
|
212
|
+
|
|
213
|
+
// Check if directory exists and has content
|
|
214
|
+
try {
|
|
215
|
+
const entries = await readdir(projectDir);
|
|
216
|
+
if (entries.length > 0) {
|
|
217
|
+
// Check if it's a Next.js project
|
|
218
|
+
const hasNextConfig = entries.some(
|
|
219
|
+
(e) =>
|
|
220
|
+
e === 'next.config.js' ||
|
|
221
|
+
e === 'next.config.ts' ||
|
|
222
|
+
e === 'next.config.mjs',
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (hasNextConfig) {
|
|
226
|
+
console.log(
|
|
227
|
+
'📦 Detected Next.js project. Converting to Aeon Pages...\n',
|
|
228
|
+
);
|
|
229
|
+
await convertNextProject(projectDir);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Non-empty directory, ask to overwrite
|
|
234
|
+
console.log('⚠️ Directory is not empty.');
|
|
235
|
+
console.log(' Use --force to overwrite existing files.\n');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Directory doesn't exist, create it
|
|
240
|
+
await mkdir(projectDir, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Create project from template
|
|
244
|
+
console.log('📁 Creating project structure...');
|
|
245
|
+
|
|
246
|
+
for (const [relativePath, content] of Object.entries(BASIC_TEMPLATE.files)) {
|
|
247
|
+
const filePath = join(projectDir, relativePath);
|
|
248
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
249
|
+
|
|
250
|
+
await mkdir(dir, { recursive: true });
|
|
251
|
+
|
|
252
|
+
// Update package name
|
|
253
|
+
let fileContent = content;
|
|
254
|
+
if (relativePath === 'package.json') {
|
|
255
|
+
fileContent = content.replace('"my-aeon-app"', `"${projectName}"`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await writeFile(filePath, fileContent);
|
|
259
|
+
console.log(` ✓ ${relativePath}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log('\n✨ Project initialized!\n');
|
|
263
|
+
console.log('Next steps:');
|
|
264
|
+
console.log(` cd ${targetDir || '.'}`);
|
|
265
|
+
console.log(' bun install');
|
|
266
|
+
console.log(' bun run dev');
|
|
267
|
+
console.log('\nDocs: https://github.com/affectively/aeon-pages\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Convert an existing Next.js project to Aeon Pages
|
|
272
|
+
*/
|
|
273
|
+
async function convertNextProject(projectDir: string): Promise<void> {
|
|
274
|
+
console.log('🔄 Converting Next.js project to Aeon Pages...\n');
|
|
275
|
+
|
|
276
|
+
// Find app or pages directory
|
|
277
|
+
const appDir = join(projectDir, 'app');
|
|
278
|
+
const pagesDir = join(projectDir, 'pages');
|
|
279
|
+
|
|
280
|
+
let sourceDir: string;
|
|
281
|
+
let isAppRouter = false;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await stat(appDir);
|
|
285
|
+
sourceDir = appDir;
|
|
286
|
+
isAppRouter = true;
|
|
287
|
+
console.log(' Found App Router (app/)\n');
|
|
288
|
+
} catch {
|
|
289
|
+
try {
|
|
290
|
+
await stat(pagesDir);
|
|
291
|
+
sourceDir = pagesDir;
|
|
292
|
+
console.log(' Found Pages Router (pages/)\n');
|
|
293
|
+
} catch {
|
|
294
|
+
console.error('❌ No app/ or pages/ directory found.');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Scan for page files
|
|
300
|
+
const pages = await scanPages(sourceDir, isAppRouter);
|
|
301
|
+
console.log(` Found ${pages.length} page(s):\n`);
|
|
302
|
+
|
|
303
|
+
for (const page of pages) {
|
|
304
|
+
console.log(` 📄 ${page.route}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Create aeon.config.ts
|
|
308
|
+
const configPath = join(projectDir, 'aeon.config.ts');
|
|
309
|
+
await writeFile(configPath, BASIC_TEMPLATE.files['aeon.config.ts']);
|
|
310
|
+
console.log('\n ✓ Created aeon.config.ts');
|
|
311
|
+
|
|
312
|
+
// Update package.json
|
|
313
|
+
await updatePackageJson(projectDir);
|
|
314
|
+
console.log(' ✓ Updated package.json scripts');
|
|
315
|
+
|
|
316
|
+
console.log('\n✨ Conversion complete!\n');
|
|
317
|
+
console.log('Next steps:');
|
|
318
|
+
console.log(" 1. Add 'use aeon'; to pages you want to make collaborative");
|
|
319
|
+
console.log(' 2. bun install');
|
|
320
|
+
console.log(' 3. bun run dev');
|
|
321
|
+
console.log('\nNote: Your existing Next.js code will work with Aeon Pages.');
|
|
322
|
+
console.log(
|
|
323
|
+
" Just add 'use aeon'; at the top of any page to enable collaboration.\n",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface PageInfo {
|
|
328
|
+
route: string;
|
|
329
|
+
filePath: string;
|
|
330
|
+
hasAeonDirective: boolean;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function scanPages(
|
|
334
|
+
dir: string,
|
|
335
|
+
isAppRouter: boolean,
|
|
336
|
+
): Promise<PageInfo[]> {
|
|
337
|
+
const pages: PageInfo[] = [];
|
|
338
|
+
|
|
339
|
+
async function scan(currentDir: string, routePath: string): Promise<void> {
|
|
340
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
341
|
+
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
const fullPath = join(currentDir, entry.name);
|
|
344
|
+
|
|
345
|
+
if (entry.isDirectory()) {
|
|
346
|
+
// Handle dynamic routes
|
|
347
|
+
let segment = entry.name;
|
|
348
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
349
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
350
|
+
}
|
|
351
|
+
await scan(fullPath, `${routePath}/${segment}`);
|
|
352
|
+
} else if (entry.isFile()) {
|
|
353
|
+
const isPage = isAppRouter
|
|
354
|
+
? entry.name === 'page.tsx' ||
|
|
355
|
+
entry.name === 'page.ts' ||
|
|
356
|
+
entry.name === 'page.jsx' ||
|
|
357
|
+
entry.name === 'page.js'
|
|
358
|
+
: entry.name.endsWith('.tsx') || entry.name.endsWith('.jsx');
|
|
359
|
+
|
|
360
|
+
if (isPage) {
|
|
361
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
362
|
+
const hasAeon =
|
|
363
|
+
content.includes("'use aeon'") || content.includes('"use aeon"');
|
|
364
|
+
|
|
365
|
+
let route = routePath;
|
|
366
|
+
if (!isAppRouter) {
|
|
367
|
+
// Pages router: file name is the route
|
|
368
|
+
const baseName = entry.name.replace(/\.(tsx|jsx|ts|js)$/, '');
|
|
369
|
+
if (baseName !== 'index') {
|
|
370
|
+
route = `${routePath}/${baseName}`;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
pages.push({
|
|
375
|
+
route: route || '/',
|
|
376
|
+
filePath: fullPath,
|
|
377
|
+
hasAeonDirective: hasAeon,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await scan(dir, '');
|
|
385
|
+
return pages;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function updatePackageJson(projectDir: string): Promise<void> {
|
|
389
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const content = await readFile(pkgPath, 'utf-8');
|
|
393
|
+
const pkg = JSON.parse(content);
|
|
394
|
+
|
|
395
|
+
// Update scripts
|
|
396
|
+
pkg.scripts = pkg.scripts || {};
|
|
397
|
+
pkg.scripts.dev = 'aeon dev';
|
|
398
|
+
pkg.scripts.build = 'aeon build';
|
|
399
|
+
pkg.scripts.start = 'aeon start';
|
|
400
|
+
|
|
401
|
+
// Add dependencies
|
|
402
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
403
|
+
pkg.dependencies['@affectively/aeon-pages'] = 'workspace:*';
|
|
404
|
+
|
|
405
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
406
|
+
} catch (err) {
|
|
407
|
+
console.error('Failed to update package.json:', err);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aeon start - Start production server
|
|
3
|
+
*
|
|
4
|
+
* For local testing of production builds before deploying to Cloudflare.
|
|
5
|
+
* In production, you deploy to Cloudflare Workers - no server needed!
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'fs/promises';
|
|
9
|
+
import { resolve, join, relative } from 'path';
|
|
10
|
+
|
|
11
|
+
interface StartOptions {
|
|
12
|
+
port: number;
|
|
13
|
+
config?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AeonConfig {
|
|
17
|
+
pagesDir: string;
|
|
18
|
+
output?: { dir?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RouteManifest {
|
|
22
|
+
version: string;
|
|
23
|
+
routes: Array<{
|
|
24
|
+
pattern: string;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
isAeon: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function start(options: StartOptions): Promise<void> {
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
const configPath = options.config || 'aeon.config.ts';
|
|
33
|
+
const port = options.port || 3000;
|
|
34
|
+
|
|
35
|
+
console.log('\n🚀 Starting Aeon Flux production server...\n');
|
|
36
|
+
|
|
37
|
+
// Load config
|
|
38
|
+
const config = await loadConfig(resolve(cwd, configPath));
|
|
39
|
+
const outputDir = resolve(cwd, config.output?.dir || '.aeon');
|
|
40
|
+
|
|
41
|
+
// Check for build output
|
|
42
|
+
const manifestPath = join(outputDir, 'manifest.json');
|
|
43
|
+
let manifest: RouteManifest;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = await readFile(manifestPath, 'utf-8');
|
|
47
|
+
manifest = JSON.parse(content);
|
|
48
|
+
} catch {
|
|
49
|
+
console.error('❌ No build found. Run `aeon build` first.\n');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`📦 Build: ${relative(cwd, outputDir)}`);
|
|
54
|
+
console.log(`📄 Routes: ${manifest.routes.length}`);
|
|
55
|
+
console.log(`🌐 Port: ${port}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
// Load seed data (simulate D1)
|
|
59
|
+
const sessions = await loadSeedData(outputDir);
|
|
60
|
+
|
|
61
|
+
// Start server
|
|
62
|
+
const server = Bun.serve({
|
|
63
|
+
port,
|
|
64
|
+
async fetch(req) {
|
|
65
|
+
const url = new URL(req.url);
|
|
66
|
+
|
|
67
|
+
// Match route
|
|
68
|
+
const match = matchRoute(url.pathname, manifest.routes);
|
|
69
|
+
if (!match) {
|
|
70
|
+
return new Response('Not Found', { status: 404 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get session (simulating D1 query)
|
|
74
|
+
const session = sessions.get(match.sessionId);
|
|
75
|
+
if (!session) {
|
|
76
|
+
return new Response('Session not found', { status: 404 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Render page
|
|
80
|
+
const html = renderPage(session, match);
|
|
81
|
+
|
|
82
|
+
return new Response(html, {
|
|
83
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
websocket: {
|
|
87
|
+
open(_ws) {
|
|
88
|
+
console.log(' WebSocket client connected');
|
|
89
|
+
},
|
|
90
|
+
close(_ws) {
|
|
91
|
+
console.log(' WebSocket client disconnected');
|
|
92
|
+
},
|
|
93
|
+
message(_ws, _message) {
|
|
94
|
+
// Handle collaboration messages
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log(`✨ Ready at http://localhost:${port}\n`);
|
|
100
|
+
console.log(
|
|
101
|
+
' This is a local preview. In production, deploy to Cloudflare Workers.',
|
|
102
|
+
);
|
|
103
|
+
console.log(' Press Ctrl+C to stop\n');
|
|
104
|
+
|
|
105
|
+
// Handle graceful shutdown
|
|
106
|
+
process.on('SIGINT', () => {
|
|
107
|
+
console.log('\n\n👋 Shutting down...\n');
|
|
108
|
+
server.stop();
|
|
109
|
+
process.exit(0);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function loadConfig(configPath: string): Promise<AeonConfig> {
|
|
114
|
+
try {
|
|
115
|
+
const module = await import(configPath);
|
|
116
|
+
return module.default || module;
|
|
117
|
+
} catch {
|
|
118
|
+
return {
|
|
119
|
+
pagesDir: './pages',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface Session {
|
|
125
|
+
route: string;
|
|
126
|
+
tree: SerializedComponent;
|
|
127
|
+
data: Record<string, unknown>;
|
|
128
|
+
schema: { version: string };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface SerializedComponent {
|
|
132
|
+
type: string;
|
|
133
|
+
props?: Record<string, unknown>;
|
|
134
|
+
children?: (SerializedComponent | string)[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function loadSeedData(outputDir: string): Promise<Map<string, Session>> {
|
|
138
|
+
const sessions = new Map<string, Session>();
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const seedPath = join(outputDir, 'seed.sql');
|
|
142
|
+
const content = await readFile(seedPath, 'utf-8');
|
|
143
|
+
|
|
144
|
+
// Parse INSERT statements (simplified)
|
|
145
|
+
const sessionInserts = content.match(
|
|
146
|
+
/INSERT OR REPLACE INTO sessions \(session_id, route, tree, data, schema_version\) VALUES \('([^']+)', '([^']+)', '(.+?)', '\{\}', '([^']+)'\);/g,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (sessionInserts) {
|
|
150
|
+
for (const insert of sessionInserts) {
|
|
151
|
+
const match = insert.match(
|
|
152
|
+
/VALUES \('([^']+)', '([^']+)', '(.+?)', '\{\}', '([^']+)'\)/,
|
|
153
|
+
);
|
|
154
|
+
if (match) {
|
|
155
|
+
const [, sessionId, route, treeStr, version] = match;
|
|
156
|
+
const tree = JSON.parse(treeStr.replace(/''/g, "'"));
|
|
157
|
+
sessions.set(sessionId, {
|
|
158
|
+
route,
|
|
159
|
+
tree,
|
|
160
|
+
data: {},
|
|
161
|
+
schema: { version },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error('Failed to load seed data:', err);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return sessions;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface RouteMatch {
|
|
174
|
+
pattern: string;
|
|
175
|
+
sessionId: string;
|
|
176
|
+
isAeon: boolean;
|
|
177
|
+
params: Record<string, string>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function matchRoute(
|
|
181
|
+
path: string,
|
|
182
|
+
routes: RouteManifest['routes'],
|
|
183
|
+
): RouteMatch | null {
|
|
184
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
185
|
+
|
|
186
|
+
for (const route of routes) {
|
|
187
|
+
const params = matchPattern(pathParts, route.pattern);
|
|
188
|
+
if (params !== null) {
|
|
189
|
+
return { ...route, params };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function matchPattern(
|
|
196
|
+
pathParts: string[],
|
|
197
|
+
pattern: string,
|
|
198
|
+
): Record<string, string> | null {
|
|
199
|
+
const patternParts = pattern.split('/').filter(Boolean);
|
|
200
|
+
const params: Record<string, string> = {};
|
|
201
|
+
|
|
202
|
+
let pi = 0;
|
|
203
|
+
let pati = 0;
|
|
204
|
+
|
|
205
|
+
while (pi < pathParts.length && pati < patternParts.length) {
|
|
206
|
+
const pathPart = pathParts[pi];
|
|
207
|
+
const patternPart = patternParts[pati];
|
|
208
|
+
|
|
209
|
+
if (patternPart.startsWith('[[...') && patternPart.endsWith(']]')) {
|
|
210
|
+
const name = patternPart.slice(5, -2);
|
|
211
|
+
params[name] = pathParts.slice(pi).join('/');
|
|
212
|
+
return params;
|
|
213
|
+
} else if (patternPart.startsWith('[...') && patternPart.endsWith(']')) {
|
|
214
|
+
const name = patternPart.slice(4, -1);
|
|
215
|
+
params[name] = pathParts.slice(pi).join('/');
|
|
216
|
+
return params;
|
|
217
|
+
} else if (patternPart.startsWith('[') && patternPart.endsWith(']')) {
|
|
218
|
+
const name = patternPart.slice(1, -1);
|
|
219
|
+
params[name] = pathPart;
|
|
220
|
+
pi++;
|
|
221
|
+
pati++;
|
|
222
|
+
} else if (pathPart === patternPart) {
|
|
223
|
+
pi++;
|
|
224
|
+
pati++;
|
|
225
|
+
} else {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (pati < patternParts.length) {
|
|
231
|
+
const last = patternParts[pati];
|
|
232
|
+
if (last.startsWith('[[...') && last.endsWith(']]')) {
|
|
233
|
+
params[last.slice(5, -2)] = '';
|
|
234
|
+
return params;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return pi === pathParts.length && pati === patternParts.length
|
|
239
|
+
? params
|
|
240
|
+
: null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderPage(session: Session, match: RouteMatch): string {
|
|
244
|
+
const html = renderTree(session.tree);
|
|
245
|
+
|
|
246
|
+
return `<!DOCTYPE html>
|
|
247
|
+
<html lang="en">
|
|
248
|
+
<head>
|
|
249
|
+
<meta charset="UTF-8">
|
|
250
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
251
|
+
<title>Aeon Flux</title>
|
|
252
|
+
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css" rel="stylesheet">
|
|
253
|
+
</head>
|
|
254
|
+
<body>
|
|
255
|
+
<div id="root">${html}</div>
|
|
256
|
+
${
|
|
257
|
+
match.isAeon
|
|
258
|
+
? `
|
|
259
|
+
<script>
|
|
260
|
+
window.__AEON_FLUX__ = {
|
|
261
|
+
route: "${match.pattern}",
|
|
262
|
+
sessionId: "${match.sessionId}",
|
|
263
|
+
params: ${JSON.stringify(match.params)},
|
|
264
|
+
};
|
|
265
|
+
</script>
|
|
266
|
+
`
|
|
267
|
+
: ''
|
|
268
|
+
}
|
|
269
|
+
</body>
|
|
270
|
+
</html>`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderTree(node: SerializedComponent | string): string {
|
|
274
|
+
if (typeof node === 'string') return escapeHtml(node);
|
|
275
|
+
if (!node) return '';
|
|
276
|
+
|
|
277
|
+
const { type, props = {}, children = [] } = node;
|
|
278
|
+
const attrs = Object.entries(props)
|
|
279
|
+
.map(([k, v]) => {
|
|
280
|
+
if (k === 'className') return `class="${v}"`;
|
|
281
|
+
if (typeof v === 'boolean') return v ? k : '';
|
|
282
|
+
return `${k}="${escapeHtml(String(v))}"`;
|
|
283
|
+
})
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join(' ');
|
|
286
|
+
|
|
287
|
+
const childHtml = children.map(renderTree).join('');
|
|
288
|
+
return `<${type}${attrs ? ' ' + attrs : ''}>${childHtml}</${type}>`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function escapeHtml(s: string): string {
|
|
292
|
+
return s
|
|
293
|
+
.replace(/&/g, '&')
|
|
294
|
+
.replace(/</g, '<')
|
|
295
|
+
.replace(/>/g, '>')
|
|
296
|
+
.replace(/"/g, '"');
|
|
297
|
+
}
|