litestar-vite-plugin 0.15.0-alpha.2 → 0.15.0-alpha.4

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/nuxt.js CHANGED
@@ -3,9 +3,46 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import colors from "picocolors";
6
- import { resolveInstallHint } from "./install-hint.js";
6
+ import { resolveInstallHint, resolvePackageExecutor } from "./install-hint.js";
7
+ import { debounce } from "./shared/debounce.js";
7
8
  const execAsync = promisify(exec);
9
+ function normalizeHost(host) {
10
+ if (host === "::" || host === "::1" || host === "0.0.0.0") {
11
+ return "localhost";
12
+ }
13
+ if (host.includes(":") && !host.startsWith("[")) {
14
+ return `[${host}]`;
15
+ }
16
+ return host;
17
+ }
8
18
  function resolveConfig(config = {}) {
19
+ const runtimeConfigPath = process.env.LITESTAR_VITE_CONFIG_PATH;
20
+ let hotFile;
21
+ let proxyMode = "vite";
22
+ let devPort;
23
+ const envPort = process.env.VITE_PORT;
24
+ if (envPort) {
25
+ devPort = Number.parseInt(envPort, 10);
26
+ if (Number.isNaN(devPort)) {
27
+ devPort = void 0;
28
+ }
29
+ }
30
+ let pythonExecutor;
31
+ if (runtimeConfigPath && fs.existsSync(runtimeConfigPath)) {
32
+ try {
33
+ const json = JSON.parse(fs.readFileSync(runtimeConfigPath, "utf-8"));
34
+ const bundleDir = json.bundleDir ?? "public";
35
+ const hot = json.hotFile ?? "hot";
36
+ hotFile = path.resolve(process.cwd(), bundleDir, hot);
37
+ proxyMode = json.proxyMode ?? "vite";
38
+ if (json.port !== void 0) {
39
+ devPort = json.port;
40
+ }
41
+ pythonExecutor = json.executor;
42
+ } catch {
43
+ hotFile = void 0;
44
+ }
45
+ }
9
46
  let typesConfig = false;
10
47
  if (config.types === true) {
11
48
  typesConfig = {
@@ -30,30 +67,40 @@ function resolveConfig(config = {}) {
30
67
  apiProxy: config.apiProxy ?? "http://localhost:8000",
31
68
  apiPrefix: config.apiPrefix ?? "/api",
32
69
  types: typesConfig,
33
- verbose: config.verbose ?? false
70
+ verbose: config.verbose ?? false,
71
+ hotFile,
72
+ proxyMode,
73
+ devPort,
74
+ executor: config.executor ?? pythonExecutor
34
75
  };
35
76
  }
36
- function debounce(func, wait) {
37
- let timeout = null;
38
- return (...args) => {
39
- if (timeout) {
40
- clearTimeout(timeout);
41
- }
42
- timeout = setTimeout(() => func(...args), wait);
43
- };
77
+ async function getPort() {
78
+ return new Promise((resolve, reject) => {
79
+ import("node:net").then(({ createServer }) => {
80
+ const server = createServer();
81
+ server.unref();
82
+ server.on("error", reject);
83
+ server.listen(0, () => {
84
+ const address = server.address();
85
+ const port = typeof address === "object" && address ? address.port : 0;
86
+ server.close(() => resolve(port));
87
+ });
88
+ });
89
+ });
44
90
  }
45
91
  function createProxyPlugin(config) {
92
+ let hmrPort = 0;
46
93
  return {
47
94
  name: "litestar-nuxt-proxy",
48
- config() {
95
+ async config() {
96
+ hmrPort = await getPort();
49
97
  return {
50
98
  server: {
51
- proxy: {
52
- [config.apiPrefix]: {
53
- target: config.apiProxy,
54
- changeOrigin: true,
55
- secure: false
56
- }
99
+ // Avoid HMR port collisions by letting Vite pick a free port for WS
100
+ hmr: {
101
+ port: hmrPort,
102
+ host: "127.0.0.1",
103
+ clientPort: config.devPort
57
104
  }
58
105
  }
59
106
  };
@@ -67,12 +114,21 @@ function createProxyPlugin(config) {
67
114
  next();
68
115
  });
69
116
  }
117
+ if (config.hotFile) {
118
+ const hmrHotFile = `${config.hotFile}.hmr`;
119
+ const hmrUrl = `http://127.0.0.1:${hmrPort}`;
120
+ fs.writeFileSync(hmrHotFile, hmrUrl);
121
+ if (config.verbose) {
122
+ console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`HMR Hotfile written: ${hmrHotFile} -> ${hmrUrl}`));
123
+ }
124
+ }
70
125
  server.httpServer?.once("listening", () => {
71
126
  setTimeout(() => {
72
127
  console.log("");
73
128
  console.log(` ${colors.cyan("[litestar-nuxt]")} ${colors.green("Integration active")}`);
74
129
  console.log(` ${colors.dim("\u251C\u2500")} API Proxy: ${colors.yellow(config.apiProxy)}`);
75
130
  console.log(` ${colors.dim("\u251C\u2500")} API Prefix: ${colors.yellow(config.apiPrefix)}`);
131
+ console.log(` ${colors.dim("\u251C\u2500")} HMR Port: ${colors.yellow(hmrPort)}`);
76
132
  if (config.types !== false && config.types.enabled) {
77
133
  console.log(` ${colors.dim("\u2514\u2500")} Types Output: ${colors.yellow(config.types.output)}`);
78
134
  } else {
@@ -84,7 +140,132 @@ function createProxyPlugin(config) {
84
140
  }
85
141
  };
86
142
  }
87
- function createTypeGenerationPlugin(typesConfig) {
143
+ async function emitRouteTypes(routesPath, outputDir) {
144
+ const contents = await fs.promises.readFile(routesPath, "utf-8");
145
+ const json = JSON.parse(contents);
146
+ const outDir = path.resolve(process.cwd(), outputDir);
147
+ await fs.promises.mkdir(outDir, { recursive: true });
148
+ const outFile = path.join(outDir, "routes.ts");
149
+ const banner = `// AUTO-GENERATED by litestar-vite. Do not edit.
150
+ /* eslint-disable */
151
+
152
+ `;
153
+ const routesData = json.routes || json;
154
+ const routeNames = Object.keys(routesData);
155
+ const routeNameType = routeNames.length > 0 ? routeNames.map((n) => `"${n}"`).join(" | ") : "never";
156
+ const routeParamTypes = [];
157
+ for (const [name, data] of Object.entries(routesData)) {
158
+ const routeData = data;
159
+ if (routeData.parameters && routeData.parameters.length > 0) {
160
+ const params = routeData.parameters.map((p) => `${p}: string | number`).join("; ");
161
+ routeParamTypes.push(` "${name}": { ${params} }`);
162
+ } else {
163
+ routeParamTypes.push(` "${name}": Record<string, never>`);
164
+ }
165
+ }
166
+ const body = `/**
167
+ * AUTO-GENERATED by litestar-vite.
168
+ *
169
+ * Exports:
170
+ * - routesMeta: full route metadata
171
+ * - routes: name -> uri map
172
+ * - serverRoutes: alias of routes for clarity in apps
173
+ * - route(): type-safe URL generator
174
+ * - hasRoute(): type guard
175
+ * - csrf helpers re-exported from litestar-vite-plugin/helpers
176
+ *
177
+ * @see https://litestar-vite.litestar.dev/
178
+ */
179
+ export const routesMeta = ${JSON.stringify(json, null, 2)} as const
180
+
181
+ /**
182
+ * Route name to URI mapping.
183
+ */
184
+ export const routes = ${JSON.stringify(Object.fromEntries(Object.entries(routesData).map(([name, data]) => [name, data.uri])), null, 2)} as const
185
+
186
+ /**
187
+ * Alias for server-injected route map (more descriptive for consumers).
188
+ */
189
+ export const serverRoutes = routes
190
+
191
+ /**
192
+ * All available route names.
193
+ */
194
+ export type RouteName = ${routeNameType}
195
+
196
+ /**
197
+ * Parameter types for each route.
198
+ */
199
+ export interface RouteParams {
200
+ ${routeParamTypes.join("\n")}
201
+ }
202
+
203
+ /**
204
+ * Generate a URL for a named route with type-safe parameters.
205
+ *
206
+ * @param name - The route name
207
+ * @param params - Route parameters (required if route has path parameters)
208
+ * @returns The generated URL
209
+ *
210
+ * @example
211
+ * \`\`\`ts
212
+ * import { route } from '@/generated/routes'
213
+ *
214
+ * // Route without parameters
215
+ * route('home') // "/"
216
+ *
217
+ * // Route with parameters
218
+ * route('user:detail', { user_id: 123 }) // "/users/123"
219
+ * \`\`\`
220
+ */
221
+ export function route<T extends RouteName>(
222
+ name: T,
223
+ ...args: RouteParams[T] extends Record<string, never> ? [] : [params: RouteParams[T]]
224
+ ): string {
225
+ let uri = routes[name] as string
226
+ const params = args[0] as Record<string, string | number> | undefined
227
+
228
+ if (params) {
229
+ for (const [key, value] of Object.entries(params)) {
230
+ // Handle both {param} and {param:type} syntax
231
+ uri = uri.replace(new RegExp(\`\\\\{\${key}(?::[^}]+)?\\\\}\`, "g"), String(value))
232
+ }
233
+ }
234
+
235
+ return uri
236
+ }
237
+
238
+ /**
239
+ * Check if a route name exists.
240
+ */
241
+ export function hasRoute(name: string): name is RouteName {
242
+ return name in routes
243
+ }
244
+
245
+ declare global {
246
+ interface Window {
247
+ /**
248
+ * Fully-typed route metadata injected by Litestar.
249
+ */
250
+ __LITESTAR_ROUTES__?: typeof routesMeta
251
+ /**
252
+ * Simple route map (name -> uri) for legacy consumers.
253
+ */
254
+ routes?: typeof routes
255
+ serverRoutes?: typeof serverRoutes
256
+ }
257
+ // eslint-disable-next-line no-var
258
+ var routes: typeof routes | undefined
259
+ var serverRoutes: typeof serverRoutes | undefined
260
+ }
261
+
262
+ // Re-export helper functions from litestar-vite-plugin
263
+ // These work with the routes defined above
264
+ export { getCsrfToken, csrfHeaders, csrfFetch } from "litestar-vite-plugin/helpers"
265
+ `;
266
+ await fs.promises.writeFile(outFile, `${banner}${body}`, "utf-8");
267
+ }
268
+ function createTypeGenerationPlugin(typesConfig, executor) {
88
269
  let server = null;
89
270
  let isGenerating = false;
90
271
  async function runTypeGeneration() {
@@ -102,11 +283,15 @@ function createTypeGenerationPlugin(typesConfig) {
102
283
  console.log(colors.cyan("[litestar-nuxt]"), colors.dim("Generating TypeScript types..."));
103
284
  const args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
104
285
  if (typesConfig.generateZod) {
105
- args.push("--plugins", "@hey-api/schemas", "@hey-api/types");
286
+ args.push("--plugins", "zod", "@hey-api/typescript");
106
287
  }
107
- await execAsync(`npx ${args.join(" ")}`, {
288
+ await execAsync(resolvePackageExecutor(args.join(" "), executor), {
108
289
  cwd: process.cwd()
109
290
  });
291
+ const routesPath = path.resolve(process.cwd(), typesConfig.routesPath);
292
+ if (fs.existsSync(routesPath)) {
293
+ await emitRouteTypes(routesPath, typesConfig.output);
294
+ }
110
295
  const duration = Date.now() - startTime;
111
296
  console.log(colors.cyan("[litestar-nuxt]"), colors.green("Types generated"), colors.dim(`in ${duration}ms`));
112
297
  if (server) {
@@ -158,37 +343,76 @@ function litestarPlugins(userConfig = {}) {
158
343
  const config = resolveConfig(userConfig);
159
344
  const plugins = [createProxyPlugin(config)];
160
345
  if (config.types !== false && config.types.enabled) {
161
- plugins.push(createTypeGenerationPlugin(config.types));
346
+ plugins.push(createTypeGenerationPlugin(config.types, config.executor));
162
347
  }
163
348
  return plugins;
164
349
  }
165
- const litestarModule = {
166
- meta: {
167
- name: "litestar-vite",
168
- configKey: "litestar",
169
- compatibility: {
170
- nuxt: ">=3.0.0"
350
+ function litestarNuxtModule(userOptions, nuxt) {
351
+ const nuxtConfigOptions = nuxt.options.litestar;
352
+ const mergedOptions = { ...nuxtConfigOptions, ...userOptions };
353
+ const config = resolveConfig(mergedOptions);
354
+ const plugins = litestarPlugins(config);
355
+ nuxt.options.vite = nuxt.options.vite || {};
356
+ nuxt.options.vite.plugins = nuxt.options.vite.plugins || [];
357
+ nuxt.options.vite.plugins.push(...plugins);
358
+ nuxt.options.runtimeConfig = nuxt.options.runtimeConfig || {};
359
+ nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
360
+ nuxt.options.runtimeConfig.public.apiProxy = config.apiProxy;
361
+ nuxt.options.runtimeConfig.public.apiPrefix = config.apiPrefix;
362
+ nuxt.options.nitro = nuxt.options.nitro || {};
363
+ nuxt.options.nitro.devProxy = nuxt.options.nitro.devProxy || {};
364
+ nuxt.options.nitro.devProxy[config.apiPrefix] = {
365
+ target: config.apiProxy,
366
+ changeOrigin: true,
367
+ ws: true
368
+ };
369
+ if (config.verbose) {
370
+ console.log(colors.cyan("[litestar-nuxt]"), "Runtime config:");
371
+ console.log(` apiProxy: ${config.apiProxy}`);
372
+ console.log(` apiPrefix: ${config.apiPrefix}`);
373
+ console.log(` verbose: ${config.verbose}`);
374
+ console.log(colors.cyan("[litestar-nuxt]"), "Nitro devProxy configured:");
375
+ console.log(JSON.stringify(nuxt.options.nitro.devProxy, null, 2));
376
+ }
377
+ if (config.hotFile && config.devPort) {
378
+ const rawHost = process.env.NUXT_HOST || process.env.HOST || "localhost";
379
+ const host = normalizeHost(rawHost);
380
+ const url = `http://${host}:${config.devPort}`;
381
+ fs.mkdirSync(path.dirname(config.hotFile), { recursive: true });
382
+ fs.writeFileSync(config.hotFile, url);
383
+ if (config.verbose) {
384
+ console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`Hotfile written: ${config.hotFile} -> ${url}`));
171
385
  }
172
- },
173
- defaults: {
174
- apiProxy: "http://localhost:8000",
175
- apiPrefix: "/api",
176
- types: false,
177
- verbose: false
178
- },
179
- /**
180
- * Setup function for the Nuxt module.
181
- * This is called by Nuxt when the module is loaded.
182
- */
183
- setup(userOptions, nuxt) {
184
- const config = resolveConfig(userOptions);
185
- const plugins = litestarPlugins(config);
186
- nuxt.options.vite = nuxt.options.vite || {};
187
- nuxt.options.vite.plugins = nuxt.options.vite.plugins || [];
188
- nuxt.options.vite.plugins.push(...plugins);
189
- console.log(colors.cyan("[litestar-nuxt]"), "Module initialized");
386
+ }
387
+ if (nuxt.hook && config.hotFile) {
388
+ nuxt.hook("listen", (_server, listener) => {
389
+ const info = listener;
390
+ if (info && typeof info.port === "number") {
391
+ const host = normalizeHost(info.host || "localhost");
392
+ const url = `http://${host}:${info.port}`;
393
+ fs.writeFileSync(config.hotFile, url);
394
+ if (config.verbose) {
395
+ console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`Hotfile updated via listen hook: ${url}`));
396
+ }
397
+ }
398
+ });
399
+ }
400
+ console.log(colors.cyan("[litestar-nuxt]"), "Module initialized");
401
+ }
402
+ litestarNuxtModule.meta = {
403
+ name: "litestar-vite",
404
+ configKey: "litestar",
405
+ compatibility: {
406
+ nuxt: ">=3.0.0"
190
407
  }
191
408
  };
409
+ litestarNuxtModule.getOptions = () => ({
410
+ apiProxy: "http://localhost:8000",
411
+ apiPrefix: "/api",
412
+ types: false,
413
+ verbose: false
414
+ });
415
+ const litestarModule = litestarNuxtModule;
192
416
  var nuxt_default = litestarModule;
193
417
  export {
194
418
  nuxt_default as default,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared debounce utility for litestar-vite.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Creates a debounced function that delays invoking `func` until after
8
+ * `wait` milliseconds have elapsed since the last invocation.
9
+ *
10
+ * This implementation is type-safe and properly infers argument types.
11
+ *
12
+ * @param func - The function to debounce
13
+ * @param wait - Milliseconds to wait before invoking
14
+ * @returns Debounced function with the same signature
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const debouncedSave = debounce((data: string) => {
19
+ * console.log('Saving:', data)
20
+ * }, 300)
21
+ *
22
+ * debouncedSave('hello') // Delayed
23
+ * debouncedSave('world') // Cancels previous, delays again
24
+ * // Only 'world' is logged after 300ms
25
+ * ```
26
+ */
27
+ export declare function debounce<Args extends unknown[]>(func: (...args: Args) => void, wait: number): (...args: Args) => void;
@@ -0,0 +1,15 @@
1
+ function debounce(func, wait) {
2
+ let timeout = null;
3
+ return (...args) => {
4
+ if (timeout !== null) {
5
+ clearTimeout(timeout);
6
+ }
7
+ timeout = setTimeout(() => {
8
+ func(...args);
9
+ timeout = null;
10
+ }, wait);
11
+ };
12
+ }
13
+ export {
14
+ debounce
15
+ };
@@ -107,6 +107,13 @@ export interface LitestarSvelteKitConfig {
107
107
  * @default false
108
108
  */
109
109
  verbose?: boolean;
110
+ /**
111
+ * JavaScript runtime executor for package commands.
112
+ * Used when running tools like @hey-api/openapi-ts.
113
+ *
114
+ * @default undefined (uses LITESTAR_VITE_RUNTIME env or 'node')
115
+ */
116
+ executor?: "node" | "bun" | "deno" | "yarn" | "pnpm";
110
117
  }
111
118
  /**
112
119
  * Litestar integration plugin for SvelteKit.