litestar-vite-plugin 0.13.2 → 0.15.0-alpha.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/dist/js/index.js CHANGED
@@ -1,14 +1,24 @@
1
+ import { exec } from "node:child_process";
2
+ import { createHash } from "node:crypto";
1
3
  import fs from "node:fs";
2
4
  import path from "node:path";
3
5
  import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
4
7
  import colors from "picocolors";
5
8
  import { loadEnv } from "vite";
6
9
  import fullReload from "vite-plugin-full-reload";
10
+ import { resolveInstallHint } from "./install-hint.js";
11
+ import { checkBackendAvailability, loadLitestarMeta } from "./litestar-meta.js";
12
+ const execAsync = promisify(exec);
7
13
  let exitHandlersBound = false;
8
14
  const refreshPaths = ["src/**", "resources/**", "assets/**"].filter((path2) => fs.existsSync(path2.replace(/\*\*$/, "")));
9
15
  function litestar(config) {
10
16
  const pluginConfig = resolvePluginConfig(config);
11
- return [resolveLitestarPlugin(pluginConfig), ...resolveFullReloadConfig(pluginConfig)];
17
+ const plugins = [resolveLitestarPlugin(pluginConfig), ...resolveFullReloadConfig(pluginConfig)];
18
+ if (pluginConfig.types !== false && pluginConfig.types.enabled) {
19
+ plugins.push(resolveTypeGenerationPlugin(pluginConfig.types));
20
+ }
21
+ return plugins;
12
22
  }
