litestar-vite-plugin 0.15.0-alpha.4 → 0.15.0-alpha.5
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/dist/js/astro.d.ts +6 -0
- package/dist/js/astro.js +42 -7
- package/dist/js/helpers/htmx.d.ts +68 -0
- package/dist/js/helpers/htmx.js +494 -0
- package/dist/js/helpers/index.d.ts +12 -4
- package/dist/js/helpers/index.js +13 -5
- package/dist/js/index.d.ts +33 -8
- package/dist/js/index.js +227 -54
- package/dist/js/inertia-helpers/index.d.ts +6 -1
- package/dist/js/inertia-helpers/index.js +7 -4
- package/dist/js/litestar-meta.js +20 -4
- package/dist/js/nuxt.d.ts +6 -0
- package/dist/js/nuxt.js +39 -4
- package/dist/js/sveltekit.d.ts +6 -0
- package/dist/js/sveltekit.js +34 -4
- package/package.json +7 -7
- package/dist/js/helpers/routes.d.ts +0 -159
- package/dist/js/helpers/routes.js +0 -302
package/dist/js/astro.d.ts
CHANGED
|
@@ -125,6 +125,12 @@ export interface AstroTypesConfig {
|
|
|
125
125
|
* @default false
|
|
126
126
|
*/
|
|
127
127
|
generateZod?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Generate SDK client functions for API calls.
|
|
130
|
+
*
|
|
131
|
+
* @default true
|
|
132
|
+
*/
|
|
133
|
+
generateSdk?: boolean;
|
|
128
134
|
/**
|
|
129
135
|
* Debounce time in milliseconds for type regeneration.
|
|
130
136
|
*
|
package/dist/js/astro.js
CHANGED
|
@@ -40,6 +40,7 @@ function resolveConfig(config = {}) {
|
|
|
40
40
|
openapiPath: "openapi.json",
|
|
41
41
|
routesPath: "routes.json",
|
|
42
42
|
generateZod: false,
|
|
43
|
+
generateSdk: true,
|
|
43
44
|
debounce: 300
|
|
44
45
|
};
|
|
45
46
|
} else if (typeof config.types === "object" && config.types !== null) {
|
|
@@ -49,6 +50,7 @@ function resolveConfig(config = {}) {
|
|
|
49
50
|
openapiPath: config.types.openapiPath ?? "openapi.json",
|
|
50
51
|
routesPath: config.types.routesPath ?? "routes.json",
|
|
51
52
|
generateZod: config.types.generateZod ?? false,
|
|
53
|
+
generateSdk: config.types.generateSdk ?? true,
|
|
52
54
|
debounce: config.types.debounce ?? 300
|
|
53
55
|
};
|
|
54
56
|
}
|
|
@@ -68,6 +70,9 @@ function createProxyPlugin(config) {
|
|
|
68
70
|
config() {
|
|
69
71
|
return {
|
|
70
72
|
server: {
|
|
73
|
+
// Force IPv4 binding for consistency with Python proxy configuration
|
|
74
|
+
// Without this, Astro might bind to IPv6 localhost which the proxy can't reach
|
|
75
|
+
host: "127.0.0.1",
|
|
71
76
|
// Set the port from Python config/env to ensure Astro uses the expected port
|
|
72
77
|
// strictPort: true prevents Astro from auto-incrementing to a different port
|
|
73
78
|
...config.port !== void 0 ? {
|
|
@@ -227,12 +232,28 @@ function createTypeGenerationPlugin(typesConfig) {
|
|
|
227
232
|
return false;
|
|
228
233
|
}
|
|
229
234
|
console.log(colors.cyan("[litestar-astro]"), colors.dim("Generating TypeScript types..."));
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
const projectRoot = process.cwd();
|
|
236
|
+
const candidates = [path.resolve(projectRoot, "openapi-ts.config.ts"), path.resolve(projectRoot, "hey-api.config.ts"), path.resolve(projectRoot, ".hey-api.config.ts")];
|
|
237
|
+
const configPath = candidates.find((p) => fs.existsSync(p)) || null;
|
|
238
|
+
let args;
|
|
239
|
+
if (configPath) {
|
|
240
|
+
console.log(colors.cyan("[litestar-astro]"), colors.dim("Using config:"), configPath);
|
|
241
|
+
args = ["@hey-api/openapi-ts", "--file", configPath];
|
|
242
|
+
} else {
|
|
243
|
+
args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
|
|
244
|
+
const plugins = ["@hey-api/typescript", "@hey-api/schemas"];
|
|
245
|
+
if (typesConfig.generateSdk) {
|
|
246
|
+
plugins.push("@hey-api/sdk", "@hey-api/client-fetch");
|
|
247
|
+
}
|
|
248
|
+
if (typesConfig.generateZod) {
|
|
249
|
+
plugins.push("zod");
|
|
250
|
+
}
|
|
251
|
+
if (plugins.length) {
|
|
252
|
+
args.push("--plugins", ...plugins);
|
|
253
|
+
}
|
|
233
254
|
}
|
|
234
255
|
await execAsync(`npx ${args.join(" ")}`, {
|
|
235
|
-
cwd:
|
|
256
|
+
cwd: projectRoot
|
|
236
257
|
});
|
|
237
258
|
const routesPath = path.resolve(process.cwd(), typesConfig.routesPath);
|
|
238
259
|
if (fs.existsSync(routesPath)) {
|
|
@@ -271,6 +292,14 @@ function createTypeGenerationPlugin(typesConfig) {
|
|
|
271
292
|
server = devServer;
|
|
272
293
|
console.log(colors.cyan("[litestar-astro]"), colors.dim("Watching for schema changes:"), colors.yellow(typesConfig.openapiPath));
|
|
273
294
|
},
|
|
295
|
+
async buildStart() {
|
|
296
|
+
if (typesConfig.enabled) {
|
|
297
|
+
const openapiPath = path.resolve(process.cwd(), typesConfig.openapiPath);
|
|
298
|
+
if (fs.existsSync(openapiPath)) {
|
|
299
|
+
await runTypeGeneration();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
274
303
|
handleHotUpdate({ file }) {
|
|
275
304
|
if (!typesConfig.enabled) {
|
|
276
305
|
return;
|
|
@@ -311,12 +340,18 @@ function litestarAstro(userConfig = {}) {
|
|
|
311
340
|
plugins
|
|
312
341
|
}
|
|
313
342
|
};
|
|
314
|
-
if (command === "dev"
|
|
343
|
+
if (command === "dev") {
|
|
315
344
|
configUpdate.server = {
|
|
316
|
-
|
|
345
|
+
// Force IPv4 binding for consistency with Python proxy configuration
|
|
346
|
+
host: "127.0.0.1",
|
|
347
|
+
// Set port from Python config/env if provided
|
|
348
|
+
...config.port !== void 0 ? { port: config.port } : {}
|
|
317
349
|
};
|
|
318
350
|
if (config.verbose) {
|
|
319
|
-
logger.info(
|
|
351
|
+
logger.info("Setting Astro server host to 127.0.0.1");
|
|
352
|
+
if (config.port !== void 0) {
|
|
353
|
+
logger.info(`Setting Astro server port to ${config.port}`);
|
|
354
|
+
}
|
|
320
355
|
}
|
|
321
356
|
}
|
|
322
357
|
updateConfig(configUpdate);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Litestar HTMX Extension
|
|
3
|
+
*
|
|
4
|
+
* Lightweight JSON templating for HTMX with CSRF support.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - `hx-swap="json"` - Client-side JSON templating
|
|
8
|
+
* - Automatic CSRF token injection
|
|
9
|
+
* - Template syntax: `ls-for`, `ls-if`, `:attr`, `@event`, `${expr}`
|
|
10
|
+
*
|
|
11
|
+
* For typed routes, import from your generated routes file:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { route } from '@/generated/routes'
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```html
|
|
18
|
+
* <div hx-get="/api/books" hx-swap="json" hx-ext="litestar">
|
|
19
|
+
* <template ls-for="book in $data">
|
|
20
|
+
* <article :id="`book-${book.id}`">
|
|
21
|
+
* <h3>${book.title}</h3>
|
|
22
|
+
* <p>${book.author} • ${book.year}</p>
|
|
23
|
+
* </article>
|
|
24
|
+
* </template>
|
|
25
|
+
* </div>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
/** Type for route function - matches generated routes.ts */
|
|
31
|
+
type RouteFn = (name: string, params?: Record<string, string | number>) => string;
|
|
32
|
+
declare global {
|
|
33
|
+
interface Window {
|
|
34
|
+
htmx?: HtmxApi;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
interface HtmxApi {
|
|
38
|
+
defineExtension: (name: string, ext: HtmxExtension) => void;
|
|
39
|
+
process: (elt: Element) => void;
|
|
40
|
+
}
|
|
41
|
+
interface HtmxExtension {
|
|
42
|
+
init?: () => void;
|
|
43
|
+
onEvent?: (name: string, evt: CustomEvent) => boolean | void;
|
|
44
|
+
transformResponse?: (text: string, xhr: XMLHttpRequest, elt: Element) => string;
|
|
45
|
+
isInlineSwap?: (swapStyle: string) => boolean;
|
|
46
|
+
handleSwap?: (swapStyle: string, target: Element, fragment: DocumentFragment | Element) => Element[];
|
|
47
|
+
}
|
|
48
|
+
/** Template context - inherits from data via prototype for direct access */
|
|
49
|
+
interface Ctx {
|
|
50
|
+
$data: unknown;
|
|
51
|
+
$parent?: Ctx;
|
|
52
|
+
$index?: number;
|
|
53
|
+
$key?: string;
|
|
54
|
+
$event?: Event;
|
|
55
|
+
route?: RouteFn;
|
|
56
|
+
navigate?: (name: string, params?: Record<string, string | number>) => void;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
export declare function registerHtmxExtension(): void;
|
|
60
|
+
export declare function swapJson(el: Element, data: unknown): void;
|
|
61
|
+
type Handler = (ctx: Ctx, el: Element) => Ctx | false | void;
|
|
62
|
+
interface Dir {
|
|
63
|
+
match: (a: Attr) => boolean;
|
|
64
|
+
create: (el: Element, a: Attr) => Handler | null;
|
|
65
|
+
}
|
|
66
|
+
export declare function setDebug(_on: boolean): void;
|
|
67
|
+
export declare function addDirective(dir: Dir): void;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Litestar HTMX Extension
|
|
3
|
+
*
|
|
4
|
+
* Lightweight JSON templating for HTMX with CSRF support.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - `hx-swap="json"` - Client-side JSON templating
|
|
8
|
+
* - Automatic CSRF token injection
|
|
9
|
+
* - Template syntax: `ls-for`, `ls-if`, `:attr`, `@event`, `${expr}`
|
|
10
|
+
*
|
|
11
|
+
* For typed routes, import from your generated routes file:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { route } from '@/generated/routes'
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```html
|
|
18
|
+
* <div hx-get="/api/books" hx-swap="json" hx-ext="litestar">
|
|
19
|
+
* <template ls-for="book in $data">
|
|
20
|
+
* <article :id="`book-${book.id}`">
|
|
21
|
+
* <h3>${book.title}</h3>
|
|
22
|
+
* <p>${book.author} • ${book.year}</p>
|
|
23
|
+
* </article>
|
|
24
|
+
* </template>
|
|
25
|
+
* </div>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import { getCsrfToken } from "./csrf.js";
|
|
31
|
+
const DEBUG = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
32
|
+
const cache = new Map();
|
|
33
|
+
const memoStore = new WeakMap();
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Registration
|
|
36
|
+
// =============================================================================
|
|
37
|
+
export function registerHtmxExtension() {
|
|
38
|
+
if (typeof window === "undefined" || !window.htmx)
|
|
39
|
+
return;
|
|
40
|
+
window.htmx.defineExtension("litestar", {
|
|
41
|
+
onEvent(name, evt) {
|
|
42
|
+
if (name === "htmx:configRequest") {
|
|
43
|
+
const token = getCsrfToken();
|
|
44
|
+
if (token)
|
|
45
|
+
evt.detail.headers["X-CSRF-Token"] = token;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
transformResponse(text, xhr) {
|
|
50
|
+
if (xhr.getResponseHeader("content-type")?.includes("application/json")) {
|
|
51
|
+
const d = document.createElement("div");
|
|
52
|
+
d.textContent = text;
|
|
53
|
+
return d.innerHTML;
|
|
54
|
+
}
|
|
55
|
+
return text;
|
|
56
|
+
},
|
|
57
|
+
isInlineSwap: (s) => s === "json",
|
|
58
|
+
handleSwap(style, target, frag) {
|
|
59
|
+
if (style === "json") {
|
|
60
|
+
try {
|
|
61
|
+
swapJson(target, JSON.parse(frag.textContent ?? ""));
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
target.innerHTML = `<div style="color:red;padding:1rem">${e}</div>`;
|
|
65
|
+
}
|
|
66
|
+
return [target];
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (DEBUG)
|
|
72
|
+
console.log("[litestar] htmx extension registered");
|
|
73
|
+
}
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Note: hx-route functionality removed - use generated routes directly
|
|
76
|
+
// Import route from your generated routes.ts file instead:
|
|
77
|
+
// import { route } from '@/generated/routes'
|
|
78
|
+
// element.setAttribute('hx-get', route('my_route', { id: 123 }))
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// JSON Swap Entry Point
|
|
82
|
+
// =============================================================================
|
|
83
|
+
export function swapJson(el, data) {
|
|
84
|
+
swap(el, rootCtx(data));
|
|
85
|
+
}
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Core Swap Logic
|
|
88
|
+
// =============================================================================
|
|
89
|
+
function swap(node, ctx, end, parse = false) {
|
|
90
|
+
// Text nodes: interpolate ${expr}
|
|
91
|
+
if (node.nodeType === 3) {
|
|
92
|
+
const g = memo(node, "t", () => {
|
|
93
|
+
const t = node.textContent ?? "";
|
|
94
|
+
return compileTextExpr(t);
|
|
95
|
+
});
|
|
96
|
+
if (!g)
|
|
97
|
+
return null;
|
|
98
|
+
if (!parse)
|
|
99
|
+
node.textContent = String(g(ctx) ?? "");
|
|
100
|
+
return node;
|
|
101
|
+
}
|
|
102
|
+
// Elements
|
|
103
|
+
if (node.nodeType === 1) {
|
|
104
|
+
const el = node;
|
|
105
|
+
// Template: structural directives
|
|
106
|
+
if (el.nodeName === "TEMPLATE") {
|
|
107
|
+
return forDir(el, ctx, end, parse) ?? ifDir(el, ctx, end, parse);
|
|
108
|
+
}
|
|
109
|
+
// Process attribute directives
|
|
110
|
+
let c = ctx;
|
|
111
|
+
const handlers = memo(el, "a", () => {
|
|
112
|
+
const h = [];
|
|
113
|
+
for (const attr of Array.from(el.attributes)) {
|
|
114
|
+
const d = directives.find((x) => x.match(attr));
|
|
115
|
+
if (d) {
|
|
116
|
+
const handler = d.create(el, attr);
|
|
117
|
+
if (handler)
|
|
118
|
+
h.push(handler);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return h;
|
|
122
|
+
});
|
|
123
|
+
for (const h of handlers) {
|
|
124
|
+
if (!parse && c) {
|
|
125
|
+
const r = h(c, el);
|
|
126
|
+
if (r !== undefined)
|
|
127
|
+
c = r;
|
|
128
|
+
}
|
|
129
|
+
if (!c)
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (c === false)
|
|
133
|
+
return el;
|
|
134
|
+
if (!c)
|
|
135
|
+
return null;
|
|
136
|
+
// Recurse children
|
|
137
|
+
swapKids(el.firstChild, undefined, c, parse);
|
|
138
|
+
return el;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function swapKids(start, end, ctx, parse = false) {
|
|
143
|
+
let current = start;
|
|
144
|
+
while (current && current !== end) {
|
|
145
|
+
const r = swap(current, ctx, end, parse);
|
|
146
|
+
current = (r ?? current).nextSibling;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const directives = [
|
|
150
|
+
// :attr="expr" - attribute binding
|
|
151
|
+
{
|
|
152
|
+
match: (a) => a.name.startsWith(":"),
|
|
153
|
+
create(_el, a) {
|
|
154
|
+
const name = a.name.slice(1);
|
|
155
|
+
const g = expr(a.value);
|
|
156
|
+
if (!g)
|
|
157
|
+
return null;
|
|
158
|
+
return (ctx, el) => {
|
|
159
|
+
const v = g(ctx);
|
|
160
|
+
if (name === "class" && typeof v === "object" && v) {
|
|
161
|
+
for (const [k, on] of Object.entries(v))
|
|
162
|
+
el.classList.toggle(k, Boolean(on));
|
|
163
|
+
}
|
|
164
|
+
else if (v == null || v === false) {
|
|
165
|
+
el.removeAttribute(name);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
el.setAttribute(name, v === true ? "" : String(v));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
// @event="expr" - event binding
|
|
174
|
+
{
|
|
175
|
+
match: (a) => a.name.startsWith("@"),
|
|
176
|
+
create(_el, a) {
|
|
177
|
+
const name = a.name.slice(1);
|
|
178
|
+
const g = expr(a.value);
|
|
179
|
+
if (!g)
|
|
180
|
+
return null;
|
|
181
|
+
let bound = false;
|
|
182
|
+
return (ctx, el) => {
|
|
183
|
+
if (!bound) {
|
|
184
|
+
bound = true;
|
|
185
|
+
el.addEventListener(name, (e) => g({ ...ctx, $event: e }));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
// ls-scope="expr" - change context
|
|
191
|
+
{
|
|
192
|
+
match: (a) => a.name === "ls-scope",
|
|
193
|
+
create(_, a) {
|
|
194
|
+
const g = expr(a.value);
|
|
195
|
+
if (!g)
|
|
196
|
+
return null;
|
|
197
|
+
return (ctx) => {
|
|
198
|
+
const d = g(ctx);
|
|
199
|
+
return d ? childCtx(ctx, d) : false;
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
// ls-text="expr" - text content
|
|
204
|
+
{
|
|
205
|
+
match: (a) => a.name === "ls-text",
|
|
206
|
+
create(_, a) {
|
|
207
|
+
const g = expr(a.value);
|
|
208
|
+
if (!g)
|
|
209
|
+
return null;
|
|
210
|
+
return (ctx, el) => {
|
|
211
|
+
el.textContent = String(g(ctx) ?? "");
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
// ls-html="expr" - innerHTML (use carefully)
|
|
216
|
+
{
|
|
217
|
+
match: (a) => a.name === "ls-html",
|
|
218
|
+
create(_, a) {
|
|
219
|
+
const g = expr(a.value);
|
|
220
|
+
if (!g)
|
|
221
|
+
return null;
|
|
222
|
+
return (ctx, el) => {
|
|
223
|
+
el.innerHTML = String(g(ctx) ?? "");
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
// ls-show/ls-hide
|
|
228
|
+
{
|
|
229
|
+
match: (a) => a.name === "ls-show",
|
|
230
|
+
create(_, a) {
|
|
231
|
+
const g = expr(a.value);
|
|
232
|
+
if (!g)
|
|
233
|
+
return null;
|
|
234
|
+
return (ctx, el) => {
|
|
235
|
+
;
|
|
236
|
+
el.style.display = g(ctx) ? "" : "none";
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
match: (a) => a.name === "ls-hide",
|
|
242
|
+
create(_, a) {
|
|
243
|
+
const g = expr(a.value);
|
|
244
|
+
if (!g)
|
|
245
|
+
return null;
|
|
246
|
+
return (ctx, el) => {
|
|
247
|
+
;
|
|
248
|
+
el.style.display = g(ctx) ? "none" : "";
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
// name attr on inputs - auto-bind from context
|
|
253
|
+
{
|
|
254
|
+
match: (a) => a.name === "name",
|
|
255
|
+
create(el, a) {
|
|
256
|
+
if (!(el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement))
|
|
257
|
+
return null;
|
|
258
|
+
const key = a.value;
|
|
259
|
+
return (ctx, el) => {
|
|
260
|
+
const v = ctx[key];
|
|
261
|
+
if (v === undefined)
|
|
262
|
+
return;
|
|
263
|
+
const inp = el;
|
|
264
|
+
if (inp.type === "checkbox")
|
|
265
|
+
inp.checked = Boolean(v);
|
|
266
|
+
else if (inp.type === "radio")
|
|
267
|
+
inp.checked = v === inp.value;
|
|
268
|
+
else
|
|
269
|
+
inp.value = String(v ?? "");
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
// =============================================================================
|
|
275
|
+
// Structural: ls-for
|
|
276
|
+
// =============================================================================
|
|
277
|
+
function forDir(tpl, ctx, _parentEnd, parse = false) {
|
|
278
|
+
const raw = tpl.getAttribute("ls-for") ?? tpl.getAttribute("ls-each");
|
|
279
|
+
if (!raw)
|
|
280
|
+
return null;
|
|
281
|
+
preparseTpl(tpl);
|
|
282
|
+
if (parse)
|
|
283
|
+
return tpl;
|
|
284
|
+
// Parse "item in items" or just "items"
|
|
285
|
+
const m = raw.match(/^\s*(\w+)\s+in\s+(.+)$/);
|
|
286
|
+
const [alias, listExpr] = m ? [m[1], m[2]] : [null, raw];
|
|
287
|
+
const g = memo(tpl, "g", () => expr(listExpr));
|
|
288
|
+
if (!g)
|
|
289
|
+
return null;
|
|
290
|
+
const items = toList(g(ctx), tpl, ctx, alias);
|
|
291
|
+
const end = memo(tpl, "end", () => insertComment(tpl, "/ls-for"));
|
|
292
|
+
const old = memo(tpl, "list", () => collectComments(tpl, end));
|
|
293
|
+
let i = 0;
|
|
294
|
+
for (; i < items.length; i++) {
|
|
295
|
+
const [key, item] = items[i];
|
|
296
|
+
const c = childCtx(ctx, item, i, key);
|
|
297
|
+
if (alias)
|
|
298
|
+
c[alias] = item;
|
|
299
|
+
if (i < old.length && old[i][0] === key) {
|
|
300
|
+
// Same key: update in place
|
|
301
|
+
swapKids(old[i][1].nextSibling, old[i + 1]?.[1] ?? end, c);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// Insert new
|
|
305
|
+
const clone = tpl.content.cloneNode(true);
|
|
306
|
+
const comment = document.createComment(key);
|
|
307
|
+
const ref = old[i]?.[1] ?? end;
|
|
308
|
+
ref.parentNode?.insertBefore(comment, ref);
|
|
309
|
+
ref.parentNode?.insertBefore(clone, ref);
|
|
310
|
+
swapKids(comment.nextSibling, ref, c);
|
|
311
|
+
// Remove old if exists
|
|
312
|
+
if (i < old.length) {
|
|
313
|
+
removeBetween(old[i][1], old[i + 1]?.[1] ?? end);
|
|
314
|
+
old[i][1].remove();
|
|
315
|
+
}
|
|
316
|
+
old[i] = [key, comment];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Remove excess
|
|
320
|
+
while (old.length > items.length) {
|
|
321
|
+
const popped = old.pop();
|
|
322
|
+
if (!popped)
|
|
323
|
+
break;
|
|
324
|
+
const [, c] = popped;
|
|
325
|
+
removeBetween(c, old[old.length]?.[1] ?? end);
|
|
326
|
+
c.remove();
|
|
327
|
+
}
|
|
328
|
+
return end;
|
|
329
|
+
}
|
|
330
|
+
// =============================================================================
|
|
331
|
+
// Structural: ls-if
|
|
332
|
+
// =============================================================================
|
|
333
|
+
function ifDir(tpl, ctx, _parentEnd, parse = false) {
|
|
334
|
+
const raw = tpl.getAttribute("ls-if");
|
|
335
|
+
if (!raw)
|
|
336
|
+
return null;
|
|
337
|
+
preparseTpl(tpl);
|
|
338
|
+
// Find else template
|
|
339
|
+
const elseTpl = tpl.nextElementSibling?.nodeName === "TEMPLATE" && tpl.nextElementSibling.hasAttribute("ls-else") ? tpl.nextElementSibling : null;
|
|
340
|
+
if (elseTpl)
|
|
341
|
+
preparseTpl(elseTpl);
|
|
342
|
+
if (parse)
|
|
343
|
+
return elseTpl ?? tpl;
|
|
344
|
+
const g = memo(tpl, "g", () => expr(raw));
|
|
345
|
+
const anchor = memo(tpl, "anchor", () => insertComment(tpl, ""));
|
|
346
|
+
const end = memo(tpl, "end", () => insertComment(anchor, "/ls-if"));
|
|
347
|
+
const show = g?.(ctx);
|
|
348
|
+
if (show) {
|
|
349
|
+
if (anchor.data !== "if") {
|
|
350
|
+
anchor.data = "if";
|
|
351
|
+
removeBetween(anchor.nextSibling, end);
|
|
352
|
+
end.parentNode?.insertBefore(tpl.content.cloneNode(true), end);
|
|
353
|
+
}
|
|
354
|
+
swapKids(anchor.nextSibling, end, ctx);
|
|
355
|
+
}
|
|
356
|
+
else if (elseTpl) {
|
|
357
|
+
if (anchor.data !== "else") {
|
|
358
|
+
anchor.data = "else";
|
|
359
|
+
removeBetween(anchor.nextSibling, end);
|
|
360
|
+
end.parentNode?.insertBefore(elseTpl.content.cloneNode(true), end);
|
|
361
|
+
}
|
|
362
|
+
swapKids(anchor.nextSibling, end, ctx);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
anchor.data = "";
|
|
366
|
+
removeBetween(anchor.nextSibling, end);
|
|
367
|
+
}
|
|
368
|
+
return end;
|
|
369
|
+
}
|
|
370
|
+
// =============================================================================
|
|
371
|
+
// Context
|
|
372
|
+
// =============================================================================
|
|
373
|
+
function rootCtx(data) {
|
|
374
|
+
const ctx = {
|
|
375
|
+
$data: data,
|
|
376
|
+
// Note: route and navigate are optional - users can provide their own
|
|
377
|
+
// by importing from their generated routes.ts file
|
|
378
|
+
};
|
|
379
|
+
if (data && typeof data === "object")
|
|
380
|
+
Object.setPrototypeOf(ctx, data);
|
|
381
|
+
return ctx;
|
|
382
|
+
}
|
|
383
|
+
function childCtx(parent, data, index, key) {
|
|
384
|
+
const ctx = Object.create(data && typeof data === "object" ? data : null);
|
|
385
|
+
ctx.$data = data;
|
|
386
|
+
ctx.$parent = parent;
|
|
387
|
+
ctx.$index = index;
|
|
388
|
+
ctx.$key = key;
|
|
389
|
+
ctx.route = parent.route;
|
|
390
|
+
ctx.navigate = parent.navigate;
|
|
391
|
+
return ctx;
|
|
392
|
+
}
|
|
393
|
+
// =============================================================================
|
|
394
|
+
// Expression Compiler
|
|
395
|
+
// =============================================================================
|
|
396
|
+
function expr(s) {
|
|
397
|
+
if (!s)
|
|
398
|
+
return null;
|
|
399
|
+
const cached = cache.get(s);
|
|
400
|
+
if (cached !== undefined)
|
|
401
|
+
return cached;
|
|
402
|
+
try {
|
|
403
|
+
const fn = new Function("ctx", `with(ctx){return(${s})}`);
|
|
404
|
+
cache.set(s, fn);
|
|
405
|
+
return fn;
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
cache.set(s, null);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** Compile text with ${expr} interpolation - escapes backticks and backslashes */
|
|
413
|
+
function compileTextExpr(t) {
|
|
414
|
+
if (!t.includes("${"))
|
|
415
|
+
return null;
|
|
416
|
+
// Escape backticks and backslashes for safe template literal compilation
|
|
417
|
+
const escaped = t.replace(/[`\\]/g, "\\$&");
|
|
418
|
+
return expr(`\`${escaped}\``);
|
|
419
|
+
}
|
|
420
|
+
// =============================================================================
|
|
421
|
+
// Utilities
|
|
422
|
+
// =============================================================================
|
|
423
|
+
function memo(node, key, fn) {
|
|
424
|
+
let store = memoStore.get(node);
|
|
425
|
+
if (!store) {
|
|
426
|
+
store = {};
|
|
427
|
+
memoStore.set(node, store);
|
|
428
|
+
}
|
|
429
|
+
if (!(key in store)) {
|
|
430
|
+
store[key] = fn();
|
|
431
|
+
}
|
|
432
|
+
return store[key];
|
|
433
|
+
}
|
|
434
|
+
function preparseTpl(t) {
|
|
435
|
+
memo(t, "p", () => {
|
|
436
|
+
swapKids(t.content.firstChild, undefined, {}, true);
|
|
437
|
+
return true;
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function toList(items, tpl, ctx, alias) {
|
|
441
|
+
const keyAttr = tpl.getAttribute("ls-key");
|
|
442
|
+
const keyFn = keyAttr ? expr(keyAttr) : null;
|
|
443
|
+
if (Array.isArray(items)) {
|
|
444
|
+
return items.map((item, i) => {
|
|
445
|
+
if (!keyFn)
|
|
446
|
+
return [String(i), item];
|
|
447
|
+
// Create a child context with the alias so key expressions like "item.id" work
|
|
448
|
+
const keyCtx = childCtx(ctx, item, i);
|
|
449
|
+
if (alias)
|
|
450
|
+
keyCtx[alias] = item;
|
|
451
|
+
return [String(keyFn(keyCtx)), item];
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (items && typeof items === "object") {
|
|
455
|
+
return Object.entries(items);
|
|
456
|
+
}
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
function insertComment(after, text) {
|
|
460
|
+
const c = document.createComment(text);
|
|
461
|
+
after.parentNode?.insertBefore(c, after.nextSibling);
|
|
462
|
+
return c;
|
|
463
|
+
}
|
|
464
|
+
function collectComments(tpl, end) {
|
|
465
|
+
const list = [];
|
|
466
|
+
let n = tpl.nextSibling;
|
|
467
|
+
while (n && n !== end) {
|
|
468
|
+
if (n.nodeType === 8 && !n.data.startsWith("/")) {
|
|
469
|
+
list.push([n.data, n]);
|
|
470
|
+
}
|
|
471
|
+
n = n.nextSibling;
|
|
472
|
+
}
|
|
473
|
+
return list;
|
|
474
|
+
}
|
|
475
|
+
function removeBetween(start, end) {
|
|
476
|
+
let current = start;
|
|
477
|
+
while (current && current !== end) {
|
|
478
|
+
const next = current.nextSibling;
|
|
479
|
+
current.parentNode?.removeChild(current);
|
|
480
|
+
current = next;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// =============================================================================
|
|
484
|
+
// Public API
|
|
485
|
+
// =============================================================================
|
|
486
|
+
export function setDebug(_on) {
|
|
487
|
+
// Debug flag is const, this is a no-op placeholder
|
|
488
|
+
}
|
|
489
|
+
export function addDirective(dir) {
|
|
490
|
+
directives.push(dir);
|
|
491
|
+
}
|
|
492
|
+
// Auto-register
|
|
493
|
+
if (typeof window !== "undefined" && window.htmx)
|
|
494
|
+
registerHtmxExtension();
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
|
-
* import {
|
|
9
|
+
* import { getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
|
|
10
10
|
*
|
|
11
|
-
* //
|
|
12
|
-
* const
|
|
11
|
+
* // Get CSRF token
|
|
12
|
+
* const token = getCsrfToken()
|
|
13
13
|
*
|
|
14
14
|
* // Make a fetch request with CSRF token
|
|
15
15
|
* await csrfFetch('/api/submit', {
|
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
* })
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
|
+
* For type-safe routing, import from your generated routes file:
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { route, routes, type RouteName } from '@/generated/routes'
|
|
24
|
+
*
|
|
25
|
+
* // Type-safe URL generation
|
|
26
|
+
* const url = route('user_detail', { user_id: 123 }) // Compile-time checked!
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
21
29
|
* @module
|
|
22
30
|
*/
|
|
23
31
|
export { csrfFetch, csrfHeaders, getCsrfToken } from "./csrf.js";
|
|
24
|
-
export {
|
|
32
|
+
export { addDirective, registerHtmxExtension, setDebug as setHtmxDebug, swapJson } from "./htmx.js";
|