@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,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aeon dev - Start development server with hot reload
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Hot module reload
|
|
6
|
+
* - 'use aeon' directive processing
|
|
7
|
+
* - Real-time sync preview
|
|
8
|
+
* - AST ↔ Source bidirectional editing
|
|
9
|
+
* - Error overlay
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { watch } from 'fs';
|
|
13
|
+
import { readFile, stat } from 'fs/promises';
|
|
14
|
+
import { resolve, join, relative } from 'path';
|
|
15
|
+
|
|
16
|
+
interface DevOptions {
|
|
17
|
+
port: number;
|
|
18
|
+
config?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AeonConfig {
|
|
22
|
+
pagesDir: string;
|
|
23
|
+
componentsDir?: string;
|
|
24
|
+
runtime: 'bun' | 'cloudflare';
|
|
25
|
+
port?: number;
|
|
26
|
+
aeon?: {
|
|
27
|
+
sync?: { mode: string };
|
|
28
|
+
presence?: { enabled: boolean };
|
|
29
|
+
versioning?: { enabled: boolean };
|
|
30
|
+
offline?: { enabled: boolean };
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function dev(options: DevOptions): Promise<void> {
|
|
35
|
+
const cwd = process.cwd();
|
|
36
|
+
const configPath = options.config || 'aeon.config.ts';
|
|
37
|
+
|
|
38
|
+
console.log('\n🚀 Starting Aeon Flux development server...\n');
|
|
39
|
+
|
|
40
|
+
// Load config
|
|
41
|
+
const config = await loadConfig(resolve(cwd, configPath));
|
|
42
|
+
const port = options.port || config.port || 3000;
|
|
43
|
+
|
|
44
|
+
// Resolve directories
|
|
45
|
+
const pagesDir = resolve(cwd, config.pagesDir || './pages');
|
|
46
|
+
const componentsDir = resolve(cwd, config.componentsDir || './components');
|
|
47
|
+
|
|
48
|
+
console.log(`📁 Pages: ${relative(cwd, pagesDir)}`);
|
|
49
|
+
console.log(`📁 Components: ${relative(cwd, componentsDir)}`);
|
|
50
|
+
console.log(`🌐 Port: ${port}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
|
|
53
|
+
// Scan for routes
|
|
54
|
+
const routes = await scanRoutes(pagesDir);
|
|
55
|
+
console.log(`📄 Found ${routes.length} route(s):`);
|
|
56
|
+
for (const route of routes) {
|
|
57
|
+
const aeonBadge = route.isAeon ? ' [aeon]' : '';
|
|
58
|
+
console.log(` ${route.pattern}${aeonBadge}`);
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
// Start file watcher
|
|
63
|
+
const watcher = startWatcher(
|
|
64
|
+
pagesDir,
|
|
65
|
+
componentsDir,
|
|
66
|
+
async (event, filename) => {
|
|
67
|
+
console.log(`\n♻️ ${event}: ${filename}`);
|
|
68
|
+
// Trigger HMR
|
|
69
|
+
broadcastHMR(filename);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Start server
|
|
74
|
+
const server = Bun.serve({
|
|
75
|
+
port,
|
|
76
|
+
async fetch(req) {
|
|
77
|
+
const url = new URL(req.url);
|
|
78
|
+
|
|
79
|
+
// HMR WebSocket
|
|
80
|
+
if (url.pathname === '/__aeon_hmr') {
|
|
81
|
+
const upgraded = server.upgrade(req);
|
|
82
|
+
if (!upgraded) {
|
|
83
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Dev overlay script
|
|
89
|
+
if (url.pathname === '/__aeon_dev.js') {
|
|
90
|
+
return new Response(DEV_OVERLAY_SCRIPT, {
|
|
91
|
+
headers: { 'Content-Type': 'application/javascript' },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Match route
|
|
96
|
+
const match = matchRoute(url.pathname, routes);
|
|
97
|
+
if (!match) {
|
|
98
|
+
return new Response('Not found', { status: 404 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read and process page
|
|
102
|
+
try {
|
|
103
|
+
const content = await readFile(match.filePath, 'utf-8');
|
|
104
|
+
const html = await renderPage(content, match, config);
|
|
105
|
+
|
|
106
|
+
// Inject dev overlay
|
|
107
|
+
const devHtml = injectDevOverlay(html, port);
|
|
108
|
+
|
|
109
|
+
return new Response(devHtml, {
|
|
110
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('Render error:', err);
|
|
114
|
+
return new Response(renderErrorPage(err as Error), {
|
|
115
|
+
status: 500,
|
|
116
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
websocket: {
|
|
121
|
+
open(ws) {
|
|
122
|
+
hmrClients.add(ws);
|
|
123
|
+
console.log(' HMR client connected');
|
|
124
|
+
},
|
|
125
|
+
close(ws) {
|
|
126
|
+
hmrClients.delete(ws);
|
|
127
|
+
},
|
|
128
|
+
message(_ws, _message) {
|
|
129
|
+
// Handle client messages if needed
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log(`\n✨ Ready at http://localhost:${port}\n`);
|
|
135
|
+
console.log(' Press Ctrl+C to stop\n');
|
|
136
|
+
|
|
137
|
+
// Handle graceful shutdown
|
|
138
|
+
process.on('SIGINT', () => {
|
|
139
|
+
console.log('\n\n👋 Shutting down...\n');
|
|
140
|
+
watcher.close();
|
|
141
|
+
server.stop();
|
|
142
|
+
process.exit(0);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// HMR clients
|
|
147
|
+
const hmrClients = new Set<unknown>();
|
|
148
|
+
|
|
149
|
+
function broadcastHMR(filename: string): void {
|
|
150
|
+
const message = JSON.stringify({ type: 'reload', filename });
|
|
151
|
+
for (const client of hmrClients) {
|
|
152
|
+
try {
|
|
153
|
+
(client as { send: (msg: string) => void }).send(message);
|
|
154
|
+
} catch {
|
|
155
|
+
hmrClients.delete(client);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function loadConfig(configPath: string): Promise<AeonConfig> {
|
|
161
|
+
try {
|
|
162
|
+
const module = await import(configPath);
|
|
163
|
+
return module.default || module;
|
|
164
|
+
} catch {
|
|
165
|
+
console.log('⚠️ No config found, using defaults');
|
|
166
|
+
return {
|
|
167
|
+
pagesDir: './pages',
|
|
168
|
+
runtime: 'bun',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface RouteInfo {
|
|
174
|
+
pattern: string;
|
|
175
|
+
filePath: string;
|
|
176
|
+
isAeon: boolean;
|
|
177
|
+
layout?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function scanRoutes(pagesDir: string): Promise<RouteInfo[]> {
|
|
181
|
+
const routes: RouteInfo[] = [];
|
|
182
|
+
|
|
183
|
+
async function scan(dir: string, routePath: string): Promise<void> {
|
|
184
|
+
const { readdir } = await import('fs/promises');
|
|
185
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
186
|
+
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const fullPath = join(dir, entry.name);
|
|
189
|
+
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
let segment = entry.name;
|
|
192
|
+
// Convert Next.js dynamic routes to Aeon format
|
|
193
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
194
|
+
if (segment.startsWith('[...')) {
|
|
195
|
+
// Catch-all: [...slug] -> *slug
|
|
196
|
+
segment = '*' + segment.slice(4, -1);
|
|
197
|
+
} else if (segment.startsWith('[[...')) {
|
|
198
|
+
// Optional catch-all: [[...slug]] -> **slug
|
|
199
|
+
segment = '**' + segment.slice(5, -2);
|
|
200
|
+
} else {
|
|
201
|
+
// Dynamic: [id] -> :id
|
|
202
|
+
segment = ':' + segment.slice(1, -1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await scan(fullPath, `${routePath}/${segment}`);
|
|
206
|
+
} else if (entry.isFile()) {
|
|
207
|
+
const isPage =
|
|
208
|
+
entry.name === 'page.tsx' ||
|
|
209
|
+
entry.name === 'page.ts' ||
|
|
210
|
+
entry.name === 'page.jsx' ||
|
|
211
|
+
entry.name === 'page.js' ||
|
|
212
|
+
(entry.name.endsWith('.tsx') && !entry.name.startsWith('_'));
|
|
213
|
+
|
|
214
|
+
if (isPage) {
|
|
215
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
216
|
+
const isAeon =
|
|
217
|
+
content.includes("'use aeon'") || content.includes('"use aeon"');
|
|
218
|
+
|
|
219
|
+
// Check for layout
|
|
220
|
+
const layoutPath = join(dir, 'layout.tsx');
|
|
221
|
+
let layout: string | undefined;
|
|
222
|
+
try {
|
|
223
|
+
await stat(layoutPath);
|
|
224
|
+
layout = layoutPath;
|
|
225
|
+
} catch {
|
|
226
|
+
// No layout
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
routes.push({
|
|
230
|
+
pattern: routePath || '/',
|
|
231
|
+
filePath: fullPath,
|
|
232
|
+
isAeon,
|
|
233
|
+
layout,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await scan(pagesDir, '');
|
|
241
|
+
return routes.sort((a, b) => {
|
|
242
|
+
// Static routes first
|
|
243
|
+
const aStatic = !a.pattern.includes(':') && !a.pattern.includes('*');
|
|
244
|
+
const bStatic = !b.pattern.includes(':') && !b.pattern.includes('*');
|
|
245
|
+
if (aStatic && !bStatic) return -1;
|
|
246
|
+
if (!aStatic && bStatic) return 1;
|
|
247
|
+
return a.pattern.localeCompare(b.pattern);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function matchRoute(
|
|
252
|
+
path: string,
|
|
253
|
+
routes: RouteInfo[],
|
|
254
|
+
): (RouteInfo & { params: Record<string, string> }) | null {
|
|
255
|
+
for (const route of routes) {
|
|
256
|
+
const params = matchPattern(path, route.pattern);
|
|
257
|
+
if (params !== null) {
|
|
258
|
+
return { ...route, params };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function matchPattern(
|
|
265
|
+
path: string,
|
|
266
|
+
pattern: string,
|
|
267
|
+
): Record<string, string> | null {
|
|
268
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
269
|
+
const patternParts = pattern.split('/').filter(Boolean);
|
|
270
|
+
const params: Record<string, string> = {};
|
|
271
|
+
|
|
272
|
+
let pi = 0;
|
|
273
|
+
let pati = 0;
|
|
274
|
+
|
|
275
|
+
while (pi < pathParts.length && pati < patternParts.length) {
|
|
276
|
+
const pathPart = pathParts[pi];
|
|
277
|
+
const patternPart = patternParts[pati];
|
|
278
|
+
|
|
279
|
+
if (patternPart.startsWith('**')) {
|
|
280
|
+
// Optional catch-all
|
|
281
|
+
const paramName = patternPart.slice(2);
|
|
282
|
+
params[paramName] = pathParts.slice(pi).join('/');
|
|
283
|
+
return params;
|
|
284
|
+
} else if (patternPart.startsWith('*')) {
|
|
285
|
+
// Catch-all
|
|
286
|
+
const paramName = patternPart.slice(1);
|
|
287
|
+
params[paramName] = pathParts.slice(pi).join('/');
|
|
288
|
+
return params;
|
|
289
|
+
} else if (patternPart.startsWith(':')) {
|
|
290
|
+
// Dynamic segment
|
|
291
|
+
const paramName = patternPart.slice(1);
|
|
292
|
+
params[paramName] = pathPart;
|
|
293
|
+
pi++;
|
|
294
|
+
pati++;
|
|
295
|
+
} else if (pathPart === patternPart) {
|
|
296
|
+
pi++;
|
|
297
|
+
pati++;
|
|
298
|
+
} else {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Handle optional catch-all at end
|
|
304
|
+
if (pati < patternParts.length && patternParts[pati].startsWith('**')) {
|
|
305
|
+
const paramName = patternParts[pati].slice(2);
|
|
306
|
+
params[paramName] = '';
|
|
307
|
+
return params;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (pi === pathParts.length && pati === patternParts.length) {
|
|
311
|
+
return params;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function renderPage(
|
|
318
|
+
content: string,
|
|
319
|
+
match: RouteInfo & { params: Record<string, string> },
|
|
320
|
+
_config: AeonConfig,
|
|
321
|
+
): Promise<string> {
|
|
322
|
+
// For dev mode, we do a simple transform
|
|
323
|
+
// In production, this would use the WASM runtime
|
|
324
|
+
|
|
325
|
+
// Check for 'use aeon' directive
|
|
326
|
+
const isAeon = match.isAeon;
|
|
327
|
+
|
|
328
|
+
// Extract the default export component
|
|
329
|
+
const componentMatch = content.match(/export\s+default\s+function\s+(\w+)/);
|
|
330
|
+
const componentName = componentMatch?.[1] || 'Page';
|
|
331
|
+
|
|
332
|
+
// Simple dev rendering - in production this uses React SSR
|
|
333
|
+
const html = `<!DOCTYPE html>
|
|
334
|
+
<html lang="en">
|
|
335
|
+
<head>
|
|
336
|
+
<meta charset="UTF-8">
|
|
337
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
338
|
+
<title>${componentName} - Aeon Flux</title>
|
|
339
|
+
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css" rel="stylesheet">
|
|
340
|
+
<script type="importmap">
|
|
341
|
+
{
|
|
342
|
+
"imports": {
|
|
343
|
+
"react": "https://esm.sh/react@18",
|
|
344
|
+
"react-dom": "https://esm.sh/react-dom@18",
|
|
345
|
+
"react-dom/client": "https://esm.sh/react-dom@18/client",
|
|
346
|
+
"@affectively/aeon-pages/react": "/__aeon_runtime.js"
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
</script>
|
|
350
|
+
</head>
|
|
351
|
+
<body>
|
|
352
|
+
<div id="root"></div>
|
|
353
|
+
${
|
|
354
|
+
isAeon
|
|
355
|
+
? `
|
|
356
|
+
<script type="module">
|
|
357
|
+
// Aeon Flux runtime
|
|
358
|
+
window.__AEON_PAGE__ = {
|
|
359
|
+
route: "${match.pattern}",
|
|
360
|
+
params: ${JSON.stringify(match.params)},
|
|
361
|
+
isAeon: true,
|
|
362
|
+
};
|
|
363
|
+
</script>
|
|
364
|
+
`
|
|
365
|
+
: ''
|
|
366
|
+
}
|
|
367
|
+
<script type="module">
|
|
368
|
+
// Dev mode hydration
|
|
369
|
+
import React from 'react';
|
|
370
|
+
import { createRoot } from 'react-dom/client';
|
|
371
|
+
|
|
372
|
+
// Simple placeholder for dev mode
|
|
373
|
+
const App = () => React.createElement('div', {
|
|
374
|
+
style: { padding: '2rem', fontFamily: 'system-ui' }
|
|
375
|
+
}, [
|
|
376
|
+
React.createElement('h1', { key: 'h1' }, '${componentName}'),
|
|
377
|
+
React.createElement('p', { key: 'p' }, 'Route: ${match.pattern}'),
|
|
378
|
+
${isAeon ? `React.createElement('p', { key: 'aeon', style: { color: 'green' } }, '✓ Aeon Flux enabled'),` : ''}
|
|
379
|
+
React.createElement('p', { key: 'note', style: { color: '#666', marginTop: '1rem' } },
|
|
380
|
+
'This is dev mode. Full rendering coming soon.')
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
const root = createRoot(document.getElementById('root'));
|
|
384
|
+
root.render(React.createElement(App));
|
|
385
|
+
</script>
|
|
386
|
+
</body>
|
|
387
|
+
</html>`;
|
|
388
|
+
|
|
389
|
+
return html;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function injectDevOverlay(html: string, port: number): string {
|
|
393
|
+
const script = `<script>
|
|
394
|
+
// Aeon Flux HMR
|
|
395
|
+
const ws = new WebSocket('ws://localhost:${port}/__aeon_hmr');
|
|
396
|
+
ws.onmessage = (e) => {
|
|
397
|
+
const msg = JSON.parse(e.data);
|
|
398
|
+
if (msg.type === 'reload') {
|
|
399
|
+
console.log('[Aeon Flux] Reloading:', msg.filename);
|
|
400
|
+
location.reload();
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
ws.onclose = () => {
|
|
404
|
+
console.log('[Aeon Flux] Server disconnected, reconnecting...');
|
|
405
|
+
setTimeout(() => location.reload(), 1000);
|
|
406
|
+
};
|
|
407
|
+
</script>`;
|
|
408
|
+
|
|
409
|
+
return html.replace('</body>', `${script}</body>`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function renderErrorPage(error: Error): string {
|
|
413
|
+
return `<!DOCTYPE html>
|
|
414
|
+
<html>
|
|
415
|
+
<head>
|
|
416
|
+
<title>Error - Aeon Flux</title>
|
|
417
|
+
<style>
|
|
418
|
+
body { font-family: system-ui; padding: 2rem; background: #1a1a1a; color: #fff; }
|
|
419
|
+
pre { background: #2d2d2d; padding: 1rem; border-radius: 8px; overflow: auto; }
|
|
420
|
+
h1 { color: #ff6b6b; }
|
|
421
|
+
</style>
|
|
422
|
+
</head>
|
|
423
|
+
<body>
|
|
424
|
+
<h1>⚠️ Error</h1>
|
|
425
|
+
<pre>${escapeHtml(error.message)}\n\n${escapeHtml(error.stack || '')}</pre>
|
|
426
|
+
</body>
|
|
427
|
+
</html>`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function escapeHtml(str: string): string {
|
|
431
|
+
return str
|
|
432
|
+
.replace(/&/g, '&')
|
|
433
|
+
.replace(/</g, '<')
|
|
434
|
+
.replace(/>/g, '>')
|
|
435
|
+
.replace(/"/g, '"');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function startWatcher(
|
|
439
|
+
pagesDir: string,
|
|
440
|
+
componentsDir: string,
|
|
441
|
+
callback: (event: string, filename: string) => void,
|
|
442
|
+
): { close: () => void } {
|
|
443
|
+
const watchers: ReturnType<typeof watch>[] = [];
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
watchers.push(
|
|
447
|
+
watch(pagesDir, { recursive: true }, (event, filename) => {
|
|
448
|
+
if (filename) callback(event, `pages/${filename}`);
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
} catch {
|
|
452
|
+
console.log('⚠️ Could not watch pages directory');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
watchers.push(
|
|
457
|
+
watch(componentsDir, { recursive: true }, (event, filename) => {
|
|
458
|
+
if (filename) callback(event, `components/${filename}`);
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
} catch {
|
|
462
|
+
// Components dir may not exist
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
close: () => watchers.forEach((w) => w.close()),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const DEV_OVERLAY_SCRIPT = `
|
|
471
|
+
// Aeon Flux Dev Overlay
|
|
472
|
+
console.log('[Aeon Flux] Dev mode active');
|
|
473
|
+
`;
|