litestar-vite-plugin 0.15.0-alpha.6 → 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
 
@@ -35,6 +35,13 @@ export interface TypesConfig {
35
35
  * @default 'routes.json'
36
36
  */
37
37
  routesPath?: string;
38
+ /**
39
+ * Path where Inertia page props metadata is exported by Litestar.
40
+ * The Vite plugin watches this file for page props type generation.
41
+ *
42
+ * @default 'inertia-pages.json'
43
+ */
44
+ pagePropsPath?: string;
38
45
  /**
39
46
  * Generate Zod schemas in addition to TypeScript types.
40
47
  *
@@ -47,6 +54,16 @@ export interface TypesConfig {
47
54
  * @default false
48
55
  */
49
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;
50
67
  /**
51
68
  * Debounce time in milliseconds for type regeneration.
52
69
  * Prevents regeneration from running too frequently when
@@ -72,7 +89,7 @@ export interface PluginConfig {
72
89
  *
73
90
  * @default 'public/dist'
74
91
  */
75
- bundleDirectory?: string;
92
+ bundleDir?: string;
76
93
  /**
77
94
  * Vite's public directory for static, unprocessed assets.
78
95
  * Mirrors Vite's `publicDir` option.
@@ -83,13 +100,13 @@ export interface PluginConfig {
83
100
  /**
84
101
  * Litestar's public assets directory. These are the assets that Vite will serve when developing.
85
102
  *
86
- * @default 'resources'
103
+ * @default 'src'
87
104
  */
88
- resourceDirectory?: string;
105
+ resourceDir?: string;
89
106
  /**
90
107
  * The path to the "hot" file.
91
108
  *
92
- * @default `${bundleDirectory}/hot`
109
+ * @default `${bundleDir}/hot`
93
110
  */
94
111
  hotFile?: string;
95
112
  /**
@@ -99,9 +116,9 @@ export interface PluginConfig {
99
116
  /**
100
117
  * The directory where the SSR bundle should be written.
101
118
  *
102
- * @default '${bundleDirectory}/bootstrap/ssr'
119
+ * @default '${bundleDir}/bootstrap/ssr'
103
120
  */
104
- ssrOutputDirectory?: string;
121
+ ssrOutDir?: string;
105
122
  /**
106
123
  * Configuration for performing full page refresh on python (or other) file changes.
107
124
  *
@@ -121,6 +138,18 @@ export interface PluginConfig {
121
138
  * @default true
122
139
  */
123
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;
124
153
  /**
125
154
  * Transform the code while serving.
126
155
  */
@@ -186,6 +215,50 @@ interface RefreshConfig {
186
215
  paths: string[];
187
216
  config?: FullReloadConfig;
188
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
+ }
189
262
  type DevServerUrl = `${"http" | "https"}://${string}:${number}`;
190
263
  export declare const refreshPaths: string[];
191
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,12 +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(
201
- `${colors.cyan("litestar-vite")} ${colors.yellow("Resource directory not found:")} ${resourceDir}
202
- Expected directory: ${colors.dim(pluginConfig.resourceDirectory)}`
203
- );
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}`);
204
207
  }
205
208
  const hint = pluginConfig.types !== false ? pluginConfig.types.routesPath : void 0;
206
209
  litestarMeta = await loadLitestarMeta(resolvedConfig, hint);
@@ -232,19 +235,20 @@ function resolveLitestarPlugin(pluginConfig) {
232
235
  fs.writeFileSync(pluginConfig.hotFile, viteDevServerUrl);
233
236
  }
234
237
  setTimeout(async () => {
235
- 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";
236
240
  const backendStatus = await checkBackendAvailability(appUrl);
237
241
  resolvedConfig.logger.info(`
238
- ${colors.red(`${colors.bold("LITESTAR")} ${version}`)}`);
242
+ ${colors.red(`${colors.bold("LITESTAR")} ${litestarVersion}`)}`);
239
243
  resolvedConfig.logger.info("");
240
244
  if (initialIndexPath) {
241
- resolvedConfig.logger.info(
242
- ` ${colors.green("\u279C")} ${colors.bold("Index Mode")}: SPA (Serving ${colors.cyan(path.relative(server.config.root, initialIndexPath))} from root)`
243
- );
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`);
244
249
  } else {
245
- 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`);
246
251
  }
