litestar-vite-plugin 0.24.1 → 0.26.0

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.
Files changed (35) hide show
  1. package/README.md +6 -6
  2. package/dist/js/astro.d.ts +6 -0
  3. package/dist/js/astro.js +14 -77
  4. package/dist/js/dev-server-index.html +2 -2
  5. package/dist/js/helpers/csrf.d.ts +2 -0
  6. package/dist/js/helpers/csrf.js +28 -1
  7. package/dist/js/helpers/htmx.js +105 -2
  8. package/dist/js/index.d.ts +6 -0
  9. package/dist/js/index.js +75 -128
  10. package/dist/js/inertia-helpers/index.d.ts +27 -9
  11. package/dist/js/install-hint.d.ts +17 -0
  12. package/dist/js/install-hint.js +35 -1
  13. package/dist/js/nuxt.d.ts +6 -0
  14. package/dist/js/nuxt.js +12 -72
  15. package/dist/js/shared/bridge-schema.d.ts +1 -0
  16. package/dist/js/shared/bridge-schema.js +41 -9
  17. package/dist/js/shared/constants.d.ts +4 -18
  18. package/dist/js/shared/constants.js +2 -6
  19. package/dist/js/shared/emit-page-props-types.d.ts +1 -1
  20. package/dist/js/shared/emit-page-props-types.js +51 -5
  21. package/dist/js/shared/emit-schemas-types.d.ts +1 -1
  22. package/dist/js/shared/emit-schemas-types.js +86 -15
  23. package/dist/js/shared/emit-static-props-types.d.ts +1 -1
  24. package/dist/js/shared/emit-static-props-types.js +2 -2
  25. package/dist/js/shared/typegen-cache.d.ts +10 -11
  26. package/dist/js/shared/typegen-cache.js +10 -1
  27. package/dist/js/shared/typegen-core.d.ts +24 -0
  28. package/dist/js/shared/typegen-core.js +101 -45
  29. package/dist/js/shared/typegen-plugin.d.ts +26 -0
  30. package/dist/js/shared/typegen-plugin.js +191 -129
  31. package/dist/js/shared/vite-compat.d.ts +23 -15
  32. package/dist/js/shared/vite-compat.js +12 -6
  33. package/dist/js/sveltekit.d.ts +6 -0
  34. package/dist/js/sveltekit.js +14 -77
  35. package/package.json +14 -8
@@ -1,19 +1,23 @@
1
- import { exec } from "node:child_process";
1
+ import { execFile } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { resolveInstallHint, resolvePackageExecutor } from "../install-hint.js";
6
+ import { resolveInstallHint, resolvePackageExecutorArgv } from "../install-hint.js";
7
+ import { HEY_API_PINNED_SPEC } from "./constants.js";
7
8
  import { emitPagePropsTypes } from "./emit-page-props-types.js";
8
9
  import { emitSchemasTypes } from "./emit-schemas-types.js";
9
- const execAsync = promisify(exec);
10
+ import { emitStaticPropsTypes } from "./emit-static-props-types.js";
11
+ const execFileAsync = promisify(execFile);
10
12
  const nodeRequire = createRequire(import.meta.url);
11
13
  function resolveDefaultSdkClientPlugin(options) {
12
14
  void options;
13
15
  return "@hey-api/client-fetch";
14
16
  }
15
17
  function findOpenApiTsConfig(projectRoot) {
16
- const candidates = [path.resolve(projectRoot, "openapi-ts.config.ts"), path.resolve(projectRoot, "hey-api.config.ts"), path.resolve(projectRoot, ".hey-api.config.ts")];
18
+ const bases = ["openapi-ts.config", "hey-api.config", ".hey-api.config"];
19
+ const extensions = [".ts", ".mjs", ".cjs", ".js"];
20
+ const candidates = bases.flatMap((base) => extensions.map((extension) => path.resolve(projectRoot, `${base}${extension}`)));
17
21
  return candidates.find((p) => fs.existsSync(p)) || null;
18
22
  }
19
23
  function buildHeyApiPlugins(config) {
@@ -26,22 +30,28 @@ function buildHeyApiPlugins(config) {
26
30
  }
27
31
  return plugins;
28
32
  }
