litestar-vite-plugin 0.14.0 → 0.15.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * SvelteKit integration for Litestar-Vite.
3
+ *
4
+ * This module provides a Vite plugin specifically designed to work alongside
5
+ * SvelteKit's own Vite plugin. It enables:
6
+ * - API proxy configuration for dev server
7
+ * - Type generation integration (shares @hey-api/openapi-ts output)
8
+ * - Seamless integration with SvelteKit's load functions
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // vite.config.ts
13
+ * import { sveltekit } from '@sveltejs/kit/vite';
14
+ * import { litestarSvelteKit } from 'litestar-vite-plugin/sveltekit';
15
+ * import { defineConfig } from 'vite';
16
+ *
17
+ * export default defineConfig({
18
+ * plugins: [
19
+ * litestarSvelteKit({
20
+ * apiProxy: 'http://localhost:8000',
21
+ * types: {
22
+ * enabled: true,
23
+ * output: 'src/lib/api',
24
+ * },
25
+ * }),
26
+ * sveltekit(), // SvelteKit plugin comes after
27
+ * ],
28
+ * });
29
+ * ```
30
+ *
31
+ * @module
32
+ */
33
+ import type { Plugin } from "vite";
34
+ /**
35
+ * Configuration for TypeScript type generation in SvelteKit.
36
+ */
37
+ export interface SvelteKitTypesConfig {
38
+ /**
39
+ * Enable type generation.
40
+ *
41
+ * @default false
42
+ */
43
+ enabled?: boolean;
44
+ /**
45
+ * Path to output generated TypeScript types.
46
+ * Recommended to use SvelteKit's $lib alias path.
47
+ *
48
+ * @default 'src/lib/api'
49
+ */
50
+ output?: string;
51
+ /**
52
+ * Path where the OpenAPI schema is exported by Litestar.
53
+ *
54
+ * @default 'openapi.json'
55
+ */
56
+ openapiPath?: string;
57
+ /**
58
+ * Path where route metadata is exported by Litestar.
59
+ *
60
+ * @default 'routes.json'
61
+ */
62
+ routesPath?: string;
63
+ /**
64
+ * Generate Zod schemas in addition to TypeScript types.
65
+ *
66
+ * @default false
67
+ */
68
+ generateZod?: boolean;
69
+ /**
70
+ * Debounce time in milliseconds for type regeneration.
71
+ *
72
+ * @default 300
73
+ */
74
+ debounce?: number;
75
+ }
76
+ /**
77
+ * Configuration options for the Litestar SvelteKit integration.
78
+ */
79
+ export interface LitestarSvelteKitConfig {
80
+ /**
81
+ * URL of the Litestar API backend for proxying requests during development.
82
+ *
83
+ * @example 'http://localhost:8000'
84
+ * @default 'http://localhost:8000'
85
+ */
86
+ apiProxy?: string;
87
+ /**
88
+ * API route prefix to proxy to the Litestar backend.
89
+ * Requests matching this prefix will be forwarded to the apiProxy URL.
90
+ *
91
+ * @example '/api'
92
+ * @default '/api'
93
+ */
94
+ apiPrefix?: string;
95
+ /**
96
+ * Enable and configure TypeScript type generation.
97
+ *
98
+ * When set to `true`, enables type generation with default settings.
99
+ * When set to a SvelteKitTypesConfig object, enables type generation with custom settings.
100
+ *
101
+ * @default false
102
+ */
103
+ types?: boolean | SvelteKitTypesConfig;
104
+ /**
105
+ * Enable verbose logging for debugging.
106
+ *
107
+ * @default false
108
+ */
109
+ verbose?: boolean;
110
+ }
111
+ /**
112
+ * Litestar integration plugin for SvelteKit.
113
+ *
114
+ * This plugin should be added BEFORE the sveltekit() plugin in your vite.config.ts.
115
+ * It provides API proxying during development and integrates type generation.
116
+ *
117
+ * @param userConfig - Configuration options for the integration
118
+ * @returns A Vite plugin array
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * // vite.config.ts
123
+ * import { sveltekit } from '@sveltejs/kit/vite';
124
+ * import { litestarSvelteKit } from 'litestar-vite-plugin/sveltekit';
125
+ * import { defineConfig } from 'vite';
126
+ *
127
+ * export default defineConfig({
128
+ * plugins: [
129
+ * litestarSvelteKit({
130
+ * apiProxy: 'http://localhost:8000',
131
+ * apiPrefix: '/api',
132
+ * types: {
133
+ * enabled: true,
134
+ * output: 'src/lib/api',
135
+ * generateZod: true,
136
+ * },
137
+ * }),
138
+ * sveltekit(),
139
+ * ],
140
+ * });
141
+ * ```
142
+ *
143
+ * @example Using with SvelteKit load functions
144
+ * ```typescript
145
+ * // src/routes/users/[id]/+page.ts
146
+ * import type { PageLoad } from './$types';
147
+ * import type { User } from '$lib/api/types.gen';
148
+ * import { route } from '$lib/api/routes';
149
+ *
150
+ * export const load: PageLoad = async ({ params, fetch }) => {
151
+ * const response = await fetch(route('users.show', { id: params.id }));
152
+ * const user: User = await response.json();
153
+ * return { user };
154
+ * };
155
+ * ```
156
+ */
157
+ export declare function litestarSvelteKit(userConfig?: LitestarSvelteKitConfig): Plugin[];
158
+ export default litestarSvelteKit;
@@ -0,0 +1,168 @@
1
+ import { exec } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import colors from "picocolors";
6
+ import { resolveInstallHint } from "./install-hint.js";
7
+ const execAsync = promisify(exec);
8
+ function resolveConfig(config = {}) {
9
+ let typesConfig = false;
10
+ if (config.types === true) {
11
+ typesConfig = {
12
+ enabled: true,
13
+ output: "src/lib/api",
14
+ openapiPath: "openapi.json",
15
+ routesPath: "routes.json",
16
+ generateZod: false,
17
+ debounce: 300
18
+ };
19
+ } else if (typeof config.types === "object" && config.types !== null) {
20
+ typesConfig = {
21
+ enabled: config.types.enabled ?? true,
22
+ output: config.types.output ?? "src/lib/api",
23
+ openapiPath: config.types.openapiPath ?? "openapi.json",
24
+ routesPath: config.types.routesPath ?? "routes.json",
25
+ generateZod: config.types.generateZod ?? false,
26
+ debounce: config.types.debounce ?? 300
27
+ };
28
+ }
29
+ return {
30
+ apiProxy: config.apiProxy ?? "http://localhost:8000",
31
+ apiPrefix: config.apiPrefix ?? "/api",
32
+ types: typesConfig,
33
+ verbose: config.verbose ?? false
34
+ };
35
+ }
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
+ };
44
+ }
45
+ function litestarSvelteKit(userConfig = {}) {
46
+ const config = resolveConfig(userConfig);
47
+ const plugins = [];
48
+ plugins.push({
49
+ name: "litestar-sveltekit",
50
+ enforce: "pre",
51
+ config() {
52
+ return {
53
+ server: {
54
+ proxy: {
55
+ [config.apiPrefix]: {
56
+ target: config.apiProxy,
57
+ changeOrigin: true,
58
+ secure: false
59
+ }
60
+ }
61
+ }
62
+ };
63
+ },
64
+ configureServer(server) {
65
+ if (config.verbose) {
66
+ server.middlewares.use((req, _res, next) => {
67
+ if (req.url?.startsWith(config.apiPrefix)) {
68
+ console.log(colors.cyan("[litestar-sveltekit]"), `Proxying: ${req.method} ${req.url}`);
69
+ }
70
+ next();
71
+ });
72
+ }
73
+ server.httpServer?.once("listening", () => {
74
+ setTimeout(() => {
75
+ console.log("");
76
+ console.log(` ${colors.cyan("[litestar-sveltekit]")} ${colors.green("Integration active")}`);
77
+ console.log(` ${colors.dim("\u251C\u2500")} API Proxy: ${colors.yellow(config.apiProxy)}`);
78
+ console.log(` ${colors.dim("\u251C\u2500")} API Prefix: ${colors.yellow(config.apiPrefix)}`);
79
+ if (config.types !== false && config.types.enabled) {
80
+ console.log(` ${colors.dim("\u2514\u2500")} Types Output: ${colors.yellow(config.types.output)}`);
81
+ } else {
82
+ console.log(` ${colors.dim("\u2514\u2500")} Types: ${colors.dim("disabled")}`);
83
+ }
84
+ console.log("");
85
+ }, 100);
86
+ });
87
+ }
88
+ });
89
+ if (config.types !== false && config.types.enabled) {
90
+ plugins.push(createTypeGenerationPlugin(config.types));
91
+ }
92
+ return plugins;
93
+ }
94
+ function createTypeGenerationPlugin(typesConfig) {
95
+ let server = null;
96
+ let isGenerating = false;
97
+ async function runTypeGeneration() {
98
+ if (isGenerating) {
99
+ return false;
100
+ }
101
+ isGenerating = true;
102
+ const startTime = Date.now();
103
+ try {
104
+ const openapiPath = path.resolve(process.cwd(), typesConfig.openapiPath);
105
+ if (!fs.existsSync(openapiPath)) {
106
+ console.log(colors.cyan("[litestar-sveltekit]"), colors.yellow("OpenAPI schema not found:"), typesConfig.openapiPath);
107
+ return false;
108
+ }
109
+ console.log(colors.cyan("[litestar-sveltekit]"), colors.dim("Generating TypeScript types..."));
110
+ const args = ["@hey-api/openapi-ts", "-i", typesConfig.openapiPath, "-o", typesConfig.output];
111
+ if (typesConfig.generateZod) {
112
+ args.push("--plugins", "@hey-api/schemas", "@hey-api/types");
113
+ }
114
+ await execAsync(`npx ${args.join(" ")}`, {
115
+ cwd: process.cwd()
116
+ });
117
+ const duration = Date.now() - startTime;
118
+ console.log(colors.cyan("[litestar-sveltekit]"), colors.green("Types generated"), colors.dim(`in ${duration}ms`));
119
+ if (server) {
120
+ server.ws.send({
121
+ type: "custom",
122
+ event: "litestar:types-updated",
123
+ data: {
124
+ output: typesConfig.output,
125
+ timestamp: Date.now()
126
+ }
127
+ });
128
+ }
129
+ return true;
130
+ } catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ if (message.includes("not found") || message.includes("ENOENT")) {
133
+ console.log(colors.cyan("[litestar-sveltekit]"), colors.yellow("@hey-api/openapi-ts not installed"), "- run:", resolveInstallHint());
134
+ } else {
135
+ console.error(colors.cyan("[litestar-sveltekit]"), colors.red("Type generation failed:"), message);
136
+ }
137
+ return false;
138
+ } finally {
139
+ isGenerating = false;
140
+ }
141
+ }
142
+ const debouncedRunTypeGeneration = debounce(runTypeGeneration, typesConfig.debounce);
143
+ return {
144
+ name: "litestar-sveltekit-types",
145
+ enforce: "pre",
146
+ configureServer(devServer) {
147
+ server = devServer;
148
+ console.log(colors.cyan("[litestar-sveltekit]"), colors.dim("Watching for schema changes:"), colors.yellow(typesConfig.openapiPath));
149
+ },
150
+ handleHotUpdate({ file }) {
151
+ if (!typesConfig.enabled) {
152
+ return;
153
+ }
154
+ const relativePath = path.relative(process.cwd(), file);
155
+ const openapiPath = typesConfig.openapiPath.replace(/^\.\//, "");
156
+ const routesPath = typesConfig.routesPath.replace(/^\.\//, "");
157
+ if (relativePath === openapiPath || relativePath === routesPath || file.endsWith(openapiPath) || file.endsWith(routesPath)) {
158
+ console.log(colors.cyan("[litestar-sveltekit]"), colors.dim("Schema changed:"), colors.yellow(relativePath));
159
+ debouncedRunTypeGeneration();
160
+ }
161
+ }
162
+ };
163
+ }
164
+ var sveltekit_default = litestarSvelteKit;
165
+ export {
166
+ sveltekit_default as default,
167
+ litestarSvelteKit
168
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litestar-vite-plugin",
3
- "version": "0.14.0",
3
+ "version": "0.15.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "Litestar plugin for Vite.",
6
6
  "keywords": ["litestar", "vite", "vite-plugin"],
@@ -16,9 +16,27 @@
16
16
  "types": "./dist/js/index.d.ts",
17
17
  "default": "./dist/js/index.js"
18
18
  },
19
+ "./helpers": {
20
+ "types": "./dist/js/helpers/index.d.ts",
21
+ "default": "./dist/js/helpers/index.js"
22
+ },
23
+ "./install-hint": "./dist/js/install-hint.js",
24
+ "./litestar-meta": "./dist/js/litestar-meta.js",
19
25
  "./inertia-helpers": {
20
26
  "types": "./dist/js/inertia-helpers/index.d.ts",
21
27
  "default": "./dist/js/inertia-helpers/index.js"
28
+ },
29
+ "./astro": {
30
+ "types": "./dist/js/astro.d.ts",
31
+ "default": "./dist/js/astro.js"
32
+ },
33
+ "./sveltekit": {
34
+ "types": "./dist/js/sveltekit.d.ts",
35
+ "default": "./dist/js/sveltekit.js"
36
+ },
37
+ "./nuxt": {
38
+ "types": "./dist/js/nuxt.d.ts",
39
+ "default": "./dist/js/nuxt.js"
22
40
  }
23
41
  },
