litestar-vite-plugin 0.15.0-alpha.7 → 0.15.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -160,7 +160,7 @@ litestar assets generate-types # one-off or CI
160
160
 
161
161
  - Prints Python vs Vite config snapshot (asset URLs, bundle/hot paths, ports, modes).
162
162
  - Flags missing hot file (dev proxy), missing manifest (prod), type-gen exports, env/config mismatches, and plugin install issues.
163
- - `--fix` can rewrite simple vite.config values (assetUrl, bundleDirectory, hotFile, type paths) after creating a backup.
163
+ - `--fix` can rewrite simple vite.config values (assetUrl, bundleDir, hotFile, type paths) after creating a backup.
164
164
 
165
165
  ## Links
166
166
 
@@ -54,6 +54,16 @@ export interface TypesConfig {
54
54
  * @default false
55
55
  */
56
56
  generateSdk?: boolean;
57
+ /**
58
+ * Register route() function globally on window object.
59
+ *
60
+ * When true, the generated routes.ts will include code that registers
61
+ * the type-safe route() function on `window.route`, similar to Laravel's
62
+ * Ziggy library. This allows using route() without imports.
63
+ *
64
+ * @default false
65
+ */
66
+ globalRoute?: boolean;
57
67
  /**
58
68
  * Debounce time in milliseconds for type regeneration.
59
69
  * Prevents regeneration from running too frequently when
@@ -79,7 +89,7 @@ export interface PluginConfig {
79
89
  *
80
90
  * @default 'public/dist'
81
91
  */
82
- bundleDirectory?: string;
92
+ bundleDir?: string;
83
93
  /**
84
94
  * Vite's public directory for static, unprocessed assets.
85
95
  * Mirrors Vite's `publicDir` option.
@@ -90,13 +100,13 @@ export interface PluginConfig {
90
100
  /**
91
101
  * Litestar's public assets directory. These are the assets that Vite will serve when developing.
92
102
  *
93
- * @default 'resources'
103
+ * @default 'src'
94
104
  */
95
- resourceDirectory?: string;
105
+ resourceDir?: string;
96
106
  /**
97
107
  * The path to the "hot" file.
98
108
  *
99
- * @default `${bundleDirectory}/hot`
109
+ * @default `${bundleDir}/hot`
100
110
  */
101
111
  hotFile?: string;
102
112
  /**
@@ -106,9 +116,9 @@ export interface PluginConfig {
106
116
  /**
107
117
  * The directory where the SSR bundle should be written.
108
118
  *
109
- * @default '${bundleDirectory}/bootstrap/ssr'
119
+ * @default '${bundleDir}/bootstrap/ssr'
110
120
  */
111
- ssrOutputDirectory?: string;
121
+ ssrOutDir?: string;
112
122
  /**
113
123
  * Configuration for performing full page refresh on python (or other) file changes.
114
124
  *
@@ -128,6 +138,18 @@ export interface PluginConfig {
128
138
  * @default true
129
139
  */
130
140
  autoDetectIndex?: boolean;
141
+ /**
142
+ * Enable Inertia mode, which disables index.html auto-detection.
143
+ *
144
+ * In Inertia apps, the backend (Litestar) serves all HTML responses.
145
+ * When enabled, direct access to the Vite dev server will show a placeholder
146
+ * page directing users to access the app through the backend.
147
+ *
148
+ * Auto-detected from `.litestar.json` when mode is "inertia".
149
+ *
150
+ * @default false (auto-detected from .litestar.json)
151
+ */
152
+ inertiaMode?: boolean;
131
153
  /**
132
154
  * Transform the code while serving.
133
155
  */
@@ -193,6 +215,50 @@ interface RefreshConfig {
193
215
  paths: string[];
194
216
  config?: FullReloadConfig;
195
217
  }
218
+ /**
219
+ * Bridge schema for `.litestar.json` - the shared configuration contract
220
+ * between Python (Litestar) and TypeScript (Vite plugin).
221
+ *
222
+ * Python writes this file on startup; TypeScript reads it as defaults.
223
+ * Field names use camelCase (JavaScript convention) and match exactly
224
+ * between the JSON file and this TypeScript interface.
225
+ *
226
+ * Precedence: vite.config.ts > .litestar.json > hardcoded defaults
227
+ */
228
+ export interface BridgeSchema {
229
+ assetUrl: string;
230
+ bundleDir: string;
231
+ resourceDir: string;
232
+ publicDir: string;
233
+ hotFile: string;
234
+ manifest: string;
235
+ mode: "spa" | "inertia" | "ssr" | "hybrid";
236
+ proxyMode: "vite_proxy" | "vite_direct" | "external_proxy";
237
+ host: string;
238
+ port: number;
239
+ protocol: "http" | "https";
240
+ ssrEnabled: boolean;
241
+ ssrOutDir: string | null;
242
+ types: {
243
+ enabled: boolean;
244
+ output: string;
245
+ openapiPath: string;
246
+ routesPath: string;
247
+ pagePropsPath?: string;
248
+ generateZod: boolean;
249
+ generateSdk: boolean;
250
+ globalRoute: boolean;
251
+ } | null;
252
+ executor: "node" | "bun" | "deno" | "yarn" | "pnpm";
253
+ logging: {
254
+ level: "quiet" | "normal" | "verbose";
255
+ showPathsAbsolute: boolean;
256
+ suppressNpmOutput: boolean;
257
+ suppressViteBanner: boolean;
258
+ timestamps: boolean;
259
+ } | null;
260
+ litestarVersion: string;
261
+ }
196
262
  type DevServerUrl = `${"http" | "https"}://${string}:${number}`;
197
263
  export declare const refreshPaths: string[];
198
264
  /**
package/dist/js/index.js CHANGED
@@ -10,6 +10,8 @@ import fullReload from "vite-plugin-full-reload";
10
10
  import { resolveInstallHint, resolvePackageExecutor } from "./install-hint.js";
11
11
  import { checkBackendAvailability, loadLitestarMeta } from "./litestar-meta.js";
12
12
  import { debounce } from "./shared/debounce.js";
13
+ import { formatPath } from "./shared/format-path.js";
14
+ import { createLogger } from "./shared/logger.js";
13
15
  const execAsync = promisify(exec);
14
16
  let exitHandlersBound = false;
15
17
  let warnedMissingRuntimeConfig = false;
@@ -23,16 +25,19 @@ function litestar(config) {
23
25
  return plugins;
24
26
  }
25
27
  async function findIndexHtmlPath(server, pluginConfig) {
28
+ if (pluginConfig.inertiaMode) {
29
+ return null;
30
+ }
26
31
  if (!pluginConfig.autoDetectIndex) {
27
32
  return null;
28
33
  }
29
34
  const root = server.config.root;
30
35
  const possiblePaths = [
31
36
  path.join(root, "index.html"),
32
- path.join(root, pluginConfig.resourceDirectory.replace(/^\//, ""), "index.html"),
33
- // Ensure resourceDirectory path is relative to root
37
+ path.join(root, pluginConfig.resourceDir.replace(/^\//, ""), "index.html"),
38
+ // Ensure resourceDir path is relative to root
34
39
  path.join(root, pluginConfig.publicDir.replace(/^\//, ""), "index.html"),
35
- path.join(root, pluginConfig.bundleDirectory.replace(/^\//, ""), "index.html")
40
+ path.join(root, pluginConfig.bundleDir.replace(/^\//, ""), "index.html")
36
41
  ];
37
42
  for (const indexPath of possiblePaths) {
38
43
  try {
@@ -63,8 +68,9 @@ function resolveLitestarPlugin(pluginConfig) {
63
68
  let shuttingDown = false;
64
69
  const pythonDefaults = loadPythonDefaults();
65
70
  const proxyMode = pythonDefaults?.proxyMode ?? "vite_proxy";
71
+ const logger = createLogger(pythonDefaults?.logging);
66
72
  const defaultAliases = {
67
- "@": `/${pluginConfig.resourceDirectory.replace(/^\/+/, "").replace(/\/+$/, "")}/`
73
+ "@": `/${pluginConfig.resourceDir.replace(/^\/+/, "").replace(/\/+$/, "")}/`
68
74
  };
69
75
  return {
70
76
  name: "litestar",
@@ -195,9 +201,9 @@ function resolveLitestarPlugin(pluginConfig) {
195
201
  resolvedConfig.logger.warn(formatMissingConfigWarning());
196
202
  }
197
203
  }
198
- const resourceDir = path.resolve(resolvedConfig.root, pluginConfig.resourceDirectory);
199
- if (!fs.existsSync(resourceDir) && typeof resolvedConfig.logger?.warn === "function") {
200
- resolvedConfig.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("Resource directory not found:")} ${pluginConfig.resourceDirectory}`);
204
+ const resourceDirPath = path.resolve(resolvedConfig.root, pluginConfig.resourceDir);
205
+ if (!fs.existsSync(resourceDirPath) && typeof resolvedConfig.logger?.warn === "function") {
206
+ resolvedConfig.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("Resource directory not found:")} ${pluginConfig.resourceDir}`);
201
207
  }
202
208
  const hint = pluginConfig.types !== false ? pluginConfig.types.routesPath : void 0;
203
209
  litestarMeta = await loadLitestarMeta(resolvedConfig, hint);
@@ -229,19 +235,20 @@ function resolveLitestarPlugin(pluginConfig) {
229
235
  fs.writeFileSync(pluginConfig.hotFile, viteDevServerUrl);
230
236
  }
231
237
  setTimeout(async () => {
232
- const version = litestarMeta.litestarVersion ?? process.env.LITESTAR_VERSION ?? "unknown";
238
+ if (logger.config.level === "quiet") return;
239
+ const litestarVersion = litestarMeta.litestarVersion ?? process.env.LITESTAR_VERSION ?? "unknown";
233
240
  const backendStatus = await checkBackendAvailability(appUrl);
234
241
  resolvedConfig.logger.info(`
235
- ${colors.red(`${colors.bold("LITESTAR")} ${version}`)}`);
242
+ ${colors.red(`${colors.bold("LITESTAR")} ${litestarVersion}`)}`);
236
243
  resolvedConfig.logger.info("");
237
244
  if (initialIndexPath) {
238
- resolvedConfig.logger.info(
239
- ` ${colors.green("\u279C")} ${colors.bold("Index Mode")}: SPA (Serving ${colors.cyan(path.relative(server.config.root, initialIndexPath))} from root)`
240
- );
245
+ const relIndexPath = logger.path(initialIndexPath, server.config.root);
246
+ resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: SPA (${colors.cyan(relIndexPath)})`);
247
+ } else if (pluginConfig.inertiaMode) {
248
+ resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: Inertia`);
241
249
  } else {
242
- resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Index Mode")}: Litestar (Plugin will serve placeholder for /index.html)`);
250
+ resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: Litestar`);
243
251
  }
244
- resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Dev Server")}: ${colors.cyan(viteDevServerUrl)}`);
245
252
  if (backendStatus.available) {
246
253
  resolvedConfig.logger.info(
247
254
  ` ${colors.green("\u279C")} ${colors.bold("App URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))} ${colors.green("\u2713")}`
@@ -251,12 +258,13 @@ function resolveLitestarPlugin(pluginConfig) {
251
258
  ` ${colors.yellow("\u279C")} ${colors.bold("App URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))} ${colors.yellow("\u26A0")}`
252
259
  );
253
260
  }
254
- resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Assets Base")}: ${colors.cyan(resolvedConfig.base)}`);
261
+ resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Dev Server")}: ${colors.cyan(viteDevServerUrl)}`);
255
262
  if (pluginConfig.types !== false && pluginConfig.types.enabled) {
256
263
  const openapiExists = fs.existsSync(path.resolve(process.cwd(), pluginConfig.types.openapiPath));
257
264
  const routesExists = fs.existsSync(path.resolve(process.cwd(), pluginConfig.types.routesPath));
265
+ const relTypesOutput = logger.path(pluginConfig.types.output, process.cwd());
258
266
  if (openapiExists || routesExists) {
259
- resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Type Gen")}: ${colors.green("enabled")} ${colors.dim(`\u2192 ${pluginConfig.types.output}`)}`);
267
+ resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Type Gen")}: ${colors.dim(`${relTypesOutput}/`)}`);
260
268
  } else {
261
269
  resolvedConfig.logger.info(` ${colors.yellow("\u279C")} ${colors.bold("Type Gen")}: ${colors.yellow("waiting")} ${colors.dim("(no schema files yet)")}`);
262
270
  }
@@ -413,7 +421,7 @@ function formatMissingConfigWarning() {
413
421
  `${y("\u2502")} ${d("litestar({")} ${y("\u2502")}`,
414
422
  `${y("\u2502")} ${d(' input: ["src/main.tsx"],')} ${y("\u2502")}`,
415
423
  `${y("\u2502")} ${d(' assetUrl: "/static/",')} ${y("\u2502")}`,
416
- `${y("\u2502")} ${d(' bundleDirectory: "public",')} ${y("\u2502")}`,
424
+ `${y("\u2502")} ${d(' bundleDir: "public",')} ${y("\u2502")}`,
417
425
  `${y("\u2502")} ${d(" types: false,")} ${y("\u2502")}`,
418
426
  `${y("\u2502")} ${d("})")} ${y("\u2502")}`,
419
427
  `${y("\u2502")} ${y("\u2502")}`,
@@ -440,16 +448,16 @@ function resolvePluginConfig(config) {
440
448
  if (typeof resolvedConfig.input === "undefined") {
441
449
  throw new Error('litestar-vite-plugin: missing configuration for "input".');
442
450
  }
443
- if (typeof resolvedConfig.resourceDirectory === "string") {
444
- resolvedConfig.resourceDirectory = resolvedConfig.resourceDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, "");
445
- if (resolvedConfig.resourceDirectory === "") {
446
- throw new Error("litestar-vite-plugin: resourceDirectory must be a subdirectory. E.g. 'resources'.");
451
+ if (typeof resolvedConfig.resourceDir === "string") {
452
+ resolvedConfig.resourceDir = resolvedConfig.resourceDir.trim().replace(/^\/+/, "").replace(/\/+$/, "");
453
+ if (resolvedConfig.resourceDir === "") {
454
+ throw new Error("litestar-vite-plugin: resourceDir must be a subdirectory. E.g. 'resources'.");
447
455
  }
448
456
  }
449
- if (typeof resolvedConfig.bundleDirectory === "string") {
450
- resolvedConfig.bundleDirectory = resolvedConfig.bundleDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, "");
451
- if (resolvedConfig.bundleDirectory === "") {
452
- throw new Error("litestar-vite-plugin: bundleDirectory must be a subdirectory. E.g. 'public'.");
457
+ if (typeof resolvedConfig.bundleDir === "string") {
458
+ resolvedConfig.bundleDir = resolvedConfig.bundleDir.trim().replace(/^\/+/, "").replace(/\/+$/, "");
459
+ if (resolvedConfig.bundleDir === "") {
460
+ throw new Error("litestar-vite-plugin: bundleDir must be a subdirectory. E.g. 'public'.");
453
461
  }
454
462
  }
455
463
  if (typeof resolvedConfig.publicDir === "string") {
@@ -458,8 +466,8 @@ function resolvePluginConfig(config) {
458
466
  throw new Error("litestar-vite-plugin: publicDir must be a subdirectory. E.g. 'public'.");
459
467
  }
460
468
  }
461
- if (typeof resolvedConfig.ssrOutputDirectory === "string") {
462
- resolvedConfig.ssrOutputDirectory = resolvedConfig.ssrOutputDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, "");
469
+ if (typeof resolvedConfig.ssrOutDir === "string") {
470
+ resolvedConfig.ssrOutDir = resolvedConfig.ssrOutDir.trim().replace(/^\/+/, "").replace(/\/+$/, "");
463
471
  }
464
472
  if (resolvedConfig.refresh === true) {
465
473
  resolvedConfig.refresh = [{ paths: refreshPaths }];
@@ -475,6 +483,7 @@ function resolvePluginConfig(config) {
475
483
  pagePropsPath: "src/generated/inertia-pages.json",
476
484
  generateZod: false,
477
485
  generateSdk: false,
486
+ globalRoute: false,
478
487
  debounce: 300
479
488
  };
480
489
  } else if (resolvedConfig.types === "auto" || typeof resolvedConfig.types === "undefined") {
@@ -487,6 +496,7 @@ function resolvePluginConfig(config) {
487
496
  pagePropsPath: pythonDefaults.types.pagePropsPath ?? path.join(pythonDefaults.types.output, "inertia-pages.json"),
488
497
  generateZod: pythonDefaults.types.generateZod,
489
498
  generateSdk: pythonDefaults.types.generateSdk,
499
+ globalRoute: pythonDefaults.types.globalRoute ?? false,
490
500
  debounce: 300
491
501
  };
492
502
  }
@@ -502,6 +512,7 @@ function resolvePluginConfig(config) {
502
512
  pagePropsPath: resolvedConfig.types.pagePropsPath ?? (resolvedConfig.types.output ? path.join(resolvedConfig.types.output, "inertia-pages.json") : "src/generated/inertia-pages.json"),
503
513
  generateZod: resolvedConfig.types.generateZod ?? false,
504
514
  generateSdk: resolvedConfig.types.generateSdk ?? false,
515
+ globalRoute: resolvedConfig.types.globalRoute ?? false,
505
516
  debounce: resolvedConfig.types.debounce ?? 300
506
517
  };
507
518
  if (!userProvidedOpenapi && resolvedConfig.types.output) {
@@ -514,23 +525,55 @@ function resolvePluginConfig(config) {
514
525
  typesConfig.pagePropsPath = path.join(typesConfig.output, "inertia-pages.json");
515
526
  }
516
527
  }
517
- return {
528
+ const inertiaMode = resolvedConfig.inertiaMode ?? pythonDefaults?.mode === "inertia";
529
+ const result = {
518
530
  input: resolvedConfig.input,
519
531
  assetUrl: normalizeAssetUrl(resolvedConfig.assetUrl ?? pythonDefaults?.assetUrl ?? "/static/"),
520
- resourceDirectory: resolvedConfig.resourceDirectory ?? pythonDefaults?.resourceDir ?? "resources",
521
- bundleDirectory: resolvedConfig.bundleDirectory ?? pythonDefaults?.bundleDir ?? "public",
532
+ resourceDir: resolvedConfig.resourceDir ?? pythonDefaults?.resourceDir ?? "src",
533
+ bundleDir: resolvedConfig.bundleDir ?? pythonDefaults?.bundleDir ?? "public",
522
534
  publicDir: resolvedConfig.publicDir ?? pythonDefaults?.publicDir ?? "public",
523
535
  ssr: resolvedConfig.ssr ?? resolvedConfig.input,
524
- ssrOutputDirectory: resolvedConfig.ssrOutputDirectory ?? pythonDefaults?.ssrOutDir ?? path.join(resolvedConfig.resourceDirectory ?? pythonDefaults?.resourceDir ?? "resources", "bootstrap/ssr"),
536
+ ssrOutDir: resolvedConfig.ssrOutDir ?? pythonDefaults?.ssrOutDir ?? path.join(resolvedConfig.resourceDir ?? pythonDefaults?.resourceDir ?? "src", "bootstrap/ssr"),
525
537
  refresh: resolvedConfig.refresh ?? false,
526
- hotFile: resolvedConfig.hotFile ?? path.join(resolvedConfig.bundleDirectory ?? "public", "hot"),
538
+ hotFile: resolvedConfig.hotFile ?? path.join(resolvedConfig.bundleDir ?? "public", "hot"),
527
539
  detectTls: resolvedConfig.detectTls ?? false,
528
540
  autoDetectIndex: resolvedConfig.autoDetectIndex ?? true,
541
+ inertiaMode,
529
542
  transformOnServe: resolvedConfig.transformOnServe ?? ((code) => code),
530
543
  types: typesConfig,
531
544
  executor: resolvedConfig.executor ?? pythonDefaults?.executor,
532
545
  hasPythonConfig: pythonDefaults !== null
533
546
  };
547
+ validateAgainstPythonDefaults(result, pythonDefaults, resolvedConfig);
548
+ return result;
549
+ }
550
+ function validateAgainstPythonDefaults(resolved, pythonDefaults, userConfig) {
551
+ if (!pythonDefaults) return;
552
+ const warnings = [];
553
+ const hasPythonValue = (value) => typeof value === "string" && value.length > 0;
554
+ if (userConfig.assetUrl !== void 0 && hasPythonValue(pythonDefaults.assetUrl) && resolved.assetUrl !== pythonDefaults.assetUrl) {
555
+ warnings.push(`assetUrl: vite.config.ts="${resolved.assetUrl}" differs from Python="${pythonDefaults.assetUrl}"`);
556
+ }
557
+ if (userConfig.bundleDir !== void 0 && hasPythonValue(pythonDefaults.bundleDir) && resolved.bundleDir !== pythonDefaults.bundleDir) {
558
+ warnings.push(`bundleDir: vite.config.ts="${resolved.bundleDir}" differs from Python="${pythonDefaults.bundleDir}"`);
559
+ }
560
+ if (userConfig.resourceDir !== void 0 && hasPythonValue(pythonDefaults.resourceDir) && resolved.resourceDir !== pythonDefaults.resourceDir) {
561
+ warnings.push(`resourceDir: vite.config.ts="${resolved.resourceDir}" differs from Python="${pythonDefaults.resourceDir}"`);
562
+ }
563
+ if (userConfig.publicDir !== void 0 && hasPythonValue(pythonDefaults.publicDir) && resolved.publicDir !== pythonDefaults.publicDir) {
564
+ warnings.push(`publicDir: vite.config.ts="${resolved.publicDir}" differs from Python="${pythonDefaults.publicDir}"`);
565
+ }
566
+ if (pythonDefaults.ssrEnabled && userConfig.ssrOutDir !== void 0 && hasPythonValue(pythonDefaults.ssrOutDir) && resolved.ssrOutDir !== pythonDefaults.ssrOutDir) {
567
+ warnings.push(`ssrOutDir: vite.config.ts="${resolved.ssrOutDir}" differs from Python="${pythonDefaults.ssrOutDir}"`);
568
+ }
569
+ if (warnings.length > 0) {
570
+ console.warn(
571
+ colors.yellow("[litestar-vite] Configuration mismatch detected:\n") + warnings.map((w) => ` ${colors.dim("\u2022")} ${w}`).join("\n") + `
572
+
573
+ ${colors.dim("Precedence: vite.config.ts > .litestar.json > defaults")}
574
+ ` + colors.dim("See: https://docs.litestar.dev/vite/config-precedence\n")
575
+ );
576
+ }
534
577
  }
535
578
  function resolveBase(_config, assetUrl) {
536
579
  if (process.env.NODE_ENV === "development") {
@@ -546,9 +589,9 @@ function resolveInput(config, ssr) {
546
589
  }
547
590
  function resolveOutDir(config, ssr) {
548
591
  if (ssr) {
549
- return config.ssrOutputDirectory.replace(/^\/+/, "").replace(/\/+$/, "");
592
+ return config.ssrOutDir.replace(/^\/+/, "").replace(/\/+$/, "");
550
593
  }
551
- return config.bundleDirectory.replace(/^\/+/, "").replace(/\/+$/, "");
594
+ return config.bundleDir.replace(/^\/+/, "").replace(/\/+$/, "");
552
595
  }
553
596
  function resolveFullReloadConfig({ refresh: config }) {
554
597
  if (typeof config === "boolean") {
@@ -717,7 +760,7 @@ declare module "litestar-vite/inertia" {
717
760
  `;
718
761
  await fs.promises.writeFile(outFile, body, "utf-8");
719
762
  }
720
- async function emitRouteTypes(routesPath, outputDir) {
763
+ async function emitRouteTypes(routesPath, outputDir, globalRoute = false) {
721
764
  const contents = await fs.promises.readFile(routesPath, "utf-8");
722
765
  const json = JSON.parse(contents);
723
766
  const outDir = path.resolve(process.cwd(), outputDir);
@@ -830,12 +873,23 @@ declare global {
830
873
  */
831
874
  routes?: Record<string, string>
832
875
  serverRoutes?: Record<string, string>
876
+ /**
877
+ * Global route helper (available when globalRoute=true in TypeGenConfig).
878
+ * @see route
879
+ */
880
+ route?: typeof route
833
881
  }