247
- resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Dev Server")}: ${colors.cyan(viteDevServerUrl)}`);
248
252
  if (backendStatus.available) {
249
253
  resolvedConfig.logger.info(
250
254
  ` ${colors.green("\u279C")} ${colors.bold("App URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))} ${colors.green("\u2713")}`
@@ -254,12 +258,13 @@ function resolveLitestarPlugin(pluginConfig) {
254
258
  ` ${colors.yellow("\u279C")} ${colors.bold("App URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))} ${colors.yellow("\u26A0")}`
255
259
  );
256
260
  }
257
- 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)}`);
258
262
  if (pluginConfig.types !== false && pluginConfig.types.enabled) {
259
263
  const openapiExists = fs.existsSync(path.resolve(process.cwd(), pluginConfig.types.openapiPath));
260
264
  const routesExists = fs.existsSync(path.resolve(process.cwd(), pluginConfig.types.routesPath));
265
+ const relTypesOutput = logger.path(pluginConfig.types.output, process.cwd());
261
266
  if (openapiExists || routesExists) {
262
- 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}/`)}`);
263
268
  } else {
264
269
  resolvedConfig.logger.info(` ${colors.yellow("\u279C")} ${colors.bold("Type Gen")}: ${colors.yellow("waiting")} ${colors.dim("(no schema files yet)")}`);
265
270
  }
@@ -320,12 +325,13 @@ function resolveLitestarPlugin(pluginConfig) {
320
325
  res.end(transformedHtml);
321
326
  return;
322
327
  } catch (e) {
323
- resolvedConfig.logger.error(`Error serving index.html from ${indexPath}: ${e instanceof Error ? e.message : e}`);
328
+ const relIndexPath = path.relative(server.config.root, indexPath);
329
+ resolvedConfig.logger.error(`Error serving index.html from ${relIndexPath}: ${e instanceof Error ? e.message : e}`);
324
330
  next(e);
325
331
  return;
326
332
  }
327
333
  }
328
- if (!indexPath && req.url === "/index.html") {
334
+ if (!indexPath && (req.url === "/" || req.url === "/index.html")) {
329
335
  try {
330
336
  const placeholderPath = path.join(dirname(), "dev-server-index.html");
331
337
  const placeholderContent = await fs.promises.readFile(placeholderPath, "utf-8");
@@ -415,7 +421,7 @@ function formatMissingConfigWarning() {
415
421
  `${y("\u2502")} ${d("litestar({")} ${y("\u2502")}`,
416
422
  `${y("\u2502")} ${d(' input: ["src/main.tsx"],')} ${y("\u2502")}`,
417
423
  `${y("\u2502")} ${d(' assetUrl: "/static/",')} ${y("\u2502")}`,
418
- `${y("\u2502")} ${d(' bundleDirectory: "public",')} ${y("\u2502")}`,
424
+ `${y("\u2502")} ${d(' bundleDir: "public",')} ${y("\u2502")}`,
419
425
  `${y("\u2502")} ${d(" types: false,")} ${y("\u2502")}`,
420
426
  `${y("\u2502")} ${d("})")} ${y("\u2502")}`,
421
427
  `${y("\u2502")} ${y("\u2502")}`,
@@ -442,16 +448,16 @@ function resolvePluginConfig(config) {
442
448
  if (typeof resolvedConfig.input === "undefined") {
443
449
  throw new Error('litestar-vite-plugin: missing configuration for "input".');
444
450
  }
445
- if (typeof resolvedConfig.resourceDirectory === "string") {
446
- resolvedConfig.resourceDirectory = resolvedConfig.resourceDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, "");
447
- if (resolvedConfig.resourceDirectory === "") {
448
- 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'.");
449
455
  }
450
456
  }
451
- if (typeof resolvedConfig.bundleDirectory === "string") {
452
- resolvedConfig.bundleDirectory = resolvedConfig.bundleDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, "");
453
- if (resolvedConfig.bundleDirectory === "") {
454
- 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'.");
455
461
  }
456
462
  }
457
463
  if (typeof resolvedConfig.publicDir === "string") {
@@ -460,8 +466,8 @@ function resolvePluginConfig(config) {
460
466
  throw new Error("litestar-vite-plugin: publicDir must be a subdirectory. E.g. 'public'.");
461
467
  }
462
468
  }
463
- if (typeof resolvedConfig.ssrOutputDirectory === "string") {
464
- resolvedConfig.ssrOutputDirectory = resolvedConfig.ssrOutputDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, "");
469
+ if (typeof resolvedConfig.ssrOutDir === "string") {
470
+ resolvedConfig.ssrOutDir = resolvedConfig.ssrOutDir.trim().replace(/^\/+/, "").replace(/\/+$/, "");
465
471
  }
466
472
  if (resolvedConfig.refresh === true) {
467
473
  resolvedConfig.refresh = [{ paths: refreshPaths }];
@@ -474,8 +480,10 @@ function resolvePluginConfig(config) {
474
480
  output: "src/generated/types",
475
481
  openapiPath: "src/generated/openapi.json",
476
482
  routesPath: "src/generated/routes.json",
483
+ pagePropsPath: "src/generated/inertia-pages.json",
477
484
  generateZod: false,
478
485
  generateSdk: false,
486
+ globalRoute: false,
479
487
  debounce: 300
480
488
  };
481
489
  } else if (resolvedConfig.types === "auto" || typeof resolvedConfig.types === "undefined") {
@@ -485,21 +493,26 @@ function resolvePluginConfig(config) {
485
493
  output: pythonDefaults.types.output,
486
494
  openapiPath: pythonDefaults.types.openapiPath,
487
495
  routesPath: pythonDefaults.types.routesPath,
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
  }
493
503
  } else if (typeof resolvedConfig.types === "object" && resolvedConfig.types !== null) {
494
504
  const userProvidedOpenapi = Object.hasOwn(resolvedConfig.types, "openapiPath");
495
505
  const userProvidedRoutes = Object.hasOwn(resolvedConfig.types, "routesPath");
506
+ const userProvidedPageProps = Object.hasOwn(resolvedConfig.types, "pagePropsPath");
496
507
  typesConfig = {
497
508
  enabled: resolvedConfig.types.enabled ?? true,
498
509
  output: resolvedConfig.types.output ?? "src/generated/types",
499
510
  openapiPath: resolvedConfig.types.openapiPath ?? (resolvedConfig.types.output ? path.join(resolvedConfig.types.output, "openapi.json") : "src/generated/openapi.json"),
500
511
  routesPath: resolvedConfig.types.routesPath ?? (resolvedConfig.types.output ? path.join(resolvedConfig.types.output, "routes.json") : "src/generated/routes.json"),
512
+ pagePropsPath: resolvedConfig.types.pagePropsPath ?? (resolvedConfig.types.output ? path.join(resolvedConfig.types.output, "inertia-pages.json") : "src/generated/inertia-pages.json"),
501
513
  generateZod: resolvedConfig.types.generateZod ?? false,
502
514
  generateSdk: resolvedConfig.types.generateSdk ?? false,
515
+ globalRoute: resolvedConfig.types.globalRoute ?? false,
503
516
  debounce: resolvedConfig.types.debounce ?? 300
504
517
  };
505
518
  if (!userProvidedOpenapi && resolvedConfig.types.output) {
@@ -508,24 +521,59 @@ function resolvePluginConfig(config) {
508
521
  if (!userProvidedRoutes && resolvedConfig.types.output) {
509
522
  typesConfig.routesPath = path.join(typesConfig.output, "routes.json");
510
523
  }
524
+ if (!userProvidedPageProps && resolvedConfig.types.output) {
525
+ typesConfig.pagePropsPath = path.join(typesConfig.output, "inertia-pages.json");
526
+ }
511
527
  }
512
- return {
528
+ const inertiaMode = resolvedConfig.inertiaMode ?? pythonDefaults?.mode === "inertia";
529
+ const result = {
513
530
  input: resolvedConfig.input,
514
531
  assetUrl: normalizeAssetUrl(resolvedConfig.assetUrl ?? pythonDefaults?.assetUrl ?? "/static/"),
515
- resourceDirectory: resolvedConfig.resourceDirectory ?? pythonDefaults?.resourceDir ?? "resources",
516
- bundleDirectory: resolvedConfig.bundleDirectory ?? pythonDefaults?.bundleDir ?? "public",
532
+ resourceDir: resolvedConfig.resourceDir ?? pythonDefaults?.resourceDir ?? "src",
533
+ bundleDir: resolvedConfig.bundleDir ?? pythonDefaults?.bundleDir ?? "public",
517
534
  publicDir: resolvedConfig.publicDir ?? pythonDefaults?.publicDir ?? "public",
518
535
  ssr: resolvedConfig.ssr ?? resolvedConfig.input,
519
- 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"),
520
537
  refresh: resolvedConfig.refresh ?? false,
521
- hotFile: resolvedConfig.hotFile ?? path.join(resolvedConfig.bundleDirectory ?? "public", "hot"),
538
+ hotFile: resolvedConfig.hotFile ?? path.join(resolvedConfig.bundleDir ?? "public", "hot"),
522
539
  detectTls: resolvedConfig.detectTls ?? false,
523
540
  autoDetectIndex: resolvedConfig.autoDetectIndex ?? true,
541
+ inertiaMode,
524
542
  transformOnServe: resolvedConfig.transformOnServe ?? ((code) => code),
525
543
  types: typesConfig,
526
544
  executor: resolvedConfig.executor ?? pythonDefaults?.executor,
527
545
  hasPythonConfig: pythonDefaults !== null
528
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
+ }
529
577
  }
530
578
  function resolveBase(_config, assetUrl) {
531
579
  if (process.env.NODE_ENV === "development") {
@@ -541,9 +589,9 @@ function resolveInput(config, ssr) {
541
589
  }
542
590
  function resolveOutDir(config, ssr) {
543
591
  if (ssr) {
544
- return config.ssrOutputDirectory.replace(/^\/+/, "").replace(/\/+$/, "");
592
+ return config.ssrOutDir.replace(/^\/+/, "").replace(/\/+$/, "");
545
593
  }
546
- return config.bundleDirectory.replace(/^\/+/, "").replace(/\/+$/, "");
594
+ return config.bundleDir.replace(/^\/+/, "").replace(/\/+$/, "");
547
595
  }
548
596
  function resolveFullReloadConfig({ refresh: config }) {
549
597
  if (typeof config === "boolean") {
@@ -564,7 +612,155 @@ function resolveFullReloadConfig({ refresh: config }) {
564
612
  return plugin;
565
613
  });
566
614
  }
567
- async function emitRouteTypes(routesPath, outputDir) {
615
+ async function emitPagePropsTypes(pagesPath, outputDir) {
616
+ const contents = await fs.promises.readFile(pagesPath, "utf-8");
617
+ const json = JSON.parse(contents);
618
+ const outDir = path.resolve(process.cwd(), outputDir);
619
+ await fs.promises.mkdir(outDir, { recursive: true });
620
+ const outFile = path.join(outDir, "page-props.ts");
621
+ const { includeDefaultAuth, includeDefaultFlash } = json.typeGenConfig;
622
+ let userTypes = "";
623
+ let authTypes = "";
624
+ let flashTypes = "";
625
+ if (includeDefaultAuth) {
626
+ userTypes = `/**
627
+ * Default User interface - minimal baseline for common auth patterns.
628
+ * Users extend this via module augmentation with their full user model.
629
+ *
630
+ * @example
631
+ * declare module 'litestar-vite/inertia' {
632
+ * interface User {
633
+ * avatarUrl?: string | null
634
+ * roles: Role[]
635
+ * teams: Team[]
636
+ * }
637
+ * }
638
+ */
639
+ export interface User {
640
+ id: string
641
+ email: string
642
+ name?: string | null
643
+ }
644
+
645
+ `;
646
+ authTypes = `/**
647
+ * Default AuthData interface - mirrors Laravel Jetstream pattern.
648
+ * isAuthenticated + optional user is the universal pattern.
649
+ */
650
+ export interface AuthData {
651
+ isAuthenticated: boolean
652
+ user?: User
653
+ }
654
+
655
+ `;
656
+ } else {
657
+ userTypes = `/**
658
+ * User interface - define via module augmentation.
659
+ * Default auth types are disabled.
660
+ *
661
+ * @example
662
+ * declare module 'litestar-vite/inertia' {
663
+ * interface User {
664
+ * uuid: string
665
+ * username: string
666
+ * }
667
+ * }
668
+ */
669
+ export interface User {}
670
+
671
+ `;
672
+ authTypes = `/**
673
+ * AuthData interface - define via module augmentation.
674
+ * Default auth types are disabled.
675
+ */
676
+ export interface AuthData {}
677
+
678
+ `;
679
+ }
680
+ if (includeDefaultFlash) {
681
+ flashTypes = `/**
682
+ * Default FlashMessages interface - category to messages mapping.
683
+ * Standard categories: success, error, info, warning.
684
+ */
685
+ export interface FlashMessages {
686
+ [category: string]: string[]
687
+ }
688
+
689
+ `;
690
+ } else {
691
+ flashTypes = `/**
692
+ * FlashMessages interface - define via module augmentation.
693
+ * Default flash types are disabled.
694
+ */
695
+ export interface FlashMessages {}
696
+
697
+ `;
698
+ }
699
+ const sharedPropsContent = includeDefaultAuth || includeDefaultFlash ? ` auth?: AuthData
700
+ flash?: FlashMessages` : "";
701
+ const pageEntries = [];
702
+ for (const [component, data] of Object.entries(json.pages)) {
703
+ const propsType = data.propsType ? data.propsType : "Record<string, unknown>";
704
+ pageEntries.push(` "${component}": ${propsType} & FullSharedProps`);
705
+ }
706
+ const body = `// AUTO-GENERATED by litestar-vite. Do not edit.
707
+ /* eslint-disable */
708
+
709
+ ${userTypes}${authTypes}${flashTypes}/**
710
+ * Generated shared props (always present).
711
+ * Includes built-in props + static config props.
712
+ */
713
+ export interface GeneratedSharedProps {
714
+ errors?: Record<string, string[]>
715
+ csrf_token?: string
716
+ }
717
+
718
+ /**
719
+ * User-defined shared props for dynamic share() calls in guards/middleware.
720
+ * Extend this interface via module augmentation.
721
+ *
722
+ * @example
723
+ * declare module 'litestar-vite/inertia' {
724
+ * interface User {
725
+ * avatarUrl?: string | null
726
+ * roles: Role[]
727
+ * teams: Team[]
728
+ * }
729
+ * interface SharedProps {
730
+ * locale?: string
731
+ * currentTeam?: CurrentTeam
732
+ * }
733
+ * }
734
+ */
735
+ export interface SharedProps {
736
+ ${sharedPropsContent}
737
+ }
738
+
739
+ /** Full shared props = generated + user-defined */
740
+ export type FullSharedProps = GeneratedSharedProps & SharedProps
741
+
742
+ /** Page props mapped by component name */
743
+ export interface PageProps {
744
+ ${pageEntries.join("\n")}
745
+ }
746
+
747
+ /** Component name union type */
748
+ export type ComponentName = keyof PageProps
749
+
750
+ /** Type-safe props for a specific component */
751
+ export type InertiaPageProps<C extends ComponentName> = PageProps[C]
752
+
753
+ /** Get props type for a specific page component */
754
+ export type PagePropsFor<C extends ComponentName> = PageProps[C]
755
+
756
+ // Re-export for module augmentation
757
+ declare module "litestar-vite/inertia" {
758
+ export { User, AuthData, FlashMessages, SharedProps, GeneratedSharedProps, FullSharedProps, PageProps, ComponentName, InertiaPageProps, PagePropsFor }
759
+ }
760
+ `;
761
+ await fs.promises.writeFile(outFile, body, "utf-8");
762
+ }
763
+ async function emitRouteTypes(routesPath, outputDir, globalRoute = false) {
568
764
  const contents = await fs.promises.readFile(routesPath, "utf-8");
569
765
  const json = JSON.parse(contents);
570
766
  const outDir = path.resolve(process.cwd(), outputDir);
@@ -677,18 +873,30 @@ declare global {
677
873
  */
678
874
  routes?: Record<string, string>
679
875
  serverRoutes?: Record<string, string>
876
+ /**
877
+ * Global route helper (available when globalRoute=true in TypeGenConfig).
878
+ * @see route
879
+ */
880
+ route?: typeof route
680
881
  }
681
882
  }
682
883
 
683
884
  // Re-export helper functions from litestar-vite-plugin
684
885
  // These work with the routes defined above
685
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
+ ` : ""}
686
893
  `;
687
894
  await fs.promises.writeFile(outFile, `${banner}${body}`, "utf-8");
688
895
  }
689
896
  function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
690
897
  let lastTypesHash = null;
691
898
  let lastRoutesHash = null;
899
+ let lastPagePropsHash = null;
692
900
  let server = null;
693
901
  let isGenerating = false;
694
902
  let resolvedConfig = null;
@@ -703,15 +911,17 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
703
911
  const projectRoot = resolvedConfig?.root ?? process.cwd();
704
912
  const openapiPath = path.resolve(projectRoot, typesConfig.openapiPath);
705
913
  const routesPath = path.resolve(projectRoot, typesConfig.routesPath);
914
+ const pagePropsPath = path.resolve(projectRoot, typesConfig.pagePropsPath);
706
915
  let generated = false;
707
916
  const candidates = [path.resolve(projectRoot, "openapi-ts.config.ts"), path.resolve(projectRoot, "hey-api.config.ts"), path.resolve(projectRoot, ".hey-api.config.ts")];
708
917
  const configPath = candidates.find((p) => fs.existsSync(p)) || null;
709
918
  chosenConfigPath = configPath;
710
919
  const shouldRunOpenApiTs = configPath || typesConfig.generateSdk;
711
920
  if (fs.existsSync(openapiPath) && shouldRunOpenApiTs) {
712
- resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("generating TypeScript types...")}`);
713
- if (resolvedConfig) {
714
- resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("openapi-ts config: ")}${configPath ?? "<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}`);
715
925
  }
