litestar-vite-plugin 0.15.0-alpha.1 → 0.15.0-alpha.3

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
@@ -5,14 +5,16 @@ Litestar Vite connects the Litestar backend to a Vite toolchain. It supports SPA
5
5
  ## Features
6
6
 
7
7
  - One-port dev: proxies Vite HTTP + WS/HMR through Litestar by default; switch to two-port with `VITE_PROXY_MODE=direct`.
8
+ - SSR framework support: use `proxy_mode="ssr"` for Astro, Nuxt, SvelteKit - proxies everything except your API routes.
8
9
  - Production assets: reads Vite manifest from `public/manifest.json` (configurable) and serves under `asset_url`.
9
10
  - Type-safe frontends: optional OpenAPI/routes export + `@hey-api/openapi-ts` via the Vite plugin.
10
11
  - Inertia support: v2 protocol with session middleware and optional SPA mode.
11
12
 
12
- ## Quick start (SPA)
13
+ ## Quick start (React TanStack SPA)
13
14
 
14
15
  ```bash
15
16
  pip install litestar-vite
17
+ litestar assets init --template react-tanstack
16
18
  litestar assets install # installs frontend deps via configured executor
17
19
  ```
18
20
 
@@ -20,13 +22,15 @@ litestar assets install # installs frontend deps via configured executor
20
22
  from litestar import Litestar
21
23
  from litestar_vite import VitePlugin, ViteConfig
22
24
 
23
- app = Litestar(plugins=[VitePlugin(config=ViteConfig(dev_mode=True))])
25
+ app = Litestar(plugins=[VitePlugin(config=ViteConfig(dev_mode=True))]) # SPA mode by default
24
26
  ```
25
27
 
26
28
  ```bash
27
- litestar run # starts Litestar; Vite dev is proxied automatically
29
+ litestar run --reload # starts Litestar; Vite dev is proxied automatically
28
30
  ```
29
31
 
32
+ Other templates: `react`, `vue`, `svelte`, `htmx`, `react-inertia`, `vue-inertia`, `svelte-inertia`, `angular`, `angular-cli`, `astro`, `nuxt`, `sveltekit` (see `litestar assets init --help`).
33
+
30
34
  ## Template / HTMX
31
35
 
32
36
  ```python
@@ -47,13 +51,15 @@ Requires session middleware.
47
51
 
48
52
  ```python
49
53
  from litestar import Litestar
50
- from litestar.middleware.session.server_side import ServerSideSessionConfig, ServerSideSessionMiddleware
54
+ from litestar.middleware.session.client_side import CookieBackendConfig
51
55
  from litestar_vite import VitePlugin, ViteConfig
52
56
  from litestar_vite.inertia import InertiaPlugin
53
57
  from litestar_vite.inertia.config import InertiaConfig
54
58
 
59
+ session_backend = CookieBackendConfig(secret="dev-secret")
60
+
55
61
  app = Litestar(
56
- middleware=[ServerSideSessionMiddleware(config=ServerSideSessionConfig(secret="secret"))],
62
+ middleware=[session_backend.middleware],
57
63
  plugins=[
58
64
  VitePlugin(config=ViteConfig(mode="template", inertia=True, dev_mode=True)),
59
65
  InertiaPlugin(InertiaConfig()),
@@ -61,6 +67,62 @@ app = Litestar(
61
67
  )
62
68
  ```
63
69
 
