@ereo/bundler 0.1.6

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/dist/index.js ADDED
@@ -0,0 +1,2965 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/dev/hmr.ts
5
+ var HMR_CLIENT_CODE = `
6
+ (function() {
7
+ const ws = new WebSocket('ws://' + location.host + '/__hmr');
8
+
9
+ // Module registry for hot updates
10
+ window.__EREO_HMR__ = window.__EREO_HMR__ || {
11
+ modules: new Map(),
12
+ islands: new Map(),
13
+ acceptedModules: new Set(),
14
+ };
15
+
16
+ ws.onmessage = function(event) {
17
+ const update = JSON.parse(event.data);
18
+ const startTime = performance.now();
19
+
20
+ // Log with timing info
21
+ const logUpdate = (msg) => {
22
+ const duration = (performance.now() - startTime).toFixed(1);
23
+ console.log('[HMR] ' + msg + ' (' + duration + 'ms)');
24
+ };
25
+
26
+ switch (update.type) {
27
+ case 'full-reload':
28
+ logHMRReason(update);
29
+ location.reload();
30
+ break;
31
+
32
+ case 'css-update':
33
+ updateCSS(update.path);
34
+ logUpdate('CSS updated: ' + update.path);
35
+ break;
36
+
37
+ case 'island-update':
38
+ if (handleIslandUpdate(update)) {
39
+ logUpdate('Island hot-updated: ' + (update.module?.id || update.path));
40
+ } else {
41
+ logHMRReason(update);
42
+ location.reload();
43
+ }
44
+ break;
45
+
46
+ case 'component-update':
47
+ if (handleComponentUpdate(update)) {
48
+ logUpdate('Component hot-updated: ' + (update.module?.id || update.path));
49
+ } else {
50
+ logHMRReason(update);
51
+ location.reload();
52
+ }
53
+ break;
54
+
55
+ case 'loader-update':
56
+ // Loaders require data refetch, do soft reload
57
+ logUpdate('Loader changed, refreshing data...');
58
+ refreshLoaderData(update.path);
59
+ break;
60
+
61
+ case 'js-update':
62
+ // Check if we can do granular update
63
+ if (update.module?.isIsland && handleIslandUpdate(update)) {
64
+ logUpdate('Island hot-updated: ' + (update.module?.id || update.path));
65
+ } else if (update.module?.isComponent && handleComponentUpdate(update)) {
66
+ logUpdate('Component hot-updated: ' + (update.module?.id || update.path));
67
+ } else {
68
+ logHMRReason(update);
69
+ location.reload();
70
+ }
71
+ break;
72
+
73
+ case 'error':
74
+ showErrorOverlay(update.error);
75
+ break;
76
+ }
77
+ };
78
+
79
+ ws.onclose = function() {
80
+ console.log('[HMR] Connection lost, attempting reconnect...');
81
+ setTimeout(function() {
82
+ location.reload();
83
+ }, 1000);
84
+ };
85
+
86
+ function updateCSS(path) {
87
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
88
+ for (const link of links) {
89
+ if (link.href.includes(path)) {
90
+ const newHref = link.href.split('?')[0] + '?t=' + Date.now();
91
+ link.href = newHref;
92
+ }
93
+ }
94
+ }
95
+
96
+ function handleIslandUpdate(update) {
97
+ const moduleId = update.module?.id || update.path;
98
+ if (!moduleId) return false;
99
+
100
+ // Find all island elements for this component
101
+ const componentName = moduleId.split('/').pop()?.replace(/\\.[jt]sx?$/, '');
102
+ if (!componentName) return false;
103
+
104
+ const islands = document.querySelectorAll('[data-component="' + componentName + '"]');
105
+ if (islands.length === 0) return false;
106
+
107
+ // Fetch the updated module and re-hydrate islands
108
+ return fetchAndRehydrate(moduleId, islands);
109
+ }
110
+
111
+ function handleComponentUpdate(update) {
112
+ // For now, component updates trigger a soft reload
113
+ // Future: implement React Fast Refresh integration
114
+ return false;
115
+ }
116
+
117
+ function fetchAndRehydrate(moduleId, islands) {
118
+ // Dynamic import with cache busting
119
+ const importUrl = '/' + moduleId + '?t=' + Date.now();
120
+
121
+ import(importUrl)
122
+ .then(function(module) {
123
+ const Component = module.default;
124
+ if (!Component) return;
125
+
126
+ // Re-render each island
127
+ islands.forEach(function(element) {
128
+ const propsJson = element.getAttribute('data-props');
129
+ const props = propsJson ? JSON.parse(propsJson) : {};
130
+
131
+ // Use React to re-render
132
+ if (window.__EREO_REACT__) {
133
+ const { createRoot } = window.__EREO_REACT_DOM__;
134
+ const { createElement } = window.__EREO_REACT__;
135
+
136
+ // Unmount existing
137
+ const existingRoot = window.__EREO_HMR__.islands.get(element);
138
+ if (existingRoot) {
139
+ existingRoot.unmount();
140
+ }
141
+
142
+ // Create new root and render
143
+ const root = createRoot(element);
144
+ root.render(createElement(Component, props));
145
+ window.__EREO_HMR__.islands.set(element, root);
146
+ }
147
+ });
148
+ })
149
+ .catch(function(err) {
150
+ console.error('[HMR] Failed to hot-update island:', err);
151
+ location.reload();
152
+ });
153
+
154
+ return true;
155
+ }
156
+
157
+ function refreshLoaderData(path) {
158
+ // Fetch fresh loader data and update the page
159
+ const routePath = path.replace(/\\/routes\\//, '/').replace(/\\.[jt]sx?$/, '');
160
+ fetch('/__ereo/loader-data' + routePath + '?t=' + Date.now())
161
+ .then(function(res) { return res.json(); })
162
+ .then(function(data) {
163
+ // Emit event for components to update
164
+ window.dispatchEvent(new CustomEvent('ereo:loader-update', {
165
+ detail: { path: routePath, data: data }
166
+ }));
167
+ })
168
+ .catch(function() {
169
+ location.reload();
170
+ });
171
+ }
172
+
173
+ function logHMRReason(update) {
174
+ if (update.reason) {
175
+ console.log('[HMR] ' + update.reason);
176
+ }
177
+ }
178
+
179
+ function showErrorOverlay(error) {
180
+ if (!error) return;
181
+
182
+ let overlay = document.getElementById('ereo-error-overlay');
183
+ if (!overlay) {
184
+ overlay = document.createElement('div');
185
+ overlay.id = 'ereo-error-overlay';
186
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.9);color:#ff5555;padding:2rem;font-family:monospace;white-space:pre-wrap;overflow:auto;z-index:99999';
187
+ document.body.appendChild(overlay);
188
+ }
189
+
190
+ overlay.innerHTML = '<h2 style="color:#ff5555;margin:0 0 1rem">Error</h2>' +
191
+ '<p style="color:#fff">' + escapeHtml(error.message) + '</p>' +
192
+ (error.stack ? '<pre style="color:#888;margin-top:1rem">' + escapeHtml(error.stack) + '</pre>' : '') +
193
+ '<button onclick="this.parentElement.remove()" style="position:absolute;top:1rem;right:1rem;background:none;border:1px solid #666;color:#fff;padding:0.5rem 1rem;cursor:pointer">Close</button>';
194
+ }
195
+
196
+ function escapeHtml(str) {
197
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
198
+ }
199
+
200
+ // Clear error overlay on successful update
201
+ ws.addEventListener('message', function(event) {
202
+ const update = JSON.parse(event.data);
203
+ if (update.type !== 'error') {
204
+ const overlay = document.getElementById('ereo-error-overlay');
205
+ if (overlay) overlay.remove();
206
+ }
207
+ });
208
+ })();
209
+ `;
210
+
211
+ class HMRServer {
212
+ clients = new Set;
213
+ lastUpdate = null;
214
+ depGraph;
215
+ moduleAnalyzer;
216
+ constructor() {
217
+ this.depGraph = {
218
+ dependents: new Map,
219
+ dependencies: new Map,
220
+ exports: new Map,
221
+ islands: new Set,
222
+ routes: new Set
223
+ };
224
+ this.moduleAnalyzer = new ModuleAnalyzer;
225
+ }
226
+ handleConnection(ws) {
227
+ this.clients.add(ws);
228
+ if (this.lastUpdate?.type === "error") {
229
+ ws.send(JSON.stringify(this.lastUpdate));
230
+ }
231
+ }
232
+ handleClose(ws) {
233
+ this.clients.delete(ws);
234
+ }
235
+ send(update) {
236
+ this.lastUpdate = update;
237
+ const message = JSON.stringify(update);
238
+ for (const client of this.clients) {
239
+ try {
240
+ client.send(message);
241
+ } catch {
242
+ this.clients.delete(client);
243
+ }
244
+ }
245
+ }
246
+ reload(reason) {
247
+ this.send({
248
+ type: "full-reload",
249
+ timestamp: Date.now(),
250
+ reason: reason || "Full reload triggered"
251
+ });
252
+ }
253
+ cssUpdate(path) {
254
+ this.send({
255
+ type: "css-update",
256
+ path,
257
+ timestamp: Date.now()
258
+ });
259
+ }
260
+ async jsUpdate(path) {
261
+ const analysis = await this.moduleAnalyzer.analyze(path);
262
+ if (analysis.isIsland) {
263
+ this.send({
264
+ type: "island-update",
265
+ path,
266
+ timestamp: Date.now(),
267
+ module: {
268
+ id: path,
269
+ exports: analysis.exports,
270
+ isIsland: true
271
+ },
272
+ reason: `Island component changed: ${path}`
273
+ });
274
+ return;
275
+ }
276
+ if (analysis.isLoader && !analysis.isComponent) {
277
+ this.send({
278
+ type: "loader-update",
279
+ path,
280
+ timestamp: Date.now(),
281
+ module: {
282
+ id: path,
283
+ exports: analysis.exports,
284
+ isLoader: true
285
+ },
286
+ reason: `Loader changed: ${path}`
287
+ });
288
+ return;
289
+ }
290
+ if (analysis.isComponent && !analysis.hasNonComponentExports) {
291
+ this.send({
292
+ type: "component-update",
293
+ path,
294
+ timestamp: Date.now(),
295
+ module: {
296
+ id: path,
297
+ exports: analysis.exports,
298
+ isComponent: true
299
+ },
300
+ reason: `Component changed: ${path}`
301
+ });
302
+ return;
303
+ }
304
+ this.send({
305
+ type: "js-update",
306
+ path,
307
+ timestamp: Date.now(),
308
+ module: {
309
+ id: path,
310
+ exports: analysis.exports,
311
+ isIsland: analysis.isIsland,
312
+ isLoader: analysis.isLoader,
313
+ isAction: analysis.isAction,
314
+ isComponent: analysis.isComponent
315
+ },
316
+ reason: this.getReloadReason(path, analysis)
317
+ });
318
+ }
319
+ getReloadReason(path, analysis) {
320
+ const reasons = [];
321
+ if (analysis.hasNonComponentExports) {
322
+ reasons.push(`exports changed (${analysis.exports.join(", ")})`);
323
+ }
324
+ if (analysis.isLoader && analysis.isComponent) {
325
+ reasons.push("mixed loader and component in same file");
326
+ }
327
+ if (path.includes("_layout") || path.includes("_error")) {
328
+ reasons.push("layout/error boundary changed");
329
+ }
330
+ if (reasons.length === 0) {
331
+ reasons.push("module structure changed");
332
+ }
333
+ return `Full reload: ${path} - ${reasons.join(", ")}`;
334
+ }
335
+ registerModule(moduleId, info) {
336
+ if (info.dependencies) {
337
+ this.depGraph.dependencies.set(moduleId, new Set(info.dependencies));
338
+ for (const dep of info.dependencies) {
339
+ if (!this.depGraph.dependents.has(dep)) {
340
+ this.depGraph.dependents.set(dep, new Set);
341
+ }
342
+ this.depGraph.dependents.get(dep).add(moduleId);
343
+ }
344
+ }
345
+ if (info.exports) {
346
+ this.depGraph.exports.set(moduleId, new Set(info.exports));
347
+ }
348
+ if (info.isIsland) {
349
+ this.depGraph.islands.add(moduleId);
350
+ }
351
+ if (info.isRoute) {
352
+ this.depGraph.routes.add(moduleId);
353
+ }
354
+ }
355
+ canHotUpdate(moduleId) {
356
+ if (this.depGraph.islands.has(moduleId)) {
357
+ return true;
358
+ }
359
+ const dependents = this.depGraph.dependents.get(moduleId);
360
+ if (dependents) {
361
+ for (const dep of dependents) {
362
+ if (this.depGraph.routes.has(dep)) {
363
+ return false;
364
+ }
365
+ }
366
+ }
367
+ return true;
368
+ }
369
+ error(message, stack) {
370
+ this.send({
371
+ type: "error",
372
+ timestamp: Date.now(),
373
+ error: { message, stack }
374
+ });
375
+ }
376
+ clearError() {
377
+ if (this.lastUpdate?.type === "error") {
378
+ this.lastUpdate = null;
379
+ }
380
+ }
381
+ getClientCount() {
382
+ return this.clients.size;
383
+ }
384
+ getDependencyGraph() {
385
+ return this.depGraph;
386
+ }
387
+ }
388
+
389
+ class ModuleAnalyzer {
390
+ cache = new Map;
391
+ async analyze(filePath) {
392
+ try {
393
+ const file = Bun.file(filePath);
394
+ const stat = await file.stat();
395
+ const mtime = stat?.mtime?.getTime() || 0;
396
+ const cached = this.cache.get(filePath);
397
+ if (cached && cached.mtime === mtime) {
398
+ return cached.analysis;
399
+ }
400
+ const content = await file.text();
401
+ const analysis = this.analyzeContent(content, filePath);
402
+ this.cache.set(filePath, { analysis, mtime });
403
+ return analysis;
404
+ } catch {
405
+ return {
406
+ exports: [],
407
+ isIsland: false,
408
+ isComponent: false,
409
+ isLoader: false,
410
+ isAction: false,
411
+ hasNonComponentExports: true
412
+ };
413
+ }
414
+ }
415
+ analyzeContent(content, filePath) {
416
+ const exports = [];
417
+ let isIsland = false;
418
+ let isComponent = false;
419
+ let isLoader = false;
420
+ let isAction = false;
421
+ isIsland = content.includes("client:load") || content.includes("client:idle") || content.includes("client:visible") || content.includes("client:media") || content.includes("data-island") || content.includes("createIsland(") || filePath.includes("/islands/");
422
+ isLoader = content.includes("export const loader") || content.includes("export async function loader") || content.includes("export function loader");
423
+ isAction = content.includes("export const action") || content.includes("export async function action") || content.includes("export function action");
424
+ isComponent = content.includes("export default function") || content.includes("export default class") || /export\s+default\s+\w+/.test(content);
425
+ const exportMatches = content.matchAll(/export\s+(?:const|let|var|function|async\s+function|class)\s+(\w+)/g);
426
+ for (const match of exportMatches) {
427
+ exports.push(match[1]);
428
+ }
429
+ if (content.includes("export default")) {
430
+ exports.push("default");
431
+ }
432
+ const namedExportMatch = content.match(/export\s*{([^}]+)}/g);
433
+ if (namedExportMatch) {
434
+ for (const match of namedExportMatch) {
435
+ const names = match.replace(/export\s*{/, "").replace("}", "").split(",");
436
+ for (const name of names) {
437
+ const cleanName = name.trim().split(" as ")[0].trim();
438
+ if (cleanName)
439
+ exports.push(cleanName);
440
+ }
441
+ }
442
+ }
443
+ const componentExports = new Set(["default", "loader", "action", "meta", "headers", "config", "handle", "ErrorBoundary"]);
444
+ const hasNonComponentExports = exports.some((e) => !componentExports.has(e));
445
+ return {
446
+ exports,
447
+ isIsland,
448
+ isComponent,
449
+ isLoader,
450
+ isAction,
451
+ hasNonComponentExports
452
+ };
453
+ }
454
+ clearCache() {
455
+ this.cache.clear();
456
+ }
457
+ }
458
+ function createHMRServer() {
459
+ return new HMRServer;
460
+ }
461
+ function createHMRWebSocket(hmr) {
462
+ return {
463
+ open(ws) {
464
+ hmr.handleConnection(ws);
465
+ },
466
+ close(ws) {
467
+ hmr.handleClose(ws);
468
+ },
469
+ message() {}
470
+ };
471
+ }
472
+
473
+ class HMRWatcher {
474
+ hmr;
475
+ watching = false;
476
+ debounceTimer = null;
477
+ pendingChanges = new Set;
478
+ watchDir = "";
479
+ constructor(hmr) {
480
+ this.hmr = hmr;
481
+ }
482
+ watch(dir) {
483
+ if (this.watching)
484
+ return;
485
+ this.watching = true;
486
+ this.watchDir = dir;
487
+ try {
488
+ const { watch } = __require("fs");
489
+ watch(dir, { recursive: true }, (event, filename) => {
490
+ if (!filename)
491
+ return;
492
+ if (filename.startsWith(".") || filename.includes("node_modules"))
493
+ return;
494
+ this.pendingChanges.add(filename);
495
+ if (this.debounceTimer) {
496
+ clearTimeout(this.debounceTimer);
497
+ }
498
+ this.debounceTimer = setTimeout(() => {
499
+ this.processPendingChanges();
500
+ }, 50);
501
+ });
502
+ } catch (error) {
503
+ console.warn("File watching not available:", error);
504
+ }
505
+ }
506
+ async processPendingChanges() {
507
+ const changes = Array.from(this.pendingChanges);
508
+ this.pendingChanges.clear();
509
+ const cssChanges = [];
510
+ const jsChanges = [];
511
+ const configChanges = [];
512
+ const otherChanges = [];
513
+ for (const filename of changes) {
514
+ const ext = filename.split(".").pop()?.toLowerCase();
515
+ switch (ext) {
516
+ case "css":
517
+ case "scss":
518
+ case "less":
519
+ cssChanges.push(filename);
520
+ break;
521
+ case "ts":
522
+ case "tsx":
523
+ case "js":
524
+ case "jsx":
525
+ if (filename.includes(".config.") || filename === "ereo.config.ts") {
526
+ configChanges.push(filename);
527
+ } else {
528
+ jsChanges.push(filename);
529
+ }
530
+ break;
531
+ case "json":
532
+ if (filename === "package.json" || filename === "tsconfig.json") {
533
+ configChanges.push(filename);
534
+ } else {
535
+ otherChanges.push(filename);
536
+ }
537
+ break;
538
+ default:
539
+ otherChanges.push(filename);
540
+ }
541
+ }
542
+ if (configChanges.length > 0) {
543
+ this.hmr.reload(`Config changed: ${configChanges.join(", ")}`);
544
+ return;
545
+ }
546
+ for (const css of cssChanges) {
547
+ this.hmr.cssUpdate(css);
548
+ }
549
+ for (const js of jsChanges) {
550
+ const fullPath = `${this.watchDir}/${js}`;
551
+ await this.hmr.jsUpdate(fullPath);
552
+ }
553
+ if (otherChanges.length > 0 && cssChanges.length === 0 && jsChanges.length === 0) {
554
+ this.hmr.reload(`Files changed: ${otherChanges.join(", ")}`);
555
+ }
556
+ }
557
+ stop() {
558
+ this.watching = false;
559
+ if (this.debounceTimer) {
560
+ clearTimeout(this.debounceTimer);
561
+ }
562
+ this.pendingChanges.clear();
563
+ }
564
+ }
565
+ function createHMRWatcher(hmr) {
566
+ return new HMRWatcher(hmr);
567
+ }
568
+ // src/dev/error-overlay.ts
569
+ function parseError(error) {
570
+ if (typeof error === "string") {
571
+ return {
572
+ message: error,
573
+ type: "runtime"
574
+ };
575
+ }
576
+ const info = {
577
+ message: error.message,
578
+ stack: error.stack,
579
+ type: "runtime"
580
+ };
581
+ if (error.stack) {
582
+ const match = error.stack.match(/at\s+.+\((.+):(\d+):(\d+)\)/);
583
+ if (match) {
584
+ info.source = {
585
+ file: match[1],
586
+ line: parseInt(match[2], 10),
587
+ column: parseInt(match[3], 10)
588
+ };
589
+ }
590
+ }
591
+ if (error.name === "SyntaxError") {
592
+ info.type = "syntax";
593
+ } else if (error.name === "TypeError") {
594
+ info.type = "type";
595
+ }
596
+ return info;
597
+ }
598
+ function generateErrorOverlayHTML(error) {
599
+ const escapeHtml = (str) => str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
600
+ const typeColors = {
601
+ runtime: "#ff5555",
602
+ build: "#ffaa00",
603
+ syntax: "#ff55ff",
604
+ type: "#5555ff"
605
+ };
606
+ const typeLabels = {
607
+ runtime: "Runtime Error",
608
+ build: "Build Error",
609
+ syntax: "Syntax Error",
610
+ type: "Type Error"
611
+ };
612
+ const sourceSection = error.source ? `
613
+ <div style="margin-top: 1rem; padding: 1rem; background: #1a1a1a; border-radius: 4px;">
614
+ <div style="color: #888; margin-bottom: 0.5rem;">
615
+ ${escapeHtml(error.source.file)}:${error.source.line}:${error.source.column}
616
+ </div>
617
+ ${error.source.code ? `<pre style="color: #fff; margin: 0;">${escapeHtml(error.source.code)}</pre>` : ""}
618
+ </div>
619
+ ` : "";
620
+ const stackSection = error.stack ? `
621
+ <details style="margin-top: 1rem;">
622
+ <summary style="cursor: pointer; color: #888;">Stack Trace</summary>
623
+ <pre style="color: #666; margin-top: 0.5rem; white-space: pre-wrap;">${escapeHtml(error.stack)}</pre>
624
+ </details>
625
+ ` : "";
626
+ return `
627
+ <!DOCTYPE html>
628
+ <html>
629
+ <head>
630
+ <meta charset="utf-8">
631
+ <title>Error - EreoJS Dev</title>
632
+ <style>
633
+ * { box-sizing: border-box; }
634
+ body {
635
+ margin: 0;
636
+ padding: 2rem;
637
+ background: #111;
638
+ color: #fff;
639
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
640
+ min-height: 100vh;
641
+ }
642
+ .container {
643
+ max-width: 800px;
644
+ margin: 0 auto;
645
+ }
646
+ .badge {
647
+ display: inline-block;
648
+ padding: 0.25rem 0.75rem;
649
+ border-radius: 4px;
650
+ font-size: 0.75rem;
651
+ font-weight: 600;
652
+ text-transform: uppercase;
653
+ letter-spacing: 0.05em;
654
+ }
655
+ h1 {
656
+ margin: 1rem 0;
657
+ font-size: 1.5rem;
658
+ font-weight: 500;
659
+ }
660
+ pre {
661
+ overflow-x: auto;
662
+ }
663
+ </style>
664
+ </head>
665
+ <body>
666
+ <div class="container">
667
+ <div class="badge" style="background: ${typeColors[error.type]}20; color: ${typeColors[error.type]};">
668
+ ${typeLabels[error.type]}
669
+ </div>
670
+ <h1>${escapeHtml(error.message)}</h1>
671
+ ${sourceSection}
672
+ ${stackSection}
673
+ <p style="margin-top: 2rem; color: #666; font-size: 0.875rem;">
674
+ Fix the error and save the file to see changes.
675
+ </p>
676
+ </div>
677
+ </body>
678
+ </html>
679
+ `.trim();
680
+ }
681
+ function createErrorResponse(error) {
682
+ const info = parseError(error);
683
+ const html = generateErrorOverlayHTML(info);
684
+ return new Response(html, {
685
+ status: 500,
686
+ headers: {
687
+ "Content-Type": "text/html; charset=utf-8"
688
+ }
689
+ });
690
+ }
691
+ function createErrorJSON(error) {
692
+ const info = parseError(error);
693
+ return new Response(JSON.stringify(info), {
694
+ status: 500,
695
+ headers: {
696
+ "Content-Type": "application/json"
697
+ }
698
+ });
699
+ }
700
+ var ERROR_OVERLAY_SCRIPT = `
701
+ <script>
702
+ (function() {
703
+ window.addEventListener('error', function(event) {
704
+ showOverlay({
705
+ message: event.message,
706
+ source: {
707
+ file: event.filename,
708
+ line: event.lineno,
709
+ column: event.colno,
710
+ },
711
+ type: 'runtime',
712
+ });
713
+ });
714
+
715
+ window.addEventListener('unhandledrejection', function(event) {
716
+ const error = event.reason;
717
+ showOverlay({
718
+ message: error instanceof Error ? error.message : String(error),
719
+ stack: error instanceof Error ? error.stack : undefined,
720
+ type: 'runtime',
721
+ });
722
+ });
723
+
724
+ function showOverlay(error) {
725
+ let overlay = document.getElementById('ereo-error-overlay');
726
+ if (overlay) overlay.remove();
727
+
728
+ overlay = document.createElement('div');
729
+ overlay.id = 'ereo-error-overlay';
730
+ overlay.innerHTML = \`
731
+ <style>
732
+ #ereo-error-overlay {
733
+ position: fixed;
734
+ inset: 0;
735
+ background: rgba(0,0,0,0.95);
736
+ color: #fff;
737
+ padding: 2rem;
738
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
739
+ overflow: auto;
740
+ z-index: 99999;
741
+ }
742
+ #ereo-error-overlay .close {
743
+ position: absolute;
744
+ top: 1rem;
745
+ right: 1rem;
746
+ background: none;
747
+ border: 1px solid #666;
748
+ color: #fff;
749
+ padding: 0.5rem 1rem;
750
+ cursor: pointer;
751
+ border-radius: 4px;
752
+ }
753
+ #ereo-error-overlay .close:hover {
754
+ background: #333;
755
+ }
756
+ #ereo-error-overlay h2 {
757
+ color: #ff5555;
758
+ margin: 0 0 1rem;
759
+ }
760
+ #ereo-error-overlay pre {
761
+ background: #1a1a1a;
762
+ padding: 1rem;
763
+ border-radius: 4px;
764
+ overflow-x: auto;
765
+ color: #888;
766
+ }
767
+ </style>
768
+ <button class="close" onclick="this.parentElement.remove()">Close (Esc)</button>
769
+ <h2>\${escapeHtml(error.message)}</h2>
770
+ \${error.source ? '<p style="color:#888">' + escapeHtml(error.source.file) + ':' + error.source.line + '</p>' : ''}
771
+ \${error.stack ? '<pre>' + escapeHtml(error.stack) + '</pre>' : ''}
772
+ \`;
773
+
774
+ document.body.appendChild(overlay);
775
+
776
+ document.addEventListener('keydown', function handler(e) {
777
+ if (e.key === 'Escape') {
778
+ overlay.remove();
779
+ document.removeEventListener('keydown', handler);
780
+ }
781
+ });
782
+ }
783
+
784
+ function escapeHtml(str) {
785
+ if (!str) return '';
786
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
787
+ }
788
+ })();
789
+ </script>
790
+ `;
791
+ // src/prod/build.ts
792
+ import { join as join2, relative, dirname as dirname2, basename as basename2, extname } from "path";
793
+ import { mkdir, rm, readdir, stat, copyFile } from "fs/promises";
794
+ import { initFileRouter } from "@ereo/router";
795
+
796
+ // src/plugins/islands.ts
797
+ import { basename } from "path";
798
+ var ISLAND_DIRECTIVE_PATTERN = /client:(load|idle|visible|media)(?:="([^"]+)")?/g;
799
+ var COMPONENT_EXPORT_PATTERN = /export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)/g;
800
+ var USE_CLIENT_PATTERN = /^['"]use client['"]/m;
801
+ function extractIslands(content, filePath) {
802
+ const islands = [];
803
+ const isClientComponent = USE_CLIENT_PATTERN.test(content);
804
+ if (!isClientComponent) {
805
+ const hasDirectives = ISLAND_DIRECTIVE_PATTERN.test(content);
806
+ if (!hasDirectives) {
807
+ return islands;
808
+ }
809
+ }
810
+ const componentNames = [];
811
+ let match;
812
+ COMPONENT_EXPORT_PATTERN.lastIndex = 0;
813
+ while ((match = COMPONENT_EXPORT_PATTERN.exec(content)) !== null) {
814
+ componentNames.push(match[1]);
815
+ }
816
+ const fileName = basename(filePath, ".tsx").replace(".ts", "");
817
+ if (isClientComponent && componentNames.length > 0) {
818
+ islands.push({
819
+ id: generateIslandId(filePath),
820
+ name: componentNames[0],
821
+ file: filePath,
822
+ strategy: "load",
823
+ exports: componentNames
824
+ });
825
+ }
826
+ return islands;
827
+ }
828
+ function generateIslandId(filePath) {
829
+ return filePath.replace(/[\/\\]/g, "_").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_]/g, "");
830
+ }
831
+ function transformIslandJSX(code) {
832
+ let transformed = code;
833
+ transformed = transformed.replace(/<(\w+)\s+([^>]*client:(load|idle|visible|media)[^>]*)>/g, (match, tag, props, strategy) => {
834
+ const id = `island-${Math.random().toString(36).slice(2, 8)}`;
835
+ return `<${tag} data-island="${id}" data-strategy="${strategy}" ${props}>`;
836
+ });
837
+ return transformed;
838
+ }
839
+ function generateIslandManifest(islands) {
840
+ const manifest = {};
841
+ for (const island of islands) {
842
+ manifest[island.id] = {
843
+ name: island.name,
844
+ file: island.file,
845
+ strategy: island.strategy,
846
+ media: island.media,
847
+ exports: island.exports
848
+ };
849
+ }
850
+ return JSON.stringify(manifest, null, 2);
851
+ }
852
+ function generateIslandEntry(islands) {
853
+ const imports = [];
854
+ const registrations = [];
855
+ for (const island of islands) {
856
+ const importName = `Island_${island.id}`;
857
+ imports.push(`import ${importName} from '${island.file}';`);
858
+ registrations.push(` registerIslandComponent('${island.name}', ${importName});`);
859
+ }
860
+ return `
861
+ import { registerIslandComponent, initializeIslands } from '@ereo/client';
862
+
863
+ // Import all islands
864
+ ${imports.join(`
865
+ `)}
866
+
867
+ // Register islands
868
+ ${registrations.join(`
869
+ `)}
870
+
871
+ // Initialize
872
+ initializeIslands();
873
+ `.trim();
874
+ }
875
+ function createIslandsPlugin() {
876
+ const islands = [];
877
+ return {
878
+ name: "ereo:islands",
879
+ transform(code, id) {
880
+ if (!id.endsWith(".tsx") && !id.endsWith(".jsx")) {
881
+ return null;
882
+ }
883
+ const fileIslands = extractIslands(code, id);
884
+ islands.push(...fileIslands);
885
+ if (fileIslands.length > 0) {
886
+ return transformIslandJSX(code);
887
+ }
888
+ return null;
889
+ },
890
+ async buildEnd() {
891
+ if (islands.length === 0) {
892
+ return;
893
+ }
894
+ console.log(`Found ${islands.length} island(s)`);
895
+ const manifest = generateIslandManifest(islands);
896
+ await Bun.write(".ereo/islands.json", manifest);
897
+ const entry = generateIslandEntry(islands);
898
+ await Bun.write(".ereo/islands.entry.ts", entry);
899
+ }
900
+ };
901
+ }
902
+ function findIslandByName(islands, name) {
903
+ return islands.find((i) => i.name === name);
904
+ }
905
+ function hasIslands(content) {
906
+ return USE_CLIENT_PATTERN.test(content) || ISLAND_DIRECTIVE_PATTERN.test(content);
907
+ }
908
+
909
+ // src/prod/build.ts
910
+ var DEFAULT_ASSET_EXTENSIONS = [
911
+ ".png",
912
+ ".jpg",
913
+ ".jpeg",
914
+ ".gif",
915
+ ".svg",
916
+ ".ico",
917
+ ".webp",
918
+ ".avif",
919
+ ".woff",
920
+ ".woff2",
921
+ ".ttf",
922
+ ".eot",
923
+ ".otf",
924
+ ".mp3",
925
+ ".mp4",
926
+ ".webm",
927
+ ".ogg",
928
+ ".wav",
929
+ ".json",
930
+ ".xml",
931
+ ".txt",
932
+ ".pdf"
933
+ ];
934
+ async function build(options = {}) {
935
+ const startTime = performance.now();
936
+ const root = options.root || process.cwd();
937
+ const outDir = options.outDir || join2(root, ".ereo");
938
+ const minify = options.minify ?? true;
939
+ const sourcemap = options.sourcemap ?? true;
940
+ const splitting = options.splitting ?? true;
941
+ const publicPath = options.publicPath || "/_ereo/";
942
+ const assetExtensions = options.assetExtensions || DEFAULT_ASSET_EXTENSIONS;
943
+ const buildId = generateBuildId();
944
+ const allOutputs = [];
945
+ const errors = [];
946
+ console.log(`\x1B[36m\u26A1\x1B[0m Building for production...
947
+ `);
948
+ try {
949
+ await cleanAndCreateDirs(outDir);
950
+ if (options.plugins) {
951
+ for (const plugin of options.plugins) {
952
+ await plugin.buildStart?.();
953
+ }
954
+ }
955
+ const router = await initFileRouter({
956
+ routesDir: join2(root, "app/routes")
957
+ });
958
+ const routes = router.getRoutes();
959
+ const routeCount = countRoutes(routes);
960
+ console.log(` \x1B[32m\u2713\x1B[0m Found ${routeCount} route(s)`);
961
+ const islands = await extractAllIslands(root, routes);
962
+ if (islands.length > 0) {
963
+ console.log(` \x1B[32m\u2713\x1B[0m Found ${islands.length} island(s)`);
964
+ }
965
+ const cssFiles = await collectCSSFiles(root);
966
+ if (cssFiles.length > 0) {
967
+ console.log(` \x1B[32m\u2713\x1B[0m Found ${cssFiles.length} CSS file(s)`);
968
+ }
969
+ console.log(`
970
+ Building server bundle...`);
971
+ const serverResult = await buildServer({
972
+ root,
973
+ outDir: join2(outDir, "server"),
974
+ routes,
975
+ minify,
976
+ sourcemap,
977
+ splitting,
978
+ external: options.external,
979
+ plugins: options.plugins
980
+ });
981
+ if (serverResult.errors.length > 0) {
982
+ errors.push(...serverResult.errors);
983
+ }
984
+ allOutputs.push(...serverResult.outputs);
985
+ console.log(` \x1B[32m\u2713\x1B[0m Server bundle built (${serverResult.outputs.length} files)`);
986
+ console.log(`
987
+ Building client bundle...`);
988
+ const clientResult = await buildClient({
989
+ root,
990
+ outDir: join2(outDir, "client"),
991
+ routes,
992
+ minify,
993
+ sourcemap,
994
+ splitting,
995
+ plugins: options.plugins
996
+ });
997
+ if (clientResult.errors.length > 0) {
998
+ errors.push(...clientResult.errors);
999
+ }
1000
+ allOutputs.push(...clientResult.outputs);
1001
+ console.log(` \x1B[32m\u2713\x1B[0m Client bundle built (${clientResult.outputs.length} files)`);
1002
+ if (islands.length > 0) {
1003
+ console.log(`
1004
+ Building island bundles...`);
1005
+ const islandResult = await buildIslands({
1006
+ root,
1007
+ outDir: join2(outDir, "client/islands"),
1008
+ islands,
1009
+ minify,
1010
+ sourcemap,
1011
+ splitting
1012
+ });
1013
+ if (islandResult.errors.length > 0) {
1014
+ errors.push(...islandResult.errors);
1015
+ }
1016
+ allOutputs.push(...islandResult.outputs);
1017
+ console.log(` \x1B[32m\u2713\x1B[0m Island bundles built (${islandResult.outputs.length} files)`);
1018
+ }
1019
+ if (cssFiles.length > 0) {
1020
+ console.log(`
1021
+ Building CSS bundle...`);
1022
+ const cssResult = await buildCSS({
1023
+ root,
1024
+ outDir: join2(outDir, "assets"),
1025
+ cssFiles,
1026
+ minify,
1027
+ sourcemap,
1028
+ plugins: options.plugins
1029
+ });
1030
+ if (cssResult.errors.length > 0) {
1031
+ errors.push(...cssResult.errors);
1032
+ }
1033
+ allOutputs.push(...cssResult.outputs);
1034
+ console.log(` \x1B[32m\u2713\x1B[0m CSS bundle built (${cssResult.outputs.length} files)`);
1035
+ }
1036
+ console.log(`
1037
+ Copying static assets...`);
1038
+ const assetResult = await copyAssets({
1039
+ root,
1040
+ outDir: join2(outDir, "assets"),
1041
+ extensions: assetExtensions
1042
+ });
1043
+ allOutputs.push(...assetResult.outputs);
1044
+ if (assetResult.outputs.length > 0) {
1045
+ console.log(` \x1B[32m\u2713\x1B[0m Copied ${assetResult.outputs.length} static assets`);
1046
+ }
1047
+ console.log(`
1048
+ Generating manifest...`);
1049
+ await generateManifest({
1050
+ outDir,
1051
+ buildId,
1052
+ routes,
1053
+ islands,
1054
+ serverResult,
1055
+ clientResult,
1056
+ cssFiles: allOutputs.filter((o) => o.type === "css").map((o) => o.path)
1057
+ });
1058
+ console.log(` \x1B[32m\u2713\x1B[0m Manifest generated`);
1059
+ if (options.plugins) {
1060
+ for (const plugin of options.plugins) {
1061
+ await plugin.buildEnd?.();
1062
+ }
1063
+ }
1064
+ const duration = performance.now() - startTime;
1065
+ const success = errors.length === 0;
1066
+ console.log(`
1067
+ \x1B[32m\u2713\x1B[0m Build ${success ? "completed" : "completed with warnings"} in ${duration.toFixed(0)}ms`);
1068
+ return {
1069
+ success,
1070
+ outputs: allOutputs,
1071
+ duration,
1072
+ errors: errors.length > 0 ? errors : undefined
1073
+ };
1074
+ } catch (error) {
1075
+ const duration = performance.now() - startTime;
1076
+ const errorMessage = error instanceof Error ? error.message : String(error);
1077
+ console.error(`
1078
+ \x1B[31m\u2717\x1B[0m Build failed:`, errorMessage);
1079
+ return {
1080
+ success: false,
1081
+ outputs: allOutputs,
1082
+ duration,
1083
+ errors: [errorMessage]
1084
+ };
1085
+ }
1086
+ }
1087
+ function generateBuildId() {
1088
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
1089
+ }
1090
+ async function cleanAndCreateDirs(outDir) {
1091
+ await rm(outDir, { recursive: true, force: true });
1092
+ await mkdir(outDir, { recursive: true });
1093
+ await mkdir(join2(outDir, "server"), { recursive: true });
1094
+ await mkdir(join2(outDir, "server/routes"), { recursive: true });
1095
+ await mkdir(join2(outDir, "client"), { recursive: true });
1096
+ await mkdir(join2(outDir, "client/islands"), { recursive: true });
1097
+ await mkdir(join2(outDir, "client/chunks"), { recursive: true });
1098
+ await mkdir(join2(outDir, "assets"), { recursive: true });
1099
+ }
1100
+ function countRoutes(routes) {
1101
+ let count = 0;
1102
+ for (const route of routes) {
1103
+ count++;
1104
+ if (route.children) {
1105
+ count += countRoutes(route.children);
1106
+ }
1107
+ }
1108
+ return count;
1109
+ }
1110
+ async function extractAllIslands(root, routes) {
1111
+ const islands = [];
1112
+ const processedFiles = new Set;
1113
+ const processRoute = async (route) => {
1114
+ if (processedFiles.has(route.file))
1115
+ return;
1116
+ processedFiles.add(route.file);
1117
+ try {
1118
+ const content = await Bun.file(route.file).text();
1119
+ if (hasIslands(content)) {
1120
+ const fileIslands = extractIslands(content, route.file);
1121
+ islands.push(...fileIslands);
1122
+ }
1123
+ } catch (error) {}
1124
+ if (route.children) {
1125
+ for (const child of route.children) {
1126
+ await processRoute(child);
1127
+ }
1128
+ }
1129
+ };
1130
+ const componentsDir = join2(root, "app/components");
1131
+ try {
1132
+ const componentFiles = await scanForFiles(componentsDir, [".tsx", ".jsx"]);
1133
+ for (const file of componentFiles) {
1134
+ if (processedFiles.has(file))
1135
+ continue;
1136
+ processedFiles.add(file);
1137
+ try {
1138
+ const content = await Bun.file(file).text();
1139
+ if (hasIslands(content)) {
1140
+ const fileIslands = extractIslands(content, file);
1141
+ islands.push(...fileIslands);
1142
+ }
1143
+ } catch (error) {}
1144
+ }
1145
+ } catch (error) {}
1146
+ for (const route of routes) {
1147
+ await processRoute(route);
1148
+ }
1149
+ return islands;
1150
+ }
1151
+ async function scanForFiles(dir, extensions) {
1152
+ const files = [];
1153
+ try {
1154
+ const entries = await readdir(dir, { withFileTypes: true });
1155
+ for (const entry of entries) {
1156
+ const fullPath = join2(dir, entry.name);
1157
+ if (entry.isDirectory()) {
1158
+ const subFiles = await scanForFiles(fullPath, extensions);
1159
+ files.push(...subFiles);
1160
+ } else if (entry.isFile()) {
1161
+ const ext = extname(entry.name);
1162
+ if (extensions.includes(ext)) {
1163
+ files.push(fullPath);
1164
+ }
1165
+ }
1166
+ }
1167
+ } catch (error) {}
1168
+ return files;
1169
+ }
1170
+ async function collectCSSFiles(root) {
1171
+ const cssFiles = [];
1172
+ const dirsToScan = [
1173
+ join2(root, "app"),
1174
+ join2(root, "styles"),
1175
+ join2(root, "src")
1176
+ ];
1177
+ for (const dir of dirsToScan) {
1178
+ try {
1179
+ const files = await scanForFiles(dir, [".css"]);
1180
+ cssFiles.push(...files);
1181
+ } catch (error) {}
1182
+ }
1183
+ return cssFiles;
1184
+ }
1185
+ async function buildServer(options) {
1186
+ const {
1187
+ root,
1188
+ outDir,
1189
+ routes,
1190
+ minify = true,
1191
+ sourcemap = true,
1192
+ splitting = true,
1193
+ external = [],
1194
+ plugins
1195
+ } = options;
1196
+ const outputs = [];
1197
+ const errors = [];
1198
+ const routeModules = {};
1199
+ const entrypoints = [];
1200
+ const routesDir = join2(outDir, "routes");
1201
+ const collectEntrypoints = (routeList) => {
1202
+ for (const route of routeList) {
1203
+ entrypoints.push(route.file);
1204
+ if (route.children) {
1205
+ collectEntrypoints(route.children);
1206
+ }
1207
+ }
1208
+ };
1209
+ collectEntrypoints(routes);
1210
+ if (entrypoints.length === 0) {
1211
+ return {
1212
+ outputs: [],
1213
+ errors: [],
1214
+ entryFile: "",
1215
+ routeModules: {}
1216
+ };
1217
+ }
1218
+ const serverEntry = generateServerEntry(routes, root);
1219
+ const serverEntryPath = join2(outDir, "_entry.server.ts");
1220
+ await Bun.write(serverEntryPath, serverEntry);
1221
+ try {
1222
+ const result = await Bun.build({
1223
+ entrypoints,
1224
+ outdir: routesDir,
1225
+ target: "bun",
1226
+ minify,
1227
+ sourcemap: sourcemap ? "external" : "none",
1228
+ splitting,
1229
+ external: ["react", "react-dom", "@ereo/core", "@ereo/router", "@ereo/render", ...external],
1230
+ naming: {
1231
+ entry: "[dir]/[name].[ext]",
1232
+ chunk: "../chunks/[name]-[hash].[ext]",
1233
+ asset: "../assets/[name]-[hash].[ext]"
1234
+ }
1235
+ });
1236
+ if (!result.success) {
1237
+ for (const log of result.logs) {
1238
+ errors.push(log.message);
1239
+ }
1240
+ }
1241
+ for (const output of result.outputs) {
1242
+ const fileStat = await Bun.file(output.path).stat();
1243
+ const relativePath = relative(options.root, output.path);
1244
+ outputs.push({
1245
+ path: relativePath,
1246
+ size: fileStat?.size || 0,
1247
+ type: output.path.endsWith(".css") ? "css" : output.path.endsWith(".map") ? "map" : "js",
1248
+ isEntry: output.kind === "entry-point"
1249
+ });
1250
+ if (output.kind === "entry-point") {
1251
+ const sourcePath = entrypoints.find((e) => output.path.includes(basename2(e, extname(e))));
1252
+ if (sourcePath) {
1253
+ const routeId = relative(join2(root, "app/routes"), sourcePath).replace(/\.[^.]+$/, "").replace(/\\/g, "/");
1254
+ routeModules[routeId] = relativePath;
1255
+ }
1256
+ }
1257
+ }
1258
+ } catch (error) {
1259
+ errors.push(error instanceof Error ? error.message : String(error));
1260
+ }
1261
+ try {
1262
+ const entryResult = await Bun.build({
1263
+ entrypoints: [serverEntryPath],
1264
+ outdir: outDir,
1265
+ target: "bun",
1266
+ minify,
1267
+ sourcemap: sourcemap ? "external" : "none",
1268
+ splitting: false,
1269
+ external: ["react", "react-dom", "@ereo/core", "@ereo/router", "@ereo/render", ...external],
1270
+ naming: {
1271
+ entry: "index.[ext]"
1272
+ }
1273
+ });
1274
+ if (!entryResult.success) {
1275
+ for (const log of entryResult.logs) {
1276
+ errors.push(log.message);
1277
+ }
1278
+ }
1279
+ for (const output of entryResult.outputs) {
1280
+ const fileStat = await Bun.file(output.path).stat();
1281
+ outputs.push({
1282
+ path: relative(options.root, output.path),
1283
+ size: fileStat?.size || 0,
1284
+ type: output.path.endsWith(".map") ? "map" : "js",
1285
+ isEntry: true
1286
+ });
1287
+ }
1288
+ } catch (error) {
1289
+ errors.push(error instanceof Error ? error.message : String(error));
1290
+ }
1291
+ try {
1292
+ await rm(serverEntryPath);
1293
+ } catch (error) {}
1294
+ return {
1295
+ outputs,
1296
+ errors,
1297
+ entryFile: ".ereo/server/index.js",
1298
+ routeModules
1299
+ };
1300
+ }
1301
+ function generateServerEntry(routes, root) {
1302
+ const imports = [];
1303
+ const routeRegistrations = [];
1304
+ const processRoute = (route, index) => {
1305
+ const varName = `route_${index}`;
1306
+ const relativePath = relative(root, route.file).replace(/\\/g, "/");
1307
+ imports.push(`import * as ${varName} from './routes/${relative(join2(root, "app/routes"), route.file).replace(/\\/g, "/").replace(/\.[^.]+$/, ".js")}';`);
1308
+ routeRegistrations.push(` {
1309
+ id: '${route.id}',
1310
+ path: '${route.path}',
1311
+ module: ${varName},
1312
+ index: ${route.index || false},
1313
+ layout: ${route.layout || false},
1314
+ }`);
1315
+ if (route.children) {
1316
+ route.children.forEach((child, childIndex) => {
1317
+ processRoute(child, index * 100 + childIndex);
1318
+ });
1319
+ }
1320
+ };
1321
+ routes.forEach((route, index) => processRoute(route, index));
1322
+ return `/**
1323
+ * Server Entry - Auto-generated by @ereo/bundler
1324
+ * Do not edit this file directly.
1325
+ */
1326
+
1327
+ ${imports.join(`
1328
+ `)}
1329
+
1330
+ // Route registry
1331
+ export const routes = [
1332
+ ${routeRegistrations.join(`,
1333
+ `)}
1334
+ ];
1335
+
1336
+ // Route lookup map for fast access
1337
+ export const routeMap = new Map(routes.map(r => [r.path, r]));
1338
+
1339
+ // Find route by path
1340
+ export function findRoute(path) {
1341
+ return routeMap.get(path);
1342
+ }
1343
+
1344
+ // Get all route paths
1345
+ export function getRoutePaths() {
1346
+ return routes.map(r => r.path);
1347
+ }
1348
+
1349
+ // Default export for Bun.serve compatibility
1350
+ export default {
1351
+ routes,
1352
+ routeMap,
1353
+ findRoute,
1354
+ getRoutePaths,
1355
+ };
1356
+ `;
1357
+ }
1358
+ async function buildClient(options) {
1359
+ const { root, outDir, routes, minify = true, sourcemap = true, splitting = true } = options;
1360
+ const outputs = [];
1361
+ const errors = [];
1362
+ const chunks = {};
1363
+ const clientEntry = join2(root, "app/entry.client.tsx");
1364
+ const clientEntryAlt = join2(root, "app/entry.client.ts");
1365
+ const hasClientEntry = await Bun.file(clientEntry).exists();
1366
+ const hasClientEntryAlt = await Bun.file(clientEntryAlt).exists();
1367
+ let entrypoint;
1368
+ if (hasClientEntry) {
1369
+ entrypoint = clientEntry;
1370
+ } else if (hasClientEntryAlt) {
1371
+ entrypoint = clientEntryAlt;
1372
+ } else {
1373
+ const defaultEntry = join2(outDir, "_entry.client.tsx");
1374
+ await Bun.write(defaultEntry, generateDefaultClientEntry(routes));
1375
+ entrypoint = defaultEntry;
1376
+ }
1377
+ try {
1378
+ const result = await Bun.build({
1379
+ entrypoints: [entrypoint],
1380
+ outdir: outDir,
1381
+ target: "browser",
1382
+ minify,
1383
+ sourcemap: sourcemap ? "external" : "none",
1384
+ splitting,
1385
+ naming: {
1386
+ entry: "index.[ext]",
1387
+ chunk: "chunks/[name]-[hash].[ext]",
1388
+ asset: "../assets/[name]-[hash].[ext]"
1389
+ }
1390
+ });
1391
+ if (!result.success) {
1392
+ for (const log of result.logs) {
1393
+ errors.push(log.message);
1394
+ }
1395
+ }
1396
+ for (const output of result.outputs) {
1397
+ const fileStat = await Bun.file(output.path).stat();
1398
+ const relativePath = relative(options.root, output.path);
1399
+ outputs.push({
1400
+ path: relativePath,
1401
+ size: fileStat?.size || 0,
1402
+ type: output.path.endsWith(".css") ? "css" : output.path.endsWith(".map") ? "map" : "js",
1403
+ isEntry: output.kind === "entry-point"
1404
+ });
1405
+ if (output.kind === "chunk") {
1406
+ const chunkName = basename2(output.path, extname(output.path));
1407
+ chunks[chunkName] = relativePath;
1408
+ }
1409
+ }
1410
+ } catch (error) {
1411
+ errors.push(error instanceof Error ? error.message : String(error));
1412
+ }
1413
+ if (!hasClientEntry && !hasClientEntryAlt) {
1414
+ try {
1415
+ await rm(join2(outDir, "_entry.client.tsx"));
1416
+ } catch (error) {}
1417
+ }
1418
+ return {
1419
+ outputs,
1420
+ errors,
1421
+ entryFile: ".ereo/client/index.js",
1422
+ chunks
1423
+ };
1424
+ }
1425
+ function generateDefaultClientEntry(routes) {
1426
+ return `/**
1427
+ * Client Entry - Auto-generated by @ereo/bundler
1428
+ * Do not edit this file directly.
1429
+ */
1430
+
1431
+ // Initialize client-side hydration
1432
+ import { hydrateRoot } from 'react-dom/client';
1433
+
1434
+ // Client-side router and hydration
1435
+ async function initClient() {
1436
+ // Wait for DOM to be ready
1437
+ if (document.readyState === 'loading') {
1438
+ await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve));
1439
+ }
1440
+
1441
+ // Get hydration data from server-rendered script
1442
+ const dataScript = document.getElementById('__EREO_DATA__');
1443
+ const serverData = dataScript ? JSON.parse(dataScript.textContent || '{}') : {};
1444
+
1445
+ console.log('[EreoJS] Client hydration initialized');
1446
+
1447
+ // Initialize islands
1448
+ const islands = document.querySelectorAll('[data-island]');
1449
+ if (islands.length > 0) {
1450
+ console.log(\`[EreoJS] Found \${islands.length} island(s) to hydrate\`);
1451
+ }
1452
+ }
1453
+
1454
+ // Auto-initialize
1455
+ initClient().catch(console.error);
1456
+
1457
+ export { initClient };
1458
+ `;
1459
+ }
1460
+ async function buildIslands(options) {
1461
+ const { root, outDir, islands, minify = true, sourcemap = true, splitting = true } = options;
1462
+ const outputs = [];
1463
+ const errors = [];
1464
+ const islandMap = {};
1465
+ if (islands.length === 0) {
1466
+ return { outputs, errors, islands: islandMap };
1467
+ }
1468
+ const islandEntry = generateIslandEntry(islands);
1469
+ const islandEntryPath = join2(outDir, "_islands.entry.ts");
1470
+ await Bun.write(islandEntryPath, islandEntry);
1471
+ const islandManifest = generateIslandManifest(islands);
1472
+ await Bun.write(join2(outDir, "manifest.json"), islandManifest);
1473
+ const islandEntrypoints = islands.map((island) => island.file);
1474
+ try {
1475
+ const result = await Bun.build({
1476
+ entrypoints: islandEntrypoints,
1477
+ outdir: outDir,
1478
+ target: "browser",
1479
+ minify,
1480
+ sourcemap: sourcemap ? "external" : "none",
1481
+ splitting,
1482
+ external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
1483
+ naming: {
1484
+ entry: "[name]-[hash].[ext]",
1485
+ chunk: "shared/[name]-[hash].[ext]",
1486
+ asset: "../assets/[name]-[hash].[ext]"
1487
+ }
1488
+ });
1489
+ if (!result.success) {
1490
+ for (const log of result.logs) {
1491
+ errors.push(log.message);
1492
+ }
1493
+ }
1494
+ for (const output of result.outputs) {
1495
+ const fileStat = await Bun.file(output.path).stat();
1496
+ const relativePath = relative(options.root, output.path);
1497
+ outputs.push({
1498
+ path: relativePath,
1499
+ size: fileStat?.size || 0,
1500
+ type: output.path.endsWith(".css") ? "css" : output.path.endsWith(".map") ? "map" : "js",
1501
+ isEntry: output.kind === "entry-point"
1502
+ });
1503
+ if (output.kind === "entry-point") {
1504
+ const sourceIsland = islands.find((i) => output.path.includes(basename2(i.file, extname(i.file))));
1505
+ if (sourceIsland) {
1506
+ islandMap[sourceIsland.id] = relativePath;
1507
+ }
1508
+ }
1509
+ }
1510
+ } catch (error) {
1511
+ errors.push(error instanceof Error ? error.message : String(error));
1512
+ }
1513
+ try {
1514
+ await rm(islandEntryPath);
1515
+ } catch (error) {}
1516
+ return {
1517
+ outputs,
1518
+ errors,
1519
+ islands: islandMap
1520
+ };
1521
+ }
1522
+ async function buildCSS(options) {
1523
+ const { root, outDir, cssFiles, minify = true, sourcemap = true, plugins } = options;
1524
+ const outputs = [];
1525
+ const errors = [];
1526
+ if (cssFiles.length === 0) {
1527
+ return { outputs, errors };
1528
+ }
1529
+ let combinedCSS = "";
1530
+ for (const cssFile of cssFiles) {
1531
+ try {
1532
+ let content = await Bun.file(cssFile).text();
1533
+ if (plugins) {
1534
+ for (const plugin of plugins) {
1535
+ if (plugin.transform) {
1536
+ const transformed = await plugin.transform(content, cssFile);
1537
+ if (transformed) {
1538
+ content = transformed;
1539
+ }
1540
+ }
1541
+ }
1542
+ }
1543
+ combinedCSS += `/* Source: ${relative(root, cssFile)} */
1544
+ ${content}
1545
+
1546
+ `;
1547
+ } catch (error) {
1548
+ errors.push(`Failed to read CSS file ${cssFile}: ${error}`);
1549
+ }
1550
+ }
1551
+ const outputPath = join2(outDir, "styles.css");
1552
+ if (minify) {
1553
+ combinedCSS = minifyCSS(combinedCSS);
1554
+ }
1555
+ await Bun.write(outputPath, combinedCSS);
1556
+ const fileStat = await Bun.file(outputPath).stat();
1557
+ outputs.push({
1558
+ path: relative(root, outputPath),
1559
+ size: fileStat?.size || 0,
1560
+ type: "css",
1561
+ isEntry: true
1562
+ });
1563
+ if (sourcemap) {
1564
+ const mapContent = JSON.stringify({
1565
+ version: 3,
1566
+ sources: cssFiles.map((f) => relative(root, f)),
1567
+ names: [],
1568
+ mappings: ""
1569
+ });
1570
+ const mapPath = outputPath + ".map";
1571
+ await Bun.write(mapPath, mapContent);
1572
+ const mapStat = await Bun.file(mapPath).stat();
1573
+ outputs.push({
1574
+ path: relative(root, mapPath),
1575
+ size: mapStat?.size || 0,
1576
+ type: "map"
1577
+ });
1578
+ }
1579
+ return { outputs, errors };
1580
+ }
1581
+ function minifyCSS(css) {
1582
+ return css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").replace(/\s*([{}:;,>+~])\s*/g, "$1").replace(/;}/g, "}").trim();
1583
+ }
1584
+ async function copyAssets(options) {
1585
+ const { root, outDir, extensions } = options;
1586
+ const outputs = [];
1587
+ const assetDirs = [
1588
+ join2(root, "public"),
1589
+ join2(root, "app/assets"),
1590
+ join2(root, "assets")
1591
+ ];
1592
+ for (const dir of assetDirs) {
1593
+ try {
1594
+ const files = await scanForFiles(dir, extensions);
1595
+ for (const file of files) {
1596
+ const relativePath = relative(dir, file);
1597
+ const destPath = join2(outDir, relativePath);
1598
+ await mkdir(dirname2(destPath), { recursive: true });
1599
+ await copyFile(file, destPath);
1600
+ const fileStat = await stat(destPath);
1601
+ outputs.push({
1602
+ path: relative(options.root, destPath),
1603
+ size: fileStat.size,
1604
+ type: "asset"
1605
+ });
1606
+ }
1607
+ } catch (error) {}
1608
+ }
1609
+ return { outputs };
1610
+ }
1611
+ async function generateManifest(options) {
1612
+ const { outDir, buildId, routes, islands, serverResult, clientResult, cssFiles } = options;
1613
+ const routeEntries = [];
1614
+ const processRoute = (route, parentId) => {
1615
+ routeEntries.push({
1616
+ id: route.id,
1617
+ path: route.path,
1618
+ file: route.file,
1619
+ index: route.index,
1620
+ layout: route.layout,
1621
+ hasLoader: !!route.module?.loader,
1622
+ hasAction: !!route.module?.action,
1623
+ hasMeta: !!route.module?.meta,
1624
+ hasErrorBoundary: !!route.module?.ErrorBoundary,
1625
+ parentId
1626
+ });
1627
+ if (route.children) {
1628
+ route.children.forEach((child) => processRoute(child, route.id));
1629
+ }
1630
+ };
1631
+ routes.forEach((route) => processRoute(route));
1632
+ const islandEntries = {};
1633
+ for (const island of islands) {
1634
+ islandEntries[island.id] = {
1635
+ id: island.id,
1636
+ name: island.name,
1637
+ file: island.file,
1638
+ strategy: island.strategy,
1639
+ exports: island.exports
1640
+ };
1641
+ }
1642
+ const assets = {};
1643
+ const allOutputs = [...serverResult.outputs, ...clientResult.outputs];
1644
+ for (const output of allOutputs) {
1645
+ if (output.isEntry) {
1646
+ assets[output.path] = {
1647
+ file: output.path,
1648
+ isEntry: true
1649
+ };
1650
+ }
1651
+ }
1652
+ const manifest = {
1653
+ version: 1,
1654
+ buildTime: new Date().toISOString(),
1655
+ buildId,
1656
+ routes: routeEntries,
1657
+ server: {
1658
+ entry: serverResult.entryFile,
1659
+ modules: serverResult.routeModules
1660
+ },
1661
+ client: {
1662
+ entry: clientResult.entryFile,
1663
+ islands: islandEntries,
1664
+ chunks: clientResult.chunks
1665
+ },
1666
+ assets,
1667
+ css: cssFiles
1668
+ };
1669
+ await Bun.write(join2(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
1670
+ }
1671
+ function formatSize(bytes) {
1672
+ if (bytes < 1024)
1673
+ return bytes + " B";
1674
+ if (bytes < 1024 * 1024)
1675
+ return (bytes / 1024).toFixed(2) + " KB";
1676
+ return (bytes / (1024 * 1024)).toFixed(2) + " MB";
1677
+ }
1678
+ function printBuildReport(result) {
1679
+ console.log(`
1680
+ Build Report:`);
1681
+ console.log("\u2500".repeat(60));
1682
+ const byType = {
1683
+ js: result.outputs.filter((o) => o.type === "js"),
1684
+ css: result.outputs.filter((o) => o.type === "css"),
1685
+ asset: result.outputs.filter((o) => o.type === "asset")
1686
+ };
1687
+ for (const [type, files] of Object.entries(byType)) {
1688
+ if (files.length === 0)
1689
+ continue;
1690
+ const totalSize2 = files.reduce((sum, f) => sum + f.size, 0);
1691
+ console.log(`
1692
+ ${type.toUpperCase()} (${files.length} files, ${formatSize(totalSize2)}):`);
1693
+ for (const file of files.slice(0, 10)) {
1694
+ const entryMark = file.isEntry ? " (entry)" : "";
1695
+ console.log(` ${file.path} (${formatSize(file.size)})${entryMark}`);
1696
+ }
1697
+ if (files.length > 10) {
1698
+ console.log(` ... and ${files.length - 10} more`);
1699
+ }
1700
+ }
1701
+ console.log(`
1702
+ ` + "\u2500".repeat(60));
1703
+ const totalSize = result.outputs.reduce((sum, o) => sum + o.size, 0);
1704
+ console.log(`Total: ${result.outputs.length} files (${formatSize(totalSize)})`);
1705
+ console.log(`Duration: ${result.duration.toFixed(0)}ms`);
1706
+ if (result.errors && result.errors.length > 0) {
1707
+ console.log(`
1708
+ \x1B[33mWarnings/Errors:\x1B[0m`);
1709
+ for (const error of result.errors.slice(0, 5)) {
1710
+ console.log(` - ${error}`);
1711
+ }
1712
+ if (result.errors.length > 5) {
1713
+ console.log(` ... and ${result.errors.length - 5} more`);
1714
+ }
1715
+ }
1716
+ }
1717
+ function analyzeBuild(result) {
1718
+ const jsFiles = result.outputs.filter((o) => o.type === "js");
1719
+ const cssFiles = result.outputs.filter((o) => o.type === "css");
1720
+ const assetFiles = result.outputs.filter((o) => o.type === "asset");
1721
+ const totalSize = result.outputs.reduce((sum, o) => sum + o.size, 0);
1722
+ const jsSize = jsFiles.reduce((sum, o) => sum + o.size, 0);
1723
+ const cssSize = cssFiles.reduce((sum, o) => sum + o.size, 0);
1724
+ const assetSize = assetFiles.reduce((sum, o) => sum + o.size, 0);
1725
+ const largestFiles = [...result.outputs].filter((o) => o.type !== "map").sort((a, b) => b.size - a.size).slice(0, 5);
1726
+ const recommendations = [];
1727
+ if (jsSize > 500 * 1024) {
1728
+ recommendations.push("Consider code splitting to reduce initial bundle size");
1729
+ }
1730
+ const largeJsFiles = jsFiles.filter((f) => f.size > 100 * 1024);
1731
+ if (largeJsFiles.length > 0) {
1732
+ recommendations.push(`${largeJsFiles.length} JS file(s) exceed 100KB - consider splitting`);
1733
+ }
1734
+ if (jsFiles.length > 50) {
1735
+ recommendations.push("Many small chunks detected - consider adjusting splitting strategy");
1736
+ }
1737
+ return {
1738
+ totalSize,
1739
+ jsSize,
1740
+ cssSize,
1741
+ assetSize,
1742
+ largestFiles,
1743
+ recommendations
1744
+ };
1745
+ }
1746
+ // src/plugins/types.ts
1747
+ import { join as join3 } from "path";
1748
+ function extractParams(path) {
1749
+ const params = {};
1750
+ const segments = path.split("/").filter(Boolean);
1751
+ for (const segment of segments) {
1752
+ const catchAllMatch = segment.match(/^\[\.\.\.(\w+)\]$/);
1753
+ if (catchAllMatch) {
1754
+ params[catchAllMatch[1]] = { type: "string[]" };
1755
+ continue;
1756
+ }
1757
+ const optionalMatch = segment.match(/^\[\[(\w+)\]\]$/);
1758
+ if (optionalMatch) {
1759
+ params[optionalMatch[1]] = { type: "string", optional: true };
1760
+ continue;
1761
+ }
1762
+ const dynamicMatch = segment.match(/^\[(\w+)\]$/);
1763
+ if (dynamicMatch) {
1764
+ params[dynamicMatch[1]] = { type: "string" };
1765
+ continue;
1766
+ }
1767
+ }
1768
+ return params;
1769
+ }
1770
+ function generateImportPath(file, routesDir) {
1771
+ let importPath = file.replace(routesDir, "@routes").replace(/\.(tsx?|jsx?)$/, "");
1772
+ return importPath;
1773
+ }
1774
+ function safeIdentifier(path) {
1775
+ return path.replace(/[^a-zA-Z0-9]/g, "_");
1776
+ }
1777
+ function collectRouteInfos(routes, routesDir, inferTypes, parentPath) {
1778
+ const routeInfos = [];
1779
+ for (const route of routes) {
1780
+ if (!route.layout) {
1781
+ const importPath = generateImportPath(route.file, routesDir);
1782
+ const info = {
1783
+ path: route.path,
1784
+ file: route.file,
1785
+ params: extractParams(route.path),
1786
+ hasLoader: !!route.module?.loader,
1787
+ hasAction: !!route.module?.action,
1788
+ hasMeta: !!route.module?.meta,
1789
+ hasHandle: !!route.module?.handle,
1790
+ hasSearchParams: !!route.module?.searchParams,
1791
+ hasHashParams: false,
1792
+ parentPath,
1793
+ config: {
1794
+ renderMode: route.config?.render?.mode,
1795
+ auth: route.config?.auth?.required
1796
+ }
1797
+ };
1798
+ if (inferTypes) {
1799
+ if (info.hasLoader) {
1800
+ info.loaderTypeRef = `typeof import('${importPath}')['loader']`;
1801
+ }
1802
+ if (info.hasAction) {
1803
+ info.actionTypeRef = `typeof import('${importPath}')['action']`;
1804
+ }
1805
+ if (info.hasSearchParams) {
1806
+ info.searchParamsTypeRef = `typeof import('${importPath}')['searchParams']`;
1807
+ }
1808
+ if (route.module?.hashParams) {
1809
+ info.hasHashParams = true;
1810
+ info.hashParamsTypeRef = `typeof import('${importPath}')['hashParams']`;
1811
+ }
1812
+ }
1813
+ routeInfos.push(info);
1814
+ }
1815
+ if (route.children) {
1816
+ const childInfos = collectRouteInfos(route.children, routesDir, inferTypes, route.path);
1817
+ routeInfos.push(...childInfos);
1818
+ }
1819
+ }
1820
+ return routeInfos;
1821
+ }
1822
+ function generateParamsType(params) {
1823
+ const entries = Object.entries(params);
1824
+ if (entries.length === 0) {
1825
+ return "Record<string, never>";
1826
+ }
1827
+ const parts = entries.map(([key, { type, optional }]) => {
1828
+ const optionalMark = optional ? "?" : "";
1829
+ return `${key}${optionalMark}: ${type}`;
1830
+ });
1831
+ return `{ ${parts.join("; ")} }`;
1832
+ }
1833
+ function generateRouteTypes(routes, options = {}) {
1834
+ const {
1835
+ routesDir = "app/routes",
1836
+ inferTypes = true,
1837
+ generateSearchParams = true,
1838
+ generateHashParams = true,
1839
+ generateContext = true,
1840
+ lazyEvaluation = true
1841
+ } = options;
1842
+ const routeInfos = collectRouteInfos(routes, routesDir, inferTypes);
1843
+ const lines = [
1844
+ "// Auto-generated by @ereo/bundler",
1845
+ "// Do not edit this file manually",
1846
+ "// Generated at: " + new Date().toISOString(),
1847
+ "",
1848
+ "// Performance: Uses object maps and lazy evaluation for large route trees",
1849
+ ""
1850
+ ];
1851
+ if (lazyEvaluation) {
1852
+ lines.push("// Lazy evaluation wrapper for better TypeScript performance");
1853
+ lines.push("type LazyEval<T> = T extends infer U ? U : never;");
1854
+ lines.push("");
1855
+ }
1856
+ if (inferTypes) {
1857
+ lines.push("// Route module imports for type inference");
1858
+ const uniqueImports = new Set;
1859
+ for (const info of routeInfos) {
1860
+ const importPath = generateImportPath(info.file, routesDir);
1861
+ if (!uniqueImports.has(importPath)) {
1862
+ uniqueImports.add(importPath);
1863
+ const safeName = safeIdentifier(importPath);
1864
+ lines.push(`import type * as ${safeName} from '${importPath}';`);
1865
+ }
1866
+ }
1867
+ lines.push("");
1868
+ }
1869
+ lines.push("declare module '@ereo/core' {");
1870
+ lines.push(" export interface RouteTypes {");
1871
+ for (const info of routeInfos) {
1872
+ const paramsType = generateParamsType(info.params);
1873
+ const importPath = generateImportPath(info.file, routesDir);
1874
+ const safeName = safeIdentifier(importPath);
1875
+ const wrapType = lazyEvaluation ? (t) => `LazyEval<${t}>` : (t) => t;
1876
+ lines.push(` '${info.path}': {`);
1877
+ lines.push(` params: ${paramsType};`);
1878
+ if (generateSearchParams && info.hasSearchParams && inferTypes) {
1879
+ lines.push(` search: ${wrapType(`${safeName} extends { searchParams: infer S } ? (S extends { parse: (data: any) => infer R } ? R : Record<string, string | string[] | undefined>) : Record<string, string | string[] | undefined>`)};`);
1880
+ } else {
1881
+ lines.push(` search: Record<string, string | string[] | undefined>;`);
1882
+ }
1883
+ if (generateHashParams && info.hasHashParams && inferTypes) {
1884
+ lines.push(` hash: ${wrapType(`${safeName} extends { hashParams: infer H } ? (H extends { parse: (data: any) => infer R } ? R : Record<string, string | undefined>) : Record<string, string | undefined>`)};`);
1885
+ } else {
1886
+ lines.push(` hash: Record<string, string | undefined>;`);
1887
+ }
1888
+ if (info.hasLoader && inferTypes) {
1889
+ lines.push(` loader: ${wrapType(`${safeName} extends { loader: infer L } ? (L extends (...args: any[]) => infer R ? Awaited<R> : never) : never`)};`);
1890
+ } else {
1891
+ lines.push(` loader: unknown;`);
1892
+ }
1893
+ if (info.hasAction && inferTypes) {
1894
+ lines.push(` action: ${wrapType(`${safeName} extends { action: infer A } ? (A extends (...args: any[]) => infer R ? Awaited<R> : never) : never`)};`);
1895
+ } else {
1896
+ lines.push(` action: unknown;`);
1897
+ }
1898
+ if (generateContext && info.parentPath) {
1899
+ lines.push(` context: RouteTypes['${info.parentPath}'] extends { context: infer C } ? C : Record<string, unknown>;`);
1900
+ } else {
1901
+ lines.push(` context: Record<string, unknown>;`);
1902
+ }
1903
+ lines.push(` meta: ${info.hasMeta};`);
1904
+ lines.push(` handle: ${info.hasHandle ? `${safeName}['handle']` : "undefined"};`);
1905
+ lines.push(" };");
1906
+ }
1907
+ lines.push(" }");
1908
+ lines.push("}");
1909
+ lines.push("");
1910
+ lines.push("// All available route paths (using object map for performance)");
1911
+ lines.push("type RoutePathMap = {");
1912
+ for (const info of routeInfos) {
1913
+ lines.push(` '${info.path}': true;`);
1914
+ }
1915
+ lines.push("};");
1916
+ lines.push("");
1917
+ lines.push("export type RoutePath = keyof RoutePathMap;");
1918
+ lines.push("");
1919
+ lines.push("// Helper types for route-safe navigation");
1920
+ lines.push(generateHelperTypes(lazyEvaluation));
1921
+ lines.push("");
1922
+ lines.push("// Runtime path builder");
1923
+ lines.push(generateBuildPathFunction());
1924
+ lines.push("");
1925
+ lines.push("export {};");
1926
+ return lines.join(`
1927
+ `);
1928
+ }
1929
+ function generateHelperTypes(lazyEvaluation) {
1930
+ const lazy = lazyEvaluation ? "LazyEval" : "";
1931
+ const wrap = (t) => lazyEvaluation ? `LazyEval<${t}>` : t;
1932
+ return `
1933
+ /**
1934
+ * Extract params type for a route path.
1935
+ */
1936
+ export type ParamsFor<T extends RoutePath> =
1937
+ T extends keyof import('@ereo/core').RouteTypes
1938
+ ? ${wrap("import('@ereo/core').RouteTypes[T]['params']")}
1939
+ : Record<string, string>;
1940
+
1941
+ /**
1942
+ * Extract search params type for a route path.
1943
+ * This is typed per-route (TanStack limitation solved).
1944
+ */
1945
+ export type SearchParamsFor<T extends RoutePath> =
1946
+ T extends keyof import('@ereo/core').RouteTypes
1947
+ ? ${wrap("import('@ereo/core').RouteTypes[T]['search']")}
1948
+ : Record<string, string | string[] | undefined>;
1949
+
1950
+ /**
1951
+ * Extract hash params type for a route path.
1952
+ * UNIQUE to Ereo - TanStack has no hash param support.
1953
+ */
1954
+ export type HashParamsFor<T extends RoutePath> =
1955
+ T extends keyof import('@ereo/core').RouteTypes
1956
+ ? ${wrap("import('@ereo/core').RouteTypes[T]['hash']")}
1957
+ : Record<string, string | undefined>;
1958
+
1959
+ /**
1960
+ * Extract loader data type for a route path.
1961
+ */
1962
+ export type LoaderDataFor<T extends RoutePath> =
1963
+ T extends keyof import('@ereo/core').RouteTypes
1964
+ ? ${wrap("import('@ereo/core').RouteTypes[T]['loader']")}
1965
+ : unknown;
1966
+
1967
+ /**
1968
+ * Extract action data type for a route path.
1969
+ */
1970
+ export type ActionDataFor<T extends RoutePath> =
1971
+ T extends keyof import('@ereo/core').RouteTypes
1972
+ ? ${wrap("import('@ereo/core').RouteTypes[T]['action']")}
1973
+ : unknown;
1974
+
1975
+ /**
1976
+ * Extract context type for a route path.
1977
+ * Context is accumulated from parent layouts.
1978
+ */
1979
+ export type ContextFor<T extends RoutePath> =
1980
+ T extends keyof import('@ereo/core').RouteTypes
1981
+ ? ${wrap("import('@ereo/core').RouteTypes[T]['context']")}
1982
+ : Record<string, unknown>;
1983
+
1984
+ /**
1985
+ * Extract handle type for a route path.
1986
+ */
1987
+ export type HandleFor<T extends RoutePath> =
1988
+ T extends keyof import('@ereo/core').RouteTypes
1989
+ ? ${wrap("import('@ereo/core').RouteTypes[T]['handle']")}
1990
+ : undefined;
1991
+
1992
+ /**
1993
+ * Type-safe route with all params.
1994
+ */
1995
+ export type TypedRoute<T extends RoutePath> = {
1996
+ path: T;
1997
+ params: ParamsFor<T>;
1998
+ search?: SearchParamsFor<T>;
1999
+ hash?: HashParamsFor<T>;
2000
+ };
2001
+
2002
+ /**
2003
+ * Full route data type.
2004
+ */
2005
+ export type RouteData<T extends RoutePath> = {
2006
+ params: ParamsFor<T>;
2007
+ search: SearchParamsFor<T>;
2008
+ hash: HashParamsFor<T>;
2009
+ loaderData: LoaderDataFor<T>;
2010
+ actionData: ActionDataFor<T> | undefined;
2011
+ context: ContextFor<T>;
2012
+ handle: HandleFor<T>;
2013
+ };
2014
+
2015
+ /**
2016
+ * Check if params object is empty (no required params).
2017
+ */
2018
+ export type HasRequiredParams<T extends RoutePath> =
2019
+ keyof ParamsFor<T> extends never ? false : true;
2020
+
2021
+ /**
2022
+ * Conditionally require params based on route.
2023
+ */
2024
+ export type ParamsRequired<T extends RoutePath> =
2025
+ Record<string, never> extends ParamsFor<T>
2026
+ ? { params?: ParamsFor<T> }
2027
+ : { params: ParamsFor<T> };
2028
+ `.trim();
2029
+ }
2030
+ function generateBuildPathFunction() {
2031
+ return `
2032
+ /**
2033
+ * Build a URL path with params, search, and hash.
2034
+ */
2035
+ export function buildPath<T extends RoutePath>(
2036
+ path: T,
2037
+ options: {
2038
+ params?: ParamsFor<T>;
2039
+ search?: SearchParamsFor<T>;
2040
+ hash?: HashParamsFor<T>;
2041
+ } = {}
2042
+ ): string {
2043
+ const { params, search, hash } = options;
2044
+
2045
+ // Build path with params
2046
+ let result: string = path;
2047
+ if (params) {
2048
+ for (const [key, value] of Object.entries(params as Record<string, string | string[]>)) {
2049
+ if (value === undefined) continue;
2050
+ result = result.replace(\`[...\${key}]\`, Array.isArray(value) ? value.join('/') : value);
2051
+ result = result.replace(\`[[\${key}]]\`, Array.isArray(value) ? value[0] : value || '');
2052
+ result = result.replace(\`[\${key}]\`, Array.isArray(value) ? value[0] : value);
2053
+ }
2054
+ }
2055
+
2056
+ // Remove unfilled optional params
2057
+ result = result.replace(/\\/\\?\\[\\[[^\\]]+\\]\\]/g, '');
2058
+
2059
+ // Add search params
2060
+ if (search && Object.keys(search).length > 0) {
2061
+ const searchParams = new URLSearchParams();
2062
+ for (const [key, value] of Object.entries(search as Record<string, unknown>)) {
2063
+ if (value === undefined || value === null) continue;
2064
+ if (Array.isArray(value)) {
2065
+ for (const v of value) {
2066
+ searchParams.append(key, String(v));
2067
+ }
2068
+ } else {
2069
+ searchParams.set(key, String(value));
2070
+ }
2071
+ }
2072
+ const queryString = searchParams.toString();
2073
+ if (queryString) {
2074
+ result += '?' + queryString;
2075
+ }
2076
+ }
2077
+
2078
+ // Add hash params
2079
+ if (hash && Object.keys(hash).length > 0) {
2080
+ const hashParams = new URLSearchParams();
2081
+ for (const [key, value] of Object.entries(hash as Record<string, unknown>)) {
2082
+ if (value === undefined || value === null) continue;
2083
+ hashParams.set(key, String(value));
2084
+ }
2085
+ const hashString = hashParams.toString();
2086
+ if (hashString) {
2087
+ result += '#' + hashString;
2088
+ }
2089
+ }
2090
+
2091
+ return result;
2092
+ }
2093
+ `.trim();
2094
+ }
2095
+ function groupRoutesByPrefix(routeInfos, maxPerFile) {
2096
+ const groups = new Map;
2097
+ for (const info of routeInfos) {
2098
+ const prefix = info.path.split("/").filter(Boolean)[0] || "_root";
2099
+ const key = `routes_${prefix}`;
2100
+ if (!groups.has(key)) {
2101
+ groups.set(key, []);
2102
+ }
2103
+ groups.get(key).push(info);
2104
+ }
2105
+ const result = new Map;
2106
+ for (const [key, infos] of groups) {
2107
+ if (infos.length <= maxPerFile) {
2108
+ result.set(key, infos);
2109
+ } else {
2110
+ let chunkIndex = 0;
2111
+ for (let i = 0;i < infos.length; i += maxPerFile) {
2112
+ result.set(`${key}_${chunkIndex}`, infos.slice(i, i + maxPerFile));
2113
+ chunkIndex++;
2114
+ }
2115
+ }
2116
+ }
2117
+ return result;
2118
+ }
2119
+ function generateSplitRouteTypes(routes, options = {}) {
2120
+ const {
2121
+ routesDir = "app/routes",
2122
+ inferTypes = true,
2123
+ maxRoutesPerFile = 50
2124
+ } = options;
2125
+ const routeInfos = collectRouteInfos(routes, routesDir, inferTypes);
2126
+ const groups = groupRoutesByPrefix(routeInfos, maxRoutesPerFile);
2127
+ const files = new Map;
2128
+ for (const [fileName, infos] of groups) {
2129
+ const content = generatePartialRouteTypes(infos, routesDir, inferTypes);
2130
+ files.set(`${fileName}.d.ts`, content);
2131
+ }
2132
+ const indexContent = generateIndexFile(Array.from(groups.keys()));
2133
+ files.set("index.d.ts", indexContent);
2134
+ return files;
2135
+ }
2136
+ function generatePartialRouteTypes(routeInfos, routesDir, inferTypes) {
2137
+ const lines = [
2138
+ "// Auto-generated partial route types",
2139
+ ""
2140
+ ];
2141
+ if (inferTypes) {
2142
+ const uniqueImports = new Set;
2143
+ for (const info of routeInfos) {
2144
+ const importPath = generateImportPath(info.file, routesDir);
2145
+ if (!uniqueImports.has(importPath)) {
2146
+ uniqueImports.add(importPath);
2147
+ lines.push(`import type * as ${safeIdentifier(importPath)} from '${importPath}';`);
2148
+ }
2149
+ }
2150
+ lines.push("");
2151
+ }
2152
+ lines.push("export const routeTypes = {");
2153
+ for (const info of routeInfos) {
2154
+ const paramsType = generateParamsType(info.params);
2155
+ lines.push(` '${info.path}': {} as {`);
2156
+ lines.push(` params: ${paramsType};`);
2157
+ lines.push(` search: Record<string, string | string[] | undefined>;`);
2158
+ lines.push(` hash: Record<string, string | undefined>;`);
2159
+ lines.push(` loader: unknown;`);
2160
+ lines.push(` action: unknown;`);
2161
+ lines.push(` context: Record<string, unknown>;`);
2162
+ lines.push(` meta: boolean;`);
2163
+ lines.push(` handle: unknown;`);
2164
+ lines.push(` },`);
2165
+ }
2166
+ lines.push("};");
2167
+ return lines.join(`
2168
+ `);
2169
+ }
2170
+ function generateIndexFile(fileNames) {
2171
+ const lines = [
2172
+ "// Auto-generated index file",
2173
+ "// Combines split route type files",
2174
+ ""
2175
+ ];
2176
+ for (const name of fileNames) {
2177
+ lines.push(`export * from './${name}';`);
2178
+ }
2179
+ return lines.join(`
2180
+ `);
2181
+ }
2182
+ async function writeRouteTypes(outDir, routes, options = {}) {
2183
+ const types = generateRouteTypes(routes, options);
2184
+ const outPath = join3(outDir, "routes.d.ts");
2185
+ await Bun.write(outPath, types);
2186
+ console.log(`\x1B[32m\u2713\x1B[0m Route types written to ${outPath}`);
2187
+ }
2188
+ async function writeSplitRouteTypes(outDir, routes, options = {}) {
2189
+ const files = generateSplitRouteTypes(routes, options);
2190
+ for (const [fileName, content] of files) {
2191
+ const outPath = join3(outDir, fileName);
2192
+ await Bun.write(outPath, content);
2193
+ }
2194
+ console.log(`\x1B[32m\u2713\x1B[0m Split route types written to ${outDir} (${files.size} files)`);
2195
+ }
2196
+ function createTypesPlugin(options = {}) {
2197
+ const {
2198
+ outDir = ".ereo",
2199
+ routesDir = "app/routes",
2200
+ inferTypes = true,
2201
+ watch = false,
2202
+ splitFiles = false,
2203
+ maxRoutesPerFile = 50
2204
+ } = options;
2205
+ let routes = [];
2206
+ return {
2207
+ name: "ereo:types",
2208
+ transformRoutes(routeList) {
2209
+ routes = routeList;
2210
+ return routeList;
2211
+ },
2212
+ async buildEnd() {
2213
+ if (routes.length === 0)
2214
+ return;
2215
+ const genOptions = {
2216
+ routesDir,
2217
+ inferTypes,
2218
+ generateSearchParams: true,
2219
+ generateHashParams: true,
2220
+ generateContext: true,
2221
+ lazyEvaluation: true,
2222
+ maxRoutesPerFile
2223
+ };
2224
+ if (splitFiles && routes.length > maxRoutesPerFile) {
2225
+ await writeSplitRouteTypes(outDir, routes, genOptions);
2226
+ } else {
2227
+ await writeRouteTypes(outDir, routes, genOptions);
2228
+ }
2229
+ },
2230
+ async configureServer(_server) {
2231
+ if (watch) {}
2232
+ }
2233
+ };
2234
+ }
2235
+ function generateLinkTypes(routes) {
2236
+ const routeInfos = routes.filter((r) => !r.layout).map((r) => ({
2237
+ path: r.path,
2238
+ params: extractParams(r.path)
2239
+ }));
2240
+ const pathTypes = routeInfos.map((r) => `'${r.path}'`);
2241
+ return `
2242
+ // Auto-generated Link types
2243
+ import type { ComponentProps, ReactNode } from 'react';
2244
+ import type { RoutePath, ParamsFor, SearchParamsFor, HashParamsFor, ParamsRequired } from './routes';
2245
+
2246
+ /**
2247
+ * Type-safe Link component props.
2248
+ * Validates route existence and params at compile time.
2249
+ */
2250
+ export type LinkProps<T extends RoutePath = RoutePath> =
2251
+ Omit<ComponentProps<'a'>, 'href'> &
2252
+ { to: T } &
2253
+ ParamsRequired<T> &
2254
+ {
2255
+ /** Search params (type-safe per route) */
2256
+ search?: SearchParamsFor<T>;
2257
+ /** Hash params (type-safe per route, Ereo exclusive) */
2258
+ hash?: HashParamsFor<T>;
2259
+ /** Prefetch strategy */
2260
+ prefetch?: 'hover' | 'viewport' | 'none' | 'intent' | 'render';
2261
+ /** Replace current history entry */
2262
+ replace?: boolean;
2263
+ /** Scroll to top on navigation */
2264
+ scroll?: boolean;
2265
+ /** State to pass */
2266
+ state?: unknown;
2267
+ /** Children */
2268
+ children?: ReactNode;
2269
+ };
2270
+
2271
+ /**
2272
+ * Type-safe NavLink component props.
2273
+ */
2274
+ export type NavLinkProps<T extends RoutePath = RoutePath> =
2275
+ Omit<LinkProps<T>, 'className' | 'style'> & {
2276
+ className?: string | ((props: { isActive: boolean; isPending: boolean }) => string);
2277
+ style?: React.CSSProperties | ((props: { isActive: boolean; isPending: boolean }) => React.CSSProperties);
2278
+ end?: boolean;
2279
+ };
2280
+
2281
+ /**
2282
+ * All available routes.
2283
+ */
2284
+ export type AppRoutes = ${pathTypes.join(" | ") || "string"};
2285
+ `.trim();
2286
+ }
2287
+ function generateHookTypes() {
2288
+ return `
2289
+ // Auto-generated hook types
2290
+ import type {
2291
+ RoutePath,
2292
+ LoaderDataFor,
2293
+ ActionDataFor,
2294
+ ParamsFor,
2295
+ SearchParamsFor,
2296
+ HashParamsFor,
2297
+ ContextFor,
2298
+ HandleFor,
2299
+ } from './routes';
2300
+
2301
+ /**
2302
+ * Get loader data for the current route (type-safe).
2303
+ *
2304
+ * @example
2305
+ * // In /blog/[slug].tsx
2306
+ * const { post, comments } = useLoaderData<'/blog/[slug]'>();
2307
+ * // ^? { post: Post, comments: Comment[] }
2308
+ */
2309
+ export declare function useLoaderData<T extends RoutePath>(): LoaderDataFor<T>;
2310
+
2311
+ /**
2312
+ * Get route params for the current route (type-safe).
2313
+ *
2314
+ * @example
2315
+ * // In /blog/[slug].tsx
2316
+ * const { slug } = useParams<'/blog/[slug]'>();
2317
+ * // ^? string
2318
+ */
2319
+ export declare function useParams<T extends RoutePath>(): ParamsFor<T>;
2320
+
2321
+ /**
2322
+ * Get search params for the current route (type-safe).
2323
+ *
2324
+ * @example
2325
+ * // In /posts.tsx with searchParams schema
2326
+ * const { page, sort } = useSearchParams<'/posts'>();
2327
+ */
2328
+ export declare function useSearchParams<T extends RoutePath>(): SearchParamsFor<T>;
2329
+
2330
+ /**
2331
+ * Get hash params for the current route (type-safe).
2332
+ * UNIQUE to Ereo - TanStack has no hash param support.
2333
+ *
2334
+ * @example
2335
+ * // In /docs/[topic].tsx with hashParams schema
2336
+ * const { section } = useHashParams<'/docs/[topic]'>();
2337
+ */
2338
+ export declare function useHashParams<T extends RoutePath>(): HashParamsFor<T>;
2339
+
2340
+ /**
2341
+ * Get action data for the current route (type-safe).
2342
+ *
2343
+ * @example
2344
+ * // In /blog/[slug].tsx
2345
+ * const actionData = useActionData<'/blog/[slug]'>();
2346
+ */
2347
+ export declare function useActionData<T extends RoutePath>(): ActionDataFor<T> | undefined;
2348
+
2349
+ /**
2350
+ * Get accumulated context from parent layouts.
2351
+ *
2352
+ * @example
2353
+ * const { user } = useRouteContext<'/dashboard/settings'>();
2354
+ */
2355
+ export declare function useRouteContext<T extends RoutePath>(): ContextFor<T>;
2356
+
2357
+ /**
2358
+ * Get route matches with typed data.
2359
+ */
2360
+ export declare function useMatches<T extends RoutePath>(): Array<{
2361
+ id: string;
2362
+ pathname: string;
2363
+ params: ParamsFor<T>;
2364
+ data: LoaderDataFor<T>;
2365
+ handle: HandleFor<T>;
2366
+ }>;
2367
+
2368
+ /**
2369
+ * Navigation hook with type-safe paths.
2370
+ */
2371
+ export declare function useNavigate(): {
2372
+ <T extends RoutePath>(to: T, options?: {
2373
+ params?: ParamsFor<T>;
2374
+ search?: SearchParamsFor<T>;
2375
+ hash?: HashParamsFor<T>;
2376
+ replace?: boolean;
2377
+ state?: unknown;
2378
+ }): void;
2379
+ (delta: number): void;
2380
+ };
2381
+ `.trim();
2382
+ }
2383
+ // src/plugins/tailwind.ts
2384
+ import { join as join4, resolve } from "path";
2385
+ import { readFileSync, existsSync, statSync } from "fs";
2386
+ import postcss from "postcss";
2387
+ import tailwindcss from "tailwindcss";
2388
+ import autoprefixer from "autoprefixer";
2389
+ var DEFAULT_CONTENT_PATHS = [
2390
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
2391
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
2392
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
2393
+ "./src/**/*.{js,ts,jsx,tsx,mdx}"
2394
+ ];
2395
+ function createTailwindPlugin(options = {}) {
2396
+ const {
2397
+ content = DEFAULT_CONTENT_PATHS,
2398
+ config,
2399
+ darkMode = "class",
2400
+ minify = false,
2401
+ sourcemap = true,
2402
+ postcssPlugins = [],
2403
+ watch = true
2404
+ } = options;
2405
+ let tailwindConfig = null;
2406
+ let root = process.cwd();
2407
+ let mode = "development";
2408
+ let cssCache = new Map;
2409
+ let processor = null;
2410
+ let contentFilesCache = new Map;
2411
+ let scannedClasses = new Set;
2412
+ async function loadTailwindConfig() {
2413
+ const configPaths = [
2414
+ config ? resolve(root, config) : null,
2415
+ join4(root, "tailwind.config.js"),
2416
+ join4(root, "tailwind.config.ts"),
2417
+ join4(root, "tailwind.config.mjs"),
2418
+ join4(root, "tailwind.config.cjs")
2419
+ ].filter(Boolean);
2420
+ for (const configPath of configPaths) {
2421
+ try {
2422
+ if (existsSync(configPath)) {
2423
+ delete __require.cache?.[configPath];
2424
+ const imported = await import(configPath);
2425
+ const loadedConfig = imported.default || imported;
2426
+ console.log(` [Tailwind] Using config: ${configPath}`);
2427
+ return loadedConfig;
2428
+ }
2429
+ } catch (error) {
2430
+ console.warn(` [Tailwind] Failed to load config from ${configPath}:`, error);
2431
+ }
2432
+ }
2433
+ console.log(" [Tailwind] Using default configuration");
2434
+ return createDefaultConfig();
2435
+ }
2436
+ function createDefaultConfig() {
2437
+ return {
2438
+ content: content.map((p) => resolve(root, p)),
2439
+ darkMode,
2440
+ theme: {
2441
+ extend: {}
2442
+ },
2443
+ plugins: []
2444
+ };
2445
+ }
2446
+ function mergeConfig(userConfig) {
2447
+ const resolvedContent = userConfig.content || content.map((p) => resolve(root, p));
2448
+ return {
2449
+ ...userConfig,
2450
+ content: Array.isArray(resolvedContent) ? resolvedContent.map((p) => typeof p === "string" && !p.startsWith("/") && !p.startsWith(".") ? resolve(root, p) : p) : resolvedContent,
2451
+ darkMode: userConfig.darkMode ?? darkMode
2452
+ };
2453
+ }
2454
+ function createProcessor(tailwindCfg) {
2455
+ const plugins = [
2456
+ tailwindcss(tailwindCfg),
2457
+ autoprefixer(),
2458
+ ...postcssPlugins
2459
+ ];
2460
+ if (minify) {
2461
+ try {
2462
+ const cssnano = __require("cssnano");
2463
+ plugins.push(cssnano({
2464
+ preset: ["default", {
2465
+ discardComments: { removeAll: true },
2466
+ normalizeWhitespace: true
2467
+ }]
2468
+ }));
2469
+ } catch {
2470
+ console.warn(" [Tailwind] cssnano not available, skipping minification");
2471
+ }
2472
+ }
2473
+ return postcss(plugins);
2474
+ }
2475
+ async function processTailwindCSS(css, filename, force = false) {
2476
+ const cached = cssCache.get(filename);
2477
+ if (cached && !force) {
2478
+ const fileStats = existsSync(filename) ? statSync(filename).mtimeMs : 0;
2479
+ if (cached.timestamp >= fileStats) {
2480
+ let contentChanged = false;
2481
+ for (const dep of cached.dependencies) {
2482
+ if (existsSync(dep)) {
2483
+ const depStats = statSync(dep).mtimeMs;
2484
+ if (depStats > cached.timestamp) {
2485
+ contentChanged = true;
2486
+ break;
2487
+ }
2488
+ }
2489
+ }
2490
+ if (!contentChanged) {
2491
+ return { css: cached.css, map: cached.map };
2492
+ }
2493
+ }
2494
+ }
2495
+ if (!processor) {
2496
+ processor = createProcessor(tailwindConfig);
2497
+ }
2498
+ try {
2499
+ const result = await processor.process(css, {
2500
+ from: filename,
2501
+ to: filename.replace(/\.css$/, ".out.css"),
2502
+ map: sourcemap ? { inline: false, annotation: false } : false
2503
+ });
2504
+ const dependencies = [];
2505
+ for (const message of result.messages) {
2506
+ if (message.type === "dependency") {
2507
+ dependencies.push(message.file);
2508
+ }
2509
+ }
2510
+ cssCache.set(filename, {
2511
+ css: result.css,
2512
+ map: result.map?.toString(),
2513
+ timestamp: Date.now(),
2514
+ dependencies
2515
+ });
2516
+ return {
2517
+ css: result.css,
2518
+ map: result.map?.toString()
2519
+ };
2520
+ } catch (error) {
2521
+ console.error(` [Tailwind] Processing error in ${filename}:`);
2522
+ console.error(` ${error.message}`);
2523
+ if (error.line && error.column) {
2524
+ console.error(` at line ${error.line}, column ${error.column}`);
2525
+ }
2526
+ throw error;
2527
+ }
2528
+ }
2529
+ async function scanContentFiles() {
2530
+ const classes = new Set;
2531
+ const files = [];
2532
+ try {
2533
+ const { glob } = await import("glob");
2534
+ for (const pattern of tailwindConfig.content) {
2535
+ if (typeof pattern === "string") {
2536
+ const matches = await glob(pattern, {
2537
+ cwd: root,
2538
+ absolute: true,
2539
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
2540
+ });
2541
+ for (const file of matches) {
2542
+ files.push(file);
2543
+ const content2 = readFileSync(file, "utf-8");
2544
+ extractClasses(content2, classes);
2545
+ }
2546
+ } else if (pattern.raw) {
2547
+ extractClasses(pattern.raw, classes);
2548
+ }
2549
+ }
2550
+ } catch (error) {
2551
+ console.warn(" [Tailwind] Failed to scan content files:", error);
2552
+ }
2553
+ return { classes, files };
2554
+ }
2555
+ function extractClasses(content2, classes) {
2556
+ const patterns = [
2557
+ /class(?:Name)?=["'`]([^"'`]+)["'`]/g,
2558
+ /class(?:Name)?={[`"]([^`"]+)[`"]}/g,
2559
+ /clsx\(([^)]+)\)/g,
2560
+ /cn\(([^)]+)\)/g,
2561
+ /tw`([^`]+)`/g,
2562
+ /cva\(([^)]+)\)/g
2563
+ ];
2564
+ for (const pattern of patterns) {
2565
+ let match;
2566
+ while ((match = pattern.exec(content2)) !== null) {
2567
+ const classString = match[1];
2568
+ const individualClasses = classString.split(/\s+/).filter(Boolean);
2569
+ for (const cls of individualClasses) {
2570
+ const cleanClass = cls.replace(/['"`,]/g, "").trim();
2571
+ if (cleanClass && !cleanClass.includes("{") && !cleanClass.includes("$")) {
2572
+ classes.add(cleanClass);
2573
+ }
2574
+ }
2575
+ }
2576
+ }
2577
+ }
2578
+ async function haveContentFilesChanged() {
2579
+ const { glob } = await import("glob");
2580
+ for (const pattern of tailwindConfig.content) {
2581
+ if (typeof pattern !== "string")
2582
+ continue;
2583
+ const matches = await glob(pattern, {
2584
+ cwd: root,
2585
+ absolute: true,
2586
+ ignore: ["**/node_modules/**"]
2587
+ });
2588
+ for (const file of matches) {
2589
+ try {
2590
+ const stats = statSync(file);
2591
+ const cachedTime = contentFilesCache.get(file);
2592
+ if (!cachedTime || stats.mtimeMs > cachedTime) {
2593
+ contentFilesCache.set(file, stats.mtimeMs);
2594
+ return true;
2595
+ }
2596
+ } catch {}
2597
+ }
2598
+ }
2599
+ return false;
2600
+ }
2601
+ function invalidateCache() {
2602
+ cssCache.clear();
2603
+ processor = null;
2604
+ }
2605
+ return {
2606
+ name: "ereo:tailwind",
2607
+ async setup(context) {
2608
+ root = context.root;
2609
+ mode = context.mode;
2610
+ const userConfig = await loadTailwindConfig();
2611
+ tailwindConfig = mergeConfig(userConfig);
2612
+ processor = createProcessor(tailwindConfig);
2613
+ console.log(` [Tailwind] Initialized in ${mode} mode`);
2614
+ console.log(` [Tailwind] Scanning ${tailwindConfig.content.length} content patterns`);
2615
+ },
2616
+ async transform(code, id) {
2617
+ if (!id.endsWith(".css")) {
2618
+ return null;
2619
+ }
2620
+ const hasTailwindDirectives = code.includes("@tailwind") || code.includes("@apply") || code.includes("@layer") || code.includes("@config");
2621
+ if (!hasTailwindDirectives) {
2622
+ return null;
2623
+ }
2624
+ try {
2625
+ if (mode === "development" && watch) {
2626
+ const changed = await haveContentFilesChanged();
2627
+ if (changed) {
2628
+ invalidateCache();
2629
+ }
2630
+ }
2631
+ const result = await processTailwindCSS(code, id);
2632
+ return result.css;
2633
+ } catch (error) {
2634
+ if (mode === "development") {
2635
+ return `/* Tailwind CSS Error: ${error.message.replace(/\*\//g, "*\\/")} */
2636
+ ${code}`;
2637
+ }
2638
+ throw error;
2639
+ }
2640
+ },
2641
+ resolveId(id) {
2642
+ if (id === "virtual:tailwind.css" || id === "@ereo/tailwind") {
2643
+ return "\x00virtual:tailwind.css";
2644
+ }
2645
+ return null;
2646
+ },
2647
+ async load(id) {
2648
+ if (id === "\x00virtual:tailwind.css") {
2649
+ const css = `
2650
+ @tailwind base;
2651
+ @tailwind components;
2652
+ @tailwind utilities;
2653
+ `.trim();
2654
+ try {
2655
+ const result = await processTailwindCSS(css, "virtual:tailwind.css");
2656
+ return result.css;
2657
+ } catch (error) {
2658
+ console.error(" [Tailwind] Failed to generate virtual CSS:", error.message);
2659
+ return css;
2660
+ }
2661
+ }
2662
+ return null;
2663
+ },
2664
+ async configureServer(server) {
2665
+ const configPaths = [
2666
+ "tailwind.config.js",
2667
+ "tailwind.config.ts",
2668
+ "tailwind.config.mjs",
2669
+ "tailwind.config.cjs"
2670
+ ];
2671
+ if (server.watcher) {
2672
+ for (const configFile of configPaths) {
2673
+ const fullPath = join4(root, configFile);
2674
+ if (existsSync(fullPath)) {
2675
+ server.watcher.add(fullPath);
2676
+ }
2677
+ }
2678
+ server.watcher.on("change", async (file) => {
2679
+ if (configPaths.some((cfg) => file.endsWith(cfg))) {
2680
+ console.log(" [Tailwind] Config changed, reloading...");
2681
+ const userConfig = await loadTailwindConfig();
2682
+ tailwindConfig = mergeConfig(userConfig);
2683
+ processor = createProcessor(tailwindConfig);
2684
+ invalidateCache();
2685
+ if (server.ws) {
2686
+ server.ws.send({
2687
+ type: "full-reload",
2688
+ path: "*"
2689
+ });
2690
+ }
2691
+ }
2692
+ });
2693
+ }
2694
+ if (server.middlewares) {
2695
+ server.middlewares.push(async (request, context, next) => {
2696
+ const url = new URL(request.url);
2697
+ if (url.pathname === "/__tailwind.css") {
2698
+ try {
2699
+ const css = `
2700
+ @tailwind base;
2701
+ @tailwind components;
2702
+ @tailwind utilities;
2703
+ `.trim();
2704
+ const result = await processTailwindCSS(css, "__tailwind.css");
2705
+ return new Response(result.css, {
2706
+ headers: {
2707
+ "Content-Type": "text/css",
2708
+ "Cache-Control": "no-cache, no-store, must-revalidate",
2709
+ "X-Tailwind-Version": "3.4"
2710
+ }
2711
+ });
2712
+ } catch (error) {
2713
+ return new Response(`/* Tailwind CSS Error: ${error.message} */`, {
2714
+ status: 500,
2715
+ headers: { "Content-Type": "text/css" }
2716
+ });
2717
+ }
2718
+ }
2719
+ return next();
2720
+ });
2721
+ }
2722
+ },
2723
+ async buildStart() {
2724
+ if (mode === "production") {
2725
+ console.log(" [Tailwind] Scanning content files...");
2726
+ const { classes, files } = await scanContentFiles();
2727
+ scannedClasses = classes;
2728
+ console.log(` [Tailwind] Found ${classes.size} unique classes in ${files.length} files`);
2729
+ }
2730
+ },
2731
+ async buildEnd() {
2732
+ if (mode === "production") {
2733
+ console.log(" [Tailwind] Build complete");
2734
+ }
2735
+ }
2736
+ };
2737
+ }
2738
+ function generateTailwindConfig(options = {}) {
2739
+ const { content = DEFAULT_CONTENT_PATHS, darkMode = "class" } = options;
2740
+ return `
2741
+ /** @type {import('tailwindcss').Config} */
2742
+ export default {
2743
+ content: ${JSON.stringify(content, null, 4)},
2744
+ darkMode: '${darkMode}',
2745
+ theme: {
2746
+ extend: {
2747
+ // Add your custom theme extensions here
2748
+ colors: {
2749
+ // primary: '#3b82f6',
2750
+ },
2751
+ fontFamily: {
2752
+ // sans: ['Inter', 'sans-serif'],
2753
+ },
2754
+ },
2755
+ },
2756
+ plugins: [
2757
+ // Add plugins here
2758
+ // require('@tailwindcss/forms'),
2759
+ // require('@tailwindcss/typography'),
2760
+ ],
2761
+ };
2762
+ `.trim();
2763
+ }
2764
+ function generateCSSEntry() {
2765
+ return `
2766
+ @tailwind base;
2767
+ @tailwind components;
2768
+ @tailwind utilities;
2769
+
2770
+ /*
2771
+ * Custom base styles
2772
+ * Use @layer base { ... } for base styles
2773
+ */
2774
+
2775
+ /*
2776
+ * Custom components
2777
+ * Use @layer components { ... } for component styles
2778
+ */
2779
+
2780
+ /*
2781
+ * Custom utilities
2782
+ * Use @layer utilities { ... } for utility styles
2783
+ */
2784
+ `.trim();
2785
+ }
2786
+ async function hasTailwindConfig(root) {
2787
+ const configFiles = [
2788
+ "tailwind.config.js",
2789
+ "tailwind.config.ts",
2790
+ "tailwind.config.cjs",
2791
+ "tailwind.config.mjs"
2792
+ ];
2793
+ for (const file of configFiles) {
2794
+ if (existsSync(join4(root, file))) {
2795
+ return true;
2796
+ }
2797
+ }
2798
+ try {
2799
+ const pkgPath = join4(root, "package.json");
2800
+ if (existsSync(pkgPath)) {
2801
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2802
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2803
+ return "tailwindcss" in deps;
2804
+ }
2805
+ } catch {}
2806
+ return false;
2807
+ }
2808
+ function tailwindMiddleware(options = {}) {
2809
+ let processor = null;
2810
+ let tailwindConfig = null;
2811
+ let cssCache = null;
2812
+ let cacheTimestamp = 0;
2813
+ async function getProcessor(root) {
2814
+ if (!processor) {
2815
+ const configPaths = [
2816
+ options.config ? resolve(root, options.config) : null,
2817
+ join4(root, "tailwind.config.js"),
2818
+ join4(root, "tailwind.config.ts")
2819
+ ].filter(Boolean);
2820
+ for (const configPath of configPaths) {
2821
+ if (existsSync(configPath)) {
2822
+ try {
2823
+ const imported = await import(configPath);
2824
+ tailwindConfig = imported.default || imported;
2825
+ break;
2826
+ } catch {}
2827
+ }
2828
+ }
2829
+ if (!tailwindConfig) {
2830
+ tailwindConfig = {
2831
+ content: (options.content || DEFAULT_CONTENT_PATHS).map((p) => resolve(root, p)),
2832
+ darkMode: options.darkMode || "class",
2833
+ theme: { extend: {} },
2834
+ plugins: []
2835
+ };
2836
+ }
2837
+ processor = postcss([
2838
+ tailwindcss(tailwindConfig),
2839
+ autoprefixer()
2840
+ ]);
2841
+ }
2842
+ return processor;
2843
+ }
2844
+ return async (request, context, next) => {
2845
+ const url = new URL(request.url);
2846
+ if (url.pathname === "/__tailwind.css") {
2847
+ const root = context.root || process.cwd();
2848
+ try {
2849
+ const now = Date.now();
2850
+ if (cssCache && now - cacheTimestamp < 1000) {
2851
+ return new Response(cssCache, {
2852
+ headers: {
2853
+ "Content-Type": "text/css",
2854
+ "Cache-Control": "no-cache"
2855
+ }
2856
+ });
2857
+ }
2858
+ const proc = await getProcessor(root);
2859
+ const css = `
2860
+ @tailwind base;
2861
+ @tailwind components;
2862
+ @tailwind utilities;
2863
+ `.trim();
2864
+ const result = await proc.process(css, {
2865
+ from: "__tailwind.css"
2866
+ });
2867
+ cssCache = result.css;
2868
+ cacheTimestamp = now;
2869
+ return new Response(result.css, {
2870
+ headers: {
2871
+ "Content-Type": "text/css",
2872
+ "Cache-Control": "no-cache"
2873
+ }
2874
+ });
2875
+ } catch (error) {
2876
+ return new Response(`/* Tailwind CSS Error: ${error.message} */`, {
2877
+ status: 500,
2878
+ headers: { "Content-Type": "text/css" }
2879
+ });
2880
+ }
2881
+ }
2882
+ return next();
2883
+ };
2884
+ }
2885
+ function extractTailwindClasses(content) {
2886
+ const classes = new Set;
2887
+ const patterns = [
2888
+ /class(?:Name)?=["']([^"']+)["']/g,
2889
+ /class(?:Name)?={`([^`]+)`}/g,
2890
+ /class(?:Name)?={\s*["']([^"']+)["']\s*}/g,
2891
+ /tw`([^`]+)`/g,
2892
+ /clsx\(\s*["']([^"']+)["']/g,
2893
+ /cn\(\s*["']([^"']+)["']/g
2894
+ ];
2895
+ for (const pattern of patterns) {
2896
+ let match;
2897
+ while ((match = pattern.exec(content)) !== null) {
2898
+ const classString = match[1];
2899
+ classString.split(/\s+/).filter(Boolean).forEach((cls) => {
2900
+ const cleanClass = cls.replace(/['"`,]/g, "").trim();
2901
+ if (cleanClass && !cleanClass.includes("{") && !cleanClass.includes("$")) {
2902
+ classes.add(cleanClass);
2903
+ }
2904
+ });
2905
+ }
2906
+ }
2907
+ return Array.from(classes);
2908
+ }
2909
+ async function generateSafelist(root, patterns) {
2910
+ const classes = new Set;
2911
+ try {
2912
+ const { glob } = await import("glob");
2913
+ for (const pattern of patterns) {
2914
+ const files = await glob(pattern, {
2915
+ cwd: root,
2916
+ absolute: true,
2917
+ ignore: ["**/node_modules/**"]
2918
+ });
2919
+ for (const file of files) {
2920
+ const content = readFileSync(file, "utf-8");
2921
+ extractTailwindClasses(content).forEach((cls) => classes.add(cls));
2922
+ }
2923
+ }
2924
+ } catch (error) {
2925
+ console.warn("Failed to generate safelist:", error);
2926
+ }
2927
+ return Array.from(classes);
2928
+ }
2929
+ export {
2930
+ writeRouteTypes,
2931
+ transformIslandJSX,
2932
+ tailwindMiddleware,
2933
+ printBuildReport,
2934
+ parseError,
2935
+ hasTailwindConfig,
2936
+ hasIslands,
2937
+ generateTailwindConfig,
2938
+ generateSafelist,
2939
+ generateRouteTypes,
2940
+ generateLinkTypes,
2941
+ generateIslandManifest,
2942
+ generateIslandEntry,
2943
+ generateHookTypes,
2944
+ generateErrorOverlayHTML,
2945
+ generateCSSEntry,
2946
+ formatSize,
2947
+ findIslandByName,
2948
+ extractTailwindClasses,
2949
+ extractParams,
2950
+ extractIslands,
2951
+ createTypesPlugin,
2952
+ createTailwindPlugin,
2953
+ createIslandsPlugin,
2954
+ createHMRWebSocket,
2955
+ createHMRWatcher,
2956
+ createHMRServer,
2957
+ createErrorResponse,
2958
+ createErrorJSON,
2959
+ build,
2960
+ analyzeBuild,
2961
+ HMR_CLIENT_CODE,
2962
+ HMRWatcher,
2963
+ HMRServer,
2964
+ ERROR_OVERLAY_SCRIPT
2965
+ };