@luckystack/devkit 0.8.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,11 +3,12 @@ import {
3
3
  } from "./chunk-IWGWJU4M.js";
4
4
 
5
5
  // src/typeMap/discovery.ts
6
- import fs from "fs";
7
- import path from "path";
8
- import { ROOT_DIR } from "@luckystack/core";
6
+ import fs2 from "fs";
7
+ import path2 from "path";
8
+ import { ROOT_DIR as ROOT_DIR2 } from "@luckystack/core";
9
9
 
10
10
  // src/routingRules.ts
11
+ import { isTestDirectory } from "@luckystack/core";
11
12
  import { validatePagePath as corePagePath } from "@luckystack/core";
12
13
  var DEFAULT_RULES = {
13
14
  apiMarker: "_api",
@@ -66,24 +67,371 @@ var isSyncFileName = (fileName) => {
66
67
  var isRouteTestFile = (fileNameOrPath) => {
67
68
  return fileNameOrPath.endsWith(".tests.ts");
68
69
  };
69
- var validatePagePath = (srcRelativePath) => {
70
- const rules2 = getRoutingRules();
71
- const pageRules = {
72
- privateFolderPrefix: rules2.privateFolderPrefix,
73
- scaffoldIgnoredFolders: rules2.scaffoldIgnoredFolders
74
- };
75
- return corePagePath(srcRelativePath, pageRules);
70
+ var isNonRouteDirectory = (directoryName) => directoryName.startsWith(getRoutingRules().privateFolderPrefix) || isTestDirectory(directoryName);
71
+ var validatePagePath = (srcRelativePath) => {
72
+ const rules2 = getRoutingRules();
73
+ const pageRules = {
74
+ privateFolderPrefix: rules2.privateFolderPrefix,
75
+ scaffoldIgnoredFolders: rules2.scaffoldIgnoredFolders
76
+ };
77
+ return corePagePath(srcRelativePath, pageRules);
78
+ };
79
+
80
+ // src/routeNamingValidation.ts
81
+ import fs from "fs";
82
+ import path from "path";
83
+ import { ROOT_DIR, isTestFile } from "@luckystack/core";
84
+
85
+ // src/routeConventions.ts
86
+ var API_VERSION_TOKEN_REGEX = /_v(\d+)$/;
87
+ var SYNC_VERSION_TOKEN_REGEX = /_(server|client)_v(\d+)$/;
88
+ var ROUTE_NAMING_RULES = {
89
+ api: "<name>_v<number>.ts",
90
+ syncServer: "<name>_server_v<number>.ts",
91
+ syncClient: "<name>_client_v<number>.ts"
92
+ };
93
+ var ROUTE_NAMING_EXAMPLES = {
94
+ api: "updateUser_v1.ts",
95
+ syncServer: "updateCounter_server_v1.ts",
96
+ syncClient: "updateCounter_client_v1.ts"
97
+ };
98
+ var isVersionedApiFileName = (fileName) => isApiFileName(fileName);
99
+ var isVersionedSyncFileName = (fileName) => isSyncFileName(fileName);
100
+ var isVersionedSyncServerFileName = (fileName) => isSyncServerFileName(fileName);
101
+ var isVersionedSyncClientFileName = (fileName) => isSyncClientFileName(fileName);
102
+
103
+ // src/internal/mapUtils.ts
104
+ var getOrInit = (map, key, factory) => {
105
+ const existing = map.get(key);
106
+ if (existing !== void 0) return existing;
107
+ const fresh = factory();
108
+ map.set(key, fresh);
109
+ return fresh;
110
+ };
111
+ var mustGet = (map, key, label) => {
112
+ const value = map.get(key);
113
+ if (value === void 0) {
114
+ throw new Error(`[devkit] invariant violation: '${String(key)}' missing from ${label}`);
115
+ }
116
+ return value;
117
+ };
118
+
119
+ // src/routeNamingValidation.ts
120
+ var normalizePath = (value) => {
121
+ return value.replaceAll("\\", "/");
122
+ };
123
+ var toRel = (absolute) => path.relative(ROOT_DIR, absolute).replaceAll("\\", "/");
124
+ var isInsidePrivateRouteSubfolder = (fullPath) => {
125
+ const { privateFolderPrefix, apiMarker, syncMarker } = getRoutingRules();
126
+ const segments = normalizePath(fullPath).split("/");
127
+ const markerIndex = segments.findLastIndex((segment) => segment === apiMarker || segment === syncMarker);
128
+ if (markerIndex === -1) return false;
129
+ return segments.slice(markerIndex + 1).some((segment) => segment.startsWith(privateFolderPrefix));
130
+ };
131
+ var isRouteSurfaceFile = (fullPath) => !isInsidePrivateRouteSubfolder(fullPath) && !isTestFile(fullPath);
132
+ var walkRouteFiles = (dir, results = []) => {
133
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
134
+ const apiSeg = apiMarkerSegment();
135
+ const syncSeg = syncMarkerSegment();
136
+ const { ignore } = getRoutingRules();
137
+ for (const entry of entries) {
138
+ const fullPath = path.join(dir, entry.name);
139
+ const normalizedFullPath = normalizePath(fullPath);
140
+ if (ignore(toRel(fullPath))) continue;
141
+ if (isInsidePrivateRouteSubfolder(fullPath)) continue;
142
+ if (entry.isDirectory()) {
143
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
144
+ continue;
145
+ }
146
+ walkRouteFiles(fullPath, results);
147
+ continue;
148
+ }
149
+ if (!entry.isFile() || !entry.name.endsWith(".ts")) {
150
+ continue;
151
+ }
152
+ if (isRouteTestFile(entry.name)) {
153
+ continue;
154
+ }
155
+ if (normalizedFullPath.includes(apiSeg) || normalizedFullPath.includes(syncSeg)) {
156
+ results.push(fullPath);
157
+ }
158
+ }
159
+ return results;
160
+ };
161
+ var getFileRouteToken = ({
162
+ normalizedFilePath,
163
+ marker
164
+ }) => {
165
+ const markerIndex = normalizedFilePath.indexOf(marker);
166
+ if (markerIndex === -1) {
167
+ return "";
168
+ }
169
+ const tokenStart = markerIndex + marker.length;
170
+ return normalizedFilePath.slice(tokenStart, normalizedFilePath.length - ".ts".length);
171
+ };
172
+ var validateRouteFilePath = (filePath) => {
173
+ const issues = [];
174
+ const normalizedFilePath = normalizePath(path.resolve(filePath));
175
+ const fileName = path.basename(filePath);
176
+ const apiSeg = apiMarkerSegment();
177
+ const syncSeg = syncMarkerSegment();
178
+ if (normalizedFilePath.includes(apiSeg)) {
179
+ const apiRouteToken = getFileRouteToken({ normalizedFilePath, marker: apiSeg });
180
+ if (apiRouteToken.includes("/")) {
181
+ issues.push({
182
+ kind: "api",
183
+ filePath: normalizedFilePath,
184
+ reason: 'API route token cannot contain nested path segments ("/").',
185
+ expected: ROUTE_NAMING_RULES.api
186
+ });
187
+ }
188
+ if (!isVersionedApiFileName(fileName)) {
189
+ issues.push({
190
+ kind: "api",
191
+ filePath: normalizedFilePath,
192
+ reason: "API filename does not match versioned naming.",
193
+ expected: ROUTE_NAMING_RULES.api
194
+ });
195
+ }
196
+ }
197
+ if (normalizedFilePath.includes(syncSeg)) {
198
+ const syncRouteToken = getFileRouteToken({ normalizedFilePath, marker: syncSeg });
199
+ if (syncRouteToken.includes("/")) {
200
+ issues.push({
201
+ kind: "sync",
202
+ filePath: normalizedFilePath,
203
+ reason: 'Sync route token cannot contain nested path segments ("/").',
204
+ expected: `${ROUTE_NAMING_RULES.syncServer} or ${ROUTE_NAMING_RULES.syncClient}`
205
+ });
206
+ }
207
+ if (!isVersionedSyncFileName(fileName)) {
208
+ issues.push({
209
+ kind: "sync",
210
+ filePath: normalizedFilePath,
211
+ reason: "Sync filename does not match versioned naming.",
212
+ expected: `${ROUTE_NAMING_RULES.syncServer} or ${ROUTE_NAMING_RULES.syncClient}`
213
+ });
214
+ }
215
+ }
216
+ return issues;
217
+ };
218
+ var resolveApiRouteKey = ({
219
+ srcDir,
220
+ filePath
221
+ }) => {
222
+ const rules2 = getRoutingRules();
223
+ const relativePath = normalizePath(path.relative(srcDir, filePath));
224
+ const segments = relativePath.split("/");
225
+ const apiIndex = segments.indexOf(rules2.apiMarker);
226
+ if (apiIndex === -1 || apiIndex === segments.length - 1) {
227
+ return null;
228
+ }
229
+ const pageLocation = segments.slice(0, apiIndex).join("/");
230
+ const apiFilePath = segments.slice(apiIndex + 1).join("/");
231
+ const rawApiName = apiFilePath.replace(/\.ts$/, "");
232
+ const versionMatch = rawApiName.match(rules2.apiVersionRegex);
233
+ if (!versionMatch) {
234
+ return null;
235
+ }
236
+ const version = `v${versionMatch[1]}`;
237
+ const apiName = rawApiName.replace(rules2.apiVersionRegex, "");
238
+ const mappedPageLocation = pageLocation || "system";
239
+ return `api/${mappedPageLocation}/${apiName}/${version}`;
240
+ };
241
+ var resolveSyncRouteKey = ({
242
+ srcDir,
243
+ filePath
244
+ }) => {
245
+ const rules2 = getRoutingRules();
246
+ const relativePath = normalizePath(path.relative(srcDir, filePath));
247
+ const segments = relativePath.split("/");
248
+ const syncIndex = segments.indexOf(rules2.syncMarker);
249
+ if (syncIndex === -1 || syncIndex === segments.length - 1) {
250
+ return null;
251
+ }
252
+ const pageLocation = segments.slice(0, syncIndex).join("/");
253
+ const syncFilePath = segments.slice(syncIndex + 1).join("/");
254
+ const rawSyncName = syncFilePath.replace(/\.ts$/, "");
255
+ const syncMatch = rawSyncName.match(rules2.syncVersionRegex);
256
+ if (!syncMatch) {
257
+ return null;
258
+ }
259
+ const kind = syncMatch[1];
260
+ const version = `v${syncMatch[2]}`;
261
+ const syncName = rawSyncName.replace(rules2.syncVersionRegex, "");
262
+ const routeBaseKey = pageLocation ? `sync/${pageLocation}/${syncName}/${version}` : `sync/${syncName}/${version}`;
263
+ return `${routeBaseKey}_${kind}`;
264
+ };
265
+ var collectInvalidRouteNamingIssues = (srcDir) => {
266
+ const allRouteFiles = walkRouteFiles(srcDir);
267
+ const issues = [];
268
+ for (const filePath of allRouteFiles) {
269
+ issues.push(...validateRouteFilePath(filePath));
270
+ }
271
+ return issues.toSorted((a, b) => a.filePath.localeCompare(b.filePath));
272
+ };
273
+ var collectDuplicateNormalizedRouteKeyIssues = (srcDir) => {
274
+ const allRouteFiles = walkRouteFiles(srcDir);
275
+ const routeKeyToFilePaths = /* @__PURE__ */ new Map();
276
+ const routeKeyKinds = /* @__PURE__ */ new Map();
277
+ const apiSeg = apiMarkerSegment();
278
+ const syncSeg = syncMarkerSegment();
279
+ for (const filePath of allRouteFiles) {
280
+ const normalizedFilePath = normalizePath(path.resolve(filePath));
281
+ if (normalizedFilePath.includes(apiSeg)) {
282
+ const routeKey = resolveApiRouteKey({ srcDir, filePath });
283
+ if (!routeKey) {
284
+ continue;
285
+ }
286
+ getOrInit(routeKeyToFilePaths, routeKey, () => []).push(normalizedFilePath);
287
+ routeKeyKinds.set(routeKey, "api");
288
+ continue;
289
+ }
290
+ if (normalizedFilePath.includes(syncSeg)) {
291
+ const routeKey = resolveSyncRouteKey({ srcDir, filePath });
292
+ if (!routeKey) {
293
+ continue;
294
+ }
295
+ getOrInit(routeKeyToFilePaths, routeKey, () => []).push(normalizedFilePath);
296
+ routeKeyKinds.set(routeKey, "sync");
297
+ }
298
+ }
299
+ const issues = [];
300
+ for (const [routeKey, filePaths] of routeKeyToFilePaths.entries()) {
301
+ if (filePaths.length < 2) {
302
+ continue;
303
+ }
304
+ issues.push({
305
+ kind: routeKeyKinds.get(routeKey) ?? "api",
306
+ routeKey,
307
+ filePaths: filePaths.toSorted((a, b) => a.localeCompare(b))
308
+ });
309
+ }
310
+ return issues.toSorted((a, b) => a.routeKey.localeCompare(b.routeKey));
311
+ };
312
+ var formatRouteNamingIssues = ({
313
+ issues,
314
+ context
315
+ }) => {
316
+ const plural = issues.length === 1 ? "" : "s";
317
+ const header = `[RouteNaming] Found ${issues.length} invalid API/sync route file${plural} while ${context}.`;
318
+ const details = issues.map((issue, index) => {
319
+ return `${index + 1}. [${issue.kind.toUpperCase()}] ${issue.filePath}
320
+ reason: ${issue.reason}
321
+ expected: ${issue.expected}`;
322
+ }).join("\n");
323
+ return `${header}
324
+ ${details}`;
325
+ };
326
+ var formatDuplicateRouteKeyIssues = ({
327
+ issues,
328
+ context
329
+ }) => {
330
+ const plural = issues.length === 1 ? "" : "s";
331
+ const header = `[RouteNaming] Found ${issues.length} duplicate normalized route key${plural} while ${context}.`;
332
+ const details = issues.map((issue, index) => {
333
+ const fileList = issue.filePaths.map((filePath) => ` - ${filePath}`).join("\n");
334
+ return `${index + 1}. [${issue.kind.toUpperCase()}] ${issue.routeKey}
335
+ files:
336
+ ${fileList}`;
337
+ }).join("\n");
338
+ return `${header}
339
+ ${details}`;
340
+ };
341
+ var assertValidRouteNaming = ({
342
+ srcDir,
343
+ context
344
+ }) => {
345
+ const issues = collectInvalidRouteNamingIssues(srcDir);
346
+ if (issues.length === 0) {
347
+ return;
348
+ }
349
+ throw new Error(formatRouteNamingIssues({ issues, context }));
350
+ };
351
+ var assertNoDuplicateNormalizedRouteKeys = ({
352
+ srcDir,
353
+ context
354
+ }) => {
355
+ const issues = collectDuplicateNormalizedRouteKeyIssues(srcDir);
356
+ if (issues.length === 0) {
357
+ return;
358
+ }
359
+ throw new Error(formatDuplicateRouteKeyIssues({ issues, context }));
360
+ };
361
+ var walkPageFiles = (dir, results = []) => {
362
+ let entries;
363
+ try {
364
+ entries = fs.readdirSync(dir, { withFileTypes: true });
365
+ } catch {
366
+ return results;
367
+ }
368
+ for (const entry of entries) {
369
+ const fullPath = path.join(dir, entry.name);
370
+ if (entry.isDirectory()) {
371
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
372
+ walkPageFiles(fullPath, results);
373
+ continue;
374
+ }
375
+ if (!entry.isFile()) continue;
376
+ if (entry.name === "page.tsx" || entry.name === "page.jsx") {
377
+ results.push(fullPath);
378
+ }
379
+ }
380
+ return results;
381
+ };
382
+ var collectDuplicatePageRoutes = (srcDir) => {
383
+ const pageFiles = walkPageFiles(srcDir);
384
+ const routeToFiles = /* @__PURE__ */ new Map();
385
+ for (const absoluteFilePath of pageFiles) {
386
+ const relative = normalizePath(path.relative(srcDir, absoluteFilePath));
387
+ const result = validatePagePath(relative);
388
+ if (!result.valid || !result.route) continue;
389
+ const list = routeToFiles.get(result.route) ?? [];
390
+ list.push(normalizePath(path.relative(ROOT_DIR, absoluteFilePath)));
391
+ routeToFiles.set(result.route, list);
392
+ }
393
+ const issues = [];
394
+ for (const [route, filePaths] of routeToFiles) {
395
+ if (filePaths.length > 1) {
396
+ issues.push({ route, filePaths });
397
+ }
398
+ }
399
+ return issues;
400
+ };
401
+ var formatDuplicatePageRouteIssues = ({
402
+ issues,
403
+ context
404
+ }) => {
405
+ const plural = issues.length === 1 ? "" : "s";
406
+ const header = `[RouteNaming] Found ${issues.length} duplicate page route${plural} while ${context}.`;
407
+ const details = issues.map((issue, index) => {
408
+ const fileList = issue.filePaths.map((filePath) => ` - ${filePath}`).join("\n");
409
+ return `${index + 1}. ${issue.route}
410
+ files:
411
+ ${fileList}
412
+ fix: rename or move one of the files so only one page.tsx resolves to "${issue.route}". Remember that "_<folder>" segments are stripped from the URL (invisible-parent rule).`;
413
+ }).join("\n");
414
+ return `${header}
415
+ ${details}`;
416
+ };
417
+ var assertNoDuplicatePageRoutes = ({
418
+ srcDir,
419
+ context
420
+ }) => {
421
+ const issues = collectDuplicatePageRoutes(srcDir);
422
+ if (issues.length === 0) return;
423
+ throw new Error(formatDuplicatePageRouteIssues({ issues, context }));
76
424
  };
77
425
 
78
426
  // src/typeMap/discovery.ts
79
427
  var toForwardSlashRelative = (absolute) => {
80
- const rel = path.relative(ROOT_DIR, absolute);
428
+ const rel = path2.relative(ROOT_DIR2, absolute);
81
429
  return rel.replaceAll("\\", "/");
82
430
  };
83
431
  var walkFiles = (dir, matcher, results = [], visited = /* @__PURE__ */ new Set()) => {
84
432
  let realDir;
85
433
  try {
86
- realDir = fs.realpathSync(dir);
434
+ realDir = fs2.realpathSync(dir);
87
435
  } catch {
88
436
  return results;
89
437
  }
@@ -91,15 +439,15 @@ var walkFiles = (dir, matcher, results = [], visited = /* @__PURE__ */ new Set()
91
439
  visited.add(realDir);
92
440
  const { ignore } = getRoutingRules();
93
441
  try {
94
- const entries = fs.readdirSync(dir, { withFileTypes: true });
442
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
95
443
  for (const entry of entries) {
96
- const fullPath = path.join(dir, entry.name);
444
+ const fullPath = path2.join(dir, entry.name);
97
445
  const relativePath = toForwardSlashRelative(fullPath);
98
446
  if (ignore(relativePath)) continue;
99
447
  if (entry.isDirectory() || entry.isSymbolicLink()) {
100
448
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
101
449
  try {
102
- if (fs.statSync(fullPath).isDirectory()) {
450
+ if (fs2.statSync(fullPath).isDirectory()) {
103
451
  walkFiles(fullPath, matcher, results, visited);
104
452
  }
105
453
  } catch {
@@ -119,47 +467,27 @@ var findAllApiFiles = (srcDir) => {
119
467
  const apiSegment = apiMarkerSegment();
120
468
  return walkFiles(srcDir, (fullPath, entryName) => {
121
469
  const normalized = fullPath.replaceAll("\\", "/");
122
- return isApiFileName(entryName) && normalized.includes(apiSegment);
470
+ return isApiFileName(entryName) && normalized.includes(apiSegment) && isRouteSurfaceFile(normalized);
123
471
  });
124
472
  };
125
473
  var findAllSyncServerFiles = (srcDir) => {
126
474
  const syncSegment = syncMarkerSegment();
127
475
  return walkFiles(srcDir, (fullPath, entryName) => {
128
476
  const normalized = fullPath.replaceAll("\\", "/");
129
- return isSyncServerFileName(entryName) && normalized.includes(syncSegment);
477
+ return isSyncServerFileName(entryName) && normalized.includes(syncSegment) && isRouteSurfaceFile(normalized);
130
478
  });
131
479
  };
132
480
  var findAllSyncClientFiles = (srcDir) => {
133
481
  const syncSegment = syncMarkerSegment();
134
482
  return walkFiles(srcDir, (fullPath, entryName) => {
135
483
  const normalized = fullPath.replaceAll("\\", "/");
136
- return isSyncClientFileName(entryName) && normalized.includes(syncSegment);
484
+ return isSyncClientFileName(entryName) && normalized.includes(syncSegment) && isRouteSurfaceFile(normalized);
137
485
  });
138
486
  };
139
487
 
140
488
  // src/typeMap/routeMeta.ts
141
- import path2 from "path";
489
+ import path3 from "path";
142
490
  import { getSrcDir } from "@luckystack/core";
143
-
144
- // src/routeConventions.ts
145
- var API_VERSION_TOKEN_REGEX = /_v(\d+)$/;
146
- var SYNC_VERSION_TOKEN_REGEX = /_(server|client)_v(\d+)$/;
147
- var ROUTE_NAMING_RULES = {
148
- api: "<name>_v<number>.ts",
149
- syncServer: "<name>_server_v<number>.ts",
150
- syncClient: "<name>_client_v<number>.ts"
151
- };
152
- var ROUTE_NAMING_EXAMPLES = {
153
- api: "updateUser_v1.ts",
154
- syncServer: "updateCounter_server_v1.ts",
155
- syncClient: "updateCounter_client_v1.ts"
156
- };
157
- var isVersionedApiFileName = (fileName) => isApiFileName(fileName);
158
- var isVersionedSyncFileName = (fileName) => isSyncFileName(fileName);
159
- var isVersionedSyncServerFileName = (fileName) => isSyncServerFileName(fileName);
160
- var isVersionedSyncClientFileName = (fileName) => isSyncClientFileName(fileName);
161
-
162
- // src/typeMap/routeMeta.ts
163
491
  var VERSION_SUFFIX_REGEX = API_VERSION_TOKEN_REGEX;
164
492
  var escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
165
493
  var stripVersionSuffix = (name) => {
@@ -173,7 +501,7 @@ var extractVersionFromName = (name) => {
173
501
  var extractPagePath = (filePath) => {
174
502
  const normalized = filePath.replaceAll("\\", "/");
175
503
  const srcDirNormalized = getSrcDir().replaceAll("\\", "/");
176
- const rel = path2.posix.relative(srcDirNormalized, normalized);
504
+ const rel = path3.posix.relative(srcDirNormalized, normalized);
177
505
  if (rel.startsWith("..")) {
178
506
  throw new Error(`[routeMeta] file is outside srcDir \u2014 cannot extract page path: ${filePath}`);
179
507
  }
@@ -191,20 +519,20 @@ var extractApiName = (filePath) => {
191
519
  const normalized = filePath.replaceAll("\\", "/");
192
520
  const { apiMarker } = getRoutingRules();
193
521
  const match = new RegExp(String.raw`${escapeRegExp(apiMarker)}\/(.+)\.ts$`).exec(normalized);
194
- const rawName = match?.[1] ?? path2.basename(filePath, ".ts");
522
+ const rawName = match?.[1] ?? path3.basename(filePath, ".ts");
195
523
  return stripVersionSuffix(rawName);
196
524
  };
197
525
  var extractApiVersion = (filePath) => {
198
526
  const normalized = filePath.replaceAll("\\", "/");
199
527
  const { apiMarker } = getRoutingRules();
200
528
  const match = new RegExp(String.raw`${escapeRegExp(apiMarker)}\/(.+)\.ts$`).exec(normalized);
201
- const rawName = match?.[1] ?? path2.basename(filePath, ".ts");
529
+ const rawName = match?.[1] ?? path3.basename(filePath, ".ts");
202
530
  return extractVersionFromName(rawName) ?? "v1";
203
531
  };
204
532
  var extractSyncPagePath = (filePath) => {
205
533
  const normalized = filePath.replaceAll("\\", "/");
206
534
  const srcDirNormalized = getSrcDir().replaceAll("\\", "/");
207
- const rel = path2.posix.relative(srcDirNormalized, normalized);
535
+ const rel = path3.posix.relative(srcDirNormalized, normalized);
208
536
  const { syncMarker } = getRoutingRules();
209
537
  const match = new RegExp(String.raw`^(?:(.+?)\/)${escapeRegExp(syncMarker)}\/`).exec(rel);
210
538
  if (match) {
@@ -220,7 +548,7 @@ var extractSyncName = (filePath) => {
220
548
  const { syncMarker } = getRoutingRules();
221
549
  const match = new RegExp(String.raw`${escapeRegExp(syncMarker)}\/(.+)\.ts$`).exec(normalized);
222
550
  if (!match) {
223
- const basename = path2.basename(filePath, ".ts");
551
+ const basename = path3.basename(filePath, ".ts");
224
552
  return basename.replace(SYNC_VERSION_TOKEN_REGEX, "");
225
553
  }
226
554
  return (match[1] ?? "").replace(SYNC_VERSION_TOKEN_REGEX, "");
@@ -230,7 +558,7 @@ var extractSyncVersion = (filePath) => {
230
558
  const { syncMarker } = getRoutingRules();
231
559
  const match = new RegExp(String.raw`${escapeRegExp(syncMarker)}\/(.+)\.ts$`).exec(normalized);
232
560
  if (!match) {
233
- const basename = path2.basename(filePath, ".ts");
561
+ const basename = path3.basename(filePath, ".ts");
234
562
  const versionMatch2 = SYNC_VERSION_TOKEN_REGEX.exec(basename);
235
563
  return versionMatch2 ? `v${versionMatch2[2] ?? "1"}` : "v1";
236
564
  }
@@ -240,7 +568,7 @@ var extractSyncVersion = (filePath) => {
240
568
 
241
569
  // src/typeMap/apiMeta.ts
242
570
  import * as ts from "typescript";
243
- import fs2 from "fs";
571
+ import fs3 from "fs";
244
572
  import { inferHttpMethod, tryCatchSync } from "@luckystack/core";
245
573
  var findExportedConst = (sourceFile, name) => {
246
574
  for (const statement of sourceFile.statements) {
@@ -262,7 +590,7 @@ var unwrapExpression = (node) => {
262
590
  };
263
591
  var extractHttpMethod = (filePath, apiName) => {
264
592
  const [error, method] = tryCatchSync(() => {
265
- const content = fs2.readFileSync(filePath, "utf8");
593
+ const content = fs3.readFileSync(filePath, "utf8");
266
594
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
267
595
  const decl = findExportedConst(sourceFile, "httpMethod");
268
596
  const initializer = decl?.initializer ? unwrapExpression(decl.initializer) : void 0;
@@ -279,7 +607,7 @@ var extractHttpMethod = (filePath, apiName) => {
279
607
  };
280
608
  var extractRateLimit = (filePath) => {
281
609
  const [error, rateLimit] = tryCatchSync(() => {
282
- const content = fs2.readFileSync(filePath, "utf8");
610
+ const content = fs3.readFileSync(filePath, "utf8");
283
611
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
284
612
  const decl = findExportedConst(sourceFile, "rateLimit");
285
613
  if (decl?.initializer) {
@@ -325,7 +653,7 @@ var commentToString = (commentValue) => {
325
653
  };
326
654
  var extractDocsMeta = (filePath) => {
327
655
  const [error, docsMeta] = tryCatchSync(() => {
328
- const content = fs2.readFileSync(filePath, "utf8");
656
+ const content = fs3.readFileSync(filePath, "utf8");
329
657
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
330
658
  const result = {};
331
659
  const consumeTag = (tag) => {
@@ -362,7 +690,7 @@ var extractDocsMeta = (filePath) => {
362
690
  };
363
691
  var extractAuth = (filePath) => {
364
692
  const [, auth] = tryCatchSync(() => {
365
- const content = fs2.readFileSync(filePath, "utf8");
693
+ const content = fs3.readFileSync(filePath, "utf8");
366
694
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
367
695
  const decl = findExportedConst(sourceFile, "auth");
368
696
  const authInitializer = decl?.initializer ? unwrapExpression(decl.initializer) : void 0;
@@ -392,27 +720,11 @@ var extractAuth = (filePath) => {
392
720
  };
393
721
 
394
722
  // src/typeMap/emitterArtifacts.ts
395
- import fs3 from "fs";
396
- import path3 from "path";
723
+ import fs4 from "fs";
724
+ import path4 from "path";
397
725
  import * as ts3 from "typescript";
398
726
  import { getGeneratedApiDocsPath, getGeneratedApiSchemasPath, getGeneratedSocketTypesPath } from "@luckystack/core";
399
727
 
400
- // src/internal/mapUtils.ts
401
- var getOrInit = (map, key, factory) => {
402
- const existing = map.get(key);
403
- if (existing !== void 0) return existing;
404
- const fresh = factory();
405
- map.set(key, fresh);
406
- return fresh;
407
- };
408
- var mustGet = (map, key, label) => {
409
- const value = map.get(key);
410
- if (value === void 0) {
411
- throw new Error(`[devkit] invariant violation: '${String(key)}' missing from ${label}`);
412
- }
413
- return value;
414
- };
415
-
416
728
  // src/typeMap/zodEmitter.ts
417
729
  import * as ts2 from "typescript";
418
730
  var wrapOptional = (inner) => `${inner}.optional()`;
@@ -575,13 +887,13 @@ var splitVersionedKey = (value) => {
575
887
  return { name: name ?? "", version: version ?? "v1" };
576
888
  };
577
889
  var writeFileIfChanged = (filePath, content) => {
578
- if (fs3.existsSync(filePath)) {
579
- const currentContent = fs3.readFileSync(filePath, "utf8");
890
+ if (fs4.existsSync(filePath)) {
891
+ const currentContent = fs4.readFileSync(filePath, "utf8");
580
892
  if (currentContent === content) {
581
893
  return false;
582
894
  }
583
895
  }
584
- fs3.writeFileSync(filePath, content, "utf8");
896
+ fs4.writeFileSync(filePath, content, "utf8");
585
897
  return true;
586
898
  };
587
899
  var indentStr = (str, indentText) => {
@@ -1139,9 +1451,9 @@ var writeTypeMapArtifacts = ({
1139
1451
  }
1140
1452
  }
1141
1453
  const docsPath = getGeneratedApiDocsPath();
1142
- const docsDir = path3.dirname(docsPath);
1143
- if (!fs3.existsSync(docsDir)) {
1144
- fs3.mkdirSync(docsDir, { recursive: true });
1454
+ const docsDir = path4.dirname(docsPath);
1455
+ if (!fs4.existsSync(docsDir)) {
1456
+ fs4.mkdirSync(docsDir, { recursive: true });
1145
1457
  }
1146
1458
  const docsContent = JSON.stringify(docsData, null, 2);
1147
1459
  const hasUpdatedDocs = writeFileIfChanged(docsPath, docsContent);
@@ -1149,7 +1461,7 @@ var writeTypeMapArtifacts = ({
1149
1461
  console.log("[TypeMapGenerator] Generated apiDocs.generated.json");
1150
1462
  }
1151
1463
  if (diagnosticsData !== void 0) {
1152
- const diagnosticsPath = path3.join(path3.dirname(docsPath), "apiTypeDiagnostics.generated.json");
1464
+ const diagnosticsPath = path4.join(path4.dirname(docsPath), "apiTypeDiagnostics.generated.json");
1153
1465
  const diagnosticsContent = JSON.stringify(diagnosticsData, null, 2);
1154
1466
  const hasUpdatedDiagnostics = writeFileIfChanged(diagnosticsPath, diagnosticsContent);
1155
1467
  if (hasUpdatedDiagnostics) {
@@ -1166,22 +1478,22 @@ var writeTypeMapArtifacts = ({
1166
1478
 
1167
1479
  // src/typeMap/extractors.ts
1168
1480
  import * as ts5 from "typescript";
1169
- import path5 from "path";
1481
+ import path6 from "path";
1170
1482
 
1171
1483
  // src/typeMap/tsProgram.ts
1172
1484
  import * as ts4 from "typescript";
1173
- import path4 from "path";
1174
- import { ROOT_DIR as ROOT_DIR2, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath2 } from "@luckystack/core";
1485
+ import path5 from "path";
1486
+ import { ROOT_DIR as ROOT_DIR3, getGeneratedSocketTypesPath as getGeneratedSocketTypesPath2 } from "@luckystack/core";
1175
1487
  var cachedProgram = null;
1176
1488
  var getServerProgram = () => {
1177
1489
  if (cachedProgram) return cachedProgram;
1178
- const tsconfigPath = ts4.findConfigFile(ROOT_DIR2, ts4.sys.fileExists.bind(ts4.sys), "tsconfig.server.json");
1490
+ const tsconfigPath = ts4.findConfigFile(ROOT_DIR3, ts4.sys.fileExists.bind(ts4.sys), "tsconfig.server.json");
1179
1491
  if (!tsconfigPath) throw new Error("[TypeProgram] tsconfig.server.json not found");
1180
1492
  const { config } = ts4.readConfigFile(tsconfigPath, ts4.sys.readFile.bind(ts4.sys));
1181
1493
  const { options, fileNames } = ts4.parseJsonConfigFileContent(
1182
1494
  config,
1183
1495
  ts4.sys,
1184
- path4.dirname(tsconfigPath)
1496
+ path5.dirname(tsconfigPath)
1185
1497
  );
1186
1498
  cachedProgram = ts4.createProgram(fileNames, options);
1187
1499
  return cachedProgram;
@@ -1290,10 +1602,10 @@ var getLiteralTypeFromPropertySymbol = (symbol, checker, depth) => {
1290
1602
  return null;
1291
1603
  };
1292
1604
  var normalizeImportPath = (targetFilePath) => {
1293
- const fromDir = path4.dirname(getGeneratedSocketTypesPath2());
1605
+ const fromDir = path5.dirname(getGeneratedSocketTypesPath2());
1294
1606
  const from = fromDir.replaceAll("\\", "/");
1295
1607
  const to = targetFilePath.replaceAll("\\", "/");
1296
- const normalized = path4.posix.relative(from, to).replaceAll("\\", "/");
1608
+ const normalized = path5.posix.relative(from, to).replaceAll("\\", "/");
1297
1609
  const withoutExtension = normalized.replace(/(\.d)?\.(ts|tsx|js|jsx)$/i, "");
1298
1610
  if (withoutExtension.startsWith(".")) return withoutExtension;
1299
1611
  return `./${withoutExtension}`;
@@ -1586,10 +1898,10 @@ var KNOWN_GLOBAL_TYPE_NAMES = /* @__PURE__ */ new Set([
1586
1898
  "JsonPrimitive"
1587
1899
  ]);
1588
1900
  var normalizeImportPath2 = (targetFilePath) => {
1589
- const fromDir = path5.dirname(getGeneratedSocketTypesPath3());
1901
+ const fromDir = path6.dirname(getGeneratedSocketTypesPath3());
1590
1902
  const from = fromDir.replaceAll("\\", "/");
1591
1903
  const to = targetFilePath.replaceAll("\\", "/");
1592
- const normalized = path5.posix.relative(from, to).replaceAll("\\", "/");
1904
+ const normalized = path6.posix.relative(from, to).replaceAll("\\", "/");
1593
1905
  const withoutExtension = normalized.replace(/(\.d)?\.(ts|tsx|js|jsx)$/i, "");
1594
1906
  if (withoutExtension.startsWith(".")) return withoutExtension;
1595
1907
  return `./${withoutExtension}`;
@@ -1943,18 +2255,18 @@ var getSyncClientOutputTypeDetailsFromFile = (filePath) => {
1943
2255
 
1944
2256
  // src/typeMap/functionsMeta.ts
1945
2257
  import * as ts7 from "typescript";
1946
- import fs5 from "fs";
1947
- import path8 from "path";
2258
+ import fs6 from "fs";
2259
+ import path9 from "path";
1948
2260
 
1949
2261
  // src/typeMap/typeContext.ts
1950
2262
  import * as ts6 from "typescript";
1951
- import path6 from "path";
2263
+ import path7 from "path";
1952
2264
  import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath4 } from "@luckystack/core";
1953
2265
  var toGeneratedImportPath = (source, filePath) => {
1954
2266
  if (!source.startsWith(".")) return source;
1955
- const outputDir = path6.dirname(getGeneratedSocketTypesPath4());
1956
- const absoluteSource = path6.resolve(path6.dirname(filePath), source);
1957
- let relPath = path6.relative(outputDir, absoluteSource).replaceAll("\\", "/");
2267
+ const outputDir = path7.dirname(getGeneratedSocketTypesPath4());
2268
+ const absoluteSource = path7.resolve(path7.dirname(filePath), source);
2269
+ let relPath = path7.relative(outputDir, absoluteSource).replaceAll("\\", "/");
1958
2270
  relPath = relPath.replace(/\.tsx?$/, "");
1959
2271
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1960
2272
  return relPath;
@@ -2038,14 +2350,14 @@ var sanitizeTypeAndCollectImports = ({
2038
2350
  };
2039
2351
 
2040
2352
  // src/typeMap/functionsMeta.ts
2041
- import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath5, ROOT_DIR as ROOT_DIR4 } from "@luckystack/core";
2353
+ import { getGeneratedSocketTypesPath as getGeneratedSocketTypesPath5, ROOT_DIR as ROOT_DIR5 } from "@luckystack/core";
2042
2354
 
2043
2355
  // src/functionRegistry.ts
2044
- import fs4 from "fs";
2045
- import path7 from "path";
2046
- import { ROOT_DIR as ROOT_DIR3, getServerFunctionDirs, isTestFile, isTestDirectory } from "@luckystack/core";
2047
- var normalizePath = (value) => value.replaceAll("\\", "/");
2048
- var toWorkspaceRelative = (absolutePath) => normalizePath(path7.relative(ROOT_DIR3, absolutePath));
2356
+ import fs5 from "fs";
2357
+ import path8 from "path";
2358
+ import { ROOT_DIR as ROOT_DIR4, getServerFunctionDirs, isTestFile as isTestFile2, isTestDirectory as isTestDirectory2 } from "@luckystack/core";
2359
+ var normalizePath2 = (value) => value.replaceAll("\\", "/");
2360
+ var toWorkspaceRelative = (absolutePath) => normalizePath2(path8.relative(ROOT_DIR4, absolutePath));
2049
2361
  var formatFunctionKeyPath = (keyPath) => keyPath.join(".");
2050
2362
  var functionModuleFileName = (keyPath) => {
2051
2363
  const fileName = keyPath.at(-1);
@@ -2057,18 +2369,18 @@ var functionModuleFileName = (keyPath) => {
2057
2369
  var formatFunctionKeyConflict = (keyPath, firstSourcePath, secondSourcePath) => `[function-injection] Conflict at \`functions.${formatFunctionKeyPath(keyPath)}\`: defined in both \`${firstSourcePath}\` and \`${secondSourcePath}\`. Delete one \u2014 \`shared/\` is the canonical location for framework re-exports.`;
2058
2370
  var formatFunctionNamespaceConflict = (moduleKeyPath, moduleSourcePath, nestedKeyPath, nestedSourcePath) => `[function-injection] Conflict at \`functions.${formatFunctionKeyPath(moduleKeyPath)}\`: \`${moduleSourcePath}\` defines it as a module, but \`${nestedSourcePath}\` nests \`functions.${formatFunctionKeyPath(nestedKeyPath)}\` underneath it. Rename one \u2014 a key cannot be both a module and a namespace.`;
2059
2371
  var walkFunctionRoot = (dir, rootDir, basePath, collected) => {
2060
- const entries = fs4.readdirSync(dir).toSorted();
2372
+ const entries = fs5.readdirSync(dir).toSorted();
2061
2373
  for (const entry of entries) {
2062
2374
  if (entry === "node_modules") continue;
2063
- const fullPath = path7.join(dir, entry);
2375
+ const fullPath = path8.join(dir, entry);
2064
2376
  if (getRoutingRules().ignore(toWorkspaceRelative(fullPath))) continue;
2065
- if (fs4.statSync(fullPath).isDirectory()) {
2066
- if (isTestDirectory(entry)) continue;
2377
+ if (fs5.statSync(fullPath).isDirectory()) {
2378
+ if (isTestDirectory2(entry)) continue;
2067
2379
  walkFunctionRoot(fullPath, rootDir, [...basePath, entry], collected);
2068
2380
  continue;
2069
2381
  }
2070
2382
  if (!entry.endsWith(".ts")) continue;
2071
- if (isTestFile(entry)) continue;
2383
+ if (isTestFile2(entry)) continue;
2072
2384
  collected.push({
2073
2385
  keyPath: [...basePath, entry.slice(0, -".ts".length)],
2074
2386
  sourcePath: toWorkspaceRelative(fullPath),
@@ -2105,7 +2417,7 @@ var collectFunctionModules = (options = {}) => {
2105
2417
  onConflict(message);
2106
2418
  };
2107
2419
  for (const root of functionRoots) {
2108
- if (!fs4.existsSync(root)) continue;
2420
+ if (!fs5.existsSync(root)) continue;
2109
2421
  const fromThisRoot = [];
2110
2422
  walkFunctionRoot(root, root, [], fromThisRoot);
2111
2423
  for (const module of fromThisRoot) {
@@ -2202,9 +2514,9 @@ var simplifyInferredType = (value) => {
2202
2514
  if (/\bRedis\b/.test(value)) return "Redis";
2203
2515
  return value;
2204
2516
  };
2205
- var getGeneratedFileDir = () => path8.dirname(getGeneratedSocketTypesPath5());
2517
+ var getGeneratedFileDir = () => path9.dirname(getGeneratedSocketTypesPath5());
2206
2518
  var sourceModuleSpecifier = (sourceFilePath) => {
2207
- const relative = path8.relative(getGeneratedFileDir(), sourceFilePath).replaceAll("\\", "/").replace(/\.tsx?$/i, "");
2519
+ const relative = path9.relative(getGeneratedFileDir(), sourceFilePath).replaceAll("\\", "/").replace(/\.tsx?$/i, "");
2208
2520
  return relative.startsWith(".") ? relative : `./${relative}`;
2209
2521
  };
2210
2522
  var portableInferredValueType = (inferred, sourceFilePath, exportName) => {
@@ -2215,9 +2527,9 @@ var relativizeModuleSpecifier = (specifier, sourceFilePath) => {
2215
2527
  if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
2216
2528
  return specifier;
2217
2529
  }
2218
- const absolute = path8.resolve(path8.dirname(sourceFilePath), specifier);
2219
- const rel = path8.relative(getGeneratedFileDir(), absolute);
2220
- const normalized = rel.split(path8.sep).join("/");
2530
+ const absolute = path9.resolve(path9.dirname(sourceFilePath), specifier);
2531
+ const rel = path9.relative(getGeneratedFileDir(), absolute);
2532
+ const normalized = rel.split(path9.sep).join("/");
2221
2533
  return normalized.startsWith(".") ? normalized : `./${normalized}`;
2222
2534
  };
2223
2535
  var findProgramVariableDeclaration = (sourceFile, exportName) => {
@@ -2391,7 +2703,7 @@ var findSignatureForExport = (name, sourceFile, rawContent, filePath, availableE
2391
2703
  };
2392
2704
  var parseFunctionFile = (fullPath, collectors) => {
2393
2705
  try {
2394
- const rawContent = fs5.readFileSync(fullPath, "utf8");
2706
+ const rawContent = fs6.readFileSync(fullPath, "utf8");
2395
2707
  const sourceFile = ts7.createSourceFile(fullPath, rawContent, ts7.ScriptTarget.Latest, true);
2396
2708
  const { availableExports, fileImports } = parseFileTypeContext(rawContent);
2397
2709
  const program = getServerProgram();
@@ -2461,406 +2773,95 @@ var parseFunctionFile = (fullPath, collectors) => {
2461
2773
  }
2462
2774
  }
2463
2775
  if (ts7.isExportAssignment(statement) && !statement.isExportEquals && ts7.isIdentifier(statement.expression)) {
2464
- defaultExportName = statement.expression.text;
2465
- }
2466
- if (ts7.isExportDeclaration(statement) && !statement.exportClause && statement.moduleSpecifier && ts7.isStringLiteral(statement.moduleSpecifier)) {
2467
- wildcardReExport = relativizeModuleSpecifier(statement.moduleSpecifier.text, fullPath);
2468
- }
2469
- }
2470
- return { kind: "file", exports, defaultExportName, wildcardReExport, sourcePath: fullPath };
2471
- } catch (error) {
2472
- console.error(`[TypeMapGenerator] Error parsing functions file ${fullPath}:`, error);
2473
- return null;
2474
- }
2475
- };
2476
- var insertFileNode = (root, keyPath, file) => {
2477
- let target = root;
2478
- for (const segment of keyPath.slice(0, -1)) {
2479
- const existing = target.children.get(segment);
2480
- if (existing?.kind === "dir") {
2481
- target = existing;
2482
- continue;
2483
- }
2484
- const created = { kind: "dir", children: /* @__PURE__ */ new Map(), sourcePath: segment };
2485
- target.children.set(segment, created);
2486
- target = created;
2487
- }
2488
- target.children.set(functionModuleFileName(keyPath), file);
2489
- };
2490
- var serializeIRDir = (dir, indent) => {
2491
- let output = "";
2492
- for (const [name, child] of dir.children) {
2493
- if (child.kind === "dir") {
2494
- const subOutput = serializeIRDir(child, `${indent} `);
2495
- if (subOutput.trim()) {
2496
- output += `${indent}${name}: {
2497
- ${subOutput}${indent}};
2498
- `;
2499
- }
2500
- continue;
2501
- }
2502
- const exportsCopy = new Map(child.exports);
2503
- const defaultExportName = child.defaultExportName;
2504
- const defaultSig = defaultExportName ? exportsCopy.get(defaultExportName) : void 0;
2505
- if (defaultSig && defaultExportName) exportsCopy.delete(defaultExportName);
2506
- if (child.wildcardReExport && exportsCopy.size === 0 && !defaultSig) {
2507
- output += `${indent}${name}: typeof import('${child.wildcardReExport}');
2508
- `;
2509
- continue;
2510
- }
2511
- if (!defaultSig && exportsCopy.size === 1 && exportsCopy.has("default")) {
2512
- const reExportSig = exportsCopy.get("default");
2513
- if (reExportSig) {
2514
- exportsCopy.delete("default");
2515
- exportsCopy.set(name, reExportSig);
2516
- }
2517
- }
2518
- let fileOutput = "";
2519
- for (const [exportName, sig] of exportsCopy) {
2520
- fileOutput += `${indent} ${exportName}: ${sig};
2521
- `;
2522
- }
2523
- if (defaultSig && !fileOutput.trim()) {
2524
- fileOutput += `${indent} ${name}: ${defaultSig};
2525
- `;
2526
- }
2527
- if (fileOutput) {
2528
- output += `${indent}${name}: {
2529
- ${fileOutput}${indent}};
2530
- `;
2531
- }
2532
- }
2533
- return output;
2534
- };
2535
- var generateServerFunctions = (collectors) => {
2536
- const modules = collectFunctionModules();
2537
- if (modules.length === 0) return "";
2538
- const merged = { kind: "dir", children: /* @__PURE__ */ new Map(), sourcePath: "<merged>" };
2539
- for (const module of modules) {
2540
- const absolutePath = path8.resolve(ROOT_DIR4, module.sourcePath);
2541
- const parsed = parseFunctionFile(absolutePath, collectors);
2542
- if (!parsed) continue;
2543
- if (parsed.exports.size === 0 && parsed.defaultExportName === null && parsed.wildcardReExport === null) {
2544
- continue;
2545
- }
2546
- insertFileNode(merged, module.keyPath, parsed);
2547
- }
2548
- return serializeIRDir(merged, " ");
2549
- };
2550
-
2551
- // src/typeMapGenerator.ts
2552
- import { getSrcDir as getSrcDir2 } from "@luckystack/core";
2553
-
2554
- // src/routeNamingValidation.ts
2555
- import fs6 from "fs";
2556
- import path9 from "path";
2557
- import { ROOT_DIR as ROOT_DIR5 } from "@luckystack/core";
2558
- var normalizePath2 = (value) => {
2559
- return value.replaceAll("\\", "/");
2560
- };
2561
- var toRel = (absolute) => path9.relative(ROOT_DIR5, absolute).replaceAll("\\", "/");
2562
- var isInsidePrivateRouteSubfolder = (fullPath) => {
2563
- const { privateFolderPrefix, apiMarker, syncMarker } = getRoutingRules();
2564
- const segments = normalizePath2(fullPath).split("/");
2565
- const markerIndex = segments.findIndex((segment) => segment === apiMarker || segment === syncMarker);
2566
- if (markerIndex === -1) return false;
2567
- return segments.slice(markerIndex + 1).some((segment) => segment.startsWith(privateFolderPrefix));
2568
- };
2569
- var walkRouteFiles = (dir, results = []) => {
2570
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
2571
- const apiSeg = apiMarkerSegment();
2572
- const syncSeg = syncMarkerSegment();
2573
- const { ignore } = getRoutingRules();
2574
- for (const entry of entries) {
2575
- const fullPath = path9.join(dir, entry.name);
2576
- const normalizedFullPath = normalizePath2(fullPath);
2577
- if (ignore(toRel(fullPath))) continue;
2578
- if (isInsidePrivateRouteSubfolder(fullPath)) continue;
2579
- if (entry.isDirectory()) {
2580
- if (entry.name.startsWith(".") || entry.name === "node_modules") {
2581
- continue;
2582
- }
2583
- walkRouteFiles(fullPath, results);
2584
- continue;
2585
- }
2586
- if (!entry.isFile() || !entry.name.endsWith(".ts")) {
2587
- continue;
2588
- }
2589
- if (isRouteTestFile(entry.name)) {
2590
- continue;
2591
- }
2592
- if (normalizedFullPath.includes(apiSeg) || normalizedFullPath.includes(syncSeg)) {
2593
- results.push(fullPath);
2594
- }
2595
- }
2596
- return results;
2597
- };
2598
- var getFileRouteToken = ({
2599
- normalizedFilePath,
2600
- marker
2601
- }) => {
2602
- const markerIndex = normalizedFilePath.indexOf(marker);
2603
- if (markerIndex === -1) {
2604
- return "";
2605
- }
2606
- const tokenStart = markerIndex + marker.length;
2607
- return normalizedFilePath.slice(tokenStart, normalizedFilePath.length - ".ts".length);
2608
- };
2609
- var validateRouteFilePath = (filePath) => {
2610
- const issues = [];
2611
- const normalizedFilePath = normalizePath2(path9.resolve(filePath));
2612
- const fileName = path9.basename(filePath);
2613
- const apiSeg = apiMarkerSegment();
2614
- const syncSeg = syncMarkerSegment();
2615
- if (normalizedFilePath.includes(apiSeg)) {
2616
- const apiRouteToken = getFileRouteToken({ normalizedFilePath, marker: apiSeg });
2617
- if (apiRouteToken.includes("/")) {
2618
- issues.push({
2619
- kind: "api",
2620
- filePath: normalizedFilePath,
2621
- reason: 'API route token cannot contain nested path segments ("/").',
2622
- expected: ROUTE_NAMING_RULES.api
2623
- });
2624
- }
2625
- if (!isVersionedApiFileName(fileName)) {
2626
- issues.push({
2627
- kind: "api",
2628
- filePath: normalizedFilePath,
2629
- reason: "API filename does not match versioned naming.",
2630
- expected: ROUTE_NAMING_RULES.api
2631
- });
2632
- }
2633
- }
2634
- if (normalizedFilePath.includes(syncSeg)) {
2635
- const syncRouteToken = getFileRouteToken({ normalizedFilePath, marker: syncSeg });
2636
- if (syncRouteToken.includes("/")) {
2637
- issues.push({
2638
- kind: "sync",
2639
- filePath: normalizedFilePath,
2640
- reason: 'Sync route token cannot contain nested path segments ("/").',
2641
- expected: `${ROUTE_NAMING_RULES.syncServer} or ${ROUTE_NAMING_RULES.syncClient}`
2642
- });
2643
- }
2644
- if (!isVersionedSyncFileName(fileName)) {
2645
- issues.push({
2646
- kind: "sync",
2647
- filePath: normalizedFilePath,
2648
- reason: "Sync filename does not match versioned naming.",
2649
- expected: `${ROUTE_NAMING_RULES.syncServer} or ${ROUTE_NAMING_RULES.syncClient}`
2650
- });
2776
+ defaultExportName = statement.expression.text;
2777
+ }
2778
+ if (ts7.isExportDeclaration(statement) && !statement.exportClause && statement.moduleSpecifier && ts7.isStringLiteral(statement.moduleSpecifier)) {
2779
+ wildcardReExport = relativizeModuleSpecifier(statement.moduleSpecifier.text, fullPath);
2780
+ }
2651
2781
  }
2652
- }
2653
- return issues;
2654
- };
2655
- var resolveApiRouteKey = ({
2656
- srcDir,
2657
- filePath
2658
- }) => {
2659
- const rules2 = getRoutingRules();
2660
- const relativePath = normalizePath2(path9.relative(srcDir, filePath));
2661
- const segments = relativePath.split("/");
2662
- const apiIndex = segments.indexOf(rules2.apiMarker);
2663
- if (apiIndex === -1 || apiIndex === segments.length - 1) {
2664
- return null;
2665
- }
2666
- const pageLocation = segments.slice(0, apiIndex).join("/");
2667
- const apiFilePath = segments.slice(apiIndex + 1).join("/");
2668
- const rawApiName = apiFilePath.replace(/\.ts$/, "");
2669
- const versionMatch = rawApiName.match(rules2.apiVersionRegex);
2670
- if (!versionMatch) {
2671
- return null;
2672
- }
2673
- const version = `v${versionMatch[1]}`;
2674
- const apiName = rawApiName.replace(rules2.apiVersionRegex, "");
2675
- const mappedPageLocation = pageLocation || "system";
2676
- return `api/${mappedPageLocation}/${apiName}/${version}`;
2677
- };
2678
- var resolveSyncRouteKey = ({
2679
- srcDir,
2680
- filePath
2681
- }) => {
2682
- const rules2 = getRoutingRules();
2683
- const relativePath = normalizePath2(path9.relative(srcDir, filePath));
2684
- const segments = relativePath.split("/");
2685
- const syncIndex = segments.indexOf(rules2.syncMarker);
2686
- if (syncIndex === -1 || syncIndex === segments.length - 1) {
2687
- return null;
2688
- }
2689
- const pageLocation = segments.slice(0, syncIndex).join("/");
2690
- const syncFilePath = segments.slice(syncIndex + 1).join("/");
2691
- const rawSyncName = syncFilePath.replace(/\.ts$/, "");
2692
- const syncMatch = rawSyncName.match(rules2.syncVersionRegex);
2693
- if (!syncMatch) {
2782
+ return { kind: "file", exports, defaultExportName, wildcardReExport, sourcePath: fullPath };
2783
+ } catch (error) {
2784
+ console.error(`[TypeMapGenerator] Error parsing functions file ${fullPath}:`, error);
2694
2785
  return null;
2695
2786
  }
2696
- const kind = syncMatch[1];
2697
- const version = `v${syncMatch[2]}`;
2698
- const syncName = rawSyncName.replace(rules2.syncVersionRegex, "");
2699
- const routeBaseKey = pageLocation ? `sync/${pageLocation}/${syncName}/${version}` : `sync/${syncName}/${version}`;
2700
- return `${routeBaseKey}_${kind}`;
2701
2787
  };
2702
- var collectInvalidRouteNamingIssues = (srcDir) => {
2703
- const allRouteFiles = walkRouteFiles(srcDir);
2704
- const issues = [];
2705
- for (const filePath of allRouteFiles) {
2706
- issues.push(...validateRouteFilePath(filePath));
2788
+ var insertFileNode = (root, keyPath, file) => {
2789
+ let target = root;
2790
+ for (const segment of keyPath.slice(0, -1)) {
2791
+ const existing = target.children.get(segment);
2792
+ if (existing?.kind === "dir") {
2793
+ target = existing;
2794
+ continue;
2795
+ }
2796
+ const created = { kind: "dir", children: /* @__PURE__ */ new Map(), sourcePath: segment };
2797
+ target.children.set(segment, created);
2798
+ target = created;
2707
2799
  }
2708
- return issues.toSorted((a, b) => a.filePath.localeCompare(b.filePath));
2800
+ target.children.set(functionModuleFileName(keyPath), file);
2709
2801
  };
2710
- var collectDuplicateNormalizedRouteKeyIssues = (srcDir) => {
2711
- const allRouteFiles = walkRouteFiles(srcDir);
2712
- const routeKeyToFilePaths = /* @__PURE__ */ new Map();
2713
- const routeKeyKinds = /* @__PURE__ */ new Map();
2714
- const apiSeg = apiMarkerSegment();
2715
- const syncSeg = syncMarkerSegment();
2716
- for (const filePath of allRouteFiles) {
2717
- const normalizedFilePath = normalizePath2(path9.resolve(filePath));
2718
- if (normalizedFilePath.includes(apiSeg)) {
2719
- const routeKey = resolveApiRouteKey({ srcDir, filePath });
2720
- if (!routeKey) {
2721
- continue;
2802
+ var serializeIRDir = (dir, indent) => {
2803
+ let output = "";
2804
+ for (const [name, child] of dir.children) {
2805
+ if (child.kind === "dir") {
2806
+ const subOutput = serializeIRDir(child, `${indent} `);
2807
+ if (subOutput.trim()) {
2808
+ output += `${indent}${name}: {
2809
+ ${subOutput}${indent}};
2810
+ `;
2722
2811
  }
2723
- getOrInit(routeKeyToFilePaths, routeKey, () => []).push(normalizedFilePath);
2724
- routeKeyKinds.set(routeKey, "api");
2725
2812
  continue;
2726
2813
  }
2727
- if (normalizedFilePath.includes(syncSeg)) {
2728
- const routeKey = resolveSyncRouteKey({ srcDir, filePath });
2729
- if (!routeKey) {
2730
- continue;
2814
+ const exportsCopy = new Map(child.exports);
2815
+ const defaultExportName = child.defaultExportName;
2816
+ const defaultSig = defaultExportName ? exportsCopy.get(defaultExportName) : void 0;
2817
+ if (defaultSig && defaultExportName) exportsCopy.delete(defaultExportName);
2818
+ if (child.wildcardReExport && exportsCopy.size === 0 && !defaultSig) {
2819
+ output += `${indent}${name}: typeof import('${child.wildcardReExport}');
2820
+ `;
2821
+ continue;
2822
+ }
2823
+ if (!defaultSig && exportsCopy.size === 1 && exportsCopy.has("default")) {
2824
+ const reExportSig = exportsCopy.get("default");
2825
+ if (reExportSig) {
2826
+ exportsCopy.delete("default");
2827
+ exportsCopy.set(name, reExportSig);
2731
2828
  }
2732
- getOrInit(routeKeyToFilePaths, routeKey, () => []).push(normalizedFilePath);
2733
- routeKeyKinds.set(routeKey, "sync");
2734
2829
  }
2735
- }
2736
- const issues = [];
2737
- for (const [routeKey, filePaths] of routeKeyToFilePaths.entries()) {
2738
- if (filePaths.length < 2) {
2739
- continue;
2830
+ let fileOutput = "";
2831
+ for (const [exportName, sig] of exportsCopy) {
2832
+ fileOutput += `${indent} ${exportName}: ${sig};
2833
+ `;
2740
2834
  }
2741
- issues.push({
2742
- kind: routeKeyKinds.get(routeKey) ?? "api",
2743
- routeKey,
2744
- filePaths: filePaths.toSorted((a, b) => a.localeCompare(b))
2745
- });
2746
- }
2747
- return issues.toSorted((a, b) => a.routeKey.localeCompare(b.routeKey));
2748
- };
2749
- var formatRouteNamingIssues = ({
2750
- issues,
2751
- context
2752
- }) => {
2753
- const plural = issues.length === 1 ? "" : "s";
2754
- const header = `[RouteNaming] Found ${issues.length} invalid API/sync route file${plural} while ${context}.`;
2755
- const details = issues.map((issue, index) => {
2756
- return `${index + 1}. [${issue.kind.toUpperCase()}] ${issue.filePath}
2757
- reason: ${issue.reason}
2758
- expected: ${issue.expected}`;
2759
- }).join("\n");
2760
- return `${header}
2761
- ${details}`;
2762
- };
2763
- var formatDuplicateRouteKeyIssues = ({
2764
- issues,
2765
- context
2766
- }) => {
2767
- const plural = issues.length === 1 ? "" : "s";
2768
- const header = `[RouteNaming] Found ${issues.length} duplicate normalized route key${plural} while ${context}.`;
2769
- const details = issues.map((issue, index) => {
2770
- const fileList = issue.filePaths.map((filePath) => ` - ${filePath}`).join("\n");
2771
- return `${index + 1}. [${issue.kind.toUpperCase()}] ${issue.routeKey}
2772
- files:
2773
- ${fileList}`;
2774
- }).join("\n");
2775
- return `${header}
2776
- ${details}`;
2777
- };
2778
- var assertValidRouteNaming = ({
2779
- srcDir,
2780
- context
2781
- }) => {
2782
- const issues = collectInvalidRouteNamingIssues(srcDir);
2783
- if (issues.length === 0) {
2784
- return;
2785
- }
2786
- throw new Error(formatRouteNamingIssues({ issues, context }));
2787
- };
2788
- var assertNoDuplicateNormalizedRouteKeys = ({
2789
- srcDir,
2790
- context
2791
- }) => {
2792
- const issues = collectDuplicateNormalizedRouteKeyIssues(srcDir);
2793
- if (issues.length === 0) {
2794
- return;
2795
- }
2796
- throw new Error(formatDuplicateRouteKeyIssues({ issues, context }));
2797
- };
2798
- var walkPageFiles = (dir, results = []) => {
2799
- let entries;
2800
- try {
2801
- entries = fs6.readdirSync(dir, { withFileTypes: true });
2802
- } catch {
2803
- return results;
2804
- }
2805
- for (const entry of entries) {
2806
- const fullPath = path9.join(dir, entry.name);
2807
- if (entry.isDirectory()) {
2808
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2809
- walkPageFiles(fullPath, results);
2810
- continue;
2835
+ if (defaultSig && !fileOutput.trim()) {
2836
+ fileOutput += `${indent} ${name}: ${defaultSig};
2837
+ `;
2811
2838
  }
2812
- if (!entry.isFile()) continue;
2813
- if (entry.name === "page.tsx" || entry.name === "page.jsx") {
2814
- results.push(fullPath);
2839
+ if (fileOutput) {
2840
+ output += `${indent}${name}: {
2841
+ ${fileOutput}${indent}};
2842
+ `;
2815
2843
  }
2816
2844
  }
2817
- return results;
2845
+ return output;
2818
2846
  };
2819
- var collectDuplicatePageRoutes = (srcDir) => {
2820
- const pageFiles = walkPageFiles(srcDir);
2821
- const routeToFiles = /* @__PURE__ */ new Map();
2822
- for (const absoluteFilePath of pageFiles) {
2823
- const relative = normalizePath2(path9.relative(srcDir, absoluteFilePath));
2824
- const result = validatePagePath(relative);
2825
- if (!result.valid || !result.route) continue;
2826
- const list = routeToFiles.get(result.route) ?? [];
2827
- list.push(normalizePath2(path9.relative(ROOT_DIR5, absoluteFilePath)));
2828
- routeToFiles.set(result.route, list);
2829
- }
2830
- const issues = [];
2831
- for (const [route, filePaths] of routeToFiles) {
2832
- if (filePaths.length > 1) {
2833
- issues.push({ route, filePaths });
2847
+ var generateServerFunctions = (collectors) => {
2848
+ const modules = collectFunctionModules();
2849
+ if (modules.length === 0) return "";
2850
+ const merged = { kind: "dir", children: /* @__PURE__ */ new Map(), sourcePath: "<merged>" };
2851
+ for (const module of modules) {
2852
+ const absolutePath = path9.resolve(ROOT_DIR5, module.sourcePath);
2853
+ const parsed = parseFunctionFile(absolutePath, collectors);
2854
+ if (!parsed) continue;
2855
+ if (parsed.exports.size === 0 && parsed.defaultExportName === null && parsed.wildcardReExport === null) {
2856
+ continue;
2834
2857
  }
2858
+ insertFileNode(merged, module.keyPath, parsed);
2835
2859
  }
2836
- return issues;
2837
- };
2838
- var formatDuplicatePageRouteIssues = ({
2839
- issues,
2840
- context
2841
- }) => {
2842
- const plural = issues.length === 1 ? "" : "s";
2843
- const header = `[RouteNaming] Found ${issues.length} duplicate page route${plural} while ${context}.`;
2844
- const details = issues.map((issue, index) => {
2845
- const fileList = issue.filePaths.map((filePath) => ` - ${filePath}`).join("\n");
2846
- return `${index + 1}. ${issue.route}
2847
- files:
2848
- ${fileList}
2849
- fix: rename or move one of the files so only one page.tsx resolves to "${issue.route}". Remember that "_<folder>" segments are stripped from the URL (invisible-parent rule).`;
2850
- }).join("\n");
2851
- return `${header}
2852
- ${details}`;
2853
- };
2854
- var assertNoDuplicatePageRoutes = ({
2855
- srcDir,
2856
- context
2857
- }) => {
2858
- const issues = collectDuplicatePageRoutes(srcDir);
2859
- if (issues.length === 0) return;
2860
- throw new Error(formatDuplicatePageRouteIssues({ issues, context }));
2860
+ return serializeIRDir(merged, " ");
2861
2861
  };
2862
2862
 
2863
2863
  // src/typeMapGenerator.ts
2864
+ import { getSrcDir as getSrcDir2 } from "@luckystack/core";
2864
2865
  var collectApiTypes = (apiFiles, collectors) => {
2865
2866
  const { namedImports, unresolvedTypeAliases, quiet } = collectors;
2866
2867
  const typesByPage = /* @__PURE__ */ new Map();
@@ -3085,7 +3086,7 @@ registerDefaultTemplateRules();
3085
3086
  import fs7 from "fs";
3086
3087
  import path10 from "path";
3087
3088
  import { pathToFileURL } from "url";
3088
- import { tryCatch, getSrcDir as getSrcDir3, ROOT_DIR as ROOT_DIR6 } from "@luckystack/core";
3089
+ import { tryCatch, getSrcDir as getSrcDir3, ROOT_DIR as ROOT_DIR6, isTestFile as isTestFile3 } from "@luckystack/core";
3089
3090
 
3090
3091
  // src/runtimeTypeResolver.ts
3091
3092
  import * as ts8 from "typescript";
@@ -3481,6 +3482,9 @@ var resolveApiRouteMetaFromPath = (filePath) => {
3481
3482
  if (!normalizedAbsolutePath.startsWith(normalizedSrcDir) || !normalizedAbsolutePath.endsWith(".ts")) {
3482
3483
  return null;
3483
3484
  }
3485
+ if (!isRouteSurfaceFile(normalizedAbsolutePath)) {
3486
+ return null;
3487
+ }
3484
3488
  const rules2 = getRoutingRules();
3485
3489
  const relativePath = normalizePath3(path10.relative(getSrcDir3(), absolutePath));
3486
3490
  const segments = relativePath.split("/");
@@ -3508,6 +3512,9 @@ var resolveSyncRouteMetaFromPath = (filePath) => {
3508
3512
  if (!normalizedAbsolutePath.startsWith(normalizedSrcDir) || !normalizedAbsolutePath.endsWith(".ts")) {
3509
3513
  return null;
3510
3514
  }
3515
+ if (!isRouteSurfaceFile(normalizedAbsolutePath)) {
3516
+ return null;
3517
+ }
3511
3518
  const rules2 = getRoutingRules();
3512
3519
  const relativePath = normalizePath3(path10.relative(getSrcDir3(), absolutePath));
3513
3520
  const segments = relativePath.split("/");
@@ -3560,8 +3567,9 @@ var collectTsFiles = (dir, relativeTo = "") => {
3560
3567
  if (isIgnoredPath(entryPath)) continue;
3561
3568
  const relPath = relativeTo ? `${relativeTo}/${entry}` : entry;
3562
3569
  if (fs7.statSync(entryPath).isDirectory()) {
3570
+ if (isNonRouteDirectory(entry)) continue;
3563
3571
  results.push(...collectTsFiles(entryPath, relPath));
3564
- } else if (entry.endsWith(".ts") && !isRouteTestFile(entry)) {
3572
+ } else if (entry.endsWith(".ts") && !isTestFile3(entry)) {
3565
3573
  results.push(relPath);
3566
3574
  }
3567
3575
  }
@@ -3600,7 +3608,7 @@ var upsertApiFromFile = async (filePath) => {
3600
3608
  const routeMeta = resolveApiRouteMetaFromPath(filePath);
3601
3609
  if (!routeMeta) {
3602
3610
  const normalized = normalizePath3(path10.resolve(filePath));
3603
- if (normalized.includes("/_api/") && normalized.endsWith(".ts") && !isRouteTestFile(normalized)) {
3611
+ if (normalized.endsWith(".ts") && normalized.includes(apiMarkerSegment()) && isRouteSurfaceFile(normalized)) {
3604
3612
  console.log(
3605
3613
  `[loader][api] invalid filename: ${normalized}. Expected <name>_v<number>.ts. File will not be loaded.`,
3606
3614
  "red"
@@ -3650,7 +3658,7 @@ var scanApiFolder = async (file, basePath = "") => {
3650
3658
  const fullPath = path10.join(getSrcDir3(), basePath, file);
3651
3659
  if (isIgnoredPath(fullPath)) return;
3652
3660
  if (!fs7.statSync(fullPath).isDirectory()) return;
3653
- if (!file.toLowerCase().endsWith("api")) {
3661
+ if (file !== getRoutingRules().apiMarker) {
3654
3662
  const subFolders = fs7.readdirSync(fullPath);
3655
3663
  for (const sub of subFolders) {
3656
3664
  await scanApiFolder(sub, path10.join(basePath, file));
@@ -3713,7 +3721,7 @@ var upsertSyncFromFile = async (filePath) => {
3713
3721
  const routeMeta = resolveSyncRouteMetaFromPath(filePath);
3714
3722
  if (!routeMeta) {
3715
3723
  const normalized = normalizePath3(path10.resolve(filePath));
3716
- if (normalized.includes(`/${getRoutingRules().syncMarker}/`) && normalized.endsWith(".ts") && !isRouteTestFile(normalized)) {
3724
+ if (normalized.endsWith(".ts") && normalized.includes(syncMarkerSegment()) && isRouteSurfaceFile(normalized)) {
3717
3725
  console.log(
3718
3726
  `[loader][sync] invalid filename: ${normalized}. Expected <name>_server_v<number>.ts or <name>_client_v<number>.ts. File will not be loaded.`,
3719
3727
  "red"
@@ -3768,7 +3776,7 @@ var scanSyncFolder = async (file, basePath = "") => {
3768
3776
  const fullPath = path10.join(getSrcDir3(), basePath, file);
3769
3777
  if (isIgnoredPath(fullPath)) return;
3770
3778
  if (!fs7.statSync(fullPath).isDirectory()) return;
3771
- if (!file.toLowerCase().endsWith("sync")) {
3779
+ if (file !== getRoutingRules().syncMarker) {
3772
3780
  const subFolders = fs7.readdirSync(fullPath);
3773
3781
  for (const sub of subFolders) {
3774
3782
  await scanSyncFolder(sub, path10.join(basePath, file));