@cloudwerk/vite-plugin 0.6.2 → 0.6.3

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
@@ -1,5 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
- import { CloudwerkConfig, RouteManifest, ScanResult, QueueManifest, ServiceManifest } from '@cloudwerk/core/build';
2
+ import { CloudwerkConfig, QueueManifest, ServiceManifest, RouteManifest, ScanResult } from '@cloudwerk/core/build';
3
3
 
4
4
  /**
5
5
  * @cloudwerk/vite-plugin - Types
@@ -96,6 +96,17 @@ interface ClientComponentInfo {
96
96
  /** Absolute file path */
97
97
  absolutePath: string;
98
98
  }
99
+ /**
100
+ * Information about a CSS import detected in a layout or page.
101
+ */
102
+ interface CssImportInfo {
103
+ /** Absolute path to the CSS file */
104
+ absolutePath: string;
105
+ /** File that imports the CSS */
106
+ importedBy: string;
107
+ /** Whether the importing file is a layout */
108
+ isLayout: boolean;
109
+ }
99
110
  /**
100
111
  * Virtual module IDs used by the plugin.
101
112
  */
@@ -150,6 +161,20 @@ declare function cloudwerkPlugin(options?: CloudwerkVitePluginOptions): Plugin;
150
161
  * a Hono app with all routes registered from the file-based routing manifest.
151
162
  */
152
163
 
164
+ /**
165
+ * Asset manifest entry from Vite build.
166
+ */
167
+ interface AssetManifestEntry {
168
+ file: string;
169
+ css?: string[];
170
+ assets?: string[];
171
+ isEntry?: boolean;
172
+ isDynamicEntry?: boolean;
173
+ }
174
+ /**
175
+ * Asset manifest from Vite build (maps source to output files).
176
+ */
177
+ type AssetManifest = Record<string, AssetManifestEntry>;
153
178
  /**
154
179
  * Options for generating server entry.
155
180
  */
@@ -158,6 +183,8 @@ interface GenerateServerEntryOptions {
158
183
  queueManifest?: QueueManifest | null;
159
184
  /** Service manifest if services are configured */
160
185
  serviceManifest?: ServiceManifest | null;
186
+ /** Asset manifest from Vite build for CSS injection */
187
+ assetManifest?: AssetManifest | null;
161
188
  }
162
189
  /**
163
190
  * Generate the server entry module code.
@@ -187,14 +214,16 @@ declare function generateServerEntry(manifest: RouteManifest, scanResult: ScanRe
187
214
  * Generate the client entry module code.
188
215
  *
189
216
  * This creates a hydration bootstrap that:
217
+ * - Imports CSS files from layouts and pages
190
218
  * - Finds all elements with data-hydrate-id attributes
191
219
  * - Dynamically imports the corresponding component bundles
192
220
  * - Hydrates each component with its serialized props
193
221
  *
194
222
  * @param clientComponents - Map of detected client components
223
+ * @param cssImports - Map of CSS imports from layouts and pages
195
224
  * @param options - Resolved plugin options
196
225
  * @returns Generated JavaScript code
197
226
  */
198
- declare function generateClientEntry(clientComponents: Map<string, ClientComponentInfo>, options: ResolvedCloudwerkOptions): string;
227
+ declare function generateClientEntry(clientComponents: Map<string, ClientComponentInfo>, cssImports: Map<string, CssImportInfo[]>, options: ResolvedCloudwerkOptions): string;
199
228
 
