@luckystack/devkit 0.1.9 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ validateDeploy
3
+ } from "./chunk-YYWCJ7VZ.js";
4
+
1
5
  // src/typeMap/discovery.ts
2
6
  import fs from "fs";
3
7
  import path from "path";
@@ -68,7 +72,15 @@ var toForwardSlashRelative = (absolute) => {
68
72
  const rel = path.relative(ROOT_DIR, absolute);
69
73
  return rel.replaceAll("\\", "/");
70
74
  };
71
- var walkFiles = (dir, matcher, results = []) => {
75
+ var walkFiles = (dir, matcher, results = [], visited = /* @__PURE__ */ new Set()) => {
76
+ let realDir;
77
+ try {
78
+ realDir = fs.realpathSync(dir);
79
+ } catch {
80
+ return results;
81
+ }
82
+ if (visited.has(realDir)) return results;
83
+ visited.add(realDir);
72
84
  const { ignore } = getRoutingRules();
73
85
  try {
74
86
  const entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -76,9 +88,14 @@ var walkFiles = (dir, matcher, results = []) => {
76
88
  const fullPath = path.join(dir, entry.name);
77
89
  const relativePath = toForwardSlashRelative(fullPath);
78
90
  if (ignore(relativePath)) continue;
79
- if (entry.isDirectory()) {
91
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
80
92
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
81
- walkFiles(fullPath, matcher, results);
93
+ try {
94
+ if (fs.statSync(fullPath).isDirectory()) {
95
+ walkFiles(fullPath, matcher, results, visited);
96
+ }
97
+ } catch {
98
+ }
82
99
  continue;
83
100
  }
84
101
  if (entry.isFile() && matcher(fullPath, entry.name)) {
@@ -93,27 +110,28 @@ var walkFiles = (dir, matcher, results = []) => {
93
110
  var findAllApiFiles = (srcDir) => {
94
111
  const apiSegment = apiMarkerSegment();
95
112
  return walkFiles(srcDir, (fullPath, entryName) => {
96
- const normalized = fullPath.replace(/\\/g, "/");
113
+ const normalized = fullPath.replaceAll("\\", "/");
97
114
  return isApiFileName(entryName) && normalized.includes(apiSegment);
98
115
  });
99
116
  };
100
117
  var findAllSyncServerFiles = (srcDir) => {
101
118
  const syncSegment = syncMarkerSegment();
102
119
  return walkFiles(srcDir, (fullPath, entryName) => {
103
- const normalized = fullPath.replace(/\\/g, "/");
120
+ const normalized = fullPath.replaceAll("\\", "/");
104
121
  return isSyncServerFileName(entryName) && normalized.includes(syncSegment);
105
122
  });
106
123
  };
107
124
  var findAllSyncClientFiles = (srcDir) => {
108
125
  const syncSegment = syncMarkerSegment();
109
126
  return walkFiles(srcDir, (fullPath, entryName) => {
110
- const normalized = fullPath.replace(/\\/g, "/");
127
+ const normalized = fullPath.replaceAll("\\", "/");
111
128
  return isSyncClientFileName(entryName) && normalized.includes(syncSegment);
112
129
  });
113
130
  };
114
131
 
115
132
  // src/typeMap/routeMeta.ts
116
133
  import path2 from "path";
134
+ import { getSrcDir } from "@luckystack/core";
117
135
 
118
136
  // src/routeConventions.ts
119
137
  var API_VERSION_TOKEN_REGEX = /_v(\d+)$/;
@@ -139,71 +157,76 @@ var stripVersionSuffix = (name) => {
139
157
  return name.replace(VERSION_SUFFIX_REGEX, "");
140
158
  };
141
159
  var extractVersionFromName = (name) => {
142
- const match = name.match(VERSION_SUFFIX_REGEX);
160
+ const match = VERSION_SUFFIX_REGEX.exec(name);
143
161
  if (!match) return null;
144
162
  return `v${match[1]}`;
145
163
  };
146
164
  var extractPagePath = (filePath) => {
147
- const normalized = filePath.replace(/\\/g, "/");
148
- const match = normalized.match(/src\/(?:(.+?)\/)_api\//);
165
+ const normalized = filePath.replaceAll("\\", "/");
166
+ const srcDirNormalized = getSrcDir().replaceAll("\\", "/");
167
+ const rel = path2.posix.relative(srcDirNormalized, normalized);
168
+ if (rel.startsWith("..")) {
169
+ throw new Error(`[routeMeta] file is outside srcDir \u2014 cannot extract page path: ${filePath}`);
170
+ }
171
+ const match = /^(?:(.+?)\/)_api\//.exec(rel);
149
172
  if (match) {
150
- return match[1] || "system";
173
+ return match[1] ?? "system";
151
174
  }
152
- if (normalized.includes("/src/_api/")) {
175
+ if (rel.startsWith("_api/")) {
153
176
  return "system";
154
177
  }
155
178
  return "";
156
179
  };
157
180
  var extractApiName = (filePath) => {
158
- const normalized = filePath.replace(/\\/g, "/");
159
- const match = normalized.match(/_api\/(.+)\.ts$/);
181
+ const normalized = filePath.replaceAll("\\", "/");
182
+ const match = /_api\/(.+)\.ts$/.exec(normalized);
160
183
  const rawName = match?.[1] ?? path2.basename(filePath, ".ts");
161
184
  return stripVersionSuffix(rawName);
162
185
  };
163
186
  var extractApiVersion = (filePath) => {
164
- const normalized = filePath.replace(/\\/g, "/");
165
- const match = normalized.match(/_api\/(.+)\.ts$/);
187
+ const normalized = filePath.replaceAll("\\", "/");
188
+ const match = /_api\/(.+)\.ts$/.exec(normalized);
166
189
  const rawName = match?.[1] ?? path2.basename(filePath, ".ts");
167
- return extractVersionFromName(rawName) || "v1";
190
+ return extractVersionFromName(rawName) ?? "v1";
168
191
  };
169
192
  var extractSyncPagePath = (filePath) => {
170
- const normalized = filePath.replace(/\\/g, "/");
171
- const match = normalized.match(/src\/(?:(.+?)\/)_sync\//);
193
+ const normalized = filePath.replaceAll("\\", "/");
194
+ const srcDirNormalized = getSrcDir().replaceAll("\\", "/");
195
+ const rel = path2.posix.relative(srcDirNormalized, normalized);
196
+ const match = /^(?:(.+?)\/)_sync\//.exec(rel);
172
197
  if (match) {
173
- return match[1] || "root";
198
+ return match[1] ?? "system";
174
199
  }
175
- if (normalized.includes("/src/_sync/")) {
176
- return "root";
200
+ if (rel.startsWith("_sync/")) {
201
+ return "system";
177
202
  }
178
203
  return "";
179
204
  };
180
205
  var extractSyncName = (filePath) => {
181
- const normalized = filePath.replace(/\\/g, "/");
182
- const match = normalized.match(/_sync\/(.+)\.ts$/);
206
+ const normalized = filePath.replaceAll("\\", "/");
207
+ const match = /_sync\/(.+)\.ts$/.exec(normalized);
183
208
  if (!match) {
184
209
  const basename = path2.basename(filePath, ".ts");
185
- const rawName2 = basename.replace(SYNC_VERSION_TOKEN_REGEX, "");
186
- return rawName2;
210
+ return basename.replace(SYNC_VERSION_TOKEN_REGEX, "");
187
211
  }
188
- const rawName = (match[1] ?? "").replace(SYNC_VERSION_TOKEN_REGEX, "");
189
- return rawName;
212
+ return (match[1] ?? "").replace(SYNC_VERSION_TOKEN_REGEX, "");
190
213
  };
191
214
  var extractSyncVersion = (filePath) => {
192
- const normalized = filePath.replace(/\\/g, "/");
193
- const match = normalized.match(/_sync\/(.+)\.ts$/);
215
+ const normalized = filePath.replaceAll("\\", "/");
216
+ const match = /_sync\/(.+)\.ts$/.exec(normalized);
194
217
  if (!match) {
195
218
  const basename = path2.basename(filePath, ".ts");
196
- const versionMatch2 = basename.match(SYNC_VERSION_TOKEN_REGEX);
219
+ const versionMatch2 = SYNC_VERSION_TOKEN_REGEX.exec(basename);
197
220
  return versionMatch2 ? `v${versionMatch2[2] ?? "1"}` : "v1";
198
221
  }
199
- const versionMatch = (match[1] ?? "").match(SYNC_VERSION_TOKEN_REGEX);
222
+ const versionMatch = SYNC_VERSION_TOKEN_REGEX.exec(match[1] ?? "");
200
223
  return versionMatch ? `v${versionMatch[2] ?? "1"}` : "v1";
201
224
  };
202
225
 
203
226
  // src/typeMap/apiMeta.ts
204
227
  import * as ts from "typescript";
205
228
  import fs2 from "fs";
206
- import { inferHttpMethod } from "@luckystack/core";
229
+ import { inferHttpMethod, tryCatchSync } from "@luckystack/core";
207
230
  var findExportedConst = (sourceFile, name) => {
208
231
  for (const statement of sourceFile.statements) {
209
232
  if (!ts.isVariableStatement(statement)) continue;
@@ -223,22 +246,24 @@ var unwrapExpression = (node) => {
223
246
  return current;
224
247
  };
225
248
  var extractHttpMethod = (filePath, apiName) => {
226
- try {
249
+ const [error, method] = tryCatchSync(() => {
227
250
  const content = fs2.readFileSync(filePath, "utf8");
228
251
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
229
252
  const decl = findExportedConst(sourceFile, "httpMethod");
230
253
  const initializer = decl?.initializer ? unwrapExpression(decl.initializer) : void 0;
231
254
  if (initializer && ts.isStringLiteral(initializer)) {
232
- const method = initializer.text.toUpperCase();
233
- if (["GET", "POST", "PUT", "DELETE"].includes(method)) return method;
255
+ const candidate = initializer.text.toUpperCase();
256
+ if (["GET", "POST", "PUT", "DELETE"].includes(candidate)) return candidate;
234
257
  }
235
- } catch (error) {
258
+ return void 0;
259
+ });
260
+ if (error) {
236
261
  console.error(`[TypeMapGenerator] Error extracting httpMethod from ${filePath}:`, error);
237
262
  }
238
- return inferHttpMethod(apiName);
263
+ return method ?? inferHttpMethod(apiName);
239
264
  };
240
265
  var extractRateLimit = (filePath) => {
241
- try {
266
+ const [error, rateLimit] = tryCatchSync(() => {
242
267
  const content = fs2.readFileSync(filePath, "utf8");
243
268
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
244
269
  const decl = findExportedConst(sourceFile, "rateLimit");
@@ -247,10 +272,12 @@ var extractRateLimit = (filePath) => {
247
272
  if (initializer.kind === ts.SyntaxKind.FalseKeyword) return false;
248
273
  if (ts.isNumericLiteral(initializer)) return Number(initializer.text);
249
274
  }
250
- } catch (error) {
275
+ return void 0;
276
+ });
277
+ if (error) {
251
278
  console.error(`[TypeMapGenerator] Error extracting rateLimit from ${filePath}:`, error);
252
279
  }
253
- return void 0;
280
+ return rateLimit ?? void 0;
254
281
  };
255
282
  var readPrimitive = (rawNode) => {
256
283
  const node = unwrapExpression(rawNode);
@@ -269,23 +296,23 @@ var parseAdditionalItem = (objectLiteral) => {
269
296
  }
270
297
  return item.key ? item : null;
271
298
  };
299
+ var commentToString = (commentValue) => {
300
+ if (typeof commentValue === "string") return commentValue;
301
+ if (Array.isArray(commentValue)) {
302
+ return commentValue.map((part) => {
303
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
304
+ return part.text;
305
+ }
306
+ return "";
307
+ }).join("");
308
+ }
309
+ return "";
310
+ };
272
311
  var extractDocsMeta = (filePath) => {
273
- try {
312
+ const [error, docsMeta] = tryCatchSync(() => {
274
313
  const content = fs2.readFileSync(filePath, "utf8");
275
314
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
276
315
  const result = {};
277
- const commentToString = (commentValue) => {
278
- if (typeof commentValue === "string") return commentValue;
279
- if (Array.isArray(commentValue)) {
280
- return commentValue.map((part) => {
281
- if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
282
- return part.text;
283
- }
284
- return "";
285
- }).join("");
286
- }
287
- return "";
288
- };
289
316
  const consumeTag = (tag) => {
290
317
  if (tag.tagName.text !== "docs") return;
291
318
  const commentText = commentToString(tag.comment).trim();
@@ -311,19 +338,21 @@ var extractDocsMeta = (filePath) => {
311
338
  return void 0;
312
339
  }
313
340
  return result;
314
- } catch (error) {
341
+ });
342
+ if (error) {
315
343
  console.error(`[TypeMapGenerator] Error extracting @docs metadata from ${filePath}:`, error);
316
344
  return void 0;
317
345
  }
346
+ return docsMeta ?? void 0;
318
347
  };
319
348
  var extractAuth = (filePath) => {
320
- try {
349
+ const [, auth] = tryCatchSync(() => {
321
350
  const content = fs2.readFileSync(filePath, "utf8");
322
351
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
323
352
  const decl = findExportedConst(sourceFile, "auth");
324
353
  const authInitializer = decl?.initializer ? unwrapExpression(decl.initializer) : void 0;
325
- if (!authInitializer || !ts.isObjectLiteralExpression(authInitializer)) return { login: true };
326
- let login = true;
354
+ if (!authInitializer || !ts.isObjectLiteralExpression(authInitializer)) return { login: false };
355
+ let login = false;
327
356
  let additional;
328
357
  for (const prop of authInitializer.properties) {
329
358
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
@@ -341,9 +370,8 @@ var extractAuth = (filePath) => {
341
370
  }
342
371
  }
343
372
  return additional && additional.length > 0 ? { login, additional } : { login };
344
- } catch {
345
- }
346
- return { login: true };
373
+ });
374
+ return auth ?? { login: false };
347
375
  };
348
376
 
349
377
  // src/typeMap/emitterArtifacts.ts
@@ -454,7 +482,7 @@ var convertTypeNode = (node) => {
454
482
  }
455
483
  return `${JSON.stringify(name)}: ${schema}`;
456
484
  }).filter((entry) => entry !== null);
457
- return `z.object({ ${entries.join(", ")} })`;
485
+ return `z.object({ ${entries.join(", ")} }).strict()`;
458
486
  }
459
487
  if (ts2.isParenthesizedTypeNode(node)) {
460
488
  return convertTypeNode(node.type);
@@ -604,6 +632,37 @@ var validateGeneratedTypeIdentifiers = (content) => {
604
632
  throw new Error(`[TypeMapGenerator] Generated type map has unresolved type identifiers: ${unknown.join(", ")}`);
605
633
  }
606
634
  };
635
+ var collectFallbacks = (typesByPage, syncTypesByPage) => {
636
+ const entries = [];
637
+ const flagField = (route, kind, field, value) => {
638
+ if (value === "{ }" || value === "{ status: string }") {
639
+ entries.push({ route, kind, field, fallback: value, reason: "default-fallback" });
640
+ return;
641
+ }
642
+ const zodSrc = typeTextToZodSource(value);
643
+ if (zodSrc?.includes("z.any()")) {
644
+ entries.push({ route, kind, field, fallback: value.slice(0, 80), reason: "zod-any-fallback" });
645
+ }
646
+ };
647
+ for (const [pagePath, apis] of typesByPage) {
648
+ for (const [apiKey, entry] of apis) {
649
+ const { name, version } = splitVersionedKey(apiKey);
650
+ const route = `${pagePath}/${name}@${version}`;
651
+ flagField(route, "api", "input", entry.input);
652
+ flagField(route, "api", "output", entry.output);
653
+ }
654
+ }
655
+ for (const [pagePath, syncs] of syncTypesByPage) {
656
+ for (const [syncKey, entry] of syncs) {
657
+ const { name, version } = splitVersionedKey(syncKey);
658
+ const route = `${pagePath}/${name}@${version}`;
659
+ flagField(route, "sync", "clientInput", entry.clientInput);
660
+ flagField(route, "sync", "serverOutput", entry.serverOutput);
661
+ flagField(route, "sync", "clientOutput", entry.clientOutput);
662
+ }
663
+ }
664
+ return entries;
665
+ };
607
666
  var buildTypeMapArtifacts = ({
608
667
  typesByPage,
609
668
  syncTypesByPage,
@@ -701,7 +760,7 @@ type _ProjectApiTypeMap = {
701
760
  `;
702
761
  content += ` input: ${indentStr(entry.input, " ")};
703
762
  `;
704
- content += ` output: ${indentStr(entry.output, " ")};
763
+ content += ` output: ${indentStr(entry.output, " ")} | { status: 'error'; errorCode: string; [key: string]: unknown };
705
764
  `;
706
765
  content += ` stream: ${indentStr(entry.stream, " ")};
707
766
  `;
@@ -844,16 +903,21 @@ type _ProjectSyncTypeMap = {
844
903
  clientOutput: entry.clientOutput,
845
904
  serverStream: entry.serverStream,
846
905
  clientStream: entry.clientStream,
847
- path: pagePath === "root" ? `sync/${syncName}/${version}` : `sync/${pagePath}/${syncName}/${version}`,
906
+ //? `extractSyncPagePath` returns the `'system'` sentinel (NOT
907
+ //? `'root'`) for a src-root sync, matching the loader's runtime key
908
+ //? (`sync/system/<name>/<version>`) AND the wire name the typed
909
+ //? `syncRequest` sends. The old `pagePath === 'root'` branch is now
910
+ //? dead and would emit a non-matching path, so it is removed.
911
+ path: `sync/${pagePath}/${syncName}/${version}`,
848
912
  ...entry.meta ? { meta: entry.meta } : {}
849
913
  });
850
914
  content += ` '${version}': {
851
915
  `;
852
916
  content += ` clientInput: ${indentStr(entry.clientInput, " ")};
853
917
  `;
854
- content += ` serverOutput: ${indentStr(entry.serverOutput, " ")};
918
+ content += ` serverOutput: ${indentStr(entry.serverOutput, " ")} | { status: 'error'; errorCode: string; [key: string]: unknown };
855
919
  `;
856
- content += ` clientOutput: ${indentStr(entry.clientOutput, " ")};
920
+ content += ` clientOutput: ${indentStr(entry.clientOutput, " ")} | { status: 'error'; errorCode: string; [key: string]: unknown };
857
921
  `;
858
922
  content += ` serverStream: ${indentStr(entry.serverStream, " ")};
859
923
  `;
@@ -897,7 +961,15 @@ declare module '@luckystack/core/typemap' {
897
961
  `;
898
962
  validateGeneratedTypeIdentifiers(content);
899
963
  const schemasContent = buildSchemasContent({ typesByPage });
900
- return { content, docsData, schemasContent };
964
+ const fallbacks = collectFallbacks(typesByPage, syncTypesByPage);
965
+ const totalRoutes = [...typesByPage.values()].reduce((n, m) => n + m.size, 0) + [...syncTypesByPage.values()].reduce((n, m) => n + m.size, 0);
966
+ const diagnosticsData = {
967
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
968
+ totalRoutes,
969
+ fallbackCount: fallbacks.length,
970
+ fallbacks
971
+ };
972
+ return { content, docsData, schemasContent, diagnosticsData };
901
973
  };
902
974
  var buildSchemasContent = ({
903
975
  typesByPage
@@ -954,7 +1026,8 @@ export const getApiInputSchema = (
954
1026
  var writeTypeMapArtifacts = ({
955
1027
  content,
956
1028
  docsData,
957
- schemasContent
1029
+ schemasContent,
1030
+ diagnosticsData
958
1031
  }) => {
959
1032
  try {
960
1033
  const outputPath = getGeneratedSocketTypesPath();
@@ -978,6 +1051,17 @@ var writeTypeMapArtifacts = ({
978
1051
  if (hasUpdatedDocs) {
979
1052
  console.log("[TypeMapGenerator] Generated apiDocs.generated.json");
980
1053
  }
1054
+ if (diagnosticsData !== void 0) {
1055
+ const diagnosticsPath = path3.join(path3.dirname(docsPath), "apiTypeDiagnostics.generated.json");
1056
+ const diagnosticsContent = JSON.stringify(diagnosticsData, null, 2);
1057
+ const hasUpdatedDiagnostics = writeFileIfChanged(diagnosticsPath, diagnosticsContent);
1058
+ if (hasUpdatedDiagnostics) {
1059
+ console.log("[TypeMapGenerator] Generated apiTypeDiagnostics.generated.json");
1060
+ }
1061
+ if (diagnosticsData.fallbackCount > 0) {
1062
+ console.warn(`[TypeMapGenerator] ${diagnosticsData.fallbackCount} route(s) have degraded type extraction (see apiTypeDiagnostics.generated.json)`);
1063
+ }
1064
+ }
981
1065
  } catch (error) {
982
1066
  console.error("[TypeMapGenerator] Error writing type map or docs:", error);
983
1067
  }
@@ -990,13 +1074,13 @@ import path5 from "path";
990
1074
  // src/typeMap/tsProgram.ts
991
1075
  import * as ts4 from "typescript";
992
1076
  import path4 from "path";
993
- import { ROOT_DIR as ROOT_DIR2 } from "@luckystack/core";
1077
+ import { ROOT_DIR as ROOT_DIR2, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath2 } from "@luckystack/core";
994
1078
  var cachedProgram = null;
995
1079
  var getServerProgram = () => {
996
1080
  if (cachedProgram) return cachedProgram;
997
- const tsconfigPath = ts4.findConfigFile(ROOT_DIR2, ts4.sys.fileExists, "tsconfig.server.json");
1081
+ const tsconfigPath = ts4.findConfigFile(ROOT_DIR2, ts4.sys.fileExists.bind(ts4.sys), "tsconfig.server.json");
998
1082
  if (!tsconfigPath) throw new Error("[TypeProgram] tsconfig.server.json not found");
999
- const { config } = ts4.readConfigFile(tsconfigPath, ts4.sys.readFile);
1083
+ const { config } = ts4.readConfigFile(tsconfigPath, ts4.sys.readFile.bind(ts4.sys));
1000
1084
  const { options, fileNames } = ts4.parseJsonConfigFileContent(
1001
1085
  config,
1002
1086
  ts4.sys,
@@ -1032,8 +1116,8 @@ var SKIP_EXPANSION = /* @__PURE__ */ new Set([
1032
1116
  "ReadonlyArray"
1033
1117
  ]);
1034
1118
  var isJsonLikeType = (type, checker) => {
1035
- const symbolName = type.symbol?.name || "";
1036
- const aliasName = type.aliasSymbol?.name || "";
1119
+ const symbolName = type.symbol?.name ?? "";
1120
+ const aliasName = type.aliasSymbol?.name ?? "";
1037
1121
  if (JSON_TYPE_NAMES.has(symbolName) || JSON_TYPE_NAMES.has(aliasName)) return true;
1038
1122
  const rendered = checker.typeToString(type);
1039
1123
  return /(\bPrisma\.)?(Input)?Json(Value|Object|Array)\b/.test(rendered);
@@ -1084,7 +1168,7 @@ var getLiteralTypeFromPropertySymbol = (symbol, checker, depth) => {
1084
1168
  return null;
1085
1169
  };
1086
1170
  var normalizeImportPath = (targetFilePath) => {
1087
- const fromDir = path4.join(ROOT_DIR2, "src", "_sockets");
1171
+ const fromDir = path4.dirname(getGeneratedSocketTypesPath2());
1088
1172
  const from = fromDir.replaceAll("\\", "/");
1089
1173
  const to = targetFilePath.replaceAll("\\", "/");
1090
1174
  const normalized = path4.posix.relative(from, to).replaceAll("\\", "/");
@@ -1177,7 +1261,7 @@ var expandTypeDetailed = (type, checker, depth = 0, state) => {
1177
1261
  }
1178
1262
  if (objectType.objectFlags & ts4.ObjectFlags.Reference) {
1179
1263
  const refType = objectType;
1180
- const targetName = refType.target?.symbol?.name ?? "";
1264
+ const targetName = refType.target.symbol.name;
1181
1265
  if (targetName === "Array" || targetName === "ReadonlyArray") {
1182
1266
  const typeArgs = checker.getTypeArguments(refType);
1183
1267
  const firstArg = typeArgs[0];
@@ -1194,7 +1278,7 @@ var expandTypeDetailed = (type, checker, depth = 0, state) => {
1194
1278
  return { text: checker.typeToString(type), unresolvedSymbols: [] };
1195
1279
  }
1196
1280
  }
1197
- const symbolName = type.symbol?.name || type.aliasSymbol?.name || "";
1281
+ const symbolName = type.symbol.name || (type.aliasSymbol?.name ?? "");
1198
1282
  if (SKIP_EXPANSION.has(symbolName)) {
1199
1283
  return { text: checker.typeToString(type), unresolvedSymbols: [] };
1200
1284
  }
@@ -1249,7 +1333,7 @@ var expandType = (type, checker, depth = 0) => {
1249
1333
  };
1250
1334
 
1251
1335
  // src/typeMap/extractors.ts
1252
- import { ROOT_DIR as ROOT_DIR3 } from "@luckystack/core";
1336
+ import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath3 } from "@luckystack/core";
1253
1337
  var TYPE_NAME_PATTERN = /\b[A-Z][A-Za-z0-9_]*\b/g;
1254
1338
  var KNOWN_GLOBAL_TYPE_NAMES = /* @__PURE__ */ new Set([
1255
1339
  "String",
@@ -1283,7 +1367,7 @@ var KNOWN_GLOBAL_TYPE_NAMES = /* @__PURE__ */ new Set([
1283
1367
  "JsonPrimitive"
1284
1368
  ]);
1285
1369
  var normalizeImportPath2 = (targetFilePath) => {
1286
- const fromDir = path5.join(ROOT_DIR3, "src", "_sockets");
1370
+ const fromDir = path5.dirname(getGeneratedSocketTypesPath3());
1287
1371
  const from = fromDir.replaceAll("\\", "/");
1288
1372
  const to = targetFilePath.replaceAll("\\", "/");
1289
1373
  const normalized = path5.posix.relative(from, to).replaceAll("\\", "/");
@@ -1338,7 +1422,7 @@ var findInterface = (sourceFile, name) => {
1338
1422
  };
1339
1423
  var getInterfacePropertyType = (iface, propertyName, checker) => {
1340
1424
  for (const member of iface.members) {
1341
- if (ts5.isPropertySignature(member) && member.name && ts5.isIdentifier(member.name) && member.name.text === propertyName && member.type) {
1425
+ if (ts5.isPropertySignature(member) && ts5.isIdentifier(member.name) && member.name.text === propertyName && member.type) {
1342
1426
  return checker.getTypeFromTypeNode(member.type);
1343
1427
  }
1344
1428
  }
@@ -1422,7 +1506,10 @@ var getInputTypeDetailsFromFile = (filePath) => {
1422
1506
  try {
1423
1507
  const program = getServerProgram();
1424
1508
  const sourceFile = program.getSourceFile(filePath);
1425
- if (!sourceFile) return { text: DEFAULT, unresolvedSymbols: [] };
1509
+ if (!sourceFile) {
1510
+ console.warn(`[TypeMapGenerator] getSourceFile miss for ${filePath} \u2014 input types will default to { }`);
1511
+ return { text: DEFAULT, unresolvedSymbols: [] };
1512
+ }
1426
1513
  const checker = program.getTypeChecker();
1427
1514
  const iface = findInterface(sourceFile, "ApiParams");
1428
1515
  if (!iface) return { text: DEFAULT, unresolvedSymbols: [] };
@@ -1561,10 +1648,10 @@ import path7 from "path";
1561
1648
  // src/typeMap/typeContext.ts
1562
1649
  import * as ts6 from "typescript";
1563
1650
  import path6 from "path";
1564
- import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath2 } from "@luckystack/core";
1651
+ import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath4 } from "@luckystack/core";
1565
1652
  var toGeneratedImportPath = (source, filePath) => {
1566
1653
  if (!source.startsWith(".")) return source;
1567
- const outputDir = path6.dirname(getGeneratedSocketTypesPath2());
1654
+ const outputDir = path6.dirname(getGeneratedSocketTypesPath4());
1568
1655
  const absoluteSource = path6.resolve(path6.dirname(filePath), source);
1569
1656
  let relPath = path6.relative(outputDir, absoluteSource).replaceAll("\\", "/");
1570
1657
  relPath = relPath.replace(/\.tsx?$/, "");
@@ -1651,7 +1738,7 @@ var sanitizeTypeAndCollectImports = ({
1651
1738
  };
1652
1739
 
1653
1740
  // src/typeMap/functionsMeta.ts
1654
- import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath3, getServerFunctionDirs } from "@luckystack/core";
1741
+ import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath5, getServerFunctionDirs } from "@luckystack/core";
1655
1742
  var stripDefaultValues = (params) => {
1656
1743
  return params.replaceAll(/\s*=(?!>)[^,)]+/g, "");
1657
1744
  };
@@ -1679,7 +1766,7 @@ var simplifyInferredType = (value) => {
1679
1766
  if (/\bRedis\b/.test(value)) return "Redis";
1680
1767
  return value;
1681
1768
  };
1682
- var getGeneratedFileDir = () => path7.dirname(getGeneratedSocketTypesPath3());
1769
+ var getGeneratedFileDir = () => path7.dirname(getGeneratedSocketTypesPath5());
1683
1770
  var relativizeModuleSpecifier = (specifier, sourceFilePath) => {
1684
1771
  if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
1685
1772
  return specifier;
@@ -2041,16 +2128,16 @@ var generateServerFunctions = (collectors) => {
2041
2128
  };
2042
2129
 
2043
2130
  // src/typeMapGenerator.ts
2044
- import { getSrcDir } from "@luckystack/core";
2131
+ import { getSrcDir as getSrcDir2 } from "@luckystack/core";
2045
2132
 
2046
2133
  // src/routeNamingValidation.ts
2047
2134
  import fs5 from "fs";
2048
2135
  import path8 from "path";
2049
- import { ROOT_DIR as ROOT_DIR4, validatePagePath } from "@luckystack/core";
2136
+ import { ROOT_DIR as ROOT_DIR3, validatePagePath } from "@luckystack/core";
2050
2137
  var normalizePath = (value) => {
2051
2138
  return value.replaceAll("\\", "/");
2052
2139
  };
2053
- var toRel = (absolute) => path8.relative(ROOT_DIR4, absolute).replaceAll("\\", "/");
2140
+ var toRel = (absolute) => path8.relative(ROOT_DIR3, absolute).replaceAll("\\", "/");
2054
2141
  var walkRouteFiles = (dir, results = []) => {
2055
2142
  const entries = fs5.readdirSync(dir, { withFileTypes: true });
2056
2143
  const apiSeg = apiMarkerSegment();
@@ -2308,7 +2395,7 @@ var collectDuplicatePageRoutes = (srcDir) => {
2308
2395
  const result = validatePagePath(relative);
2309
2396
  if (!result.valid || !result.route) continue;
2310
2397
  const list = routeToFiles.get(result.route) ?? [];
2311
- list.push(normalizePath(path8.relative(ROOT_DIR4, absoluteFilePath)));
2398
+ list.push(normalizePath(path8.relative(ROOT_DIR3, absoluteFilePath)));
2312
2399
  routeToFiles.set(result.route, list);
2313
2400
  }
2314
2401
  const issues = [];
@@ -2347,26 +2434,9 @@ var assertNoDuplicatePageRoutes = ({
2347
2434
  // src/typeMapGenerator.ts
2348
2435
  var namedImports = /* @__PURE__ */ new Map();
2349
2436
  var defaultImports = /* @__PURE__ */ new Map();
2350
- var generateTypeMapFile = (options = {}) => {
2351
- const { quiet = false } = options;
2352
- assertValidRouteNaming({
2353
- srcDir: getSrcDir(),
2354
- context: "generating API/sync type maps"
2355
- });
2356
- assertNoDuplicateNormalizedRouteKeys({
2357
- srcDir: getSrcDir(),
2358
- context: "generating API/sync type maps"
2359
- });
2360
- assertNoDuplicatePageRoutes({
2361
- srcDir: getSrcDir(),
2362
- context: "generating API/sync type maps"
2363
- });
2364
- invalidateProgramCache();
2365
- namedImports.clear();
2366
- defaultImports.clear();
2367
- const apiFiles = findAllApiFiles(getSrcDir());
2437
+ var collectApiTypes = (apiFiles, collectors) => {
2438
+ const { namedImports: namedImports2, unresolvedTypeAliases, quiet } = collectors;
2368
2439
  const typesByPage = /* @__PURE__ */ new Map();
2369
- const unresolvedTypeAliases = /* @__PURE__ */ new Set();
2370
2440
  if (!quiet) {
2371
2441
  console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2372
2442
  console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
@@ -2393,15 +2463,17 @@ var generateTypeMapFile = (options = {}) => {
2393
2463
  console.error(`[TypeMapGenerator] Unresolved API type (${pagePath}/${apiName}/${apiVersion}): ${symbol.name}`);
2394
2464
  continue;
2395
2465
  }
2396
- getOrInit(namedImports, symbol.importPath, () => /* @__PURE__ */ new Set()).add(symbol.name);
2466
+ getOrInit(namedImports2, symbol.importPath, () => /* @__PURE__ */ new Set()).add(symbol.name);
2397
2467
  }
2398
2468
  if (!quiet) {
2399
2469
  console.log(`[TypeMapGenerator] API: ${pagePath}/${apiName}/${apiVersion} (${httpMethod}${rateLimit === void 0 ? "" : `, rateLimit: ${rateLimit}`})`);
2400
2470
  }
2401
2471
  getOrInit(typesByPage, pagePath, () => /* @__PURE__ */ new Map()).set(`${apiName}@${apiVersion}`, { input: inputType, output: outputType, stream: streamType, method: httpMethod, rateLimit, auth, version: apiVersion, ...meta ? { meta } : {} });
2402
2472
  }
2403
- const syncServerFiles = findAllSyncServerFiles(getSrcDir());
2404
- const syncClientFiles = findAllSyncClientFiles(getSrcDir());
2473
+ return typesByPage;
2474
+ };
2475
+ var collectSyncTypes = (syncServerFiles, syncClientFiles, collectors) => {
2476
+ const { namedImports: namedImports2, unresolvedTypeAliases, quiet } = collectors;
2405
2477
  const syncTypesByPage = /* @__PURE__ */ new Map();
2406
2478
  if (!quiet) {
2407
2479
  console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
@@ -2454,7 +2526,7 @@ var generateTypeMapFile = (options = {}) => {
2454
2526
  console.error(`[TypeMapGenerator] Unresolved Sync type (${pagePath}/${syncName}/${syncVersion}): ${symbol.name}`);
2455
2527
  continue;
2456
2528
  }
2457
- getOrInit(namedImports, symbol.importPath, () => /* @__PURE__ */ new Set()).add(symbol.name);
2529
+ getOrInit(namedImports2, symbol.importPath, () => /* @__PURE__ */ new Set()).add(symbol.name);
2458
2530
  }
2459
2531
  if (!quiet) {
2460
2532
  console.log(`[TypeMapGenerator] Sync: ${pagePath}/${syncName}/${syncVersion} (server: ${!!serverFile}, client: ${!!clientFile})`);
@@ -2470,19 +2542,45 @@ var generateTypeMapFile = (options = {}) => {
2470
2542
  ...syncMeta ? { meta: syncMeta } : {}
2471
2543
  });
2472
2544
  }
2545
+ return syncTypesByPage;
2546
+ };
2547
+ var generateTypeMapFile = (options = {}) => {
2548
+ const { quiet = false } = options;
2549
+ assertValidRouteNaming({
2550
+ srcDir: getSrcDir2(),
2551
+ context: "generating API/sync type maps"
2552
+ });
2553
+ assertNoDuplicateNormalizedRouteKeys({
2554
+ srcDir: getSrcDir2(),
2555
+ context: "generating API/sync type maps"
2556
+ });
2557
+ assertNoDuplicatePageRoutes({
2558
+ srcDir: getSrcDir2(),
2559
+ context: "generating API/sync type maps"
2560
+ });
2561
+ invalidateProgramCache();
2562
+ namedImports.clear();
2563
+ defaultImports.clear();
2564
+ const unresolvedTypeAliases = /* @__PURE__ */ new Set();
2565
+ const collectors = { namedImports, unresolvedTypeAliases, quiet };
2566
+ const apiFiles = findAllApiFiles(getSrcDir2());
2567
+ const typesByPage = collectApiTypes(apiFiles, collectors);
2568
+ const syncServerFiles = findAllSyncServerFiles(getSrcDir2());
2569
+ const syncClientFiles = findAllSyncClientFiles(getSrcDir2());
2570
+ const syncTypesByPage = collectSyncTypes(syncServerFiles, syncClientFiles, collectors);
2473
2571
  const functionsInterface = generateServerFunctions({ namedImports, defaultImports });
2474
2572
  if (unresolvedTypeAliases.size > 0) {
2475
2573
  const unresolvedList = [...unresolvedTypeAliases].toSorted().join(", ");
2476
2574
  throw new Error(`[TypeMapGenerator] Aborting generation because unresolved type symbols were found: ${unresolvedList}`);
2477
2575
  }
2478
- const { content, docsData, schemasContent } = buildTypeMapArtifacts({
2576
+ const { content, docsData, schemasContent, diagnosticsData } = buildTypeMapArtifacts({
2479
2577
  typesByPage,
2480
2578
  syncTypesByPage,
2481
2579
  namedImports,
2482
2580
  defaultImports,
2483
2581
  functionsInterface
2484
2582
  });
2485
- writeTypeMapArtifacts({ content, docsData, schemasContent });
2583
+ writeTypeMapArtifacts({ content, docsData, schemasContent, diagnosticsData });
2486
2584
  };
2487
2585
 
2488
2586
  // src/templateRegistry.ts
@@ -2560,7 +2658,7 @@ registerDefaultTemplateRules();
2560
2658
  import fs6 from "fs";
2561
2659
  import path9 from "path";
2562
2660
  import { pathToFileURL } from "url";
2563
- import { tryCatch, getServerFunctionDirs as getServerFunctionDirs2, getSrcDir as getSrcDir2 } from "@luckystack/core";
2661
+ import { tryCatch, getServerFunctionDirs as getServerFunctionDirs2, getSrcDir as getSrcDir3 } from "@luckystack/core";
2564
2662
 
2565
2663
  // src/runtimeTypeResolver.ts
2566
2664
  import * as ts8 from "typescript";
@@ -2610,8 +2708,19 @@ var splitTopLevel = (value, splitter) => {
2610
2708
  let depthBrace = 0;
2611
2709
  let depthBracket = 0;
2612
2710
  let depthAngle = 0;
2711
+ let quote = null;
2613
2712
  let token = "";
2614
2713
  for (const char of value) {
2714
+ if (quote !== null) {
2715
+ if (char === quote) quote = null;
2716
+ token += char;
2717
+ continue;
2718
+ }
2719
+ if (char === "'" || char === '"' || char === "`") {
2720
+ quote = char;
2721
+ token += char;
2722
+ continue;
2723
+ }
2615
2724
  if (char === "(") depthParen += 1;
2616
2725
  if (char === ")") depthParen -= 1;
2617
2726
  if (char === "{") depthBrace += 1;
@@ -2619,7 +2728,7 @@ var splitTopLevel = (value, splitter) => {
2619
2728
  if (char === "[") depthBracket += 1;
2620
2729
  if (char === "]") depthBracket -= 1;
2621
2730
  if (char === "<") depthAngle += 1;
2622
- if (char === ">") depthAngle -= 1;
2731
+ if (char === ">") depthAngle = Math.max(0, depthAngle - 1);
2623
2732
  if (char === splitter && depthParen === 0 && depthBrace === 0 && depthBracket === 0 && depthAngle === 0) {
2624
2733
  if (token.trim()) items.push(token.trim());
2625
2734
  token = "";
@@ -2929,17 +3038,20 @@ var devSyncs = {};
2929
3038
  var devFunctions = {};
2930
3039
  var normalizePath2 = (value) => value.replaceAll("\\", "/");
2931
3040
  var mapApiPageLocation = (pageLocation) => {
2932
- return pageLocation ? pageLocation : "system";
3041
+ return pageLocation || "system";
3042
+ };
3043
+ var mapSyncPageLocation = (pageLocation) => {
3044
+ return pageLocation || "system";
2933
3045
  };
2934
3046
  var resolveApiRouteMetaFromPath = (filePath) => {
2935
3047
  const absolutePath = path9.resolve(filePath);
2936
3048
  const normalizedAbsolutePath = normalizePath2(absolutePath);
2937
- const normalizedSrcDir = normalizePath2(getSrcDir2());
3049
+ const normalizedSrcDir = normalizePath2(getSrcDir3());
2938
3050
  if (!normalizedAbsolutePath.startsWith(normalizedSrcDir) || !normalizedAbsolutePath.endsWith(".ts")) {
2939
3051
  return null;
2940
3052
  }
2941
3053
  const rules2 = getRoutingRules();
2942
- const relativePath = normalizePath2(path9.relative(getSrcDir2(), absolutePath));
3054
+ const relativePath = normalizePath2(path9.relative(getSrcDir3(), absolutePath));
2943
3055
  const segments = relativePath.split("/");
2944
3056
  const apiIndex = segments.indexOf(rules2.apiMarker);
2945
3057
  if (apiIndex === -1 || apiIndex === segments.length - 1) {
@@ -2961,12 +3073,12 @@ var resolveApiRouteMetaFromPath = (filePath) => {
2961
3073
  var resolveSyncRouteMetaFromPath = (filePath) => {
2962
3074
  const absolutePath = path9.resolve(filePath);
2963
3075
  const normalizedAbsolutePath = normalizePath2(absolutePath);
2964
- const normalizedSrcDir = normalizePath2(getSrcDir2());
3076
+ const normalizedSrcDir = normalizePath2(getSrcDir3());
2965
3077
  if (!normalizedAbsolutePath.startsWith(normalizedSrcDir) || !normalizedAbsolutePath.endsWith(".ts")) {
2966
3078
  return null;
2967
3079
  }
2968
3080
  const rules2 = getRoutingRules();
2969
- const relativePath = normalizePath2(path9.relative(getSrcDir2(), absolutePath));
3081
+ const relativePath = normalizePath2(path9.relative(getSrcDir3(), absolutePath));
2970
3082
  const segments = relativePath.split("/");
2971
3083
  const syncIndex = segments.indexOf(rules2.syncMarker);
2972
3084
  if (syncIndex === -1 || syncIndex === segments.length - 1) {
@@ -2982,7 +3094,8 @@ var resolveSyncRouteMetaFromPath = (filePath) => {
2982
3094
  const kind = match[1];
2983
3095
  const version = `v${match[2]}`;
2984
3096
  const syncName = rawSyncName.replace(rules2.syncVersionRegex, "");
2985
- const routeBaseKey = pageLocation ? `sync/${pageLocation}/${syncName}/${version}` : `sync/${syncName}/${version}`;
3097
+ const mappedPageLocation = mapSyncPageLocation(pageLocation);
3098
+ const routeBaseKey = `sync/${mappedPageLocation}/${syncName}/${version}`;
2986
3099
  return {
2987
3100
  routeKey: `${routeBaseKey}_${kind}`,
2988
3101
  kind,
@@ -2991,10 +3104,10 @@ var resolveSyncRouteMetaFromPath = (filePath) => {
2991
3104
  };
2992
3105
  var initializeAll = async () => {
2993
3106
  assertValidRouteNaming({
2994
- srcDir: getSrcDir2(),
3107
+ srcDir: getSrcDir3(),
2995
3108
  context: "starting dev server (npm run server)"
2996
3109
  });
2997
- const pageRouteIssues = collectDuplicatePageRoutes(getSrcDir2());
3110
+ const pageRouteIssues = collectDuplicatePageRoutes(getSrcDir3());
2998
3111
  if (pageRouteIssues.length > 0) {
2999
3112
  console.warn(formatDuplicatePageRouteIssues({
3000
3113
  issues: pageRouteIssues,
@@ -3011,6 +3124,7 @@ var collectTsFiles = (dir, relativeTo = "") => {
3011
3124
  const results = [];
3012
3125
  const entries = fs6.readdirSync(dir);
3013
3126
  for (const entry of entries) {
3127
+ if (entry === "node_modules") continue;
3014
3128
  const entryPath = path9.join(dir, entry);
3015
3129
  const relPath = relativeTo ? `${relativeTo}/${entry}` : entry;
3016
3130
  if (fs6.statSync(entryPath).isDirectory()) {
@@ -3044,7 +3158,7 @@ var resolveFunctionModule = (loadedModule, fileName) => {
3044
3158
  var initializeApis = async () => {
3045
3159
  for (const key of Object.keys(devApis)) delete devApis[key];
3046
3160
  clearRuntimeTypeResolverCache();
3047
- const srcFolder = fs6.readdirSync(getSrcDir2());
3161
+ const srcFolder = fs6.readdirSync(getSrcDir3());
3048
3162
  for (const file of srcFolder) {
3049
3163
  await scanApiFolder(file);
3050
3164
  }
@@ -3100,7 +3214,7 @@ var removeApiFromFile = (filePath) => {
3100
3214
  delete devApis[routeMeta.routeKey];
3101
3215
  };
3102
3216
  var scanApiFolder = async (file, basePath = "") => {
3103
- const fullPath = path9.join(getSrcDir2(), basePath, file);
3217
+ const fullPath = path9.join(getSrcDir3(), basePath, file);
3104
3218
  if (!fs6.statSync(fullPath).isDirectory()) return;
3105
3219
  if (!file.toLowerCase().endsWith("api")) {
3106
3220
  const subFolders = fs6.readdirSync(fullPath);
@@ -3155,7 +3269,7 @@ var scanApiFolder = async (file, basePath = "") => {
3155
3269
  var initializeSyncs = async () => {
3156
3270
  for (const key of Object.keys(devSyncs)) delete devSyncs[key];
3157
3271
  clearRuntimeTypeResolverCache();
3158
- const srcFolder = fs6.readdirSync(getSrcDir2());
3272
+ const srcFolder = fs6.readdirSync(getSrcDir3());
3159
3273
  for (const file of srcFolder) {
3160
3274
  await scanSyncFolder(file);
3161
3275
  }
@@ -3190,7 +3304,13 @@ var upsertSyncFromFile = async (filePath) => {
3190
3304
  main: resolvedSyncModule.main,
3191
3305
  auth: resolvedSyncModule.auth || {},
3192
3306
  inputType,
3193
- inputTypeFilePath: routeMeta.absolutePath
3307
+ inputTypeFilePath: routeMeta.absolutePath,
3308
+ //? Forward the per-route `validation` toggle + `errorFormatter` so the dev
3309
+ //? loader's sync entry shape matches the prod generator's. Dropping them
3310
+ //? made `validation: { input: 'skip' }` / a per-route errorFormatter work
3311
+ //? in prod but silently no-op in dev (QUA-013 / QUA-044).
3312
+ validation: resolvedSyncModule.validation,
3313
+ errorFormatter: resolvedSyncModule.errorFormatter
3194
3314
  };
3195
3315
  return;
3196
3316
  }
@@ -3210,7 +3330,7 @@ var removeSyncFromFile = (filePath) => {
3210
3330
  delete devSyncs[routeMeta.routeKey];
3211
3331
  };
3212
3332
  var scanSyncFolder = async (file, basePath = "") => {
3213
- const fullPath = path9.join(getSrcDir2(), basePath, file);
3333
+ const fullPath = path9.join(getSrcDir3(), basePath, file);
3214
3334
  if (!fs6.statSync(fullPath).isDirectory()) return;
3215
3335
  if (!file.toLowerCase().endsWith("sync")) {
3216
3336
  const subFolders = fs6.readdirSync(fullPath);
@@ -3235,7 +3355,8 @@ var scanSyncFolder = async (file, basePath = "") => {
3235
3355
  const kind = syncMatch[1];
3236
3356
  const version = `v${syncMatch[2]}`;
3237
3357
  const syncName = rawSyncFileName.replace(syncRules.syncVersionRegex, "");
3238
- const routeBaseKey = pageLocation ? `sync/${pageLocation}/${syncName}/${version}` : `sync/${syncName}/${version}`;
3358
+ const mappedPageLocation = mapSyncPageLocation(pageLocation);
3359
+ const routeBaseKey = `sync/${mappedPageLocation}/${syncName}/${version}`;
3239
3360
  const filePath = path9.resolve(path9.join(fullPath, relFile));
3240
3361
  const [fileError, fileResult] = await tryCatch(async () => importFile(filePath));
3241
3362
  if (fileError) {
@@ -3249,7 +3370,11 @@ var scanSyncFolder = async (file, basePath = "") => {
3249
3370
  main: resolvedSyncModule.main,
3250
3371
  auth: resolvedSyncModule.auth || {},
3251
3372
  inputType,
3252
- inputTypeFilePath: filePath
3373
+ inputTypeFilePath: filePath,
3374
+ //? Forward `validation` + `errorFormatter` so the dev loader matches the
3375
+ //? prod generator's sync entry shape (QUA-013 / QUA-044).
3376
+ validation: resolvedSyncModule.validation,
3377
+ errorFormatter: resolvedSyncModule.errorFormatter
3253
3378
  };
3254
3379
  } else {
3255
3380
  devSyncs[`${routeBaseKey}_client`] = resolvedSyncModule.main;
@@ -3315,18 +3440,19 @@ var scanFunctionsFolder = async (dir, rootDir, basePath = []) => {
3315
3440
 
3316
3441
  // src/hotReload.ts
3317
3442
  import { watch } from "chokidar";
3318
- import fs9 from "fs";
3319
- import path12 from "path";
3443
+ import fs10 from "fs";
3444
+ import path13 from "path";
3320
3445
 
3321
3446
  // src/templateInjector.ts
3322
3447
  import fs7 from "fs";
3323
3448
  import path10 from "path";
3324
3449
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
3325
- import { ROOT_DIR as ROOT_DIR5, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath4, getSrcDir as getSrcDir3, validatePagePath as validatePagePath2 } from "@luckystack/core";
3450
+ import * as ts9 from "typescript";
3451
+ import { ROOT_DIR as ROOT_DIR4, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath6, getSrcDir as getSrcDir4, validatePagePath as validatePagePath2 } from "@luckystack/core";
3326
3452
  var __filename = fileURLToPath(import.meta.url);
3327
3453
  var __dirname = path10.dirname(__filename);
3328
3454
  var templatesDir = path10.join(__dirname, "templates");
3329
- var getConsumerTemplatesDir = () => path10.join(ROOT_DIR5, ".luckystack", "templates");
3455
+ var getConsumerTemplatesDir = () => path10.join(ROOT_DIR4, ".luckystack", "templates");
3330
3456
  var templateContentFilenames = (kind) => {
3331
3457
  const builtIn = BUILT_IN_TEMPLATE_FILENAMES[kind];
3332
3458
  if (builtIn) return [builtIn];
@@ -3420,7 +3546,7 @@ var isVersionedSyncFile = (filePath) => {
3420
3546
  return isVersionedSyncFileName(filePath);
3421
3547
  };
3422
3548
  var getFileName = (filePath) => {
3423
- return path10.basename(filePath.replace(/\\/g, "/"));
3549
+ return path10.basename(filePath.replaceAll("\\", "/"));
3424
3550
  };
3425
3551
  var stripTsExtension = (fileName) => {
3426
3552
  return fileName.replace(/\.ts$/, "");
@@ -3464,7 +3590,7 @@ var getSyncFilenameReason = (fileName) => {
3464
3590
  return "Missing required sync kind and version suffix.";
3465
3591
  };
3466
3592
  var getRouteFilenameValidationMessage = (filePath) => {
3467
- const normalized = filePath.replace(/\\/g, "/");
3593
+ const normalized = filePath.replaceAll("\\", "/");
3468
3594
  const fileName = getFileName(normalized);
3469
3595
  if (isInApiFolder(normalized) && !isVersionedApiFile(normalized)) {
3470
3596
  const base = toApiBaseName(fileName);
@@ -3489,13 +3615,13 @@ var getRouteFilenameValidationMessage = (filePath) => {
3489
3615
  return null;
3490
3616
  };
3491
3617
  var getInvalidVersionMessage = (filePath) => {
3492
- const validationMessage = getRouteFilenameValidationMessage(filePath) || "Invalid route filename.";
3618
+ const validationMessage = getRouteFilenameValidationMessage(filePath) ?? "Invalid route filename.";
3493
3619
  return `// ${validationMessage}
3494
3620
  `;
3495
3621
  };
3496
3622
  var computeSrcRelativePath = (filePath) => {
3497
3623
  try {
3498
- const srcDir = getSrcDir3();
3624
+ const srcDir = getSrcDir4();
3499
3625
  const absolute = path10.resolve(filePath);
3500
3626
  const normalizedSrc = srcDir.replaceAll("\\", "/");
3501
3627
  const normalizedAbs = absolute.replaceAll("\\", "/");
@@ -3549,7 +3675,9 @@ var hasPairedFile = (filePath) => {
3549
3675
  var extractSyncPagePath2 = (filePath) => {
3550
3676
  const normalized = filePath.replaceAll("\\", "/");
3551
3677
  const match = /src\/(.+?)\/_sync\//.exec(normalized);
3552
- return match?.[1] ?? "";
3678
+ if (match?.[1]) return match[1];
3679
+ if (/(?:^|\/)src\/_sync\//.test(normalized)) return "system";
3680
+ return "";
3553
3681
  };
3554
3682
  var extractSyncName2 = (filePath) => {
3555
3683
  const normalized = filePath.replaceAll("\\", "/");
@@ -3565,11 +3693,16 @@ var extractClientInputFromFile = (filePath) => {
3565
3693
  const content = fs7.readFileSync(filePath, "utf8");
3566
3694
  const syncParamsMatch = /interface\s+SyncParams\s*\{/.exec(content);
3567
3695
  if (!syncParamsMatch) return null;
3568
- const clientInputMatch = /clientInput\s*:\s*\{/.exec(content);
3696
+ const searchRegion = content.slice(syncParamsMatch.index + syncParamsMatch[0].length);
3697
+ const clientInputMatch = /clientInput\s*:\s*\{/.exec(searchRegion);
3569
3698
  if (!clientInputMatch) return null;
3570
- const startIndex = content.indexOf("{", clientInputMatch.index);
3699
+ const startIndex = content.indexOf(
3700
+ "{",
3701
+ syncParamsMatch.index + syncParamsMatch[0].length + clientInputMatch.index
3702
+ );
3703
+ if (startIndex === -1) return null;
3571
3704
  let depth = 0;
3572
- let endIndex = startIndex;
3705
+ let endIndex = -1;
3573
3706
  for (let i = startIndex; i < content.length; i++) {
3574
3707
  if (content[i] === "{") depth++;
3575
3708
  else if (content[i] === "}") depth--;
@@ -3578,7 +3711,8 @@ var extractClientInputFromFile = (filePath) => {
3578
3711
  break;
3579
3712
  }
3580
3713
  }
3581
- return content.substring(startIndex, endIndex + 1);
3714
+ if (endIndex === -1) return null;
3715
+ return content.slice(startIndex, endIndex + 1);
3582
3716
  } catch (error) {
3583
3717
  console.error(`[TemplateInjector] Error extracting clientInput from ${filePath}:`, error);
3584
3718
  return null;
@@ -3586,7 +3720,7 @@ var extractClientInputFromFile = (filePath) => {
3586
3720
  };
3587
3721
  var extractClientInputFromGeneratedTypes = (pagePath, syncName) => {
3588
3722
  try {
3589
- const generatedTypesPath = getGeneratedSocketTypesPath4();
3723
+ const generatedTypesPath = getGeneratedSocketTypesPath6();
3590
3724
  const content = fs7.readFileSync(generatedTypesPath, "utf8");
3591
3725
  const escapeRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
3592
3726
  const escapedPagePath = escapeRegex(pagePath);
@@ -3610,7 +3744,7 @@ var extractClientInputFromGeneratedTypes = (pagePath, syncName) => {
3610
3744
  const braceStart = content.indexOf("{", searchStart - 1);
3611
3745
  if (braceStart === -1) return null;
3612
3746
  let depth = 0;
3613
- let endIndex = braceStart;
3747
+ let endIndex = -1;
3614
3748
  for (let i = braceStart; i < content.length; i++) {
3615
3749
  if (content[i] === "{") depth++;
3616
3750
  else if (content[i] === "}") depth--;
@@ -3619,7 +3753,8 @@ var extractClientInputFromGeneratedTypes = (pagePath, syncName) => {
3619
3753
  break;
3620
3754
  }
3621
3755
  }
3622
- const extracted = content.substring(braceStart, endIndex + 1);
3756
+ if (endIndex === -1) return null;
3757
+ const extracted = content.slice(braceStart, endIndex + 1);
3623
3758
  console.log(`[TemplateInjector] Extracted clientInput types: ${extracted}`);
3624
3759
  return extracted;
3625
3760
  } catch (error) {
@@ -3629,6 +3764,18 @@ var extractClientInputFromGeneratedTypes = (pagePath, syncName) => {
3629
3764
  };
3630
3765
  var calculateRelativePath = (filePath) => {
3631
3766
  const normalized = filePath.replaceAll("\\", "/");
3767
+ const srcRelative = computeSrcRelativePath(filePath);
3768
+ if (srcRelative !== null) {
3769
+ try {
3770
+ const srcDepthFromRoot = path10.relative(ROOT_DIR4, getSrcDir4()).replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0).length;
3771
+ const dirSegmentsAfterSrc = Math.max(
3772
+ 0,
3773
+ srcRelative.split("/").filter((segment) => segment.length > 0).length - 1
3774
+ );
3775
+ return "../".repeat(srcDepthFromRoot + dirSegmentsAfterSrc);
3776
+ } catch {
3777
+ }
3778
+ }
3632
3779
  const srcIndex = normalized.indexOf("src/");
3633
3780
  if (srcIndex === -1) {
3634
3781
  console.warn(`[TemplateInjector] Could not find /src/ in path: ${filePath}`);
@@ -3638,58 +3785,68 @@ var calculateRelativePath = (filePath) => {
3638
3785
  const segments = relativePath.split("/").filter((s) => s.length > 0).length;
3639
3786
  return "../".repeat(segments);
3640
3787
  };
3641
- var getTemplate = (filePath) => {
3642
- let fileKind;
3643
- let hasPairedServer = false;
3644
- let srcRelativePath = null;
3788
+ var classifyFile = (filePath) => {
3645
3789
  if (isInApiFolder(filePath)) {
3646
- fileKind = "api";
3647
- } else if (isInSyncFolder(filePath)) {
3790
+ return { fileKind: "api", hasPairedServer: false, srcRelativePath: null };
3791
+ }
3792
+ if (isInSyncFolder(filePath)) {
3648
3793
  if (isSyncServerFile(filePath)) {
3649
- fileKind = "sync_server";
3650
- } else if (isSyncClientFile(filePath)) {
3651
- fileKind = "sync_client";
3652
- hasPairedServer = hasPairedFile(filePath);
3653
- } else {
3654
- console.log(`[TemplateInjector] Unknown sync file type: ${filePath}`);
3655
- return null;
3794
+ return { fileKind: "sync_server", hasPairedServer: false, srcRelativePath: null };
3656
3795
  }
3657
- } else if (isPageFile(filePath)) {
3658
- fileKind = "page";
3659
- srcRelativePath = computeSrcRelativePath(filePath);
3660
- if (srcRelativePath !== null) {
3661
- const placement = validatePagePath2(srcRelativePath);
3662
- if (!placement.valid) {
3663
- return getInvalidPagePlacementMessage(filePath, placement.reason ?? "invalid placement", srcRelativePath);
3664
- }
3796
+ if (isSyncClientFile(filePath)) {
3797
+ return { fileKind: "sync_client", hasPairedServer: hasPairedFile(filePath), srcRelativePath: null };
3665
3798
  }
3666
- } else {
3799
+ console.log(`[TemplateInjector] Unknown sync file type: ${filePath}`);
3667
3800
  return null;
3668
3801
  }
3669
- const ctx = { filePath, fileKind, hasPairedServer, srcRelativePath };
3670
- const templateKind = resolveTemplateKind(ctx);
3671
- if (!templateKind) {
3672
- console.log(`[TemplateInjector] No template rule matched for ${filePath} (fileKind=${fileKind})`);
3673
- return null;
3802
+ if (isPageFile(filePath)) {
3803
+ return { fileKind: "page", hasPairedServer: false, srcRelativePath: computeSrcRelativePath(filePath) };
3674
3804
  }
3675
- let content = resolveTemplateContent(templateKind);
3676
- if (content === null) return null;
3805
+ return null;
3806
+ };
3807
+ var checkPagePlacement = (filePath, srcRelativePath) => {
3808
+ if (srcRelativePath === null) return null;
3809
+ const placement = validatePagePath2(srcRelativePath);
3810
+ if (!placement.valid) {
3811
+ return getInvalidPagePlacementMessage(filePath, placement.reason ?? "invalid placement", srcRelativePath);
3812
+ }
3813
+ return null;
3814
+ };
3815
+ var applyTemplatePlaceholders = (content, filePath, fileKind) => {
3677
3816
  const relPath = calculateRelativePath(filePath);
3678
3817
  const pragmaPattern = /\/\/\s*@ts-(?:ignore|expect-error).*\r?\n(.*)\{\{REL_PATH\}\}/g;
3679
- content = content.replaceAll(pragmaPattern, (_, prefix) => `${prefix}${relPath}`);
3680
- content = content.replaceAll("{{REL_PATH}}", relPath);
3818
+ let result = content.replaceAll(pragmaPattern, (_, prefix) => `${prefix}${relPath}`);
3819
+ result = result.replaceAll("{{REL_PATH}}", relPath);
3681
3820
  if (fileKind === "sync_client" || fileKind === "sync_server") {
3682
3821
  const pagePath = extractSyncPagePath2(filePath);
3683
3822
  const syncName = extractSyncName2(filePath);
3684
- content = content.replaceAll("{{PAGE_PATH}}", pagePath);
3685
- content = content.replaceAll("{{SYNC_NAME}}", syncName);
3823
+ result = result.replaceAll("{{PAGE_PATH}}", pagePath);
3824
+ result = result.replaceAll("{{SYNC_NAME}}", syncName);
3686
3825
  }
3687
- return content;
3826
+ return result;
3827
+ };
3828
+ var getTemplate = (filePath) => {
3829
+ const classification = classifyFile(filePath);
3830
+ if (!classification) return null;
3831
+ const { fileKind, hasPairedServer, srcRelativePath } = classification;
3832
+ if (fileKind === "page") {
3833
+ const placementWarning = checkPagePlacement(filePath, srcRelativePath);
3834
+ if (placementWarning !== null) return placementWarning;
3835
+ }
3836
+ const ctx = { filePath, fileKind, hasPairedServer, srcRelativePath };
3837
+ const templateKind = resolveTemplateKind(ctx);
3838
+ if (!templateKind) {
3839
+ console.log(`[TemplateInjector] No template rule matched for ${filePath} (fileKind=${fileKind})`);
3840
+ return null;
3841
+ }
3842
+ const rawContent = resolveTemplateContent(templateKind);
3843
+ if (rawContent === null) return null;
3844
+ return applyTemplatePlaceholders(rawContent, filePath, fileKind);
3688
3845
  };
3689
3846
  var injectTemplate = async (filePath) => {
3690
3847
  await ensureConsumerTemplateConfigLoaded();
3691
3848
  if (getRouteFilenameValidationMessage(filePath)) {
3692
- fs7.writeFileSync(filePath, getInvalidVersionMessage(filePath), "utf-8");
3849
+ fs7.writeFileSync(filePath, getInvalidVersionMessage(filePath), "utf8");
3693
3850
  console.log(`[TemplateInjector] Invalid route filename, injected guidance: ${filePath}`);
3694
3851
  return true;
3695
3852
  }
@@ -3698,7 +3855,7 @@ var injectTemplate = async (filePath) => {
3698
3855
  return false;
3699
3856
  }
3700
3857
  try {
3701
- fs7.writeFileSync(filePath, template, "utf-8");
3858
+ fs7.writeFileSync(filePath, template, "utf8");
3702
3859
  console.log(`[TemplateInjector] Injected template into: ${filePath}`);
3703
3860
  return true;
3704
3861
  } catch (error) {
@@ -3706,62 +3863,142 @@ var injectTemplate = async (filePath) => {
3706
3863
  return false;
3707
3864
  }
3708
3865
  };
3709
- var shouldInjectTemplate = (filePath) => {
3866
+ var shouldInjectTemplate = (filePath, options = {}) => {
3710
3867
  const { disableTemplateInjection } = getRoutingRules();
3711
- if (disableTemplateInjection && disableTemplateInjection(filePath)) {
3868
+ if (disableTemplateInjection?.(filePath)) {
3712
3869
  return false;
3713
3870
  }
3714
3871
  if (!(isInApiFolder(filePath) || isInSyncFolder(filePath) || isPageFile(filePath))) {
3715
3872
  return false;
3716
3873
  }
3717
- return isEmptyFile(filePath) || isCommentOnlyFile(filePath);
3874
+ if (isEmptyFile(filePath)) return true;
3875
+ if (options.isNewFile) return isCommentOnlyFile(filePath);
3876
+ return false;
3718
3877
  };
3719
- var updateClientFileForPairedServer = async (clientFilePath) => {
3720
- try {
3721
- const pagePath = extractSyncPagePath2(clientFilePath);
3722
- const syncName = extractSyncName2(clientFilePath);
3723
- let content = fs7.readFileSync(clientFilePath, "utf8");
3724
- if (!content.includes("SyncClientInput")) {
3725
- content = content.replace(
3726
- /import \{([^}]+)\} from ['"]([^'"]*apiTypes\.generated)['"]/,
3727
- (_match, imports, path13) => {
3728
- return `import {${imports}, SyncClientInput, SyncServerOutput } from '${path13}'`;
3729
- }
3730
- );
3878
+ var ensureImportedTypes = (content, clientFilePath) => {
3879
+ if (content.includes("SyncClientInput")) return content;
3880
+ let result = content.replace(
3881
+ /import \{([^}]+)\} from ['"]([^'"]*apiTypes\.generated)['"]/,
3882
+ (_match, imports, importPath) => {
3883
+ return `import {${imports}, SyncClientInput, SyncServerOutput } from '${importPath}'`;
3731
3884
  }
3732
- if (!content.includes("type PagePath")) {
3733
- const importEndMatch = content.match(/import .+?;[\r\n]+/g);
3734
- if (importEndMatch) {
3735
- const lastImport = importEndMatch.at(-1);
3736
- if (!lastImport) {
3737
- return false;
3738
- }
3739
- const lastImportEnd = content.lastIndexOf(lastImport) + lastImport.length;
3740
- const typeAliases = `
3885
+ );
3886
+ if (!result.includes("SyncClientInput")) {
3887
+ const relPath = calculateRelativePath(clientFilePath);
3888
+ result = `import { SyncClientInput, SyncServerOutput } from '${relPath}src/_sockets/apiTypes.generated';
3889
+ ` + result;
3890
+ }
3891
+ return result;
3892
+ };
3893
+ var insertTypeAliases = (content, pagePath, syncName) => {
3894
+ if (content.includes("type PagePath")) return content;
3895
+ const importEndMatch = content.match(/import .+?;[\r\n]+/g);
3896
+ if (!importEndMatch) return content;
3897
+ const lastImport = importEndMatch.at(-1);
3898
+ if (!lastImport) return content;
3899
+ const lastImportEnd = content.lastIndexOf(lastImport) + lastImport.length;
3900
+ const typeAliases = `
3741
3901
  // Types are imported from the generated file based on the _server.ts definition
3742
3902
  type PagePath = '${pagePath}';
3743
3903
  type SyncName = '${syncName}';
3744
3904
  `;
3745
- content = content.slice(0, lastImportEnd) + typeAliases + content.slice(lastImportEnd);
3746
- }
3905
+ return content.slice(0, lastImportEnd) + typeAliases + content.slice(lastImportEnd);
3906
+ };
3907
+ var rewriteClientInputType = (content) => {
3908
+ return content.replace(
3909
+ /^(\s*)clientInput:\s*\{[^}]*\}/m,
3910
+ "$1clientInput: SyncClientInput<PagePath, SyncName>"
3911
+ );
3912
+ };
3913
+ var rewriteClientInputTypeAst = (content, sourceFileName) => {
3914
+ const parseSource = (src) => ts9.createSourceFile(
3915
+ sourceFileName,
3916
+ src,
3917
+ ts9.ScriptTarget.Latest,
3918
+ /* setParentNodes */
3919
+ true,
3920
+ ts9.ScriptKind.TS
3921
+ );
3922
+ const sourceFile = parseSource(content);
3923
+ let syncParamsInterface;
3924
+ for (const stmt of sourceFile.statements) {
3925
+ if (ts9.isInterfaceDeclaration(stmt) && stmt.name.text === "SyncParams") {
3926
+ syncParamsInterface = stmt;
3927
+ break;
3928
+ }
3929
+ }
3930
+ if (!syncParamsInterface) return null;
3931
+ let clientInputMember;
3932
+ for (const member of syncParamsInterface.members) {
3933
+ if (ts9.isPropertySignature(member) && ts9.isIdentifier(member.name) && member.name.text === "clientInput") {
3934
+ clientInputMember = member;
3935
+ break;
3936
+ }
3937
+ }
3938
+ if (!clientInputMember?.type) return null;
3939
+ if (!ts9.isTypeLiteralNode(clientInputMember.type)) return null;
3940
+ const typeNode = clientInputMember.type;
3941
+ const start = typeNode.getStart(sourceFile);
3942
+ const end = typeNode.getEnd();
3943
+ const rewritten = `${content.slice(0, start)}SyncClientInput<PagePath, SyncName>${content.slice(end)}`;
3944
+ const reparsedFile = parseSource(rewritten);
3945
+ const verifyProgram = ts9.createProgram({
3946
+ rootNames: [sourceFileName],
3947
+ options: { noResolve: true, skipLibCheck: true },
3948
+ // Supply the in-memory source so `ts.createProgram` never touches the disk.
3949
+ host: {
3950
+ ...ts9.createCompilerHost({}),
3951
+ getSourceFile: (name) => name === sourceFileName ? reparsedFile : void 0,
3952
+ fileExists: (name) => name === sourceFileName,
3953
+ readFile: (name) => name === sourceFileName ? rewritten : void 0
3747
3954
  }
3748
- content = content.replace(
3749
- /^(\s*)clientInput:\s*\{[^}]*\}/m,
3750
- "$1clientInput: SyncClientInput<PagePath, SyncName>"
3955
+ });
3956
+ const syntaxErrors = verifyProgram.getSyntacticDiagnostics(reparsedFile);
3957
+ if (syntaxErrors.length > 0) {
3958
+ console.log(
3959
+ `[TemplateInjector] AST rewrite produced invalid TypeScript for ${sourceFileName} \u2014 leaving untouched`,
3960
+ "yellow"
3751
3961
  );
3752
- if (!content.includes("serverOutput:")) {
3753
- content = content.replace(
3754
- /^(\s*)(clientInput:\s*SyncClientInput<PagePath, SyncName>);?\s*$/m,
3755
- "$1$2;\n$1serverOutput: SyncServerOutput<PagePath, SyncName>;"
3756
- );
3757
- }
3758
- if (content.includes("main") && !/\{\s*[^}]*serverOutput[^}]*\}\s*:\s*SyncParams/.test(content)) {
3759
- content = content.replace(
3760
- /\{\s*([^}]*?clientInput)([^}]*)\}\s*:\s*SyncParams/,
3761
- "{ $1, serverOutput$2 }: SyncParams"
3962
+ return null;
3963
+ }
3964
+ return rewritten;
3965
+ };
3966
+ var addServerOutputToParams = (content) => {
3967
+ if (content.includes("serverOutput:")) return content;
3968
+ return content.replace(
3969
+ /^(\s*)(clientInput:\s*SyncClientInput<PagePath, SyncName>);?\s*$/m,
3970
+ "$1$2;\n$1serverOutput: SyncServerOutput<PagePath, SyncName>;"
3971
+ );
3972
+ };
3973
+ var addServerOutputToDestructuring = (content) => {
3974
+ if (!content.includes("main") || /\{\s*[^}]*serverOutput[^}]*\}\s*:\s*SyncParams/.test(content)) {
3975
+ return content;
3976
+ }
3977
+ return content.replace(
3978
+ /\{\s*([^}]*?clientInput)([^}]*)\}\s*:\s*SyncParams/,
3979
+ "{ $1, serverOutput$2 }: SyncParams"
3980
+ );
3981
+ };
3982
+ var updateClientFileForPairedServer = async (clientFilePath) => {
3983
+ try {
3984
+ const pagePath = extractSyncPagePath2(clientFilePath);
3985
+ const syncName = extractSyncName2(clientFilePath);
3986
+ let content = fs7.readFileSync(clientFilePath, "utf8");
3987
+ content = ensureImportedTypes(content, clientFilePath);
3988
+ content = insertTypeAliases(content, pagePath, syncName);
3989
+ const astResult = rewriteClientInputTypeAst(content, path10.basename(clientFilePath));
3990
+ if (astResult === null) {
3991
+ console.log(
3992
+ `[TemplateInjector] AST clientInput rewrite unavailable for ${clientFilePath} \u2014 using regex fallback`,
3993
+ "yellow"
3762
3994
  );
3995
+ content = rewriteClientInputType(content);
3996
+ } else {
3997
+ content = astResult;
3763
3998
  }
3764
- fs7.writeFileSync(clientFilePath, content, "utf-8");
3999
+ content = addServerOutputToParams(content);
4000
+ content = addServerOutputToDestructuring(content);
4001
+ fs7.writeFileSync(clientFilePath, content, "utf8");
3765
4002
  console.log(`[TemplateInjector] Updated client file to use paired types (preserved code): ${clientFilePath}`);
3766
4003
  return true;
3767
4004
  } catch (error) {
@@ -3769,34 +4006,39 @@ type SyncName = '${syncName}';
3769
4006
  return false;
3770
4007
  }
3771
4008
  };
4009
+ var inlineClientInputType = (content, clientInputTypes) => {
4010
+ return content.replace(
4011
+ /^(\s*)clientInput:\s*SyncClientInput<[^>]+>/m,
4012
+ (_match, indent) => `${indent}clientInput: ${clientInputTypes}`
4013
+ ).replace(
4014
+ /^(\s*)clientInput:\s*\{[^}]*\}/m,
4015
+ (_match, indent) => `${indent}clientInput: ${clientInputTypes}`
4016
+ );
4017
+ };
4018
+ var removeServerOutputFromParams = (content) => {
4019
+ return content.replace(/^[ \t]*serverOutput:\s*SyncServerOutput<[^>]+>;?\s*\r?\n?/m, "").replace(/^[ \t]*serverOutput:\s*\{[^}]*\};?\s*\r?\n?/m, "");
4020
+ };
4021
+ var removeServerOutputFromDestructuring = (content) => {
4022
+ return content.replaceAll(/,\s*serverOutput(?=\s*[,}])/g, "").replaceAll(/serverOutput\s*,\s*/g, "");
4023
+ };
4024
+ var removeGeneratedTypeImports = (content) => {
4025
+ return content.replaceAll(/,\s*SyncClientInput(?=\s*[,}])/g, "").replaceAll(/,\s*SyncServerOutput(?=\s*[,}])/g, "");
4026
+ };
4027
+ var removeTypeAliases = (content) => {
4028
+ let result = content.replaceAll(/\/\/\s*Types are imported.*\n?/g, "");
4029
+ result = result.replaceAll(/type PagePath = '[^']*';\s*\n?/g, "");
4030
+ return result.replaceAll(/type SyncName = '[^']*';\s*\n?/g, "");
4031
+ };
3772
4032
  var updateClientFileForDeletedServer = async (clientFilePath, clientInputTypes) => {
3773
4033
  try {
3774
4034
  let content = fs7.readFileSync(clientFilePath, "utf8");
3775
- content = content.replace(
3776
- /^(\s*)clientInput:\s*SyncClientInput<[^>]+>/m,
3777
- `$1clientInput: ${clientInputTypes}`
3778
- );
3779
- content = content.replace(
3780
- /^(\s*)clientInput:\s*\{[^}]*\}/m,
3781
- `$1clientInput: ${clientInputTypes}`
3782
- );
3783
- content = content.replace(
3784
- /^[ \t]*serverOutput:\s*SyncServerOutput<[^>]+>;?\s*\r?\n?/m,
3785
- ""
3786
- );
3787
- content = content.replace(
3788
- /^[ \t]*serverOutput:\s*\{[^}]*\};?\s*\r?\n?/m,
3789
- ""
3790
- );
3791
- content = content.replaceAll(/,\s*serverOutput(?=\s*[,}])/g, "");
3792
- content = content.replaceAll(/serverOutput\s*,\s*/g, "");
3793
- content = content.replaceAll(/,\s*SyncClientInput(?=\s*[,}])/g, "");
3794
- content = content.replaceAll(/,\s*SyncServerOutput(?=\s*[,}])/g, "");
3795
- content = content.replaceAll(/\/\/\s*Types are imported.*\n?/g, "");
3796
- content = content.replaceAll(/type PagePath = '[^']*';\s*\n?/g, "");
3797
- content = content.replaceAll(/type SyncName = '[^']*';\s*\n?/g, "");
4035
+ content = inlineClientInputType(content, clientInputTypes);
4036
+ content = removeServerOutputFromParams(content);
4037
+ content = removeServerOutputFromDestructuring(content);
4038
+ content = removeGeneratedTypeImports(content);
4039
+ content = removeTypeAliases(content);
3798
4040
  content = content.replaceAll(/\n{3,}/g, "\n\n");
3799
- fs7.writeFileSync(clientFilePath, content, "utf-8");
4041
+ fs7.writeFileSync(clientFilePath, content, "utf8");
3800
4042
  console.log(`[TemplateInjector] Updated client file for deleted server (preserved code): ${clientFilePath}`);
3801
4043
  return true;
3802
4044
  } catch (error) {
@@ -3820,9 +4062,9 @@ var injectServerTemplateWithClientInput = async (serverFilePath, clientInputType
3820
4062
  content = content.replaceAll("{{REL_PATH}}", relPath);
3821
4063
  content = content.replace(
3822
4064
  /clientInput:\s*\{[^}]*\}/s,
3823
- `clientInput: ${clientInputTypes}`
4065
+ (_match) => `clientInput: ${clientInputTypes}`
3824
4066
  );
3825
- fs7.writeFileSync(serverFilePath, content, "utf-8");
4067
+ fs7.writeFileSync(serverFilePath, content, "utf8");
3826
4068
  console.log(`[TemplateInjector] Injected server template with clientInput: ${serverFilePath}`);
3827
4069
  return true;
3828
4070
  } catch (error) {
@@ -3834,7 +4076,7 @@ var injectServerTemplateWithClientInput = async (serverFilePath, clientInputType
3834
4076
  // src/importDependencyGraph.ts
3835
4077
  import fs8 from "fs";
3836
4078
  import path11 from "path";
3837
- import { ROOT_DIR as ROOT_DIR6, getServerFunctionDirs as getServerFunctionDirs3, getSharedDir, getSrcDir as getSrcDir4 } from "@luckystack/core";
4079
+ import { ROOT_DIR as ROOT_DIR5, getServerFunctionDirs as getServerFunctionDirs3, getSharedDir, getSrcDir as getSrcDir5 } from "@luckystack/core";
3838
4080
  var SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
3839
4081
  var normalizePath3 = (value) => path11.resolve(value).replaceAll("\\", "/");
3840
4082
  var isSupportedScriptFile = (value) => {
@@ -3854,15 +4096,26 @@ var existsDirectory = (value) => {
3854
4096
  return false;
3855
4097
  }
3856
4098
  };
3857
- var collectScriptFiles = (dir, output) => {
4099
+ var collectScriptFiles = (dir, output, visited = /* @__PURE__ */ new Set()) => {
3858
4100
  if (!existsDirectory(dir)) {
3859
4101
  return;
3860
4102
  }
3861
- const entries = fs8.readdirSync(dir);
4103
+ let realDir;
4104
+ try {
4105
+ realDir = fs8.realpathSync(dir);
4106
+ } catch {
4107
+ return;
4108
+ }
4109
+ if (visited.has(realDir)) return;
4110
+ visited.add(realDir);
4111
+ const entries = fs8.readdirSync(dir, { withFileTypes: true });
3862
4112
  for (const entry of entries) {
3863
- const fullPath = path11.join(dir, entry);
3864
- if (existsDirectory(fullPath)) {
3865
- collectScriptFiles(fullPath, output);
4113
+ if (entry.name === "node_modules") continue;
4114
+ const fullPath = path11.join(dir, entry.name);
4115
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
4116
+ if (existsDirectory(fullPath)) {
4117
+ collectScriptFiles(fullPath, output, visited);
4118
+ }
3866
4119
  continue;
3867
4120
  }
3868
4121
  const normalizedPath = normalizePath3(fullPath);
@@ -3889,8 +4142,8 @@ var extractImportSpecifiers = (filePath) => {
3889
4142
  try {
3890
4143
  const source = fs8.readFileSync(filePath, "utf8");
3891
4144
  const specifiers = /* @__PURE__ */ new Set();
3892
- const importExportRegex = /(?:import|export)\s+(?:[^'"\n]*?\s+from\s+)?['"]([^'"\n]+)['"]/g;
3893
- const dynamicImportRegex = /import\(\s*['"]([^'"\n]+)['"]\s*\)/g;
4145
+ const importExportRegex = /(?:import|export)\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
4146
+ const dynamicImportRegex = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
3894
4147
  let match = null;
3895
4148
  while ((match = importExportRegex.exec(source)) !== null) {
3896
4149
  if (match[1]) specifiers.add(match[1]);
@@ -3935,7 +4188,7 @@ var resolveImportToFile = (importerPath, specifier) => {
3935
4188
  return null;
3936
4189
  }
3937
4190
  if (specifier === "config") {
3938
- return tryResolveWithExtensions(path11.join(ROOT_DIR6, "config"));
4191
+ return tryResolveWithExtensions(path11.join(ROOT_DIR5, "config"));
3939
4192
  }
3940
4193
  if (specifier.startsWith("./") || specifier.startsWith("../")) {
3941
4194
  const relativeBase = path11.resolve(path11.dirname(importerPath), specifier);
@@ -3943,7 +4196,7 @@ var resolveImportToFile = (importerPath, specifier) => {
3943
4196
  }
3944
4197
  if (specifier.startsWith("src/") || specifier.startsWith("@/")) {
3945
4198
  const sourcePath = specifier.startsWith("@/") ? specifier.slice(2) : specifier.slice(4);
3946
- return tryResolveWithExtensions(path11.join(getSrcDir4(), sourcePath));
4199
+ return tryResolveWithExtensions(path11.join(getSrcDir5(), sourcePath));
3947
4200
  }
3948
4201
  if (specifier.startsWith("shared/")) {
3949
4202
  return tryResolveWithExtensions(path11.join(getSharedDir(), specifier.slice(7)));
@@ -3959,12 +4212,12 @@ var collectScopedFiles = () => {
3959
4212
  return scopedFilesCache.files;
3960
4213
  }
3961
4214
  const files = /* @__PURE__ */ new Set();
3962
- collectScriptFiles(getSrcDir4(), files);
4215
+ collectScriptFiles(getSrcDir5(), files);
3963
4216
  collectScriptFiles(getSharedDir(), files);
3964
4217
  for (const dir of getServerFunctionDirs3()) {
3965
4218
  collectScriptFiles(dir, files);
3966
4219
  }
3967
- const configFile = tryResolveWithExtensions(path11.join(ROOT_DIR6, "config"));
4220
+ const configFile = tryResolveWithExtensions(path11.join(ROOT_DIR5, "config"));
3968
4221
  if (configFile) {
3969
4222
  files.add(configFile);
3970
4223
  }
@@ -4011,37 +4264,46 @@ var findDependentRouteFiles = (changedFilePath) => {
4011
4264
  return affectedRoutes;
4012
4265
  };
4013
4266
 
4267
+ // src/prismaClientCheck.ts
4268
+ import fs9 from "fs";
4269
+ import path12 from "path";
4270
+ import { spawn } from "child_process";
4271
+ import { ROOT_DIR as ROOT_DIR6, tryCatch as tryCatch2 } from "@luckystack/core";
4272
+ var GENERATED_CLIENT_DIR = path12.join(ROOT_DIR6, "node_modules", ".prisma", "client");
4273
+ var DEFAULT_SCHEMA_PATH = path12.join(ROOT_DIR6, "prisma", "schema.prisma");
4274
+ var isPrismaClientMissing = () => fs9.existsSync(DEFAULT_SCHEMA_PATH) && !fs9.existsSync(GENERATED_CLIENT_DIR);
4275
+ var runPrismaGenerate = async () => tryCatch2(
4276
+ () => new Promise((resolve, reject) => {
4277
+ const child = spawn("npx", ["prisma", "generate"], {
4278
+ cwd: ROOT_DIR6,
4279
+ stdio: "inherit",
4280
+ //? `npx` resolves to `npx.cmd` on Windows, which requires shell
4281
+ //? invocation to be found on PATH. Dev-only, fixed argv (no user
4282
+ //? input interpolated), so the shell is not an injection surface.
4283
+ shell: process.platform === "win32"
4284
+ });
4285
+ child.on("error", reject);
4286
+ child.on("exit", (code) => {
4287
+ resolve(code ?? 0);
4288
+ });
4289
+ })
4290
+ );
4291
+
4014
4292
  // src/hotReload.ts
4015
- import { tryCatch as tryCatch2, getProjectConfig, getLocaleReloader } from "@luckystack/core";
4016
- var setupWatchers = () => {
4017
- const isDevMode = process.env.NODE_ENV !== "production";
4018
- if (!isDevMode) return;
4019
- const apiMarkerSlash = `/${getRoutingRules().apiMarker}/`;
4020
- const syncMarkerSlash = `/${getRoutingRules().syncMarker}/`;
4021
- const apiMarkerNoLead = `${getRoutingRules().apiMarker}/`;
4022
- const syncMarkerNoLead = `${getRoutingRules().syncMarker}/`;
4023
- const pathsCfg = getProjectConfig().paths;
4024
- const srcSegment = `/${pathsCfg.srcDir.replaceAll("\\", "/")}/`;
4025
- const sharedSegment = `/${pathsCfg.sharedDir.replaceAll("\\", "/")}/`;
4026
- const serverFunctionsSegments = (pathsCfg.serverFunctionDirs ?? [pathsCfg.serverFunctionsDir]).map(
4027
- (dir) => `/${dir.replaceAll("\\", "/")}/`
4028
- );
4029
- const isInServerFunctionsDir = (normalizedPath) => serverFunctionsSegments.some((segment) => normalizedPath.includes(segment));
4030
- const localesSegment = `${srcSegment}_locales/`;
4031
- const reloadTimers = /* @__PURE__ */ new Map();
4032
- const pendingApiUpserts = /* @__PURE__ */ new Set();
4033
- const pendingApiDeletes = /* @__PURE__ */ new Set();
4034
- const pendingSyncUpserts = /* @__PURE__ */ new Set();
4035
- const pendingSyncDeletes = /* @__PURE__ */ new Set();
4036
- const normalizeFsPath = (value) => path12.resolve(value).replaceAll("\\", "/");
4037
- const typeMapQueue = { pending: false, running: false };
4038
- const runTypeMapRegeneration = () => {
4039
- typeMapQueue.running = true;
4040
- typeMapQueue.pending = false;
4293
+ import { tryCatch as tryCatch3, getProjectConfig, getLocaleReloader } from "@luckystack/core";
4294
+ var normalizeFsPath = (value) => path13.resolve(value).replaceAll("\\", "/");
4295
+ var isGeneratedPath = (normalizedPath) => {
4296
+ return normalizedPath.includes("apiTypes.generated.ts") || normalizedPath.includes("apiDocs.generated.json");
4297
+ };
4298
+ var createTypeMapQueue = () => {
4299
+ const queue = { pending: false, running: false };
4300
+ const run = () => {
4301
+ queue.running = true;
4302
+ queue.pending = false;
4041
4303
  const startedAt = Date.now();
4042
4304
  setImmediate(() => {
4043
4305
  void (async () => {
4044
- const [err] = await tryCatch2(() => {
4306
+ const [err] = await tryCatch3(() => {
4045
4307
  generateTypeMapFile({ quiet: true });
4046
4308
  });
4047
4309
  if (err) {
@@ -4049,39 +4311,142 @@ var setupWatchers = () => {
4049
4311
  } else {
4050
4312
  console.log(`[HotReload] type map ready in ${Date.now() - startedAt}ms`, "green");
4051
4313
  }
4052
- typeMapQueue.running = false;
4053
- if (typeMapQueue.pending) {
4054
- runTypeMapRegeneration();
4314
+ queue.running = false;
4315
+ if (queue.pending) {
4316
+ run();
4055
4317
  }
4056
4318
  })();
4057
4319
  });
4058
4320
  };
4059
- const requestTypeMapRegeneration = () => {
4060
- if (typeMapQueue.running) {
4061
- typeMapQueue.pending = true;
4321
+ const request = () => {
4322
+ if (queue.running) {
4323
+ queue.pending = true;
4062
4324
  return;
4063
4325
  }
4064
- runTypeMapRegeneration();
4326
+ run();
4065
4327
  };
4066
- const isRouteDependencyFile = (normalizedPath) => {
4067
- if (!normalizedPath.endsWith(".ts") && !normalizedPath.endsWith(".tsx")) {
4068
- return false;
4328
+ return { request };
4329
+ };
4330
+ var createPendingChangeSets = () => ({
4331
+ apiUpserts: /* @__PURE__ */ new Set(),
4332
+ apiDeletes: /* @__PURE__ */ new Set(),
4333
+ syncUpserts: /* @__PURE__ */ new Set(),
4334
+ syncDeletes: /* @__PURE__ */ new Set()
4335
+ });
4336
+ var buildPathSegments = () => {
4337
+ const rules2 = getRoutingRules();
4338
+ const pathsCfg = getProjectConfig().paths;
4339
+ const srcSegment = `/${pathsCfg.srcDir.replaceAll("\\", "/")}/`;
4340
+ const sharedSegment = `/${pathsCfg.sharedDir.replaceAll("\\", "/")}/`;
4341
+ return {
4342
+ apiMarkerSlash: `/${rules2.apiMarker}/`,
4343
+ syncMarkerSlash: `/${rules2.syncMarker}/`,
4344
+ apiMarkerNoLead: `${rules2.apiMarker}/`,
4345
+ syncMarkerNoLead: `${rules2.syncMarker}/`,
4346
+ srcSegment,
4347
+ sharedSegment,
4348
+ localesSegment: `${srcSegment}_locales/`,
4349
+ serverFunctionsSegments: pathsCfg.serverFunctionDirs.map(
4350
+ (dir) => `/${dir.replaceAll("\\", "/")}/`
4351
+ )
4352
+ };
4353
+ };
4354
+ var makeIsInServerFunctionsDir = (segments) => (normalizedPath) => segments.serverFunctionsSegments.some((seg) => normalizedPath.includes(seg));
4355
+ var makeIsRouteDependencyFile = (segments) => (normalizedPath) => {
4356
+ if (!normalizedPath.endsWith(".ts") && !normalizedPath.endsWith(".tsx")) return false;
4357
+ if (!normalizedPath.includes(segments.srcSegment)) return false;
4358
+ if (normalizedPath.includes(segments.apiMarkerSlash) || normalizedPath.includes(segments.syncMarkerSlash)) return false;
4359
+ if (isGeneratedPath(normalizedPath)) return false;
4360
+ return true;
4361
+ };
4362
+ var makeIsSharedDependencyFile = (segments, isInServerFunctionsDir) => (normalizedPath) => {
4363
+ if (!(normalizedPath.endsWith(".ts") || normalizedPath.endsWith(".tsx"))) return false;
4364
+ if (normalizedPath.includes(segments.sharedSegment)) return true;
4365
+ return isInServerFunctionsDir(normalizedPath);
4366
+ };
4367
+ var makeIsTypeMapRelevantFile = (segments) => (normalizedPath) => {
4368
+ if (isGeneratedPath(normalizedPath)) return false;
4369
+ if (!(normalizedPath.endsWith(".ts") || normalizedPath.endsWith(".tsx"))) return false;
4370
+ if (normalizedPath.includes(segments.apiMarkerSlash) || normalizedPath.includes(segments.syncMarkerSlash)) return false;
4371
+ return normalizedPath.includes(segments.srcSegment) || normalizedPath.endsWith("/config.ts") || normalizedPath.includes(segments.sharedSegment);
4372
+ };
4373
+ var makeIsLocaleFile = (segments) => (normalizedPath) => normalizedPath.includes(segments.localesSegment) && normalizedPath.endsWith(".json");
4374
+ var createReloadScheduler = (debounceMs) => {
4375
+ const timers = /* @__PURE__ */ new Map();
4376
+ return (key, task, delay = debounceMs()) => {
4377
+ const active = timers.get(key);
4378
+ if (active) clearTimeout(active);
4379
+ const timer = setTimeout(() => {
4380
+ timers.delete(key);
4381
+ Promise.resolve(task()).catch((error) => {
4382
+ console.log(`[HotReload] Scheduled task threw an error: ${String(error)}`, "red");
4383
+ });
4384
+ }, delay);
4385
+ timers.set(key, timer);
4386
+ };
4387
+ };
4388
+ var mountWatchers = (onAdd, onChange, onDelete, onFunctionChange) => {
4389
+ const devConfig = getProjectConfig().dev;
4390
+ const pathsConfig = getProjectConfig().paths;
4391
+ watch(pathsConfig.srcDir, {
4392
+ ignoreInitial: true,
4393
+ awaitWriteFinish: {
4394
+ stabilityThreshold: devConfig.watcherStabilityThresholdMs,
4395
+ pollInterval: devConfig.watcherPollIntervalMs
4069
4396
  }
4070
- if (!normalizedPath.includes(srcSegment)) {
4071
- return false;
4397
+ }).on("add", onAdd).on("change", onChange).on("unlink", onDelete);
4398
+ for (const dir of pathsConfig.serverFunctionDirs) {
4399
+ watch(dir, { ignoreInitial: true }).on("add", onFunctionChange).on("change", onFunctionChange).on("unlink", onFunctionChange);
4400
+ }
4401
+ watch(pathsConfig.sharedDir, { ignoreInitial: true }).on("add", onFunctionChange).on("change", onFunctionChange).on("unlink", onFunctionChange);
4402
+ };
4403
+ var setupWatchers = () => {
4404
+ const isDevMode = process.env.NODE_ENV !== "production";
4405
+ if (!isDevMode) return;
4406
+ const segments = buildPathSegments();
4407
+ const isInServerFunctionsDir = makeIsInServerFunctionsDir(segments);
4408
+ const isRouteDependencyFile = makeIsRouteDependencyFile(segments);
4409
+ const isSharedDependencyFile = makeIsSharedDependencyFile(segments, isInServerFunctionsDir);
4410
+ const isTypeMapRelevantFile = makeIsTypeMapRelevantFile(segments);
4411
+ const isLocaleFile = makeIsLocaleFile(segments);
4412
+ const typeMap = createTypeMapQueue();
4413
+ const pending = createPendingChangeSets();
4414
+ const scheduleReload = createReloadScheduler(() => getProjectConfig().dev.hotReloadDebounceMs);
4415
+ const processPendingApiChanges = async ({ regenerateTypeMap = false } = {}) => {
4416
+ const deletePaths = [...pending.apiDeletes];
4417
+ const upsertPaths = [...pending.apiUpserts];
4418
+ pending.apiDeletes.clear();
4419
+ pending.apiUpserts.clear();
4420
+ if (regenerateTypeMap) {
4421
+ console.log(`[HotReload] API routes changed (add/delete), scheduling type map regeneration`, "blue");
4422
+ typeMap.request();
4072
4423
  }
4073
- if (normalizedPath.includes(apiMarkerSlash) || normalizedPath.includes(syncMarkerSlash)) {
4074
- return false;
4424
+ for (const deletePath of deletePaths) {
4425
+ removeApiFromFile(deletePath);
4426
+ console.log(`[HotReload] API removed: ${deletePath}`, "yellow");
4075
4427
  }
4076
- if (isGeneratedPath(normalizedPath)) {
4077
- return false;
4428
+ for (const upsertPath of upsertPaths) {
4429
+ await upsertApiFromFile(upsertPath);
4430
+ console.log(`[HotReload] API reloaded: ${upsertPath}`, "green");
4078
4431
  }
4079
- return true;
4080
4432
  };
4081
- const isSharedDependencyFile = (normalizedPath) => {
4082
- if (!(normalizedPath.endsWith(".ts") || normalizedPath.endsWith(".tsx"))) return false;
4083
- if (normalizedPath.includes(sharedSegment)) return true;
4084
- return isInServerFunctionsDir(normalizedPath);
4433
+ const processPendingSyncChanges = async ({ regenerateTypeMap = false } = {}) => {
4434
+ const deletePaths = [...pending.syncDeletes];
4435
+ const upsertPaths = [...pending.syncUpserts];
4436
+ pending.syncDeletes.clear();
4437
+ pending.syncUpserts.clear();
4438
+ if (regenerateTypeMap) {
4439
+ console.log(`[HotReload] Sync routes changed (add/delete), scheduling type map regeneration`, "blue");
4440
+ typeMap.request();
4441
+ }
4442
+ for (const deletePath of deletePaths) {
4443
+ removeSyncFromFile(deletePath);
4444
+ console.log(`[HotReload] Sync removed: ${deletePath}`, "yellow");
4445
+ }
4446
+ for (const upsertPath of upsertPaths) {
4447
+ await upsertSyncFromFile(upsertPath);
4448
+ console.log(`[HotReload] Sync reloaded: ${upsertPath}`, "green");
4449
+ }
4085
4450
  };
4086
4451
  const enqueueAffectedRoutesFromDependency = (changedPath) => {
4087
4452
  const affectedRoutes = findDependentRouteFiles(changedPath);
@@ -4092,13 +4457,13 @@ var setupWatchers = () => {
4092
4457
  let queuedApiCount = 0;
4093
4458
  let queuedSyncCount = 0;
4094
4459
  for (const routePath of affectedRoutes) {
4095
- if (routePath.includes(apiMarkerSlash)) {
4096
- pendingApiDeletes.delete(routePath);
4097
- pendingApiUpserts.add(routePath);
4460
+ if (routePath.includes(segments.apiMarkerSlash)) {
4461
+ pending.apiDeletes.delete(routePath);
4462
+ pending.apiUpserts.add(routePath);
4098
4463
  queuedApiCount += 1;
4099
- } else if (routePath.includes(syncMarkerSlash)) {
4100
- pendingSyncDeletes.delete(routePath);
4101
- pendingSyncUpserts.add(routePath);
4464
+ } else if (routePath.includes(segments.syncMarkerSlash)) {
4465
+ pending.syncDeletes.delete(routePath);
4466
+ pending.syncUpserts.add(routePath);
4102
4467
  queuedSyncCount += 1;
4103
4468
  }
4104
4469
  }
@@ -4117,36 +4482,13 @@ var setupWatchers = () => {
4117
4482
  });
4118
4483
  }
4119
4484
  };
4120
- const isGeneratedPath = (normalizedPath) => {
4121
- return normalizedPath.includes("apiTypes.generated.ts") || normalizedPath.includes("apiDocs.generated.json");
4122
- };
4123
- const isTypeMapRelevantFile = (normalizedPath) => {
4124
- if (isGeneratedPath(normalizedPath)) return false;
4125
- if (!(normalizedPath.endsWith(".ts") || normalizedPath.endsWith(".tsx"))) return false;
4126
- if (normalizedPath.includes(apiMarkerSlash) || normalizedPath.includes(syncMarkerSlash)) return false;
4127
- return normalizedPath.includes(srcSegment) || normalizedPath.endsWith("/config.ts") || normalizedPath.includes(sharedSegment);
4128
- };
4129
- const isLocaleFile = (normalizedPath) => {
4130
- return normalizedPath.includes(localesSegment) && normalizedPath.endsWith(".json");
4131
- };
4132
- const scheduleReload = (key, task, delay = getProjectConfig().dev.hotReloadDebounceMs) => {
4133
- const activeTimer = reloadTimers.get(key);
4134
- if (activeTimer) {
4135
- clearTimeout(activeTimer);
4136
- }
4137
- const timer = setTimeout(() => {
4138
- reloadTimers.delete(key);
4139
- void task();
4140
- }, delay);
4141
- reloadTimers.set(key, timer);
4142
- };
4143
- const handleAdd = async (path13) => {
4144
- const normalizedPath = normalizeFsPath(path13);
4485
+ const handleAdd = async (filePath) => {
4486
+ const normalizedPath = normalizeFsPath(filePath);
4145
4487
  invalidateGraphForFile(normalizedPath);
4146
4488
  const routeValidationMessage = getRouteFilenameValidationMessage(normalizedPath);
4147
4489
  if (routeValidationMessage) {
4148
- if (shouldInjectTemplate(path13)) {
4149
- const injected = await injectTemplate(path13);
4490
+ if (shouldInjectTemplate(filePath, { isNewFile: true })) {
4491
+ const injected = await injectTemplate(filePath);
4150
4492
  if (injected) {
4151
4493
  return;
4152
4494
  }
@@ -4154,87 +4496,57 @@ var setupWatchers = () => {
4154
4496
  console.log(`[HotReload] ${routeValidationMessage}`, "yellow");
4155
4497
  return;
4156
4498
  }
4157
- if (shouldInjectTemplate(path13)) {
4499
+ if (shouldInjectTemplate(filePath, { isNewFile: true })) {
4158
4500
  if (isSyncServerFile(normalizedPath)) {
4159
4501
  const clientPath = getPairedSyncFile(normalizedPath);
4160
- if (clientPath && fs9.existsSync(clientPath) && !isEmptyFile(clientPath)) {
4502
+ if (clientPath && fs10.existsSync(clientPath) && !isEmptyFile(clientPath)) {
4161
4503
  const clientInputTypes = extractClientInputFromFile(clientPath);
4162
4504
  if (clientInputTypes) {
4163
- await injectServerTemplateWithClientInput(path13, clientInputTypes);
4164
- requestTypeMapRegeneration();
4165
- await updateClientFileForPairedServer(clientPath);
4166
- await upsertSyncFromFile(path13);
4505
+ await injectServerTemplateWithClientInput(filePath, clientInputTypes);
4506
+ typeMap.request();
4507
+ const clientRewritten = await updateClientFileForPairedServer(clientPath);
4508
+ if (!clientRewritten) {
4509
+ console.log(`[HotReload] Paired client rewrite failed, skipping upsert for: ${clientPath}`, "yellow");
4510
+ return;
4511
+ }
4512
+ await upsertSyncFromFile(filePath);
4167
4513
  await upsertSyncFromFile(clientPath);
4168
4514
  return;
4169
4515
  }
4170
4516
  }
4171
4517
  }
4172
- const injected = await injectTemplate(path13);
4518
+ const injected = await injectTemplate(filePath);
4173
4519
  if (injected) {
4174
4520
  return;
4175
4521
  }
4176
4522
  }
4177
- if (normalizedPath.includes(apiMarkerNoLead)) {
4178
- pendingApiDeletes.delete(normalizedPath);
4179
- pendingApiUpserts.add(normalizedPath);
4523
+ if (normalizedPath.includes(segments.apiMarkerNoLead)) {
4524
+ pending.apiDeletes.delete(normalizedPath);
4525
+ pending.apiUpserts.add(normalizedPath);
4180
4526
  scheduleReload("api", async () => {
4181
4527
  await processPendingApiChanges({ regenerateTypeMap: true });
4182
4528
  });
4183
4529
  return;
4184
4530
  }
4185
- if (normalizedPath.includes(syncMarkerNoLead)) {
4186
- pendingSyncDeletes.delete(normalizedPath);
4187
- pendingSyncUpserts.add(normalizedPath);
4531
+ if (normalizedPath.includes(segments.syncMarkerNoLead)) {
4532
+ pending.syncDeletes.delete(normalizedPath);
4533
+ pending.syncUpserts.add(normalizedPath);
4188
4534
  scheduleReload("sync", async () => {
4189
4535
  await processPendingSyncChanges({ regenerateTypeMap: true });
4190
4536
  });
4191
4537
  return;
4192
4538
  }
4193
- handleChange(path13);
4194
- };
4195
- const processPendingApiChanges = async ({ regenerateTypeMap = false } = {}) => {
4196
- const deletePaths = [...pendingApiDeletes];
4197
- const upsertPaths = [...pendingApiUpserts];
4198
- pendingApiDeletes.clear();
4199
- pendingApiUpserts.clear();
4200
- if (regenerateTypeMap) {
4201
- console.log(`[HotReload] API routes changed (add/delete), scheduling type map regeneration`, "blue");
4202
- requestTypeMapRegeneration();
4203
- }
4204
- for (const deletePath of deletePaths) {
4205
- removeApiFromFile(deletePath);
4206
- console.log(`[HotReload] API removed: ${deletePath}`, "yellow");
4207
- }
4208
- for (const upsertPath of upsertPaths) {
4209
- await upsertApiFromFile(upsertPath);
4210
- console.log(`[HotReload] API reloaded: ${upsertPath}`, "green");
4211
- }
4212
- };
4213
- const processPendingSyncChanges = async ({ regenerateTypeMap = false } = {}) => {
4214
- const deletePaths = [...pendingSyncDeletes];
4215
- const upsertPaths = [...pendingSyncUpserts];
4216
- pendingSyncDeletes.clear();
4217
- pendingSyncUpserts.clear();
4218
- if (regenerateTypeMap) {
4219
- console.log(`[HotReload] Sync routes changed (add/delete), scheduling type map regeneration`, "blue");
4220
- requestTypeMapRegeneration();
4221
- }
4222
- for (const deletePath of deletePaths) {
4223
- removeSyncFromFile(deletePath);
4224
- console.log(`[HotReload] Sync removed: ${deletePath}`, "yellow");
4225
- }
4226
- for (const upsertPath of upsertPaths) {
4227
- await upsertSyncFromFile(upsertPath);
4228
- console.log(`[HotReload] Sync reloaded: ${upsertPath}`, "green");
4229
- }
4539
+ handleChange(filePath).catch((error) => {
4540
+ console.log(`[HotReload] handleChange threw an error: ${String(error)}`, "red");
4541
+ });
4230
4542
  };
4231
- const handleChange = async (path13) => {
4232
- const normalizedPath = normalizeFsPath(path13);
4543
+ const handleChange = async (filePath) => {
4544
+ const normalizedPath = normalizeFsPath(filePath);
4233
4545
  invalidateGraphForFile(normalizedPath);
4234
4546
  const routeValidationMessage = getRouteFilenameValidationMessage(normalizedPath);
4235
4547
  if (routeValidationMessage) {
4236
- if (shouldInjectTemplate(path13)) {
4237
- const injected = await injectTemplate(path13);
4548
+ if (shouldInjectTemplate(filePath)) {
4549
+ const injected = await injectTemplate(filePath);
4238
4550
  if (injected) {
4239
4551
  return;
4240
4552
  }
@@ -4242,8 +4554,8 @@ var setupWatchers = () => {
4242
4554
  console.log(`[HotReload] ${routeValidationMessage}`, "yellow");
4243
4555
  return;
4244
4556
  }
4245
- if (shouldInjectTemplate(path13)) {
4246
- const injected = await injectTemplate(path13);
4557
+ if (shouldInjectTemplate(filePath)) {
4558
+ const injected = await injectTemplate(filePath);
4247
4559
  if (injected) {
4248
4560
  return;
4249
4561
  }
@@ -4260,7 +4572,7 @@ var setupWatchers = () => {
4260
4572
  if (isRouteDependencyFile(normalizedPath)) {
4261
4573
  scheduleReload("typemap", () => {
4262
4574
  console.log(`[HotReload] Route dependency changed, scheduling type map regeneration`, "blue");
4263
- requestTypeMapRegeneration();
4575
+ typeMap.request();
4264
4576
  });
4265
4577
  enqueueAffectedRoutesFromDependency(normalizedPath);
4266
4578
  return;
@@ -4268,26 +4580,26 @@ var setupWatchers = () => {
4268
4580
  if (isTypeMapRelevantFile(normalizedPath)) {
4269
4581
  scheduleReload("typemap", () => {
4270
4582
  console.log(`[HotReload] Route dependency changed, scheduling type map regeneration`, "blue");
4271
- requestTypeMapRegeneration();
4583
+ typeMap.request();
4272
4584
  });
4273
4585
  }
4274
- if (normalizedPath.includes(apiMarkerNoLead)) {
4586
+ if (normalizedPath.includes(segments.apiMarkerNoLead)) {
4275
4587
  scheduleReload("typemap", () => {
4276
4588
  console.log(`[HotReload] API changed, scheduling type map regeneration`, "blue");
4277
- requestTypeMapRegeneration();
4589
+ typeMap.request();
4278
4590
  });
4279
- pendingApiDeletes.delete(normalizedPath);
4280
- pendingApiUpserts.add(normalizedPath);
4591
+ pending.apiDeletes.delete(normalizedPath);
4592
+ pending.apiUpserts.add(normalizedPath);
4281
4593
  scheduleReload("api", async () => {
4282
4594
  await processPendingApiChanges();
4283
4595
  });
4284
- } else if (normalizedPath.includes(syncMarkerNoLead)) {
4596
+ } else if (normalizedPath.includes(segments.syncMarkerNoLead)) {
4285
4597
  scheduleReload("typemap", () => {
4286
4598
  console.log(`[HotReload] Sync changed, scheduling type map regeneration`, "blue");
4287
- requestTypeMapRegeneration();
4599
+ typeMap.request();
4288
4600
  });
4289
- pendingSyncDeletes.delete(normalizedPath);
4290
- pendingSyncUpserts.add(normalizedPath);
4601
+ pending.syncDeletes.delete(normalizedPath);
4602
+ pending.syncUpserts.add(normalizedPath);
4291
4603
  scheduleReload("sync", async () => {
4292
4604
  await processPendingSyncChanges();
4293
4605
  });
@@ -4297,15 +4609,15 @@ var setupWatchers = () => {
4297
4609
  const normalizedPath = normalizeFsPath(changedPath);
4298
4610
  invalidateGraphForFile(normalizedPath);
4299
4611
  scheduleReload("functions", async () => {
4300
- requestTypeMapRegeneration();
4612
+ typeMap.request();
4301
4613
  await initializeFunctions();
4302
4614
  });
4303
4615
  if (isSharedDependencyFile(normalizedPath)) {
4304
4616
  enqueueAffectedRoutesFromDependency(normalizedPath);
4305
4617
  }
4306
4618
  };
4307
- const handleDelete = async (path13) => {
4308
- const normalizedPath = normalizeFsPath(path13);
4619
+ const handleDelete = (filePath) => {
4620
+ const normalizedPath = normalizeFsPath(filePath);
4309
4621
  invalidateGraphForFile(normalizedPath);
4310
4622
  if (isGeneratedPath(normalizedPath)) {
4311
4623
  return;
@@ -4319,7 +4631,7 @@ var setupWatchers = () => {
4319
4631
  if (isRouteDependencyFile(normalizedPath)) {
4320
4632
  scheduleReload("typemap", () => {
4321
4633
  console.log(`[HotReload] Route dependency deleted, scheduling type map regeneration`, "blue");
4322
- requestTypeMapRegeneration();
4634
+ typeMap.request();
4323
4635
  });
4324
4636
  enqueueAffectedRoutesFromDependency(normalizedPath);
4325
4637
  return;
@@ -4327,237 +4639,80 @@ var setupWatchers = () => {
4327
4639
  if (isTypeMapRelevantFile(normalizedPath)) {
4328
4640
  scheduleReload("typemap", () => {
4329
4641
  console.log(`[HotReload] Route dependency deleted, scheduling type map regeneration`, "blue");
4330
- requestTypeMapRegeneration();
4642
+ typeMap.request();
4331
4643
  });
4332
4644
  }
4333
- if (normalizedPath.includes(apiMarkerNoLead)) {
4645
+ if (normalizedPath.includes(segments.apiMarkerNoLead)) {
4334
4646
  scheduleReload("typemap", () => {
4335
4647
  console.log(`[HotReload] API deleted, scheduling type map regeneration`, "blue");
4336
- requestTypeMapRegeneration();
4648
+ typeMap.request();
4337
4649
  });
4338
- pendingApiUpserts.delete(normalizedPath);
4339
- pendingApiDeletes.add(normalizedPath);
4650
+ pending.apiUpserts.delete(normalizedPath);
4651
+ pending.apiDeletes.add(normalizedPath);
4340
4652
  scheduleReload("api", async () => {
4341
4653
  await processPendingApiChanges();
4342
4654
  });
4343
- } else if (normalizedPath.includes(syncMarkerNoLead)) {
4655
+ } else if (normalizedPath.includes(segments.syncMarkerNoLead)) {
4344
4656
  scheduleReload("typemap", () => {
4345
4657
  console.log(`[HotReload] Sync deleted, scheduling type map regeneration`, "blue");
4346
- requestTypeMapRegeneration();
4658
+ typeMap.request();
4347
4659
  });
4348
- pendingSyncUpserts.delete(normalizedPath);
4349
- pendingSyncDeletes.add(normalizedPath);
4660
+ pending.syncUpserts.delete(normalizedPath);
4661
+ pending.syncDeletes.add(normalizedPath);
4350
4662
  scheduleReload("sync", async () => {
4351
4663
  if (isSyncServerFile(normalizedPath)) {
4352
4664
  const clientPath = getPairedSyncFile(normalizedPath);
4353
- if (clientPath && fs9.existsSync(clientPath)) {
4665
+ if (clientPath && fs10.existsSync(clientPath)) {
4354
4666
  const pagePath = extractSyncPagePath2(normalizedPath);
4355
4667
  const syncName = extractSyncName2(normalizedPath);
4356
4668
  const clientInputTypes = extractClientInputFromGeneratedTypes(pagePath, syncName);
4357
- if (clientInputTypes) {
4358
- await updateClientFileForDeletedServer(clientPath, clientInputTypes);
4359
- } else {
4360
- await updateClientFileForDeletedServer(clientPath, "{\n // Types were in _server.ts - please add them here\n }");
4361
- }
4669
+ await updateClientFileForDeletedServer(
4670
+ clientPath,
4671
+ clientInputTypes ?? "{\n // Types were in _server.ts - please add them here\n }"
4672
+ );
4362
4673
  }
4363
4674
  }
4364
4675
  await processPendingSyncChanges();
4365
4676
  });
4366
4677
  }
4367
4678
  };
4368
- const devConfig = getProjectConfig().dev;
4369
- const pathsConfig = getProjectConfig().paths;
4370
- watch(pathsConfig.srcDir, {
4371
- ignoreInitial: true,
4372
- awaitWriteFinish: {
4373
- stabilityThreshold: devConfig.watcherStabilityThresholdMs,
4374
- pollInterval: devConfig.watcherPollIntervalMs
4375
- }
4376
- }).on("add", handleAdd).on("change", handleChange).on("unlink", handleDelete);
4377
- const serverFunctionDirsToWatch = pathsConfig.serverFunctionDirs && pathsConfig.serverFunctionDirs.length > 0 ? pathsConfig.serverFunctionDirs : [pathsConfig.serverFunctionsDir];
4378
- for (const dir of serverFunctionDirsToWatch) {
4379
- watch(dir, { ignoreInitial: true }).on("add", handleFunctionChange).on("change", handleFunctionChange).on("unlink", handleFunctionChange);
4380
- }
4381
- watch(pathsConfig.sharedDir, { ignoreInitial: true }).on("add", handleFunctionChange).on("change", handleFunctionChange).on("unlink", handleFunctionChange);
4679
+ const safeHandleAdd = (p) => {
4680
+ void handleAdd(p).catch((error) => {
4681
+ console.log(`[HotReload] handleAdd threw an error: ${String(error)}`, "red");
4682
+ });
4683
+ };
4684
+ const safeHandleChange = (p) => {
4685
+ void handleChange(p).catch((error) => {
4686
+ console.log(`[HotReload] handleChange threw an error: ${String(error)}`, "red");
4687
+ });
4688
+ };
4689
+ const safeHandleDelete = (p) => {
4690
+ handleDelete(p);
4691
+ };
4692
+ mountWatchers(safeHandleAdd, safeHandleChange, safeHandleDelete, handleFunctionChange);
4382
4693
  setImmediate(() => {
4383
4694
  void (async () => {
4384
- const [err] = await tryCatch2(() => {
4695
+ if (isPrismaClientMissing()) {
4696
+ console.log(`[HotReload] @prisma/client not generated yet \u2014 running prisma generate (no DB needed)\u2026`, "blue");
4697
+ const [generateErr, exitCode] = await runPrismaGenerate();
4698
+ if (generateErr || exitCode !== 0) {
4699
+ console.log(`[HotReload] prisma generate failed${generateErr ? `: ${String(generateErr)}` : ` (exit code ${String(exitCode)})`} \u2014 run \`npm run prisma:generate\` manually and restart`, "red");
4700
+ } else {
4701
+ console.log(`[HotReload] prisma generate complete`, "green");
4702
+ }
4703
+ }
4704
+ const [err] = await tryCatch3(() => {
4385
4705
  generateTypeMapFile({ quiet: true });
4386
4706
  });
4387
4707
  if (err) {
4388
- console.log(`[HotReload] initial type map generation failed: ${String(err)}`, "red");
4708
+ const hint = isPrismaClientMissing() ? "\n \u2192 This usually means @prisma/client is not generated. Run `npm run prisma:generate` and restart." : "";
4709
+ console.log(`[HotReload] initial type map generation failed: ${String(err)}${hint}`, "red");
4389
4710
  } else {
4390
4711
  console.log(`[HotReload] type map ready in background`, "green");
4391
4712
  }
4392
4713
  })();
4393
4714
  });
4394
4715
  };
4395
-
4396
- // src/validateDeploy.ts
4397
- var validateDeploy = ({
4398
- services,
4399
- deploy,
4400
- env = process.env
4401
- }) => {
4402
- const findings = [];
4403
- const serviceNames = new Set(Object.keys(services.services));
4404
- const servicesInPresets = /* @__PURE__ */ new Map();
4405
- for (const [presetKey, preset] of Object.entries(services.presets)) {
4406
- for (const service of preset.services) {
4407
- const existing = servicesInPresets.get(service) ?? [];
4408
- existing.push(presetKey);
4409
- servicesInPresets.set(service, existing);
4410
- }
4411
- }
4412
- for (const service of serviceNames) {
4413
- const owners = servicesInPresets.get(service) ?? [];
4414
- if (owners.length === 0) {
4415
- findings.push({
4416
- severity: "error",
4417
- code: "service-unassigned",
4418
- message: `Service "${service}" is declared in services.services but not assigned to any preset. Every service must belong to exactly one preset.`,
4419
- location: `services.config.ts > services.${service}`
4420
- });
4421
- } else if (owners.length > 1) {
4422
- findings.push({
4423
- severity: "error",
4424
- code: "service-in-multiple-presets",
4425
- message: `Service "${service}" is in multiple presets: ${owners.join(", ")}. A service may belong to exactly one preset.`,
4426
- location: `services.config.ts > services.${service}`
4427
- });
4428
- }
4429
- }
4430
- for (const [presetKey, preset] of Object.entries(services.presets)) {
4431
- for (const service of preset.services) {
4432
- if (!serviceNames.has(service)) {
4433
- findings.push({
4434
- severity: "error",
4435
- code: "preset-references-unknown-service",
4436
- message: `Preset "${presetKey}" references unknown service "${service}". Add it to services.services or remove the reference.`,
4437
- location: `services.config.ts > presets.${presetKey}.services`
4438
- });
4439
- }
4440
- }
4441
- }
4442
- const environments = deploy.environments ?? {};
4443
- const resourceNames = new Set(Object.keys(deploy.resources));
4444
- for (const [envKey, envDef] of Object.entries(environments)) {
4445
- for (const [bindingService, bindingUrl] of Object.entries(envDef.bindings)) {
4446
- if (!serviceNames.has(bindingService)) {
4447
- findings.push({
4448
- severity: "error",
4449
- code: "binding-references-unknown-service",
4450
- message: `Environment "${envKey}" binds unknown service "${bindingService}". Either add it to services.services or remove the binding.`,
4451
- location: `deploy.config.ts > environments.${envKey}.bindings`
4452
- });
4453
- }
4454
- let parsed = null;
4455
- try {
4456
- parsed = new URL(bindingUrl);
4457
- } catch {
4458
- findings.push({
4459
- severity: "error",
4460
- code: "binding-invalid-url",
4461
- message: `Environment "${envKey}" service "${bindingService}" binding is not a valid URL: "${bindingUrl}".`,
4462
- location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`
4463
- });
4464
- }
4465
- if (parsed && !parsed.port) {
4466
- findings.push({
4467
- severity: "error",
4468
- code: "binding-missing-port",
4469
- message: `Environment "${envKey}" service "${bindingService}" binding "${bindingUrl}" is missing an explicit port. Set one to avoid the URL-spec default (80/443) silently winning.`,
4470
- location: `deploy.config.ts > environments.${envKey}.bindings.${bindingService}`
4471
- });
4472
- }
4473
- }
4474
- if (!resourceNames.has(envDef.redis)) {
4475
- findings.push({
4476
- severity: "error",
4477
- code: "unknown-redis-resource",
4478
- message: `Environment "${envKey}" references unknown redis resource "${envDef.redis}". Add it to deploy.resources.`,
4479
- location: `deploy.config.ts > environments.${envKey}.redis`
4480
- });
4481
- }
4482
- if (!resourceNames.has(envDef.mongo)) {
4483
- findings.push({
4484
- severity: "error",
4485
- code: "unknown-mongo-resource",
4486
- message: `Environment "${envKey}" references unknown mongo resource "${envDef.mongo}". Add it to deploy.resources.`,
4487
- location: `deploy.config.ts > environments.${envKey}.mongo`
4488
- });
4489
- }
4490
- if (envDef.fallback) {
4491
- const fallbackEnv = environments[envDef.fallback];
4492
- if (fallbackEnv) {
4493
- if (fallbackEnv.redis !== envDef.redis) {
4494
- findings.push({
4495
- severity: "error",
4496
- code: "fallback-redis-mismatch",
4497
- message: `Environment "${envKey}" and its fallback "${envDef.fallback}" reference different redis resources ("${envDef.redis}" vs "${fallbackEnv.redis}"). Fallback envs must share the same redis resource key to keep the shared-Redis invariant.`,
4498
- location: `deploy.config.ts > environments.${envKey}.fallback`
4499
- });
4500
- }
4501
- if (fallbackEnv.mongo !== envDef.mongo) {
4502
- findings.push({
4503
- severity: "error",
4504
- code: "fallback-mongo-mismatch",
4505
- message: `Environment "${envKey}" and its fallback "${envDef.fallback}" reference different mongo resources ("${envDef.mongo}" vs "${fallbackEnv.mongo}"). Fallback envs must share the same mongo resource key.`,
4506
- location: `deploy.config.ts > environments.${envKey}.fallback`
4507
- });
4508
- }
4509
- } else {
4510
- findings.push({
4511
- severity: "error",
4512
- code: "unknown-fallback-env",
4513
- message: `Environment "${envKey}" has fallback "${envDef.fallback}" which does not exist in deploy.environments.`,
4514
- location: `deploy.config.ts > environments.${envKey}.fallback`
4515
- });
4516
- }
4517
- }
4518
- }
4519
- for (const [resourceKey, resource] of Object.entries(deploy.resources)) {
4520
- const urlValue = env[resource.urlEnvKey];
4521
- if (urlValue === void 0 || urlValue === "") {
4522
- findings.push({
4523
- severity: "warning",
4524
- code: "missing-resource-env-var",
4525
- message: `Resource "${resourceKey}" expects env var "${resource.urlEnvKey}" but it is unset. The framework will still boot if the env is provided at runtime, but config-time tooling cannot verify the value.`,
4526
- location: `deploy.config.ts > resources.${resourceKey}.urlEnvKey`
4527
- });
4528
- }
4529
- for (const synchronizedKey of resource.synchronizedEnvKeys ?? []) {
4530
- const value = env[synchronizedKey];
4531
- if (value === void 0 || value === "") {
4532
- findings.push({
4533
- severity: "warning",
4534
- code: "missing-synchronized-env-var",
4535
- message: `Resource "${resourceKey}" requires synchronized env var "${synchronizedKey}" but it is unset. Boot will compute an empty hash and fall back to warning-only.`,
4536
- location: `deploy.config.ts > resources.${resourceKey}.synchronizedEnvKeys`
4537
- });
4538
- }
4539
- }
4540
- }
4541
- for (const service of serviceNames) {
4542
- const boundIn = Object.entries(environments).filter(([, env2]) => service in env2.bindings);
4543
- if (boundIn.length === 0) {
4544
- findings.push({
4545
- severity: "warning",
4546
- code: "service-bound-in-no-environment",
4547
- message: `Service "${service}" is in services.services and assigned to a preset but never bound in any environment. Requests for this service will fall through to the missing-service handler.`,
4548
- location: `services.config.ts > services.${service}`
4549
- });
4550
- }
4551
- }
4552
- const errorCount = findings.filter((f) => f.severity === "error").length;
4553
- const warningCount = findings.filter((f) => f.severity === "warning").length;
4554
- return {
4555
- ok: errorCount === 0,
4556
- findings,
4557
- errorCount,
4558
- warningCount
4559
- };
4560
- };
4561
4716
  export {
4562
4717
  API_VERSION_TOKEN_REGEX,
4563
4718
  BUILT_IN_TEMPLATE_FILENAMES,