@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/README.md +91 -0
- package/dist/dev/error-overlay.d.ts +40 -0
- package/dist/dev/error-overlay.d.ts.map +1 -0
- package/dist/dev/hmr.d.ts +161 -0
- package/dist/dev/hmr.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2965 -0
- package/dist/plugins/islands.d.ts +46 -0
- package/dist/plugins/islands.d.ts.map +1 -0
- package/dist/plugins/tailwind.d.ts +56 -0
- package/dist/plugins/tailwind.d.ts.map +1 -0
- package/dist/plugins/types.d.ts +108 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/prod/build.d.ts +78 -0
- package/dist/prod/build.d.ts.map +1 -0
- package/package.json +47 -0
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
+
};
|