834
882
  }
835
883
 
836
884
  // Re-export helper functions from litestar-vite-plugin
837
885
  // These work with the routes defined above
838
886
  export { getCsrfToken, csrfHeaders, csrfFetch } from "litestar-vite-plugin/helpers"
887
+ ${globalRoute ? `
888
+ // Register route() globally on window for Laravel/Ziggy-style usage
889
+ if (typeof window !== "undefined") {
890
+ window.route = route
891
+ }
892
+ ` : ""}
839
893
  `;
840
894
  await fs.promises.writeFile(outFile, `${banner}${body}`, "utf-8");
841
895
  }
@@ -864,10 +918,10 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
864
918
  chosenConfigPath = configPath;
865
919
  const shouldRunOpenApiTs = configPath || typesConfig.generateSdk;
866
920
  if (fs.existsSync(openapiPath) && shouldRunOpenApiTs) {
867
- resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("generating TypeScript types...")}`);
868
- if (resolvedConfig) {
869
- const relConfigPath = configPath ? path.relative(resolvedConfig.root, configPath) : null;
870
- resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("openapi-ts config: ")}${relConfigPath ?? "<built-in defaults>"}`);
921
+ resolvedConfig?.logger.info(`${colors.cyan("\u2022")} Generating TypeScript types...`);
922
+ if (resolvedConfig && configPath) {
923
+ const relConfigPath = formatPath(configPath, resolvedConfig.root);
924
+ resolvedConfig.logger.info(`${colors.cyan("\u2022")} openapi-ts config: ${relConfigPath}`);
871
925
  }
872
926
  const sdkOutput = path.join(typesConfig.output, "api");
873
927
  let args;
@@ -890,7 +944,7 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
890
944
  try {
891
945
  require.resolve("zod", { paths: [process.cwd()] });
892
946
  } catch {
893
- resolvedConfig?.logger.warn(`${colors.cyan("litestar-vite")} ${colors.yellow("zod not installed")} - run: ${resolveInstallHint()} zod`);
947
+ resolvedConfig?.logger.warn(`${colors.yellow("!")} zod not installed - run: ${resolveInstallHint()} zod`);
894
948
  }
895
949
  }
896
950
  await execAsync(resolvePackageExecutor(args.join(" "), executor), {
@@ -899,7 +953,7 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
899
953
  generated = true;
900
954
  }
901
955
  if (fs.existsSync(routesPath)) {
902
- await emitRouteTypes(routesPath, typesConfig.output);
956
+ await emitRouteTypes(routesPath, typesConfig.output, typesConfig.globalRoute ?? false);
903
957
  generated = true;
904
958
  }
905
959
  if (fs.existsSync(pagePropsPath)) {
@@ -908,7 +962,7 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
908
962
  }
909
963
  if (generated && resolvedConfig) {
910
964
  const duration = Date.now() - startTime;
911
- resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.green("TypeScript artifacts updated")} ${colors.dim(`in ${duration}ms`)}`);
965
+ resolvedConfig.logger.info(`${colors.green("\u2713")} TypeScript artifacts updated ${colors.dim(`(${duration}ms)`)}`);
912
966
  }
