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.
@@ -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 args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
231
- if (typesConfig.generateZod) {
232
- args.push("--plugins", "zod", "@hey-api/typescript");
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: process.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" && config.port !== void 0) {
343
+ if (command === "dev") {
315
344
  configUpdate.server = {
316
- port: config.port
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(`Setting Astro server port to ${config.port}`);
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 { route, getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
9
+ * import { getCsrfToken, csrfFetch } from 'litestar-vite-plugin/helpers'
10
10
  *
11
- * // Generate a URL for a named route
12
- * const url = route('user:detail', { user_id: 123 })
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 { currentRoute, getRelativeUrlPath, getRoutes, isCurrentRoute, isRoute, LITESTAR, type LitestarHelpers, type RouteDefinition, type RoutesMap, route, toRoute, } from "./routes.js";
32
+ export { addDirective, registerHtmxExtension, setDebug as setHtmxDebug, swapJson } from "./htmx.js";