716
926
  const sdkOutput = path.join(typesConfig.output, "api");
717
927
  let args;
@@ -734,7 +944,7 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
734
944
  try {
735
945
  require.resolve("zod", { paths: [process.cwd()] });
736
946
  } catch {
737
- 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`);
738
948
  }
739
949
  }
740
950
  await execAsync(resolvePackageExecutor(args.join(" "), executor), {
@@ -743,12 +953,16 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
743
953
  generated = true;
744
954
  }
745
955
  if (fs.existsSync(routesPath)) {
746
- await emitRouteTypes(routesPath, typesConfig.output);
956
+ await emitRouteTypes(routesPath, typesConfig.output, typesConfig.globalRoute ?? false);
957
+ generated = true;
958
+ }
959
+ if (fs.existsSync(pagePropsPath)) {
960
+ await emitPagePropsTypes(pagePropsPath, typesConfig.output);
747
961
  generated = true;
748
962
  }
749
963
  if (generated && resolvedConfig) {
750
964
  const duration = Date.now() - startTime;
751
- 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)`)}`);
752
966
  }
753
967
  if (generated && server) {
754
968
  server.ws.send({
@@ -789,11 +1003,12 @@ function resolveTypeGenerationPlugin(typesConfig, executor, hasPythonConfig) {
789
1003
  server = devServer;
790
1004
  if (typesConfig.enabled) {
791
1005
  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)}`);
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)}`);
795
1009
  if (chosenConfigPath) {
796
- resolvedConfig?.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("openapi-ts config:")} ${colors.yellow(chosenConfigPath)}`);
1010
+ const relConfigPath = formatPath(chosenConfigPath, root);
1011
+ resolvedConfig?.logger.info(`${colors.cyan("\u2022")} openapi-ts config: ${colors.yellow(relConfigPath)}`);
797
1012
  }