24
42
  "types": "./dist/js/index.d.ts",
@@ -27,11 +45,13 @@
27
45
  "clean-orphaned-assets": "tools/clean.js"
28
46
  },
29
47
  "scripts": {
30
- "build": "npm run build-plugin && npm run build-inertia-helpers",
48
+ "build": "npm run build-plugin && npm run build-helpers && npm run build-inertia-helpers && npm run build-integrations",
31
49
  "build-plugin": "rm -rf dist/js && npm run build-plugin-types && npm run build-plugin-esm && cp src/js/src/dev-server-index.html dist/js/",
32
50
  "build-plugin-types": "tsc --project src/js/tsconfig.json --emitDeclarationOnly",
33
- "build-plugin-esm": "esbuild src/js/src/index.ts --platform=node --format=esm --outfile=dist/js/index.js",
34
- "build-inertia-helpers": "rm -rf dist/inertia-helpers && tsc --project src/js/tsconfig.inertia-helpers.json",
51
+ "build-plugin-esm": "esbuild src/js/src/index.ts --platform=node --format=esm --outfile=dist/js/index.js && esbuild src/js/src/install-hint.ts --platform=node --format=esm --outfile=dist/js/install-hint.js && esbuild src/js/src/litestar-meta.ts --platform=node --format=esm --outfile=dist/js/litestar-meta.js",
52
+ "build-helpers": "rm -rf dist/js/helpers && tsc --project src/js/tsconfig.helpers.json",
53
+ "build-inertia-helpers": "rm -rf dist/js/inertia-helpers && tsc --project src/js/tsconfig.inertia-helpers.json",
54
+ "build-integrations": "esbuild src/js/src/astro.ts src/js/src/sveltekit.ts src/js/src/nuxt.ts --platform=node --format=esm --outdir=dist/js",
35
55
  "lint": "eslint --ext .ts ./src/js/src ./src/js/tests",
36
56
  "test": "vitest --config ./src/js/vitest.config.ts run"
37
57
  },
