@cloudwerk/vite-plugin 0.6.2 → 0.6.5

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, AuthManifest, 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,12 @@ 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;
188
+ /** Auth manifest if auth providers are configured */
189
+ authManifest?: AuthManifest | null;
190
+ /** CSS imports from layouts/pages (for dev mode injection) */
191
+ cssImports?: Map<string, CssImportInfo[]>;
161
192
  }
162
193
  /**
163
194
  * Generate the server entry module code.
@@ -187,14 +218,16 @@ declare function generateServerEntry(manifest: RouteManifest, scanResult: ScanRe
187
218
  * Generate the client entry module code.
188
219
  *
189
220
  * This creates a hydration bootstrap that:
221
+ * - Imports CSS files from layouts and pages
190
222
  * - Finds all elements with data-hydrate-id attributes
191
223
  * - Dynamically imports the corresponding component bundles
192
224
  * - Hydrates each component with its serialized props
193
225
  *
194
226
  * @param clientComponents - Map of detected client components
227
+ * @param cssImports - Map of CSS imports from layouts and pages
195
228
  * @param options - Resolved plugin options
196
229
  * @returns Generated JavaScript code
197
230
  */
198
- declare function generateClientEntry(clientComponents: Map<string, ClientComponentInfo>, options: ResolvedCloudwerkOptions): string;
231
+ declare function generateClientEntry(clientComponents: Map<string, ClientComponentInfo>, cssImports: Map<string, CssImportInfo[]>, options: ResolvedCloudwerkOptions): string;
199
232
 
200
- export { type ClientComponentInfo, type CloudwerkVitePluginOptions, RESOLVED_VIRTUAL_IDS, type ResolvedCloudwerkOptions, VIRTUAL_MODULE_IDS, cloudwerkPlugin, cloudwerkPlugin as default, generateClientEntry, generateServerEntry };
233
+ 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
@@ -19,7 +19,13 @@ import {
19
19
  scanServices,
20
20
  buildServiceManifest,
21
21
  SERVICES_DIR,
22
- SERVICE_FILE_NAME
22
+ SERVICE_FILE_NAME,
23
+ scanImages,
24
+ buildImageManifest,
25
+ IMAGES_DIR,
26
+ scanAuth,
27
+ buildAuthManifestWithModules,
28
+ AUTH_DIR
23
29
  } from "@cloudwerk/core/build";
24
30
 
25
31
  // src/types.ts
