litestar-vite-plugin 0.14.0 → 0.15.0-alpha.2

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