@@ -39,7 +59,7 @@
39
59
  "@biomejs/biome": "1.9.4",
40
60
  "@types/node": "^22.15.3",
41
61
  "esbuild": "0.25.3",
42
- "happy-dom": "^17.4.6",
62
+ "happy-dom": "^20.0.2",
43
63
  "typescript": "^5.8.3",
44
64
  "vite": "^7.1.2",
45
65
  "vitest": "^3.1.2"
@@ -1,20 +0,0 @@
1
- export declare function resolvePageComponent<T>(path: string | string[], pages: Record<string, Promise<T> | (() => Promise<T>)>): Promise<T>;
2
- type RouteArg = string | number | boolean;
3
- type RouteArgs = Record<string, RouteArg> | RouteArg[];
4
- export declare function route(routeName: string, ...args: [RouteArgs?]): string;
5
- export declare function getRelativeUrlPath(url: string): string;
6
- export declare function toRoute(url: string): string | null;
7
- export declare function currentRoute(): string | null;
8
- export declare function isRoute(url: string, routeName: string): boolean;
9
- export declare function isCurrentRoute(routeName: string): boolean;
10
- declare global {
11
- var routes: {
12
- [key: string]: string;
13
- };
14
- function route(routeName: string, ...args: [RouteArgs?]): string;
15
- function toRoute(url: string): string | null;
16
- function currentRoute(): string | null;
17
- function isRoute(url: string, routeName: string): boolean;
18
- function isCurrentRoute(routeName: string): boolean;
19
- }
20
- export {};
@@ -1,138 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- export async function resolvePageComponent(path, pages) {
3
- for (const p of Array.isArray(path) ? path : [path]) {
4
- const page = pages[p];
5
- if (typeof page === "undefined") {
6
- continue;
7
- }
8
- return typeof page === "function" ? page() : page;
9
- }
10
- throw new Error(`Page not found: ${path}`);
11
- }
12
- export function route(routeName, ...args) {
13
- let url = globalThis.routes[routeName];
14
- if (!url) {
15
- console.error(`URL '${routeName}' not found in routes.`);
16
- return "#"; // Return "#" to indicate failure
17
- }
18
- const argTokens = url.match(/\{([^}]+):([^}]+)\}/g);
19
- if (!argTokens && args.length > 0) {
20
- console.error(`Invalid URL lookup: URL '${routeName}' does not expect arguments.`);
21
- return "#";
22
- }
23
- try {
24
- if (typeof args[0] === "object" && !Array.isArray(args[0])) {
25
- for (const token of argTokens ?? []) {
26
- let argName = token.slice(1, -1);
27
- if (argName.includes(":")) {
28
- argName = argName.split(":")[0];
29
- }
30
- const argValue = args[0][argName];
31
- if (argValue === undefined) {
32
- throw new Error(`Invalid URL lookup: Argument '${argName}' was not provided.`);
33
- }
34
- url = url.replace(token, String(argValue));
35
- }
36
- }
37
- else {
38
- const argsArray = Array.isArray(args[0]) ? args[0] : Array.from(args);
39
- if (argTokens && argTokens.length !== argsArray.length) {
40
- throw new Error(`Invalid URL lookup: Wrong number of arguments; expected ${argTokens.length.toString()} arguments.`);
41
- }
42
- argTokens?.forEach((token, i) => {
43
- const argValue = argsArray[i];
44
- if (argValue === undefined) {
45
- throw new Error(`Missing argument at position ${i}`);
46
- }
47
- url = url.replace(token, argValue.toString());
48
- });
49
- }
50
- }
51
- catch (error) {
52
- console.error(error instanceof Error ? error.message : String(error));
53
- return "#";
54
- }
55
- const fullUrl = new URL(url, window.location.origin);
56
- return fullUrl.href;
57
- }
58
- export function getRelativeUrlPath(url) {
59
- try {
60
- const urlObject = new URL(url);
61
- return urlObject.pathname + urlObject.search + urlObject.hash;
62
- }
63
- catch (e) {
64
- // If the URL is invalid or already a relative path, just return it as is
65
- return url;
66
- }
67
- }
68
- function routePatternToRegex(pattern) {
69
- return new RegExp(`^${pattern.replace(/\*/g, ".*")}$`);
70
- }
71
- export function toRoute(url) {
72
- const processedUrl = getRelativeUrlPath(url);
73
- const normalizedUrl = processedUrl === "/" ? processedUrl : processedUrl.replace(/\/$/, "");
74
- for (const [routeName, routePattern] of Object.entries(routes)) {
75
- const regexPattern = routePattern.replace(/\//g, "\\/").replace(/\{([^}]+):([^}]+)\}/g, (_, __, paramType) => {
76
- switch (paramType) {
77
- case "uuid":
78
- return "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
79
- case "path":
80
- return ".*"; // Matches any characters including forward slashes
81
- default:
82
- return "[^/]+"; // Matches any characters except forward slashes
83
- }
84
- });
85
- const regex = new RegExp(`^${regexPattern}$`);
86
- if (regex.test(normalizedUrl)) {
87
- return routeName;
88
- }
89
- }
90
- return null;
91
- }
92
- export function currentRoute() {
93
- const currentUrl = window.location.pathname;
94
- return toRoute(currentUrl);
95
- }
96
- export function isRoute(url, routeName) {
97
- const processedUrl = getRelativeUrlPath(url);
98
- const normalizedUrl = processedUrl === "/" ? processedUrl : processedUrl.replace(/\/$/, "");
99
- const routeNameRegex = routePatternToRegex(routeName);
100
- // Find all matching route names based on the pattern
101
- const matchingRouteNames = Object.keys(routes).filter((name) => routeNameRegex.test(name));
102
- // For each matching route name, check if the URL matches its pattern
103
- for (const name of matchingRouteNames) {
104
- const routePattern = routes[name];
105
- const regexPattern = routePattern.replace(/\//g, "\\/").replace(/\{([^}]+):([^}]+)\}/g, (match, paramName, paramType) => {
106
- switch (paramType) {
107
- case "str":
108
- return "([^/]+)";
109
- case "path":
110
- return "(.*)";
111
- case "uuid":
112
- return "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
113
- default:
114
- return "([^/]+)";
115
- }
116
- });
117
- const regex = new RegExp(`^${regexPattern}$`);
118
- if (regex.test(normalizedUrl)) {
119
- return true;
120
- }
121
- }
122
- return false;
123
- }
124
- export function isCurrentRoute(routeName) {
125
- const currentRouteName = currentRoute();
126
- if (!currentRouteName) {
127
- console.error("Could not match current window location to a named route.");
128
- return false;
129
- }
130
- const routeNameRegex = routePatternToRegex(routeName);
131
- return routeNameRegex.test(currentRouteName);
132
- }
133
- globalThis.routes = globalThis.routes || {};
134
- globalThis.route = route;
135
- globalThis.toRoute = toRoute;
136
- globalThis.currentRoute = currentRoute;
137
- globalThis.isRoute = isRoute;
138
- globalThis.isCurrentRoute = isCurrentRoute;