litestar-vite-plugin 0.25.0 → 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.
@@ -64,12 +64,16 @@ export declare function resolvePageComponent<T>(path: string | string[], pages:
64
64
  * Offset-based pagination props.
65
65
  *
66
66
  * Returned when a route returns Litestar's `OffsetPagination` type.
67
- * Contains items plus metadata for offset/limit pagination.
67
+ * Litestar-Vite flattens pagination metadata as top-level sibling props:
68
+ * the route prop contains the item array, and `total`, `limit`, and `offset`
69
+ * are emitted beside it.
68
70
  *
69
71
  * @example
70
72
  * ```ts
71
73
  * interface User { id: string; name: string }
72
- * const { items, total, limit, offset } = props.users as OffsetPaginationProps<User>
74
+ * type UsersPageProps = { users: User[] } & Omit<OffsetPaginationProps<User>, 'items'>
75
+ *
76
+ * const { users, total, limit, offset } = props as UsersPageProps
73
77
  * ```
74
78
  */
75
79
  export interface OffsetPaginationProps<T> {
@@ -86,12 +90,16 @@ export interface OffsetPaginationProps<T> {
86
90
  * Classic page-based pagination props.
87
91
  *
88
92
  * Returned when a route returns Litestar's `ClassicPagination` type.
89
- * Contains items plus metadata for page number pagination.
93
+ * Litestar-Vite flattens pagination metadata as top-level sibling props:
94
+ * the route prop contains the item array, and `currentPage`, `totalPages`,
95
+ * and `pageSize` are emitted beside it.
90
96
  *
91
97
  * @example
92
98
  * ```ts
93
99
  * interface Post { id: string; title: string }
94
- * const { items, currentPage, totalPages, pageSize } = props.posts as ClassicPaginationProps<Post>
100
+ * type PostsPageProps = { posts: Post[] } & Omit<ClassicPaginationProps<Post>, 'items'>
101
+ *
102
+ * const { posts, currentPage, totalPages, pageSize } = props as PostsPageProps
95
103
  * ```
96
104
  */
97
105
  export interface ClassicPaginationProps<T> {
@@ -108,12 +116,16 @@ export interface ClassicPaginationProps<T> {
108
116
  * Cursor-based pagination props.
109
117
  *
110
118
  * Used for cursor/keyset pagination, commonly with infinite scroll.
111
- * Contains items plus cursor tokens for navigation.
119
+ * Litestar-Vite flattens pagination metadata as top-level sibling props:
120
+ * the route prop contains the item array, and cursor metadata is emitted
121
+ * beside it.
112
122
  *
113
123
  * @example
114
124
  * ```ts
115
125
  * interface Message { id: string; content: string }
116
- * const { items, hasMore, nextCursor } = props.messages as CursorPaginationProps<Message>
126
+ * type MessagesPageProps = { messages: Message[] } & Omit<CursorPaginationProps<Message>, 'items'>
127
+ *
128
+ * const { messages, hasMore, nextCursor } = props as MessagesPageProps
117
129
  * if (hasMore && nextCursor) {
118
130
  * // Fetch more with cursor
119
131
  * }
@@ -139,6 +151,11 @@ export interface CursorPaginationProps<T> {
139
151
  * Union type for any pagination props.
140
152
  *
141
153
  * Use when you need to handle multiple pagination styles.
154
+ * Because pagination metadata is flattened as sibling keys, two paginators in
155
+ * one response can collide on keys such as `total`, `limit`, `offset`,
156
+ * `currentPage`, or `pageSize`. Return one flattened paginator per response,
157
+ * or wrap additional paginators in explicit prop containers with distinct
158
+ * metadata.
142
159
  *
143
160
  * @example
144
161
  * ```ts
@@ -166,11 +183,12 @@ export type PaginationProps<T> = OffsetPaginationProps<T> | ClassicPaginationPro
166
183
  * import { usePage } from '@inertiajs/vue3'
167
184
  *
168
185
  * const page = usePage()
169
- * const scrollProps = page.props.scrollProps as ScrollProps
186
+ * const scrollProps = page.props.scrollProps as Record<string, ScrollProps> | undefined
187
+ * const postsScroll = scrollProps?.posts
170
188
  *
171
189
  * function loadMore() {
172
- * if (scrollProps.nextPage) {
173
- * router.get(url, { [scrollProps.pageName]: scrollProps.nextPage })
190
+ * if (postsScroll?.nextPage) {
191
+ * router.get(url, { [postsScroll.pageName]: postsScroll.nextPage })
174
192
  * }
175
193
  * }
176
194
  * ```
@@ -13,3 +13,20 @@ export declare function resolveInstallHint(pkg?: string): string;
13
13
  * @returns The full command string (e.g., "npx @hey-api/openapi-ts ..." or "bunx @hey-api/openapi-ts ...")
14
14
  */
15
15
  export declare function resolvePackageExecutor(pkg: string, executor?: string): string;
16
+ /**
17
+ * Resolves the package executor command as argv.
18
+ *
19
+ * This is used for actual process execution so package arguments are never
20
+ * shell-joined. The string-returning ``resolvePackageExecutor`` remains the
21
+ * display/back-compat helper.
22
+ */
23
+ export interface PackageExecutorArgvOptions {
24
+ /**
25
+ * Package spec to install for executors that support an explicit package
26
+ * separate from the command binary, e.g. npm exec --package.
27
+ */
28
+ packageSpec?: string;
29
+ /** Binary command exposed by packageSpec. */
30
+ binName?: string;
31
+ }
32
+ export declare function resolvePackageExecutorArgv(args: string[], executor?: string, options?: PackageExecutorArgvOptions): string[];
@@ -63,8 +63,42 @@ function resolvePackageExecutor(pkg, executor) {
63
63
  return `npx ${pkg}`;
64
64
  }
65
65
  }
66
+ function getPackageNameFromSpec(packageSpec) {
67
+ if (packageSpec.startsWith("@")) {
68
+ const versionIndex2 = packageSpec.indexOf("@", packageSpec.indexOf("/") + 1);
69
+ return versionIndex2 === -1 ? packageSpec : packageSpec.slice(0, versionIndex2);
70
+ }
71
+ const versionIndex = packageSpec.indexOf("@");
72
+ return versionIndex === -1 ? packageSpec : packageSpec.slice(0, versionIndex);
73
+ }
74
+ function resolveDenoPackageSpec(packageSpec, binName) {
75
+ if (!binName) return packageSpec;
76
+ const packageName = getPackageNameFromSpec(packageSpec);
77
+ const defaultBinName = packageName.split("/").pop();
78
+ return defaultBinName === binName ? packageSpec : `${packageSpec}/${binName}`;
79
+ }
80
+ function resolvePackageExecutorArgv(args, executor, options = {}) {
81
+ const runtime = executor || detectExecutor();
82
+ const { packageSpec, binName } = options;
83
+ switch (runtime) {
84
+ case "bun":
85
+ return ["bunx", ...packageSpec ? [packageSpec, ...args] : args];
86
+ case "deno":
87
+ return ["deno", "run", "-A", ...packageSpec ? [`npm:${resolveDenoPackageSpec(packageSpec, binName)}`, ...args] : args];
88
+ case "pnpm":
89
+ return ["pnpm", "dlx", ...packageSpec ? [packageSpec, ...args] : args];
90
+ case "yarn":
91
+ return ["yarn", "dlx", ...packageSpec ? [packageSpec, ...args] : args];
92
+ default:
93
+ if (packageSpec && binName) {
94
+ return ["npm", "exec", "--yes", "--package", packageSpec, "--", binName, ...args];
95
+ }
96
+ return ["npx", ...args];
97
+ }
98
+ }
66
99
  export {
67
100
  detectExecutor,
68
101
  resolveInstallHint,
69
- resolvePackageExecutor
102
+ resolvePackageExecutor,
103
+ resolvePackageExecutorArgv
70
104
  };
package/dist/js/nuxt.d.ts CHANGED
@@ -100,6 +100,12 @@ export interface NuxtTypesConfig {
100
100
  * @default false
101
101
  */
102
102
  globalRoute?: boolean;
103
+ /**
104
+ * Fail Vite when type generation fails.
105
+ *
106
+ * Defaults to true during build and false during dev.
107
+ */
108
+ failOnError?: boolean;
103
109
  /**
104
110
  * Debounce time in milliseconds for type regeneration.
105
111
  *
package/dist/js/nuxt.js CHANGED
@@ -2,9 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import colors from "picocolors";
4
4
  import { readBridgeConfig } from "./shared/bridge-schema.js";
5
- import { DEBOUNCE_MS } from "./shared/constants.js";
6
5
  import { normalizeHost, resolveHotFilePath, resolveLitestarPort } from "./shared/network.js";
7
- import { createLitestarTypeGenPlugin } from "./shared/typegen-plugin.js";
6
+ import { createLitestarTypeGenPlugin, resolveTypesConfig } from "./shared/typegen-plugin.js";
8
7
  import { hmrServerConfig } from "./shared/vite-compat.js";
9
8
  function resolveConfig(config = {}) {
10
9
  let hotFile;
@@ -39,74 +38,13 @@ function resolveConfig(config = {}) {
39
38
  if (resolvedLitestarPort !== null) {
40
39
  litestarPort = resolvedLitestarPort;
41
40
  }
42
- let typesConfig = false;
43
- const defaultTypesOutput = "generated";
44
- const buildTypeDefaults = (output) => ({
45
- openapiPath: path.join(output, "openapi.json"),
46
- routesPath: path.join(output, "routes.json"),
47
- pagePropsPath: path.join(output, "inertia-pages.json"),
48
- schemasTsPath: path.join(output, "schemas.ts")
41
+ const typesConfig = resolveTypesConfig({
42
+ requested: config.types,
43
+ pythonConfig: pythonTypesConfig ?? void 0,
44
+ defaultOutput: "generated",
45
+ mergePythonWhenTrue: true,
46
+ mergePythonForObject: true
49
47
  });
50
- if (config.types === true) {
51
- const output = pythonTypesConfig?.output ?? defaultTypesOutput;
52
- const defaults = buildTypeDefaults(output);
53
- typesConfig = {
54
- enabled: true,
55
- output,
56
- openapiPath: pythonTypesConfig?.openapiPath ?? defaults.openapiPath,
57
- routesPath: pythonTypesConfig?.routesPath ?? defaults.routesPath,
58
- pagePropsPath: pythonTypesConfig?.pagePropsPath ?? defaults.pagePropsPath,
59
- schemasTsPath: pythonTypesConfig?.schemasTsPath ?? defaults.schemasTsPath,
60
- generateZod: pythonTypesConfig?.generateZod ?? false,
61
- generateSdk: pythonTypesConfig?.generateSdk ?? true,
62
- generateRoutes: pythonTypesConfig?.generateRoutes ?? true,
63
- generatePageProps: pythonTypesConfig?.generatePageProps ?? true,
64
- generateSchemas: pythonTypesConfig?.generateSchemas ?? true,
65
- globalRoute: pythonTypesConfig?.globalRoute ?? false,
66
- debounce: DEBOUNCE_MS
67
- };
68
- } else if (typeof config.types === "object" && config.types !== null) {
69
- const userProvidedOutput = Object.hasOwn(config.types, "output");
70
- const output = config.types.output ?? pythonTypesConfig?.output ?? defaultTypesOutput;
71
- const defaults = buildTypeDefaults(output);
72
- const openapiFallback = userProvidedOutput ? defaults.openapiPath : pythonTypesConfig?.openapiPath ?? defaults.openapiPath;
73
- const routesFallback = userProvidedOutput ? defaults.routesPath : pythonTypesConfig?.routesPath ?? defaults.routesPath;
74
- const pagePropsFallback = userProvidedOutput ? defaults.pagePropsPath : pythonTypesConfig?.pagePropsPath ?? defaults.pagePropsPath;
75
- const schemasFallback = userProvidedOutput ? defaults.schemasTsPath : pythonTypesConfig?.schemasTsPath ?? defaults.schemasTsPath;
76
- typesConfig = {
77
- enabled: config.types.enabled ?? true,
78
- output,
79
- openapiPath: config.types.openapiPath ?? openapiFallback,
80
- routesPath: config.types.routesPath ?? routesFallback,
81
- pagePropsPath: config.types.pagePropsPath ?? pagePropsFallback,
82
- schemasTsPath: config.types.schemasTsPath ?? schemasFallback,
83
- generateZod: config.types.generateZod ?? pythonTypesConfig?.generateZod ?? false,
84
- generateSdk: config.types.generateSdk ?? pythonTypesConfig?.generateSdk ?? true,
85
- generateRoutes: config.types.generateRoutes ?? pythonTypesConfig?.generateRoutes ?? true,
86
- generatePageProps: config.types.generatePageProps ?? pythonTypesConfig?.generatePageProps ?? true,
87
- generateSchemas: config.types.generateSchemas ?? pythonTypesConfig?.generateSchemas ?? true,
88
- globalRoute: config.types.globalRoute ?? pythonTypesConfig?.globalRoute ?? false,
89
- debounce: config.types.debounce ?? DEBOUNCE_MS
90
- };
91
- } else if (config.types !== false && pythonTypesConfig?.enabled) {
92
- const output = pythonTypesConfig.output ?? defaultTypesOutput;
93
- const defaults = buildTypeDefaults(output);
94
- typesConfig = {
95
- enabled: true,
96
- output,
97
- openapiPath: pythonTypesConfig.openapiPath ?? defaults.openapiPath,
98
- routesPath: pythonTypesConfig.routesPath ?? defaults.routesPath,
99
- pagePropsPath: pythonTypesConfig.pagePropsPath ?? defaults.pagePropsPath,
100
- schemasTsPath: pythonTypesConfig.schemasTsPath ?? defaults.schemasTsPath,
101
- generateZod: pythonTypesConfig.generateZod ?? false,
102
- generateSdk: pythonTypesConfig.generateSdk ?? true,
103
- generateRoutes: pythonTypesConfig.generateRoutes ?? true,
104
- generatePageProps: pythonTypesConfig.generatePageProps ?? true,
105
- generateSchemas: pythonTypesConfig.generateSchemas ?? true,
106
- globalRoute: pythonTypesConfig.globalRoute ?? false,
107
- debounce: DEBOUNCE_MS
108
- };
109
- }
110
48
  return {
111
49
  apiProxy: config.apiProxy ?? "http://localhost:8000",
112
50
  apiPrefix: config.apiPrefix ?? "/api",
@@ -25,6 +25,7 @@ export interface BridgeTypesConfig {
25
25
  generatePageProps: boolean;
26
26
  generateSchemas: boolean;
27
27
  globalRoute: boolean;
28
+ failOnError?: boolean;
28
29
  }
29
30
  export interface BridgeSpaConfig {
30
31
  /** Use script element instead of data-page attribute for Inertia page data */
@@ -29,6 +29,9 @@ const allowedLogLevels = /* @__PURE__ */ new Set(["quiet", "normal", "verbose"])
29
29
  function fail(message) {
30
30
  throw new Error(`litestar-vite-plugin: invalid .litestar.json - ${message}`);
31
31
  }
32
+ function warn(message) {
33
+ console.warn(`litestar-vite-plugin: invalid .litestar.json - ${message}`);
34
+ }
32
35
  function assertObject(value, label) {
33
36
  if (!value || typeof value !== "object" || Array.isArray(value)) {
34
37
  fail(`${label} must be an object`);
@@ -122,6 +125,8 @@ function parseTypesConfig(value) {
122
125
  const generatePageProps = assertBoolean(obj, "generatePageProps");
123
126
  const generateSchemas = assertOptionalBoolean(obj, "generateSchemas", true);
124
127
  const globalRoute = assertBoolean(obj, "globalRoute");
128
+ const rawFailOnError = obj.failOnError;
129
+ const failOnError = rawFailOnError === void 0 || rawFailOnError === null ? void 0 : assertBoolean(obj, "failOnError");
125
130
  return {
126
131
  enabled,
127
132
  output,
@@ -135,7 +140,8 @@ function parseTypesConfig(value) {
135
140
  generateRoutes,
136
141
  generatePageProps,
137
142
  generateSchemas,
138
- globalRoute
143
+ globalRoute,
144
+ failOnError
139
145
  };
140
146
  }
141
147
  function parseLogging(value) {
@@ -162,7 +168,7 @@ function parseBridgeSchema(value) {
162
168
  const obj = assertObject(value, "root");
163
169
  for (const key of Object.keys(obj)) {
164
170
  if (!allowedTopLevelKeys.has(key)) {
165
- fail(`unknown top-level key "${key}"`);
171
+ warn(`unknown top-level key "${key}" ignored`);
166
172
  }
167
173
  }
168
174
  const assetUrl = assertString(obj, "assetUrl");
@@ -211,16 +217,42 @@ function parseBridgeSchema(value) {
211
217
  function readBridgeConfig(explicitPath) {
212
218
  const envPath = explicitPath ?? process.env.LITESTAR_VITE_CONFIG_PATH;
213
219
  if (envPath) {
214
- if (!fs.existsSync(envPath)) {
215
- return null;
216
- }
217
- return readBridgeConfigFile(envPath);
220
+ return readBridgeConfigFileCached(envPath);
218
221
  }
219
222
  const defaultPath = path.join(process.cwd(), ".litestar.json");
220
- if (fs.existsSync(defaultPath)) {
221
- return readBridgeConfigFile(defaultPath);
223
+ return readBridgeConfigFileCached(defaultPath);
224
+ }
225
+ const bridgeConfigCache = /* @__PURE__ */ new Map();
226
+ function readBridgeConfigFileCached(filePath) {
227
+ const resolvedPath = path.resolve(filePath);
228
+ if (!fs.existsSync(resolvedPath)) {
229
+ return null;
230
+ }
231
+ let mtimeMs = null;
232
+ try {
233
+ mtimeMs = fs.statSync(resolvedPath).mtimeMs;
234
+ } catch {
235
+ mtimeMs = null;
236
+ }
237
+ if (mtimeMs !== null) {
238
+ const cached = bridgeConfigCache.get(resolvedPath);
239
+ if (cached?.mtimeMs === mtimeMs) {
240
+ return cached.config;
241
+ }
242
+ }
243
+ try {
244
+ const config = readBridgeConfigFile(resolvedPath);
245
+ if (mtimeMs !== null) {
246
+ bridgeConfigCache.set(resolvedPath, { mtimeMs, config });
247
+ }
248
+ return config;
249
+ } catch (error) {
250
+ warn(error instanceof Error ? error.message : String(error));
251
+ if (mtimeMs !== null) {
252
+ bridgeConfigCache.set(resolvedPath, { mtimeMs, config: null });
253
+ }
254
+ return null;
222
255
  }
223
- return null;
224
256
  }
225
257
  function readBridgeConfigFile(filePath) {
226
258
  let raw;
@@ -11,23 +11,9 @@
11
11
  */
12
12
  export declare const DEBOUNCE_MS = 300;
13
13
  /**
14
- * Timeout in milliseconds for backend health checks.
14
+ * Pinned fallback package spec for @hey-api/openapi-ts.
15
15
  *
16
- * Used when checking if the Litestar backend is available
17
- * before attempting to fetch OpenAPI schema.
16
+ * Keep this in sync with CURRENT_NPM_VERSION_RANGES in the Python scaffold
17
+ * registry and the optional peer dependency range in package.json.
18
18
  */
19
- export declare const BACKEND_CHECK_TIMEOUT_MS = 2000;
20
- /**
21
- * Default asset URL path.
22
- *
23
- * Used as fallback when no explicit assetUrl is configured.
24
- * Should match the default in Python ViteConfig.
25
- */
26
- export declare const DEFAULT_ASSET_URL = "/static/";
27
- /**
28
- * Default output directory for generated TypeScript types.
29
- *
30
- * Used by framework integrations as fallback when no output
31
- * is specified in Python bridge config or plugin options.
32
- */
33
- export declare const DEFAULT_TYPES_OUTPUT = "src/generated";
19
+ export declare const HEY_API_PINNED_SPEC = "@hey-api/openapi-ts@0.98.2";
@@ -1,10 +1,6 @@
1
1
  const DEBOUNCE_MS = 300;
2
- const BACKEND_CHECK_TIMEOUT_MS = 2e3;
3
- const DEFAULT_ASSET_URL = "/static/";
4
- const DEFAULT_TYPES_OUTPUT = "src/generated";
2
+ const HEY_API_PINNED_SPEC = "@hey-api/openapi-ts@0.98.2";
5
3
  export {
6
- BACKEND_CHECK_TIMEOUT_MS,
7
4
  DEBOUNCE_MS,
8
- DEFAULT_ASSET_URL,
9
- DEFAULT_TYPES_OUTPUT
5
+ HEY_API_PINNED_SPEC
10
6
  };
@@ -3,4 +3,4 @@
3
3
  *
4
4
  * @returns true if file was changed, false if unchanged
5
5
  */
6
- export declare function emitPagePropsTypes(pagesPath: string, outputDir: string): Promise<boolean>;
6
+ export declare function emitPagePropsTypes(pagesPath: string, outputDir: string, projectRoot?: string): Promise<boolean>;
@@ -1,10 +1,44 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { writeIfChanged } from "./write-if-changed.js";
4
- async function emitPagePropsTypes(pagesPath, outputDir) {
4
+ const semanticAliases = {
5
+ UUID: { description: "UUID v4 string", base: "string" },
6
+ DateTime: { description: "RFC 3339 date-time string", base: "string" },
7
+ DateOnly: { description: "ISO 8601 date string (YYYY-MM-DD)", base: "string" },
8
+ TimeOnly: { description: "ISO 8601 time string", base: "string" },
9
+ Duration: { description: "ISO 8601 duration string", base: "string" },
10
+ Email: { description: "Email address string", base: "string" },
11
+ URI: { description: "URI/URL string", base: "string" },
12
+ IPv4: { description: "IPv4 address string", base: "string" },
13
+ IPv6: { description: "IPv6 address string", base: "string" }
14
+ };
15
+ function collectSemanticAliases(typeExpr) {
16
+ const aliases = /* @__PURE__ */ new Set();
17
+ for (const match of typeExpr.matchAll(/\b[A-Za-z_][A-Za-z0-9_]*\b/g)) {
18
+ const name = match[0];
19
+ if (name in semanticAliases) {
20
+ aliases.add(name);
21
+ }
22
+ }
23
+ return [...aliases].toSorted();
24
+ }
25
+ function renderSemanticAliases(aliases) {
26
+ if (aliases.size === 0) {
27
+ return "";
28
+ }
29
+ const lines = ["/** Semantic string aliases derived from OpenAPI `format`. */"];
30
+ for (const alias of [...aliases].toSorted()) {
31
+ const { description, base } = semanticAliases[alias];
32
+ lines.push(`/** ${description} */`, `export type ${alias} = ${base}`, "");
33
+ }
34
+ return `${lines.join("\n").trimEnd()}
35
+
36
+ `;
37
+ }
38
+ async function emitPagePropsTypes(pagesPath, outputDir, projectRoot = process.cwd()) {
5
39
  const contents = await fs.promises.readFile(pagesPath, "utf-8");
6
40
  const json = JSON.parse(contents);
7
- const outDir = path.resolve(process.cwd(), outputDir);
41
+ const outDir = path.resolve(projectRoot, outputDir);
8
42
  await fs.promises.mkdir(outDir, { recursive: true });
9
43
  const outFile = path.join(outDir, "page-props.ts");
10
44
  const { includeDefaultAuth, includeDefaultFlash } = json.typeGenConfig;
@@ -84,8 +118,15 @@ export interface FlashMessages {}
84
118
  return ` ${safeKey}${optional}: ${def.type}`;
85
119
  });
86
120
  const allCustomTypes = /* @__PURE__ */ new Set();
121
+ const usedSemanticAliases = /* @__PURE__ */ new Set();
87
122
  for (const data of Object.values(json.pages)) {
88
- if (data.tsType) {
123
+ const typeExpr = data.tsType ?? data.propsType;
124
+ if (typeExpr) {
125
+ for (const alias of collectSemanticAliases(typeExpr)) {
126
+ usedSemanticAliases.add(alias);
127
+ }
128
+ }
129
+ if (data.tsType && /^[A-Za-z_][A-Za-z0-9_]*$/.test(data.tsType)) {
89
130
  allCustomTypes.add(data.tsType);
90
131
  }
91
132
  for (const t of data.customTypes ?? []) {
@@ -131,9 +172,13 @@ export interface FlashMessages {}
131
172
  "RegExp",
132
173
  "User",
133
174
  "AuthData",
134
- "FlashMessages"
175
+ "FlashMessages",
176
+ ...Object.keys(semanticAliases)
135
177
  ]);
136
178
  for (const def of Object.values(generatedSharedProps)) {
179
+ for (const alias of collectSemanticAliases(def.type)) {
180
+ usedSemanticAliases.add(alias);
181
+ }
137
182
  for (const match of def.type.matchAll(/\b[A-Za-z_][A-Za-z0-9_]*\b/g)) {
138
183
  const name = match[0];
139
184
  if (!builtinTypes.has(name)) {
@@ -190,6 +235,7 @@ export interface FlashMessages {}
190
235
  if (importStatement) {
191
236
  importStatement += "\n";
192
237
  }
238
+ const semanticAliasTypes = renderSemanticAliases(usedSemanticAliases);
193
239
  const pageEntries = [];
194
240
  for (const [component, data] of Object.entries(json.pages)) {
195
241
  const rawType = data.tsType || data.propsType || defaultFallback;
@@ -206,7 +252,7 @@ export interface FlashMessages {}
206
252
  // Import user-defined type extensions (edit page-props.user.ts to customize)
207
253
  import type { UserExtensions, SharedPropsExtensions } from "./page-props.user"
208
254
 
209
- ${importStatement}${userTypes}${authTypes}${flashTypes}/**
255
+ ${importStatement}${semanticAliasTypes}${userTypes}${authTypes}${flashTypes}/**
210
256
  * Generated shared props (always present).
211
257
  * Includes built-in props + static config props.
212
258
  */
@@ -5,4 +5,4 @@
5
5
  * @param outputDir - Output directory (contains api/types.gen.ts)
6
6
  * @returns true if file was changed, false if unchanged
7
7
  */
8
- export declare function emitSchemasTypes(routesJsonPath: string, outputDir: string, schemasOutputPath?: string): Promise<boolean>;
8
+ export declare function emitSchemasTypes(routesJsonPath: string, outputDir: string, schemasOutputPath?: string, projectRoot?: string): Promise<boolean>;
@@ -10,21 +10,12 @@ function toImportPath(fromDir, targetFile) {
10
10
  return `./${withoutExtension}`;
11
11
  }
12
12
  function parseHeyApiTypes(content) {
13
- const dataTypes = /* @__PURE__ */ new Map();
14
13
  const responsesTypes = /* @__PURE__ */ new Set();
15
14
  const errorsTypes = /* @__PURE__ */ new Set();
16
15
  const urlToDataType = /* @__PURE__ */ new Map();
17
- const typeBlockRegex = /export\s+type\s+(\w+Data)\s*=\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs;
18
- for (const match of content.matchAll(typeBlockRegex)) {
19
- const typeName = match[1];
20
- const typeBody = match[2];
21
- const urlMatch = typeBody.match(/url:\s*['"]([^'"]+)['"]/);
16
+ for (const { typeName, body } of findExportedDataTypeBlocks(content)) {
17
+ const urlMatch = body.match(/url:\s*['"]([^'"]+)['"]/);
22
18
  const url = urlMatch ? urlMatch[1] : null;
23
- const hasBody = /\bbody\s*:/.test(typeBody) && !/\bbody\s*\?\s*:\s*never/.test(typeBody);
24
- const hasPath = /\bpath\s*:/.test(typeBody) && !/\bpath\s*\?\s*:\s*never/.test(typeBody);
25
- const hasQuery = /\bquery\s*:/.test(typeBody) && !/\bquery\s*\?\s*:\s*never/.test(typeBody);
26
- const info = { typeName, url, hasBody, hasPath, hasQuery };
27
- dataTypes.set(typeName, info);
28
19
  if (url) {
29
20
  urlToDataType.set(url, typeName);
30
21
  }
@@ -37,7 +28,87 @@ function parseHeyApiTypes(content) {
37
28
  for (const match of content.matchAll(errorsRegex)) {
38
29
  errorsTypes.add(match[1]);
39
30
  }
40
- return { dataTypes, responsesTypes, errorsTypes, urlToDataType };
31
+ return { responsesTypes, errorsTypes, urlToDataType };
32
+ }
33
+ function findExportedDataTypeBlocks(content) {
34
+ const blocks = [];
35
+ const declarationRegex = /export\s+type\s+(\w+Data)\s*=\s*/g;
36
+ for (const match of content.matchAll(declarationRegex)) {
37
+ const typeName = match[1];
38
+ const bodyStart = match.index + match[0].length;
39
+ const openIndex = content.indexOf("{", bodyStart);
40
+ const nextToken = content.slice(bodyStart, openIndex === -1 ? content.length : openIndex).trim();
41
+ if (openIndex === -1 || nextToken.length > 0) {
42
+ console.warn(`litestar-vite: Unable to parse hey-api Data type ${typeName}; expected object type`);
43
+ continue;
44
+ }
45
+ const closeIndex = findMatchingBrace(content, openIndex);
46
+ if (closeIndex === -1) {
47
+ console.warn(`litestar-vite: Unable to parse hey-api Data type ${typeName}; unbalanced braces`);
48
+ continue;
49
+ }
50
+ blocks.push({ typeName, body: content.slice(openIndex + 1, closeIndex) });
51
+ }
52
+ return blocks;
53
+ }
54
+ function findMatchingBrace(content, openIndex) {
55
+ let depth = 0;
56
+ let quote = null;
57
+ let escaped = false;
58
+ let lineComment = false;
59
+ let blockComment = false;
60
+ for (let i = openIndex; i < content.length; i += 1) {
61
+ const char = content[i];
62
+ const next = content[i + 1];
63
+ if (lineComment) {
64
+ if (char === "\n") {
65
+ lineComment = false;
66
+ }
67
+ continue;
68
+ }
69
+ if (blockComment) {
70
+ if (char === "*" && next === "/") {
71
+ blockComment = false;
72
+ i += 1;
73
+ }
74
+ continue;
75
+ }
76
+ if (quote) {
77
+ if (escaped) {
78
+ escaped = false;
79
+ } else if (char === "\\") {
80
+ escaped = true;
81
+ } else if (char === quote) {
82
+ quote = null;
83
+ }
84
+ continue;
85
+ }
86
+ if (char === "/" && next === "/") {
87
+ lineComment = true;
88
+ i += 1;
89
+ continue;
90
+ }
91
+ if (char === "/" && next === "*") {
92
+ blockComment = true;
93
+ i += 1;
94
+ continue;
95
+ }
96
+ if (char === '"' || char === "'" || char === "`") {
97
+ quote = char;
98
+ continue;
99
+ }
100
+ if (char === "{") {
101
+ depth += 1;
102
+ continue;
103
+ }
104
+ if (char === "}") {
105
+ depth -= 1;
106
+ if (depth === 0) {
107
+ return i;
108
+ }
109
+ }
110
+ }
111
+ return -1;
41
112
  }
42
113
  function normalizePath(routePath) {
43
114
  return routePath.replace(/\{([^:}]+):[^}]+\}/g, "{$1}");
@@ -312,9 +383,9 @@ export type HasPathParams<T extends OperationName> = PathParams<T> extends never
312
383
  export type HasQueryParams<T extends OperationName> = QueryParams<T> extends never ? false : true
313
384
  `;
314
385
  }
315
- async function emitSchemasTypes(routesJsonPath, outputDir, schemasOutputPath) {
316
- const outDir = path.resolve(process.cwd(), outputDir);
317
- const outFile = schemasOutputPath ? path.resolve(process.cwd(), schemasOutputPath) : path.join(outDir, "schemas.ts");
386
+ async function emitSchemasTypes(routesJsonPath, outputDir, schemasOutputPath, projectRoot = process.cwd()) {
387
+ const outDir = path.resolve(projectRoot, outputDir);
388
+ const outFile = schemasOutputPath ? path.resolve(projectRoot, schemasOutputPath) : path.join(outDir, "schemas.ts");
318
389
  const schemasDir = path.dirname(outFile);
319
390
  const apiTypesPath = path.join(outDir, "api", "types.gen.ts");
320
391
  const apiTypesImportPath = toImportPath(schemasDir, apiTypesPath);
@@ -8,4 +8,4 @@
8
8
  *
9
9
  * @returns true if file was changed, false if unchanged
10
10
  */
11
- export declare function emitStaticPropsTypes(outputDir: string): Promise<boolean>;
11
+ export declare function emitStaticPropsTypes(outputDir: string, projectRoot?: string): Promise<boolean>;
@@ -53,10 +53,10 @@ function toInterfaceName(key) {
53
53
  const pascal = key.replace(/[^a-zA-Z0-9_$]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
54
54
  return pascal || "StaticProp";
55
55
  }
56
- async function emitStaticPropsTypes(outputDir) {
56
+ async function emitStaticPropsTypes(outputDir, projectRoot = process.cwd()) {
57
57
  const bridgeConfig = readBridgeConfig();
58
58
  const staticProps = bridgeConfig?.staticProps ?? {};
59
- const outDir = path.resolve(process.cwd(), outputDir);
59
+ const outDir = path.resolve(projectRoot, outputDir);
60
60
  await fs.promises.mkdir(outDir, { recursive: true });
61
61
  const outFile = path.join(outDir, "static-props.ts");
62
62
  if (Object.keys(staticProps).length === 0) {