33
+ function resolveHeyApiBin(projectRoot) {
34
+ try {
35
+ const packageJsonPath = nodeRequire.resolve("@hey-api/openapi-ts/package.json", { paths: [projectRoot] });
36
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
37
+ const bin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.["openapi-ts"];
38
+ if (!bin) {
39
+ return null;
40
+ }
41
+ return { binPath: path.resolve(path.dirname(packageJsonPath), bin) };
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
29
46
  async function runHeyApiGeneration(config, configPath, plugins, logger) {
30
47
  const { projectRoot, openapiPath, output, executor, generateZod } = config;
31
48
  const sdkOutput = path.join(output, "api");
32
- if (process.env.VITEST || process.env.VITE_TEST || process.env.NODE_ENV === "test" || process.env.CI) {
33
- try {
34
- nodeRequire.resolve("@hey-api/openapi-ts/package.json", { paths: [projectRoot] });
35
- } catch {
36
- throw new Error("@hey-api/openapi-ts not installed");
37
- }
38
- }
39
49
  let args;
40
50
  if (configPath) {
41
- args = ["@hey-api/openapi-ts", "--file", configPath];
51
+ args = ["--file", configPath];
42
52
  } else {
43
53
  const relativeOpenapiPath = path.relative(projectRoot, path.resolve(projectRoot, openapiPath));
44
- args = ["@hey-api/openapi-ts", "-i", relativeOpenapiPath, "-o", sdkOutput];
54
+ args = ["-i", relativeOpenapiPath, "-o", sdkOutput];
45
55
  if (plugins.length) {
46
56
  args.push("--plugins", ...plugins);
47
57
  }
@@ -53,12 +63,23 @@ async function runHeyApiGeneration(config, configPath, plugins, logger) {
53
63
  logger?.warn(`zod not installed - run: ${resolveInstallHint("zod")}`);
54
64
  }
55
65
  }
56
- const cmd = resolvePackageExecutor(args.join(" "), executor);
57
- await execAsync(cmd, { cwd: projectRoot });
66
+ const local = resolveHeyApiBin(projectRoot);
67
+ if (local) {
68
+ await execFileAsync(process.execPath, [local.binPath, ...args], { cwd: projectRoot });
69
+ return sdkOutput;
70
+ }
71
+ const fallback = resolvePackageExecutorArgv(args, executor, {
72
+ packageSpec: HEY_API_PINNED_SPEC,
73
+ binName: "openapi-ts"
74
+ });
75
+ if (!fallback.length) {
76
+ throw new Error("@hey-api/openapi-ts not installed");
77
+ }
78
+ await execFileAsync(fallback[0], fallback.slice(1), { cwd: projectRoot });
58
79
  return sdkOutput;
59
80
  }
60
81
  async function runTypeGeneration(config, options = {}) {
61
- const { logger } = options;
82
+ const { cache, logger } = options;
62
83
  const startTime = Date.now();
63
84
  const result = {
64
85
  generated: false,
@@ -76,40 +97,60 @@ async function runTypeGeneration(config, options = {}) {
76
97
  const shouldGenerateSdk = configPath || generateSdk;
77
98
  if (fs.existsSync(absoluteOpenapiPath) && shouldGenerateSdk) {
78
99
  const plugins = buildHeyApiPlugins(config);
79
- logger?.info("Generating TypeScript types...");
80
- if (configPath) {
81
- const relConfigPath = path.relative(projectRoot, configPath);
82
- logger?.info(`openapi-ts config: ${relConfigPath}`);
83
- }
84
- try {
85
- const outputPath = await runHeyApiGeneration(config, configPath, plugins, logger);
86
- result.generatedFiles.push(outputPath);
87
- result.generated = true;
88
- } catch (error) {
89
- const message = error instanceof Error ? error.message : String(error);
90
- const isPackageNotInstalled = message.includes("not installed");
91
- const isRuntimeEnoent = !isPackageNotInstalled && (message.includes("ENOENT") || error instanceof Error && "code" in error && error.code === "ENOENT");
92
- if (isPackageNotInstalled) {
93
- const zodHint = config.generateZod ? " zod" : "";
94
- const warning = `@hey-api/openapi-ts not installed - run: ${resolveInstallHint(`@hey-api/openapi-ts${zodHint}`)}`;
95
- result.warnings.push(warning);
96
- logger?.warn(warning);
97
- } else if (isRuntimeEnoent) {
98
- result.errors.push(`File not found during type generation: ${message}`);
99
- logger?.error(`Type generation failed (file not found): ${message}`);
100
- } else {
101
- result.errors.push(message);
102
- logger?.error(`Type generation failed: ${message}`);
100
+ const sdkOutput = path.join(output, "api");
101
+ const cacheOptions = {
102
+ generateSdk: config.generateSdk,
103
+ generateZod: config.generateZod,
104
+ plugins,
105
+ outputPaths: [path.join(projectRoot, sdkOutput, "types.gen.ts")]
106
+ };
107
+ const shouldRun = cache ? await cache.shouldRunOpenApiTs(absoluteOpenapiPath, configPath, cacheOptions) : true;
108
+ if (shouldRun) {
109
+ logger?.info("Generating TypeScript types...");
110
+ if (configPath) {
111
+ const relConfigPath = path.relative(projectRoot, configPath);
112
+ logger?.info(`openapi-ts config: ${relConfigPath}`);
103
113
  }
114
+ try {
115
+ const outputPath = await runHeyApiGeneration(config, configPath, plugins, logger);
116
+ await cache?.updateOpenApiTsCache(absoluteOpenapiPath, configPath, cacheOptions);
117
+ result.generatedFiles.push(outputPath);
118
+ result.generated = true;
119
+ } catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ const isPackageNotInstalled = message.includes("not installed");
122
+ const isRuntimeEnoent = !isPackageNotInstalled && (message.includes("ENOENT") || error instanceof Error && "code" in error && error.code === "ENOENT");
123
+ if (isPackageNotInstalled) {
124
+ const zodHint = config.generateZod ? " zod" : "";
125
+ const errorMessage = `@hey-api/openapi-ts not installed - run: ${resolveInstallHint(`@hey-api/openapi-ts${zodHint}`)}`;
126
+ result.errors.push(errorMessage);
127
+ logger?.error(errorMessage);
128
+ } else if (isRuntimeEnoent) {
129
+ result.errors.push(`File not found during type generation: ${message}`);
130
+ logger?.error(`Type generation failed (file not found): ${message}`);
131
+ } else {
132
+ result.errors.push(message);
133
+ logger?.error(`Type generation failed: ${message}`);
134
+ }
135
+ }
136
+ } else {
137
+ result.skippedFiles.push(sdkOutput);
104
138
  }
105
139
  }
106
140
  if (generatePageProps && fs.existsSync(absolutePagePropsPath)) {
107
141
  try {
108
- const changed = await emitPagePropsTypes(absolutePagePropsPath, output);
109
142
  const pagePropsOutput = path.join(output, "page-props.ts");
110
- if (changed) {
111
- result.generatedFiles.push(pagePropsOutput);
112
- result.generated = true;
143
+ const absolutePagePropsOutput = path.resolve(projectRoot, pagePropsOutput);
144
+ const shouldRun = cache ? await cache.shouldRegeneratePageProps(absolutePagePropsPath, absolutePagePropsOutput) : true;
145
+ if (shouldRun) {
146
+ const changed = await emitPagePropsTypes(absolutePagePropsPath, output, projectRoot);
147
+ await cache?.updatePagePropsCache(absolutePagePropsPath);
148
+ if (changed) {
149
+ result.generatedFiles.push(pagePropsOutput);
150
+ result.generated = true;
151
+ } else {
152
+ result.skippedFiles.push(pagePropsOutput);
153
+ }
113
154
  } else {
114
155
  result.skippedFiles.push(pagePropsOutput);
115
156
  }
@@ -124,7 +165,7 @@ async function runTypeGeneration(config, options = {}) {
124
165
  const absoluteRoutesPath = path.resolve(projectRoot, routesPath);
125
166
  if (fs.existsSync(absoluteRoutesPath)) {
126
167
  try {
127
- const changed = await emitSchemasTypes(absoluteRoutesPath, output, schemasTsPath);
168
+ const changed = await emitSchemasTypes(absoluteRoutesPath, output, schemasTsPath, projectRoot);
128
169
  const schemasOutput = schemasTsPath ?? path.join(output, "schemas.ts");
129
170
  if (changed) {
130
171
  result.generatedFiles.push(schemasOutput);
@@ -139,6 +180,20 @@ async function runTypeGeneration(config, options = {}) {
139
180
  }
140
181
  }
141
182
  }
183
+ try {
184
+ const changed = await emitStaticPropsTypes(output, projectRoot);
185
+ const staticPropsOutput = path.join(output, "static-props.ts");
186
+ if (changed) {
187
+ result.generatedFiles.push(staticPropsOutput);
188
+ result.generated = true;
189
+ } else {
190
+ result.skippedFiles.push(staticPropsOutput);
191
+ }
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ result.errors.push(`Static props generation failed: ${message}`);
195
+ logger?.error(`Static props generation failed: ${message}`);
196
+ }
142
197
  } finally {
143
198
  result.durationMs = Date.now() - startTime;
144
199
  }
@@ -148,6 +203,7 @@ export {
148
203
  buildHeyApiPlugins,
149
204
  findOpenApiTsConfig,
150
205
  resolveDefaultSdkClientPlugin,
206
+ resolveHeyApiBin,
151
207
  runHeyApiGeneration,
152
208
  runTypeGeneration
153
209
  };
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from "vite";
2
+ import type { BridgeTypesConfig } from "./bridge-schema.js";
2
3
  export interface RequiredTypeGenConfig {
3
4
  enabled: boolean;
4
5
  output: string;
@@ -12,6 +13,7 @@ export interface RequiredTypeGenConfig {
12
13
  generatePageProps: boolean;
13
14
  generateSchemas: boolean;
14
15
  globalRoute: boolean;
16
+ failOnError?: boolean;
15
17
  debounce: number;
16
18
  }
17
19
  export interface TypeGenPluginOptions {
@@ -26,6 +28,30 @@ export interface TypeGenPluginOptions {
26
28
  /** Whether .litestar.json was present (used for buildStart warnings) */
27
29
  hasPythonConfig?: boolean;
28
30
  }
31
+ export interface TypesConfigShape {
32
+ enabled?: boolean;
33
+ output?: string;
34
+ openapiPath?: string;
35
+ routesPath?: string;
36
+ pagePropsPath?: string;
37
+ schemasTsPath?: string;
38
+ generateZod?: boolean;
39
+ generateSdk?: boolean;
40
+ generateRoutes?: boolean;
41
+ generatePageProps?: boolean;
42
+ generateSchemas?: boolean;
43
+ globalRoute?: boolean;
44
+ failOnError?: boolean;
45
+ debounce?: number;
46
+ }
47
+ export interface ResolveTypesConfigOptions {
48
+ requested: boolean | "auto" | TypesConfigShape | undefined;
49
+ pythonConfig?: BridgeTypesConfig;
50
+ defaultOutput: string;
51
+ mergePythonWhenTrue?: boolean;
52
+ mergePythonForObject?: boolean;
53
+ }
54
+ export declare function resolveTypesConfig(options: ResolveTypesConfigOptions): RequiredTypeGenConfig | false;
29
55
  /**
30
56
  * Unified Litestar type generation Vite plugin.
31
57
  *
@@ -1,13 +1,97 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import colors from "picocolors";
4
+ import { DEBOUNCE_MS } from "./constants.js";
4
5
  import { debounce } from "./debounce.js";
5
- import { emitPagePropsTypes } from "./emit-page-props-types.js";
6
- import { emitSchemasTypes } from "./emit-schemas-types.js";
7
- import { emitStaticPropsTypes } from "./emit-static-props-types.js";
8
- import { formatPath } from "./format-path.js";
9
6
  import { shouldRegeneratePageProps, shouldRunOpenApiTs, updateOpenApiTsCache, updatePagePropsCache } from "./typegen-cache.js";
10
- import { buildHeyApiPlugins, findOpenApiTsConfig, runHeyApiGeneration } from "./typegen-core.js";
7
+ import { runTypeGeneration } from "./typegen-core.js";
8
+ function buildTypeDefaults(output) {
9
+ return {
10
+ openapiPath: path.join(output, "openapi.json"),
11
+ routesPath: path.join(output, "routes.json"),
12
+ pagePropsPath: path.join(output, "inertia-pages.json"),
13
+ schemasTsPath: path.join(output, "schemas.ts")
14
+ };
15
+ }
16
+ function resolveFromPython(pythonConfig, defaultOutput) {
17
+ const output = pythonConfig.output ?? defaultOutput;
18
+ const defaults = buildTypeDefaults(output);
19
+ return {
20
+ enabled: true,
21
+ output,
22
+ openapiPath: pythonConfig.openapiPath ?? defaults.openapiPath,
23
+ routesPath: pythonConfig.routesPath ?? defaults.routesPath,
24
+ pagePropsPath: pythonConfig.pagePropsPath ?? defaults.pagePropsPath,
25
+ schemasTsPath: pythonConfig.schemasTsPath ?? defaults.schemasTsPath,
26
+ generateZod: pythonConfig.generateZod ?? false,
27
+ generateSdk: pythonConfig.generateSdk ?? true,
28
+ generateRoutes: pythonConfig.generateRoutes ?? true,
29
+ generatePageProps: pythonConfig.generatePageProps ?? true,
30
+ generateSchemas: pythonConfig.generateSchemas ?? true,
31
+ globalRoute: pythonConfig.globalRoute ?? false,
32
+ failOnError: pythonConfig.failOnError,
33
+ debounce: DEBOUNCE_MS
34
+ };
35
+ }
36
+ function resolveDefaultTypesConfig(defaultOutput) {
37
+ const defaults = buildTypeDefaults(defaultOutput);
38
+ return {
39
+ enabled: true,
40
+ output: defaultOutput,
41
+ openapiPath: defaults.openapiPath,
42
+ routesPath: defaults.routesPath,
43
+ pagePropsPath: defaults.pagePropsPath,
44
+ schemasTsPath: defaults.schemasTsPath,
45
+ generateZod: false,
46
+ generateSdk: true,
47
+ generateRoutes: true,
48
+ generatePageProps: true,
49
+ generateSchemas: true,
50
+ globalRoute: false,
51
+ failOnError: void 0,
52
+ debounce: DEBOUNCE_MS
53
+ };
54
+ }
55
+ function resolveTypesConfig(options) {
56
+ const { requested, pythonConfig, defaultOutput, mergePythonWhenTrue = false, mergePythonForObject = false } = options;
57
+ if (requested === false) {
58
+ return false;
59
+ }
60
+ if (requested === true) {
61
+ if (mergePythonWhenTrue && pythonConfig) {
62
+ return resolveFromPython(pythonConfig, defaultOutput);
63
+ }
64
+ return resolveDefaultTypesConfig(defaultOutput);
65
+ }
66
+ if (requested === "auto" || typeof requested === "undefined") {
67
+ return pythonConfig?.enabled ? resolveFromPython(pythonConfig, defaultOutput) : false;
68
+ }
69
+ const userProvidedOutput = Object.hasOwn(requested, "output");
70
+ const output = requested.output ?? (mergePythonForObject ? pythonConfig?.output : void 0) ?? defaultOutput;
71
+ const defaults = buildTypeDefaults(output);
72
+ const pathFallback = (key) => {
73
+ if (!mergePythonForObject || userProvidedOutput) {
74
+ return defaults[key];
75
+ }
76
+ return pythonConfig?.[key] ?? defaults[key];
77
+ };
78
+ return {
79
+ enabled: requested.enabled ?? true,
80
+ output,
81
+ openapiPath: requested.openapiPath ?? pathFallback("openapiPath"),
82
+ routesPath: requested.routesPath ?? pathFallback("routesPath"),
83
+ pagePropsPath: requested.pagePropsPath ?? pathFallback("pagePropsPath"),
84
+ schemasTsPath: requested.schemasTsPath ?? pathFallback("schemasTsPath"),
85
+ generateZod: requested.generateZod ?? (mergePythonForObject ? pythonConfig?.generateZod : void 0) ?? false,
86
+ generateSdk: requested.generateSdk ?? (mergePythonForObject ? pythonConfig?.generateSdk : void 0) ?? true,
87
+ generateRoutes: requested.generateRoutes ?? (mergePythonForObject ? pythonConfig?.generateRoutes : void 0) ?? true,
88
+ generatePageProps: requested.generatePageProps ?? (mergePythonForObject ? pythonConfig?.generatePageProps : void 0) ?? true,
89
+ generateSchemas: requested.generateSchemas ?? (mergePythonForObject ? pythonConfig?.generateSchemas : void 0) ?? true,
90
+ globalRoute: requested.globalRoute ?? (mergePythonForObject ? pythonConfig?.globalRoute : void 0) ?? false,
91
+ failOnError: requested.failOnError ?? pythonConfig?.failOnError,
92
+ debounce: requested.debounce ?? DEBOUNCE_MS
93
+ };
94
+ }
11
95
  async function getFileMtime(filePath) {
12
96
  const stat = await fs.promises.stat(filePath);
13
97
  return stat.mtimeMs.toString();
@@ -16,8 +100,10 @@ function createLitestarTypeGenPlugin(typesConfig, options) {
16
100
  const { pluginName, frameworkName, sdkClientPlugin, executor, hasPythonConfig } = options;
17
101
  let lastTypesHash = null;
18
102
  let lastPagePropsHash = null;
103
+ let lastRoutesHash = null;
19
104
  let server = null;
20
- let isGenerating = false;
105
+ let generationPromise = null;
106
+ let rerunRequested = false;
21
107
  let resolvedConfig = null;
22
108
  function createViteLogger() {
23
109
  return {
@@ -27,37 +113,31 @@ function createLitestarTypeGenPlugin(typesConfig, options) {
27
113
  };
28
114
  }
29
115
  async function runTypeGenerationWithCache() {
30
- if (isGenerating) {
31
- return false;
116
+ if (generationPromise) {
117
+ rerunRequested = true;
118
+ return generationPromise;
32
119
  }
33
- isGenerating = true;
34
- const startTime = Date.now();
35
- try {
120
+ generationPromise = (async () => {
121
+ const combined = {
122
+ generated: false,
123
+ generatedFiles: [],
124
+ skippedFiles: [],
125
+ durationMs: 0,
126
+ warnings: [],
127
+ errors: []
128
+ };
36
129
  const projectRoot = resolvedConfig?.root ?? process.cwd();
37
- const absoluteOpenapiPath = path.resolve(projectRoot, typesConfig.openapiPath);
38
- const absolutePagePropsPath = path.resolve(projectRoot, typesConfig.pagePropsPath);
39
- let generated = false;
40
130
  const logger = createViteLogger();
41
- const configPath = findOpenApiTsConfig(projectRoot);
42
- const shouldGenerateSdk = configPath || typesConfig.generateSdk;
43
- if (fs.existsSync(absoluteOpenapiPath) && shouldGenerateSdk) {
44
- const plugins = buildHeyApiPlugins({
45
- generateSdk: typesConfig.generateSdk,
46
- generateZod: typesConfig.generateZod,
47
- sdkClientPlugin
48
- });
49
- const cacheOptions = {
50
- generateSdk: typesConfig.generateSdk,
51
- generateZod: typesConfig.generateZod,
52
- plugins
53
- };
54
- const shouldRun = await shouldRunOpenApiTs(absoluteOpenapiPath, configPath, cacheOptions);
55
- if (shouldRun) {
56
- logger.info("Generating TypeScript types...");
57
- if (configPath && resolvedConfig) {
58
- const relConfigPath = formatPath(configPath, resolvedConfig.root);
59
- logger.info(`openapi-ts config: ${colors.yellow(relConfigPath)}`);
60
- }
131
+ const cache = {
132
+ shouldRunOpenApiTs,
133
+ updateOpenApiTsCache,
134
+ shouldRegeneratePageProps,
135
+ updatePagePropsCache
136
+ };
137
+ try {
138
+ do {
139
+ rerunRequested = false;
140
+ const startTime = Date.now();
61
141
  const coreConfig = {
62
142
  projectRoot,
63
143
  openapiPath: typesConfig.openapiPath,
@@ -66,96 +146,76 @@ function createLitestarTypeGenPlugin(typesConfig, options) {
66
146
  pagePropsPath: typesConfig.pagePropsPath,
67
147
  generateSdk: typesConfig.generateSdk,
68
148
  generateZod: typesConfig.generateZod,
69
- generatePageProps: false,
70
- // Handle separately below
149
+ generatePageProps: typesConfig.generatePageProps,
71
150
  generateSchemas: typesConfig.generateSchemas,
151
+ schemasTsPath: typesConfig.schemasTsPath,
72
152
  sdkClientPlugin,
73
153
  executor
74
154
  };
75
- try {
76
- await runHeyApiGeneration(coreConfig, configPath, plugins, logger);
77
- await updateOpenApiTsCache(absoluteOpenapiPath, configPath, cacheOptions);
78
- generated = true;
79
- } catch (error) {
80
- const message = error instanceof Error ? error.message : String(error);
81
- if (message.includes("not found") || message.includes("ENOENT")) {
82
- logger.warn("@hey-api/openapi-ts not installed");
83
- } else {
84
- logger.error(`Type generation failed: ${message}`);
85
- }
155
+ const result = await runTypeGeneration(coreConfig, { logger, cache });
156
+ combined.generated ||= result.generated;
157
+ combined.generatedFiles.push(...result.generatedFiles);
158
+ combined.skippedFiles.push(...result.skippedFiles);
159
+ combined.warnings.push(...result.warnings);
160
+ combined.errors.push(...result.errors);
161
+ combined.durationMs += result.durationMs || Date.now() - startTime;
162
+ for (const file of result.skippedFiles) {
163
+ const label = file.endsWith("page-props.ts") ? "Page props types" : file.endsWith("schemas.ts") ? "Schema types" : file.includes("api") ? "TypeScript types" : file;
164
+ resolvedConfig?.logger.info(`${colors.cyan("\u2022")} ${label} ${colors.dim("(unchanged)")}`);
86
165
  }
87
- } else {
88
- resolvedConfig?.logger.info(`${colors.cyan("\u2022")} TypeScript types ${colors.dim("(unchanged)")}`);
166
+ } while (rerunRequested);
167
+ if (combined.generated && resolvedConfig) {
168
+ resolvedConfig.logger.info(`${colors.green("\u2713")} TypeScript artifacts updated ${colors.dim(`(${combined.durationMs}ms)`)}`);
89
169
  }
90
- }
91
- if (typesConfig.generatePageProps && fs.existsSync(absolutePagePropsPath)) {
92
- try {
93
- const shouldRun = await shouldRegeneratePageProps(absolutePagePropsPath);
94
- if (shouldRun) {
95
- const changed = await emitPagePropsTypes(absolutePagePropsPath, typesConfig.output);
96
- await updatePagePropsCache(absolutePagePropsPath);
97
- if (changed) {
98
- generated = true;
99
- } else {
100
- resolvedConfig?.logger.info(`${colors.cyan("\u2022")} Page props types ${colors.dim("(unchanged)")}`);
170
+ if (combined.generated && server) {
171
+ server.ws.send({
172
+ type: "custom",
173
+ event: "litestar:types-updated",
174
+ data: {
175
+ output: typesConfig.output,
176
+ timestamp: Date.now()
101
177
  }
102
- } else {
103
- resolvedConfig?.logger.info(`${colors.cyan("\u2022")} Page props types ${colors.dim("(unchanged)")}`);
104
- }
105
- } catch (error) {
106
- const message = error instanceof Error ? error.message : String(error);
107
- logger.error(`Page props generation failed: ${message}`);
108
- }
109
- }
110
- const absoluteRoutesPath = path.resolve(projectRoot, typesConfig.routesPath);
111
- if (typesConfig.generateSchemas && fs.existsSync(absoluteRoutesPath)) {
112
- try {
113
- const changed = await emitSchemasTypes(absoluteRoutesPath, typesConfig.output, typesConfig.schemasTsPath);
114
- if (changed) {
115
- generated = true;
116
- } else {
117
- resolvedConfig?.logger.info(`${colors.cyan("\u2022")} Schema types ${colors.dim("(unchanged)")}`);
118
- }
119
- } catch (error) {
120
- const message = error instanceof Error ? error.message : String(error);
121
- logger.error(`Schema types generation failed: ${message}`);
122
- }
123
- }
124
- try {
125
- const changed = await emitStaticPropsTypes(typesConfig.output);
126
- if (changed) {
127
- generated = true;
128
- } else {
129
- resolvedConfig?.logger.info(`${colors.cyan("\u2022")} Static props types ${colors.dim("(unchanged)")}`);
178
+ });
130
179
  }
180
+ return combined;
131
181
  } catch (error) {
132
182
  const message = error instanceof Error ? error.message : String(error);
133
- logger.error(`Static props generation failed: ${message}`);
134
- }
135
- if (generated && resolvedConfig) {
136
- const duration = Date.now() - startTime;
137
- resolvedConfig.logger.info(`${colors.green("\u2713")} TypeScript artifacts updated ${colors.dim(`(${duration}ms)`)}`);
183
+ resolvedConfig?.logger.error(`${colors.cyan("litestar-vite")} ${colors.red("type generation failed:")} ${message}`);
184
+ combined.errors.push(message);
185
+ return combined;
186
+ } finally {
187
+ generationPromise = null;
138
188
  }
139
- if (generated && server) {
140
- server.ws.send({
141
- type: "custom",
142
- event: "litestar:types-updated",
143
- data: {
144
- output: typesConfig.output,
145
- timestamp: Date.now()
146
- }
147
- });
148
- }
149
- return true;
150
- } catch (error) {
151
- const message = error instanceof Error ? error.message : String(error);
152
- resolvedConfig?.logger.error(`${colors.cyan("litestar-vite")} ${colors.red("type generation failed:")} ${message}`);
153
- return false;
154
- } finally {
155
- isGenerating = false;
189
+ })();
190
+ return generationPromise;
191
+ }
192
+ const debouncedRunTypeGeneration = debounce(async (file, cacheKey) => {
193
+ const newHash = await getFileMtime(file);
194
+ if (cacheKey === "openapi" && lastTypesHash === newHash) return;
195
+ if (cacheKey === "pageProps" && lastPagePropsHash === newHash) return;
196
+ if (cacheKey === "routes" && lastRoutesHash === newHash) return;
197
+ const result = await runTypeGenerationWithCache();
198
+ if (result.errors.length === 0) {
199
+ if (cacheKey === "openapi") lastTypesHash = newHash;
200
+ if (cacheKey === "pageProps") lastPagePropsHash = newHash;
201
+ if (cacheKey === "routes") lastRoutesHash = newHash;
202
+ }
203
+ }, typesConfig.debounce);
204
+ function shouldFailOnTypegenError() {
205
+ return typesConfig.failOnError ?? resolvedConfig?.command === "build";
206
+ }
207
+ function reportTypegenErrors(result, context) {
208
+ if (!result.errors.length) {
209
+ return;
210
+ }
211
+ const message = `Litestar type generation failed:
212
+ ${result.errors.join("\n")}`;
213
+ if (shouldFailOnTypegenError()) {
214
+ context.error(message);
215
+ } else {
216
+ context.warn(message);
156
217
  }
157
218
  }
158
- const debouncedRunTypeGeneration = debounce(runTypeGenerationWithCache, typesConfig.debounce);
159
219
  return {
160
220
  name: pluginName,
161
221
  enforce: "pre",
@@ -170,6 +230,9 @@ function createLitestarTypeGenPlugin(typesConfig, options) {
170
230
  }
171
231
  },
172
232
  async buildStart() {
233
+ if (resolvedConfig?.command === "build" && process.env.LITESTAR_VITE_SKIP_BUILD_TYPEGEN === "1") {
234
+ return;
235
+ }
173
236
  if (typesConfig.enabled && hasPythonConfig === false) {
174
237
  const projectRoot = resolvedConfig?.root ?? process.cwd();
175
238
  const openapiPath = path.resolve(projectRoot, typesConfig.openapiPath);
@@ -190,10 +253,13 @@ Solutions:
190
253
  const projectRoot = resolvedConfig?.root ?? process.cwd();
191
254
  const openapiPath = path.resolve(projectRoot, typesConfig.openapiPath);
192
255
  const pagePropsPath = path.resolve(projectRoot, typesConfig.pagePropsPath);
256
+ const routesPath = path.resolve(projectRoot, typesConfig.routesPath);
193
257
  const hasOpenapi = fs.existsSync(openapiPath);
194
258
  const hasPageProps = typesConfig.generatePageProps && fs.existsSync(pagePropsPath);
195
- if (hasOpenapi || hasPageProps) {
196
- await runTypeGenerationWithCache();
259
+ const hasRoutes = typesConfig.generateSchemas && fs.existsSync(routesPath);
260
+ if (hasOpenapi || hasPageProps || hasRoutes) {
261
+ const result = await runTypeGenerationWithCache();
262
+ reportTypegenErrors(result, this);
197
263
  }
198
264
  }
199
265
  },
@@ -202,26 +268,22 @@ Solutions:
202
268
  return;
203
269
  }
204
270
  const root = resolvedConfig?.root ?? process.cwd();
205
- const relativePath = path.relative(root, file);
206
- const openapiPath = typesConfig.openapiPath.replace(/^\.\//, "");
207
- const pagePropsPath = typesConfig.pagePropsPath.replace(/^\.\//, "");
208
- const isOpenapi = relativePath === openapiPath || file.endsWith(openapiPath);
209
- const isPageProps = typesConfig.generatePageProps && (relativePath === pagePropsPath || file.endsWith(pagePropsPath));
210
- if (isOpenapi || isPageProps) {
271
+ const absoluteFile = path.resolve(file);
272
+ const relativePath = path.relative(root, absoluteFile);
273
+ const openapiPath = path.resolve(root, typesConfig.openapiPath);
274
+ const pagePropsPath = path.resolve(root, typesConfig.pagePropsPath);
275
+ const routesPath = path.resolve(root, typesConfig.routesPath);
276
+ const isOpenapi = absoluteFile === openapiPath;
277
+ const isPageProps = typesConfig.generatePageProps && absoluteFile === pagePropsPath;
278
+ const isRoutes = typesConfig.generateSchemas && absoluteFile === routesPath;
279
+ if (isOpenapi || isPageProps || isRoutes) {
211
280
  resolvedConfig?.logger.info(`${colors.cyan(frameworkName)} ${colors.dim("schema changed:")} ${colors.yellow(relativePath)}`);
212
- const newHash = await getFileMtime(file);
213
- if (isOpenapi) {
214
- if (lastTypesHash === newHash) return;
215
- lastTypesHash = newHash;
216
- } else if (isPageProps) {
217
- if (lastPagePropsHash === newHash) return;
218
- lastPagePropsHash = newHash;
219
- }
220
- debouncedRunTypeGeneration();
281
+ debouncedRunTypeGeneration(absoluteFile, isOpenapi ? "openapi" : isPageProps ? "pageProps" : "routes");
221
282
  }
222
283
  }
223
284
  };
224
285
  }
225
286
  export {
226
- createLitestarTypeGenPlugin
287
+ createLitestarTypeGenPlugin,
288
+ resolveTypesConfig
227
289
  };