200
- export { type ClientComponentInfo, type CloudwerkVitePluginOptions, RESOLVED_VIRTUAL_IDS, type ResolvedCloudwerkOptions, VIRTUAL_MODULE_IDS, cloudwerkPlugin, cloudwerkPlugin as default, generateClientEntry, generateServerEntry };
229
+ export { type AssetManifest, type AssetManifestEntry, type ClientComponentInfo, type CloudwerkVitePluginOptions, type CssImportInfo, type GenerateServerEntryOptions, RESOLVED_VIRTUAL_IDS, type ResolvedCloudwerkOptions, VIRTUAL_MODULE_IDS, cloudwerkPlugin, cloudwerkPlugin as default, generateClientEntry, generateServerEntry };
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ import * as path from "path";
39
39
  function generateServerEntry(manifest, scanResult, options, entryOptions) {
40
40
  const queueManifest = entryOptions?.queueManifest;
41
41
  const serviceManifest = entryOptions?.serviceManifest;
42
+ const assetManifest = entryOptions?.assetManifest;
42
43
  const imports = [];
43
44
  const pageRegistrations = [];
44
45
  const routeRegistrations = [];
@@ -139,6 +140,18 @@ function generateServerEntry(manifest, scanResult, options, entryOptions) {
139
140
  }
140
141
  const rendererName = options.renderer;
141
142
  const clientEntryPath = options.isProduction ? `${options.hydrationEndpoint}/client.js` : "/@id/__x00__virtual:cloudwerk/client-entry";
143
+ let cssLinksCode = "";
144
+ if (options.isProduction && assetManifest) {
145
+ const clientEntry = assetManifest["virtual:cloudwerk/client-entry"];
146
+ if (clientEntry?.css && clientEntry.css.length > 0) {
147
+ const cssLinks = clientEntry.css.map((css) => `<link rel="stylesheet" href="/${css}" />`).join("");
148
+ cssLinksCode = `const CSS_LINKS = '${cssLinks}'`;
149
+ }
150
+ }
151
+ if (!cssLinksCode) {
152
+ cssLinksCode = `const CSS_LINKS = ''`;
153
+ }
154
+ const viteClientScript = options.isProduction ? "" : '<script type="module" src="/@vite/client"></script>';
142
155
  return `/**
143
156
  * Generated Cloudwerk Server Entry
144
157
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
@@ -176,6 +189,16 @@ const notFoundBoundaryMap = new Map([
176
189
  ${notFoundBoundaryMapEntries.join(",\n")}
177
190
  ])
178
191
 
192
+ // ============================================================================
193
+ // Asset Injection Configuration
194
+ // ============================================================================
195
+
196
+ // CSS links from asset manifest (production) or empty (dev - CSS served by Vite)
197
+ ${cssLinksCode}
198
+
199
+ // Vite client script for HMR (dev only)
200
+ const VITE_CLIENT = '${viteClientScript}'
201
+
179
202
  // ============================================================================
180
203
  // Helper Functions
181
204
  // ============================================================================
@@ -395,20 +418,34 @@ function registerPage(app, pattern, pageModule, layoutModules, middlewareModules
395
418
  }
396
419
 
397
420
  /**
398
- * Render element to a Response, injecting hydration script before </body>.
421
+ * Render element to a Response, injecting CSS and scripts.
422
+ * - CSS links are injected before </head>
423
+ * - Vite client (dev) and hydration script are injected before </body>
399
424
  */
400
425
  function renderWithHydration(element, status = 200) {
401
426
  // Hono JSX elements have toString() for synchronous rendering
402
- const html = '<!DOCTYPE html>' + String(element)
427
+ let html = '<!DOCTYPE html>' + String(element)
403
428
 
404
- // Inject hydration script before </body> if it exists (case-insensitive for HTML compat)
405
- const hydrationScript = '<script type="module" src="${clientEntryPath}"></script>'
429
+ // Inject CSS links before </head> if present
430
+ if (CSS_LINKS) {
431
+ const headCloseRegex = /<\\/head>/i
432
+ if (headCloseRegex.test(html)) {
433
+ html = html.replace(headCloseRegex, CSS_LINKS + '</head>')
434
+ }
435
+ }
436
+
437
+ // Inject scripts before </body>
438
+ // - Vite client for HMR (dev only)
439
+ // - Hydration script for client components
440
+ const scripts = VITE_CLIENT + '<script type="module" src="${clientEntryPath}"></script>'
406
441
  const bodyCloseRegex = /<\\/body>/i
407
- const injectedHtml = bodyCloseRegex.test(html)
408
- ? html.replace(bodyCloseRegex, hydrationScript + '</body>')
409
- : html + hydrationScript
442
+ if (bodyCloseRegex.test(html)) {
443
+ html = html.replace(bodyCloseRegex, scripts + '</body>')
444
+ } else {
445
+ html = html + scripts
446
+ }
410
447
 
411
- return new Response(injectedHtml, {
448
+ return new Response(html, {
412
449
  status,
413
450
  headers: {
414
451
  'Content-Type': 'text/html; charset=utf-8',
@@ -469,6 +506,15 @@ app.use('*', async (c, next) => {
469
506
  return
470
507
  }
471
508
 
509
+ // Only serve static assets for GET/HEAD requests
510
+ // Other methods (POST, PUT, etc.) should go directly to route handlers
511
+ // to avoid consuming the request body
512
+ const method = c.req.method
513
+ if (method !== 'GET' && method !== 'HEAD') {
514
+ await next()
515
+ return
516
+ }
517
+
472
518
  // Try to serve the request as a static asset
473
519
  const response = await c.env.ASSETS.fetch(c.req.raw)
474
520
 
@@ -722,12 +768,28 @@ function generateServiceRegistration(serviceManifest) {
722
768
  }
723
769
 
724
770
  // src/virtual-modules/client-entry.ts
725
- function generateClientEntry(clientComponents, options) {
771
+ function collectCssImports(cssImports) {
772
+ const allCss = /* @__PURE__ */ new Set();
773
+ for (const imports of cssImports.values()) {
774
+ for (const info of imports) {
775
+ allCss.add(info.absolutePath);
776
+ }
777
+ }
778
+ return Array.from(allCss);
779
+ }
780
+ function generateCssImportStatements(cssPaths) {
781
+ if (cssPaths.length === 0) {
782
+ return "";
783
+ }
784
+ return cssPaths.map((cssPath) => `import '${cssPath}'`).join("\n") + "\n\n";
785
+ }
786
+ function generateClientEntry(clientComponents, cssImports, options) {
726
787
  const { renderer, hydrationEndpoint, isProduction } = options;
788
+ const cssPaths = collectCssImports(cssImports);
727
789
  if (renderer === "react") {
728
- return generateReactClientEntry(clientComponents, hydrationEndpoint, isProduction);
790
+ return generateReactClientEntry(clientComponents, cssPaths, hydrationEndpoint, isProduction);
729
791
  }
730
- return generateHonoClientEntry(clientComponents, hydrationEndpoint, isProduction);
792
+ return generateHonoClientEntry(clientComponents, cssPaths, hydrationEndpoint, isProduction);
731
793
  }
732
794
  function generateStaticImportsAndMap(clientComponents) {
733
795
  const components = Array.from(clientComponents.values());
@@ -746,7 +808,7 @@ function generateProductionClientEntry(clientComponents, config) {
746
808
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
747
809
  */
748
810
 
749
- ${config.rendererImports}
811
+ ${config.cssImports}${config.rendererImports}
750
812
 
751
813
  // Static component imports
752
814
  ${imports}
@@ -810,10 +872,12 @@ if (document.readyState === 'loading') {
810
872
  export { hydrate }
811
873
  `;
812
874
  }