70
+ ## SSR Frameworks (Astro, Nuxt, SvelteKit)
71
+
72
+ For SSR frameworks that handle their own routing, use `proxy_mode="ssr"`:
73
+
74
+ ```python
75
+ import os
76
+ from pathlib import Path
77
+ from litestar import Litestar
78
+ from litestar_vite import VitePlugin, ViteConfig, PathConfig, RuntimeConfig
79
+
80
+ DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in ("true", "1", "yes")
81
+
82
+ app = Litestar(
83
+ plugins=[
84
+ VitePlugin(config=ViteConfig(
85
+ dev_mode=DEV_MODE,
86
+ paths=PathConfig(root=Path(__file__).parent),
87
+ runtime=RuntimeConfig(
88
+ proxy_mode="ssr" if DEV_MODE else None, # Proxy in dev, static in prod
89
+ spa_handler=not DEV_MODE, # Serve built files in production
90
+ ),
91
+ ))
92
+ ],
93
+ )
94
+ ```
95
+
96
+ ### Proxy Modes
97
+
98
+ | Mode | Alias | Use Case |
99
+ |------|-------|----------|
100
+ | `vite` | - | SPAs - proxies Vite assets only (default) |
101
+ | `direct` | - | Two-port dev - expose Vite port directly |
102
+ | `proxy` | `ssr` | SSR frameworks - proxies everything except API routes |
103
+
104
+ ### Production Deployment
105
+
106
+ **Static Build (recommended):** Build your SSR framework to static files, then serve with Litestar:
107
+
108
+ ```bash
109
+ # Build frontend
110
+ cd examples/astro && npm run build
111
+
112
+ # Run in production mode
113
+ VITE_DEV_MODE=false litestar --app-dir examples/astro run
114
+ ```
115
+
116
+ Configure `bundle_dir` to match your framework's build output:
117
+
118
+ | Framework | Default Output | PathConfig |
119
+ |-----------|---------------|------------|
120
+ | Astro | `dist/` | `bundle_dir=Path("dist")` |
121
+ | Nuxt | `.output/public/` | `bundle_dir=Path(".output/public")` |
122
+ | SvelteKit | `build/` | `bundle_dir=Path("build")` |
123
+
124
+ **Two-Service:** For dynamic SSR, run the Node server alongside Litestar behind a reverse proxy.
125
+
64
126
  ## Type generation
65
127
 
