@cloudwerk/vite-plugin 0.1.1
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/LICENSE +21 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +903 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Squirrelsoft Dev Tools
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import { CloudwerkConfig, RouteManifest, ScanResult } from '@cloudwerk/core/build';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @cloudwerk/vite-plugin - Types
|
|
6
|
+
*
|
|
7
|
+
* Configuration and internal types for the Cloudwerk Vite plugin.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for the Cloudwerk Vite plugin.
|
|
12
|
+
*/
|
|
13
|
+
interface CloudwerkVitePluginOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Directory containing route files.
|
|
16
|
+
* @default 'app'
|
|
17
|
+
*/
|
|
18
|
+
appDir?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Subdirectory within appDir for routes.
|
|
21
|
+
* @default 'routes'
|
|
22
|
+
*/
|
|
23
|
+
routesDir?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Override Cloudwerk configuration.
|
|
26
|
+
* If not provided, loads from cloudwerk.config.ts
|
|
27
|
+
*/
|
|
28
|
+
config?: Partial<CloudwerkConfig>;
|
|
29
|
+
/**
|
|
30
|
+
* Custom server entry file path.
|
|
31
|
+
* If provided, disables virtual:cloudwerk/server-entry generation.
|
|
32
|
+
* Plugin also auto-detects if user provides their own app/server.ts
|
|
33
|
+
*/
|
|
34
|
+
serverEntry?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Custom client entry file path.
|
|
37
|
+
* If provided, disables virtual:cloudwerk/client-entry generation.
|
|
38
|
+
*/
|
|
39
|
+
clientEntry?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Enable verbose logging for debugging.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
verbose?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Hydration endpoint path for client bundles.
|
|
47
|
+
* @default '/__cloudwerk'
|
|
48
|
+
*/
|
|
49
|
+
hydrationEndpoint?: string;
|
|
50
|
+
/**
|
|
51
|
+
* UI renderer to use.
|
|
52
|
+
* @default 'hono-jsx'
|
|
53
|
+
*/
|
|
54
|
+
renderer?: 'hono-jsx' | 'react';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolved plugin options after applying defaults and loading config.
|
|
58
|
+
*/
|
|
59
|
+
interface ResolvedCloudwerkOptions {
|
|
60
|
+
/** Directory containing app files (relative to root) */
|
|
61
|
+
appDir: string;
|
|
62
|
+
/** Subdirectory within appDir for routes */
|
|
63
|
+
routesDir: string;
|
|
64
|
+
/** Loaded Cloudwerk configuration */
|
|
65
|
+
config: CloudwerkConfig;
|
|
66
|
+
/** Path to user-provided server entry, or null for generated */
|
|
67
|
+
serverEntry: string | null;
|
|
68
|
+
/** Path to user-provided client entry, or null for generated */
|
|
69
|
+
clientEntry: string | null;
|
|
70
|
+
/** Whether verbose logging is enabled */
|
|
71
|
+
verbose: boolean;
|
|
72
|
+
/** Hydration endpoint path */
|
|
73
|
+
hydrationEndpoint: string;
|
|
74
|
+
/** UI renderer name */
|
|
75
|
+
renderer: 'hono-jsx' | 'react';
|
|
76
|
+
/** Vite root directory (absolute path) */
|
|
77
|
+
root: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Information about a detected client component.
|
|
81
|
+
*/
|
|
82
|
+
interface ClientComponentInfo {
|
|
83
|
+
/** Component identifier for hydration */
|
|
84
|
+
componentId: string;
|
|
85
|
+
/** Path to the component's client bundle */
|
|
86
|
+
bundlePath: string;
|
|
87
|
+
/** Absolute file path */
|
|
88
|
+
absolutePath: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Virtual module IDs used by the plugin.
|
|
92
|
+
*/
|
|
93
|
+
declare const VIRTUAL_MODULE_IDS: {
|
|
94
|
+
readonly SERVER_ENTRY: "virtual:cloudwerk/server-entry";
|
|
95
|
+
readonly CLIENT_ENTRY: "virtual:cloudwerk/client-entry";
|
|
96
|
+
readonly MANIFEST: "virtual:cloudwerk/manifest";
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Resolved (internal) virtual module IDs with \0 prefix.
|
|
100
|
+
*/
|
|
101
|
+
declare const RESOLVED_VIRTUAL_IDS: {
|
|
102
|
+
readonly SERVER_ENTRY: "\0virtual:cloudwerk/server-entry";
|
|
103
|
+
readonly CLIENT_ENTRY: "\0virtual:cloudwerk/client-entry";
|
|
104
|
+
readonly MANIFEST: "\0virtual:cloudwerk/manifest";
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @cloudwerk/vite-plugin - Core Plugin
|
|
109
|
+
*
|
|
110
|
+
* Vite plugin that provides file-based routing for Cloudwerk with
|
|
111
|
+
* virtual module generation for server and client entry points.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create the Cloudwerk Vite plugin.
|
|
116
|
+
*
|
|
117
|
+
* @param options - Plugin configuration options
|
|
118
|
+
* @returns Vite plugin
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* // vite.config.ts
|
|
123
|
+
* import { defineConfig } from 'vite'
|
|
124
|
+
* import devServer from '@hono/vite-dev-server'
|
|
125
|
+
* import cloudwerk from '@cloudwerk/vite-plugin'
|
|
126
|
+
*
|
|
127
|
+
* export default defineConfig({
|
|
128
|
+
* plugins: [
|
|
129
|
+
* cloudwerk(),
|
|
130
|
+
* devServer({ entry: 'virtual:cloudwerk/server-entry' }),
|
|
131
|
+
* ],
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare function cloudwerkPlugin(options?: CloudwerkVitePluginOptions): Plugin;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Server Entry Virtual Module Generator
|
|
139
|
+
*
|
|
140
|
+
* Generates the virtual:cloudwerk/server-entry module that creates
|
|
141
|
+
* a Hono app with all routes registered from the file-based routing manifest.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate the server entry module code.
|
|
146
|
+
*
|
|
147
|
+
* This creates a complete Hono application with:
|
|
148
|
+
* - All page and API routes registered
|
|
149
|
+
* - Layouts applied to pages in correct order
|
|
150
|
+
* - Middleware chains applied
|
|
151
|
+
* - Route config support
|
|
152
|
+
* - Error and 404 handling
|
|
153
|
+
*
|
|
154
|
+
* @param manifest - Route manifest from @cloudwerk/core
|
|
155
|
+
* @param scanResult - Scan result with file information
|
|
156
|
+
* @param options - Resolved plugin options
|
|
157
|
+
* @returns Generated TypeScript/JavaScript code
|
|
158
|
+
*/
|
|
159
|
+
declare function generateServerEntry(manifest: RouteManifest, scanResult: ScanResult, options: ResolvedCloudwerkOptions): string;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Client Entry Virtual Module Generator
|
|
163
|
+
*
|
|
164
|
+
* Generates the virtual:cloudwerk/client-entry module that handles
|
|
165
|
+
* client-side hydration of server-rendered components.
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate the client entry module code.
|
|
170
|
+
*
|
|
171
|
+
* This creates a hydration bootstrap that:
|
|
172
|
+
* - Finds all elements with data-hydrate-id attributes
|
|
173
|
+
* - Dynamically imports the corresponding component bundles
|
|
174
|
+
* - Hydrates each component with its serialized props
|
|
175
|
+
*
|
|
176
|
+
* @param clientComponents - Map of detected client components
|
|
177
|
+
* @param options - Resolved plugin options
|
|
178
|
+
* @returns Generated JavaScript code
|
|
179
|
+
*/
|
|
180
|
+
declare function generateClientEntry(clientComponents: Map<string, ClientComponentInfo>, options: ResolvedCloudwerkOptions): string;
|
|
181
|
+
|
|
182
|
+
export { type ClientComponentInfo, type CloudwerkVitePluginOptions, RESOLVED_VIRTUAL_IDS, type ResolvedCloudwerkOptions, VIRTUAL_MODULE_IDS, cloudwerkPlugin, cloudwerkPlugin as default, generateClientEntry, generateServerEntry };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import {
|
|
5
|
+
scanRoutes,
|
|
6
|
+
buildRouteManifest,
|
|
7
|
+
resolveLayouts,
|
|
8
|
+
resolveMiddleware,
|
|
9
|
+
loadConfig,
|
|
10
|
+
resolveRoutesPath,
|
|
11
|
+
hasUseClientDirective,
|
|
12
|
+
generateComponentId,
|
|
13
|
+
ROUTE_FILE_NAMES
|
|
14
|
+
} from "@cloudwerk/core/build";
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var VIRTUAL_MODULE_IDS = {
|
|
18
|
+
SERVER_ENTRY: "virtual:cloudwerk/server-entry",
|
|
19
|
+
CLIENT_ENTRY: "virtual:cloudwerk/client-entry",
|
|
20
|
+
MANIFEST: "virtual:cloudwerk/manifest"
|
|
21
|
+
};
|
|
22
|
+
var RESOLVED_VIRTUAL_IDS = {
|
|
23
|
+
SERVER_ENTRY: "\0virtual:cloudwerk/server-entry",
|
|
24
|
+
CLIENT_ENTRY: "\0virtual:cloudwerk/client-entry",
|
|
25
|
+
MANIFEST: "\0virtual:cloudwerk/manifest"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/virtual-modules/server-entry.ts
|
|
29
|
+
function generateServerEntry(manifest, scanResult, options) {
|
|
30
|
+
const imports = [];
|
|
31
|
+
const pageRegistrations = [];
|
|
32
|
+
const routeRegistrations = [];
|
|
33
|
+
const layoutImports = [];
|
|
34
|
+
const middlewareImports = [];
|
|
35
|
+
const importedModules = /* @__PURE__ */ new Set();
|
|
36
|
+
const layoutModules = /* @__PURE__ */ new Map();
|
|
37
|
+
const middlewareModules = /* @__PURE__ */ new Map();
|
|
38
|
+
let pageIndex = 0;
|
|
39
|
+
let routeIndex = 0;
|
|
40
|
+
let layoutIndex = 0;
|
|
41
|
+
let middlewareIndex = 0;
|
|
42
|
+
for (const route of manifest.routes) {
|
|
43
|
+
for (const middlewarePath of route.middleware) {
|
|
44
|
+
if (!importedModules.has(middlewarePath)) {
|
|
45
|
+
const varName = `middleware_${middlewareIndex++}`;
|
|
46
|
+
middlewareImports.push(`import ${varName} from '${middlewarePath}'`);
|
|
47
|
+
middlewareModules.set(middlewarePath, varName);
|
|
48
|
+
importedModules.add(middlewarePath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (route.fileType === "page") {
|
|
52
|
+
for (const layoutPath of route.layouts) {
|
|
53
|
+
if (!importedModules.has(layoutPath)) {
|
|
54
|
+
const varName = `layout_${layoutIndex++}`;
|
|
55
|
+
layoutImports.push(`import * as ${varName} from '${layoutPath}'`);
|
|
56
|
+
layoutModules.set(layoutPath, varName);
|
|
57
|
+
importedModules.add(layoutPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (route.fileType === "page") {
|
|
62
|
+
const varName = `page_${pageIndex++}`;
|
|
63
|
+
imports.push(`import * as ${varName} from '${route.absolutePath}'`);
|
|
64
|
+
const layoutChain = route.layouts.map((p) => layoutModules.get(p)).join(", ");
|
|
65
|
+
const middlewareChain = route.middleware.map((p) => middlewareModules.get(p)).join(", ");
|
|
66
|
+
pageRegistrations.push(
|
|
67
|
+
` registerPage(app, '${route.urlPattern}', ${varName}, [${layoutChain}], [${middlewareChain}])`
|
|
68
|
+
);
|
|
69
|
+
} else if (route.fileType === "route") {
|
|
70
|
+
const varName = `route_${routeIndex++}`;
|
|
71
|
+
imports.push(`import * as ${varName} from '${route.absolutePath}'`);
|
|
72
|
+
const middlewareChain = route.middleware.map((p) => middlewareModules.get(p)).join(", ");
|
|
73
|
+
routeRegistrations.push(
|
|
74
|
+
` registerRoute(app, '${route.urlPattern}', ${varName}, [${middlewareChain}])`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const rendererName = options.renderer;
|
|
79
|
+
return `/**
|
|
80
|
+
* Generated Cloudwerk Server Entry
|
|
81
|
+
* This file is auto-generated by @cloudwerk/vite-plugin - do not edit
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import { Hono } from 'hono'
|
|
85
|
+
import { contextMiddleware, createHandlerAdapter, setRouteConfig } from '@cloudwerk/core/runtime'
|
|
86
|
+
import { setActiveRenderer } from '@cloudwerk/ui'
|
|
87
|
+
|
|
88
|
+
// Page and Route Imports
|
|
89
|
+
${imports.join("\n")}
|
|
90
|
+
|
|
91
|
+
// Layout Imports
|
|
92
|
+
${layoutImports.join("\n")}
|
|
93
|
+
|
|
94
|
+
// Middleware Imports
|
|
95
|
+
${middlewareImports.join("\n")}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Route Registration Helpers
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
|
|
102
|
+
|
|
103
|
+
function registerPage(app, pattern, pageModule, layoutModules, middlewareModules) {
|
|
104
|
+
// Apply middleware
|
|
105
|
+
for (const mw of middlewareModules) {
|
|
106
|
+
app.use(pattern, mw)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Apply config middleware if present
|
|
110
|
+
if (pageModule.config) {
|
|
111
|
+
app.use(pattern, async (_c, next) => {
|
|
112
|
+
setRouteConfig(pageModule.config)
|
|
113
|
+
await next()
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Register GET handler for page
|
|
118
|
+
app.get(pattern, async (c) => {
|
|
119
|
+
const params = c.req.param()
|
|
120
|
+
const request = c.req.raw
|
|
121
|
+
const url = new URL(request.url)
|
|
122
|
+
const searchParams = Object.fromEntries(url.searchParams.entries())
|
|
123
|
+
|
|
124
|
+
// Execute layout loaders
|
|
125
|
+
const layoutLoaderData = []
|
|
126
|
+
const loaderArgs = { params, request, context: c }
|
|
127
|
+
|
|
128
|
+
for (const layoutModule of layoutModules) {
|
|
129
|
+
if (layoutModule.loader) {
|
|
130
|
+
const data = await Promise.resolve(layoutModule.loader(loaderArgs))
|
|
131
|
+
layoutLoaderData.push(data ?? {})
|
|
132
|
+
} else {
|
|
133
|
+
layoutLoaderData.push({})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Execute page loader
|
|
138
|
+
let pageLoaderData = {}
|
|
139
|
+
if (pageModule.loader) {
|
|
140
|
+
pageLoaderData = (await Promise.resolve(pageModule.loader(loaderArgs))) ?? {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build page props
|
|
144
|
+
const pageProps = { params, searchParams, ...pageLoaderData }
|
|
145
|
+
|
|
146
|
+
// Render page
|
|
147
|
+
let element = await Promise.resolve(pageModule.default(pageProps))
|
|
148
|
+
|
|
149
|
+
// Wrap with layouts (inside-out)
|
|
150
|
+
for (let i = layoutModules.length - 1; i >= 0; i--) {
|
|
151
|
+
const Layout = layoutModules[i].default
|
|
152
|
+
const layoutProps = {
|
|
153
|
+
children: element,
|
|
154
|
+
params,
|
|
155
|
+
...layoutLoaderData[i],
|
|
156
|
+
}
|
|
157
|
+
element = await Promise.resolve(Layout(layoutProps))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Render the page with hydration script injection
|
|
161
|
+
return renderWithHydration(element)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Render element to a Response, injecting hydration script before </body>.
|
|
167
|
+
*/
|
|
168
|
+
function renderWithHydration(element) {
|
|
169
|
+
// Hono JSX elements have toString() for synchronous rendering
|
|
170
|
+
const html = '<!DOCTYPE html>' + String(element)
|
|
171
|
+
|
|
172
|
+
// Inject hydration script before </body> if it exists (case-insensitive for HTML compat)
|
|
173
|
+
const hydrationScript = '<script type="module" src="/@id/__x00__virtual:cloudwerk/client-entry"></script>'
|
|
174
|
+
const bodyCloseRegex = /<\\/body>/i
|
|
175
|
+
const injectedHtml = bodyCloseRegex.test(html)
|
|
176
|
+
? html.replace(bodyCloseRegex, hydrationScript + '</body>')
|
|
177
|
+
: html + hydrationScript
|
|
178
|
+
|
|
179
|
+
return new Response(injectedHtml, {
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function registerRoute(app, pattern, routeModule, middlewareModules) {
|
|
187
|
+
// Apply middleware
|
|
188
|
+
for (const mw of middlewareModules) {
|
|
189
|
+
app.use(pattern, mw)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Apply config middleware if present
|
|
193
|
+
if (routeModule.config) {
|
|
194
|
+
app.use(pattern, async (_c, next) => {
|
|
195
|
+
setRouteConfig(routeModule.config)
|
|
196
|
+
await next()
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Register each HTTP method handler
|
|
201
|
+
for (const method of HTTP_METHODS) {
|
|
202
|
+
const handler = routeModule[method]
|
|
203
|
+
if (handler && typeof handler === 'function') {
|
|
204
|
+
const h = handler.length === 2 ? createHandlerAdapter(handler) : handler
|
|
205
|
+
switch (method) {
|
|
206
|
+
case 'GET': app.get(pattern, h); break
|
|
207
|
+
case 'POST': app.post(pattern, h); break
|
|
208
|
+
case 'PUT': app.put(pattern, h); break
|
|
209
|
+
case 'PATCH': app.patch(pattern, h); break
|
|
210
|
+
case 'DELETE': app.delete(pattern, h); break
|
|
211
|
+
case 'OPTIONS': app.options(pattern, h); break
|
|
212
|
+
case 'HEAD': app.on('HEAD', [pattern], h); break
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// App Initialization
|
|
220
|
+
// ============================================================================
|
|
221
|
+
|
|
222
|
+
// Initialize renderer
|
|
223
|
+
setActiveRenderer('${rendererName}')
|
|
224
|
+
|
|
225
|
+
// Create Hono app
|
|
226
|
+
const app = new Hono({ strict: false })
|
|
227
|
+
|
|
228
|
+
// Add context middleware
|
|
229
|
+
app.use('*', contextMiddleware())
|
|
230
|
+
|
|
231
|
+
// Register all routes
|
|
232
|
+
${pageRegistrations.join("\n")}
|
|
233
|
+
${routeRegistrations.join("\n")}
|
|
234
|
+
|
|
235
|
+
// 404 handler
|
|
236
|
+
app.notFound((c) => {
|
|
237
|
+
return c.json({ error: 'Not Found', path: c.req.path }, 404)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Error handler
|
|
241
|
+
app.onError((err, c) => {
|
|
242
|
+
console.error('Request error:', err.message)
|
|
243
|
+
return c.json({ error: 'Internal Server Error', message: err.message }, 500)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Export
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
export default app
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/virtual-modules/client-entry.ts
|
|
255
|
+
function generateClientEntry(clientComponents, options) {
|
|
256
|
+
const { renderer, hydrationEndpoint } = options;
|
|
257
|
+
if (renderer === "react") {
|
|
258
|
+
return generateReactClientEntry(clientComponents, hydrationEndpoint);
|
|
259
|
+
}
|
|
260
|
+
return generateHonoClientEntry(clientComponents, hydrationEndpoint);
|
|
261
|
+
}
|
|
262
|
+
function generateHonoClientEntry(clientComponents, _hydrationEndpoint) {
|
|
263
|
+
const bundleMap = Object.fromEntries(
|
|
264
|
+
Array.from(clientComponents.values()).map((info) => [info.componentId, `/@fs${info.absolutePath}`])
|
|
265
|
+
);
|
|
266
|
+
return `/**
|
|
267
|
+
* Generated Cloudwerk Client Entry (Hono JSX)
|
|
268
|
+
* This file is auto-generated by @cloudwerk/vite-plugin - do not edit
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
import { render } from 'hono/jsx/dom'
|
|
272
|
+
import { jsx } from 'hono/jsx/jsx-runtime'
|
|
273
|
+
|
|
274
|
+
// Bundle map for component lookups
|
|
275
|
+
const bundles = ${JSON.stringify(bundleMap, null, 2)}
|
|
276
|
+
|
|
277
|
+
// Module cache to avoid re-importing
|
|
278
|
+
const moduleCache = new Map()
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Load a component module, using cache for repeated imports.
|
|
282
|
+
*/
|
|
283
|
+
async function loadComponent(bundlePath) {
|
|
284
|
+
if (moduleCache.has(bundlePath)) {
|
|
285
|
+
return moduleCache.get(bundlePath)
|
|
286
|
+
}
|
|
287
|
+
const module = await import(/* @vite-ignore */ bundlePath)
|
|
288
|
+
moduleCache.set(bundlePath, module)
|
|
289
|
+
return module
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Hydrate all marked elements on the page.
|
|
294
|
+
*/
|
|
295
|
+
async function hydrate() {
|
|
296
|
+
const elements = document.querySelectorAll('[data-hydrate-id]')
|
|
297
|
+
if (elements.length === 0) {
|
|
298
|
+
console.debug('[Cloudwerk] No client components to hydrate')
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.debug('[Cloudwerk] Hydrating', elements.length, 'client components')
|
|
303
|
+
|
|
304
|
+
for (const el of elements) {
|
|
305
|
+
const componentId = el.getAttribute('data-hydrate-id')
|
|
306
|
+
const propsJson = el.getAttribute('data-hydrate-props')
|
|
307
|
+
|
|
308
|
+
if (!componentId) {
|
|
309
|
+
console.warn('[Cloudwerk] Element missing data-hydrate-id')
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const bundlePath = bundles[componentId]
|
|
314
|
+
if (!bundlePath) {
|
|
315
|
+
console.warn('[Cloudwerk] Unknown client component:', componentId)
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const props = propsJson ? JSON.parse(propsJson) : {}
|
|
321
|
+
const module = await loadComponent(bundlePath)
|
|
322
|
+
const Component = module.default
|
|
323
|
+
|
|
324
|
+
if (!Component) {
|
|
325
|
+
console.error('[Cloudwerk] No default export in component:', componentId)
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Hydrate the component using jsx() to create a proper element
|
|
330
|
+
// This allows Hono's reactive system to manage re-renders
|
|
331
|
+
render(jsx(Component, props), el)
|
|
332
|
+
|
|
333
|
+
// Clean up hydration attributes
|
|
334
|
+
el.removeAttribute('data-hydrate-id')
|
|
335
|
+
el.removeAttribute('data-hydrate-props')
|
|
336
|
+
|
|
337
|
+
console.debug('[Cloudwerk] Hydrated:', componentId)
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error('[Cloudwerk] Failed to hydrate component:', componentId, error)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Run hydration when DOM is ready
|
|
345
|
+
if (document.readyState === 'loading') {
|
|
346
|
+
document.addEventListener('DOMContentLoaded', hydrate)
|
|
347
|
+
} else {
|
|
348
|
+
hydrate()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Export for manual hydration if needed
|
|
352
|
+
export { hydrate }
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
function generateReactClientEntry(clientComponents, _hydrationEndpoint) {
|
|
356
|
+
const bundleMap = Object.fromEntries(
|
|
357
|
+
Array.from(clientComponents.values()).map((info) => [info.componentId, `/@fs${info.absolutePath}`])
|
|
358
|
+
);
|
|
359
|
+
return `/**
|
|
360
|
+
* Generated Cloudwerk Client Entry (React)
|
|
361
|
+
* This file is auto-generated by @cloudwerk/vite-plugin - do not edit
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
import { hydrateRoot, createRoot } from 'react-dom/client'
|
|
365
|
+
import { createElement } from 'react'
|
|
366
|
+
|
|
367
|
+
// Bundle map for component lookups
|
|
368
|
+
const bundles = ${JSON.stringify(bundleMap, null, 2)}
|
|
369
|
+
|
|
370
|
+
// Module cache to avoid re-importing
|
|
371
|
+
const moduleCache = new Map()
|
|
372
|
+
|
|
373
|
+
// Root cache for React 18 concurrent mode
|
|
374
|
+
const rootCache = new Map()
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Load a component module, using cache for repeated imports.
|
|
378
|
+
*/
|
|
379
|
+
async function loadComponent(bundlePath) {
|
|
380
|
+
if (moduleCache.has(bundlePath)) {
|
|
381
|
+
return moduleCache.get(bundlePath)
|
|
382
|
+
}
|
|
383
|
+
const module = await import(/* @vite-ignore */ bundlePath)
|
|
384
|
+
moduleCache.set(bundlePath, module)
|
|
385
|
+
return module
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Hydrate all marked elements on the page.
|
|
390
|
+
*/
|
|
391
|
+
async function hydrate() {
|
|
392
|
+
const elements = document.querySelectorAll('[data-hydrate-id]')
|
|
393
|
+
if (elements.length === 0) {
|
|
394
|
+
console.debug('[Cloudwerk] No client components to hydrate')
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
console.debug('[Cloudwerk] Hydrating', elements.length, 'client components')
|
|
399
|
+
|
|
400
|
+
for (const el of elements) {
|
|
401
|
+
const componentId = el.getAttribute('data-hydrate-id')
|
|
402
|
+
const propsJson = el.getAttribute('data-hydrate-props')
|
|
403
|
+
|
|
404
|
+
if (!componentId) {
|
|
405
|
+
console.warn('[Cloudwerk] Element missing data-hydrate-id')
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const bundlePath = bundles[componentId]
|
|
410
|
+
if (!bundlePath) {
|
|
411
|
+
console.warn('[Cloudwerk] Unknown client component:', componentId)
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const props = propsJson ? JSON.parse(propsJson) : {}
|
|
417
|
+
const module = await loadComponent(bundlePath)
|
|
418
|
+
const Component = module.default
|
|
419
|
+
|
|
420
|
+
if (!Component) {
|
|
421
|
+
console.error('[Cloudwerk] No default export in component:', componentId)
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Hydrate the component using React 18 hydrateRoot
|
|
426
|
+
const root = hydrateRoot(el, createElement(Component, props))
|
|
427
|
+
rootCache.set(el, root)
|
|
428
|
+
|
|
429
|
+
// Clean up hydration attributes
|
|
430
|
+
el.removeAttribute('data-hydrate-id')
|
|
431
|
+
el.removeAttribute('data-hydrate-props')
|
|
432
|
+
|
|
433
|
+
console.debug('[Cloudwerk] Hydrated:', componentId)
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.error('[Cloudwerk] Failed to hydrate component:', componentId, error)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Run hydration when DOM is ready
|
|
441
|
+
if (document.readyState === 'loading') {
|
|
442
|
+
document.addEventListener('DOMContentLoaded', hydrate)
|
|
443
|
+
} else {
|
|
444
|
+
hydrate()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Export for manual hydration if needed
|
|
448
|
+
export { hydrate }
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/transform-client-component.ts
|
|
453
|
+
import { parseSync } from "@swc/core";
|
|
454
|
+
function findDefaultExport(ast) {
|
|
455
|
+
for (let i = 0; i < ast.body.length; i++) {
|
|
456
|
+
const node = ast.body[i];
|
|
457
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
458
|
+
const decl = node;
|
|
459
|
+
if (decl.decl.type === "FunctionExpression") {
|
|
460
|
+
return {
|
|
461
|
+
type: "function",
|
|
462
|
+
name: decl.decl.identifier?.value ?? null,
|
|
463
|
+
index: i,
|
|
464
|
+
isAsync: decl.decl.async
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (decl.decl.type === "ClassExpression") {
|
|
468
|
+
return {
|
|
469
|
+
type: "class",
|
|
470
|
+
name: decl.decl.identifier?.value ?? null,
|
|
471
|
+
index: i
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (node.type === "ExportDefaultExpression") {
|
|
476
|
+
const expr = node;
|
|
477
|
+
if (expr.expression.type === "Identifier") {
|
|
478
|
+
return {
|
|
479
|
+
type: "identifier",
|
|
480
|
+
name: expr.expression.value,
|
|
481
|
+
index: i
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (expr.expression.type === "ArrowFunctionExpression") {
|
|
485
|
+
return {
|
|
486
|
+
type: "arrow",
|
|
487
|
+
name: null,
|
|
488
|
+
index: i,
|
|
489
|
+
isAsync: expr.expression.async
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (expr.expression.type === "FunctionExpression") {
|
|
493
|
+
return {
|
|
494
|
+
type: "function",
|
|
495
|
+
name: expr.expression.identifier?.value ?? null,
|
|
496
|
+
index: i,
|
|
497
|
+
isAsync: expr.expression.async
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (expr.expression.type === "ClassExpression") {
|
|
501
|
+
return {
|
|
502
|
+
type: "class",
|
|
503
|
+
name: expr.expression.identifier?.value ?? null,
|
|
504
|
+
index: i
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
509
|
+
const specifiers = node.specifiers;
|
|
510
|
+
for (const spec of specifiers) {
|
|
511
|
+
if (spec.type === "ExportSpecifier") {
|
|
512
|
+
const exported = spec.exported;
|
|
513
|
+
if (exported && exported.type === "Identifier" && exported.value === "default") {
|
|
514
|
+
if (spec.orig.type === "Identifier") {
|
|
515
|
+
return {
|
|
516
|
+
type: "named-export",
|
|
517
|
+
name: spec.orig.value,
|
|
518
|
+
index: i
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return { type: null, name: null, index: -1 };
|
|
527
|
+
}
|
|
528
|
+
function transformClientComponent(code, options) {
|
|
529
|
+
const { componentId, bundlePath } = options;
|
|
530
|
+
try {
|
|
531
|
+
const ast = parseSync(code, {
|
|
532
|
+
syntax: "typescript",
|
|
533
|
+
tsx: true,
|
|
534
|
+
comments: true
|
|
535
|
+
});
|
|
536
|
+
const exportInfo = findDefaultExport(ast);
|
|
537
|
+
if (exportInfo.type === null) {
|
|
538
|
+
return {
|
|
539
|
+
code,
|
|
540
|
+
success: false,
|
|
541
|
+
error: `Could not find default export in client component: ${componentId}`
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
let transformed = code.replace(/['"]use client['"]\s*;?\s*\n?/g, "");
|
|
545
|
+
const wrapperImport = `import { createClientComponentWrapper as __createWrapper } from '@cloudwerk/ui/client'
|
|
546
|
+
`;
|
|
547
|
+
const metaObj = JSON.stringify({ componentId, bundlePath });
|
|
548
|
+
switch (exportInfo.type) {
|
|
549
|
+
case "function": {
|
|
550
|
+
if (exportInfo.name) {
|
|
551
|
+
const asyncPrefix = exportInfo.isAsync ? "async " : "";
|
|
552
|
+
transformed = transformed.replace(
|
|
553
|
+
new RegExp(`export\\s+default\\s+${asyncPrefix}function\\s+${exportInfo.name}`),
|
|
554
|
+
`${asyncPrefix}function ${exportInfo.name}`
|
|
555
|
+
);
|
|
556
|
+
transformed = wrapperImport + transformed;
|
|
557
|
+
transformed += `
|
|
558
|
+
const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
|
|
559
|
+
export default __WrappedComponent
|
|
560
|
+
`;
|
|
561
|
+
} else {
|
|
562
|
+
const asyncPrefix = exportInfo.isAsync ? "async " : "";
|
|
563
|
+
transformed = transformed.replace(
|
|
564
|
+
new RegExp(`export\\s+default\\s+${asyncPrefix}function\\s*\\(`),
|
|
565
|
+
`const __OriginalComponent = ${asyncPrefix}function(`
|
|
566
|
+
);
|
|
567
|
+
transformed = wrapperImport + transformed;
|
|
568
|
+
transformed += `
|
|
569
|
+
const __WrappedComponent = __createWrapper(__OriginalComponent, ${metaObj})
|
|
570
|
+
export default __WrappedComponent
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case "arrow": {
|
|
576
|
+
transformed = transformed.replace(
|
|
577
|
+
/export\s+default/,
|
|
578
|
+
"const __OriginalComponent ="
|
|
579
|
+
);
|
|
580
|
+
transformed = wrapperImport + transformed;
|
|
581
|
+
transformed += `
|
|
582
|
+
const __WrappedComponent = __createWrapper(__OriginalComponent, ${metaObj})
|
|
583
|
+
export default __WrappedComponent
|
|
584
|
+
`;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case "class": {
|
|
588
|
+
if (exportInfo.name) {
|
|
589
|
+
transformed = transformed.replace(
|
|
590
|
+
new RegExp(`export\\s+default\\s+class\\s+${exportInfo.name}`),
|
|
591
|
+
`class ${exportInfo.name}`
|
|
592
|
+
);
|
|
593
|
+
transformed = wrapperImport + transformed;
|
|
594
|
+
transformed += `
|
|
595
|
+
const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
|
|
596
|
+
export default __WrappedComponent
|
|
597
|
+
`;
|
|
598
|
+
} else {
|
|
599
|
+
transformed = transformed.replace(
|
|
600
|
+
/export\s+default\s+class\s*\{/,
|
|
601
|
+
"const __OriginalComponent = class {"
|
|
602
|
+
);
|
|
603
|
+
transformed = wrapperImport + transformed;
|
|
604
|
+
transformed += `
|
|
605
|
+
const __WrappedComponent = __createWrapper(__OriginalComponent, ${metaObj})
|
|
606
|
+
export default __WrappedComponent
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case "identifier": {
|
|
612
|
+
transformed = transformed.replace(
|
|
613
|
+
/export\s+default\s+\w+\s*;?\s*$/m,
|
|
614
|
+
""
|
|
615
|
+
);
|
|
616
|
+
transformed = wrapperImport + transformed;
|
|
617
|
+
transformed += `
|
|
618
|
+
const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
|
|
619
|
+
export default __WrappedComponent
|
|
620
|
+
`;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case "named-export": {
|
|
624
|
+
transformed = transformed.replace(
|
|
625
|
+
/export\s*\{\s*\w+\s+as\s+default\s*\}\s*;?/,
|
|
626
|
+
""
|
|
627
|
+
);
|
|
628
|
+
transformed = wrapperImport + transformed;
|
|
629
|
+
transformed += `
|
|
630
|
+
const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
|
|
631
|
+
export default __WrappedComponent
|
|
632
|
+
`;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
code: transformed,
|
|
638
|
+
success: true
|
|
639
|
+
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
642
|
+
return {
|
|
643
|
+
code,
|
|
644
|
+
success: false,
|
|
645
|
+
error: `Failed to parse client component ${componentId}: ${message}`
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/plugin.ts
|
|
651
|
+
function cloudwerkPlugin(options = {}) {
|
|
652
|
+
let state = null;
|
|
653
|
+
let server = null;
|
|
654
|
+
async function buildManifest(root) {
|
|
655
|
+
if (!state) {
|
|
656
|
+
throw new Error("Plugin state not initialized");
|
|
657
|
+
}
|
|
658
|
+
const routesPath = resolveRoutesPath(
|
|
659
|
+
state.options.routesDir,
|
|
660
|
+
state.options.appDir,
|
|
661
|
+
root
|
|
662
|
+
);
|
|
663
|
+
state.scanResult = await scanRoutes(routesPath, {
|
|
664
|
+
extensions: state.options.config.extensions
|
|
665
|
+
});
|
|
666
|
+
state.manifest = buildRouteManifest(
|
|
667
|
+
state.scanResult,
|
|
668
|
+
routesPath,
|
|
669
|
+
resolveLayouts,
|
|
670
|
+
resolveMiddleware
|
|
671
|
+
);
|
|
672
|
+
state.serverEntryCache = null;
|
|
673
|
+
state.clientEntryCache = null;
|
|
674
|
+
if (state.options.verbose) {
|
|
675
|
+
console.log(`[cloudwerk] Found ${state.manifest.routes.length} routes`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function isRouteFile(filePath) {
|
|
679
|
+
if (!state) return false;
|
|
680
|
+
const appDir = path.resolve(state.options.root, state.options.appDir);
|
|
681
|
+
if (!filePath.startsWith(appDir)) return false;
|
|
682
|
+
const basename2 = path.basename(filePath);
|
|
683
|
+
const nameWithoutExt = basename2.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
684
|
+
return ROUTE_FILE_NAMES.includes(nameWithoutExt);
|
|
685
|
+
}
|
|
686
|
+
function invalidateVirtualModules() {
|
|
687
|
+
if (!server) return;
|
|
688
|
+
const idsToInvalidate = [
|
|
689
|
+
RESOLVED_VIRTUAL_IDS.SERVER_ENTRY,
|
|
690
|
+
RESOLVED_VIRTUAL_IDS.CLIENT_ENTRY,
|
|
691
|
+
RESOLVED_VIRTUAL_IDS.MANIFEST
|
|
692
|
+
];
|
|
693
|
+
for (const id of idsToInvalidate) {
|
|
694
|
+
const mod = server.moduleGraph.getModuleById(id);
|
|
695
|
+
if (mod) {
|
|
696
|
+
server.moduleGraph.invalidateModule(mod);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
server.ws.send({ type: "full-reload", path: "*" });
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
name: "cloudwerk",
|
|
703
|
+
/**
|
|
704
|
+
* Resolve configuration and build initial manifest.
|
|
705
|
+
*/
|
|
706
|
+
async configResolved(config) {
|
|
707
|
+
const root = config.root;
|
|
708
|
+
let detectedServerEntry = options.serverEntry ? path.resolve(root, options.serverEntry) : null;
|
|
709
|
+
if (!detectedServerEntry) {
|
|
710
|
+
const conventionalPaths = [
|
|
711
|
+
path.resolve(root, "app/server.ts"),
|
|
712
|
+
path.resolve(root, "app/server.tsx"),
|
|
713
|
+
path.resolve(root, "src/server.ts"),
|
|
714
|
+
path.resolve(root, "src/server.tsx")
|
|
715
|
+
];
|
|
716
|
+
for (const p of conventionalPaths) {
|
|
717
|
+
if (fs.existsSync(p)) {
|
|
718
|
+
detectedServerEntry = p;
|
|
719
|
+
if (options.verbose) {
|
|
720
|
+
console.log(`[cloudwerk] Detected custom server entry: ${p}`);
|
|
721
|
+
}
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const cloudwerkConfig = await loadConfig(root);
|
|
727
|
+
const resolvedOptions = {
|
|
728
|
+
appDir: options.appDir ?? cloudwerkConfig.appDir,
|
|
729
|
+
routesDir: options.routesDir ?? cloudwerkConfig.routesDir ?? "routes",
|
|
730
|
+
config: { ...cloudwerkConfig, ...options.config },
|
|
731
|
+
serverEntry: detectedServerEntry,
|
|
732
|
+
clientEntry: options.clientEntry ?? null,
|
|
733
|
+
verbose: options.verbose ?? false,
|
|
734
|
+
hydrationEndpoint: options.hydrationEndpoint ?? "/__cloudwerk",
|
|
735
|
+
renderer: options.renderer ?? cloudwerkConfig.ui?.renderer ?? "hono-jsx",
|
|
736
|
+
root
|
|
737
|
+
};
|
|
738
|
+
state = {
|
|
739
|
+
options: resolvedOptions,
|
|
740
|
+
manifest: {
|
|
741
|
+
routes: [],
|
|
742
|
+
layouts: /* @__PURE__ */ new Map(),
|
|
743
|
+
middleware: /* @__PURE__ */ new Map(),
|
|
744
|
+
errors: [],
|
|
745
|
+
warnings: [],
|
|
746
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
747
|
+
rootDir: ""
|
|
748
|
+
},
|
|
749
|
+
scanResult: {
|
|
750
|
+
routes: [],
|
|
751
|
+
layouts: [],
|
|
752
|
+
middleware: [],
|
|
753
|
+
loading: [],
|
|
754
|
+
errors: [],
|
|
755
|
+
notFound: []
|
|
756
|
+
},
|
|
757
|
+
clientComponents: /* @__PURE__ */ new Map(),
|
|
758
|
+
serverEntryCache: null,
|
|
759
|
+
clientEntryCache: null
|
|
760
|
+
};
|
|
761
|
+
await buildManifest(root);
|
|
762
|
+
},
|
|
763
|
+
/**
|
|
764
|
+
* Configure the dev server with file watching.
|
|
765
|
+
*/
|
|
766
|
+
configureServer(devServer) {
|
|
767
|
+
server = devServer;
|
|
768
|
+
if (!state) return;
|
|
769
|
+
const appDir = path.resolve(state.options.root, state.options.appDir);
|
|
770
|
+
devServer.watcher.on("add", async (filePath) => {
|
|
771
|
+
if (isRouteFile(filePath)) {
|
|
772
|
+
if (state?.options.verbose) {
|
|
773
|
+
console.log(`[cloudwerk] Route added: ${path.relative(appDir, filePath)}`);
|
|
774
|
+
}
|
|
775
|
+
await buildManifest(state.options.root);
|
|
776
|
+
invalidateVirtualModules();
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
devServer.watcher.on("unlink", async (filePath) => {
|
|
780
|
+
if (isRouteFile(filePath)) {
|
|
781
|
+
if (state?.options.verbose) {
|
|
782
|
+
console.log(`[cloudwerk] Route removed: ${path.relative(appDir, filePath)}`);
|
|
783
|
+
}
|
|
784
|
+
await buildManifest(state.options.root);
|
|
785
|
+
invalidateVirtualModules();
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
devServer.watcher.on("change", async (filePath) => {
|
|
789
|
+
if (isRouteFile(filePath)) {
|
|
790
|
+
if (state?.options.verbose) {
|
|
791
|
+
console.log(`[cloudwerk] Route changed: ${path.relative(appDir, filePath)}`);
|
|
792
|
+
}
|
|
793
|
+
await buildManifest(state.options.root);
|
|
794
|
+
invalidateVirtualModules();
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
},
|
|
798
|
+
/**
|
|
799
|
+
* Resolve virtual module IDs.
|
|
800
|
+
*/
|
|
801
|
+
resolveId(id) {
|
|
802
|
+
if (id === VIRTUAL_MODULE_IDS.SERVER_ENTRY) {
|
|
803
|
+
return RESOLVED_VIRTUAL_IDS.SERVER_ENTRY;
|
|
804
|
+
}
|
|
805
|
+
if (id === VIRTUAL_MODULE_IDS.CLIENT_ENTRY) {
|
|
806
|
+
return RESOLVED_VIRTUAL_IDS.CLIENT_ENTRY;
|
|
807
|
+
}
|
|
808
|
+
if (id === VIRTUAL_MODULE_IDS.MANIFEST) {
|
|
809
|
+
return RESOLVED_VIRTUAL_IDS.MANIFEST;
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
},
|
|
813
|
+
/**
|
|
814
|
+
* Load virtual module content.
|
|
815
|
+
*/
|
|
816
|
+
load(id) {
|
|
817
|
+
if (!state) return null;
|
|
818
|
+
if (id === RESOLVED_VIRTUAL_IDS.SERVER_ENTRY) {
|
|
819
|
+
if (state.options.serverEntry) {
|
|
820
|
+
return `export { default } from '${state.options.serverEntry}'`;
|
|
821
|
+
}
|
|
822
|
+
if (!state.serverEntryCache) {
|
|
823
|
+
state.serverEntryCache = generateServerEntry(
|
|
824
|
+
state.manifest,
|
|
825
|
+
state.scanResult,
|
|
826
|
+
state.options
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
return state.serverEntryCache;
|
|
830
|
+
}
|
|
831
|
+
if (id === RESOLVED_VIRTUAL_IDS.CLIENT_ENTRY) {
|
|
832
|
+
if (state.options.clientEntry) {
|
|
833
|
+
return `export * from '${state.options.clientEntry}'`;
|
|
834
|
+
}
|
|
835
|
+
if (!state.clientEntryCache) {
|
|
836
|
+
state.clientEntryCache = generateClientEntry(
|
|
837
|
+
state.clientComponents,
|
|
838
|
+
state.options
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
return state.clientEntryCache;
|
|
842
|
+
}
|
|
843
|
+
if (id === RESOLVED_VIRTUAL_IDS.MANIFEST) {
|
|
844
|
+
return `export default ${JSON.stringify(
|
|
845
|
+
{
|
|
846
|
+
routes: state.manifest.routes.map((r) => ({
|
|
847
|
+
urlPattern: r.urlPattern,
|
|
848
|
+
filePath: r.filePath,
|
|
849
|
+
fileType: r.fileType
|
|
850
|
+
})),
|
|
851
|
+
generatedAt: state.manifest.generatedAt.toISOString()
|
|
852
|
+
},
|
|
853
|
+
null,
|
|
854
|
+
2
|
|
855
|
+
)}`;
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
},
|
|
859
|
+
/**
|
|
860
|
+
* Transform hook to detect and wrap client components.
|
|
861
|
+
*/
|
|
862
|
+
transform(code, id) {
|
|
863
|
+
if (!state) return null;
|
|
864
|
+
if (id.includes("node_modules")) return null;
|
|
865
|
+
if (!id.endsWith(".tsx") && !id.endsWith(".ts")) return null;
|
|
866
|
+
if (hasUseClientDirective(code)) {
|
|
867
|
+
const componentId = generateComponentId(id, state.options.root);
|
|
868
|
+
const bundlePath = `${state.options.hydrationEndpoint}/${componentId}.js`;
|
|
869
|
+
const info = {
|
|
870
|
+
componentId,
|
|
871
|
+
bundlePath,
|
|
872
|
+
absolutePath: id
|
|
873
|
+
};
|
|
874
|
+
state.clientComponents.set(id, info);
|
|
875
|
+
state.clientEntryCache = null;
|
|
876
|
+
if (state.options.verbose) {
|
|
877
|
+
console.log(`[cloudwerk] Detected client component: ${componentId}`);
|
|
878
|
+
}
|
|
879
|
+
const result = transformClientComponent(code, {
|
|
880
|
+
componentId,
|
|
881
|
+
bundlePath: id
|
|
882
|
+
// Use file path for Vite to resolve in dev mode
|
|
883
|
+
});
|
|
884
|
+
if (!result.success && state.options.verbose) {
|
|
885
|
+
console.warn(`[cloudwerk] ${result.error}`);
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
code: result.code,
|
|
889
|
+
map: null
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
export {
|
|
897
|
+
RESOLVED_VIRTUAL_IDS,
|
|
898
|
+
VIRTUAL_MODULE_IDS,
|
|
899
|
+
cloudwerkPlugin,
|
|
900
|
+
cloudwerkPlugin as default,
|
|
901
|
+
generateClientEntry,
|
|
902
|
+
generateServerEntry
|
|
903
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudwerk/vite-plugin",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Vite plugin for Cloudwerk file-based routing with virtual entry generation",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/squirrelsoft-dev/cloudwerk.git",
|
|
8
|
+
"directory": "packages/vite-plugin"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@swc/core": "^1.3.100",
|
|
22
|
+
"@cloudwerk/core": "^0.7.0",
|
|
23
|
+
"@cloudwerk/ui": "^0.7.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"vite": "^5.0.0 || ^6.0.0",
|
|
27
|
+
"@hono/vite-dev-server": ">=0.18.0",
|
|
28
|
+
"hono": "^4.0.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@hono/vite-dev-server": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.0.0",
|
|
39
|
+
"vite": "^6.0.0",
|
|
40
|
+
"vitest": "^1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
44
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
45
|
+
"test": "vitest --run --passWithNoTests",
|
|
46
|
+
"test:watch": "vitest"
|
|
47
|
+
}
|
|
48
|
+
}
|