13
23
  async function findIndexHtmlPath(server, pluginConfig) {
14
24
  if (!pluginConfig.autoDetectIndex) {
@@ -36,6 +46,7 @@ function resolveLitestarPlugin(pluginConfig) {
36
46
  let viteDevServerUrl;
37
47
  let resolvedConfig;
38
48
  let userConfig;
49
+ let litestarMeta = {};
39
50
  const defaultAliases = {
40
51
  "@": `/${pluginConfig.resourceDirectory.replace(/^\/+/, "").replace(/\/+$/, "")}/`
41
52
  };
@@ -46,11 +57,12 @@ function resolveLitestarPlugin(pluginConfig) {
46
57
  userConfig = config;
47
58
  const ssr = !!userConfig.build?.ssr;
48
59
  const env = loadEnv(mode, userConfig.envDir || process.cwd(), "");
49
- const assetUrl = env.ASSET_URL || pluginConfig.assetUrl;
60
+ const assetUrl = normalizeAssetUrl(env.ASSET_URL || pluginConfig.assetUrl);
50
61
  const serverConfig = command === "serve" ? resolveDevelopmentEnvironmentServerConfig(pluginConfig.detectTls) ?? resolveEnvironmentServerConfig(env) : void 0;
62
+ const devBase = pluginConfig.assetUrl.startsWith("/") ? pluginConfig.assetUrl : pluginConfig.assetUrl.replace(/\/+$/, "");
51
63
  ensureCommandShouldRunInEnvironment(command, env);
52
64
  return {
53
- base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig, assetUrl) : pluginConfig.assetUrl),
65
+ base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig, assetUrl) : devBase),
54
66
  publicDir: userConfig.publicDir ?? false,
55
67
  clearScreen: false,
56
68
  build: {
@@ -64,6 +76,27 @@ function resolveLitestarPlugin(pluginConfig) {
64
76
  },
65
77
  server: {
66
78
  origin: userConfig.server?.origin ?? "__litestar_vite_placeholder__",
79
+ // Auto-configure HMR to use a path that routes through Litestar proxy
80
+ // Note: Vite automatically prepends `base` to `hmr.path`, so we just use "vite-hmr"
81
+ // Result: base="/static/" + path="vite-hmr" = "/static/vite-hmr"
82
+ hmr: userConfig.server?.hmr === false ? false : {
83
+ path: "vite-hmr",
84
+ ...serverConfig?.hmr ?? {},
85
+ ...userConfig.server?.hmr === true ? {} : userConfig.server?.hmr
86
+ },
87
+ // Auto-configure proxy to forward API requests to Litestar backend
88
+ // This allows the app to work when accessing Vite directly (not through Litestar proxy)
89
+ // Only proxies /api and /schema routes - everything else is handled by Vite
90
+ proxy: userConfig.server?.proxy ?? (env.APP_URL ? {
91
+ "/api": {
92
+ target: env.APP_URL,
93
+ changeOrigin: true
94
+ },
95
+ "/schema": {
96
+ target: env.APP_URL,
97
+ changeOrigin: true
98
+ }
99
+ } : void 0),
67
100
  ...process.env.VITE_ALLOW_REMOTE ? {
68
101
  host: userConfig.server?.host ?? "0.0.0.0",
69
102
  port: userConfig.server?.port ?? (env.VITE_PORT ? Number.parseInt(env.VITE_PORT) : 5173),
@@ -71,10 +104,6 @@ function resolveLitestarPlugin(pluginConfig) {
71
104
  } : void 0,
72
105
  ...serverConfig ? {
73
106
  host: userConfig.server?.host ?? serverConfig.host,
74
- hmr: userConfig.server?.hmr === false ? false : {
75
- ...serverConfig.hmr,
76
- ...userConfig.server?.hmr === true ? {} : userConfig.server?.hmr
77
- },
78
107
  https: userConfig.server?.https ?? serverConfig.https
79
108
  } : void 0
80
109
  },
@@ -97,7 +126,7 @@ function resolveLitestarPlugin(pluginConfig) {
97
126
  // appType: 'spa', // Try adding this - might simplify things if appropriate
98
127
  };
99
128
  },
100
- configResolved(config) {
129
+ async configResolved(config) {
101
130
  resolvedConfig = config;
102
131
  if (resolvedConfig.command === "serve" && resolvedConfig.base && !resolvedConfig.base.endsWith("/")) {
103
132
  resolvedConfig = {
@@ -105,6 +134,8 @@ function resolveLitestarPlugin(pluginConfig) {
105
134
  base: `${resolvedConfig.base}/`
106
135
  };
107
136
  }
137
+ const hint = pluginConfig.types !== false ? pluginConfig.types.routesPath : void 0;
138
+ litestarMeta = await loadLitestarMeta(resolvedConfig, hint);
108
139
  },
109
140
  transform(code, id) {
110
141
  if (resolvedConfig.command === "serve" && code.includes("__litestar_vite_placeholder__")) {
@@ -116,6 +147,9 @@ function resolveLitestarPlugin(pluginConfig) {
116
147
  async configureServer(server) {
117
148
  const envDir = resolvedConfig.envDir || process.cwd();
118
149
  const appUrl = loadEnv(resolvedConfig.mode, envDir, "APP_URL").APP_URL ?? "undefined";
150
+ if (pluginConfig.hotFile && !path.isAbsolute(pluginConfig.hotFile)) {
151
+ pluginConfig.hotFile = path.resolve(server.config.root, pluginConfig.hotFile);
152
+ }
119
153
  const initialIndexPath = await findIndexHtmlPath(server, pluginConfig);
120
154
  server.httpServer?.once("listening", () => {
121
155
  const address = server.httpServer?.address();
@@ -124,9 +158,11 @@ function resolveLitestarPlugin(pluginConfig) {
124
158
  viteDevServerUrl = userConfig.server?.origin ? userConfig.server.origin : resolveDevServerUrl(address, server.config, userConfig);
125
159
  fs.mkdirSync(path.dirname(pluginConfig.hotFile), { recursive: true });
126
160
  fs.writeFileSync(pluginConfig.hotFile, viteDevServerUrl);
127
- setTimeout(() => {
161
+ setTimeout(async () => {
162
+ const version = litestarMeta.litestarVersion ?? process.env.LITESTAR_VERSION ?? "unknown";
163
+ const backendStatus = await checkBackendAvailability(appUrl);
128
164
  resolvedConfig.logger.info(`
129
- ${colors.red(`${colors.bold("LITESTAR")} ${litestarVersion()}`)} ${colors.dim("plugin")} ${colors.bold(`v${pluginVersion()}`)}`);
165
+ ${colors.red(`${colors.bold("LITESTAR")} ${version}`)} ${colors.dim("plugin")} ${colors.bold(`v${pluginVersion()}`)}`);
130
166
  resolvedConfig.logger.info("");
131
167
  if (initialIndexPath) {
132
168
  resolvedConfig.logger.info(
@@ -136,8 +172,42 @@ function resolveLitestarPlugin(pluginConfig) {
136
172
  resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Index Mode")}: Litestar (Plugin will serve placeholder for /index.html)`);
137
173
  }
138
174
  resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Dev Server")}: ${colors.cyan(viteDevServerUrl)}`);
139
- resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("App URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))}`);
175
+ if (backendStatus.available) {
176
+ resolvedConfig.logger.info(
177
+ ` ${colors.green("\u279C")} ${colors.bold("App URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))} ${colors.green("\u2713")}`
178
+ );
179
+ } else {
180
+ resolvedConfig.logger.info(
181
+ ` ${colors.yellow("\u279C")} ${colors.bold("App URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))} ${colors.yellow("\u26A0")}`
182
+ );
183
+ }
140
184
  resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Assets Base")}: ${colors.cyan(resolvedConfig.base)}`);
185
+ if (pluginConfig.types !== false && pluginConfig.types.enabled) {
186
+ const openapiExists = fs.existsSync(path.resolve(process.cwd(), pluginConfig.types.openapiPath));
187
+ const routesExists = fs.existsSync(path.resolve(process.cwd(), pluginConfig.types.routesPath));
188
+ if (openapiExists || routesExists) {
189
+ resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Type Gen")}: ${colors.green("enabled")} ${colors.dim(`\u2192 ${pluginConfig.types.output}`)}`);
190
+ } else {
191
+ resolvedConfig.logger.info(` ${colors.yellow("\u279C")} ${colors.bold("Type Gen")}: ${colors.yellow("waiting")} ${colors.dim("(no schema files yet)")}`);
192
+ }
193
+ }
194
+ if (!backendStatus.available) {
195
+ resolvedConfig.logger.info("");
196
+ resolvedConfig.logger.info(` ${colors.yellow("\u26A0")} ${colors.bold("Backend Status")}`);
197
+ if (backendStatus.error === "APP_URL not configured") {
198
+ resolvedConfig.logger.info(` ${colors.dim("APP_URL environment variable is not set.")}`);
199
+ resolvedConfig.logger.info(` ${colors.dim("Set APP_URL in your .env file or environment.")}`);
200
+ } else {
201
+ resolvedConfig.logger.info(` ${colors.dim(backendStatus.error ?? "Backend not available")}`);
202
+ resolvedConfig.logger.info("");
203
+ resolvedConfig.logger.info(` ${colors.bold("To start your Litestar app:")}`);
204
+ resolvedConfig.logger.info(` ${colors.cyan("litestar run")} ${colors.dim("or")} ${colors.cyan("uvicorn app:app --reload")}`);
205
+ }
206
+ resolvedConfig.logger.info("");
207
+ resolvedConfig.logger.info(` ${colors.dim("The Vite dev server is running and will serve assets.")}`);
208
+ resolvedConfig.logger.info(` ${colors.dim("Start your Litestar backend to view the full application.")}`);
209
+ }
210
+ resolvedConfig.logger.info("");
141
211
  }, 100);
142
212
  }
143
213
  });
@@ -153,53 +223,49 @@ function resolveLitestarPlugin(pluginConfig) {
153
223
  process.on("SIGHUP", () => process.exit());
154
224
  exitHandlersBound = true;
155
225
  }
156
- return () => {
157
- server.middlewares.use(async (req, res, next) => {
158
- const indexPath = await findIndexHtmlPath(server, pluginConfig);
159
- if (indexPath && (req.url === "/" || req.url === "/index.html")) {
160
- const currentUrl = req.url;
161
- try {
162
- const htmlContent = await fs.promises.readFile(indexPath, "utf-8");
163
- const transformedHtml = await server.transformIndexHtml(req.originalUrl ?? currentUrl, htmlContent, req.originalUrl);
164
- res.statusCode = 200;
165
- res.setHeader("Content-Type", "text/html");
166
- res.end(transformedHtml);
167
- return;
168
- } catch (e) {
169
- resolvedConfig.logger.error(`Error serving index.html from ${indexPath}: ${e instanceof Error ? e.message : e}`);
170
- next(e);
171
- return;
172
- }
173
- }
174
- if (!indexPath && req.url === "/index.html") {
175
- try {
176
- const placeholderPath = path.join(dirname(), "dev-server-index.html");
177
- const placeholderContent = await fs.promises.readFile(placeholderPath, "utf-8");
178
- res.statusCode = 200;
179
- res.setHeader("Content-Type", "text/html");
180
- res.end(placeholderContent.replace(/{{ APP_URL }}/g, appUrl));
181
- } catch (e) {
182
- resolvedConfig.logger.error(`Error serving placeholder index.html: ${e instanceof Error ? e.message : e}`);
183
- res.statusCode = 404;
184
- res.end("Not Found (Error loading placeholder)");
185
- }
226
+ server.middlewares.use(async (req, res, next) => {
227
+ const indexPath = await findIndexHtmlPath(server, pluginConfig);
228
+ if (indexPath && (req.url === "/" || req.url === "/index.html")) {
229
+ const currentUrl = req.url;
230
+ try {
231
+ const htmlContent = await fs.promises.readFile(indexPath, "utf-8");
232
+ const transformedHtml = await server.transformIndexHtml(req.originalUrl ?? currentUrl, htmlContent, req.originalUrl);
233
+ res.statusCode = 200;
234
+ res.setHeader("Content-Type", "text/html");
235
+ res.end(transformedHtml);
236
+ return;
237
+ } catch (e) {
238
+ resolvedConfig.logger.error(`Error serving index.html from ${indexPath}: ${e instanceof Error ? e.message : e}`);
239
+ next(e);
186
240
  return;
187
241
  }
188
- next();
189
- });
190
- };
242
+ }
243
+ if (!indexPath && req.url === "/index.html") {
244
+ try {
245
+ const placeholderPath = path.join(dirname(), "dev-server-index.html");
246
+ const placeholderContent = await fs.promises.readFile(placeholderPath, "utf-8");
247
+ res.statusCode = 200;
248
+ res.setHeader("Content-Type", "text/html");
249
+ res.end(placeholderContent.replace(/{{ APP_URL }}/g, appUrl));
250
+ } catch (e) {
251
+ resolvedConfig.logger.error(`Error serving placeholder index.html: ${e instanceof Error ? e.message : e}`);
252
+ res.statusCode = 404;
253
+ res.end("Not Found (Error loading placeholder)");
254
+ }
255
+ return;
256
+ }
257
+ next();
258
+ });
191
259
  }
192
260
  };
193
261
  }
194
262
  function ensureCommandShouldRunInEnvironment(command, env) {
195
- const validEnvironmentNames = ["dev", "development", "local", "docker"];
263
+ const allowedDevModes = ["dev", "development", "local", "docker"];
196
264
  if (command === "build" || env.LITESTAR_BYPASS_ENV_CHECK === "1") {
197
265
  return;
198
266
  }
199
- if (typeof env.LITESTAR_MODE !== "undefined" && validEnvironmentNames.some((e) => e === env.LITESTAR_MODE)) {
200
- throw Error(
201
- "You should only run Vite dev server when Litestar is development mode. You should build your assets for production instead. To disable this ENV check you may set LITESTAR_BYPASS_ENV_CHECK=1"
202
- );
267
+ if (typeof env.LITESTAR_MODE !== "undefined" && !allowedDevModes.includes(env.LITESTAR_MODE)) {
268
+ throw Error("Run the Vite dev server only in development. Set LITESTAR_MODE=dev/development/local/docker or set LITESTAR_BYPASS_ENV_CHECK=1 to skip this check.");
203
269
  }
204
270
  if (typeof env.CI !== "undefined") {
205
271
  throw Error(
@@ -207,9 +273,6 @@ function ensureCommandShouldRunInEnvironment(command, env) {
207
273
  );
208
274
  }
209
275
  }
210
- function litestarVersion() {
211
- return "";
212
- }
213
276
  function pluginVersion() {
214
277
  try {
215
278
  return JSON.parse(fs.readFileSync(path.join(dirname(), "../package.json")).toString())?.version;
@@ -217,10 +280,26 @@ function pluginVersion() {
217
280
  return "";
218
281
  }
219
282
  }
283
+ function loadPythonDefaults() {
284
+ const configPath = process.env.LITESTAR_VITE_CONFIG_PATH;
285
+ if (!configPath) {
286
+ return null;
287
+ }
288
+ if (!fs.existsSync(configPath)) {
289
+ return null;
290
+ }
291
+ try {
292
+ const data = JSON.parse(fs.readFileSync(configPath, "utf8"));
293
+ return data;
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
220
298
  function resolvePluginConfig(config) {
221
299
  if (typeof config === "undefined") {
222
300
  throw new Error("litestar-vite-plugin: missing configuration.");
223
301
  }
302
+ const pythonDefaults = loadPythonDefaults();
224
303
  const resolvedConfig = typeof config === "string" || Array.isArray(config) ? { input: config, ssr: config } : config;
225
304
  if (typeof resolvedConfig.input === "undefined") {
226
305
  throw new Error('litestar-vite-plugin: missing configuration for "input".');
@@ -243,18 +322,51 @@ function resolvePluginConfig(config) {
243
322
  if (resolvedConfig.refresh === true) {
244
323
  resolvedConfig.refresh = [{ paths: refreshPaths }];
245
324
  }
325
+ let typesConfig = false;
326
+ if (typeof resolvedConfig.types === "undefined" && pythonDefaults?.types) {
327
+ typesConfig = {
328
+ enabled: pythonDefaults.types.enabled,
329
+ output: pythonDefaults.types.output,
330
+ openapiPath: pythonDefaults.types.openapiPath,
331
+ routesPath: pythonDefaults.types.routesPath,
332
+ generateZod: pythonDefaults.types.generateZod,
333
+ generateSdk: pythonDefaults.types.generateSdk,
334
+ debounce: 300
335
+ };
336
+ } else if (resolvedConfig.types === true || typeof resolvedConfig.types === "undefined") {
337
+ typesConfig = {
338
+ enabled: true,
339
+ output: "src/generated/types",
340
+ openapiPath: "src/generated/openapi.json",
341
+ routesPath: "src/generated/routes.json",
342
+ generateZod: false,
343
+ generateSdk: false,
344
+ debounce: 300
345
+ };
346
+ } else if (typeof resolvedConfig.types === "object" && resolvedConfig.types !== null) {
347
+ typesConfig = {
348
+ enabled: resolvedConfig.types.enabled ?? true,
349
+ output: resolvedConfig.types.output ?? "src/generated/types",
350
+ openapiPath: resolvedConfig.types.openapiPath ?? "src/generated/openapi.json",
351
+ routesPath: resolvedConfig.types.routesPath ?? "src/generated/routes.json",
352
+ generateZod: resolvedConfig.types.generateZod ?? false,
353
+ generateSdk: resolvedConfig.types.generateSdk ?? false,
354
+ debounce: resolvedConfig.types.debounce ?? 300
355
+ };
356
+ }
246
357
  return {
247
358
  input: resolvedConfig.input,
248
- assetUrl: resolvedConfig.assetUrl ?? "static",
249
- resourceDirectory: resolvedConfig.resourceDirectory ?? "resources",
250
- bundleDirectory: resolvedConfig.bundleDirectory ?? "public",
359
+ assetUrl: normalizeAssetUrl(resolvedConfig.assetUrl ?? pythonDefaults?.assetUrl ?? "/static/"),
360
+ resourceDirectory: resolvedConfig.resourceDirectory ?? pythonDefaults?.resourceDir ?? "resources",
361
+ bundleDirectory: resolvedConfig.bundleDirectory ?? pythonDefaults?.bundleDir ?? "public",
251
362
  ssr: resolvedConfig.ssr ?? resolvedConfig.input,
252
- ssrOutputDirectory: resolvedConfig.ssrOutputDirectory ?? path.join(resolvedConfig.resourceDirectory ?? "resources", "bootstrap/ssr"),
363
+ ssrOutputDirectory: resolvedConfig.ssrOutputDirectory ?? pythonDefaults?.ssrOutDir ?? path.join(resolvedConfig.resourceDirectory ?? pythonDefaults?.resourceDir ?? "resources", "bootstrap/ssr"),
253
364
  refresh: resolvedConfig.refresh ?? false,
254
365
  hotFile: resolvedConfig.hotFile ?? path.join(resolvedConfig.bundleDirectory ?? "public", "hot"),
255
366
  detectTls: resolvedConfig.detectTls ?? false,
256
367
  autoDetectIndex: resolvedConfig.autoDetectIndex ?? true,
257
- transformOnServe: resolvedConfig.transformOnServe ?? ((code) => code)
368
+ transformOnServe: resolvedConfig.transformOnServe ?? ((code) => code),
369
+ types: typesConfig
258
370
  };
259
371
  }
260
372
  function resolveBase(config, assetUrl) {
@@ -294,6 +406,248 @@ function resolveFullReloadConfig({ refresh: config }) {
294
406
  return plugin;
295
407
  });
296
408
  }
409
+ function debounce(func, wait) {
410
+ let timeout = null;
411
+ return (...args) => {
412
+ if (timeout) {
413
+ clearTimeout(timeout);
414
+ }
415
+ timeout = setTimeout(() => func(...args), wait);
416
+ };
417
+ }
418
+ async function emitRouteTypes(routesPath, outputDir) {
419
+ const contents = await fs.promises.readFile(routesPath, "utf-8");
420
+ const json = JSON.parse(contents);
421
+ const outDir = path.resolve(process.cwd(), outputDir);
422
+ await fs.promises.mkdir(outDir, { recursive: true });
423
+ const outFile = path.join(outDir, "routes.ts");
424
+ const banner = `// AUTO-GENERATED by litestar-vite. Do not edit.
425
+ /* eslint-disable */
426
+
427
+ `;
428
+ const routesData = json.routes || json;
429
+ const routeNames = Object.keys(routesData);
430
+ const routeNameType = routeNames.length > 0 ? routeNames.map((n) => `"${n}"`).join(" | ") : "never";
431
+ const routeParamTypes = [];
432
+ for (const [name, data] of Object.entries(routesData)) {
433
+ const routeData = data;
434
+ if (routeData.parameters && routeData.parameters.length > 0) {
435
+ const params = routeData.parameters.map((p) => `${p}: string | number`).join("; ");
436
+ routeParamTypes.push(` "${name}": { ${params} }`);
437
+ } else {
438
+ routeParamTypes.push(` "${name}": Record<string, never>`);
439
+ }
440
+ }
441
+ const body = `/**
442
+ * AUTO-GENERATED by litestar-vite.
443
+ *
444
+ * Exports:
445
+ * - routesMeta: full route metadata
446
+ * - routes: name -> uri map
447
+ * - serverRoutes: alias of routes for clarity in apps
448
+ * - route(): type-safe URL generator
449
+ * - hasRoute(): type guard
450
+ * - csrf helpers re-exported from litestar-vite-plugin/helpers
451
+ *
452
+ * @see https://litestar-vite.litestar.dev/
453
+ */
454
+ export const routesMeta = ${JSON.stringify(json, null, 2)} as const
455
+
456
+ /**
457
+ * Route name to URI mapping.
458
+ */
459
+ export const routes = ${JSON.stringify(Object.fromEntries(Object.entries(routesData).map(([name, data]) => [name, data.uri])), null, 2)} as const
460
+
461
+ /**
462
+ * Alias for server-injected route map (more descriptive for consumers).
463
+ */
464
+ export const serverRoutes = routes
465
+
466
+ /**
467
+ * All available route names.
468
+ */
469
+ export type RouteName = ${routeNameType}
470
+
471
+ /**
472
+ * Parameter types for each route.
473
+ */
474
+ export interface RouteParams {
475
+ ${routeParamTypes.join("\n")}
476
+ }
477
+
478
+ /**
479
+ * Generate a URL for a named route with type-safe parameters.
480
+ *
481
+ * @param name - The route name
482
+ * @param params - Route parameters (required if route has path parameters)
483
+ * @returns The generated URL
484
+ *
485
+ * @example
486
+ * \`\`\`ts
487
+ * import { route } from '@/generated/routes'
488
+ *
489
+ * // Route without parameters
490
+ * route('home') // "/"
491
+ *
492
+ * // Route with parameters
493
+ * route('user:detail', { user_id: 123 }) // "/users/123"
494
+ * \`\`\`
495
+ */
496
+ export function route<T extends RouteName>(
497
+ name: T,
498
+ ...args: RouteParams[T] extends Record<string, never> ? [] : [params: RouteParams[T]]
499
+ ): string {
500
+ let uri = routes[name] as string
501
+ const params = args[0] as Record<string, string | number> | undefined
502
+
503
+ if (params) {
504
+ for (const [key, value] of Object.entries(params)) {
505
+ // Handle both {param} and {param:type} syntax
506
+ uri = uri.replace(new RegExp(\`\\\\{\${key}(?::[^}]+)?\\\\}\`, "g"), String(value))
507
+ }
508
+ }
509
+
510
+ return uri
511
+ }
512
+
513
+ /**
514
+ * Check if a route name exists.
515
+ */
516
+ export function hasRoute(name: string): name is RouteName {
517
+ return name in routes
518
+ }
519
+
520
+ declare global {
521
+ interface Window {
522
+ /**
523
+ * Fully-typed route metadata injected by Litestar.
524
+ */
525
+ __LITESTAR_ROUTES__?: typeof routesMeta
526
+ /**
527
+ * Simple route map (name -> uri) for legacy consumers.
528
+ */
529
+ routes?: typeof routes
530
+ serverRoutes?: typeof serverRoutes
531
+ }
532
+ // eslint-disable-next-line no-var
533
+ var routes: typeof routes | undefined
534
+ var serverRoutes: typeof serverRoutes | undefined
535
+ }
536
+
537
+ // Re-export helper functions from litestar-vite-plugin
538
+ // These work with the routes defined above
539
+ export { getCsrfToken, csrfHeaders, csrfFetch } from "litestar-vite-plugin/helpers"
540
+ `;
541
+ await fs.promises.writeFile(outFile, `${banner}${body}`, "utf-8");
542
+ }
543
+ function resolveTypeGenerationPlugin(typesConfig) {
544
+ let lastTypesHash = null;
545
+ let lastRoutesHash = null;
546
+ let server = null;
547
+ let isGenerating = false;
548
+ let resolvedConfig = null;
549
+ async function runTypeGeneration() {
550
+ if (isGenerating) {
551
+ return false;
552
+ }
553
+ isGenerating = true;
554
+ const startTime = Date.now();
555
+ try {
556
+ const openapiPath = path.resolve(process.cwd(), typesConfig.openapiPath);
557
+ const routesPath = path.resolve(process.cwd(), typesConfig.routesPath);
558
+ let generated = false;
559
+ if (fs.existsSync(openapiPath)) {
560
+ if (resolvedConfig) {
561
+ resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("generating TypeScript types...")}`);
562
+ }
563
+ const args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
564
+ if (typesConfig.generateZod) {
565
+ args.push("--plugins", "@hey-api/schemas", "@hey-api/types");
566
+ }
567
+ if (typesConfig.generateSdk) {
568
+ args.push("--client", "fetch");
569
+ }
570
+ await execAsync(`npx ${args.join(" ")}`, {
571
+ cwd: process.cwd()
572
+ });
573
+ generated = true;
574
+ } else if (resolvedConfig) {
575
+ resolvedConfig.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("OpenAPI schema not found:")} ${typesConfig.openapiPath}`);
576
+ }
577
+ if (fs.existsSync(routesPath)) {
578
+ await emitRouteTypes(routesPath, typesConfig.output);
579
+ generated = true;
580
+ }
581
+ if (generated && resolvedConfig) {
582
+ const duration = Date.now() - startTime;
583
+ resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.green("TypeScript artifacts updated")} ${colors.dim(`in ${duration}ms`)}`);
584
+ }
585
+ if (generated && server) {
586
+ server.ws.send({
587
+ type: "custom",
588
+ event: "litestar:types-updated",
589
+ data: {
590
+ output: typesConfig.output,
591
+ timestamp: Date.now()
592
+ }
593
+ });
594
+ }
595
+ return true;
596
+ } catch (error) {
597
+ if (resolvedConfig) {
598
+ const message = error instanceof Error ? error.message : String(error);
599
+ if (message.includes("not found") || message.includes("ENOENT")) {
600
+ resolvedConfig.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("@hey-api/openapi-ts not installed")} - run: ${resolveInstallHint()}`);
601
+ } else {
602
+ resolvedConfig.logger.error(`${colors.cyan("litestar-vite")} ${colors.red("type generation failed:")} ${message}`);
603
+ }
604
+ }
605
+ return false;
606
+ } finally {
607
+ isGenerating = false;
608
+ }
609
+ }
610
+ const debouncedRunTypeGeneration = debounce(runTypeGeneration, typesConfig.debounce);
611
+ return {
612
+ name: "litestar-vite-types",
613
+ enforce: "pre",
614
+ configResolved(config) {
615
+ resolvedConfig = config;
616
+ },
617
+ configureServer(devServer) {
618
+ server = devServer;
619
+ if (typesConfig.enabled) {
620
+ resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("watching for schema changes:")} ${colors.yellow(typesConfig.openapiPath)}`);
621
+ }
622
+ },
623
+ async handleHotUpdate({ file }) {
624
+ if (!typesConfig.enabled) {
625
+ return;
626
+ }
627
+ const relativePath = path.relative(process.cwd(), file);
628
+ const openapiPath = typesConfig.openapiPath.replace(/^\.\//, "");
629
+ const routesPath = typesConfig.routesPath.replace(/^\.\//, "");
630
+ if (relativePath === openapiPath || relativePath === routesPath || file.endsWith(openapiPath) || file.endsWith(routesPath)) {
631
+ if (resolvedConfig) {
632
+ resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("schema changed:")} ${colors.yellow(relativePath)}`);
633
+ }
634
+ const newHash = await hashFile(file);
635
+ if (relativePath === openapiPath) {
636
+ if (lastTypesHash === newHash) return;
637
+ lastTypesHash = newHash;
638
+ } else {
639
+ if (lastRoutesHash === newHash) return;
640
+ lastRoutesHash = newHash;
641
+ }
642
+ debouncedRunTypeGeneration();
643
+ }
644
+ }
645
+ };
646
+ }
647
+ async function hashFile(filePath) {
648
+ const content = await fs.promises.readFile(filePath);
649
+ return createHash("sha1").update(content).digest("hex");
650
+ }
297
651
  function resolveDevServerUrl(address, config, userConfig) {
298
652
  const configHmrProtocol = typeof config.server.hmr === "object" ? config.server.hmr.protocol : null;
299
653
  const clientProtocol = configHmrProtocol ? configHmrProtocol === "wss" ? "https" : "http" : null;
@@ -303,7 +657,10 @@ function resolveDevServerUrl(address, config, userConfig) {
303
657
  const configHost = typeof config.server.host === "string" ? config.server.host : null;
304
658
  const remoteHost = process.env.VITE_ALLOW_REMOTE && !userConfig.server?.host ? "localhost" : null;
305
659
  const serverAddress = isIpv6(address) ? `[${address.address}]` : address.address;
306
- const host = configHmrHost ?? remoteHost ?? configHost ?? serverAddress;
660
+ let host = configHmrHost ?? remoteHost ?? configHost ?? serverAddress;
661
+ if (host === "0.0.0.0") {
662
+ host = "127.0.0.1";
663
+ }
307
664
  const configHmrClientPort = typeof config.server.hmr === "object" ? config.server.hmr.clientPort : null;
308
665
  const port = configHmrClientPort ?? address.port;
309
666
  return `${protocol}://${host}:${port}`;
@@ -403,6 +760,19 @@ function dirname() {
403
760
  return path.resolve(process.cwd(), "src/js/src");
404
761
  }
405
762
  }
763
+ function normalizeAssetUrl(url) {
764
+ const trimmed = url.trim();
765
+ if (trimmed === "") {
766
+ return "static";
767
+ }
768
+ const isExternal = trimmed.startsWith("http://") || trimmed.startsWith("https://");
769
+ if (isExternal) {
770
+ return trimmed.replace(/\/+$/, "");
771
+ }
772
+ const withLeading = trimmed.startsWith("/") ? `/${trimmed.replace(/^\/+/, "")}` : trimmed;
773
+ const withTrailing = withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
774
+ return withTrailing;
775
+ }
406
776
  export {
407
777
  litestar as default,
408
778
  refreshPaths