@cloudwerk/vite-plugin 0.3.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 CHANGED
@@ -82,6 +82,8 @@ interface ResolvedCloudwerkOptions {
82
82
  publicDir: string;
83
83
  /** Vite root directory (absolute path) */
84
84
  root: string;
85
+ /** Whether building for production (affects asset paths) */
86
+ isProduction?: boolean;
85
87
  }
86
88
  /**
87
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="/@id/__x00__virtual:cloudwerk/client-entry"></script>'
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);
543
+ }
544
+ return generateHonoClientEntry(clientComponents, hydrationEndpoint, isProduction);
545
+ }
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
+ }
522
613
  }
523
- return generateHonoClientEntry(clientComponents, hydrationEndpoint);
524
614
  }
525
- function generateHonoClientEntry(clientComponents, _hydrationEndpoint) {
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;
@@ -1003,6 +1167,7 @@ function cloudwerkPlugin(options = {}) {
1003
1167
  }
1004
1168
  }
1005
1169
  const cloudwerkConfig = await loadConfig(root);
1170
+ const isProduction = config.command === "build" || config.mode === "production";
1006
1171
  const resolvedOptions = {
1007
1172
  appDir: options.appDir ?? cloudwerkConfig.appDir,
1008
1173
  routesDir: options.routesDir ?? cloudwerkConfig.routesDir ?? "routes",
@@ -1013,7 +1178,8 @@ function cloudwerkPlugin(options = {}) {
1013
1178
  hydrationEndpoint: options.hydrationEndpoint ?? "/__cloudwerk",
1014
1179
  renderer: options.renderer ?? cloudwerkConfig.ui?.renderer ?? "hono-jsx",
1015
1180
  publicDir: options.publicDir ?? cloudwerkConfig.publicDir ?? "public",
1016
- root
1181
+ root,
1182
+ isProduction
1017
1183
  };
1018
1184
  state = {
1019
1185
  options: resolvedOptions,
@@ -1041,6 +1207,7 @@ function cloudwerkPlugin(options = {}) {
1041
1207
  clientEntryCache: null
1042
1208
  };
1043
1209
  await buildManifest(root);
1210
+ await scanClientComponents(root, state);
1044
1211
  },
1045
1212
  /**
1046
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.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",