@luckystack/devkit 0.6.0 → 0.6.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
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ Codegen fixes surfaced by a MikroORM/MongoDB consumer (verified against a real
13
+ MikroORM project + `tsc`); consumers can drop the corresponding
14
+ `node_modules/@luckystack/devkit/dist` patches once on this version.
15
+
16
+ - **DEVKIT-1** — a route returning a MikroORM entity no longer corrupts
17
+ `apiTypes.generated.ts`. Symbol-keyed entity members (`[OptionalProps]`,
18
+ `[loadedType]`, `[selectedType]`) serialize as the invalid identifier
19
+ `__@<name>@<id>`; they are now dropped from the inlined type (skipped in
20
+ `expandTypeDetailed`, plus a brace-aware content-level safety net for the
21
+ cycle/depth `typeToString` fallback paths).
22
+ - **DEVKIT-2** — `SessionLayout` used to sit on an internal whitelist that kept
23
+ the identifier without ever emitting its import, tripping the unresolved-type
24
+ validator. It now runs through normal import collection, like `AuthProps`.
25
+ - **DEVKIT-3** — the per-route error arm lists its real fields explicitly
26
+ (`message?`, `errorParams?`, `httpStatus?`) alongside the `[key: string]:
27
+ unknown` index signature, so `res.errorParams` narrows to its type after a
28
+ status check instead of collapsing to `unknown`.
29
+ - **DEVKIT-4** — a route that hands its stream emitter to a helper (no literal
30
+ `stream(...)` call in `main`) now infers the payload type from the declared
31
+ `stream: ApiStreamEmitter<T>` on `ApiParams` instead of degrading to `never`.
32
+ - **DEVKIT-5** — private helper subtrees under a marker (`_api/_lib/*`,
33
+ `_sync/_lib/__tests__/*`) are skipped by route-naming validation + discovery
34
+ instead of being flagged as invalid route files.
35
+
10
36
  ## [0.1.0]
11
37
 
12
38
  ### Added