913
967
  if (generated && server) {
914
968
  server.ws.send({
@@ -949,12 +1003,12 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
949
1003
  server = devServer;
950
1004
  if (typesConfig.enabled) {
951
1005
  const root = resolvedConfig?.root ?? process.cwd();
952
- const openapiRel = typesConfig.openapiPath;
953
- const routesRel = typesConfig.routesPath;
954
- resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("watching schema/routes:")} ${colors.yellow(openapiRel)}, ${colors.yellow(routesRel)}`);
1006
+ const openapiRel = path.basename(typesConfig.openapiPath);
1007
+ const routesRel = path.basename(typesConfig.routesPath);
1008
+ resolvedConfig?.logger.info(`${colors.cyan("\u2022")} Watching: ${colors.yellow(openapiRel)}, ${colors.yellow(routesRel)}`);
955
1009
  if (chosenConfigPath) {
956
- const relConfigPath = path.relative(root, chosenConfigPath);
957
- resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("openapi-ts config:")} ${colors.yellow(relConfigPath)}`);
1010
+ const relConfigPath = formatPath(chosenConfigPath, root);
1011
+ resolvedConfig?.logger.info(`${colors.cyan("\u2022")} openapi-ts config: ${colors.yellow(relConfigPath)}`);
958
1012
  }
959
1013
  }
