litestar-vite-plugin 0.15.0-rc.1 → 0.15.0-rc.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 +4 -4
- package/dist/js/helpers/index.d.ts +20 -9
- package/dist/js/helpers/index.js +21 -9
- package/dist/js/helpers/routes.d.ts +138 -0
- package/dist/js/helpers/routes.js +181 -0
- package/dist/js/index.d.ts +10 -2
- package/dist/js/index.js +83 -23
- package/dist/js/inertia-types.d.ts +37 -0
- package/dist/js/inertia-types.js +0 -0
- package/dist/js/install-hint.d.ts +6 -1
- package/dist/js/install-hint.js +35 -2
- package/dist/js/shared/bridge-schema.d.ts +7 -2
- package/dist/js/shared/bridge-schema.js +13 -4
- package/dist/js/shared/emit-page-props-types.d.ts +3 -1
- package/dist/js/shared/emit-page-props-types.js +70 -48
- package/dist/js/shared/typegen-cache.d.ts +45 -0
- package/dist/js/shared/typegen-cache.js +83 -0
- package/dist/js/shared/typegen-core.d.ts +90 -0
- package/dist/js/shared/typegen-core.js +122 -0
- package/dist/js/shared/typegen-plugin.js +71 -57
- package/dist/js/shared/write-if-changed.d.ts +16 -0
- package/dist/js/shared/write-if-changed.js +19 -0
- package/dist/js/typegen-cli.js +66 -0
- package/package.json +33 -4
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ 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 `
|
|
8
|
+
- SSR framework support: use `mode="ssr"` for Astro, Nuxt, SvelteKit - proxies everything except your API routes.
|
|
9
9
|
- Production assets: reads Vite manifest from `public/manifest.json` (configurable) and serves under `asset_url`.
|
|
10
10
|
- Type-safe frontends: optional OpenAPI/routes export + `@hey-api/openapi-ts` via the Vite plugin.
|
|
11
11
|
- Inertia support: v2 protocol with session middleware and optional SPA mode.
|
|
@@ -103,13 +103,13 @@ app = Litestar(
|
|
|
103
103
|
|
|
104
104
|
## Meta-frameworks (Astro, Nuxt, SvelteKit)
|
|
105
105
|
|
|
106
|
-
Use `
|
|
106
|
+
Use `mode="ssr"` (or `mode="framework"`) to proxy non-API routes to the framework's dev server:
|
|
107
107
|
|
|
108
108
|
```python
|
|
109
109
|
import os
|
|
110
110
|
from pathlib import Path
|
|
111
111
|
from litestar import Litestar
|
|
112
|
-
from litestar_vite import VitePlugin, ViteConfig, PathConfig
|
|
112
|
+
from litestar_vite import VitePlugin, ViteConfig, PathConfig
|
|
113
113
|
|
|
114
114
|
here = Path(__file__).parent
|
|
115
115
|
DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in ("true", "1", "yes")
|
|
@@ -117,9 +117,9 @@ DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in ("true", "1", "yes")
|
|
|
117
117
|
app = Litestar(
|
|
118
118
|
plugins=[
|
|
119
119
|
VitePlugin(config=ViteConfig(
|
|
120
|
+
mode="ssr",
|
|
120
121
|
dev_mode=DEV_MODE,
|
|
121
122
|
paths=PathConfig(root=here),
|
|
122
|
-
runtime=RuntimeConfig(proxy_mode="ssr"),
|
|
123
123
|
))
|
|
124
124
|
],
|
|
125
125
|
)
|
|
@@ -6,23 +6,33 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
|
+
* // CSRF utilities
|
|
9
10
|
* import { getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
|
|
10
11
|
*
|
|
11
|
-
* // Get CSRF token
|
|
12
12
|
* const token = getCsrfToken()
|
|
13
|
+
* await csrfFetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Route matching utilities
|
|
19
|
+
* import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
|
|
20
|
+
* import { routeDefinitions } from '@/generated/routes'
|
|
13
21
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
|
|
23
|
+
*
|
|
24
|
+
* // Highlight active nav items
|
|
25
|
+
* if (isCurrentRoute('dashboard')) { ... }
|
|
26
|
+
*
|
|
27
|
+
* // Match with wildcards
|
|
28
|
+
* if (isCurrentRoute('book_*')) { ... }
|
|
19
29
|
* ```
|
|
20
30
|
*
|
|
21
|
-
*
|
|
31
|
+
* @example
|
|
22
32
|
* ```ts
|
|
23
|
-
*
|
|
33
|
+
* // Type-safe URL generation (from generated routes)
|
|
34
|
+
* import { route } from '@/generated/routes'
|
|
24
35
|
*
|
|
25
|
-
* // Type-safe URL generation
|
|
26
36
|
* const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
|
|
27
37
|
* ```
|
|
28
38
|
*
|
|
@@ -30,3 +40,4 @@
|
|
|
30
40
|
*/
|
|
31
41
|
export { csrfFetch, csrfHeaders, getCsrfToken } from "./csrf.js";
|
|
32
42
|
export { addDirective, registerHtmxExtension, setDebug as setHtmxDebug, swapJson } from "./htmx.js";
|
|
43
|
+
export { createRouteHelpers, currentRoute, isCurrentRoute, isRoute, type RouteDefinition, type RouteDefinitions, type RouteHelpers, toRoute, } from "./routes.js";
|
package/dist/js/helpers/index.js
CHANGED
|
@@ -6,23 +6,33 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
|
+
* // CSRF utilities
|
|
9
10
|
* import { getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
|
|
10
11
|
*
|
|
11
|
-
* // Get CSRF token
|
|
12
12
|
* const token = getCsrfToken()
|
|
13
|
+
* await csrfFetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Route matching utilities
|
|
19
|
+
* import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
|
|
20
|
+
* import { routeDefinitions } from '@/generated/routes'
|
|
13
21
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
|
|
23
|
+
*
|
|
24
|
+
* // Highlight active nav items
|
|
25
|
+
* if (isCurrentRoute('dashboard')) { ... }
|
|
26
|
+
*
|
|
27
|
+
* // Match with wildcards
|
|
28
|
+
* if (isCurrentRoute('book_*')) { ... }
|
|
19
29
|
* ```
|
|
20
30
|
*
|
|
21
|
-
*
|
|
31
|
+
* @example
|
|
22
32
|
* ```ts
|
|
23
|
-
*
|
|
33
|
+
* // Type-safe URL generation (from generated routes)
|
|
34
|
+
* import { route } from '@/generated/routes'
|
|
24
35
|
*
|
|
25
|
-
* // Type-safe URL generation
|
|
26
36
|
* const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
|
|
27
37
|
* ```
|
|
28
38
|
*
|
|
@@ -32,3 +42,5 @@
|
|
|
32
42
|
export { csrfFetch, csrfHeaders, getCsrfToken } from "./csrf.js";
|
|
33
43
|
// HTMX utilities
|
|
34
44
|
export { addDirective, registerHtmxExtension, setDebug as setHtmxDebug, swapJson } from "./htmx.js";
|
|
45
|
+
// Route matching utilities
|
|
46
|
+
export { createRouteHelpers, currentRoute, isCurrentRoute, isRoute, toRoute, } from "./routes.js";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route matching utilities for Litestar applications.
|
|
3
|
+
*
|
|
4
|
+
* These helpers work with the generated `routeDefinitions` from your routes.ts file
|
|
5
|
+
* to provide runtime route matching capabilities.
|
|
6
|
+
*
|
|
7
|
+
* **IMPORTANT**: This file serves as both the runtime library AND the reference
|
|
8
|
+
* implementation for the Python code generator. The same logic is also generated
|
|
9
|
+
* inline in `routes.ts` by `src/py/litestar_vite/_codegen/routes.py`.
|
|
10
|
+
*
|
|
11
|
+
* **If you modify the logic here, you MUST also update the Python generator!**
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { routeDefinitions } from '@/generated/routes'
|
|
16
|
+
* import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
|
|
17
|
+
*
|
|
18
|
+
* const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
|
|
19
|
+
*
|
|
20
|
+
* // Check if current URL matches a route
|
|
21
|
+
* if (isCurrentRoute('dashboard')) {
|
|
22
|
+
* // highlight nav item
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Route definition structure from generated routes.
|
|
30
|
+
*/
|
|
31
|
+
export interface RouteDefinition {
|
|
32
|
+
path: string;
|
|
33
|
+
methods: readonly string[];
|
|
34
|
+
method: string;
|
|
35
|
+
pathParams: readonly string[];
|
|
36
|
+
queryParams: readonly string[];
|
|
37
|
+
component?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Map of route names to their definitions.
|
|
41
|
+
*/
|
|
42
|
+
export type RouteDefinitions = Record<string, RouteDefinition>;
|
|
43
|
+
/**
|
|
44
|
+
* Convert a URL to its corresponding route name.
|
|
45
|
+
*
|
|
46
|
+
* @param url - URL or path to match (query strings and hashes are stripped)
|
|
47
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
48
|
+
* @returns The matching route name, or null if no match found
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* toRoute('/api/books', routeDefinitions) // 'books'
|
|
52
|
+
* toRoute('/api/books/123', routeDefinitions) // 'book_detail'
|
|
53
|
+
* toRoute('/unknown', routeDefinitions) // null
|
|
54
|
+
*/
|
|
55
|
+
export declare function toRoute<T extends string>(url: string, routes: Record<T, RouteDefinition>): T | null;
|
|
56
|
+
/**
|
|
57
|
+
* Get the current route name based on the browser URL.
|
|
58
|
+
*
|
|
59
|
+
* Returns null in SSR/non-browser environments.
|
|
60
|
+
*
|
|
61
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
62
|
+
* @returns Current route name, or null if no match or not in browser
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // On page /api/books/123
|
|
66
|
+
* currentRoute(routeDefinitions) // 'book_detail'
|
|
67
|
+
*/
|
|
68
|
+
export declare function currentRoute<T extends string>(routes: Record<T, RouteDefinition>): T | null;
|
|
69
|
+
/**
|
|
70
|
+
* Check if a URL matches a route name or pattern.
|
|
71
|
+
*
|
|
72
|
+
* Supports wildcard patterns with `*` to match multiple routes.
|
|
73
|
+
*
|
|
74
|
+
* @param url - URL or path to check
|
|
75
|
+
* @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_detail')
|
|
76
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
77
|
+
* @returns True if the URL matches the route pattern
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* isRoute('/api/books', 'books', routeDefinitions) // true
|
|
81
|
+
* isRoute('/api/books/123', 'book_detail', routeDefinitions) // true
|
|
82
|
+
* isRoute('/api/books/123', 'book_*', routeDefinitions) // true (wildcard)
|
|
83
|
+
* isRoute('/api/users', 'book_*', routeDefinitions) // false
|
|
84
|
+
*/
|
|
85
|
+
export declare function isRoute<T extends string>(url: string, pattern: string, routes: Record<T, RouteDefinition>): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Check if the current browser URL matches a route name or pattern.
|
|
88
|
+
*
|
|
89
|
+
* Supports wildcard patterns with `*` to match multiple routes.
|
|
90
|
+
* Returns false in SSR/non-browser environments.
|
|
91
|
+
*
|
|
92
|
+
* @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_page')
|
|
93
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
94
|
+
* @returns True if current URL matches the route pattern
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // On page /books
|
|
98
|
+
* isCurrentRoute('books_page', routeDefinitions) // true
|
|
99
|
+
* isCurrentRoute('book_*', routeDefinitions) // false (no match)
|
|
100
|
+
* isCurrentRoute('*_page', routeDefinitions) // true (wildcard)
|
|
101
|
+
*/
|
|
102
|
+
export declare function isCurrentRoute<T extends string>(pattern: string, routes: Record<T, RouteDefinition>): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Route helpers interface returned by createRouteHelpers.
|
|
105
|
+
*/
|
|
106
|
+
export interface RouteHelpers<T extends string> {
|
|
107
|
+
/** Convert URL to route name */
|
|
108
|
+
toRoute: (url: string) => T | null;
|
|
109
|
+
/** Get current route name (SSR-safe) */
|
|
110
|
+
currentRoute: () => T | null;
|
|
111
|
+
/** Check if URL matches route pattern */
|
|
112
|
+
isRoute: (url: string, pattern: string) => boolean;
|
|
113
|
+
/** Check if current URL matches route pattern */
|
|
114
|
+
isCurrentRoute: (pattern: string) => boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create route helpers bound to a specific routeDefinitions object.
|
|
118
|
+
*
|
|
119
|
+
* This is the recommended way to use route helpers - it creates bound functions
|
|
120
|
+
* so you don't need to pass routeDefinitions to every call.
|
|
121
|
+
*
|
|
122
|
+
* @param routes - The routeDefinitions object from your generated routes
|
|
123
|
+
* @returns Object with bound route helper functions
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { routeDefinitions } from '@/generated/routes'
|
|
128
|
+
* import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
|
|
129
|
+
*
|
|
130
|
+
* export const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
|
|
131
|
+
*
|
|
132
|
+
* // Now use without passing routes:
|
|
133
|
+
* if (isCurrentRoute('dashboard')) {
|
|
134
|
+
* // ...
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare function createRouteHelpers<T extends string>(routes: Record<T, RouteDefinition>): RouteHelpers<T>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route matching utilities for Litestar applications.
|
|
3
|
+
*
|
|
4
|
+
* These helpers work with the generated `routeDefinitions` from your routes.ts file
|
|
5
|
+
* to provide runtime route matching capabilities.
|
|
6
|
+
*
|
|
7
|
+
* **IMPORTANT**: This file serves as both the runtime library AND the reference
|
|
8
|
+
* implementation for the Python code generator. The same logic is also generated
|
|
9
|
+
* inline in `routes.ts` by `src/py/litestar_vite/_codegen/routes.py`.
|
|
10
|
+
*
|
|
11
|
+
* **If you modify the logic here, you MUST also update the Python generator!**
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { routeDefinitions } from '@/generated/routes'
|
|
16
|
+
* import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
|
|
17
|
+
*
|
|
18
|
+
* const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
|
|
19
|
+
*
|
|
20
|
+
* // Check if current URL matches a route
|
|
21
|
+
* if (isCurrentRoute('dashboard')) {
|
|
22
|
+
* // highlight nav item
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
/** Cache for compiled route patterns */
|
|
29
|
+
const patternCache = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* Compile a route path pattern to a regex for URL matching.
|
|
32
|
+
* Results are cached for performance.
|
|
33
|
+
*
|
|
34
|
+
* @param path - Route path with {param} placeholders
|
|
35
|
+
* @returns Compiled regex pattern
|
|
36
|
+
*/
|
|
37
|
+
function compilePattern(path) {
|
|
38
|
+
const cached = patternCache.get(path);
|
|
39
|
+
if (cached)
|
|
40
|
+
return cached;
|
|
41
|
+
// Escape special regex characters except { }
|
|
42
|
+
let pattern = path.replace(/[.*+?^$|()[\]]/g, "\\$&");
|
|
43
|
+
// Replace {param} or {param:type} with matchers
|
|
44
|
+
pattern = pattern.replace(/\{([^}]+)\}/g, (_match, paramSpec) => {
|
|
45
|
+
const paramType = paramSpec.includes(":") ? paramSpec.split(":")[1] : "str";
|
|
46
|
+
switch (paramType) {
|
|
47
|
+
case "uuid":
|
|
48
|
+
return "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
49
|
+
case "path":
|
|
50
|
+
return ".*";
|
|
51
|
+
case "int":
|
|
52
|
+
return "\\d+";
|
|
53
|
+
default:
|
|
54
|
+
return "[^/]+";
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const regex = new RegExp(`^${pattern}$`, "i");
|
|
58
|
+
patternCache.set(path, regex);
|
|
59
|
+
return regex;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Convert a URL to its corresponding route name.
|
|
63
|
+
*
|
|
64
|
+
* @param url - URL or path to match (query strings and hashes are stripped)
|
|
65
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
66
|
+
* @returns The matching route name, or null if no match found
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* toRoute('/api/books', routeDefinitions) // 'books'
|
|
70
|
+
* toRoute('/api/books/123', routeDefinitions) // 'book_detail'
|
|
71
|
+
* toRoute('/unknown', routeDefinitions) // null
|
|
72
|
+
*/
|
|
73
|
+
export function toRoute(url, routes) {
|
|
74
|
+
// Strip query string and hash
|
|
75
|
+
const path = url.split("?")[0].split("#")[0];
|
|
76
|
+
// Normalize: remove trailing slash except for root
|
|
77
|
+
const normalized = path === "/" ? path : path.replace(/\/$/, "");
|
|
78
|
+
for (const [name, def] of Object.entries(routes)) {
|
|
79
|
+
if (compilePattern(def.path).test(normalized)) {
|
|
80
|
+
return name;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the current route name based on the browser URL.
|
|
87
|
+
*
|
|
88
|
+
* Returns null in SSR/non-browser environments.
|
|
89
|
+
*
|
|
90
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
91
|
+
* @returns Current route name, or null if no match or not in browser
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // On page /api/books/123
|
|
95
|
+
* currentRoute(routeDefinitions) // 'book_detail'
|
|
96
|
+
*/
|
|
97
|
+
export function currentRoute(routes) {
|
|
98
|
+
if (typeof window === "undefined")
|
|
99
|
+
return null;
|
|
100
|
+
return toRoute(window.location.pathname, routes);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if a URL matches a route name or pattern.
|
|
104
|
+
*
|
|
105
|
+
* Supports wildcard patterns with `*` to match multiple routes.
|
|
106
|
+
*
|
|
107
|
+
* @param url - URL or path to check
|
|
108
|
+
* @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_detail')
|
|
109
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
110
|
+
* @returns True if the URL matches the route pattern
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* isRoute('/api/books', 'books', routeDefinitions) // true
|
|
114
|
+
* isRoute('/api/books/123', 'book_detail', routeDefinitions) // true
|
|
115
|
+
* isRoute('/api/books/123', 'book_*', routeDefinitions) // true (wildcard)
|
|
116
|
+
* isRoute('/api/users', 'book_*', routeDefinitions) // false
|
|
117
|
+
*/
|
|
118
|
+
export function isRoute(url, pattern, routes) {
|
|
119
|
+
const routeName = toRoute(url, routes);
|
|
120
|
+
if (!routeName)
|
|
121
|
+
return false;
|
|
122
|
+
// Escape special regex chars (except *), then convert * to .*
|
|
123
|
+
const escaped = pattern.replace(/[.+?^$|()[\]{}]/g, "\\$&");
|
|
124
|
+
const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
|
|
125
|
+
return regex.test(routeName);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if the current browser URL matches a route name or pattern.
|
|
129
|
+
*
|
|
130
|
+
* Supports wildcard patterns with `*` to match multiple routes.
|
|
131
|
+
* Returns false in SSR/non-browser environments.
|
|
132
|
+
*
|
|
133
|
+
* @param pattern - Route name or pattern (e.g., 'books', 'book_*', '*_page')
|
|
134
|
+
* @param routes - The routeDefinitions object from generated routes
|
|
135
|
+
* @returns True if current URL matches the route pattern
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* // On page /books
|
|
139
|
+
* isCurrentRoute('books_page', routeDefinitions) // true
|
|
140
|
+
* isCurrentRoute('book_*', routeDefinitions) // false (no match)
|
|
141
|
+
* isCurrentRoute('*_page', routeDefinitions) // true (wildcard)
|
|
142
|
+
*/
|
|
143
|
+
export function isCurrentRoute(pattern, routes) {
|
|
144
|
+
const current = currentRoute(routes);
|
|
145
|
+
if (!current)
|
|
146
|
+
return false;
|
|
147
|
+
// Escape special regex chars (except *), then convert * to .*
|
|
148
|
+
const escaped = pattern.replace(/[.+?^$|()[\]{}]/g, "\\$&");
|
|
149
|
+
const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
|
|
150
|
+
return regex.test(current);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Create route helpers bound to a specific routeDefinitions object.
|
|
154
|
+
*
|
|
155
|
+
* This is the recommended way to use route helpers - it creates bound functions
|
|
156
|
+
* so you don't need to pass routeDefinitions to every call.
|
|
157
|
+
*
|
|
158
|
+
* @param routes - The routeDefinitions object from your generated routes
|
|
159
|
+
* @returns Object with bound route helper functions
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { routeDefinitions } from '@/generated/routes'
|
|
164
|
+
* import { createRouteHelpers } from 'litestar-vite-plugin/helpers'
|
|
165
|
+
*
|
|
166
|
+
* export const { isCurrentRoute, currentRoute, toRoute, isRoute } = createRouteHelpers(routeDefinitions)
|
|
167
|
+
*
|
|
168
|
+
* // Now use without passing routes:
|
|
169
|
+
* if (isCurrentRoute('dashboard')) {
|
|
170
|
+
* // ...
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export function createRouteHelpers(routes) {
|
|
175
|
+
return {
|
|
176
|
+
toRoute: (url) => toRoute(url, routes),
|
|
177
|
+
currentRoute: () => currentRoute(routes),
|
|
178
|
+
isRoute: (url, pattern) => isRoute(url, pattern, routes),
|
|
179
|
+
isCurrentRoute: (pattern) => isCurrentRoute(pattern, routes),
|
|
180
|
+
};
|
|
181
|
+
}
|
package/dist/js/index.d.ts
CHANGED
|
@@ -100,6 +100,13 @@ export interface PluginConfig {
|
|
|
100
100
|
* @default '/static/'
|
|
101
101
|
*/
|
|
102
102
|
assetUrl?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Optional asset URL to use only during production builds.
|
|
105
|
+
*
|
|
106
|
+
* This is typically derived from Python DeployConfig.asset_url and written into `.litestar.json`
|
|
107
|
+
* as `deployAssetUrl`. It is only used when `command === "build"`.
|
|
108
|
+
*/
|
|
109
|
+
deployAssetUrl?: string;
|
|
103
110
|
/**
|
|
104
111
|
* The public directory where all compiled/bundled assets should be written.
|
|
105
112
|
*
|
|
@@ -160,11 +167,12 @@ export interface PluginConfig {
|
|
|
160
167
|
*/
|
|
161
168
|
autoDetectIndex?: boolean;
|
|
162
169
|
/**
|
|
163
|
-
* Enable Inertia mode
|
|
170
|
+
* Enable Inertia mode.
|
|
164
171
|
*
|
|
165
172
|
* In Inertia apps, the backend (Litestar) serves all HTML responses.
|
|
166
173
|
* When enabled, direct access to the Vite dev server will show a placeholder
|
|
167
|
-
* page directing users to access the app through the backend
|
|
174
|
+
* page directing users to access the app through the backend (even if an
|
|
175
|
+
* index.html exists for the backend to render).
|
|
168
176
|
*
|
|
169
177
|
* Auto-detected from `.litestar.json` when mode is "inertia".
|
|
170
178
|
*
|
package/dist/js/index.js
CHANGED
|
@@ -28,9 +28,6 @@ function litestar(config) {
|
|
|
28
28
|
return plugins;
|
|
29
29
|
}
|
|
30
30
|
async function findIndexHtmlPath(server, pluginConfig) {
|
|
31
|
-
if (pluginConfig.inertiaMode) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
31
|
if (!pluginConfig.autoDetectIndex) {
|
|
35
32
|
return null;
|
|
36
33
|
}
|
|
@@ -81,7 +78,8 @@ function resolveLitestarPlugin(pluginConfig) {
|
|
|
81
78
|
userConfig = config;
|
|
82
79
|
const ssr = !!userConfig.build?.ssr;
|
|
83
80
|
const env = loadEnv(mode, userConfig.envDir || process.cwd(), "");
|
|
84
|
-
const
|
|
81
|
+
const runtimeAssetUrl = normalizeAssetUrl(env.ASSET_URL || pluginConfig.assetUrl);
|
|
82
|
+
const buildAssetUrl = pluginConfig.deployAssetUrl ?? runtimeAssetUrl;
|
|
85
83
|
const serverConfig = command === "serve" ? resolveDevelopmentEnvironmentServerConfig(pluginConfig.detectTls) ?? resolveEnvironmentServerConfig(env) : void 0;
|
|
86
84
|
const withProxyErrorSilencer = (proxyConfig) => {
|
|
87
85
|
if (!proxyConfig) return void 0;
|
|
@@ -114,7 +112,7 @@ function resolveLitestarPlugin(pluginConfig) {
|
|
|
114
112
|
const devBase = pluginConfig.assetUrl.startsWith("/") ? pluginConfig.assetUrl : pluginConfig.assetUrl.replace(/\/+$/, "");
|
|
115
113
|
ensureCommandShouldRunInEnvironment(command, env, mode);
|
|
116
114
|
return {
|
|
117
|
-
base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig,
|
|
115
|
+
base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig, buildAssetUrl) : devBase),
|
|
118
116
|
publicDir: userConfig.publicDir ?? pluginConfig.staticDir ?? false,
|
|
119
117
|
clearScreen: false,
|
|
120
118
|
build: {
|
|
@@ -237,15 +235,21 @@ function resolveLitestarPlugin(pluginConfig) {
|
|
|
237
235
|
setTimeout(async () => {
|
|
238
236
|
if (logger.config.level === "quiet") return;
|
|
239
237
|
const litestarVersion = litestarMeta.litestarVersion ?? process.env.LITESTAR_VERSION ?? "unknown";
|
|
240
|
-
|
|
238
|
+
let backendStatus = await checkBackendAvailability(appUrl);
|
|
239
|
+
if (!backendStatus.available) {
|
|
240
|
+
for (let i = 0; i < 3 && !backendStatus.available; i++) {
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
242
|
+
backendStatus = await checkBackendAvailability(appUrl);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
241
245
|
resolvedConfig.logger.info(`
|
|
242
246
|
${colors.red(`${colors.bold("LITESTAR")} ${litestarVersion}`)}`);
|
|
243
247
|
resolvedConfig.logger.info("");
|
|
244
|
-
if (
|
|
248
|
+
if (pluginConfig.inertiaMode) {
|
|
249
|
+
resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: Inertia`);
|
|
250
|
+
} else if (initialIndexPath) {
|
|
245
251
|
const relIndexPath = logger.path(initialIndexPath, server.config.root);
|
|
246
252
|
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`);
|
|
249
253
|
} else {
|
|
250
254
|
resolvedConfig.logger.info(` ${colors.green("\u279C")} ${colors.bold("Mode")}: Litestar`);
|
|
251
255
|
}
|
|
@@ -314,24 +318,51 @@ function resolveLitestarPlugin(pluginConfig) {
|
|
|
314
318
|
exitHandlersBound = true;
|
|
315
319
|
}
|
|
316
320
|
server.middlewares.use(async (req, res, next) => {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
const requestUrl = req.originalUrl ?? req.url ?? "/";
|
|
322
|
+
const requestPath = requestUrl.split("?")[0];
|
|
323
|
+
const isRootRequest = requestPath === "/" || requestPath === "/index.html";
|
|
324
|
+
if (requestPath === "/__litestar__/transform-index") {
|
|
325
|
+
if (req.method !== "POST") {
|
|
326
|
+
res.statusCode = 405;
|
|
327
|
+
res.setHeader("Content-Type", "text/plain");
|
|
328
|
+
res.end("Method Not Allowed");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const readBody = async () => new Promise((resolve, reject) => {
|
|
332
|
+
let data = "";
|
|
333
|
+
req.on("data", (chunk) => {
|
|
334
|
+
data += chunk;
|
|
335
|
+
});
|
|
336
|
+
req.on("end", () => resolve(data));
|
|
337
|
+
req.on("error", (err) => reject(err));
|
|
338
|
+
});
|
|
320
339
|
try {
|
|
321
|
-
const
|
|
322
|
-
const
|
|
340
|
+
const body = await readBody();
|
|
341
|
+
const payload = JSON.parse(body);
|
|
342
|
+
if (!payload.html || typeof payload.html !== "string") {
|
|
343
|
+
res.statusCode = 400;
|
|
344
|
+
res.setHeader("Content-Type", "text/plain");
|
|
345
|
+
res.end("Invalid payload");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const url = typeof payload.url === "string" && payload.url ? payload.url : "/";
|
|
349
|
+
const transformedHtml = await server.transformIndexHtml(url, payload.html, url);
|
|
323
350
|
res.statusCode = 200;
|
|
324
351
|
res.setHeader("Content-Type", "text/html");
|
|
325
352
|
res.end(transformedHtml);
|
|
326
|
-
return;
|
|
327
353
|
} catch (e) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
354
|
+
resolvedConfig.logger.error(`Error transforming index.html: ${e instanceof Error ? e.message : e}`);
|
|
355
|
+
res.statusCode = 500;
|
|
356
|
+
res.setHeader("Content-Type", "text/plain");
|
|
357
|
+
res.end("Error transforming HTML");
|
|
332
358
|
}
|
|
359
|
+
return;
|
|
333
360
|
}
|
|
334
|
-
if (!
|
|
361
|
+
if (!isRootRequest) {
|
|
362
|
+
next();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (pluginConfig.inertiaMode) {
|
|
335
366
|
try {
|
|
336
367
|
const placeholderPath = path.join(dirname(), "dev-server-index.html");
|
|
337
368
|
const placeholderContent = await fs.promises.readFile(placeholderPath, "utf-8");
|
|
@@ -345,7 +376,33 @@ function resolveLitestarPlugin(pluginConfig) {
|
|
|
345
376
|
}
|
|
346
377
|
return;
|
|
347
378
|
}
|
|
348
|
-
|
|
379
|
+
const indexPath = await findIndexHtmlPath(server, pluginConfig);
|
|
380
|
+
if (indexPath) {
|
|
381
|
+
try {
|
|
382
|
+
const htmlContent = await fs.promises.readFile(indexPath, "utf-8");
|
|
383
|
+
const transformedHtml = await server.transformIndexHtml(requestUrl, htmlContent, requestUrl);
|
|
384
|
+
res.statusCode = 200;
|
|
385
|
+
res.setHeader("Content-Type", "text/html");
|
|
386
|
+
res.end(transformedHtml);
|
|
387
|
+
return;
|
|
388
|
+
} catch (e) {
|
|
389
|
+
const relIndexPath = path.relative(server.config.root, indexPath);
|
|
390
|
+
resolvedConfig.logger.error(`Error serving index.html from ${relIndexPath}: ${e instanceof Error ? e.message : e}`);
|
|
391
|
+
next(e);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const placeholderPath = path.join(dirname(), "dev-server-index.html");
|
|
397
|
+
const placeholderContent = await fs.promises.readFile(placeholderPath, "utf-8");
|
|
398
|
+
res.statusCode = 200;
|
|
399
|
+
res.setHeader("Content-Type", "text/html");
|
|
400
|
+
res.end(placeholderContent.replace(/{{ APP_URL }}/g, appUrl));
|
|
401
|
+
} catch (e) {
|
|
402
|
+
resolvedConfig.logger.error(`Error serving placeholder index.html: ${e instanceof Error ? e.message : e}`);
|
|
403
|
+
res.statusCode = 404;
|
|
404
|
+
res.end("Not Found (Error loading placeholder)");
|
|
405
|
+
}
|
|
349
406
|
});
|
|
350
407
|
}
|
|
351
408
|
};
|
|
@@ -510,11 +567,13 @@ function resolvePluginConfig(config) {
|
|
|
510
567
|
typesConfig.pagePropsPath = path.join(typesConfig.output, "inertia-pages.json");
|
|
511
568
|
}
|
|
512
569
|
}
|
|
513
|
-
const inertiaMode = resolvedConfig.inertiaMode ?? pythonDefaults?.mode === "inertia";
|
|
570
|
+
const inertiaMode = resolvedConfig.inertiaMode ?? (pythonDefaults?.mode === "hybrid" || pythonDefaults?.mode === "inertia");
|
|
514
571
|
const effectiveResourceDir = resolvedConfig.resourceDir ?? pythonDefaults?.resourceDir ?? "src";
|
|
572
|
+
const deployAssetUrlRaw = resolvedConfig.deployAssetUrl ?? pythonDefaults?.deployAssetUrl ?? void 0;
|
|
515
573
|
const result = {
|
|
516
574
|
input: resolvedConfig.input,
|
|
517
575
|
assetUrl: normalizeAssetUrl(resolvedConfig.assetUrl ?? pythonDefaults?.assetUrl ?? "/static/"),
|
|
576
|
+
deployAssetUrl: typeof deployAssetUrlRaw === "string" ? normalizeAssetUrl(deployAssetUrlRaw) : void 0,
|
|
518
577
|
resourceDir: effectiveResourceDir,
|
|
519
578
|
bundleDir: resolvedConfig.bundleDir ?? pythonDefaults?.bundleDir ?? "public",
|
|
520
579
|
staticDir: resolvedConfig.staticDir ?? pythonDefaults?.staticDir ?? path.join(effectiveResourceDir, "public"),
|
|
@@ -549,7 +608,8 @@ function validateAgainstPythonDefaults(resolved, pythonDefaults, userConfig) {
|
|
|
549
608
|
if (userConfig.staticDir !== void 0 && hasPythonValue(pythonDefaults.staticDir) && resolved.staticDir !== pythonDefaults.staticDir) {
|
|
550
609
|
warnings.push(`staticDir: vite.config.ts="${resolved.staticDir}" differs from Python="${pythonDefaults.staticDir}"`);
|
|
551
610
|
}
|
|
552
|
-
|
|
611
|
+
const frameworkMode = pythonDefaults.mode === "framework" || pythonDefaults.mode === "ssr" || pythonDefaults.mode === "ssg";
|
|
612
|
+
if (frameworkMode && userConfig.ssrOutDir !== void 0 && hasPythonValue(pythonDefaults.ssrOutDir) && resolved.ssrOutDir !== pythonDefaults.ssrOutDir) {
|
|
553
613
|
warnings.push(`ssrOutDir: vite.config.ts="${resolved.ssrOutDir}" differs from Python="${pythonDefaults.ssrOutDir}"`);
|
|
554
614
|
}
|
|
555
615
|
if (warnings.length > 0) {
|