package/dist/index.js CHANGED
@@ -552,6 +552,38 @@ var writeFileIfChanged = (filePath, content) => {
552
552
  var indentStr = (str, indentText) => {
553
553
  return str.split("\n").map((line, i) => i === 0 ? line : indentText + line).join("\n");
554
554
  };
555
+ var stripSymbolKeyedMembers = (text) => {
556
+ if (!text.includes("__@")) return text;
557
+ let result = text;
558
+ for (; ; ) {
559
+ const match = /__@\w+@\d+\??\s*:/.exec(result);
560
+ if (!match) break;
561
+ const symStart = match.index;
562
+ const lineStart = result.lastIndexOf("\n", symStart) + 1;
563
+ let i = symStart + match[0].length;
564
+ let depth = 0;
565
+ let end = result.length;
566
+ while (i < result.length) {
567
+ const ch = result[i];
568
+ if (ch === "{" || ch === "[" || ch === "(") {
569
+ depth++;
570
+ } else if (ch === "}" || ch === "]" || ch === ")") {
571
+ if (depth === 0) {
572
+ end = i;
573
+ break;
574
+ }
575
+ depth--;
576
+ } else if (ch === ";" && depth === 0) {
577
+ end = i + 1;
578
+ break;
579
+ }
580
+ i++;
581
+ }
582
+ if (result[end] === "\n") end += 1;
583
+ result = result.slice(0, lineStart) + result.slice(end);
584
+ }
585
+ return result;
586
+ };
555
587
  var validateGeneratedTypeIdentifiers = (content) => {
556
588
  const sourceFile = ts3.createSourceFile("apiTypes.generated.ts", content, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
557
589
  const knownSymbols = /* @__PURE__ */ new Set();
@@ -781,7 +813,7 @@ type _ProjectApiTypeMap = {
781
813
  `;
782
814
  content += ` input: ${indentStr(entry.input, " ")};
783
815
  `;
784
- content += ` output: ${indentStr(entry.output, " ")} | { status: 'error'; errorCode: string; [key: string]: unknown };
816
+ content += ` output: ${indentStr(entry.output, " ")} | { status: 'error'; errorCode: string; message?: string; errorParams?: { key: string; value: string | number | boolean }[]; httpStatus?: number; [key: string]: unknown };
785
817
  `;
786
818
  content += ` stream: ${indentStr(entry.stream, " ")};
787
819
  `;
@@ -936,9 +968,9 @@ type _ProjectSyncTypeMap = {
936
968
  `;
937
969
  content += ` clientInput: ${indentStr(entry.clientInput, " ")};
938
970
  `;
939
- content += ` serverOutput: ${indentStr(entry.serverOutput, " ")} | { status: 'error'; errorCode: string; [key: string]: unknown };
971
+ content += ` serverOutput: ${indentStr(entry.serverOutput, " ")} | { status: 'error'; errorCode: string; message?: string; errorParams?: { key: string; value: string | number | boolean }[]; [key: string]: unknown };
940
972
  `;
941
- content += ` clientOutput: ${indentStr(entry.clientOutput, " ")} | { status: 'error'; errorCode: string; [key: string]: unknown };
973
+ content += ` clientOutput: ${indentStr(entry.clientOutput, " ")} | { status: 'error'; errorCode: string; message?: string; errorParams?: { key: string; value: string | number | boolean }[]; [key: string]: unknown };
942
974
  `;
943
975
  content += ` serverStream: ${indentStr(entry.serverStream, " ")};
944
976
  `;
@@ -980,6 +1012,7 @@ declare module '@luckystack/core/typemap' {
980
1012
  interface SyncTypeMap extends _ProjectSyncTypeMap {}
981
1013
  }
982
1014
  `;
1015
+ content = stripSymbolKeyedMembers(content);
983
1016
  validateGeneratedTypeIdentifiers(content);
984
1017
  const schemasContent = buildSchemasContent({ typesByPage });
985
1018
  const fallbacks = collectFallbacks(typesByPage, syncTypesByPage);
@@ -1309,6 +1342,7 @@ var expandTypeDetailed = (type, checker, depth = 0, state) => {
1309
1342
  const fields = [];
1310
1343
  let unresolvedSymbols = [];
1311
1344
  for (const prop of props) {
1345
+ if (prop.getName().startsWith("__@")) continue;
1312
1346
  const propType = checker.getTypeOfSymbol(prop);
1313
1347
  const literalType = getLiteralTypeFromPropertySymbol(prop, checker, depth + 1);
1314
1348
  const isOptional = (prop.flags & ts4.SymbolFlags.Optional) !== 0;
@@ -1543,6 +1577,37 @@ var getInputTypeDetailsFromFile = (filePath) => {
1543
1577
  return { text: DEFAULT, unresolvedSymbols: [] };
1544
1578
  }
1545
1579
  };
1580
+ var STREAM_EMITTER_TYPE_NAMES = /* @__PURE__ */ new Set([
1581
+ "ApiStreamEmitter",
1582
+ "SyncServerStreamEmitter",
1583
+ "SyncClientStreamEmitter",
1584
+ "SyncBroadcastStreamEmitter",
1585
+ "SyncStreamToEmitter"
1586
+ ]);
1587
+ var extractDeclaredStreamTypeFromApiParams = (sourceFile, checker) => {
1588
+ let result = null;
1589
+ const visit = (node) => {
1590
+ if (result) return;
1591
+ if (ts5.isInterfaceDeclaration(node) && node.name.text.endsWith("Params")) {
1592
+ for (const member of node.members) {
1593
+ if (ts5.isPropertySignature(member) && ts5.isIdentifier(member.name) && member.name.text === "stream" && member.type && ts5.isTypeReferenceNode(member.type) && ts5.isIdentifier(member.type.typeName) && STREAM_EMITTER_TYPE_NAMES.has(member.type.typeName.text)) {
1594
+ const typeArg = member.type.typeArguments?.[0];
1595
+ if (typeArg) {
1596
+ const argType = checker.getNonNullableType(checker.getTypeFromTypeNode(typeArg));
1597
+ const expanded = expandTypeDetailed(argType, checker);
1598
+ if (expanded.text.trim().length > 0) {
1599
+ result = { text: expanded.text, unresolvedSymbols: expanded.unresolvedSymbols };
1600
+ return;
1601
+ }
1602
+ }
1603
+ }
1604
+ }
1605
+ }
1606
+ ts5.forEachChild(node, visit);
1607
+ };
1608
+ visit(sourceFile);
1609
+ return result;
1610
+ };
1546
1611
  var getApiStreamPayloadTypeDetailsFromFile = (filePath) => {
1547
1612
  const DEFAULT = "never";
1548
1613
  try {
@@ -1553,7 +1618,10 @@ var getApiStreamPayloadTypeDetailsFromFile = (filePath) => {
1553
1618
  const mainFn = findMainFunction(sourceFile);
1554
1619
  if (!mainFn) return { text: DEFAULT, unresolvedSymbols: [] };
1555
1620
  const details = collectStreamCallPayloadTypeDetails(mainFn, checker);
1556
- return { text: details.text || DEFAULT, unresolvedSymbols: details.unresolvedSymbols };
1621
+ if (details.text !== "") return details;
1622
+ const declared = extractDeclaredStreamTypeFromApiParams(sourceFile, checker);
1623
+ if (declared) return declared;
1624
+ return { text: DEFAULT, unresolvedSymbols: [] };
1557
1625
  } catch (error) {
1558
1626
  console.error(`[TypeMapGenerator] Error extracting API stream payload type from ${filePath}:`, error);
1559
1627
  return { text: DEFAULT, unresolvedSymbols: [] };
@@ -1732,8 +1800,7 @@ var sanitizeTypeAndCollectImports = ({
1732
1800
  const { namedImports, defaultImports } = collectors;
1733
1801
  return type.replaceAll(/\b([A-Z][a-zA-Z0-9_]*)(<[^>]+>)?(\[\])?\b/g, (match, typeName, _generics, isArray) => {
1734
1802
  const builtins = ["Promise", "Date", "Function", "Array", "Record", "Partial", "Pick", "Omit", "Error", "Map", "Set", "Buffer", "Uint8Array", "Object"];
1735
- const existingImports = ["SessionLayout"];
1736
- if (builtins.includes(typeName) || existingImports.includes(typeName) || knownGenerics.has(typeName)) {
1803
+ if (builtins.includes(typeName) || knownGenerics.has(typeName)) {
1737
1804
  return match;
1738
1805
  }
1739
1806
  const importConfig = fileImports.get(typeName);
@@ -2159,6 +2226,13 @@ var normalizePath = (value) => {
2159
2226
  return value.replaceAll("\\", "/");
2160
2227
  };
2161
2228
  var toRel = (absolute) => path8.relative(ROOT_DIR3, absolute).replaceAll("\\", "/");
2229
+ var isInsidePrivateRouteSubfolder = (fullPath) => {
2230
+ const { privateFolderPrefix, apiMarker, syncMarker } = getRoutingRules();
2231
+ const segments = normalizePath(fullPath).split("/");
2232
+ const markerIndex = segments.findIndex((segment) => segment === apiMarker || segment === syncMarker);
2233
+ if (markerIndex === -1) return false;
2234
+ return segments.slice(markerIndex + 1).some((segment) => segment.startsWith(privateFolderPrefix));
2235
+ };
2162
2236
  var walkRouteFiles = (dir, results = []) => {
2163
2237
  const entries = fs5.readdirSync(dir, { withFileTypes: true });
2164
2238
  const apiSeg = apiMarkerSegment();
@@ -2168,6 +2242,7 @@ var walkRouteFiles = (dir, results = []) => {
2168
2242
  const fullPath = path8.join(dir, entry.name);
2169
2243
  const normalizedFullPath = normalizePath(fullPath);
2170
2244
  if (ignore(toRel(fullPath))) continue;
2245
+ if (isInsidePrivateRouteSubfolder(fullPath)) continue;
2171
2246
  if (entry.isDirectory()) {
2172
2247
  if (entry.name.startsWith(".") || entry.name === "node_modules") {
2173
2248
  continue;