813
- function generateHonoClientEntry(clientComponents, _hydrationEndpoint, isProduction = false) {
875
+ function generateHonoClientEntry(clientComponents, cssPaths, _hydrationEndpoint, isProduction = false) {
876
+ const cssImportStatements = generateCssImportStatements(cssPaths);
814
877
  if (isProduction) {
815
878
  return generateProductionClientEntry(clientComponents, {
816
879
  header: "Generated Cloudwerk Client Entry (Hono JSX - Production)",
880
+ cssImports: cssImportStatements,
817
881
  rendererImports: `import { render } from 'hono/jsx/dom'
818
882
  import { jsx } from 'hono/jsx/jsx-runtime'`,
819
883
  additionalDeclarations: "",
@@ -830,7 +894,7 @@ import { jsx } from 'hono/jsx/jsx-runtime'`,
830
894
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
831
895
  */
832
896
 
833
- import { render } from 'hono/jsx/dom'
897
+ ${cssImportStatements}import { render } from 'hono/jsx/dom'
834
898
  import { jsx } from 'hono/jsx/jsx-runtime'
835
899
 
836
900
  // Bundle map for component lookups
@@ -914,10 +978,12 @@ if (document.readyState === 'loading') {
914
978
  export { hydrate }
915
979
  `;
916
980
  }
917
- function generateReactClientEntry(clientComponents, _hydrationEndpoint, isProduction = false) {
981
+ function generateReactClientEntry(clientComponents, cssPaths, _hydrationEndpoint, isProduction = false) {
982
+ const cssImportStatements = generateCssImportStatements(cssPaths);
918
983
  if (isProduction) {
919
984
  return generateProductionClientEntry(clientComponents, {
920
985
  header: "Generated Cloudwerk Client Entry (React - Production)",
986
+ cssImports: cssImportStatements,
921
987
  rendererImports: `import { hydrateRoot } from 'react-dom/client'
922
988
  import { createElement } from 'react'`,
923
989
  additionalDeclarations: `
@@ -937,7 +1003,7 @@ const rootCache = new Map()
937
1003
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
938
1004
  */
939
1005
 
940
- import { hydrateRoot, createRoot } from 'react-dom/client'
1006
+ ${cssImportStatements}import { hydrateRoot, createRoot } from 'react-dom/client'
941
1007
  import { createElement } from 'react'
942
1008
 
943
1009
  // Bundle map for component lookups
@@ -1594,6 +1660,66 @@ async function scanClientComponents(root, state) {
1594
1660
  }
1595
1661
  await scanDir(appDir);
1596
1662
  }
1663
+ var CSS_IMPORT_REGEX = /import\s+(?:['"]([^'"]+\.css)['"]|(?:\w+\s+from\s+)?['"]([^'"]+\.css)['"])/g;
1664
+ function extractCssImports(code) {
1665
+ const imports = [];
1666
+ let match;
1667
+ CSS_IMPORT_REGEX.lastIndex = 0;
1668
+ while ((match = CSS_IMPORT_REGEX.exec(code)) !== null) {
1669
+ const cssPath = match[1] || match[2];
1670
+ if (cssPath) {
1671
+ imports.push(cssPath);
1672
+ }
1673
+ }
1674
+ return imports;
1675
+ }
1676
+ function isLayoutOrPage(filePath) {
1677
+ const basename2 = path3.basename(filePath);
1678
+ const nameWithoutExt = basename2.replace(/\.(ts|tsx|js|jsx)$/, "");
1679
+ return {
1680
+ isLayout: nameWithoutExt === "layout",
1681
+ isPage: nameWithoutExt === "page"
1682
+ };
1683
+ }
1684
+ async function scanCssImports(root, state) {
1685
+ const appDir = path3.resolve(root, state.options.appDir);
1686
+ try {
1687
+ await fs2.promises.access(appDir);
1688
+ } catch {
1689
+ return;
1690
+ }
1691
+ async function scanDir(dir) {
1692
+ const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
1693
+ await Promise.all(
1694
+ entries.map(async (entry) => {
1695
+ const fullPath = path3.join(dir, entry.name);
1696
+ if (entry.isDirectory()) {
1697
+ if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
1698
+ await scanDir(fullPath);
1699
+ }
1700
+ } else if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
1701
+ const { isLayout, isPage } = isLayoutOrPage(fullPath);
1702
+ if (isLayout || isPage) {
1703
+ const content = await fs2.promises.readFile(fullPath, "utf-8");
1704
+ const cssImportPaths = extractCssImports(content);
1705
+ if (cssImportPaths.length > 0) {
1706
+ const cssInfos = cssImportPaths.map((cssPath) => ({
1707
+ absolutePath: path3.resolve(path3.dirname(fullPath), cssPath),
1708
+ importedBy: fullPath,
1709
+ isLayout
1710
+ }));
1711
+ state.cssImports.set(fullPath, cssInfos);
1712
+ if (state.options.verbose) {
1713
+ console.log(`[cloudwerk] Found ${cssInfos.length} CSS import(s) in ${path3.relative(root, fullPath)}`);
1714
+ }
1715
+ }
1716
+ }
1717
+ }
1718
+ })
1719
+ );
1720
+ }
1721
+ await scanDir(appDir);
1722
+ }
1597
1723
  function cloudwerkPlugin(options = {}) {
1598
1724
  let state = null;
1599
1725
  let server = null;
@@ -1794,6 +1920,7 @@ function cloudwerkPlugin(options = {}) {
1794
1920
  serviceManifest: null,
1795
1921
  serviceScanResult: null,
1796
1922
  clientComponents: /* @__PURE__ */ new Map(),
1923
+ cssImports: /* @__PURE__ */ new Map(),
1797
1924
  serverEntryCache: null,
1798
1925
  clientEntryCache: null
1799
1926
  };
@@ -1801,6 +1928,7 @@ function cloudwerkPlugin(options = {}) {
1801
1928
  await buildQueueManifestIfExists(root);
1802
1929
  await buildServiceManifestIfExists(root);
1803
1930
  await scanClientComponents(root, state);
1931
+ await scanCssImports(root, state);
1804
1932
  },
1805
1933
  /**
1806
1934
  * Configure the dev server with file watching.
@@ -1945,6 +2073,7 @@ function cloudwerkPlugin(options = {}) {
1945
2073
  if (!state.clientEntryCache) {
1946
2074
  state.clientEntryCache = generateClientEntry(
1947
2075
  state.clientComponents,
2076
+ state.cssImports,
1948
2077
  state.options
1949
2078
  );
1950
2079
  }
@@ -2031,6 +2160,22 @@ function cloudwerkPlugin(options = {}) {
2031
2160
  return parts.join("\n");
2032
2161
  });
2033
2162
  }
2163
+ const { isLayout, isPage } = isLayoutOrPage(id);
2164
+ if (isLayout || isPage) {
2165
+ const cssImportPaths = extractCssImports(transformedCode);
2166
+ if (cssImportPaths.length > 0) {
2167
+ const cssInfos = cssImportPaths.map((cssPath) => ({
2168
+ absolutePath: path3.resolve(path3.dirname(id), cssPath),
2169
+ importedBy: id,
2170
+ isLayout
2171
+ }));
2172
+ state.cssImports.set(id, cssInfos);
2173
+ state.clientEntryCache = null;
2174
+ if (state.options.verbose) {
2175
+ console.log(`[cloudwerk] Detected ${cssInfos.length} CSS import(s) in ${path3.relative(state.options.root, id)}`);
2176
+ }
2177
+ }
2178
+ }
2034
2179
  if (hasUseClientDirective(transformedCode)) {
2035
2180
  const componentId = generateComponentId(id, state.options.root);
2036
2181
  const bundlePath = `${state.options.hydrationEndpoint}/${componentId}.js`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudwerk/vite-plugin",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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.15.0",
23
- "@cloudwerk/ui": "^0.15.0"
22
+ "@cloudwerk/core": "^0.15.1",
23
+ "@cloudwerk/ui": "^0.15.1"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "vite": "^5.0.0 || ^6.0.0",