798
1013
  }
799
1014
  },
@@ -831,17 +1046,24 @@ Solutions:
831
1046
  const relativePath = path.relative(root, file);
832
1047
  const openapiPath = typesConfig.openapiPath.replace(/^\.\//, "");
833
1048
  const routesPath = typesConfig.routesPath.replace(/^\.\//, "");
834
- if (relativePath === openapiPath || relativePath === routesPath || file.endsWith(openapiPath) || file.endsWith(routesPath)) {
1049
+ const pagePropsPath = typesConfig.pagePropsPath.replace(/^\.\//, "");
1050
+ const isOpenapi = relativePath === openapiPath || file.endsWith(openapiPath);
1051
+ const isRoutes = relativePath === routesPath || file.endsWith(routesPath);
1052
+ const isPageProps = relativePath === pagePropsPath || file.endsWith(pagePropsPath);
1053
+ if (isOpenapi || isRoutes || isPageProps) {
835
1054
  if (resolvedConfig) {
836
1055
  resolvedConfig.logger.info(`${colors.cyan("litestar-vite")} ${colors.dim("schema changed:")} ${colors.yellow(relativePath)}`);
837
1056
  }
838
1057
  const newHash = await hashFile(file);
839
- if (relativePath === openapiPath) {
1058
+ if (isOpenapi) {
840
1059
  if (lastTypesHash === newHash) return;
841
1060
  lastTypesHash = newHash;
842
- } else {
1061
+ } else if (isRoutes) {
843
1062
  if (lastRoutesHash === newHash) return;
844
1063
  lastRoutesHash = newHash;
1064
+ } else if (isPageProps) {
1065
+ if (lastPagePropsHash === newHash) return;
1066
+ lastPagePropsHash = newHash;
845
1067
  }
846
1068
  debouncedRunTypeGeneration();
847
1069
  }
@@ -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.6",
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",