litestar-vite-plugin 0.15.0-alpha.3 → 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.
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 {
@@ -42,7 +43,7 @@ async function findIndexHtmlPath(server, pluginConfig) {
42
43
  }
43
44
  return null;
44
45
  }
45
- function normalizeAppUrl(appUrl, fallbackPort) {
46
+ function normalizeAppUrl(appUrl, _fallbackPort) {
46
47
  if (!appUrl || appUrl === "__litestar_app_url_missing__") {
47
48
  return { url: null, note: "APP_URL missing" };
48
49
  }
@@ -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,10 +189,23 @@ 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
  },
163
- transform(code, id) {
208
+ transform(code, _id) {
164
209
  if (resolvedConfig.command === "serve" && code.includes("__litestar_vite_placeholder__")) {
165
210
  const transformedCode = code.replace(/__litestar_vite_placeholder__/g, viteDevServerUrl);
166
211
  return pluginConfig.transformOnServe(transformedCode, viteDevServerUrl);
@@ -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
  }
@@ -304,7 +361,7 @@ function ensureCommandShouldRunInEnvironment(command, env) {
304
361
  );
305
362
  }
306
363
  }
307
- function pluginVersion() {
364
+ function _pluginVersion() {
308
365
  try {
309
366
  return JSON.parse(fs.readFileSync(path.join(dirname(), "../package.json")).toString())?.version;
310
367
  } catch {
@@ -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,9 +478,21 @@ 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
- const userProvidedOpenapi = Object.prototype.hasOwnProperty.call(resolvedConfig.types, "openapiPath");
379
- const userProvidedRoutes = Object.prototype.hasOwnProperty.call(resolvedConfig.types, "routesPath");
494
+ const userProvidedOpenapi = Object.hasOwn(resolvedConfig.types, "openapiPath");
495
+ const userProvidedRoutes = Object.hasOwn(resolvedConfig.types, "routesPath");
380
496
  typesConfig = {
381
497
  enabled: resolvedConfig.types.enabled ?? true,
382
498
  output: resolvedConfig.types.output ?? "src/generated/types",
@@ -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,10 +523,11 @@ 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
- function resolveBase(config, assetUrl) {
530
+ function resolveBase(_config, assetUrl) {
413
531
  if (process.env.NODE_ENV === "development") {
414
532
  return assetUrl;
415
533
  }
@@ -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 { getCsrfToken, csrfHeaders, csrfFetch, route, getRoutes, toRoute, currentRoute, isRoute, isCurrentRoute, getRelativeUrlPath, type RouteDefinition, type RoutesMap, } 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 {
16
+ export { csrfFetch, csrfHeaders,
12
17
  // CSRF utilities
13
- getCsrfToken, csrfHeaders, csrfFetch,
14
- // Route utilities
15
- route, getRoutes, toRoute, currentRoute, isRoute, isCurrentRoute, getRelativeUrlPath, } 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
  *
package/dist/js/nuxt.js CHANGED
@@ -51,6 +51,7 @@ function resolveConfig(config = {}) {
51
51
  openapiPath: "openapi.json",
52
52
  routesPath: "routes.json",
53
53
  generateZod: false,
54
+ generateSdk: true,
54
55
  debounce: 300
55
56
  };
56
57
  } else if (typeof config.types === "object" && config.types !== null) {
@@ -60,6 +61,7 @@ function resolveConfig(config = {}) {
60
61
  openapiPath: config.types.openapiPath ?? "openapi.json",
61
62
  routesPath: config.types.routesPath ?? "routes.json",
62
63
  generateZod: config.types.generateZod ?? false,
64
+ generateSdk: config.types.generateSdk ?? true,
63
65
  debounce: config.types.debounce ?? 300
64
66
  };
65
67
  }
@@ -96,6 +98,15 @@ function createProxyPlugin(config) {
96
98
  hmrPort = await getPort();
97
99
  return {
98
100
  server: {
101
+ // Force IPv4 binding for consistency with Python proxy configuration
102
+ // Without this, Nuxt/Nitro might bind to IPv6 localhost which the proxy can't reach
103
+ host: "127.0.0.1",
104
+ // Set the port from Python config/env to ensure Nuxt uses the expected port
105
+ // strictPort: true prevents auto-incrementing to a different port
106
+ ...config.devPort !== void 0 ? {
107
+ port: config.devPort,
108
+ strictPort: true
109
+ } : {},
99
110
  // Avoid HMR port collisions by letting Vite pick a free port for WS
100
111
  hmr: {
101
112
  port: hmrPort,
@@ -281,12 +292,28 @@ function createTypeGenerationPlugin(typesConfig, executor) {
281
292
  return false;
282
293
  }
283
294
  console.log(colors.cyan("[litestar-nuxt]"), colors.dim("Generating TypeScript types..."));
284
- const args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
285
- if (typesConfig.generateZod) {
286
- args.push("--plugins", "zod", "@hey-api/typescript");
295
+ const projectRoot = process.cwd();
296
+ const candidates = [path.resolve(projectRoot, "openapi-ts.config.ts"), path.resolve(projectRoot, "hey-api.config.ts"), path.resolve(projectRoot, ".hey-api.config.ts")];
297
+ const configPath = candidates.find((p) => fs.existsSync(p)) || null;
298
+ let args;
299
+ if (configPath) {
300
+ console.log(colors.cyan("[litestar-nuxt]"), colors.dim("Using config:"), configPath);
301
+ args = ["@hey-api/openapi-ts", "--file", configPath];
302
+ } else {
303
+ args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
304
+ const plugins = ["@hey-api/typescript", "@hey-api/schemas"];
305
+ if (typesConfig.generateSdk) {
306
+ plugins.push("@hey-api/sdk", "@hey-api/client-nuxt");
307
+ }
308
+ if (typesConfig.generateZod) {
309
+ plugins.push("zod");
310
+ }
311
+ if (plugins.length) {
312
+ args.push("--plugins", ...plugins);
313
+ }
287
314
  }
288
315
  await execAsync(resolvePackageExecutor(args.join(" "), executor), {
289
- cwd: process.cwd()
316
+ cwd: projectRoot
290
317
  });
291
318
  const routesPath = path.resolve(process.cwd(), typesConfig.routesPath);
292
319
  if (fs.existsSync(routesPath)) {
@@ -325,6 +352,14 @@ function createTypeGenerationPlugin(typesConfig, executor) {
325
352
  server = devServer;
326
353
  console.log(colors.cyan("[litestar-nuxt]"), colors.dim("Watching for schema changes:"), colors.yellow(typesConfig.openapiPath));
327
354
  },
355
+ async buildStart() {
356
+ if (typesConfig.enabled) {
357
+ const openapiPath = path.resolve(process.cwd(), typesConfig.openapiPath);
358
+ if (fs.existsSync(openapiPath)) {
359
+ await runTypeGeneration();
360
+ }
361
+ }
362
+ },
328
363
  handleHotUpdate({ file }) {
329
364
  if (!typesConfig.enabled) {
330
365
  return;