@cloudwerk/vite-plugin 0.2.0 → 0.4.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 +9 -0
- package/dist/index.js +189 -7
- 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,8 +78,12 @@ 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;
|
|
85
|
+
/** Whether building for production (affects asset paths) */
|
|
86
|
+
isProduction?: boolean;
|
|
78
87
|
}
|
|
79
88
|
/**
|
|
80
89
|
* Information about a detected client component.
|
package/dist/index.js
CHANGED
|
@@ -124,6 +124,7 @@ function generateServerEntry(manifest, scanResult, options) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
const rendererName = options.renderer;
|
|
127
|
+
const clientEntryPath = options.isProduction ? `${options.hydrationEndpoint}/client.js` : "/@id/__x00__virtual:cloudwerk/client-entry";
|
|
127
128
|
return `/**
|
|
128
129
|
* Generated Cloudwerk Server Entry
|
|
129
130
|
* This file is auto-generated by @cloudwerk/vite-plugin - do not edit
|
|
@@ -381,7 +382,7 @@ function renderWithHydration(element, status = 200) {
|
|
|
381
382
|
const html = '<!DOCTYPE html>' + String(element)
|
|
382
383
|
|
|
383
384
|
// Inject hydration script before </body> if it exists (case-insensitive for HTML compat)
|
|
384
|
-
const hydrationScript = '<script type="module" src="
|
|
385
|
+
const hydrationScript = '<script type="module" src="${clientEntryPath}"></script>'
|
|
385
386
|
const bodyCloseRegex = /<\\/body>/i
|
|
386
387
|
const injectedHtml = bodyCloseRegex.test(html)
|
|
387
388
|
? html.replace(bodyCloseRegex, hydrationScript + '</body>')
|
|
@@ -439,7 +440,27 @@ const app = new Hono({ strict: false })
|
|
|
439
440
|
|
|
440
441
|
// Add context middleware
|
|
441
442
|
app.use('*', contextMiddleware())
|
|
443
|
+
${options.isProduction ? `
|
|
444
|
+
// Serve static assets using Workers Static Assets binding (production only)
|
|
445
|
+
app.use('*', async (c, next) => {
|
|
446
|
+
// Check if ASSETS binding is available
|
|
447
|
+
if (!c.env?.ASSETS) {
|
|
448
|
+
await next()
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Try to serve the request as a static asset
|
|
453
|
+
const response = await c.env.ASSETS.fetch(c.req.raw)
|
|
442
454
|
|
|
455
|
+
// If asset found (not 404), return it
|
|
456
|
+
if (response.status !== 404) {
|
|
457
|
+
return response
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Asset not found, continue to routes
|
|
461
|
+
await next()
|
|
462
|
+
})
|
|
463
|
+
` : ""}
|
|
443
464
|
// Register all routes
|
|
444
465
|
${pageRegistrations.join("\n")}
|
|
445
466
|
${routeRegistrations.join("\n")}
|
|
@@ -516,13 +537,105 @@ export default app
|
|
|
516
537
|
|
|
517
538
|
// src/virtual-modules/client-entry.ts
|
|
518
539
|
function generateClientEntry(clientComponents, options) {
|
|
519
|
-
const { renderer, hydrationEndpoint } = options;
|
|
540
|
+
const { renderer, hydrationEndpoint, isProduction } = options;
|
|
520
541
|
if (renderer === "react") {
|
|
521
|
-
return generateReactClientEntry(clientComponents, hydrationEndpoint);
|
|
542
|
+
return generateReactClientEntry(clientComponents, hydrationEndpoint, isProduction);
|
|
522
543
|
}
|
|
523
|
-
return generateHonoClientEntry(clientComponents, hydrationEndpoint);
|
|
544
|
+
return generateHonoClientEntry(clientComponents, hydrationEndpoint, isProduction);
|
|
524
545
|
}
|
|
525
|
-
function
|
|
546
|
+
function generateStaticImportsAndMap(clientComponents) {
|
|
547
|
+
const components = Array.from(clientComponents.values());
|
|
548
|
+
const imports = components.map(
|
|
549
|
+
(info, index) => `import Component${index} from '${info.absolutePath}'`
|
|
550
|
+
).join("\n");
|
|
551
|
+
const componentMapEntries = components.map(
|
|
552
|
+
(info, index) => ` '${info.componentId}': Component${index}`
|
|
553
|
+
).join(",\n");
|
|
554
|
+
return { imports, componentMapEntries };
|
|
555
|
+
}
|
|
556
|
+
function generateProductionClientEntry(clientComponents, config) {
|
|
557
|
+
const { imports, componentMapEntries } = generateStaticImportsAndMap(clientComponents);
|
|
558
|
+
return `/**
|
|
559
|
+
* ${config.header}
|
|
560
|
+
* This file is auto-generated by @cloudwerk/vite-plugin - do not edit
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
${config.rendererImports}
|
|
564
|
+
|
|
565
|
+
// Static component imports
|
|
566
|
+
${imports}
|
|
567
|
+
|
|
568
|
+
// Component map for hydration lookups
|
|
569
|
+
const components = {
|
|
570
|
+
${componentMapEntries}
|
|
571
|
+
}
|
|
572
|
+
${config.additionalDeclarations}
|
|
573
|
+
/**
|
|
574
|
+
* Hydrate all marked elements on the page.
|
|
575
|
+
*/
|
|
576
|
+
async function hydrate() {
|
|
577
|
+
const elements = document.querySelectorAll('[data-hydrate-id]')
|
|
578
|
+
if (elements.length === 0) {
|
|
579
|
+
console.debug('[Cloudwerk] No client components to hydrate')
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
console.debug('[Cloudwerk] Hydrating', elements.length, 'client components')
|
|
584
|
+
|
|
585
|
+
for (const el of elements) {
|
|
586
|
+
const componentId = el.getAttribute('data-hydrate-id')
|
|
587
|
+
const propsJson = el.getAttribute('data-hydrate-props')
|
|
588
|
+
|
|
589
|
+
if (!componentId) {
|
|
590
|
+
console.warn('[Cloudwerk] Element missing data-hydrate-id')
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const Component = components[componentId]
|
|
595
|
+
if (!Component) {
|
|
596
|
+
console.warn('[Cloudwerk] Unknown client component:', componentId)
|
|
597
|
+
continue
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const props = propsJson ? JSON.parse(propsJson) : {}
|
|
602
|
+
|
|
603
|
+
${config.hydrateCall}
|
|
604
|
+
|
|
605
|
+
// Clean up hydration attributes
|
|
606
|
+
el.removeAttribute('data-hydrate-id')
|
|
607
|
+
el.removeAttribute('data-hydrate-props')
|
|
608
|
+
|
|
609
|
+
console.debug('[Cloudwerk] Hydrated:', componentId)
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.error('[Cloudwerk] Failed to hydrate component:', componentId, error)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Run hydration when DOM is ready
|
|
617
|
+
if (document.readyState === 'loading') {
|
|
618
|
+
document.addEventListener('DOMContentLoaded', hydrate)
|
|
619
|
+
} else {
|
|
620
|
+
hydrate()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Export for manual hydration if needed
|
|
624
|
+
export { hydrate }
|
|
625
|
+
`;
|
|
626
|
+
}
|
|
627
|
+
function generateHonoClientEntry(clientComponents, _hydrationEndpoint, isProduction = false) {
|
|
628
|
+
if (isProduction) {
|
|
629
|
+
return generateProductionClientEntry(clientComponents, {
|
|
630
|
+
header: "Generated Cloudwerk Client Entry (Hono JSX - Production)",
|
|
631
|
+
rendererImports: `import { render } from 'hono/jsx/dom'
|
|
632
|
+
import { jsx } from 'hono/jsx/jsx-runtime'`,
|
|
633
|
+
additionalDeclarations: "",
|
|
634
|
+
hydrateCall: `// Hydrate the component using jsx() to create a proper element
|
|
635
|
+
// This allows Hono's reactive system to manage re-renders
|
|
636
|
+
render(jsx(Component, props), el)`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
526
639
|
const bundleMap = Object.fromEntries(
|
|
527
640
|
Array.from(clientComponents.values()).map((info) => [info.componentId, `/@fs${info.absolutePath}`])
|
|
528
641
|
);
|
|
@@ -615,7 +728,21 @@ if (document.readyState === 'loading') {
|
|
|
615
728
|
export { hydrate }
|
|
616
729
|
`;
|
|
617
730
|
}
|
|
618
|
-
function generateReactClientEntry(clientComponents, _hydrationEndpoint) {
|
|
731
|
+
function generateReactClientEntry(clientComponents, _hydrationEndpoint, isProduction = false) {
|
|
732
|
+
if (isProduction) {
|
|
733
|
+
return generateProductionClientEntry(clientComponents, {
|
|
734
|
+
header: "Generated Cloudwerk Client Entry (React - Production)",
|
|
735
|
+
rendererImports: `import { hydrateRoot } from 'react-dom/client'
|
|
736
|
+
import { createElement } from 'react'`,
|
|
737
|
+
additionalDeclarations: `
|
|
738
|
+
// Root cache for React 18 concurrent mode
|
|
739
|
+
const rootCache = new Map()
|
|
740
|
+
`,
|
|
741
|
+
hydrateCall: `// Hydrate the component using React 18 hydrateRoot
|
|
742
|
+
const root = hydrateRoot(el, createElement(Component, props))
|
|
743
|
+
rootCache.set(el, root)`
|
|
744
|
+
});
|
|
745
|
+
}
|
|
619
746
|
const bundleMap = Object.fromEntries(
|
|
620
747
|
Array.from(clientComponents.values()).map((info) => [info.componentId, `/@fs${info.absolutePath}`])
|
|
621
748
|
);
|
|
@@ -911,6 +1038,43 @@ export default __WrappedComponent
|
|
|
911
1038
|
}
|
|
912
1039
|
|
|
913
1040
|
// src/plugin.ts
|
|
1041
|
+
async function scanClientComponents(root, state) {
|
|
1042
|
+
const appDir = path2.resolve(root, state.options.appDir);
|
|
1043
|
+
try {
|
|
1044
|
+
await fs.promises.access(appDir);
|
|
1045
|
+
} catch {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
async function scanDir(dir) {
|
|
1049
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1050
|
+
await Promise.all(
|
|
1051
|
+
entries.map(async (entry) => {
|
|
1052
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1053
|
+
if (entry.isDirectory()) {
|
|
1054
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
1055
|
+
await scanDir(fullPath);
|
|
1056
|
+
}
|
|
1057
|
+
} else if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
1058
|
+
const content = await fs.promises.readFile(fullPath, "utf-8");
|
|
1059
|
+
if (hasUseClientDirective(content)) {
|
|
1060
|
+
const componentId = generateComponentId(fullPath, root);
|
|
1061
|
+
const bundlePath = `${state.options.hydrationEndpoint}/${componentId}.js`;
|
|
1062
|
+
const info = {
|
|
1063
|
+
componentId,
|
|
1064
|
+
bundlePath,
|
|
1065
|
+
absolutePath: fullPath
|
|
1066
|
+
};
|
|
1067
|
+
state.clientComponents.set(fullPath, info);
|
|
1068
|
+
if (state.options.verbose) {
|
|
1069
|
+
console.log(`[cloudwerk] Pre-scanned client component: ${componentId}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
})
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
await scanDir(appDir);
|
|
1077
|
+
}
|
|
914
1078
|
function cloudwerkPlugin(options = {}) {
|
|
915
1079
|
let state = null;
|
|
916
1080
|
let server = null;
|
|
@@ -965,6 +1129,20 @@ function cloudwerkPlugin(options = {}) {
|
|
|
965
1129
|
}
|
|
966
1130
|
return {
|
|
967
1131
|
name: "cloudwerk",
|
|
1132
|
+
/**
|
|
1133
|
+
* Pass publicDir configuration to Vite.
|
|
1134
|
+
* This enables Vite's built-in static file serving for the public directory.
|
|
1135
|
+
*/
|
|
1136
|
+
async config(userConfig) {
|
|
1137
|
+
if (userConfig.publicDir !== void 0) {
|
|
1138
|
+
return {};
|
|
1139
|
+
}
|
|
1140
|
+
const root = userConfig.root ?? process.cwd();
|
|
1141
|
+
const cloudwerkConfig = await loadConfig(root);
|
|
1142
|
+
return {
|
|
1143
|
+
publicDir: options.publicDir ?? cloudwerkConfig.publicDir ?? "public"
|
|
1144
|
+
};
|
|
1145
|
+
},
|
|
968
1146
|
/**
|
|
969
1147
|
* Resolve configuration and build initial manifest.
|
|
970
1148
|
*/
|
|
@@ -989,6 +1167,7 @@ function cloudwerkPlugin(options = {}) {
|
|
|
989
1167
|
}
|
|
990
1168
|
}
|
|
991
1169
|
const cloudwerkConfig = await loadConfig(root);
|
|
1170
|
+
const isProduction = config.command === "build" || config.mode === "production";
|
|
992
1171
|
const resolvedOptions = {
|
|
993
1172
|
appDir: options.appDir ?? cloudwerkConfig.appDir,
|
|
994
1173
|
routesDir: options.routesDir ?? cloudwerkConfig.routesDir ?? "routes",
|
|
@@ -998,7 +1177,9 @@ function cloudwerkPlugin(options = {}) {
|
|
|
998
1177
|
verbose: options.verbose ?? false,
|
|
999
1178
|
hydrationEndpoint: options.hydrationEndpoint ?? "/__cloudwerk",
|
|
1000
1179
|
renderer: options.renderer ?? cloudwerkConfig.ui?.renderer ?? "hono-jsx",
|
|
1001
|
-
|
|
1180
|
+
publicDir: options.publicDir ?? cloudwerkConfig.publicDir ?? "public",
|
|
1181
|
+
root,
|
|
1182
|
+
isProduction
|
|
1002
1183
|
};
|
|
1003
1184
|
state = {
|
|
1004
1185
|
options: resolvedOptions,
|
|
@@ -1026,6 +1207,7 @@ function cloudwerkPlugin(options = {}) {
|
|
|
1026
1207
|
clientEntryCache: null
|
|
1027
1208
|
};
|
|
1028
1209
|
await buildManifest(root);
|
|
1210
|
+
await scanClientComponents(root, state);
|
|
1029
1211
|
},
|
|
1030
1212
|
/**
|
|
1031
1213
|
* Configure the dev server with file watching.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudwerk/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|