litestar-vite-plugin 0.15.0-beta.4 → 0.15.0-beta.6

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.
@@ -19,7 +19,7 @@
19
19
  * apiProxy: 'http://localhost:8000',
20
20
  * types: {
21
21
  * enabled: true,
22
- * output: 'src/generated/api',
22
+ * output: 'src/generated',
23
23
  * },
24
24
  * }),
25
25
  * ],
@@ -104,7 +104,7 @@ export interface AstroTypesConfig {
104
104
  * Path to output generated TypeScript types.
105
105
  * Relative to the Astro project root.
106
106
  *
107
- * @default 'src/types/api'
107
+ * @default 'src/generated'
108
108
  */
109
109
  output?: string;
110
110
  /**
@@ -119,6 +119,12 @@ export interface AstroTypesConfig {
119
119
  * @default 'routes.json'
120
120
  */
121
121
  routesPath?: string;
122
+ /**
123
+ * Path where Inertia page props metadata is exported by Litestar.
124
+ *
125
+ * @default 'inertia-pages.json'
126
+ */
127
+ pagePropsPath?: string;
122
128
  /**
123
129
  * Generate Zod schemas in addition to TypeScript types.
124
130
  *
@@ -131,6 +137,24 @@ export interface AstroTypesConfig {
131
137
  * @default true
132
138
  */
133
139
  generateSdk?: boolean;
140
+ /**
141
+ * Generate typed routes.ts from routes.json metadata.
142
+ *
143
+ * @default true
144
+ */
145
+ generateRoutes?: boolean;
146
+ /**
147
+ * Generate Inertia page props types from inertia-pages.json metadata.
148
+ *
149
+ * @default true
150
+ */
151
+ generatePageProps?: boolean;
152
+ /**
153
+ * Register route() globally on window object.
154
+ *
155
+ * @default false
156
+ */
157
+ globalRoute?: boolean;
134
158
  /**
135
159
  * Debounce time in milliseconds for type regeneration.
136
160
  *
@@ -207,7 +231,7 @@ export interface LitestarAstroConfig {
207
231
  * // src/pages/users/[id].astro
208
232
  * ---
209
233
  * import type { User } from '../generated/api/types.gen';
210
- * import { route } from '../generated/api/routes';
234
+ * import { route } from '../generated/routes';
211
235
  *
212
236
  * const { id } = Astro.params;
213
237
  * const response = await fetch(route('users.show', { id }));
package/dist/js/astro.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { createTypeGenerationPlugin } from "./shared/create-type-gen-plugin.js";
3
+ import { readBridgeConfig } from "./shared/bridge-schema.js";
4
+ import { createLitestarTypeGenPlugin } from "./shared/typegen-plugin.js";
4
5
  function resolveConfig(config = {}) {
5
- const runtimeConfigPath = process.env.LITESTAR_VITE_CONFIG_PATH;
6
6
  let hotFile;
7
7
  let proxyMode = "vite";
8
8
  let port;
9
+ let pythonTypesConfig;
10
+ let pythonExecutor;
11
+ let hasPythonConfig = false;
9
12
  const envPort = process.env.VITE_PORT;
10
13
  if (envPort) {
11
14
  port = Number.parseInt(envPort, 10);
@@ -13,41 +16,61 @@ function resolveConfig(config = {}) {
13
16
  port = void 0;
14
17
  }
15
18
  }
16
- if (runtimeConfigPath && fs.existsSync(runtimeConfigPath)) {
17
- try {
18
- const json = JSON.parse(fs.readFileSync(runtimeConfigPath, "utf-8"));
19
- const bundleDir = json.bundleDir ?? "public";
20
- const hot = json.hotFile ?? "hot";
21
- hotFile = path.resolve(process.cwd(), bundleDir, hot);
22
- proxyMode = json.proxyMode ?? "vite";
23
- if (json.port !== void 0) {
24
- port = json.port;
25
- }
26
- } catch {
27
- hotFile = void 0;
19
+ const runtime = readBridgeConfig();
20
+ if (runtime) {
21
+ hasPythonConfig = true;
22
+ const hot = runtime.hotFile;
23
+ hotFile = path.isAbsolute(hot) ? hot : path.resolve(process.cwd(), runtime.bundleDir, hot);
24
+ proxyMode = runtime.proxyMode;
25
+ port = runtime.port;
26
+ pythonExecutor = runtime.executor;
27
+ if (runtime.types) {
28
+ pythonTypesConfig = runtime.types;
28
29
  }
29
30
  }
30
31
  let typesConfig = false;
31
32
  if (config.types === true) {
32
33
  typesConfig = {
33
34
  enabled: true,
34
- output: "src/types/api",
35
- openapiPath: "openapi.json",
36
- routesPath: "routes.json",
37
- generateZod: false,
38
- generateSdk: true,
35
+ output: pythonTypesConfig?.output ?? "src/generated",
36
+ openapiPath: pythonTypesConfig?.openapiPath ?? "openapi.json",
37
+ routesPath: pythonTypesConfig?.routesPath ?? "routes.json",
38
+ pagePropsPath: pythonTypesConfig?.pagePropsPath ?? "inertia-pages.json",
39
+ generateZod: pythonTypesConfig?.generateZod ?? false,
40
+ generateSdk: pythonTypesConfig?.generateSdk ?? true,
41
+ generateRoutes: pythonTypesConfig?.generateRoutes ?? true,
42
+ generatePageProps: pythonTypesConfig?.generatePageProps ?? true,
43
+ globalRoute: pythonTypesConfig?.globalRoute ?? false,
39
44
  debounce: 300
40
45
  };
41
46
  } else if (typeof config.types === "object" && config.types !== null) {
42
47
  typesConfig = {
43
48
  enabled: config.types.enabled ?? true,
44
- output: config.types.output ?? "src/types/api",
45
- openapiPath: config.types.openapiPath ?? "openapi.json",
46
- routesPath: config.types.routesPath ?? "routes.json",
47
- generateZod: config.types.generateZod ?? false,
48
- generateSdk: config.types.generateSdk ?? true,
49
+ output: config.types.output ?? pythonTypesConfig?.output ?? "src/generated",
50
+ openapiPath: config.types.openapiPath ?? pythonTypesConfig?.openapiPath ?? "openapi.json",
51
+ routesPath: config.types.routesPath ?? pythonTypesConfig?.routesPath ?? "routes.json",
52
+ pagePropsPath: config.types.pagePropsPath ?? pythonTypesConfig?.pagePropsPath ?? "inertia-pages.json",
53
+ generateZod: config.types.generateZod ?? pythonTypesConfig?.generateZod ?? false,
54
+ generateSdk: config.types.generateSdk ?? pythonTypesConfig?.generateSdk ?? true,
55
+ generateRoutes: config.types.generateRoutes ?? pythonTypesConfig?.generateRoutes ?? true,
56
+ generatePageProps: config.types.generatePageProps ?? pythonTypesConfig?.generatePageProps ?? true,
57
+ globalRoute: config.types.globalRoute ?? pythonTypesConfig?.globalRoute ?? false,
49
58
  debounce: config.types.debounce ?? 300
50
59
  };
60
+ } else if (config.types !== false && pythonTypesConfig?.enabled) {
61
+ typesConfig = {
62
+ enabled: true,
63
+ output: pythonTypesConfig.output ?? "src/generated",
64
+ openapiPath: pythonTypesConfig.openapiPath ?? "openapi.json",
65
+ routesPath: pythonTypesConfig.routesPath ?? "routes.json",
66
+ pagePropsPath: pythonTypesConfig.pagePropsPath ?? "inertia-pages.json",
67
+ generateZod: pythonTypesConfig.generateZod ?? false,
68
+ generateSdk: pythonTypesConfig.generateSdk ?? true,
69
+ generateRoutes: pythonTypesConfig.generateRoutes ?? true,
70
+ generatePageProps: pythonTypesConfig.generatePageProps ?? true,
71
+ globalRoute: pythonTypesConfig.globalRoute ?? false,
72
+ debounce: 300
73
+ };
51
74
  }
52
75
  return {
53
76
  apiProxy: config.apiProxy ?? "http://localhost:8000",
@@ -56,7 +79,9 @@ function resolveConfig(config = {}) {
56
79
  verbose: config.verbose ?? false,
57
80
  hotFile,
58
81
  proxyMode,
59
- port
82
+ port,
83
+ executor: pythonExecutor,
84
+ hasPythonConfig
60
85
  };
61
86
  }
62
87
  function createProxyPlugin(config) {
@@ -106,10 +131,12 @@ function litestarAstro(userConfig = {}) {
106
131
  const plugins = [createProxyPlugin(config)];
107
132
  if (config.types !== false && config.types.enabled) {
108
133
  plugins.push(
109
- createTypeGenerationPlugin(config.types, {
110
- frameworkName: "litestar-astro",
134
+ createLitestarTypeGenPlugin(config.types, {
111
135
  pluginName: "litestar-astro-types",
112
- clientPlugin: "@hey-api/client-fetch"
136
+ frameworkName: "litestar-astro",
137
+ sdkClientPlugin: "@hey-api/client-fetch",
138
+ executor: config.executor,
139
+ hasPythonConfig: config.hasPythonConfig
113
140
  })
114
141
  );
115
142
  }
@@ -28,21 +28,48 @@
28
28
  * @module
29
29
  */
30
30
  import { getCsrfToken } from "./csrf.js";
31
- let debug = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
31
+ function getHeadersFromConfigRequestEvent(evt) {
32
+ const detail = evt.detail;
33
+ if (!detail || typeof detail !== "object")
34
+ return null;
35
+ const { headers } = detail;
36
+ if (!headers || typeof headers !== "object")
37
+ return null;
38
+ return headers;
39
+ }
40
+ let debug = false;
32
41
  const cache = new Map();
33
42
  const memoStore = new WeakMap();
43
+ const registeredHtmxInstances = new WeakSet();
44
+ function runtime(node) {
45
+ let store = memoStore.get(node);
46
+ if (!store) {
47
+ store = {};
48
+ memoStore.set(node, store);
49
+ }
50
+ return store;
51
+ }
34
52
  // =============================================================================
35
53
  // Registration
36
54
  // =============================================================================
37
55
  export function registerHtmxExtension() {
38
56
  if (typeof window === "undefined" || !window.htmx)
39
57
  return;
58
+ const htmx = window.htmx;
59
+ if (registeredHtmxInstances.has(htmx))
60
+ return;
61
+ registeredHtmxInstances.add(htmx);
40
62
  window.htmx.defineExtension("litestar", {
41
63
  onEvent(name, evt) {
42
64
  if (name === "htmx:configRequest") {
43
65
  const token = getCsrfToken();
44
- if (token)
45
- evt.detail.headers["X-CSRF-Token"] = token;
66
+ const headers = getHeadersFromConfigRequestEvent(evt);
67
+ if (token && headers) {
68
+ if (headers instanceof Headers)
69
+ headers.set("X-CSRF-Token", token);
70
+ else
71
+ headers["X-CSRF-Token"] = token;
72
+ }
46
73
  }
47
74
  return true;
48
75
  },
@@ -178,12 +205,24 @@ const directives = [
178
205
  const g = expr(a.value);
179
206
  if (!g)
180
207
  return null;
181
- let bound = false;
182
208
  return (ctx, el) => {
183
- if (!bound) {
184
- bound = true;
185
- el.addEventListener(name, (e) => g({ ...ctx, $event: e }));
186
- }
209
+ const store = runtime(el);
210
+ const ctxKey = `__litestar_ctx:${name}`;
211
+ const boundKey = `__litestar_bound:${name}`;
212
+ // Always keep the latest context for the handler
213
+ store[ctxKey] = ctx;
214
+ // Bind the DOM listener only once per element+event
215
+ if (store[boundKey])
216
+ return;
217
+ store[boundKey] = true;
218
+ el.addEventListener(name, (e) => {
219
+ const current = memoStore.get(el)?.[ctxKey];
220
+ if (!current)
221
+ return;
222
+ const eventCtx = Object.create(current);
223
+ eventCtx.$event = e;
224
+ g(eventCtx);
225
+ });
187
226
  };
188
227
  },
189
228
  },
@@ -421,11 +460,7 @@ function compileTextExpr(t) {
421
460
  // Utilities
422
461
  // =============================================================================
423
462
  function memo(node, key, fn) {
424
- let store = memoStore.get(node);
425
- if (!store) {
426
- store = {};
427
- memoStore.set(node, store);
428
- }
463
+ const store = runtime(node);
429
464
  if (!(key in store)) {
430
465
  store[key] = fn();
431
466
  }
@@ -495,5 +530,3 @@ export function addDirective(dir) {
495
530
  directives.push(dir);
496
531
  }
497
532
  // Auto-register
498
- if (typeof window !== "undefined" && window.htmx)
499
- registerHtmxExtension();
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * For type-safe routing, import from your generated routes file:
22
22
  * ```ts
23
- * import { route, routes, type RouteName } from '@/generated/routes'
23
+ * import { route, routeDefinitions, type RouteName } from '@/generated/routes'
24
24
  *
25
25
  * // Type-safe URL generation
26
26
  * const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * For type-safe routing, import from your generated routes file:
22
22
  * ```ts
23
- * import { route, routes, type RouteName } from '@/generated/routes'
23
+ * import { route, routeDefinitions, type RouteName } from '@/generated/routes'
24
24
  *
25
25
  * // Type-safe URL generation
26
26
  * const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
@@ -54,6 +54,22 @@ export interface TypesConfig {
54
54
  * @default false
55
55
  */
56
56
  generateSdk?: boolean;
57
+ /**
58
+ * Generate typed routes.ts from routes.json metadata.
59
+ *
60
+ * Mirrors Python TypeGenConfig.generate_routes.
61
+ *
62
+ * @default true
63
+ */
64
+ generateRoutes?: boolean;
65
+ /**
66
+ * Generate Inertia page props types from inertia-pages.json metadata.
67
+ *
68
+ * Mirrors Python TypeGenConfig.generate_page_props.
69
+ *
70
+ * @default true
71
+ */
72
+ generatePageProps?: boolean;
57
73
  /**
58
74
  * Register route() function globally on window object.
59
75
  *
@@ -91,12 +107,17 @@ export interface PluginConfig {
91
107
  */
92
108
  bundleDir?: string;
93
109
  /**
94
- * Vite's public directory for static, unprocessed assets.
95
- * Mirrors Vite's `publicDir` option.
110
+ * Directory for static, unprocessed assets.
111
+ *
112
+ * This maps to Vite's `publicDir` option, but is named `staticDir` in this
113
+ * plugin to avoid confusion with Litestar's `bundleDir` (often `public`).
96
114
  *
97
- * @default 'public'
115
+ * Note: The Litestar plugin defaults this to `${resourceDir}/public` to avoid
116
+ * Vite's `publicDir` colliding with Litestar's `bundleDir`.
117
+ *
118
+ * @default `${resourceDir}/public`
98
119
  */
99
- publicDir?: string;
120
+ staticDir?: string;
100
121
  /**
101
122
  * Litestar's public assets directory. These are the assets that Vite will serve when developing.
102
123
  *
@@ -116,7 +137,7 @@ export interface PluginConfig {
116
137
  /**
117
138
  * The directory where the SSR bundle should be written.
118
139
  *
119
- * @default '${bundleDir}/bootstrap/ssr'
140
+ * @default `${resourceDir}/bootstrap/ssr`
120
141
  */
121
142
  ssrOutDir?: string;
122
143
  /**
@@ -215,50 +236,6 @@ interface RefreshConfig {
215
236
  paths: string[];
216
237
  config?: FullReloadConfig;
217
238
  }
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
- }
262
239
  type DevServerUrl = `${"http" | "https"}://${string}:${number}`;
263
240
  /**
264
241
  * Litestar plugin for Vite.