@cloudwerk/vite-plugin 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -1
- package/dist/index.js +324 -49
- package/package.json +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,11 @@ interface CloudwerkVitePluginOptions {
|
|
|
52
52
|
* @default 'hono-jsx'
|
|
53
53
|
*/
|
|
54
54
|
renderer?: 'hono-jsx' | 'react';
|
|
55
|
+
/**
|
|
56
|
+
* Directory for static assets served at root.
|
|
57
|
+
* @default 'public'
|
|
58
|
+
*/
|
|
59
|
+
publicDir?: string;
|
|
55
60
|
}
|
|
56
61
|
/**
|
|
57
62
|
* Resolved plugin options after applying defaults and loading config.
|
|
@@ -73,6 +78,8 @@ interface ResolvedCloudwerkOptions {
|
|
|
73
78
|
hydrationEndpoint: string;
|
|
74
79
|
/** UI renderer name */
|
|
75
80
|
renderer: 'hono-jsx' | 'react';
|
|
81
|
+
/** Directory for static assets (relative to root) */
|
|
82
|
+
publicDir: string;
|
|
76
83
|
/** Vite root directory (absolute path) */
|
|
77
84
|
root: string;
|
|
78
85
|
}
|
|
@@ -149,7 +156,7 @@ declare function cloudwerkPlugin(options?: CloudwerkVitePluginOptions): Plugin;
|
|
|
149
156
|
* - Layouts applied to pages in correct order
|
|
150
157
|
* - Middleware chains applied
|
|
151
158
|
* - Route config support
|
|
152
|
-
* - Error and 404 handling
|
|
159
|
+
* - Error and 404 handling with error.tsx and not-found.tsx support
|
|
153
160
|
*
|
|
154
161
|
* @param manifest - Route manifest from @cloudwerk/core
|
|
155
162
|
* @param scanResult - Scan result with file information
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/plugin.ts
|
|
2
|
-
import * as
|
|
2
|
+
import * as path2 from "path";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import {
|
|
5
5
|
scanRoutes,
|
|
6
6
|
buildRouteManifest,
|
|
7
7
|
resolveLayouts,
|
|
8
8
|
resolveMiddleware,
|
|
9
|
+
resolveErrorBoundary,
|
|
10
|
+
resolveNotFoundBoundary,
|
|
9
11
|
loadConfig,
|
|
10
12
|
resolveRoutesPath,
|
|
11
13
|
hasUseClientDirective,
|
|
@@ -26,19 +28,56 @@ var RESOLVED_VIRTUAL_IDS = {
|
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
// src/virtual-modules/server-entry.ts
|
|
31
|
+
import * as path from "path";
|
|
29
32
|
function generateServerEntry(manifest, scanResult, options) {
|
|
30
33
|
const imports = [];
|
|
31
34
|
const pageRegistrations = [];
|
|
32
35
|
const routeRegistrations = [];
|
|
33
36
|
const layoutImports = [];
|
|
34
37
|
const middlewareImports = [];
|
|
38
|
+
const errorImports = [];
|
|
39
|
+
const notFoundImports = [];
|
|
35
40
|
const importedModules = /* @__PURE__ */ new Set();
|
|
36
41
|
const layoutModules = /* @__PURE__ */ new Map();
|
|
37
42
|
const middlewareModules = /* @__PURE__ */ new Map();
|
|
43
|
+
const errorModules = /* @__PURE__ */ new Map();
|
|
44
|
+
const notFoundModules = /* @__PURE__ */ new Map();
|
|
38
45
|
let pageIndex = 0;
|
|
39
46
|
let routeIndex = 0;
|
|
40
47
|
let layoutIndex = 0;
|
|
41
48
|
let middlewareIndex = 0;
|
|
49
|
+
let errorIndex = 0;
|
|
50
|
+
let notFoundIndex = 0;
|
|
51
|
+
for (const err of scanResult.errors) {
|
|
52
|
+
if (!importedModules.has(err.absolutePath)) {
|
|
53
|
+
const varName = `error_${errorIndex++}`;
|
|
54
|
+
errorImports.push(`import * as ${varName} from '${err.absolutePath}'`);
|
|
55
|
+
errorModules.set(err.absolutePath, varName);
|
|
56
|
+
importedModules.add(err.absolutePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const nf of scanResult.notFound) {
|
|
60
|
+
if (!importedModules.has(nf.absolutePath)) {
|
|
61
|
+
const varName = `notFound_${notFoundIndex++}`;
|
|
62
|
+
notFoundImports.push(`import * as ${varName} from '${nf.absolutePath}'`);
|
|
63
|
+
notFoundModules.set(nf.absolutePath, varName);
|
|
64
|
+
importedModules.add(nf.absolutePath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const errorBoundaryMapEntries = [];
|
|
68
|
+
for (const err of scanResult.errors) {
|
|
69
|
+
const dir = path.posix.dirname(err.relativePath);
|
|
70
|
+
const normalizedDir = dir === "." ? "" : dir;
|
|
71
|
+
const varName = errorModules.get(err.absolutePath);
|
|
72
|
+
errorBoundaryMapEntries.push(` ['${normalizedDir}', ${varName}]`);
|
|
73
|
+
}
|
|
74
|
+
const notFoundBoundaryMapEntries = [];
|
|
75
|
+
for (const nf of scanResult.notFound) {
|
|
76
|
+
const dir = path.posix.dirname(nf.relativePath);
|
|
77
|
+
const normalizedDir = dir === "." ? "" : dir;
|
|
78
|
+
const varName = notFoundModules.get(nf.absolutePath);
|
|
79
|
+
notFoundBoundaryMapEntries.push(` ['${normalizedDir}', ${varName}]`);
|
|
80
|
+
}
|
|
42
81
|
for (const route of manifest.routes) {
|
|
43
82
|
for (const middlewarePath of route.middleware) {
|
|
44
83
|
if (!importedModules.has(middlewarePath)) {
|
|
@@ -63,15 +102,17 @@ function generateServerEntry(manifest, scanResult, options) {
|
|
|
63
102
|
imports.push(`import * as ${varName} from '${route.absolutePath}'`);
|
|
64
103
|
const layoutChain = route.layouts.map((p) => layoutModules.get(p)).join(", ");
|
|
65
104
|
const middlewareChain = route.middleware.map((p) => middlewareModules.get(p)).join(", ");
|
|
105
|
+
const errorModule = route.errorBoundary ? errorModules.get(route.errorBoundary) : null;
|
|
106
|
+
const notFoundModule = route.notFoundBoundary ? notFoundModules.get(route.notFoundBoundary) : null;
|
|
66
107
|
const hasOptionalCatchAll = route.segments.some((s) => s.type === "optionalCatchAll");
|
|
67
108
|
if (hasOptionalCatchAll) {
|
|
68
109
|
const basePath = route.urlPattern.replace(/\/:[^/]+\{\.\*\}$/, "") || "/";
|
|
69
110
|
pageRegistrations.push(
|
|
70
|
-
` registerPage(app, '${basePath}', ${varName}, [${layoutChain}], [${middlewareChain}])`
|
|
111
|
+
` registerPage(app, '${basePath}', ${varName}, [${layoutChain}], [${middlewareChain}], ${errorModule || "null"}, ${notFoundModule || "null"})`
|
|
71
112
|
);
|
|
72
113
|
}
|
|
73
114
|
pageRegistrations.push(
|
|
74
|
-
` registerPage(app, '${route.urlPattern}', ${varName}, [${layoutChain}], [${middlewareChain}])`
|
|
115
|
+
` registerPage(app, '${route.urlPattern}', ${varName}, [${layoutChain}], [${middlewareChain}], ${errorModule || "null"}, ${notFoundModule || "null"})`
|
|
75
116
|
);
|
|
76
117
|
} else if (route.fileType === "route") {
|
|
77
118
|
const varName = `route_${routeIndex++}`;
|
|
@@ -89,7 +130,7 @@ function generateServerEntry(manifest, scanResult, options) {
|
|
|
89
130
|
*/
|
|
90
131
|
|
|
91
132
|
import { Hono } from 'hono'
|
|
92
|
-
import { contextMiddleware, createHandlerAdapter, createMiddlewareAdapter, setRouteConfig } from '@cloudwerk/core/runtime'
|
|
133
|
+
import { contextMiddleware, createHandlerAdapter, createMiddlewareAdapter, setRouteConfig, NotFoundError, RedirectError } from '@cloudwerk/core/runtime'
|
|
93
134
|
import { setActiveRenderer } from '@cloudwerk/ui'
|
|
94
135
|
|
|
95
136
|
// Page and Route Imports
|
|
@@ -101,13 +142,151 @@ ${layoutImports.join("\n")}
|
|
|
101
142
|
// Middleware Imports
|
|
102
143
|
${middlewareImports.join("\n")}
|
|
103
144
|
|
|
145
|
+
// Error Boundary Imports
|
|
146
|
+
${errorImports.join("\n")}
|
|
147
|
+
|
|
148
|
+
// Not-Found Boundary Imports
|
|
149
|
+
${notFoundImports.join("\n")}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Boundary Maps for Runtime Lookup
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
const errorBoundaryMap = new Map([
|
|
156
|
+
${errorBoundaryMapEntries.join(",\n")}
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
const notFoundBoundaryMap = new Map([
|
|
160
|
+
${notFoundBoundaryMapEntries.join(",\n")}
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Helper Functions
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate a unique error digest for matching server logs.
|
|
169
|
+
*/
|
|
170
|
+
function generateErrorDigest() {
|
|
171
|
+
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Find the closest error boundary for a given URL path.
|
|
176
|
+
* Walks from closest directory to root, returning first match.
|
|
177
|
+
*/
|
|
178
|
+
function findClosestErrorBoundary(urlPath) {
|
|
179
|
+
// Convert URL path to directory segments
|
|
180
|
+
const segments = urlPath.split('/').filter(Boolean)
|
|
181
|
+
|
|
182
|
+
// Walk from closest to root
|
|
183
|
+
while (segments.length >= 0) {
|
|
184
|
+
const dir = segments.join('/')
|
|
185
|
+
const boundary = errorBoundaryMap.get(dir)
|
|
186
|
+
if (boundary) {
|
|
187
|
+
return boundary
|
|
188
|
+
}
|
|
189
|
+
if (segments.length === 0) break
|
|
190
|
+
segments.pop()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Find the closest not-found boundary for a given URL path.
|
|
198
|
+
* Walks from closest directory to root, returning first match.
|
|
199
|
+
*/
|
|
200
|
+
function findClosestNotFoundBoundary(urlPath) {
|
|
201
|
+
// Convert URL path to directory segments
|
|
202
|
+
const segments = urlPath.split('/').filter(Boolean)
|
|
203
|
+
|
|
204
|
+
// Walk from closest to root
|
|
205
|
+
while (segments.length >= 0) {
|
|
206
|
+
const dir = segments.join('/')
|
|
207
|
+
const boundary = notFoundBoundaryMap.get(dir)
|
|
208
|
+
if (boundary) {
|
|
209
|
+
return boundary
|
|
210
|
+
}
|
|
211
|
+
if (segments.length === 0) break
|
|
212
|
+
segments.pop()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Render an error page with the given error boundary module.
|
|
220
|
+
*/
|
|
221
|
+
async function renderErrorPage(error, errorModule, layoutModules, layoutLoaderData, params, searchParams, errorType) {
|
|
222
|
+
// Add digest to error for log matching
|
|
223
|
+
const digest = generateErrorDigest()
|
|
224
|
+
error.digest = digest
|
|
225
|
+
|
|
226
|
+
// Build error boundary props
|
|
227
|
+
const errorProps = {
|
|
228
|
+
error: {
|
|
229
|
+
message: error.message,
|
|
230
|
+
digest,
|
|
231
|
+
stack: error.stack,
|
|
232
|
+
},
|
|
233
|
+
errorType,
|
|
234
|
+
reset: () => {}, // No-op on server
|
|
235
|
+
params,
|
|
236
|
+
searchParams,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Render error boundary
|
|
240
|
+
let element = await Promise.resolve(errorModule.default(errorProps))
|
|
241
|
+
|
|
242
|
+
// Wrap with layouts if available
|
|
243
|
+
for (let i = layoutModules.length - 1; i >= 0; i--) {
|
|
244
|
+
const Layout = layoutModules[i].default
|
|
245
|
+
const layoutProps = {
|
|
246
|
+
children: element,
|
|
247
|
+
params,
|
|
248
|
+
...layoutLoaderData[i],
|
|
249
|
+
}
|
|
250
|
+
element = await Promise.resolve(Layout(layoutProps))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return renderWithHydration(element, 500)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Render a not-found page with the given not-found boundary module.
|
|
258
|
+
*/
|
|
259
|
+
async function renderNotFoundPage(notFoundModule, layoutModules, layoutLoaderData, params, searchParams) {
|
|
260
|
+
// Build not-found props
|
|
261
|
+
const notFoundProps = {
|
|
262
|
+
params,
|
|
263
|
+
searchParams,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Render not-found boundary
|
|
267
|
+
let element = await Promise.resolve(notFoundModule.default(notFoundProps))
|
|
268
|
+
|
|
269
|
+
// Wrap with layouts if available
|
|
270
|
+
for (let i = layoutModules.length - 1; i >= 0; i--) {
|
|
271
|
+
const Layout = layoutModules[i].default
|
|
272
|
+
const layoutProps = {
|
|
273
|
+
children: element,
|
|
274
|
+
params,
|
|
275
|
+
...layoutLoaderData[i],
|
|
276
|
+
}
|
|
277
|
+
element = await Promise.resolve(Layout(layoutProps))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return renderWithHydration(element, 404)
|
|
281
|
+
}
|
|
282
|
+
|
|
104
283
|
// ============================================================================
|
|
105
284
|
// Route Registration Helpers
|
|
106
285
|
// ============================================================================
|
|
107
286
|
|
|
108
287
|
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
|
|
109
288
|
|
|
110
|
-
function registerPage(app, pattern, pageModule, layoutModules, middlewareModules) {
|
|
289
|
+
function registerPage(app, pattern, pageModule, layoutModules, middlewareModules, errorModule, notFoundModule) {
|
|
111
290
|
// Apply middleware (wrap with adapter to convert Cloudwerk middleware to Hono middleware)
|
|
112
291
|
for (const mw of middlewareModules) {
|
|
113
292
|
app.use(pattern, createMiddlewareAdapter(mw))
|
|
@@ -128,51 +307,76 @@ function registerPage(app, pattern, pageModule, layoutModules, middlewareModules
|
|
|
128
307
|
const url = new URL(request.url)
|
|
129
308
|
const searchParams = Object.fromEntries(url.searchParams.entries())
|
|
130
309
|
|
|
131
|
-
//
|
|
310
|
+
// Track layout loader data for use in error boundaries
|
|
132
311
|
const layoutLoaderData = []
|
|
133
312
|
const loaderArgs = { params, request, context: c }
|
|
134
313
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
314
|
+
try {
|
|
315
|
+
// Execute layout loaders
|
|
316
|
+
for (const layoutModule of layoutModules) {
|
|
317
|
+
if (layoutModule.loader) {
|
|
318
|
+
const data = await Promise.resolve(layoutModule.loader(loaderArgs))
|
|
319
|
+
layoutLoaderData.push(data ?? {})
|
|
320
|
+
} else {
|
|
321
|
+
layoutLoaderData.push({})
|
|
322
|
+
}
|
|
141
323
|
}
|
|
142
|
-
}
|
|
143
324
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
325
|
+
// Execute page loader
|
|
326
|
+
let pageLoaderData = {}
|
|
327
|
+
if (pageModule.loader) {
|
|
328
|
+
pageLoaderData = (await Promise.resolve(pageModule.loader(loaderArgs))) ?? {}
|
|
329
|
+
}
|
|
149
330
|
|
|
150
|
-
|
|
151
|
-
|
|
331
|
+
// Build page props
|
|
332
|
+
const pageProps = { params, searchParams, ...pageLoaderData }
|
|
152
333
|
|
|
153
|
-
|
|
154
|
-
|
|
334
|
+
// Render page
|
|
335
|
+
let element = await Promise.resolve(pageModule.default(pageProps))
|
|
155
336
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
337
|
+
// Wrap with layouts (inside-out)
|
|
338
|
+
for (let i = layoutModules.length - 1; i >= 0; i--) {
|
|
339
|
+
const Layout = layoutModules[i].default
|
|
340
|
+
const layoutProps = {
|
|
341
|
+
children: element,
|
|
342
|
+
params,
|
|
343
|
+
...layoutLoaderData[i],
|
|
344
|
+
}
|
|
345
|
+
element = await Promise.resolve(Layout(layoutProps))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Render the page with hydration script injection
|
|
349
|
+
return renderWithHydration(element)
|
|
350
|
+
} catch (error) {
|
|
351
|
+
// Handle NotFoundError (check both instanceof and name for module duplication)
|
|
352
|
+
if (error instanceof NotFoundError || error?.name === 'NotFoundError') {
|
|
353
|
+
if (notFoundModule) {
|
|
354
|
+
return renderNotFoundPage(notFoundModule, layoutModules, layoutLoaderData, params, searchParams)
|
|
355
|
+
}
|
|
356
|
+
// Re-throw to trigger global not-found handler
|
|
357
|
+
throw error
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Handle RedirectError (check both instanceof and name for module duplication)
|
|
361
|
+
if (error instanceof RedirectError || error?.name === 'RedirectError') {
|
|
362
|
+
return c.redirect(error.url, error.status)
|
|
163
363
|
}
|
|
164
|
-
element = await Promise.resolve(Layout(layoutProps))
|
|
165
|
-
}
|
|
166
364
|
|
|
167
|
-
|
|
168
|
-
|
|
365
|
+
// Handle other errors
|
|
366
|
+
console.error('Page render error:', error.message)
|
|
367
|
+
if (errorModule) {
|
|
368
|
+
return renderErrorPage(error, errorModule, layoutModules, layoutLoaderData, params, searchParams, 'loader')
|
|
369
|
+
}
|
|
370
|
+
// Re-throw to trigger global error handler
|
|
371
|
+
throw error
|
|
372
|
+
}
|
|
169
373
|
})
|
|
170
374
|
}
|
|
171
375
|
|
|
172
376
|
/**
|
|
173
377
|
* Render element to a Response, injecting hydration script before </body>.
|
|
174
378
|
*/
|
|
175
|
-
function renderWithHydration(element) {
|
|
379
|
+
function renderWithHydration(element, status = 200) {
|
|
176
380
|
// Hono JSX elements have toString() for synchronous rendering
|
|
177
381
|
const html = '<!DOCTYPE html>' + String(element)
|
|
178
382
|
|
|
@@ -184,6 +388,7 @@ function renderWithHydration(element) {
|
|
|
184
388
|
: html + hydrationScript
|
|
185
389
|
|
|
186
390
|
return new Response(injectedHtml, {
|
|
391
|
+
status,
|
|
187
392
|
headers: {
|
|
188
393
|
'Content-Type': 'text/html; charset=utf-8',
|
|
189
394
|
},
|
|
@@ -240,13 +445,64 @@ ${pageRegistrations.join("\n")}
|
|
|
240
445
|
${routeRegistrations.join("\n")}
|
|
241
446
|
|
|
242
447
|
// 404 handler
|
|
243
|
-
app.notFound((c) => {
|
|
244
|
-
|
|
448
|
+
app.notFound(async (c) => {
|
|
449
|
+
const path = c.req.path
|
|
450
|
+
|
|
451
|
+
// API routes return JSON 404
|
|
452
|
+
if (path.startsWith('/api')) {
|
|
453
|
+
return c.json({ error: 'Not Found', path }, 404)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Try to find a not-found boundary for this path
|
|
457
|
+
const notFoundModule = findClosestNotFoundBoundary(path)
|
|
458
|
+
if (notFoundModule) {
|
|
459
|
+
return renderNotFoundPage(notFoundModule, [], [], {}, {})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Fallback to JSON 404
|
|
463
|
+
return c.json({ error: 'Not Found', path }, 404)
|
|
245
464
|
})
|
|
246
465
|
|
|
247
466
|
// Error handler
|
|
248
|
-
app.onError((err, c) => {
|
|
467
|
+
app.onError(async (err, c) => {
|
|
468
|
+
const path = c.req.path
|
|
469
|
+
|
|
470
|
+
// Handle NotFoundError (check both instanceof and name for module duplication)
|
|
471
|
+
if (err instanceof NotFoundError || err?.name === 'NotFoundError') {
|
|
472
|
+
// API routes return JSON 404
|
|
473
|
+
if (path.startsWith('/api')) {
|
|
474
|
+
return c.json({ error: 'Not Found', path }, 404)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Try to find a not-found boundary
|
|
478
|
+
const notFoundModule = findClosestNotFoundBoundary(path)
|
|
479
|
+
if (notFoundModule) {
|
|
480
|
+
return renderNotFoundPage(notFoundModule, [], [], {}, {})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return c.json({ error: 'Not Found', path }, 404)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Handle RedirectError (check both instanceof and name for module duplication)
|
|
487
|
+
if (err instanceof RedirectError || err?.name === 'RedirectError') {
|
|
488
|
+
return c.redirect(err.url, err.status)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Log the error
|
|
249
492
|
console.error('Request error:', err.message)
|
|
493
|
+
|
|
494
|
+
// API routes return JSON 500
|
|
495
|
+
if (path.startsWith('/api')) {
|
|
496
|
+
return c.json({ error: 'Internal Server Error', message: err.message }, 500)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Try to find an error boundary for this path
|
|
500
|
+
const errorModule = findClosestErrorBoundary(path)
|
|
501
|
+
if (errorModule) {
|
|
502
|
+
return renderErrorPage(err, errorModule, [], [], {}, {}, 'unknown')
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Fallback to JSON 500
|
|
250
506
|
return c.json({ error: 'Internal Server Error', message: err.message }, 500)
|
|
251
507
|
})
|
|
252
508
|
|
|
@@ -674,7 +930,9 @@ function cloudwerkPlugin(options = {}) {
|
|
|
674
930
|
state.scanResult,
|
|
675
931
|
routesPath,
|
|
676
932
|
resolveLayouts,
|
|
677
|
-
resolveMiddleware
|
|
933
|
+
resolveMiddleware,
|
|
934
|
+
resolveErrorBoundary,
|
|
935
|
+
resolveNotFoundBoundary
|
|
678
936
|
);
|
|
679
937
|
state.serverEntryCache = null;
|
|
680
938
|
state.clientEntryCache = null;
|
|
@@ -684,9 +942,9 @@ function cloudwerkPlugin(options = {}) {
|
|
|
684
942
|
}
|
|
685
943
|
function isRouteFile(filePath) {
|
|
686
944
|
if (!state) return false;
|
|
687
|
-
const appDir =
|
|
945
|
+
const appDir = path2.resolve(state.options.root, state.options.appDir);
|
|
688
946
|
if (!filePath.startsWith(appDir)) return false;
|
|
689
|
-
const basename2 =
|
|
947
|
+
const basename2 = path2.basename(filePath);
|
|
690
948
|
const nameWithoutExt = basename2.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
691
949
|
return ROUTE_FILE_NAMES.includes(nameWithoutExt);
|
|
692
950
|
}
|
|
@@ -707,18 +965,32 @@ function cloudwerkPlugin(options = {}) {
|
|
|
707
965
|
}
|
|
708
966
|
return {
|
|
709
967
|
name: "cloudwerk",
|
|
968
|
+
/**
|
|
969
|
+
* Pass publicDir configuration to Vite.
|
|
970
|
+
* This enables Vite's built-in static file serving for the public directory.
|
|
971
|
+
*/
|
|
972
|
+
async config(userConfig) {
|
|
973
|
+
if (userConfig.publicDir !== void 0) {
|
|
974
|
+
return {};
|
|
975
|
+
}
|
|
976
|
+
const root = userConfig.root ?? process.cwd();
|
|
977
|
+
const cloudwerkConfig = await loadConfig(root);
|
|
978
|
+
return {
|
|
979
|
+
publicDir: options.publicDir ?? cloudwerkConfig.publicDir ?? "public"
|
|
980
|
+
};
|
|
981
|
+
},
|
|
710
982
|
/**
|
|
711
983
|
* Resolve configuration and build initial manifest.
|
|
712
984
|
*/
|
|
713
985
|
async configResolved(config) {
|
|
714
986
|
const root = config.root;
|
|
715
|
-
let detectedServerEntry = options.serverEntry ?
|
|
987
|
+
let detectedServerEntry = options.serverEntry ? path2.resolve(root, options.serverEntry) : null;
|
|
716
988
|
if (!detectedServerEntry) {
|
|
717
989
|
const conventionalPaths = [
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
990
|
+
path2.resolve(root, "app/server.ts"),
|
|
991
|
+
path2.resolve(root, "app/server.tsx"),
|
|
992
|
+
path2.resolve(root, "src/server.ts"),
|
|
993
|
+
path2.resolve(root, "src/server.tsx")
|
|
722
994
|
];
|
|
723
995
|
for (const p of conventionalPaths) {
|
|
724
996
|
if (fs.existsSync(p)) {
|
|
@@ -740,6 +1012,7 @@ function cloudwerkPlugin(options = {}) {
|
|
|
740
1012
|
verbose: options.verbose ?? false,
|
|
741
1013
|
hydrationEndpoint: options.hydrationEndpoint ?? "/__cloudwerk",
|
|
742
1014
|
renderer: options.renderer ?? cloudwerkConfig.ui?.renderer ?? "hono-jsx",
|
|
1015
|
+
publicDir: options.publicDir ?? cloudwerkConfig.publicDir ?? "public",
|
|
743
1016
|
root
|
|
744
1017
|
};
|
|
745
1018
|
state = {
|
|
@@ -748,6 +1021,8 @@ function cloudwerkPlugin(options = {}) {
|
|
|
748
1021
|
routes: [],
|
|
749
1022
|
layouts: /* @__PURE__ */ new Map(),
|
|
750
1023
|
middleware: /* @__PURE__ */ new Map(),
|
|
1024
|
+
errorBoundaries: /* @__PURE__ */ new Map(),
|
|
1025
|
+
notFoundBoundaries: /* @__PURE__ */ new Map(),
|
|
751
1026
|
errors: [],
|
|
752
1027
|
warnings: [],
|
|
753
1028
|
generatedAt: /* @__PURE__ */ new Date(),
|
|
@@ -773,11 +1048,11 @@ function cloudwerkPlugin(options = {}) {
|
|
|
773
1048
|
configureServer(devServer) {
|
|
774
1049
|
server = devServer;
|
|
775
1050
|
if (!state) return;
|
|
776
|
-
const appDir =
|
|
1051
|
+
const appDir = path2.resolve(state.options.root, state.options.appDir);
|
|
777
1052
|
devServer.watcher.on("add", async (filePath) => {
|
|
778
1053
|
if (isRouteFile(filePath)) {
|
|
779
1054
|
if (state?.options.verbose) {
|
|
780
|
-
console.log(`[cloudwerk] Route added: ${
|
|
1055
|
+
console.log(`[cloudwerk] Route added: ${path2.relative(appDir, filePath)}`);
|
|
781
1056
|
}
|
|
782
1057
|
await buildManifest(state.options.root);
|
|
783
1058
|
invalidateVirtualModules();
|
|
@@ -786,7 +1061,7 @@ function cloudwerkPlugin(options = {}) {
|
|
|
786
1061
|
devServer.watcher.on("unlink", async (filePath) => {
|
|
787
1062
|
if (isRouteFile(filePath)) {
|
|
788
1063
|
if (state?.options.verbose) {
|
|
789
|
-
console.log(`[cloudwerk] Route removed: ${
|
|
1064
|
+
console.log(`[cloudwerk] Route removed: ${path2.relative(appDir, filePath)}`);
|
|
790
1065
|
}
|
|
791
1066
|
await buildManifest(state.options.root);
|
|
792
1067
|
invalidateVirtualModules();
|
|
@@ -795,7 +1070,7 @@ function cloudwerkPlugin(options = {}) {
|
|
|
795
1070
|
devServer.watcher.on("change", async (filePath) => {
|
|
796
1071
|
if (isRouteFile(filePath)) {
|
|
797
1072
|
if (state?.options.verbose) {
|
|
798
|
-
console.log(`[cloudwerk] Route changed: ${
|
|
1073
|
+
console.log(`[cloudwerk] Route changed: ${path2.relative(appDir, filePath)}`);
|
|
799
1074
|
}
|
|
800
1075
|
await buildManifest(state.options.root);
|
|
801
1076
|
invalidateVirtualModules();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudwerk/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Vite plugin for Cloudwerk file-based routing with virtual entry generation",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@swc/core": "^1.3.100",
|
|
22
|
-
"@cloudwerk/core": "^0.
|
|
23
|
-
"@cloudwerk/ui": "^0.
|
|
22
|
+
"@cloudwerk/core": "^0.9.0",
|
|
23
|
+
"@cloudwerk/ui": "^0.9.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"vite": "^5.0.0 || ^6.0.0",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@hono/vite-dev-server": ">=0.18.0",
|
|
36
37
|
"@types/node": "^20.0.0",
|
|
38
|
+
"hono": "^4.0.0",
|
|
37
39
|
"tsup": "^8.0.0",
|
|
38
40
|
"typescript": "^5.0.0",
|
|
39
41
|
"vite": "^6.0.0",
|