litestar-vite-plugin 0.15.0-alpha.4 → 0.15.0-alpha.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.
@@ -6,10 +6,10 @@
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
- * import { route, getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
9
+ * import { getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
10
10
  *
11
- * // Generate a URL for a named route
12
- * const url = route('user:detail', { user_id: 123 })
11
+ * // Get CSRF token
12
+ * const token = getCsrfToken()
13
13
  *
14
14
  * // Make a fetch request with CSRF token
15
15
  * await csrfFetch('/api/submit', {
@@ -18,9 +18,17 @@
18
18
  * })
19
19
  * ```
20
20
  *
21
+ * For type-safe routing, import from your generated routes file:
22
+ * ```ts
23
+ * import { route, routes, type RouteName } from '@/generated/routes'
24
+ *
25
+ * // Type-safe URL generation
26
+ * const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
27
+ * ```
28
+ *
21
29
  * @module
22
30
  */
23
31
  // CSRF utilities
24
32
  export { csrfFetch, csrfHeaders, getCsrfToken } from "./csrf.js";
25
- // Route utilities
26
- export { currentRoute, getRelativeUrlPath, getRoutes, isCurrentRoute, isRoute, LITESTAR, route, toRoute, } from "./routes.js";
33
+ // HTMX utilities
34
+ export { addDirective, registerHtmxExtension, setDebug as setHtmxDebug, swapJson } from "./htmx.js";
@@ -73,6 +73,13 @@ export interface PluginConfig {
73
73
  * @default 'public/dist'
74
74
  */
75
75
  bundleDirectory?: string;
76
+ /**
77
+ * Vite's public directory for static, unprocessed assets.
78
+ * Mirrors Vite's `publicDir` option.
79
+ *
80
+ * @default 'public'
81
+ */
82
+ publicDir?: string;
76
83
  /**
77
84
  * Litestar's public assets directory. These are the assets that Vite will serve when developing.
78
85
  *
@@ -121,18 +128,36 @@ export interface PluginConfig {
121
128
  /**
122
129
  * Enable and configure TypeScript type generation.
123
130
  *
124
- * When set to `true`, enables type generation with default settings.
125
- * When set to a TypesConfig object, enables type generation with custom settings.
131
+ * Configuration priority (highest to lowest):
132
+ * 1. Explicit vite.config.ts value - ALWAYS wins
133
+ * 2. .litestar.json value - used if no explicit config
134
+ * 3. Hardcoded defaults - fallback if nothing else
135
+ *
136
+ * When set to `"auto"` (recommended): reads all config from `.litestar.json`.
137
+ * If `.litestar.json` is missing, type generation is disabled.
138
+ *
139
+ * When set to `true`: enables type generation with hardcoded defaults.
140
+ * When set to `false`: disables type generation entirely.
141
+ * When set to a TypesConfig object: uses your explicit settings.
126
142
  *
127
- * Type generation creates TypeScript types from your Litestar OpenAPI schema
128
- * and route metadata using @hey-api/openapi-ts.
143
+ * When not specified (undefined): behaves like `"auto"` - reads from
144
+ * `.litestar.json` if present, otherwise disabled.
129
145
  *
130
146
  * @example
131
147
  * ```ts
132
- * // Simple enable
148
+ * // Recommended: auto-read from .litestar.json (simplest)
149
+ * litestar({ input: 'src/main.ts' })
150
+ *
151
+ * // Explicit auto mode
152
+ * litestar({ input: 'src/main.ts', types: 'auto' })
153
+ *
154
+ * // Force enable with hardcoded defaults (ignores .litestar.json)
133
155
  * litestar({ input: 'src/main.ts', types: true })
134
156
  *
135
- * // With custom config
157
+ * // Force disable
158
+ * litestar({ input: 'src/main.ts', types: false })
159
+ *
160
+ * // Manual override (ignores .litestar.json for types)
136
161
  * litestar({
137
162
  * input: 'src/main.ts',
138
163
  * types: {
@@ -143,9 +168,9 @@ export interface PluginConfig {
143
168
  * })
144
169
  * ```
145
170
  *
146
- * @default false
171
+ * @default undefined (auto-detect from .litestar.json)
147
172
  */
148
- types?: boolean | TypesConfig;
173
+ types?: boolean | "auto" | TypesConfig;
149
174
  /**
150
175
  * JavaScript runtime executor for package commands.
151
176
  * Used when running tools like @hey-api/openapi-ts.
package/dist/js/index.js CHANGED
@@ -12,12 +12,13 @@ import { checkBackendAvailability, loadLitestarMeta } from "./litestar-meta.js";
12
12
  import { debounce } from "./shared/debounce.js";
13
13
  const execAsync = promisify(exec);
14
14
  let exitHandlersBound = false;
15
+ let warnedMissingRuntimeConfig = false;
15
16
  const refreshPaths = ["src/**", "resources/**", "assets/**"].filter((path2) => fs.existsSync(path2.replace(/\*\*$/, "")));
16
17
  function litestar(config) {
17
18
  const pluginConfig = resolvePluginConfig(config);
18
19
  const plugins = [resolveLitestarPlugin(pluginConfig), ...resolveFullReloadConfig(pluginConfig)];
19
20
  if (pluginConfig.types !== false && pluginConfig.types.enabled) {
20
- plugins.push(resolveTypeGenerationPlugin(pluginConfig.types, pluginConfig.executor));
21
+ plugins.push(resolveTypeGenerationPlugin(pluginConfig.types, pluginConfig.executor, pluginConfig.hasPythonConfig));
21
22
  }
22
23
  return plugins;
23
24
  }
@@ -30,8 +31,8 @@ async function findIndexHtmlPath(server, pluginConfig) {
30
31
  path.join(root, "index.html"),
31
32
  path.join(root, pluginConfig.resourceDirectory.replace(/^\//, ""), "index.html"),
32
33
  // Ensure resourceDirectory path is relative to root
33
- path.join(root, "public", "index.html")
34
- // Check public even if publicDir is false, might exist
34
+ path.join(root, pluginConfig.publicDir.replace(/^\//, ""), "index.html"),
35
+ path.join(root, pluginConfig.bundleDirectory.replace(/^\//, ""), "index.html")
35
36
  ];
36
37
  for (const indexPath of possiblePaths) {
37
38
  try {
@@ -59,6 +60,7 @@ function resolveLitestarPlugin(pluginConfig) {
59
60
  let resolvedConfig;
60
61
  let userConfig;
61
62
  let litestarMeta = {};
63
+ let shuttingDown = false;
62
64
  const pythonDefaults = loadPythonDefaults();
63
65
  const proxyMode = pythonDefaults?.proxyMode ?? "vite_proxy";
64
66
  const defaultAliases = {
@@ -73,11 +75,39 @@ function resolveLitestarPlugin(pluginConfig) {
73
75
  const env = loadEnv(mode, userConfig.envDir || process.cwd(), "");
74
76
  const assetUrl = normalizeAssetUrl(env.ASSET_URL || pluginConfig.assetUrl);
75
77
  const serverConfig = command === "serve" ? resolveDevelopmentEnvironmentServerConfig(pluginConfig.detectTls) ?? resolveEnvironmentServerConfig(env) : void 0;
78
+ const withProxyErrorSilencer = (proxyConfig) => {
79
+ if (!proxyConfig) return void 0;
80
+ return Object.fromEntries(
81
+ Object.entries(proxyConfig).map(([key, value]) => {
82
+ if (typeof value !== "object" || value === null) {
83
+ return [key, value];
84
+ }
85
+ const existingConfigure = value.configure;
86
+ return [
87
+ key,
88
+ {
89
+ ...value,
90
+ configure(proxy, opts) {
91
+ proxy.on("error", (err) => {
92
+ const msg = String(err?.message ?? "");
93
+ if (shuttingDown || msg.includes("ECONNREFUSED") || msg.includes("ECONNRESET") || msg.includes("socket hang up")) {
94
+ return;
95
+ }
96
+ });
97
+ if (typeof existingConfigure === "function") {
98
+ existingConfigure(proxy, opts);
99
+ }
100
+ }
101
+ }
102
+ ];
103
+ })
104
+ );
105
+ };
76
106
  const devBase = pluginConfig.assetUrl.startsWith("/") ? pluginConfig.assetUrl : pluginConfig.assetUrl.replace(/\/+$/, "");
77
- ensureCommandShouldRunInEnvironment(command, env);
107
+ ensureCommandShouldRunInEnvironment(command, env, mode);
78
108
  return {
79
109
  base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig, assetUrl) : devBase),
80
- publicDir: userConfig.publicDir ?? false,
110
+ publicDir: userConfig.publicDir ?? pluginConfig.publicDir ?? false,
81
111
  clearScreen: false,
82
112
  build: {
83
113
  manifest: userConfig.build?.manifest ?? (ssr ? false : "manifest.json"),
@@ -101,16 +131,18 @@ function resolveLitestarPlugin(pluginConfig) {
101
131
  // Auto-configure proxy to forward API requests to Litestar backend
102
132
  // This allows the app to work when accessing Vite directly (not through Litestar proxy)
103
133
  // Only proxies /api and /schema routes - everything else is handled by Vite
104
- proxy: userConfig.server?.proxy ?? (env.APP_URL ? {
105
- "/api": {
106
- target: env.APP_URL,
107
- changeOrigin: true
108
- },
109
- "/schema": {
110
- target: env.APP_URL,
111
- changeOrigin: true
112
- }
113
- } : void 0),
134
+ proxy: withProxyErrorSilencer(
135
+ userConfig.server?.proxy ?? (env.APP_URL ? {
136
+ "/api": {
137
+ target: env.APP_URL,
138
+ changeOrigin: true
139
+ },
140
+ "/schema": {
141
+ target: env.APP_URL,
142
+ changeOrigin: true
143
+ }
144
+ } : void 0)
145
+ ),
114
146
  // Always respect VITE_PORT when set by Python (regardless of VITE_ALLOW_REMOTE)
115
147
  ...process.env.VITE_PORT ? {
116
148
  port: userConfig.server?.port ?? Number.parseInt(process.env.VITE_PORT),
@@ -157,6 +189,19 @@ function resolveLitestarPlugin(pluginConfig) {
157
189
  base: `${resolvedConfig.base}/`
158
190
  };
159
191
  }
192
+ if (resolvedConfig.command === "serve" && !pluginConfig.hasPythonConfig && !warnedMissingRuntimeConfig) {
193
+ warnedMissingRuntimeConfig = true;
194
+ if (typeof resolvedConfig.logger?.warn === "function") {
195
+ resolvedConfig.logger.warn(formatMissingConfigWarning());
196
+ }
197
+ }
198
+ const resourceDir = path.resolve(resolvedConfig.root, pluginConfig.resourceDirectory);
199
+ if (!fs.existsSync(resourceDir) && typeof resolvedConfig.logger?.warn === "function") {
200
+ resolvedConfig.logger.warn(
201
+ `${colors.cyan("litestar-vite")} ${colors.yellow("Resource directory not found:")} ${resourceDir}
202
+ Expected directory: ${colors.dim(pluginConfig.resourceDirectory)}`
203
+ );
204
+ }
160
205
  const hint = pluginConfig.types !== false ? pluginConfig.types.routesPath : void 0;
161
206
  litestarMeta = await loadLitestarMeta(resolvedConfig, hint);
162
207
  },
@@ -249,9 +294,18 @@ function resolveLitestarPlugin(pluginConfig) {
249
294
  }
250
295
  };
251
296
  process.on("exit", clean);
252
- process.on("SIGINT", () => process.exit());
253
- process.on("SIGTERM", () => process.exit());
254
- process.on("SIGHUP", () => process.exit());
297
+ process.on("SIGINT", () => {
298
+ shuttingDown = true;
299
+ process.exit();
300
+ });
301
+ process.on("SIGTERM", () => {
302
+ shuttingDown = true;
303
+ process.exit();
304
+ });
305
+ process.on("SIGHUP", () => {
306
+ shuttingDown = true;
307
+ process.exit();
308
+ });
255
309
  exitHandlersBound = true;
256
310
  }
257
311
  server.middlewares.use(async (req, res, next) => {
@@ -290,11 +344,14 @@ function resolveLitestarPlugin(pluginConfig) {
290
344
  }
291
345
  };
292
346
  }
293
- function ensureCommandShouldRunInEnvironment(command, env) {
347
+ function ensureCommandShouldRunInEnvironment(command, env, mode) {
294
348
  const allowedDevModes = ["dev", "development", "local", "docker"];
295
349
  if (command === "build" || env.LITESTAR_BYPASS_ENV_CHECK === "1") {
296
350
  return;
297
351
  }
352
+ if (mode === "test" || env.VITEST || env.VITE_TEST || env.NODE_ENV === "test") {
353
+ return;
354
+ }
298
355
  if (typeof env.LITESTAR_MODE !== "undefined" && !allowedDevModes.includes(env.LITESTAR_MODE)) {
299
356
  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.");
300
357
  }
@@ -312,11 +369,19 @@ function _pluginVersion() {
312
369
  }
313
370
  }
314
371
  function loadPythonDefaults() {
315
- const configPath = process.env.LITESTAR_VITE_CONFIG_PATH;
372
+ const isTestEnv = Boolean(process.env.VITEST || process.env.VITE_TEST || process.env.NODE_ENV === "test");
373
+ let configPath = process.env.LITESTAR_VITE_CONFIG_PATH;
316
374
  if (!configPath) {
317
- return null;
375
+ const defaultPath = path.join(process.cwd(), ".litestar.json");
376
+ if (fs.existsSync(defaultPath)) {
377
+ configPath = defaultPath;
378
+ } else {
379
+ warnMissingRuntimeConfig("env", isTestEnv);
380
+ return null;
381
+ }
318
382
  }
319
383
  if (!fs.existsSync(configPath)) {
384
+ warnMissingRuntimeConfig("file", isTestEnv);
320
385
  return null;
321
386
  }
322
387
  try {
@@ -326,6 +391,48 @@ function loadPythonDefaults() {
326
391
  return null;
327
392
  }
328
393
  }
394
+ function formatMissingConfigWarning() {
395
+ const y = colors.yellow;
396
+ const c = colors.cyan;
397
+ const d = colors.dim;
398
+ const b = colors.bold;
399
+ const lines = [
400
+ "",
401
+ y("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"),
402
+ `${y("\u2502")} ${y("\u2502")}`,
403
+ `${y("\u2502")} ${y("\u26A0")} ${b("Litestar backend configuration not found")} ${y("\u2502")}`,
404
+ `${y("\u2502")} ${y("\u2502")}`,
405
+ `${y("\u2502")} The plugin couldn't find ${c(".litestar.json")} which is normally ${y("\u2502")}`,
406
+ `${y("\u2502")} created when the Litestar backend starts. ${y("\u2502")}`,
407
+ `${y("\u2502")} ${y("\u2502")}`,
408
+ `${y("\u2502")} ${b("Quick fix")} - run one of these commands first: ${y("\u2502")}`,
409
+ `${y("\u2502")} ${y("\u2502")}`,
410
+ `${y("\u2502")} ${c("$ litestar run")} ${d("# Start backend only")} ${y("\u2502")}`,
411
+ `${y("\u2502")} ${c("$ litestar assets serve")} ${d("# Start backend + Vite together")} ${y("\u2502")}`,
412
+ `${y("\u2502")} ${y("\u2502")}`,
413
+ `${y("\u2502")} Or manually configure the plugin in ${c("vite.config.ts")}: ${y("\u2502")}`,
414
+ `${y("\u2502")} ${y("\u2502")}`,
415
+ `${y("\u2502")} ${d("litestar({")} ${y("\u2502")}`,
416
+ `${y("\u2502")} ${d(' input: ["src/main.tsx"],')} ${y("\u2502")}`,
417
+ `${y("\u2502")} ${d(' assetUrl: "/static/",')} ${y("\u2502")}`,
418
+ `${y("\u2502")} ${d(' bundleDirectory: "public",')} ${y("\u2502")}`,
419
+ `${y("\u2502")} ${d(" types: false,")} ${y("\u2502")}`,
420
+ `${y("\u2502")} ${d("})")} ${y("\u2502")}`,
421
+ `${y("\u2502")} ${y("\u2502")}`,
422
+ `${y("\u2502")} Docs: ${c("https://docs.litestar.dev/vite/getting-started")} ${y("\u2502")}`,
423
+ `${y("\u2502")} ${y("\u2502")}`,
424
+ y("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"),
425
+ "",
426
+ d("Continuing with defaults... some features may not work."),
427
+ ""
428
+ ];
429
+ return lines.join("\n");
430
+ }
431
+ function warnMissingRuntimeConfig(_reason, suppress) {
432
+ if (warnedMissingRuntimeConfig || suppress) return;
433
+ warnedMissingRuntimeConfig = true;
434
+ console.warn(formatMissingConfigWarning());
435
+ }
329
436
  function resolvePluginConfig(config) {
330
437
  if (typeof config === "undefined") {
331
438
  throw new Error("litestar-vite-plugin: missing configuration.");
@@ -347,6 +454,12 @@ function resolvePluginConfig(config) {
347
454
  throw new Error("litestar-vite-plugin: bundleDirectory must be a subdirectory. E.g. 'public'.");
348
455
  }
349
456
  }
457
+ if (typeof resolvedConfig.publicDir === "string") {
458
+ resolvedConfig.publicDir = resolvedConfig.publicDir.trim().replace(/^\/+/, "").replace(/\/+$/, "");
459
+ if (resolvedConfig.publicDir === "") {
460
+ throw new Error("litestar-vite-plugin: publicDir must be a subdirectory. E.g. 'public'.");
461
+ }
462
+ }
350
463
  if (typeof resolvedConfig.ssrOutputDirectory === "string") {
351
464
  resolvedConfig.ssrOutputDirectory = resolvedConfig.ssrOutputDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, "");
352
465
  }
@@ -354,17 +467,8 @@ function resolvePluginConfig(config) {
354
467
  resolvedConfig.refresh = [{ paths: refreshPaths }];
355
468
  }
356
469
  let typesConfig = false;
357
- if (typeof resolvedConfig.types === "undefined" && pythonDefaults?.types) {
358
- typesConfig = {
359
- enabled: pythonDefaults.types.enabled,
360
- output: pythonDefaults.types.output,
361
- openapiPath: pythonDefaults.types.openapiPath,
362
- routesPath: pythonDefaults.types.routesPath,
363
- generateZod: pythonDefaults.types.generateZod,
364
- generateSdk: pythonDefaults.types.generateSdk,
365
- debounce: 300
366
- };
367
- } else if (resolvedConfig.types === true || typeof resolvedConfig.types === "undefined") {
470
+ if (resolvedConfig.types === false) {
471
+ } else if (resolvedConfig.types === true) {
368
472
  typesConfig = {
369
473
  enabled: true,
370
474
  output: "src/generated/types",
@@ -374,6 +478,18 @@ function resolvePluginConfig(config) {
374
478
  generateSdk: false,
375
479
  debounce: 300
376
480
  };
481
+ } else if (resolvedConfig.types === "auto" || typeof resolvedConfig.types === "undefined") {
482
+ if (pythonDefaults?.types) {
483
+ typesConfig = {
484
+ enabled: pythonDefaults.types.enabled,
485
+ output: pythonDefaults.types.output,
486
+ openapiPath: pythonDefaults.types.openapiPath,
487
+ routesPath: pythonDefaults.types.routesPath,
488
+ generateZod: pythonDefaults.types.generateZod,
489
+ generateSdk: pythonDefaults.types.generateSdk,
490
+ debounce: 300
491
+ };
492
+ }
377
493
  } else if (typeof resolvedConfig.types === "object" && resolvedConfig.types !== null) {
378
494
  const userProvidedOpenapi = Object.hasOwn(resolvedConfig.types, "openapiPath");
379
495
  const userProvidedRoutes = Object.hasOwn(resolvedConfig.types, "routesPath");
@@ -398,6 +514,7 @@ function resolvePluginConfig(config) {
398
514
  assetUrl: normalizeAssetUrl(resolvedConfig.assetUrl ?? pythonDefaults?.assetUrl ?? "/static/"),
399
515
  resourceDirectory: resolvedConfig.resourceDirectory ?? pythonDefaults?.resourceDir ?? "resources",
400
516
  bundleDirectory: resolvedConfig.bundleDirectory ?? pythonDefaults?.bundleDir ?? "public",
517
+ publicDir: resolvedConfig.publicDir ?? pythonDefaults?.publicDir ?? "public",
401
518
  ssr: resolvedConfig.ssr ?? resolvedConfig.input,
402
519
  ssrOutputDirectory: resolvedConfig.ssrOutputDirectory ?? pythonDefaults?.ssrOutDir ?? path.join(resolvedConfig.resourceDirectory ?? pythonDefaults?.resourceDir ?? "resources", "bootstrap/ssr"),
403
520
  refresh: resolvedConfig.refresh ?? false,
@@ -406,7 +523,8 @@ function resolvePluginConfig(config) {
406
523
  autoDetectIndex: resolvedConfig.autoDetectIndex ?? true,
407
524
  transformOnServe: resolvedConfig.transformOnServe ?? ((code) => code),
408
525
  types: typesConfig,
409
- executor: resolvedConfig.executor ?? pythonDefaults?.executor
526
+ executor: resolvedConfig.executor ?? pythonDefaults?.executor,
527
+ hasPythonConfig: pythonDefaults !== null
410
528
  };
411
529
  }
412
530
  function resolveBase(_config, assetUrl) {
@@ -557,12 +675,9 @@ declare global {
557
675
  /**
558
676
  * Simple route map (name -> uri) for legacy consumers.
559
677
  */
560
- routes?: typeof routes
561
- serverRoutes?: typeof serverRoutes
678
+ routes?: Record<string, string>
679
+ serverRoutes?: Record<string, string>
562
680
  }
563
- // eslint-disable-next-line no-var
564
- var routes: typeof routes | undefined
565
- var serverRoutes: typeof serverRoutes | undefined
566
681
  }
567
682
 
568
683
  // Re-export helper functions from litestar-vite-plugin
@@ -571,12 +686,13 @@ export { getCsrfToken, csrfHeaders, csrfFetch } from "litestar-vite-plugin/helpe
571
686
  `;
572
687
  await fs.promises.writeFile(outFile, `${banner}${body}`, "utf-8");
573
688
  }
574
- function resolveTypeGenerationPlugin(typesConfig, executor) {
689
+ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
575
690
  let lastTypesHash = null;
576
691
  let lastRoutesHash = null;
577
692
  let server = null;
578
693
  let isGenerating = false;
579
694
  let resolvedConfig = null;
695
+ let chosenConfigPath = null;
580
696
  async function runTypeGeneration() {
581
697
  if (isGenerating) {
582
698
  return false;
@@ -584,26 +700,47 @@ function resolveTypeGenerationPlugin(typesConfig, executor) {
584
700
  isGenerating = true;
585
701
  const startTime = Date.now();
586
702
  try {
587
- const openapiPath = path.resolve(process.cwd(), typesConfig.openapiPath);
588
- const routesPath = path.resolve(process.cwd(), typesConfig.routesPath);
703
+ const projectRoot = resolvedConfig?.root ?? process.cwd();
704
+ const openapiPath = path.resolve(projectRoot, typesConfig.openapiPath);
705
+ const routesPath = path.resolve(projectRoot, typesConfig.routesPath);
589
706
  let generated = false;
590
- if (fs.existsSync(openapiPath)) {
707
+ const candidates = [path.resolve(projectRoot, "openapi-ts.config.ts"), path.resolve(projectRoot, "hey-api.config.ts"), path.resolve(projectRoot, ".hey-api.config.ts")];
708
+ const configPath = candidates.find((p) => fs.existsSync(p)) || null;
709
+ chosenConfigPath = configPath;
710
+ const shouldRunOpenApiTs = configPath || typesConfig.generateSdk;
711
+ if (fs.existsSync(openapiPath) && shouldRunOpenApiTs) {
712
+ resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("generating TypeScript types...")}`);
591
713
  if (resolvedConfig) {
592
- resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("generating TypeScript types...")}`);
714
+ resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("openapi-ts config: ")}${configPath ?? "<built-in defaults>"}`);
593
715
  }
594
- const args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
595
- if (typesConfig.generateZod) {
596
- args.push("--plugins", "zod", "@hey-api/typescript");
716
+ const sdkOutput = path.join(typesConfig.output, "api");
717
+ let args;
718
+ if (configPath) {
719
+ args = ["@hey-api/openapi-ts", "--file", configPath];
720
+ } else {
721
+ args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", sdkOutput];
722
+ const plugins = ["@hey-api/typescript", "@hey-api/schemas"];
723
+ if (typesConfig.generateSdk) {
724
+ plugins.push("@hey-api/sdk", "@hey-api/client-axios");
725
+ }
726
+ if (typesConfig.generateZod) {
727
+ plugins.push("zod");
728
+ }
729
+ if (plugins.length) {
730
+ args.push("--plugins", ...plugins);
731
+ }
597
732
  }
598
- if (typesConfig.generateSdk) {
599
- args.push("--client", "fetch");
733
+ if (typesConfig.generateZod) {
734
+ try {
735
+ require.resolve("zod", { paths: [process.cwd()] });
736
+ } catch {
737
+ resolvedConfig?.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("zod not installed")} - run: ${resolveInstallHint()} zod`);
738
+ }
600
739
  }
601
740
  await execAsync(resolvePackageExecutor(args.join(" "), executor), {
602
- cwd: process.cwd()
741
+ cwd: projectRoot
603
742
  });
604
743
  generated = true;
605
- } else if (resolvedConfig) {
606
- resolvedConfig.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("OpenAPI schema not found:")} ${typesConfig.openapiPath}`);
607
744
  }
608
745
  if (fs.existsSync(routesPath)) {
609
746
  await emitRouteTypes(routesPath, typesConfig.output);
@@ -628,7 +765,10 @@ function resolveTypeGenerationPlugin(typesConfig, executor) {
628
765
  if (resolvedConfig) {
629
766
  const message = error instanceof Error ? error.message : String(error);
630
767
  if (message.includes("not found") || message.includes("ENOENT")) {
631
- resolvedConfig.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("@hey-api/openapi-ts not installed")} - run: ${resolveInstallHint()}`);
768
+ const zodHint = typesConfig.generateZod ? " zod" : "";
769
+ resolvedConfig.logger.warn(
770
+ `${colors.cyan("litestar-vite")} ${colors.yellow("@hey-api/openapi-ts not installed")} - run: ${resolveInstallHint()} -D @hey-api/openapi-ts${zodHint}`
771
+ );
632
772
  } else {
633
773
  resolvedConfig.logger.error(`${colors.cyan("litestar-vite")} ${colors.red("type generation failed:")} ${message}`);
634
774
  }
@@ -648,14 +788,47 @@ function resolveTypeGenerationPlugin(typesConfig, executor) {
648
788
  configureServer(devServer) {
649
789
  server = devServer;
650
790
  if (typesConfig.enabled) {
651
- resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("watching for schema changes:")} ${colors.yellow(typesConfig.openapiPath)}`);
791
+ const root = resolvedConfig?.root ?? process.cwd();
792
+ const openapiAbs = path.resolve(root, typesConfig.openapiPath);
793
+ const routesAbs = path.resolve(root, typesConfig.routesPath);
794
+ resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("watching schema/routes:")} ${colors.yellow(openapiAbs)}, ${colors.yellow(routesAbs)}`);
795
+ if (chosenConfigPath) {
796
+ resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("openapi-ts config:")} ${colors.yellow(chosenConfigPath)}`);
797
+ }
798
+ }
799
+ },
800
+ async buildStart() {
801
+ if (typesConfig.enabled && !hasPythonConfig) {
802
+ const projectRoot = resolvedConfig?.root ?? process.cwd();
803
+ const openapiPath = path.resolve(projectRoot, typesConfig.openapiPath);
804
+ if (!fs.existsSync(openapiPath)) {
805
+ this.warn(
806
+ `Type generation is enabled but .litestar.json was not found.
807
+ The Litestar backend generates this file on startup.
808
+
809
+ Solutions:
810
+ 1. Start the backend first: ${colors.cyan("litestar run")}
811
+ 2. Use integrated dev: ${colors.cyan("litestar assets serve")}
812
+ 3. Disable types: ${colors.cyan("litestar({ input: [...], types: false })")}
813
+ `
814
+ );
815
+ }
816
+ }
817
+ if (typesConfig.enabled) {
818
+ const projectRoot = resolvedConfig?.root ?? process.cwd();
819
+ const openapiPath = path.resolve(projectRoot, typesConfig.openapiPath);
820
+ const routesPath = path.resolve(projectRoot, typesConfig.routesPath);
821
+ if (fs.existsSync(openapiPath) || fs.existsSync(routesPath)) {
822
+ await runTypeGeneration();
823
+ }
652
824
  }
653
825
  },