960
1014
  },
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Path formatting utilities for consistent logging output.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Format an absolute path as relative to the project root for cleaner logging.
8
+ *
9
+ * @param absolutePath - The absolute path to format
10
+ * @param root - The project root directory (defaults to process.cwd())
11
+ * @returns The path relative to root, or the original path if already relative or on different drive
12
+ */
13
+ export declare function formatPath(absolutePath: string, root?: string): string;
14
+ /**
15
+ * Format multiple paths, joining them with a separator.
16
+ *
17
+ * @param paths - Array of absolute paths to format
18
+ * @param root - The project root directory (defaults to process.cwd())
19
+ * @param separator - Separator between paths (defaults to ", ")
20
+ * @returns Formatted paths joined by separator
21
+ */
22
+ export declare function formatPaths(paths: string[], root?: string, separator?: string): string;
@@ -0,0 +1,24 @@
1
+ import path from "node:path";
2
+ function formatPath(absolutePath, root) {
3
+ if (!absolutePath) return absolutePath;
4
+ const projectRoot = root ?? process.cwd();
5
+ if (!path.isAbsolute(absolutePath)) {
6
+ return absolutePath;
7
+ }
8
+ try {
9
+ const relativePath = path.relative(projectRoot, absolutePath);
10
+ if (path.isAbsolute(relativePath)) {
11
+ return absolutePath;
12
+ }
13
+ return relativePath;
14
+ } catch {
15
+ return absolutePath;
16
+ }
17
+ }
18
+ function formatPaths(paths, root, separator = ", ") {
19
+ return paths.map((p) => formatPath(p, root)).join(separator);
20
+ }
21
+ export {
22
+ formatPath,
23
+ formatPaths
24
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Logging utilities for litestar-vite with configurable output.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Logging configuration matching the Python LoggingConfig.
8
+ */
9
+ export interface LoggingConfig {
10
+ level: "quiet" | "normal" | "verbose";
11
+ showPathsAbsolute: boolean;
12
+ suppressNpmOutput: boolean;
13
+ suppressViteBanner: boolean;
14
+ timestamps: boolean;
15
+ }
16
+ /**
17
+ * Default logging configuration.
18
+ */
19
+ export declare const defaultLoggingConfig: LoggingConfig;
20
+ /**
21
+ * Logger instance with configurable behavior.
22
+ */
23
+ export interface Logger {
24
+ /** Log a message at normal level */
25
+ info: (message: string) => void;
26
+ /** Log a message at verbose level */
27
+ debug: (message: string) => void;
28
+ /** Log a warning (always shown except in quiet mode) */
29
+ warn: (message: string) => void;
30
+ /** Log an error (always shown) */
31
+ error: (message: string) => void;
32
+ /** Format a path according to config (relative or absolute) */
33
+ path: (absolutePath: string, root?: string) => string;
34
+ /** Get the current config */
35
+ config: LoggingConfig;
36
+ }
37
+ /**
38
+ * Create a logger instance with the given configuration.
39
+ *
40
+ * @param config - Logging configuration (partial, will be merged with defaults)
41
+ * @returns A Logger instance
42
+ */
43
+ export declare function createLogger(config?: Partial<LoggingConfig> | null): Logger;
@@ -0,0 +1,59 @@
1
+ import { formatPath } from "./format-path.js";
2
+ const defaultLoggingConfig = {
3
+ level: "normal",
4
+ showPathsAbsolute: false,
5
+ suppressNpmOutput: false,
6
+ suppressViteBanner: false,
7
+ timestamps: false
8
+ };
9
+ const LOG_LEVELS = {
10
+ quiet: 0,
11
+ normal: 1,
12
+ verbose: 2
13
+ };
14
+ function createLogger(config) {
15
+ const mergedConfig = {
16
+ ...defaultLoggingConfig,
17
+ ...config
18
+ };
19
+ const levelNum = LOG_LEVELS[mergedConfig.level];
20
+ const formatMessage = (message) => {
21
+ if (mergedConfig.timestamps) {
22
+ const now = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
23
+ return `[${now}] ${message}`;
24
+ }
25
+ return message;
26
+ };
27
+ const formatPathValue = (absolutePath, root) => {
28
+ if (mergedConfig.showPathsAbsolute) {
29
+ return absolutePath;
30
+ }
31
+ return formatPath(absolutePath, root);
32
+ };
33
+ return {
34
+ info: (message) => {
35
+ if (levelNum >= LOG_LEVELS.normal) {
36
+ console.log(formatMessage(message));
37
+ }
38
+ },
39
+ debug: (message) => {
40
+ if (levelNum >= LOG_LEVELS.verbose) {
41
+ console.log(formatMessage(message));
42
+ }
43
+ },
44
+ warn: (message) => {
45
+ if (levelNum >= LOG_LEVELS.normal) {
46
+ console.warn(formatMessage(message));
47
+ }
48
+ },
49
+ error: (message) => {
50
+ console.error(formatMessage(message));
51
+ },
52
+ path: formatPathValue,
53
+ config: mergedConfig
54
+ };
55
+ }
56
+ export {
57
+ createLogger,
58
+ defaultLoggingConfig
59
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litestar-vite-plugin",
3
- "version": "0.15.0-alpha.7",
3
+ "version": "0.15.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "Litestar plugin for Vite.",
6
6
  "keywords": [
@@ -55,7 +55,7 @@
55
55
  "build": "npm run build-plugin && npm run build-helpers && npm run build-inertia-helpers && npm run build-integrations",
56
56
  "build-plugin": "rm -rf dist/js && npm run build-plugin-types && npm run build-plugin-esm && cp src/js/src/dev-server-index.html dist/js/",
57
57
  "build-plugin-types": "tsc --project src/js/tsconfig.json --emitDeclarationOnly",
58
- "build-plugin-esm": "esbuild src/js/src/index.ts --platform=node --format=esm --outfile=dist/js/index.js && esbuild src/js/src/install-hint.ts --platform=node --format=esm --outfile=dist/js/install-hint.js && esbuild src/js/src/litestar-meta.ts --platform=node --format=esm --outfile=dist/js/litestar-meta.js && mkdir -p dist/js/shared && esbuild src/js/src/shared/debounce.ts --platform=node --format=esm --outfile=dist/js/shared/debounce.js",
58
+ "build-plugin-esm": "esbuild src/js/src/index.ts --platform=node --format=esm --outfile=dist/js/index.js && esbuild src/js/src/install-hint.ts --platform=node --format=esm --outfile=dist/js/install-hint.js && esbuild src/js/src/litestar-meta.ts --platform=node --format=esm --outfile=dist/js/litestar-meta.js && mkdir -p dist/js/shared && esbuild src/js/src/shared/debounce.ts src/js/src/shared/format-path.ts src/js/src/shared/logger.ts --platform=node --format=esm --outdir=dist/js/shared",
59
59
  "build-helpers": "rm -rf dist/js/helpers && tsc --project src/js/tsconfig.helpers.json",
60
60
  "build-inertia-helpers": "rm -rf dist/js/inertia-helpers && tsc --project src/js/tsconfig.inertia-helpers.json",
61
61
  "build-integrations": "esbuild src/js/src/astro.ts src/js/src/sveltekit.ts src/js/src/nuxt.ts --platform=node --format=esm --outdir=dist/js",