66
128
  ```python
@@ -89,5 +151,5 @@ litestar assets generate-types # one-off or CI
89
151
  ## Links
90
152
 
91
153
  - Docs: <https://litestar-org.github.io/litestar-vite/>
92
- - Examples: `examples/` (basic, inertia, spa-react)
154
+ - Examples: `examples/` (basic, inertia, react)
93
155
  - Issues: <https://github.com/litestar-org/litestar-vite/issues/>
@@ -17,7 +17,10 @@
17
17
  * integrations: [
18
18
  * litestar({
19
19
  * apiProxy: 'http://localhost:8000',
20
- * typesPath: './src/generated/api',
20
+ * types: {
21
+ * enabled: true,
22
+ * output: 'src/generated/api',
23
+ * },
21
24
  * }),
22
25
  * ],
23
26
  * });
@@ -31,6 +34,31 @@ import type { Plugin, ViteDevServer } from "vite";
31
34
  * This is a minimal type definition to avoid requiring astro as a dependency.
32
35
  * When using this integration, Astro will be available in the project.
33
36
  */
37
+ /**
38
+ * Astro config for updateConfig - partial types we support.
39
+ */
40
+ interface AstroConfigPartial {
41
+ server?: {
42
+ port?: number;
43
+ host?: string | boolean;
44
+ };
45
+ vite?: {
46
+ plugins?: Plugin[];
47
+ server?: {
48
+ port?: number;
49
+ strictPort?: boolean;
50
+ proxy?: Record<string, unknown>;
51
+ };
52
+ };
53
+ }
54
+ /**
55
+ * AddressInfo from Node.js net module.
56
+ */
57
+ interface AddressInfo {
58
+ address: string;
59
+ family: string;
60
+ port: number;
61
+ }
34
62
  interface AstroIntegration {
35
63
  name: string;
36
64
  hooks: {
@@ -38,17 +66,17 @@ interface AstroIntegration {
38
66
  config: unknown;
39
67
  command: "dev" | "build" | "preview" | "sync";
40
68
  isRestart: boolean;
41
- updateConfig: (newConfig: {
42
- vite?: {
43
- plugins?: Plugin[];
44
- };
45
- }) => unknown;
69
+ updateConfig: (newConfig: AstroConfigPartial) => unknown;
46
70
  logger: AstroIntegrationLogger;
47
71
  }) => void | Promise<void>;
48
72
  "astro:server:setup"?: (options: {
49
73
  server: ViteDevServer;
50
74
  logger: AstroIntegrationLogger;
51
75
  }) => void | Promise<void>;
76
+ "astro:server:start"?: (options: {
77
+ address: AddressInfo;
78
+ logger: AstroIntegrationLogger;
79
+ }) => void | Promise<void>;
52
80
  "astro:build:start"?: (options: {
53
81
  logger: AstroIntegrationLogger;
54
82
  }) => void | Promise<void>;
@@ -62,6 +90,48 @@ interface AstroIntegrationLogger {
62
90
  warn: (message: string) => void;
63
91
  error: (message: string) => void;
64
92
  }
93
+ /**
94
+ * Configuration for TypeScript type generation in Astro.
95
+ */
96
+ export interface AstroTypesConfig {
97
+ /**
98
+ * Enable type generation.
99
+ *
100
+ * @default false
101
+ */
102
+ enabled?: boolean;
103
+ /**
104
+ * Path to output generated TypeScript types.
105
+ * Relative to the Astro project root.
106
+ *
107
+ * @default 'src/types/api'
108
+ */
109
+ output?: string;
110
+ /**
111
+ * Path where the OpenAPI schema is exported by Litestar.
112
+ *
113
+ * @default 'openapi.json'
114
+ */
115
+ openapiPath?: string;
116
+ /**
117
+ * Path where route metadata is exported by Litestar.
118
+ *
119
+ * @default 'routes.json'
120
+ */
121
+ routesPath?: string;
122
+ /**
123
+ * Generate Zod schemas in addition to TypeScript types.
124
+ *
125
+ * @default false
126
+ */
127
+ generateZod?: boolean;
128
+ /**
129
+ * Debounce time in milliseconds for type regeneration.
130
+ *
131
+ * @default 300
132
+ */
133
+ debounce?: number;
134
+ }
65
135
  /**
66
136
  * Configuration options for the Litestar Astro integration.
67
137
  */
@@ -82,25 +152,14 @@ export interface LitestarAstroConfig {
82
152
  */
83
153
  apiPrefix?: string;
84
154
  /**
85
- * Path where TypeScript types are generated.
86
- * This should match the output path configured in your Litestar ViteConfig.
155
+ * Enable and configure TypeScript type generation.
87
156
  *
88
- * @example './src/generated/api'
89
- * @default './src/types/api'
90
- */
91
- typesPath?: string;
92
- /**
93
- * Path to the OpenAPI schema file exported by Litestar.
157
+ * When set to `true`, enables type generation with default settings.
158
+ * When set to an AstroTypesConfig object, enables type generation with custom settings.
94
159
  *
95
- * @default 'openapi.json'
96
- */
97
- openapiPath?: string;
98
- /**
99
- * Path to the routes metadata file exported by Litestar.
100
- *
101
- * @default 'routes.json'
160
+ * @default false
102
161
  */
103
- routesPath?: string;
162
+ types?: boolean | AstroTypesConfig;
104
163
  /**
105
164
  * Enable verbose logging for debugging.
106
165
  *
@@ -128,7 +187,10 @@ export interface LitestarAstroConfig {
128
187
  * litestar({
129
188
  * apiProxy: 'http://localhost:8000',
130
189
  * apiPrefix: '/api',
131
- * typesPath: './src/generated/api',
190
+ * types: {
191
+ * enabled: true,
192
+ * output: 'src/generated/api',
193
+ * },
132
194
  * }),
133
195
  * ],
134
196
  * });
package/dist/js/astro.js CHANGED
@@ -1,11 +1,65 @@
1
+ import { exec } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import colors from "picocolors";
6
+ import { resolveInstallHint } from "./install-hint.js";
7
+ import { debounce } from "./shared/debounce.js";
8
+ const execAsync = promisify(exec);
1
9
  function resolveConfig(config = {}) {
10
+ const runtimeConfigPath = process.env.LITESTAR_VITE_CONFIG_PATH;
11
+ let hotFile;
12
+ let proxyMode = "vite";
13
+ let port;
14
+ const envPort = process.env.VITE_PORT;
15
+ if (envPort) {
16
+ port = Number.parseInt(envPort, 10);
17
+ if (Number.isNaN(port)) {
18
+ port = void 0;
19
+ }
20
+ }
21
+ if (runtimeConfigPath && fs.existsSync(runtimeConfigPath)) {
22
+ try {
23
+ const json = JSON.parse(fs.readFileSync(runtimeConfigPath, "utf-8"));
24
+ const bundleDir = json.bundleDir ?? "public";
25
+ const hot = json.hotFile ?? "hot";
26
+ hotFile = path.resolve(process.cwd(), bundleDir, hot);
27
+ proxyMode = json.proxyMode ?? "vite";
28
+ if (json.port !== void 0) {
29
+ port = json.port;
30
+ }
31
+ } catch {
32
+ hotFile = void 0;
33
+ }
34
+ }
35
+ let typesConfig = false;
36
+ if (config.types === true) {
37
+ typesConfig = {
38
+ enabled: true,
39
+ output: "src/types/api",
40
+ openapiPath: "openapi.json",
41
+ routesPath: "routes.json",
42
+ generateZod: false,
43
+ debounce: 300
44
+ };
45
+ } else if (typeof config.types === "object" && config.types !== null) {
46
+ typesConfig = {
47
+ enabled: config.types.enabled ?? true,
48
+ output: config.types.output ?? "src/types/api",
49
+ openapiPath: config.types.openapiPath ?? "openapi.json",
50
+ routesPath: config.types.routesPath ?? "routes.json",
51
+ generateZod: config.types.generateZod ?? false,
52
+ debounce: config.types.debounce ?? 300
53
+ };
54
+ }
2
55
  return {
3
56
  apiProxy: config.apiProxy ?? "http://localhost:8000",
4
57
  apiPrefix: config.apiPrefix ?? "/api",
5
- typesPath: config.typesPath ?? "./src/types/api",
6
- openapiPath: config.openapiPath ?? "openapi.json",
7
- routesPath: config.routesPath ?? "routes.json",
8
- verbose: config.verbose ?? false
58
+ types: typesConfig,
59
+ verbose: config.verbose ?? false,
60
+ hotFile,
61
+ proxyMode,
62
+ port
9
63
  };
10
64
  }
11
65
  function createProxyPlugin(config) {
@@ -14,6 +68,12 @@ function createProxyPlugin(config) {
14
68
  config() {
15
69
  return {
16
70
  server: {
71
+ // Set the port from Python config/env to ensure Astro uses the expected port
72
+ // strictPort: true prevents Astro from auto-incrementing to a different port
73
+ ...config.port !== void 0 ? {
74
+ port: config.port,
75
+ strictPort: true
76
+ } : {},
17
77
  proxy: {
18
78
  [config.apiPrefix]: {
19
79
  target: config.apiProxy,
@@ -26,23 +86,240 @@ function createProxyPlugin(config) {
26
86
  }
27
87
  };
28
88
  }
89
+ async function emitRouteTypes(routesPath, outputDir) {
90
+ const contents = await fs.promises.readFile(routesPath, "utf-8");
91
+ const json = JSON.parse(contents);
92
+ const outDir = path.resolve(process.cwd(), outputDir);
93
+ await fs.promises.mkdir(outDir, { recursive: true });
94
+ const outFile = path.join(outDir, "routes.ts");
95
+ const banner = `// AUTO-GENERATED by litestar-vite. Do not edit.
96
+ /* eslint-disable */
97
+
98
+ `;
99
+ const routesData = json.routes || json;
100
+ const routeNames = Object.keys(routesData);
101
+ const routeNameType = routeNames.length > 0 ? routeNames.map((n) => `"${n}"`).join(" | ") : "never";
102
+ const routeParamTypes = [];
103
+ for (const [name, data] of Object.entries(routesData)) {
104
+ const routeData = data;
105
+ if (routeData.parameters && routeData.parameters.length > 0) {
106
+ const params = routeData.parameters.map((p) => `${p}: string | number`).join("; ");
107
+ routeParamTypes.push(` "${name}": { ${params} }`);
108
+ } else {
109
+ routeParamTypes.push(` "${name}": Record<string, never>`);
110
+ }
111
+ }
112
+ const body = `/**
113
+ * AUTO-GENERATED by litestar-vite.
114
+ *
115
+ * Exports:
116
+ * - routesMeta: full route metadata
117
+ * - routes: name -> uri map
118
+ * - serverRoutes: alias of routes for clarity in apps
119
+ * - route(): type-safe URL generator
120
+ * - hasRoute(): type guard
121
+ * - csrf helpers re-exported from litestar-vite-plugin/helpers
122
+ *
123
+ * @see https://litestar-vite.litestar.dev/
124
+ */
125
+ export const routesMeta = ${JSON.stringify(json, null, 2)} as const
126
+
127
+ /**
128
+ * Route name to URI mapping.
129
+ */
130
+ export const routes = ${JSON.stringify(Object.fromEntries(Object.entries(routesData).map(([name, data]) => [name, data.uri])), null, 2)} as const
131
+
132
+ /**
133
+ * Alias for server-injected route map (more descriptive for consumers).
134
+ */
135
+ export const serverRoutes = routes
136
+
137
+ /**
138
+ * All available route names.
139
+ */
140
+ export type RouteName = ${routeNameType}
141
+
142
+ /**
143
+ * Parameter types for each route.
144
+ */
145
+ export interface RouteParams {
146
+ ${routeParamTypes.join("\n")}
147
+ }
148
+
149
+ /**
150
+ * Generate a URL for a named route with type-safe parameters.
151
+ *
152
+ * @param name - The route name
153
+ * @param params - Route parameters (required if route has path parameters)
154
+ * @returns The generated URL
155
+ *
156
+ * @example
157
+ * \`\`\`ts
158
+ * import { route } from '@/generated/routes'
159
+ *
160
+ * // Route without parameters
161
+ * route('home') // "/"
162
+ *
163
+ * // Route with parameters
164
+ * route('user:detail', { user_id: 123 }) // "/users/123"
165
+ * \`\`\`
166
+ */
167
+ export function route<T extends RouteName>(
168
+ name: T,
169
+ ...args: RouteParams[T] extends Record<string, never> ? [] : [params: RouteParams[T]]
170
+ ): string {
171
+ let uri = routes[name] as string
172
+ const params = args[0] as Record<string, string | number> | undefined
173
+
174
+ if (params) {
175
+ for (const [key, value] of Object.entries(params)) {
176
+ // Handle both {param} and {param:type} syntax
177
+ uri = uri.replace(new RegExp(\`\\\\{\${key}(?::[^}]+)?\\\\}\`, "g"), String(value))
178
+ }
179
+ }
180
+
181
+ return uri
182
+ }
183
+
184
+ /**
185
+ * Check if a route name exists.
186
+ */
187
+ export function hasRoute(name: string): name is RouteName {
188
+ return name in routes
189
+ }
190
+
191
+ declare global {
192
+ interface Window {
193
+ /**
194
+ * Fully-typed route metadata injected by Litestar.
195
+ */
196
+ __LITESTAR_ROUTES__?: typeof routesMeta
197
+ /**
198
+ * Simple route map (name -> uri) for legacy consumers.
199
+ */
200
+ routes?: typeof routes
201
+ serverRoutes?: typeof serverRoutes
202
+ }
203
+ // eslint-disable-next-line no-var
204
+ var routes: typeof routes | undefined
205
+ var serverRoutes: typeof serverRoutes | undefined
206
+ }
207
+
208
+ // Re-export helper functions from litestar-vite-plugin
209
+ // These work with the routes defined above
210
+ export { getCsrfToken, csrfHeaders, csrfFetch } from "litestar-vite-plugin/helpers"
211
+ `;
212
+ await fs.promises.writeFile(outFile, `${banner}${body}`, "utf-8");
213
+ }
214
+ function createTypeGenerationPlugin(typesConfig) {
215
+ let server = null;
216
+ let isGenerating = false;
217
+ async function runTypeGeneration() {
218
+ if (isGenerating) {
219
+ return false;
220
+ }
221
+ isGenerating = true;
222
+ const startTime = Date.now();
223
+ try {
224
+ const openapiPath = path.resolve(process.cwd(), typesConfig.openapiPath);
225
+ if (!fs.existsSync(openapiPath)) {
226
+ console.log(colors.cyan("[litestar-astro]"), colors.yellow("OpenAPI schema not found:"), typesConfig.openapiPath);
227
+ return false;
228
+ }
229
+ console.log(colors.cyan("[litestar-astro]"), colors.dim("Generating TypeScript types..."));
230
+ const args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
231
+ if (typesConfig.generateZod) {
232
+ args.push("--plugins", "zod", "@hey-api/typescript");
233
+ }
234
+ await execAsync(`npx ${args.join(" ")}`, {
235
+ cwd: process.cwd()
236
+ });
237
+ const routesPath = path.resolve(process.cwd(), typesConfig.routesPath);
238
+ if (fs.existsSync(routesPath)) {
239
+ await emitRouteTypes(routesPath, typesConfig.output);
240
+ }
241
+ const duration = Date.now() - startTime;
242
+ console.log(colors.cyan("[litestar-astro]"), colors.green("Types generated"), colors.dim(`in ${duration}ms`));
243
+ if (server) {
244
+ server.ws.send({
245
+ type: "custom",
246
+ event: "litestar:types-updated",
247
+ data: {
248
+ output: typesConfig.output,
249
+ timestamp: Date.now()
250
+ }
251
+ });
252
+ }
253
+ return true;
254
+ } catch (error) {
255
+ const message = error instanceof Error ? error.message : String(error);
256
+ if (message.includes("not found") || message.includes("ENOENT")) {
257
+ console.log(colors.cyan("[litestar-astro]"), colors.yellow("@hey-api/openapi-ts not installed"), "- run:", resolveInstallHint());
258
+ } else {
259
+ console.error(colors.cyan("[litestar-astro]"), colors.red("Type generation failed:"), message);
260
+ }
261
+ return false;
262
+ } finally {
263
+ isGenerating = false;
264
+ }
265
+ }
266
+ const debouncedRunTypeGeneration = debounce(runTypeGeneration, typesConfig.debounce);
267
+ return {
268
+ name: "litestar-astro-types",
269
+ enforce: "pre",
270
+ configureServer(devServer) {
271
+ server = devServer;
272
+ console.log(colors.cyan("[litestar-astro]"), colors.dim("Watching for schema changes:"), colors.yellow(typesConfig.openapiPath));
273
+ },
274
+ handleHotUpdate({ file }) {
275
+ if (!typesConfig.enabled) {
276
+ return;
277
+ }
278
+ const relativePath = path.relative(process.cwd(), file);
279
+ const openapiPath = typesConfig.openapiPath.replace(/^\.\//, "");
280
+ const routesPath = typesConfig.routesPath.replace(/^\.\//, "");
281
+ if (relativePath === openapiPath || relativePath === routesPath || file.endsWith(openapiPath) || file.endsWith(routesPath)) {
282
+ console.log(colors.cyan("[litestar-astro]"), colors.dim("Schema changed:"), colors.yellow(relativePath));
283
+ debouncedRunTypeGeneration();
284
+ }
285
+ }
286
+ };
287
+ }
29
288
  function litestarAstro(userConfig = {}) {
30
289
  const config = resolveConfig(userConfig);
31
290
  return {
32
291
  name: "litestar-vite",
33
292
  hooks: {
34
- "astro:config:setup": ({ updateConfig, logger }) => {
293
+ "astro:config:setup": ({ updateConfig, logger, command }) => {
35
294
  if (config.verbose) {
36
295
  logger.info("Configuring Litestar integration");
37
296
  logger.info(` API Proxy: ${config.apiProxy}`);
38
297
  logger.info(` API Prefix: ${config.apiPrefix}`);
39
- logger.info(` Types Path: ${config.typesPath}`);
298
+ if (config.types !== false) {
299
+ logger.info(` Types Output: ${config.types.output}`);
300
+ }
301
+ if (config.port !== void 0) {
302
+ logger.info(` Port: ${config.port}`);
303
+ }
304
+ }
305
+ const plugins = [createProxyPlugin(config)];
306
+ if (config.types !== false && config.types.enabled) {
307
+ plugins.push(createTypeGenerationPlugin(config.types));
40
308
  }
41
- updateConfig({
309
+ const configUpdate = {
42
310
  vite: {
43
- plugins: [createProxyPlugin(config)]
311
+ plugins
44
312
  }
45
- });
313
+ };
314
+ if (command === "dev" && config.port !== void 0) {
315
+ configUpdate.server = {
316
+ port: config.port
317
+ };
318
+ if (config.verbose) {
319
+ logger.info(`Setting Astro server port to ${config.port}`);
320
+ }
321
+ }
322
+ updateConfig(configUpdate);
46
323
  logger.info(`Litestar integration configured - proxying ${config.apiPrefix}/* to ${config.apiProxy}`);
47
324
  },
48
325
  "astro:server:setup": ({ server, logger }) => {
@@ -58,6 +335,20 @@ function litestarAstro(userConfig = {}) {
58
335
  });
59
336
  }
60
337
  },
338
+ // Write hotfile AFTER server starts listening (astro:server:start fires after listen())
339
+ // Always write hotfile - proxy mode needs it for dynamic target discovery
340
+ "astro:server:start": ({ address, logger }) => {
341
+ if (config.hotFile) {
342
+ const rawAddr = address.address;
343
+ const host = rawAddr === "::" || rawAddr === "::1" || rawAddr === "0.0.0.0" || rawAddr === "127.0.0.1" ? "localhost" : rawAddr;
344
+ const url = `http://${host}:${address.port}`;
345
+ fs.mkdirSync(path.dirname(config.hotFile), { recursive: true });
346
+ fs.writeFileSync(config.hotFile, url);
347
+ if (config.verbose) {
348
+ logger.info(`Hotfile written: ${config.hotFile} -> ${url}`);
349
+ }
350
+ }
351
+ },
61
352
  "astro:build:start": ({ logger }) => {
62
353
  logger.info("Building with Litestar integration");
63
354
  logger.info(` Make sure your Litestar backend is accessible at: ${config.apiProxy}`);
@@ -21,4 +21,4 @@
21
21
  * @module
22
22
  */
23
23
  export { getCsrfToken, csrfHeaders, csrfFetch } from "./csrf.js";
24
- export { route, getRoutes, toRoute, currentRoute, isRoute, isCurrentRoute, getRelativeUrlPath, type RouteDefinition, type RoutesMap, } from "./routes.js";
24
+ export { route, getRoutes, toRoute, currentRoute, isRoute, isCurrentRoute, getRelativeUrlPath, LITESTAR, type RouteDefinition, type RoutesMap, type LitestarHelpers, } from "./routes.js";
@@ -23,4 +23,4 @@
23
23
  // CSRF utilities
24
24
  export { getCsrfToken, csrfHeaders, csrfFetch } from "./csrf.js";
25
25
  // Route utilities
26
- export { route, getRoutes, toRoute, currentRoute, isRoute, isCurrentRoute, getRelativeUrlPath, } from "./routes.js";
26
+ export { route, getRoutes, toRoute, currentRoute, isRoute, isCurrentRoute, getRelativeUrlPath, LITESTAR, } from "./routes.js";
@@ -34,12 +34,26 @@ export interface RoutesMap {
34
34
  * Convenience alias for route names when using injected metadata.
35
35
  */
36
36
  export type RouteName = keyof RoutesMap["routes"];
37
+ /**
38
+ * Litestar helpers namespace for clean global access.
39
+ */
40
+ export interface LitestarHelpers {
41
+ route: typeof route;
42
+ toRoute: typeof toRoute;
43
+ currentRoute: typeof currentRoute;
44
+ isRoute: typeof isRoute;
45
+ isCurrentRoute: typeof isCurrentRoute;
46
+ getRelativeUrlPath: typeof getRelativeUrlPath;
47
+ routes: Record<string, string>;
48
+ }
37
49
  declare global {
38
50
  interface Window {
39
51
  __LITESTAR_ROUTES__?: RoutesMap;
52
+ __LITESTAR__?: LitestarHelpers;
40
53
  routes?: Record<string, string>;
41
54
  serverRoutes?: Record<string, string>;
42
55
  }
56
+ var __LITESTAR__: LitestarHelpers | undefined;
43
57
  var routes: Record<string, string>;
44
58
  var serverRoutes: Record<string, string>;
45
59
  }
@@ -137,4 +151,9 @@ export declare function isRoute(url: string, routeName: string): boolean;
137
151
  * ```
138
152
  */
139
153
  export declare function isCurrentRoute(routeName: string): boolean;
154
+ /**
155
+ * Litestar helpers namespace object.
156
+ * Access via window.__LITESTAR__ or import functions directly from this module.
157
+ */
158
+ export declare const LITESTAR: LitestarHelpers;
140
159
  export {};