654
826
  async handleHotUpdate({ file }) {
655
827
  if (!typesConfig.enabled) {
656
828
  return;
657
829
  }
658
- const relativePath = path.relative(process.cwd(), file);
830
+ const root = resolvedConfig?.root ?? process.cwd();
831
+ const relativePath = path.relative(root, file);
659
832
  const openapiPath = typesConfig.openapiPath.replace(/^\.\//, "");
660
833
  const routesPath = typesConfig.routesPath.replace(/^\.\//, "");
661
834
  if (relativePath === openapiPath || relativePath === routesPath || file.endsWith(openapiPath) || file.endsWith(routesPath)) {
@@ -4,9 +4,14 @@
4
4
  * This module re-exports common helpers from litestar-vite-plugin/helpers
5
5
  * and adds Inertia-specific utilities.
6
6
  *
7
+ * For type-safe routing, import from your generated routes file:
8
+ * ```ts
9
+ * import { route, routes, type RouteName } from '@/generated/routes'
10
+ * ```
11
+ *
7
12
  * @module
8
13
  */
9
- export { csrfFetch, csrfHeaders, currentRoute, getCsrfToken, getRelativeUrlPath, getRoutes, isCurrentRoute, isRoute, type RouteDefinition, type RoutesMap, route, toRoute, } from "litestar-vite-plugin/helpers";
14
+ export { csrfFetch, csrfHeaders, getCsrfToken, } from "litestar-vite-plugin/helpers";
10
15
  /**
11
16
  * Unwrap page props that may have content nested under "content" key.
12
17
  *
@@ -4,15 +4,18 @@
4
4
  * This module re-exports common helpers from litestar-vite-plugin/helpers
5
5
  * and adds Inertia-specific utilities.
6
6
  *
7
+ * For type-safe routing, import from your generated routes file:
8
+ * ```ts
9
+ * import { route, routes, type RouteName } from '@/generated/routes'
10
+ * ```
11
+ *
7
12
  * @module
8
13
  */
9
14
  // Re-export all helpers from the main helpers module
10
15
  // Note: Using package path instead of relative import to ensure proper build output structure
11
- export { csrfFetch, csrfHeaders, currentRoute,
16
+ export { csrfFetch, csrfHeaders,
12
17
  // CSRF utilities
13
- getCsrfToken, getRelativeUrlPath, getRoutes, isCurrentRoute, isRoute,
14
- // Route utilities
15
- route, toRoute, } from "litestar-vite-plugin/helpers";
18
+ getCsrfToken, } from "litestar-vite-plugin/helpers";
16
19
  /**
17
20
  * Unwrap page props that may have content nested under "content" key.
18
21
  *
@@ -11,7 +11,11 @@ async function checkBackendAvailability(appUrl) {
11
11
  const controller = new AbortController();
12
12
  const timeout = setTimeout(() => controller.abort(), 2e3);
13
13
  const schemaPath = process.env.LITESTAR_OPENAPI_PATH || "/schema";
14
- const checkUrl = new URL(schemaPath, appUrl).href;
14
+ const urlObj = new URL(schemaPath, appUrl);
15
+ if (urlObj.hostname === "0.0.0.0") {
16
+ urlObj.hostname = "127.0.0.1";
17
+ }
18
+ const checkUrl = urlObj.href;
15
19
  const response = await fetch(checkUrl, {
16
20
  method: "GET",
17
21
  signal: controller.signal
@@ -58,11 +62,23 @@ function firstExisting(paths) {
58
62
  }
59
63
  return null;
60
64
  }
65
+ function loadVersionFromRuntimeConfig() {
66
+ const cfgPath = process.env.LITESTAR_VITE_CONFIG_PATH;
67
+ if (!cfgPath || !fs.existsSync(cfgPath)) return null;
68
+ try {
69
+ const raw = fs.readFileSync(cfgPath, "utf8");
70
+ const data = JSON.parse(raw);
71
+ const v = data?.litestarVersion;
72
+ return typeof v === "string" && v.trim() ? v.trim() : null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
61
77
  async function loadLitestarMeta(resolvedConfig, routesPathHint) {
62
78
  const fromEnv = process.env.LITESTAR_VERSION?.trim();
63
- if (fromEnv) {
64
- return { litestarVersion: fromEnv };
65
- }
79
+ if (fromEnv) return { litestarVersion: fromEnv };
80
+ const fromRuntime = loadVersionFromRuntimeConfig();
81
+ if (fromRuntime) return { litestarVersion: fromRuntime };
66
82
  const root = resolvedConfig.root ?? process.cwd();
67
83
  const candidates = [routesPathHint ? path.resolve(root, routesPathHint) : null, path.resolve(root, "src/generated/routes.json"), path.resolve(root, "routes.json")].filter(
68
84
  Boolean
package/dist/js/nuxt.d.ts CHANGED
@@ -61,6 +61,12 @@ export interface NuxtTypesConfig {
61
61
  * @default false
62
62
  */
63
63
  generateZod?: boolean;
64
+ /**
65
+ * Generate SDK client functions for API calls.
66
+ *
67
+ * @default true
68
+ */
69
+ generateSdk?: boolean;
64
70
  /**
65
71
  * Debounce time in milliseconds for type regeneration.
66
72
  *