@luckystack/devkit 0.10.1 → 0.10.2

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/CHANGELOG.md CHANGED
@@ -5,7 +5,11 @@ All notable changes to `@luckystack/devkit` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.10.2] - 2026-10-09
9
+
10
+ ### Added
11
+
12
+ - The type-map generator emits `syncMetaMap` / `getSyncMeta` (auth + rateLimit per sync route) in `apiTypes.generated.ts` and `syncInputSchemas` / `getSyncInputSchema` (Zod schema of each sync route's `clientInput`) in `apiInputSchemas.generated.ts` — the sync counterparts of `apiMetaMap` and `apiInputSchemas`. Only routes with a `_server_v<N>.ts` file are listed. This lets the `@luckystack/test-runner` auto-sweep layers reach sync routes, not just API routes. A sync input that falls back to `z.any()` is now reported in `apiTypeDiagnostics.generated.json`, like an API input.
9
13
 
10
14
  ## [0.9.2] - 2026-09-01
11
15
 
package/CLAUDE.md CHANGED
@@ -42,7 +42,7 @@ Dev-time tooling for LuckyStack projects: file-based route discovery + in-memory
42
42
  | `devSyncs` | In-memory map of `route key (with _server / _client suffix) -> server entry record or client callback function`. | -> docs/loader-pipeline.md |
43
43
  | `devFunctions` | Nested mirror of `serverFunctionsDir`. Each leaf is a module's named + default exports, with later folder scans merging onto existing nodes. | -> docs/loader-pipeline.md |
44
44
  | `setupWatchers()` | Boots the chokidar watchers for `srcDir` / each `serverFunctionDirs` root / `sharedDir` (legacy singular `serverFunctionsDir` still honored when set), coalesces hot-reload + type-map regenerations, fires initial background type-map generation. No-op when `NODE_ENV === 'production'`. | -> docs/hot-reload.md |
45
- | `generateTypeMapFile(options?)` | Discovers all `_api/` and `_sync/` files, runs the TypeChecker-backed extractors, emits `apiTypes.generated.ts`, `apiInputSchemas.generated.ts`, and `apiDocs.generated.json`. Aborts if any unresolved type symbol is found. | -> docs/type-map-generation.md |
45
+ | `generateTypeMapFile(options?)` | Discovers all `_api/` and `_sync/` files, runs the TypeChecker-backed extractors, emits `apiTypes.generated.ts` (incl. `apiMetaMap` + `syncMetaMap`), `apiInputSchemas.generated.ts` (`apiInputSchemas` + `syncInputSchemas`), and `apiDocs.generated.json`. Aborts if any unresolved type symbol is found. | -> docs/type-map-generation.md |
46
46
  | `getInputTypeFromFile(filePath)` | Public extractor: returns the inline-expanded `data` input type text for one API file. Consumed by the dev loader to drive `runtimeTypeValidation`. | -> docs/type-map-generation.md |
47
47
  | `getSyncClientDataType(filePath)` | Public extractor: returns the inline-expanded `clientInput` (sync server) or client data (sync client) type text. | -> docs/type-map-generation.md |
48
48
  | `API_VERSION_TOKEN_REGEX` | Canonical `_v<number>` regex used to parse API filenames. | -> docs/loader-pipeline.md |
package/dist/index.js CHANGED
@@ -867,6 +867,75 @@ var findExtractionFailure = (route, kind, field) => {
867
867
  };
868
868
 
869
869
  // src/typeMap/emitterArtifacts.ts
870
+ var compareVersions = (a, b) => a.version.localeCompare(b.version, void 0, { numeric: true });
871
+ var renderRouteMetaFields = (authValue, rateLimit) => {
872
+ const auth = authValue && typeof authValue === "object" ? authValue : { login: false, hasAdditional: false };
873
+ const rateLimitPart = rateLimit === void 0 ? "" : `, rateLimit: ${rateLimit === false ? "false" : String(rateLimit)}`;
874
+ const additionalPart = auth.additional && auth.additional.length > 0 ? `, additional: ${JSON.stringify(auth.additional)}` : "";
875
+ return `auth: { login: ${auth.login ? "true" : "false"}${additionalPart}${auth.hasAdditional ? ", hasAdditional: true" : ""} }${rateLimitPart}`;
876
+ };
877
+ var inputSchemaSource = (typeText) => typeTextToZodSource(typeText) ?? "z.any() /* unparseable input type */";
878
+ var groupServerSyncRoutes = (syncTypesByPage) => {
879
+ const pages = [];
880
+ for (const pagePath of [...syncTypesByPage.keys()].toSorted()) {
881
+ const grouped = /* @__PURE__ */ new Map();
882
+ for (const [syncKey, entry] of mustGet(syncTypesByPage, pagePath, "syncTypesByPage")) {
883
+ if (!entry.serverMeta) continue;
884
+ const { name, version } = splitVersionedKey(syncKey);
885
+ getOrInit(grouped, name, () => []).push({ version, clientInput: entry.clientInput, serverMeta: entry.serverMeta });
886
+ }
887
+ if (grouped.size === 0) continue;
888
+ pages.push([
889
+ pagePath,
890
+ [...grouped.keys()].toSorted().map((name) => [name, mustGet(grouped, name, "grouped").toSorted(compareVersions)])
891
+ ]);
892
+ }
893
+ return pages;
894
+ };
895
+ var renderServerSyncRouteMap = (syncTypesByPage, renderValue) => {
896
+ let body = "";
897
+ for (const [pagePath, syncs] of groupServerSyncRoutes(syncTypesByPage)) {
898
+ body += ` '${pagePath}': {
899
+ `;
900
+ for (const [syncName, routes] of syncs) {
901
+ body += ` '${syncName}': {
902
+ `;
903
+ for (const route of routes) {
904
+ body += ` '${route.version}': ${renderValue(route)},
905
+ `;
906
+ }
907
+ body += ` },
908
+ `;
909
+ }
910
+ body += ` },
911
+ `;
912
+ }
913
+ return body;
914
+ };
915
+ var buildSyncMetaMapSource = (syncTypesByPage) => `export interface SyncMetaEntry {
916
+ auth: { login: boolean; additional?: Record<string, unknown>[]; hasAdditional?: boolean };
917
+ rateLimit?: number | false;
918
+ }
919
+
920
+ export const syncMetaMap: Record<string, Record<string, Record<string, SyncMetaEntry>>> = {
921
+ ${renderServerSyncRouteMap(syncTypesByPage, (route) => `{ ${renderRouteMetaFields(route.serverMeta.auth, route.serverMeta.rateLimit)} }`)}};
922
+
923
+ export const getSyncMeta = (pagePath: string, syncName: string, version: string): SyncMetaEntry | undefined => {
924
+ return syncMetaMap[pagePath]?.[syncName]?.[version];
925
+ };
926
+ `;
927
+ var buildSyncInputSchemasSource = (syncTypesByPage) => `
928
+ export const syncInputSchemas: Record<string, Record<string, Record<string, z.ZodTypeAny>>> = {
929
+ ${renderServerSyncRouteMap(syncTypesByPage, (route) => inputSchemaSource(route.clientInput))}};
930
+
931
+ export const getSyncInputSchema = (
932
+ pagePath: string,
933
+ syncName: string,
934
+ version: string,
935
+ ): z.ZodTypeAny | undefined => {
936
+ return syncInputSchemas[pagePath]?.[syncName]?.[version];
937
+ };
938
+ `;
870
939
  var buildImportStatements = ({
871
940
  namedImports,
872
941
  defaultImports
@@ -1060,7 +1129,7 @@ var collectFallbacks = (typesByPage, syncTypesByPage) => {
1060
1129
  for (const [syncKey, entry] of syncs) {
1061
1130
  const { name, version } = splitVersionedKey(syncKey);
1062
1131
  const route = `${pagePath}/${name}@${version}`;
1063
- flagField(route, "sync", "clientInput", entry.clientInput);
1132
+ flagField(route, "sync", "clientInput", entry.clientInput, entry.serverMeta !== void 0);
1064
1133
  flagField(route, "sync", "serverOutput", entry.serverOutput, false, !entry.hasServer);
1065
1134
  flagField(route, "sync", "clientOutput", entry.clientOutput, false, !entry.hasClient);
1066
1135
  flagField(route, "sync", "serverStream", entry.serverStream);
@@ -1254,10 +1323,7 @@ export const apiMetaMap: Record<string, Record<string, Record<string, ApiMetaEnt
1254
1323
  content += ` '${apiName}': {
1255
1324
  `;
1256
1325
  for (const { version, entry } of mustGet(grouped, apiName, "grouped").toSorted((a, b) => a.version.localeCompare(b.version, void 0, { numeric: true }))) {
1257
- const auth = entry.auth && typeof entry.auth === "object" ? entry.auth : { login: false, hasAdditional: false };
1258
- const rateLimitPart = entry.rateLimit === void 0 ? "" : `, rateLimit: ${entry.rateLimit === false ? "false" : String(entry.rateLimit)}`;
1259
- const additionalPart = auth.additional && auth.additional.length > 0 ? `, additional: ${JSON.stringify(auth.additional)}` : "";
1260
- content += ` '${version}': { method: '${entry.method}', auth: { login: ${auth.login ? "true" : "false"}${additionalPart}${auth.hasAdditional ? ", hasAdditional: true" : ""} }${rateLimitPart} },
1326
+ content += ` '${version}': { method: '${entry.method}', ${renderRouteMetaFields(entry.auth, entry.rateLimit)} },
1261
1327
  `;
1262
1328
  }
1263
1329
  content += ` },
@@ -1272,6 +1338,7 @@ export const getApiMeta = (pagePath: string, apiName: string, version: string):
1272
1338
  return apiMetaMap[pagePath]?.[apiName]?.[version];
1273
1339
  };
1274
1340
 
1341
+ ${buildSyncMetaMapSource(syncTypesByPage)}
1275
1342
  // Sync Type Definitions
1276
1343
  //
1277
1344
 
@@ -1371,7 +1438,7 @@ declare module '@luckystack/core/typemap' {
1371
1438
  `;
1372
1439
  content = stripSymbolKeyedMembers(content);
1373
1440
  validateGeneratedTypeIdentifiers(content);
1374
- const schemasContent = buildSchemasContent({ typesByPage });
1441
+ const schemasContent = buildSchemasContent({ typesByPage, syncTypesByPage });
1375
1442
  const fallbacks = collectFallbacks(typesByPage, syncTypesByPage);
1376
1443
  const totalRoutes = [...typesByPage.values()].reduce((n, m) => n + m.size, 0) + [...syncTypesByPage.values()].reduce((n, m) => n + m.size, 0);
1377
1444
  const diagnosticsData = {
@@ -1383,11 +1450,13 @@ declare module '@luckystack/core/typemap' {
1383
1450
  return { content, docsData, schemasContent, diagnosticsData };
1384
1451
  };
1385
1452
  var buildSchemasContent = ({
1386
- typesByPage
1453
+ typesByPage,
1454
+ syncTypesByPage
1387
1455
  }) => {
1388
1456
  const sortedPages = [...typesByPage.keys()].toSorted();
1389
1457
  let body = `/* eslint-disable */
1390
- //? Auto-generated Zod schemas for every API input. Driven by the same walk
1458
+ //? Auto-generated Zod schemas for every API input and every sync-server
1459
+ //? clientInput. Driven by the same walk
1391
1460
  //? as apiTypes.generated.ts; see @luckystack/devkit/src/typeMap/zodEmitter.ts
1392
1461
  //? for the TS-AST \xE2\u2020\u2019 Zod converter. Types that fall outside the converter's
1393
1462
  //? scope emit \`z.any()\` with a TODO comment.
@@ -1412,8 +1481,7 @@ export const apiInputSchemas: Record<string, Record<string, Record<string, z.Zod
1412
1481
  for (const { version, entry } of mustGet(grouped, apiName, "grouped").toSorted(
1413
1482
  (a, b) => a.version.localeCompare(b.version, void 0, { numeric: true })
1414
1483
  )) {
1415
- const schemaSrc = typeTextToZodSource(entry.input) ?? "z.any() /* unparseable input type */";
1416
- body += ` '${version}': ${schemaSrc},
1484
+ body += ` '${version}': ${inputSchemaSource(entry.input)},
1417
1485
  `;
1418
1486
  }
1419
1487
  body += ` },
@@ -1432,6 +1500,7 @@ export const getApiInputSchema = (
1432
1500
  return apiInputSchemas[pagePath]?.[apiName]?.[version];
1433
1501
  };
1434
1502
  `;
1503
+ body += buildSyncInputSchemasSource(syncTypesByPage);
1435
1504
  return body;
1436
1505
  };
1437
1506
  var writeTypeMapArtifacts = ({
@@ -2971,7 +3040,9 @@ var collectSyncTypes = (syncServerFiles, syncClientFiles, collectors) => {
2971
3040
  version: syncVersion,
2972
3041
  hasServer: serverFile !== void 0,
2973
3042
  hasClient: clientFile !== void 0,
2974
- ...syncMeta ? { meta: syncMeta } : {}
3043
+ ...syncMeta ? { meta: syncMeta } : {},
3044
+ //? Same extractors as the API side, so `syncMetaMap` and `apiMetaMap` agree.
3045
+ ...serverFile ? { serverMeta: { auth: extractAuth(serverFile), rateLimit: extractRateLimit(serverFile) } } : {}
2975
3046
  });
2976
3047
  }
2977
3048
  return syncTypesByPage;