@@ -39,6 +45,8 @@ import * as path from "path";
39
45
  function generateServerEntry(manifest, scanResult, options, entryOptions) {
40
46
  const queueManifest = entryOptions?.queueManifest;
41
47
  const serviceManifest = entryOptions?.serviceManifest;
48
+ const assetManifest = entryOptions?.assetManifest;
49
+ const authManifest = entryOptions?.authManifest;
42
50
  const imports = [];
43
51
  const pageRegistrations = [];
44
52
  const routeRegistrations = [];
@@ -57,6 +65,10 @@ function generateServerEntry(manifest, scanResult, options, entryOptions) {
57
65
  let middlewareIndex = 0;
58
66
  let errorIndex = 0;
59
67
  let notFoundIndex = 0;
68
+ const rootMiddleware = scanResult.middleware.find(
69
+ (m) => m.relativePath === "middleware.ts" || m.relativePath === "middleware.tsx"
70
+ );
71
+ let rootMiddlewareVarName = null;
60
72
  const ssgPageInfo = [];
61
73
  for (const err of scanResult.errors) {
62
74
  if (!importedModules.has(err.absolutePath)) {
@@ -88,6 +100,12 @@ function generateServerEntry(manifest, scanResult, options, entryOptions) {
88
100
  const varName = notFoundModules.get(nf.absolutePath);
89
101
  notFoundBoundaryMapEntries.push(` ['${normalizedDir}', ${varName}]`);
90
102
  }
103
+ if (rootMiddleware) {
104
+ rootMiddlewareVarName = `middleware_${middlewareIndex++}`;
105
+ middlewareImports.push(`import { middleware as ${rootMiddlewareVarName} } from '${rootMiddleware.absolutePath}'`);
106
+ middlewareModules.set(rootMiddleware.absolutePath, rootMiddlewareVarName);
107
+ importedModules.add(rootMiddleware.absolutePath);
108
+ }
91
109
  for (const route of manifest.routes) {
92
110
  for (const middlewarePath of route.middleware) {
93
111
  if (!importedModules.has(middlewarePath)) {
@@ -138,7 +156,40 @@ function generateServerEntry(manifest, scanResult, options, entryOptions) {
138
156
  }
139
157
  }
140
158
  const rendererName = options.renderer;
141
- const clientEntryPath = options.isProduction ? `${options.hydrationEndpoint}/client.js` : "/@id/__x00__virtual:cloudwerk/client-entry";
159
+ let clientEntryPath = "/@id/__x00__virtual:cloudwerk/client-entry";
160
+ if (options.isProduction && assetManifest) {
161
+ const clientEntry = assetManifest["virtual:cloudwerk/client-entry"];
162
+ if (clientEntry?.file) {
163
+ clientEntryPath = `/${clientEntry.file}`;
164
+ } else {
165
+ clientEntryPath = `${options.hydrationEndpoint}/client.js`;
166
+ }
167
+ } else if (options.isProduction) {
168
+ clientEntryPath = `${options.hydrationEndpoint}/client.js`;
169
+ }
170
+ let cssLinksCode = "";
171
+ if (options.isProduction && assetManifest) {
172
+ const clientEntry = assetManifest["virtual:cloudwerk/client-entry"];
173
+ if (clientEntry?.css && clientEntry.css.length > 0) {
174
+ const cssLinks = clientEntry.css.map((css) => `<link rel="stylesheet" href="/${css}" />`).join("");
175
+ cssLinksCode = `const CSS_LINKS = '${cssLinks}'`;
176
+ }
177
+ } else if (!options.isProduction && entryOptions?.cssImports) {
178
+ const allCss = /* @__PURE__ */ new Set();
179
+ for (const imports2 of entryOptions.cssImports.values()) {
180
+ for (const info of imports2) {
181
+ allCss.add(info.absolutePath);
182
+ }
183
+ }
184
+ if (allCss.size > 0) {
185
+ const cssLinks = Array.from(allCss).map((css) => `<link rel="stylesheet" href="/@fs${css}" />`).join("");
186
+ cssLinksCode = `const CSS_LINKS = '${cssLinks}'`;
187
+ }
188
+ }
189
+ if (!cssLinksCode) {
190
+ cssLinksCode = `const CSS_LINKS = ''`;
191
+ }
192
+ const viteClientScript = options.isProduction ? "" : '<script type="module" src="/@vite/client"></script>';
142
193
  return `/**
143
194
  * Generated Cloudwerk Server Entry
144
195
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
@@ -176,6 +227,16 @@ const notFoundBoundaryMap = new Map([
176
227
  ${notFoundBoundaryMapEntries.join(",\n")}
177
228
  ])
178
229
 
230
+ // ============================================================================
231
+ // Asset Injection Configuration
232
+ // ============================================================================
233
+
234
+ // CSS links from asset manifest (production) or empty (dev - CSS served by Vite)
235
+ ${cssLinksCode}
236
+
237
+ // Vite client script for HMR (dev only)
238
+ const VITE_CLIENT = '${viteClientScript}'
239
+
179
240
  // ============================================================================
180
241
  // Helper Functions
181
242
  // ============================================================================
@@ -395,20 +456,34 @@ function registerPage(app, pattern, pageModule, layoutModules, middlewareModules
395
456
  }
396
457
 
397
458
  /**
398
- * Render element to a Response, injecting hydration script before </body>.
459
+ * Render element to a Response, injecting CSS and scripts.
460
+ * - CSS links are injected before </head>
461
+ * - Vite client (dev) and hydration script are injected before </body>
399
462
  */
400
463
  function renderWithHydration(element, status = 200) {
401
464
  // Hono JSX elements have toString() for synchronous rendering
402
- const html = '<!DOCTYPE html>' + String(element)
465
+ let html = '<!DOCTYPE html>' + String(element)
466
+
467
+ // Inject CSS links before </head> if present
468
+ if (CSS_LINKS) {
469
+ const headCloseRegex = /<\\/head>/i
470
+ if (headCloseRegex.test(html)) {
471
+ html = html.replace(headCloseRegex, CSS_LINKS + '</head>')
472
+ }
473
+ }
403
474
 
404
- // Inject hydration script before </body> if it exists (case-insensitive for HTML compat)
405
- const hydrationScript = '<script type="module" src="${clientEntryPath}"></script>'
475
+ // Inject scripts before </body>
476
+ // - Vite client for HMR (dev only)
477
+ // - Hydration script for client components
478
+ const scripts = VITE_CLIENT + '<script type="module" src="${clientEntryPath}"></script>'
406
479
  const bodyCloseRegex = /<\\/body>/i
407
- const injectedHtml = bodyCloseRegex.test(html)
408
- ? html.replace(bodyCloseRegex, hydrationScript + '</body>')
409
- : html + hydrationScript
480
+ if (bodyCloseRegex.test(html)) {
481
+ html = html.replace(bodyCloseRegex, scripts + '</body>')
482
+ } else {
483
+ html = html + scripts
484
+ }
410
485
 
411
- return new Response(injectedHtml, {
486
+ return new Response(html, {
412
487
  status,
413
488
  headers: {
414
489
  'Content-Type': 'text/html; charset=utf-8',
@@ -434,7 +509,7 @@ function registerRoute(app, pattern, routeModule, middlewareModules) {
434
509
  for (const method of HTTP_METHODS) {
435
510
  const handler = routeModule[method]
436
511
  if (handler && typeof handler === 'function') {
437
- const h = handler.length === 2 ? createHandlerAdapter(handler) : handler
512
+ const h = createHandlerAdapter(handler)
438
513
  switch (method) {
439
514
  case 'GET': app.get(pattern, h); break
440
515
  case 'POST': app.post(pattern, h); break
@@ -460,7 +535,10 @@ const app = new Hono({ strict: false })
460
535
 
461
536
  // Add context middleware
462
537
  app.use('*', contextMiddleware())
463
- ${options.isProduction ? `
538
+ ${rootMiddlewareVarName ? `
539
+ // Apply root middleware globally (for all routes including auth)
540
+ app.use('*', createMiddlewareAdapter(${rootMiddlewareVarName}))
541
+ ` : ""}${options.isProduction ? `
464
542
  // Serve static assets using Workers Static Assets binding (production only)
465
543
  app.use('*', async (c, next) => {
466
544
  // Check if ASSETS binding is available
@@ -469,12 +547,39 @@ app.use('*', async (c, next) => {
469
547
  return
470
548
  }
471
549
 
550
+ // Only serve static assets for GET/HEAD requests
551
+ // Other methods (POST, PUT, etc.) should go directly to route handlers
552
+ // to avoid consuming the request body
553
+ const method = c.req.method
554
+ if (method !== 'GET' && method !== 'HEAD') {
555
+ await next()
556
+ return
557
+ }
558
+
472
559
  // Try to serve the request as a static asset
473
560
  const response = await c.env.ASSETS.fetch(c.req.raw)
474
561
 
475
- // If asset found (not 404), return it
562
+ // If asset found (not 404), return it with cache headers
476
563
  if (response.status !== 404) {
477
- return response
564
+ const path = new URL(c.req.url).pathname
565
+
566
+ // Check if this is a hashed asset (Vite adds content hash to filename)
567
+ // Hashed assets are immutable and can be cached forever
568
+ const isHashedAsset = path.startsWith('/__cloudwerk/') ||
569
+ /-[a-zA-Z0-9]{8,}\\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|webp|avif|ico)$/.test(path)
570
+
571
+ const cacheControl = isHashedAsset
572
+ ? 'public, max-age=31536000, immutable'
573
+ : 'public, max-age=3600'
574
+
575
+ return new Response(response.body, {
576
+ status: response.status,
577
+ statusText: response.statusText,
578
+ headers: {
579
+ ...Object.fromEntries(response.headers.entries()),
580
+ 'Cache-Control': cacheControl,
581
+ },
582
+ })
478
583
  }
479
584
 
480
585
  // Asset not found, continue to routes
@@ -485,6 +590,9 @@ app.use('*', async (c, next) => {
485
590
  ${pageRegistrations.join("\n")}
486
591
  ${routeRegistrations.join("\n")}
487
592
 
593
+ // Register auth routes
594
+ ${generateAuthRouteRegistrations(authManifest)}
595
+
488
596
  // SSG routes endpoint - returns all static routes for build-time generation
489
597
  app.get('/__ssg/routes', async (c) => {
490
598
  const routes = []
@@ -720,14 +828,266 @@ function generateServiceRegistration(serviceManifest) {
720
828
  }
721
829
  return lines.join("\n");
722
830
  }
831
+ function generateAuthRouteRegistrations(authManifest) {
832
+ if (!authManifest) {
833
+ return "";
834
+ }
835
+ const lines = [];
836
+ const imports = [];
837
+ const basePath = authManifest.config?.basePath || "/auth";
838
+ lines.push("");
839
+ lines.push("// ============================================================================");
840
+ lines.push("// Auth Route Registration");
841
+ lines.push("// ============================================================================");
842
+ lines.push("");
843
+ imports.push(`import {
844
+ handleSession,
845
+ handleProviders,
846
+ handleSignIn,
847
+ handleSignInProvider,
848
+ handleSignOutGet,
849
+ handleSignOutPost,
850
+ } from '@cloudwerk/auth/routes'`);
851
+ imports.push(`import { createSessionManager, createKVSessionAdapter } from '@cloudwerk/auth/session'`);
852
+ const passkeyProviders = authManifest.providers.filter((p) => p.type === "passkey" && !p.disabled);
853
+ if (passkeyProviders.length > 0) {
854
+ imports.push(`import {
855
+ handlePasskeyRegisterOptions,
856
+ handlePasskeyRegisterVerify,
857
+ handlePasskeyAuthenticateOptions,
858
+ handlePasskeyAuthenticateVerify,
859
+ } from '@cloudwerk/auth/routes'`);
860
+ imports.push(`import {
861
+ createKVChallengeStorage,
862
+ createD1CredentialStorage,
863
+ } from '@cloudwerk/auth/providers'`);
864
+ for (let i = 0; i < passkeyProviders.length; i++) {
865
+ const provider = passkeyProviders[i];
866
+ imports.push(`import passkeyProviderDef_${i} from '${provider.filePath}'`);
867
+ }
868
+ }
869
+ lines.push(imports.join("\n"));
870
+ lines.push("");
871
+ lines.push(`
872
+ /**
873
+ * Build auth context for standard auth routes.
874
+ */
875
+ function buildAuthContext(c) {
876
+ const env = c.env || {}
877
+
878
+ // Get KV binding - fall back to common binding names
879
+ let kvBinding = undefined
880
+ for (const name of ['FLAGSHIP_AUTH_SESSIONS', 'AUTH_KV', 'AUTH_SESSIONS', 'KV']) {
881
+ const binding = env[name]
882
+ if (binding && typeof binding.get === 'function') {
883
+ kvBinding = binding
884
+ break
885
+ }
886
+ }
887
+
888
+ // Create session manager from KV
889
+ const sessionAdapter = kvBinding ? createKVSessionAdapter({ binding: kvBinding, enableUserIndex: true }) : undefined
890
+ const sessionManager = sessionAdapter ? createSessionManager({ adapter: sessionAdapter }) : undefined
891
+
892
+ // Build providers map
893
+ const providers = new Map()
894
+
895
+ return {
896
+ request: c.req.raw,
897
+ env,
898
+ config: { basePath: '${basePath}', session: { strategy: '${authManifest.config?.sessionStrategy || "database"}' } },
899
+ sessionManager,
900
+ providers,
901
+ user: c.get?.('user') ?? null,
902
+ session: c.get?.('session') ?? null,
903
+ url: new URL(c.req.url),
904
+ responseHeaders: new Headers(),
905
+ }
906
+ }
907
+ `);
908
+ lines.push(`
909
+ // Standard auth routes
910
+ app.get('${basePath}/session', async (c) => {
911
+ const ctx = buildAuthContext(c)
912
+ return handleSession(ctx)
913
+ })
914
+
915
+ app.get('${basePath}/providers', async (c) => {
916
+ const ctx = buildAuthContext(c)
917
+ return handleProviders(ctx)
918
+ })
919
+
920
+ app.get('${basePath}/signin', async (c) => {
921
+ const ctx = buildAuthContext(c)
922
+ return handleSignIn(ctx)
923
+ })
924
+
925
+ app.get('${basePath}/signin/:provider', async (c) => {
926
+ const ctx = buildAuthContext(c)
927
+ const providerId = c.req.param('provider')
928
+ return handleSignInProvider(ctx, providerId)
929
+ })
930
+
931
+ app.get('${basePath}/signout', async (c) => {
932
+ const ctx = buildAuthContext(c)
933
+ return handleSignOutGet(ctx)
934
+ })
935
+
936
+ app.post('${basePath}/signout', async (c) => {
937
+ const ctx = buildAuthContext(c)
938
+ return handleSignOutPost(ctx)
939
+ })
940
+ `);
941
+ if (passkeyProviders.length > 0) {
942
+ lines.push(`
943
+ /**
944
+ * Build auth context for passkey handlers.
945
+ */
946
+ function buildPasskeyAuthContext(c, passkeyProvider) {
947
+ const env = c.env || {}
948
+
949
+ // Get KV binding for challenges - use provider config or fall back to common names
950
+ const kvBindingName = passkeyProvider.kvBinding
951
+ let kvBinding = kvBindingName ? env[kvBindingName] : undefined
952
+ if (!kvBinding) {
953
+ // Fall back to common binding names
954
+ for (const name of ['AUTH_KV', 'AUTH_SESSIONS', 'KV']) {
955
+ const binding = env[name]
956
+ if (binding && typeof binding.get === 'function') {
957
+ kvBinding = binding
958
+ break
959
+ }
960
+ }
961
+ }
962
+ const challengeStorage = kvBinding ? createKVChallengeStorage(kvBinding, 'auth:challenge:') : undefined
963
+
964
+ // Get D1 binding for credentials - use provider config or fall back to common names
965
+ const d1BindingName = passkeyProvider.d1Binding
966
+ const d1Binding = d1BindingName ? env[d1BindingName] : (env.DB || env.D1 || env.DATABASE)
967
+ const credentialStorage = d1Binding ? createD1CredentialStorage(d1Binding, 'webauthn_credentials') : undefined
968
+
969
+ // Create user adapter from D1
970
+ const userAdapter = d1Binding ? {
971
+ async getUserByEmail(email) {
972
+ const user = await d1Binding.prepare(
973
+ 'SELECT id, email, email_verified, name, image, created_at, updated_at FROM users WHERE email = ?'
974
+ ).bind(email).first()
975
+ if (!user) return null
976
+ return {
977
+ id: user.id,
978
+ email: user.email,
979
+ emailVerified: user.email_verified ? new Date(user.email_verified) : null,
980
+ name: user.name,
981
+ image: user.image,
982
+ createdAt: new Date(user.created_at),
983
+ updatedAt: new Date(user.updated_at),
984
+ }
985
+ },
986
+ async createUser(userData) {
987
+ const id = crypto.randomUUID()
988
+ const now = new Date().toISOString()
989
+ await d1Binding.prepare(
990
+ 'INSERT INTO users (id, email, email_verified, name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
991
+ ).bind(id, userData.email, userData.emailVerified?.toISOString() ?? null, userData.name ?? null, now, now).run()
992
+ return { id, email: userData.email, emailVerified: userData.emailVerified, name: userData.name ?? null, image: null, createdAt: new Date(now), updatedAt: new Date(now) }
993
+ },
994
+ async getUser(id) {
995
+ const user = await d1Binding.prepare(
996
+ 'SELECT id, email, email_verified, name, image, created_at, updated_at FROM users WHERE id = ?'
997
+ ).bind(id).first()
998
+ if (!user) return null
999
+ return {
1000
+ id: user.id,
1001
+ email: user.email,
1002
+ emailVerified: user.email_verified ? new Date(user.email_verified) : null,
1003
+ name: user.name,
1004
+ image: user.image,
1005
+ createdAt: new Date(user.created_at),
1006
+ updatedAt: new Date(user.updated_at),
1007
+ }
1008
+ },
1009
+ } : undefined
1010
+
1011
+ // Create session manager from KV
1012
+ const sessionAdapter = kvBinding ? createKVSessionAdapter({ binding: kvBinding, enableUserIndex: true }) : undefined
1013
+ const sessionManager = sessionAdapter ? createSessionManager({ adapter: sessionAdapter }) : undefined
1014
+
1015
+ // Build providers map
1016
+ const providers = new Map()
1017
+ providers.set(passkeyProvider.id, passkeyProvider)
1018
+
1019
+ return {
1020
+ request: c.req.raw,
1021
+ env,
1022
+ config: { basePath: '${basePath}', session: { strategy: '${authManifest.config?.sessionStrategy || "database"}' } },
1023
+ sessionManager,
1024
+ providers,
1025
+ user: null,
1026
+ session: null,
1027
+ url: new URL(c.req.url),
1028
+ responseHeaders: new Headers(),
1029
+ challengeStorage,
1030
+ credentialStorage,
1031
+ userAdapter,
1032
+ }
1033
+ }
1034
+ `);
1035
+ for (let i = 0; i < passkeyProviders.length; i++) {
1036
+ const provider = passkeyProviders[i];
1037
+ lines.push(`
1038
+ // Get provider from definition
1039
+ const passkeyProvider_${i} = passkeyProviderDef_${i}.provider || passkeyProviderDef_${i}
1040
+ `);
1041
+ lines.push(`
1042
+ // Passkey registration routes
1043
+ app.post('${basePath}/passkey/register/options', async (c) => {
1044
+ const ctx = buildPasskeyAuthContext(c, passkeyProvider_${i})
1045
+ return handlePasskeyRegisterOptions(ctx, '${provider.id}')
1046
+ })
1047
+
1048
+ app.post('${basePath}/passkey/register/verify', async (c) => {
1049
+ const ctx = buildPasskeyAuthContext(c, passkeyProvider_${i})
1050
+ return handlePasskeyRegisterVerify(ctx, '${provider.id}')
1051
+ })
1052
+
1053
+ app.post('${basePath}/passkey/authenticate/options', async (c) => {
1054
+ const ctx = buildPasskeyAuthContext(c, passkeyProvider_${i})
1055
+ return handlePasskeyAuthenticateOptions(ctx, '${provider.id}')
1056
+ })
1057
+
1058
+ app.post('${basePath}/passkey/authenticate/verify', async (c) => {
1059
+ const ctx = buildPasskeyAuthContext(c, passkeyProvider_${i})
1060
+ return handlePasskeyAuthenticateVerify(ctx, '${provider.id}')
1061
+ })
1062
+ `);
1063
+ }
1064
+ }
1065
+ return lines.join("\n");
1066
+ }
723
1067
 
724
1068
  // src/virtual-modules/client-entry.ts
725
- function generateClientEntry(clientComponents, options) {
1069
+ function collectCssImports(cssImports) {
1070
+ const allCss = /* @__PURE__ */ new Set();
1071
+ for (const imports of cssImports.values()) {
1072
+ for (const info of imports) {
1073
+ allCss.add(info.absolutePath);
1074
+ }
1075
+ }
1076
+ return Array.from(allCss);
1077
+ }
1078
+ function generateCssImportStatements(cssPaths) {
1079
+ if (cssPaths.length === 0) {
1080
+ return "";
1081
+ }
1082
+ return cssPaths.map((cssPath) => `import '${cssPath}'`).join("\n") + "\n\n";
1083
+ }
1084
+ function generateClientEntry(clientComponents, cssImports, options) {
726
1085
  const { renderer, hydrationEndpoint, isProduction } = options;
1086
+ const cssPaths = collectCssImports(cssImports);
727
1087
  if (renderer === "react") {
728
- return generateReactClientEntry(clientComponents, hydrationEndpoint, isProduction);
1088
+ return generateReactClientEntry(clientComponents, cssPaths, hydrationEndpoint, isProduction);
729
1089
  }
730
- return generateHonoClientEntry(clientComponents, hydrationEndpoint, isProduction);
1090
+ return generateHonoClientEntry(clientComponents, cssPaths, hydrationEndpoint, isProduction);
731
1091
  }
732
1092
  function generateStaticImportsAndMap(clientComponents) {
733
1093
  const components = Array.from(clientComponents.values());
@@ -746,7 +1106,7 @@ function generateProductionClientEntry(clientComponents, config) {
746
1106
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
747
1107
  */
748
1108
 
749
- ${config.rendererImports}
1109
+ ${config.cssImports}${config.rendererImports}
750
1110
 
751
1111
  // Static component imports
752
1112
  ${imports}
@@ -810,10 +1170,12 @@ if (document.readyState === 'loading') {
810
1170
  export { hydrate }
811
1171
  `;
812
1172
  }
813
- function generateHonoClientEntry(clientComponents, _hydrationEndpoint, isProduction = false) {
1173
+ function generateHonoClientEntry(clientComponents, cssPaths, _hydrationEndpoint, isProduction = false) {
1174
+ const cssImportStatements = generateCssImportStatements(cssPaths);
814
1175
  if (isProduction) {
815
1176
  return generateProductionClientEntry(clientComponents, {
816
1177
  header: "Generated Cloudwerk Client Entry (Hono JSX - Production)",
1178
+ cssImports: cssImportStatements,
817
1179
  rendererImports: `import { render } from 'hono/jsx/dom'
818
1180
  import { jsx } from 'hono/jsx/jsx-runtime'`,
819
1181
  additionalDeclarations: "",
@@ -830,7 +1192,7 @@ import { jsx } from 'hono/jsx/jsx-runtime'`,
830
1192
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
831
1193
  */
832
1194
 
833
- import { render } from 'hono/jsx/dom'
1195
+ ${cssImportStatements}import { render } from 'hono/jsx/dom'
834
1196
  import { jsx } from 'hono/jsx/jsx-runtime'
835
1197
 
836
1198
  // Bundle map for component lookups
@@ -914,10 +1276,12 @@ if (document.readyState === 'loading') {
914
1276
  export { hydrate }
915
1277
  `;
916
1278
  }
917
- function generateReactClientEntry(clientComponents, _hydrationEndpoint, isProduction = false) {
1279
+ function generateReactClientEntry(clientComponents, cssPaths, _hydrationEndpoint, isProduction = false) {
1280
+ const cssImportStatements = generateCssImportStatements(cssPaths);
918
1281
  if (isProduction) {
919
1282
  return generateProductionClientEntry(clientComponents, {
920
1283
  header: "Generated Cloudwerk Client Entry (React - Production)",
1284
+ cssImports: cssImportStatements,
921
1285
  rendererImports: `import { hydrateRoot } from 'react-dom/client'
922
1286
  import { createElement } from 'react'`,
923
1287
  additionalDeclarations: `
@@ -937,7 +1301,7 @@ const rootCache = new Map()
937
1301
  * This file is auto-generated by @cloudwerk/vite-plugin - do not edit
938
1302
  */
939
1303
 
940
- import { hydrateRoot, createRoot } from 'react-dom/client'
1304
+ ${cssImportStatements}import { hydrateRoot, createRoot } from 'react-dom/client'
941
1305
  import { createElement } from 'react'
942
1306
 
943
1307
  // Bundle map for component lookups
@@ -1594,6 +1958,66 @@ async function scanClientComponents(root, state) {
1594
1958
  }
1595
1959
  await scanDir(appDir);
1596
1960
  }
1961
+ var CSS_IMPORT_REGEX = /import\s+(?:['"]([^'"]+\.css)['"]|(?:\w+\s+from\s+)?['"]([^'"]+\.css)['"])/g;
1962
+ function extractCssImports(code) {
1963
+ const imports = [];
1964
+ let match;
1965
+ CSS_IMPORT_REGEX.lastIndex = 0;
1966
+ while ((match = CSS_IMPORT_REGEX.exec(code)) !== null) {
1967
+ const cssPath = match[1] || match[2];
1968
+ if (cssPath) {
1969
+ imports.push(cssPath);
1970
+ }
1971
+ }
1972
+ return imports;
1973
+ }
1974
+ function isLayoutOrPage(filePath) {
1975
+ const basename2 = path3.basename(filePath);
1976
+ const nameWithoutExt = basename2.replace(/\.(ts|tsx|js|jsx)$/, "");
1977
+ return {
1978
+ isLayout: nameWithoutExt === "layout",
1979
+ isPage: nameWithoutExt === "page"
1980
+ };
1981
+ }
1982
+ async function scanCssImports(root, state) {
1983
+ const appDir = path3.resolve(root, state.options.appDir);
1984
+ try {
1985
+ await fs2.promises.access(appDir);
1986
+ } catch {
1987
+ return;
1988
+ }
1989
+ async function scanDir(dir) {
1990
+ const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
1991
+ await Promise.all(
1992
+ entries.map(async (entry) => {
1993
+ const fullPath = path3.join(dir, entry.name);
1994
+ if (entry.isDirectory()) {
1995
+ if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
1996
+ await scanDir(fullPath);
1997
+ }
1998
+ } else if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
1999
+ const { isLayout, isPage } = isLayoutOrPage(fullPath);
2000
+ if (isLayout || isPage) {
2001
+ const content = await fs2.promises.readFile(fullPath, "utf-8");
2002
+ const cssImportPaths = extractCssImports(content);
2003
+ if (cssImportPaths.length > 0) {
2004
+ const cssInfos = cssImportPaths.map((cssPath) => ({
2005
+ absolutePath: path3.resolve(path3.dirname(fullPath), cssPath),
2006
+ importedBy: fullPath,
2007
+ isLayout
2008
+ }));
2009
+ state.cssImports.set(fullPath, cssInfos);
2010
+ if (state.options.verbose) {
2011
+ console.log(`[cloudwerk] Found ${cssInfos.length} CSS import(s) in ${path3.relative(root, fullPath)}`);
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ })
2017
+ );
2018
+ }
2019
+ await scanDir(appDir);
2020
+ }
1597
2021
  function cloudwerkPlugin(options = {}) {
1598
2022
  let state = null;
1599
2023
  let server = null;
@@ -1672,6 +2096,62 @@ function cloudwerkPlugin(options = {}) {
1672
2096
  console.log(`[cloudwerk] Found ${state.serviceManifest.services.length} service(s)`);
1673
2097
  }
1674
2098
  }
2099
+ async function buildImageManifestIfExists(root) {
2100
+ if (!state) {
2101
+ throw new Error("Plugin state not initialized");
2102
+ }
2103
+ const imagesPath = path3.resolve(root, state.options.appDir, IMAGES_DIR);
2104
+ try {
2105
+ await fs2.promises.access(imagesPath);
2106
+ } catch {
2107
+ state.imageScanResult = null;
2108
+ state.imageManifest = null;
2109
+ return;
2110
+ }
2111
+ state.imageScanResult = await scanImages(
2112
+ path3.resolve(root, state.options.appDir),
2113
+ { extensions: state.options.config.extensions }
2114
+ );
2115
+ state.imageManifest = buildImageManifest(
2116
+ state.imageScanResult,
2117
+ root
2118
+ );
2119
+ if (state.options.verbose && state.imageManifest.images.length > 0) {
2120
+ console.log(`[cloudwerk] Found ${state.imageManifest.images.length} image(s)`);
2121
+ }
2122
+ }
2123
+ async function buildAuthManifestIfExists(root) {
2124
+ if (!state) {
2125
+ throw new Error("Plugin state not initialized");
2126
+ }
2127
+ const authPath = path3.resolve(root, state.options.appDir, AUTH_DIR);
2128
+ try {
2129
+ await fs2.promises.access(authPath);
2130
+ } catch {
2131
+ state.authScanResult = null;
2132
+ state.authManifest = null;
2133
+ return;
2134
+ }
2135
+ state.authScanResult = await scanAuth(
2136
+ path3.resolve(root, state.options.appDir),
2137
+ { extensions: state.options.config.extensions }
2138
+ );
2139
+ state.authManifest = await buildAuthManifestWithModules(state.authScanResult);
2140
+ state.serverEntryCache = null;
2141
+ if (state.options.verbose && state.authManifest.providers.length > 0) {
2142
+ console.log(`[cloudwerk] Found ${state.authManifest.providers.length} auth provider(s)`);
2143
+ for (const provider of state.authManifest.providers) {
2144
+ console.log(`[cloudwerk] - ${provider.id} (${provider.type})`);
2145
+ }
2146
+ }
2147
+ }
2148
+ function isAuthFile(filePath) {
2149
+ if (!state) return false;
2150
+ const authDir = path3.resolve(state.options.root, state.options.appDir, AUTH_DIR);
2151
+ if (!filePath.startsWith(authDir)) return false;
2152
+ const ext = path3.extname(filePath);
2153
+ return state.options.config.extensions.includes(ext);
2154
+ }
1675
2155
  function isRouteFile(filePath) {
1676
2156
  if (!state) return false;
1677
2157
  const appDir = path3.resolve(state.options.root, state.options.appDir);
@@ -1699,6 +2179,15 @@ function cloudwerkPlugin(options = {}) {
1699
2179
  if (nameWithoutExt !== SERVICE_FILE_NAME) return false;
1700
2180
  return state.options.config.extensions.includes(ext);
1701
2181
  }
2182
+ function isImageFile(filePath) {
2183
+ if (!state) return false;
2184
+ const imagesDir = path3.resolve(state.options.root, state.options.appDir, IMAGES_DIR);
2185
+ if (!filePath.startsWith(imagesDir)) return false;
2186
+ const relativePath = path3.relative(imagesDir, filePath);
2187
+ if (relativePath.includes(path3.sep)) return false;
2188
+ const ext = path3.extname(filePath);
2189
+ return state.options.config.extensions.includes(ext);
2190
+ }
1702
2191
  function invalidateVirtualModules() {
1703
2192
  if (!server) return;
1704
2193
  const idsToInvalidate = [
@@ -1793,14 +2282,22 @@ function cloudwerkPlugin(options = {}) {
1793
2282
  queueScanResult: null,
1794
2283
  serviceManifest: null,
1795
2284
  serviceScanResult: null,
2285
+ imageManifest: null,
2286
+ imageScanResult: null,
2287
+ authManifest: null,
2288
+ authScanResult: null,
1796
2289
  clientComponents: /* @__PURE__ */ new Map(),
2290
+ cssImports: /* @__PURE__ */ new Map(),
1797
2291
  serverEntryCache: null,
1798
2292
  clientEntryCache: null
1799
2293
  };
1800
2294
  await buildManifest(root);
1801
2295
  await buildQueueManifestIfExists(root);
1802
2296
  await buildServiceManifestIfExists(root);
2297
+ await buildImageManifestIfExists(root);
2298
+ await buildAuthManifestIfExists(root);
1803
2299
  await scanClientComponents(root, state);
2300
+ await scanCssImports(root, state);
1804
2301
  },
1805
2302
  /**
1806
2303
  * Configure the dev server with file watching.
@@ -1835,6 +2332,22 @@ function cloudwerkPlugin(options = {}) {
1835
2332
  await buildServiceManifestIfExists(state.options.root);
1836
2333
  invalidateVirtualModules();
1837
2334
  }
2335
+ if (isImageFile(filePath)) {
2336
+ const imagesDir = path3.resolve(root, state.options.appDir, IMAGES_DIR);
2337
+ if (state?.options.verbose) {
2338
+ console.log(`[cloudwerk] Image added: ${path3.relative(imagesDir, filePath)}`);
2339
+ }
2340
+ await buildImageManifestIfExists(state.options.root);
2341
+ invalidateVirtualModules();
2342
+ }
2343
+ if (isAuthFile(filePath)) {
2344
+ const authDir = path3.resolve(root, state.options.appDir, AUTH_DIR);
2345
+ if (state?.options.verbose) {
2346
+ console.log(`[cloudwerk] Auth file added: ${path3.relative(authDir, filePath)}`);
2347
+ }
2348
+ await buildAuthManifestIfExists(state.options.root);
2349
+ invalidateVirtualModules();
2350
+ }
1838
2351
  });
1839
2352
  devServer.watcher.on("unlink", async (filePath) => {
1840
2353
  if (isRouteFile(filePath)) {
@@ -1860,6 +2373,22 @@ function cloudwerkPlugin(options = {}) {
1860
2373
  await buildServiceManifestIfExists(state.options.root);
1861
2374
  invalidateVirtualModules();
1862
2375
  }
2376
+ if (isImageFile(filePath)) {
2377
+ const imagesDir = path3.resolve(root, state.options.appDir, IMAGES_DIR);
2378
+ if (state?.options.verbose) {
2379
+ console.log(`[cloudwerk] Image removed: ${path3.relative(imagesDir, filePath)}`);
2380
+ }
2381
+ await buildImageManifestIfExists(state.options.root);
2382
+ invalidateVirtualModules();
2383
+ }
2384
+ if (isAuthFile(filePath)) {
2385
+ const authDir = path3.resolve(root, state.options.appDir, AUTH_DIR);
2386
+ if (state?.options.verbose) {
2387
+ console.log(`[cloudwerk] Auth file removed: ${path3.relative(authDir, filePath)}`);
2388
+ }
2389
+ await buildAuthManifestIfExists(state.options.root);
2390
+ invalidateVirtualModules();
2391
+ }
1863
2392
  });
1864
2393
  devServer.watcher.on("change", async (filePath) => {
1865
2394
  if (isRouteFile(filePath)) {
@@ -1885,6 +2414,22 @@ function cloudwerkPlugin(options = {}) {
1885
2414
  await buildServiceManifestIfExists(state.options.root);
1886
2415
  invalidateVirtualModules();
1887
2416
  }
2417
+ if (isImageFile(filePath)) {
2418
+ const imagesDir = path3.resolve(root, state.options.appDir, IMAGES_DIR);
2419
+ if (state?.options.verbose) {
2420
+ console.log(`[cloudwerk] Image changed: ${path3.relative(imagesDir, filePath)}`);
2421
+ }
2422
+ await buildImageManifestIfExists(state.options.root);
2423
+ invalidateVirtualModules();
2424
+ }
2425
+ if (isAuthFile(filePath)) {
2426
+ const authDir = path3.resolve(root, state.options.appDir, AUTH_DIR);
2427
+ if (state?.options.verbose) {
2428
+ console.log(`[cloudwerk] Auth file changed: ${path3.relative(authDir, filePath)}`);
2429
+ }
2430
+ await buildAuthManifestIfExists(state.options.root);
2431
+ invalidateVirtualModules();
2432
+ }
1888
2433
  const wranglerPath2 = findWranglerTomlPath(root);
1889
2434
  if (wranglerPath2 && filePath === wranglerPath2) {
1890
2435
  if (verbose) {
@@ -1932,7 +2477,9 @@ function cloudwerkPlugin(options = {}) {
1932
2477
  state.options,
1933
2478
  {
1934
2479
  queueManifest: state.queueManifest,
1935
- serviceManifest: state.serviceManifest
2480
+ serviceManifest: state.serviceManifest,
2481
+ authManifest: state.authManifest,
2482
+ cssImports: state.cssImports
1936
2483
  }
1937
2484
  );
1938
2485
  }
@@ -1945,6 +2492,7 @@ function cloudwerkPlugin(options = {}) {
1945
2492
  if (!state.clientEntryCache) {
1946
2493
  state.clientEntryCache = generateClientEntry(
1947
2494
  state.clientComponents,
2495
+ state.cssImports,
1948
2496
  state.options
1949
2497
  );
1950
2498
  }
@@ -2001,6 +2549,13 @@ function cloudwerkPlugin(options = {}) {
2001
2549
  "getDurableObject",
2002
2550
  "hasDurableObject",
2003
2551
  "getDurableObjectNames",
2552
+ "images",
2553
+ "getImages",
2554
+ "hasImages",
2555
+ "getImagesNames",
2556
+ "registerLocalImages",
2557
+ "unregisterLocalImages",
2558
+ "clearLocalImages",
2004
2559
  "createLazyBinding"
2005
2560
  ];
2006
2561
  const runtimeImports = [];
@@ -2031,6 +2586,22 @@ function cloudwerkPlugin(options = {}) {
2031
2586
  return parts.join("\n");
2032
2587
  });
2033
2588
  }
2589
+ const { isLayout, isPage } = isLayoutOrPage(id);
2590
+ if (isLayout || isPage) {
2591
+ const cssImportPaths = extractCssImports(transformedCode);
2592
+ if (cssImportPaths.length > 0) {
2593
+ const cssInfos = cssImportPaths.map((cssPath) => ({
2594
+ absolutePath: path3.resolve(path3.dirname(id), cssPath),
2595
+ importedBy: id,
2596
+ isLayout
2597
+ }));
2598
+ state.cssImports.set(id, cssInfos);
2599
+ state.clientEntryCache = null;
2600
+ if (state.options.verbose) {
2601
+ console.log(`[cloudwerk] Detected ${cssInfos.length} CSS import(s) in ${path3.relative(state.options.root, id)}`);
2602
+ }
2603
+ }
2604
+ }
2034
2605
  if (hasUseClientDirective(transformedCode)) {
2035
2606
  const componentId = generateComponentId(id, state.options.root);
2036
2607
  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.5",
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",