@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 +5 -1
- package/CLAUDE.md +1 -1
- package/dist/index.js +82 -11
- package/dist/index.js.map +1 -1
- package/docs/type-map-generation.md +10 -2
- package/package.json +2 -2
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
|
-
## [
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|