litestar-vite-plugin 0.14.0 → 0.15.0-alpha.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.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Route utilities for Litestar applications.
3
+ *
4
+ * These helpers work with route metadata injected by Litestar:
5
+ * - window.__LITESTAR_ROUTES__ (SPA mode with route injection)
6
+ * - window.routes (legacy/Inertia mode)
7
+ * - Generated routes from src/generated/routes.ts (typed routing)
8
+ *
9
+ * For typed routing, import from your generated routes instead:
10
+ * ```ts
11
+ * import { route, routes } from '@/generated/routes'
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ /**
17
+ * Route definition from Litestar.
18
+ */
19
+ export interface RouteDefinition {
20
+ uri: string;
21
+ methods: string[];
22
+ parameters?: string[];
23
+ parameterTypes?: Record<string, string>;
24
+ queryParameters?: Record<string, string>;
25
+ component?: string;
26
+ }
27
+ /**
28
+ * Routes object mapping route names to definitions.
29
+ */
30
+ export interface RoutesMap {
31
+ routes: Record<string, RouteDefinition>;
32
+ }
33
+ /**
34
+ * Convenience alias for route names when using injected metadata.
35
+ */
36
+ export type RouteName = keyof RoutesMap["routes"];
37
+ declare global {
38
+ interface Window {
39
+ __LITESTAR_ROUTES__?: RoutesMap;
40
+ routes?: Record<string, string>;
41
+ serverRoutes?: Record<string, string>;
42
+ }
43
+ var routes: Record<string, string>;
44
+ var serverRoutes: Record<string, string>;
45
+ }
46
+ declare global {
47
+ interface ImportMeta {
48
+ hot?: {
49
+ on: (event: string, callback: (...args: unknown[]) => void) => void;
50
+ accept?: (cb?: () => void) => void;
51
+ };
52
+ }
53
+ }
54
+ type RouteArg = string | number | boolean;
55
+ type RouteArgs = Record<string, RouteArg> | RouteArg[];
56
+ /**
57
+ * Get the routes object from the page.
58
+ *
59
+ * Checks multiple sources:
60
+ * 1. window.__LITESTAR_ROUTES__ (SPA mode with full metadata)
61
+ * 2. window.routes (legacy mode with just paths)
62
+ *
63
+ * @returns Routes map or null if not found
64
+ */
65
+ export declare function getRoutes(): Record<string, string> | null;
66
+ /**
67
+ * Generate a URL for a named route with parameters.
68
+ *
69
+ * @param routeName - The name of the route
70
+ * @param args - Route parameters (object or array)
71
+ * @returns The generated URL or "#" if route not found
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { route } from 'litestar-vite-plugin/helpers'
76
+ *
77
+ * // Named parameters
78
+ * route('user:detail', { user_id: 123 }) // "/users/123"
79
+ *
80
+ * // Positional parameters
81
+ * route('user:detail', [123]) // "/users/123"
82
+ * ```
83
+ */
84
+ export declare function route(routeName: string, ...args: [RouteArgs?]): string;
85
+ /**
86
+ * Get the relative path portion of a URL.
87
+ *
88
+ * @param url - Full URL or path
89
+ * @returns Relative path with query string and hash
90
+ */
91
+ export declare function getRelativeUrlPath(url: string): string;
92
+ /**
93
+ * Convert a URL to a route name.
94
+ *
95
+ * @param url - URL to match
96
+ * @returns Route name or null if no match
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * toRoute('/users/123') // "user:detail"
101
+ * ```
102
+ */
103
+ export declare function toRoute(url: string): string | null;
104
+ /**
105
+ * Get the current route name based on window.location.
106
+ *
107
+ * @returns Current route name or null if no match
108
+ */
109
+ export declare function currentRoute(): string | null;
110
+ /**
111
+ * Check if a URL matches a route pattern.
112
+ *
113
+ * Supports wildcard patterns like "user:*" to match "user:list", "user:detail", etc.
114
+ *
115
+ * @param url - URL to check
116
+ * @param routeName - Route name or pattern (supports * wildcards)
117
+ * @returns True if the URL matches the route
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * isRoute('/users/123', 'user:detail') // true
122
+ * isRoute('/users/123', 'user:*') // true
123
+ * ```
124
+ */
125
+ export declare function isRoute(url: string, routeName: string): boolean;
126
+ /**
127
+ * Check if the current URL matches a route pattern.
128
+ *
129
+ * @param routeName - Route name or pattern (supports * wildcards)
130
+ * @returns True if the current URL matches the route
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * // On /users/123
135
+ * isCurrentRoute('user:detail') // true
136
+ * isCurrentRoute('user:*') // true
137
+ * ```
138
+ */
139
+ export declare function isCurrentRoute(routeName: string): boolean;
140
+ export {};
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Route utilities for Litestar applications.
3
+ *
4
+ * These helpers work with route metadata injected by Litestar:
5
+ * - window.__LITESTAR_ROUTES__ (SPA mode with route injection)
6
+ * - window.routes (legacy/Inertia mode)
7
+ * - Generated routes from src/generated/routes.ts (typed routing)
8
+ *
9
+ * For typed routing, import from your generated routes instead:
10
+ * ```ts
11
+ * import { route, routes } from '@/generated/routes'
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ /**
17
+ * Get the routes object from the page.
18
+ *
19
+ * Checks multiple sources:
20
+ * 1. window.__LITESTAR_ROUTES__ (SPA mode with full metadata)
21
+ * 2. window.routes (legacy mode with just paths)
22
+ *
23
+ * @returns Routes map or null if not found
24
+ */
25
+ export function getRoutes() {
26
+ if (typeof window === "undefined") {
27
+ return null;
28
+ }
29
+ // Check for full route metadata (SPA mode)
30
+ if (window.__LITESTAR_ROUTES__?.routes) {
31
+ const routes = {};
32
+ for (const [name, def] of Object.entries(window.__LITESTAR_ROUTES__.routes)) {
33
+ routes[name] = def.uri;
34
+ }
35
+ // Expose a descriptive alias for consumers
36
+ window.serverRoutes = routes;
37
+ return routes;
38
+ }
39
+ // Check for simple routes object (legacy/Inertia mode)
40
+ if (window.routes) {
41
+ return window.routes;
42
+ }
43
+ // Check globalThis.routes
44
+ if (typeof globalThis !== "undefined" && globalThis.routes) {
45
+ return globalThis.routes;
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Generate a URL for a named route with parameters.
51
+ *
52
+ * @param routeName - The name of the route
53
+ * @param args - Route parameters (object or array)
54
+ * @returns The generated URL or "#" if route not found
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { route } from 'litestar-vite-plugin/helpers'
59
+ *
60
+ * // Named parameters
61
+ * route('user:detail', { user_id: 123 }) // "/users/123"
62
+ *
63
+ * // Positional parameters
64
+ * route('user:detail', [123]) // "/users/123"
65
+ * ```
66
+ */
67
+ export function route(routeName, ...args) {
68
+ const routes = getRoutes();
69
+ if (!routes) {
70
+ console.error("Routes not available. Ensure route metadata is injected.");
71
+ return "#";
72
+ }
73
+ let url = routes[routeName];
74
+ if (!url) {
75
+ console.error(`Route '${routeName}' not found.`);
76
+ return "#";
77
+ }
78
+ const argTokens = url.match(/\{([^}]+)\}/g);
79
+ if (!argTokens && args.length > 0 && args[0] !== undefined) {
80
+ console.error(`Route '${routeName}' does not accept parameters.`);
81
+ return "#";
82
+ }
83
+ if (!argTokens) {
84
+ return new URL(url, window.location.origin).href;
85
+ }
86
+ try {
87
+ if (typeof args[0] === "object" && !Array.isArray(args[0])) {
88
+ // Named parameters
89
+ for (const token of argTokens) {
90
+ let argName = token.slice(1, -1);
91
+ // Handle {param:type} syntax
92
+ if (argName.includes(":")) {
93
+ argName = argName.split(":")[0];
94
+ }
95
+ const argValue = args[0][argName];
96
+ if (argValue === undefined) {
97
+ throw new Error(`Missing parameter '${argName}'.`);
98
+ }
99
+ url = url.replace(token, String(argValue));
100
+ }
101
+ }
102
+ else {
103
+ // Positional parameters
104
+ const argsArray = Array.isArray(args[0]) ? args[0] : Array.from(args);
105
+ if (argTokens.length !== argsArray.length) {
106
+ throw new Error(`Expected ${argTokens.length} parameters, got ${argsArray.length}.`);
107
+ }
108
+ argTokens.forEach((token, i) => {
109
+ const argValue = argsArray[i];
110
+ if (argValue === undefined) {
111
+ throw new Error(`Missing parameter at position ${i}.`);
112
+ }
113
+ url = url.replace(token, String(argValue));
114
+ });
115
+ }
116
+ }
117
+ catch (error) {
118
+ console.error(error instanceof Error ? error.message : String(error));
119
+ return "#";
120
+ }
121
+ return new URL(url, window.location.origin).href;
122
+ }
123
+ /**
124
+ * Get the relative path portion of a URL.
125
+ *
126
+ * @param url - Full URL or path
127
+ * @returns Relative path with query string and hash
128
+ */
129
+ export function getRelativeUrlPath(url) {
130
+ try {
131
+ const urlObject = new URL(url);
132
+ return urlObject.pathname + urlObject.search + urlObject.hash;
133
+ }
134
+ catch {
135
+ return url;
136
+ }
137
+ }
138
+ /**
139
+ * Convert a URL to a route name.
140
+ *
141
+ * @param url - URL to match
142
+ * @returns Route name or null if no match
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * toRoute('/users/123') // "user:detail"
147
+ * ```
148
+ */
149
+ export function toRoute(url) {
150
+ const routes = getRoutes();
151
+ if (!routes) {
152
+ return null;
153
+ }
154
+ const processedUrl = getRelativeUrlPath(url);
155
+ const normalizedUrl = processedUrl === "/" ? processedUrl : processedUrl.replace(/\/$/, "");
156
+ for (const [routeName, routePattern] of Object.entries(routes)) {
157
+ const regexPattern = routePattern.replace(/\//g, "\\/").replace(/\{([^}]+)\}/g, (_, paramSpec) => {
158
+ // Handle {param:type} syntax
159
+ const paramType = paramSpec.includes(":") ? paramSpec.split(":")[1] : "str";
160
+ switch (paramType) {
161
+ case "uuid":
162
+ return "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
163
+ case "path":
164
+ return ".*";
165
+ case "int":
166
+ return "\\d+";
167
+ default:
168
+ return "[^/]+";
169
+ }
170
+ });
171
+ const regex = new RegExp(`^${regexPattern}$`);
172
+ if (regex.test(normalizedUrl)) {
173
+ return routeName;
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ /**
179
+ * Get the current route name based on window.location.
180
+ *
181
+ * @returns Current route name or null if no match
182
+ */
183
+ export function currentRoute() {
184
+ if (typeof window === "undefined") {
185
+ return null;
186
+ }
187
+ return toRoute(window.location.pathname);
188
+ }
189
+ /**
190
+ * Check if a URL matches a route pattern.
191
+ *
192
+ * Supports wildcard patterns like "user:*" to match "user:list", "user:detail", etc.
193
+ *
194
+ * @param url - URL to check
195
+ * @param routeName - Route name or pattern (supports * wildcards)
196
+ * @returns True if the URL matches the route
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * isRoute('/users/123', 'user:detail') // true
201
+ * isRoute('/users/123', 'user:*') // true
202
+ * ```
203
+ */
204
+ export function isRoute(url, routeName) {
205
+ const routes = getRoutes();
206
+ if (!routes) {
207
+ return false;
208
+ }
209
+ const processedUrl = getRelativeUrlPath(url);
210
+ const normalizedUrl = processedUrl === "/" ? processedUrl : processedUrl.replace(/\/$/, "");
211
+ // Convert route name pattern to regex
212
+ const routeNameRegex = new RegExp(`^${routeName.replace(/\*/g, ".*")}$`);
213
+ // Find all matching route names based on the pattern
214
+ const matchingRouteNames = Object.keys(routes).filter((name) => routeNameRegex.test(name));
215
+ for (const name of matchingRouteNames) {
216
+ const routePattern = routes[name];
217
+ const regexPattern = routePattern.replace(/\//g, "\\/").replace(/\{([^}]+)\}/g, (_, paramSpec) => {
218
+ const paramType = paramSpec.includes(":") ? paramSpec.split(":")[1] : "str";
219
+ switch (paramType) {
220
+ case "uuid":
221
+ return "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
222
+ case "path":
223
+ return "(.*)";
224
+ case "int":
225
+ return "(\\d+)";
226
+ default:
227
+ return "([^/]+)";
228
+ }
229
+ });
230
+ const regex = new RegExp(`^${regexPattern}$`);
231
+ if (regex.test(normalizedUrl)) {
232
+ return true;
233
+ }
234
+ }
235
+ return false;
236
+ }
237
+ /**
238
+ * Check if the current URL matches a route pattern.
239
+ *
240
+ * @param routeName - Route name or pattern (supports * wildcards)
241
+ * @returns True if the current URL matches the route
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * // On /users/123
246
+ * isCurrentRoute('user:detail') // true
247
+ * isCurrentRoute('user:*') // true
248
+ * ```
249
+ */
250
+ export function isCurrentRoute(routeName) {
251
+ const current = currentRoute();
252
+ if (!current) {
253
+ return false;
254
+ }
255
+ const routeNameRegex = new RegExp(`^${routeName.replace(/\*/g, ".*")}$`);
256
+ return routeNameRegex.test(current);
257
+ }
258
+ // Set up global functions for backward compatibility
259
+ if (typeof globalThis !== "undefined") {
260
+ globalThis.routes = globalThis.routes || {};
261
+ globalThis.serverRoutes = globalThis.serverRoutes || globalThis.routes;
262
+ globalThis.route = route;
263
+ globalThis.toRoute = toRoute;
264
+ globalThis.currentRoute = currentRoute;
265
+ globalThis.isRoute = isRoute;
266
+ globalThis.isCurrentRoute = isCurrentRoute;
267
+ }
268
+ // Keep serverRoutes fresh during Vite HMR when the plugin regenerates metadata/types
269
+ if (import.meta.hot) {
270
+ import.meta.hot.on("litestar:types-updated", () => {
271
+ if (typeof window === "undefined") {
272
+ return;
273
+ }
274
+ const updated = getRoutes();
275
+ if (updated) {
276
+ window.serverRoutes = updated;
277
+ window.routes = updated;
278
+ }
279
+ });
280
+ }
@@ -1,6 +1,63 @@
1
1
  import { type ConfigEnv, type Plugin, type UserConfig } from "vite";
2
2
  import { type Config as FullReloadConfig } from "vite-plugin-full-reload";
3
- interface PluginConfig {
3
+ /**
4
+ * Configuration for TypeScript type generation.
5
+ *
6
+ * Type generation works as follows:
7
+ * 1. Python's Litestar exports openapi.json and routes.json on startup (and reload)
8
+ * 2. The Vite plugin watches these files for changes
9
+ * 3. When they change, it runs @hey-api/openapi-ts to generate TypeScript types
10
+ * 4. HMR event is sent to notify the client
11
+ */
12
+ export interface TypesConfig {
13
+ /**
14
+ * Enable type generation.
15
+ *
16
+ * @default false
17
+ */
18
+ enabled?: boolean;
19
+ /**
20
+ * Path to output generated TypeScript types.
21
+ *
22
+ * @default 'src/types/api'
23
+ */
24
+ output?: string;
25
+ /**
26
+ * Path where the OpenAPI schema is exported by Litestar.
27
+ * The Vite plugin watches this file and runs @hey-api/openapi-ts when it changes.
28
+ *
29
+ * @default 'openapi.json'
30
+ */
31
+ openapiPath?: string;
32
+ /**
33
+ * Path where route metadata is exported by Litestar.
34
+ * The Vite plugin watches this file for route helper generation.
35
+ *
36
+ * @default 'routes.json'
37
+ */
38
+ routesPath?: string;
39
+ /**
40
+ * Generate Zod schemas in addition to TypeScript types.
41
+ *
42
+ * @default false
43
+ */
44
+ generateZod?: boolean;
45
+ /**
46
+ * Generate a typed SDK client (fetch) in addition to types.
47
+ *
48
+ * @default false
49
+ */
50
+ generateSdk?: boolean;
51
+ /**
52
+ * Debounce time in milliseconds for type regeneration.
53
+ * Prevents regeneration from running too frequently when
54
+ * multiple files are written in quick succession.
55
+ *
56
+ * @default 300
57
+ */
58
+ debounce?: number;
59
+ }
60
+ export interface PluginConfig {
4
61
  /**
5
62
  * The path or paths of the entry points to compile.
6
63
  */
@@ -62,6 +119,34 @@ interface PluginConfig {
62
119
  * Transform the code while serving.
63
120
  */
64
121
  transformOnServe?: (code: string, url: DevServerUrl) => string;
122
+ /**
123
+ * Enable and configure TypeScript type generation.
124
+ *
125
+ * When set to `true`, enables type generation with default settings.
126
+ * When set to a TypesConfig object, enables type generation with custom settings.
127
+ *
128
+ * Type generation creates TypeScript types from your Litestar OpenAPI schema
129
+ * and route metadata using @hey-api/openapi-ts.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * // Simple enable
134
+ * litestar({ input: 'src/main.ts', types: true })
135
+ *
136
+ * // With custom config
137
+ * litestar({
138
+ * input: 'src/main.ts',
139
+ * types: {
140
+ * enabled: true,
141
+ * output: 'src/api/types',
142
+ * generateZod: true
143
+ * }
144
+ * })
145
+ * ```
146
+ *
147
+ * @default false
148
+ */
149
+ types?: boolean | TypesConfig;
65
150
  }
66
151
  interface RefreshConfig {
67
152
  paths: string[];