lambder 7.3.1 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/CHANGELOG.md +1047 -3
  2. package/README.md +46 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/ContractTypePrinter.d.ts +85 -0
  27. package/dist/build/ContractTypePrinter.js +402 -0
  28. package/dist/build/freshProcessVerifier.d.ts +13 -0
  29. package/dist/build/freshProcessVerifier.js +19 -0
  30. package/dist/build/moduleLocation.d.ts +11 -0
  31. package/dist/build/moduleLocation.js +6 -0
  32. package/dist/build/writeApiContract.d.ts +78 -0
  33. package/dist/build/writeApiContract.js +302 -0
  34. package/dist/build/writeApiSignatures.d.ts +114 -0
  35. package/dist/build/writeApiSignatures.js +217 -0
  36. package/dist/build/writeFileAtomically.d.ts +8 -0
  37. package/dist/build/writeFileAtomically.js +22 -0
  38. package/dist/build.d.ts +14 -0
  39. package/dist/build.js +11 -0
  40. package/dist/client/LambderCaller.d.ts +13 -44
  41. package/dist/client/LambderCaller.js +77 -84
  42. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  43. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  44. package/dist/client/LambderUploadRunner.d.ts +96 -0
  45. package/dist/client/LambderUploadRunner.js +234 -0
  46. package/dist/client/lambderFetchTransport.d.ts +4 -1
  47. package/dist/client/lambderFetchTransport.js +52 -28
  48. package/dist/client.d.ts +9 -3
  49. package/dist/client.js +6 -1
  50. package/dist/core/Lambder.d.ts +143 -79
  51. package/dist/core/Lambder.js +350 -231
  52. package/dist/core/LambderContext.d.ts +82 -15
  53. package/dist/core/LambderContext.js +107 -20
  54. package/dist/core/LambderCors.d.ts +21 -3
  55. package/dist/core/LambderCors.js +35 -16
  56. package/dist/core/LambderCrashHandling.d.ts +40 -0
  57. package/dist/core/LambderCrashHandling.js +97 -0
  58. package/dist/core/LambderCreateOptions.d.ts +151 -75
  59. package/dist/core/LambderCreateOptions.js +16 -23
  60. package/dist/core/LambderFiles.d.ts +21 -7
  61. package/dist/core/LambderFiles.js +62 -34
  62. package/dist/core/LambderIndexHtml.js +12 -11
  63. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  64. package/dist/core/LambderPolicyBuilders.js +17 -5
  65. package/dist/core/LambderPublicFiles.d.ts +11 -5
  66. package/dist/core/LambderPublicFiles.js +32 -4
  67. package/dist/core/LambderRequestPath.d.ts +43 -0
  68. package/dist/core/LambderRequestPath.js +63 -0
  69. package/dist/core/LambderResponse.d.ts +26 -5
  70. package/dist/core/LambderResponse.js +157 -70
  71. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  72. package/dist/core/LambderResponseBuilder.js +64 -3
  73. package/dist/core/LambderRouting.d.ts +2 -3
  74. package/dist/core/LambderRouting.js +22 -7
  75. package/dist/core/LambderTemplatingEngine.js +211 -32
  76. package/dist/index.d.ts +25 -8
  77. package/dist/index.js +13 -4
  78. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  79. package/dist/invoke/LambderInvokeCaller.js +76 -66
  80. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  81. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  82. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  83. package/dist/invoke/LambderLambdaEvent.js +40 -22
  84. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  85. package/dist/invoke/lambderHandlerTransport.js +15 -18
  86. package/dist/mock/LambderMockApp.d.ts +67 -83
  87. package/dist/mock/LambderMockApp.js +167 -153
  88. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  89. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  90. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  91. package/dist/mock/LambderMockCallRecorder.js +19 -28
  92. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  93. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  94. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  95. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  96. package/dist/mock/LambderMockFailureInjector.js +3 -6
  97. package/dist/mock/LambderMockTypes.d.ts +78 -108
  98. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  99. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  100. package/dist/mock/lambderMockMswHandler.d.ts +43 -33
  101. package/dist/mock/lambderMockMswHandler.js +50 -39
  102. package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
  103. package/dist/mock/lambderMockUploadMswHandler.js +28 -0
  104. package/dist/mock.d.ts +4 -1
  105. package/dist/mock.js +6 -3
  106. package/dist/session/LambderSessionController.d.ts +108 -89
  107. package/dist/session/LambderSessionController.js +187 -168
  108. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  109. package/dist/session/LambderSessionCrypto.js +26 -12
  110. package/dist/session/LambderSessionManager.d.ts +124 -46
  111. package/dist/session/LambderSessionManager.js +262 -137
  112. package/dist/shared/LambderHtml.d.ts +42 -3
  113. package/dist/shared/LambderHtml.js +127 -7
  114. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  115. package/dist/shared/LambderHtmlPositions.js +652 -0
  116. package/dist/shared/LambderI18n.d.ts +10 -11
  117. package/dist/shared/LambderI18n.js +33 -21
  118. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  119. package/dist/shared/contracts/LambderCache.js +11 -0
  120. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  121. package/dist/shared/contracts/LambderFileSource.js +5 -8
  122. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  123. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  124. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  125. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  126. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  127. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  128. package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
  129. package/dist/shared/contracts/LambderUploadBucket.js +74 -0
  130. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  131. package/dist/shared/transport/LambderApiTransport.js +7 -7
  132. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  133. package/dist/shared/transport/LambderCookieJar.js +54 -66
  134. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  135. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  136. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  137. package/dist/shared/util/LambderCallAbort.js +5 -5
  138. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  139. package/dist/shared/util/LambderClientIp.js +96 -13
  140. package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
  141. package/dist/shared/util/LambderContentDisposition.js +13 -0
  142. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  143. package/dist/shared/util/LambderExpiringMap.js +41 -57
  144. package/dist/shared/util/LambderNodeModules.js +6 -7
  145. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  146. package/dist/shared/util/LambderOptionChecks.js +4 -4
  147. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  148. package/dist/shared/util/LambderResponseBrand.js +5 -5
  149. package/dist/shared/util/LambderTextDigest.d.ts +7 -5
  150. package/dist/shared/util/LambderTextDigest.js +11 -5
  151. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  152. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  153. package/dist/shared/util/boundKeyField.d.ts +20 -0
  154. package/dist/shared/util/boundKeyField.js +34 -0
  155. package/dist/shared/util/canonicalJson.d.ts +11 -0
  156. package/dist/shared/util/canonicalJson.js +28 -0
  157. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  158. package/dist/shared/util/joinKeyFields.js +22 -0
  159. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  160. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  161. package/dist/shared/wire/LambderApiContract.d.ts +98 -53
  162. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  163. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  164. package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
  165. package/dist/shared/wire/LambderApiRefusal.js +42 -7
  166. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  167. package/dist/shared/wire/LambderApiSignature.js +16 -19
  168. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  169. package/dist/shared/wire/LambderCallOptions.js +9 -11
  170. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  171. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  172. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  173. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  174. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  175. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  176. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  177. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  178. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  179. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  180. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  181. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  182. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  183. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  184. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  185. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  186. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  187. package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
  188. package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
  189. package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
  190. package/dist/shared/wire/LambderUploadRefusal.js +18 -0
  191. package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
  192. package/dist/shared/wire/LambderUploadSchemas.js +30 -0
  193. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  194. package/dist/stores/LambderCacheFiller.js +119 -0
  195. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  196. package/dist/stores/LambderCacheKeys.js +54 -0
  197. package/dist/stores/LambderCacheValues.d.ts +45 -0
  198. package/dist/stores/LambderCacheValues.js +74 -0
  199. package/dist/stores/LambderDdbCache.d.ts +121 -56
  200. package/dist/stores/LambderDdbCache.js +528 -225
  201. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  202. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  203. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  204. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  205. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  206. package/dist/stores/LambderDdbSdk.js +80 -38
  207. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  208. package/dist/stores/LambderDdbSessionStore.js +119 -47
  209. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  210. package/dist/stores/LambderHttpFileSource.js +15 -13
  211. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  212. package/dist/stores/LambderMemoryCache.js +113 -0
  213. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  214. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  215. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  216. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  217. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  218. package/dist/stores/LambderMemorySessionStore.js +38 -19
  219. package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
  220. package/dist/stores/LambderMemoryUploadBucket.js +219 -0
  221. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  222. package/dist/stores/LambderS3FileSource.js +12 -7
  223. package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
  224. package/dist/stores/LambderS3UploadBucket.js +144 -0
  225. package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
  226. package/dist/stores/LambderSdkInstallHint.js +14 -0
  227. package/dist/testing/LambderTestApp.d.ts +23 -25
  228. package/dist/testing/LambderTestApp.js +22 -24
  229. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  230. package/dist/testing/LambderTestVisitor.js +15 -15
  231. package/dist/testing.d.ts +3 -0
  232. package/dist/testing.js +2 -0
  233. package/package.json +26 -3
  234. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  235. package/dist/api/LambderApiPolicyEngine.js +0 -85
  236. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  237. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -0,0 +1,402 @@
1
+ const INDENT = " ";
2
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
3
+ /** How long a union may run on one line before each member goes on a line of its own. */
4
+ const UNION_LINE_WIDTH = 80;
5
+ const atom = (text) => ({ text, compound: false });
6
+ const indentFollowingLines = (text, indent) => text.replace(/\n/g, `\n${indent}`);
7
+ /** Code-unit order, so the output never depends on the locale it is generated under. */
8
+ const byCodeUnits = (a, b) => a < b ? -1 : a > b ? 1 : 0;
9
+ /**
10
+ * Properties in the order they are written in the source: the file, then the
11
+ * position, then the name for those declared together (a Record's keys) or
12
+ * not at all. The compiler's own order is not used, because the properties of
13
+ * a mapped type (every zod inference) follow a union of their keys, and a
14
+ * union is ordered by when each key's type was first created, which moves
15
+ * whenever unrelated code is checked in another order.
16
+ */
17
+ const bySourceOrder = (a, b) => {
18
+ const first = a.declarations?.[0];
19
+ const second = b.declarations?.[0];
20
+ if (first && second) {
21
+ const byFile = byCodeUnits(first.getSourceFile().fileName, second.getSourceFile().fileName);
22
+ if (byFile)
23
+ return byFile;
24
+ if (first.pos !== second.pos)
25
+ return first.pos - second.pos;
26
+ }
27
+ else if (first || second) {
28
+ return first ? -1 : 1;
29
+ }
30
+ return byCodeUnits(a.name, b.name);
31
+ };
32
+ /** Union members in a stable order, with null and undefined last as they are usually written. */
33
+ const unionMemberRank = (text) => text === "null" ? 1 : text === "undefined" ? 2 : 0;
34
+ export class ContractTypePrinter {
35
+ ts;
36
+ program;
37
+ checker;
38
+ style;
39
+ failures = [];
40
+ /** Every named declaration by the type it stands for; its text is null while it is being printed. */
41
+ declarations = new Map();
42
+ /** Names a declaration may not take: the default library's, and the contract's own. */
43
+ takenNames = new Set();
44
+ /** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
45
+ inProgress = new Set();
46
+ /** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
47
+ exactOptionalProperties;
48
+ constructor(ts, program, checker, style, contractName) {
49
+ this.ts = ts;
50
+ this.program = program;
51
+ this.checker = checker;
52
+ this.style = style;
53
+ this.takenNames.add(contractName);
54
+ this.exactOptionalProperties = !!program.getCompilerOptions().exactOptionalPropertyTypes;
55
+ // A declaration named after a global the printed text refers to by
56
+ // name (Date) would shadow it.
57
+ for (const sourceFile of program.getSourceFiles()) {
58
+ if (!program.isSourceFileDefaultLibrary(sourceFile))
59
+ continue;
60
+ for (const statement of sourceFile.statements) {
61
+ if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isModuleDeclaration(statement))
62
+ && statement.name && ts.isIdentifier(statement.name))
63
+ this.takenNames.add(statement.name.text);
64
+ }
65
+ }
66
+ }
67
+ /** Prints each member of the contract type, sorted by name, and every declaration they refer to. */
68
+ printContract(contract) {
69
+ const entries = this.checker.getPropertiesOfType(contract)
70
+ .map((entry) => ({ name: entry.name, text: this.print(this.checker.getTypeOfSymbol(entry), this.keyOf(entry.name)).text }))
71
+ .sort((a, b) => byCodeUnits(a.name, b.name));
72
+ const declarations = [...this.declarations.values()]
73
+ .map(({ name, text }) => ({ name, text: text ?? "never" }))
74
+ .sort((a, b) => byCodeUnits(a.name, b.name));
75
+ return { entries, declarations, failures: this.failures };
76
+ }
77
+ /**
78
+ * `label value`, with a union too long for one line starting on the next
79
+ * line, one member per line: what a property, an index signature and a
80
+ * declaration all print through.
81
+ */
82
+ labeledValue(label, value) {
83
+ return value.startsWith("| ") ? `${label}\n${INDENT}${indentFollowingLines(value, INDENT)}` : `${label} ${value}`;
84
+ }
85
+ print(type, path) {
86
+ const { TypeFlags } = this.ts;
87
+ const flags = type.flags;
88
+ // The compiler's own `any` is the one a source wrote (or inferred);
89
+ // any other is the error type a compile error leaves behind.
90
+ if (flags & TypeFlags.Any) {
91
+ return type === this.checker.getAnyType() ? atom("any") : this.fail(path, "a type the compiler could not resolve, which a compile error in the app's sources leaves behind: run its typecheck");
92
+ }
93
+ if (flags & TypeFlags.Unknown)
94
+ return atom("unknown");
95
+ if (flags & TypeFlags.Never)
96
+ return atom("never");
97
+ if (flags & TypeFlags.String)
98
+ return atom("string");
99
+ if (flags & TypeFlags.Number)
100
+ return atom("number");
101
+ if (flags & TypeFlags.BigInt)
102
+ return atom("bigint");
103
+ if (flags & TypeFlags.Boolean)
104
+ return atom("boolean");
105
+ if (flags & TypeFlags.Void)
106
+ return atom("void");
107
+ if (flags & TypeFlags.Undefined)
108
+ return atom("undefined");
109
+ if (flags & TypeFlags.Null)
110
+ return atom("null");
111
+ if (flags & TypeFlags.ESSymbol)
112
+ return atom("symbol");
113
+ if (flags & TypeFlags.NonPrimitive)
114
+ return atom("object");
115
+ // Before the literals: an enum member is a literal type as well.
116
+ if (flags & TypeFlags.EnumLike)
117
+ return this.fail(path, `${this.checker.typeToString(type)}, an enum, which only its declaration can name`);
118
+ if (flags & TypeFlags.StringLiteral)
119
+ return atom(this.quoted(type.value));
120
+ if (flags & TypeFlags.NumberLiteral)
121
+ return atom(String(type.value));
122
+ if (flags & TypeFlags.BigIntLiteral) {
123
+ const { negative, base10Value } = type.value;
124
+ return atom(`${negative ? "-" : ""}${base10Value}n`);
125
+ }
126
+ if (flags & TypeFlags.BooleanLiteral)
127
+ return atom(this.checker.typeToString(type));
128
+ if (flags & TypeFlags.UniqueESSymbol)
129
+ return this.fail(path, "a unique symbol, which only its declaration can name");
130
+ if (flags & TypeFlags.TemplateLiteral)
131
+ return this.printTemplateLiteral(type, path);
132
+ if (flags & TypeFlags.StringMapping) {
133
+ const mapping = type;
134
+ return atom(`${mapping.symbol.name}<${this.print(mapping.type, path).text}>`);
135
+ }
136
+ if (flags & (TypeFlags.Union | TypeFlags.Intersection | TypeFlags.Object))
137
+ return this.printComposite(type, path);
138
+ return this.fail(path, `${this.checker.typeToString(type)}, which only resolves where it was written (a type parameter, or a conditional or indexed type over one)`);
139
+ }
140
+ /** A union, an intersection or an object: printed in place, or as a reference to a declaration of its own. */
141
+ printComposite(type, path) {
142
+ const known = this.declarations.get(type);
143
+ if (known)
144
+ return atom(known.name);
145
+ const ownName = this.declaredNameOf(type);
146
+ if (ownName !== undefined) {
147
+ // Registered before the body is printed, so a reference to itself
148
+ // inside the body finds the name.
149
+ const declaration = { name: this.takeName(ownName), text: null };
150
+ this.declarations.set(type, declaration);
151
+ declaration.text = this.printStructure(type, path).text;
152
+ return atom(declaration.name);
153
+ }
154
+ if (this.inProgress.has(type)) {
155
+ const declaration = { name: this.takeName(this.recursiveNameOf(type)), text: null };
156
+ this.declarations.set(type, declaration);
157
+ return atom(declaration.name);
158
+ }
159
+ this.inProgress.add(type);
160
+ const printed = this.printStructure(type, path);
161
+ this.inProgress.delete(type);
162
+ // Named while its body was being printed: it refers to itself.
163
+ const recursive = this.declarations.get(type);
164
+ if (!recursive)
165
+ return printed;
166
+ recursive.text = printed.text;
167
+ return atom(recursive.name);
168
+ }
169
+ /** The name a type is declared under, when it is one to print as a declaration: non-generic, and not the default library's. */
170
+ declaredNameOf(type) {
171
+ const { ObjectFlags, TypeFlags } = this.ts;
172
+ if (type.aliasSymbol) {
173
+ return type.aliasTypeArguments?.length || this.isDefaultLibrary(type.aliasSymbol) ? undefined : this.nameOf(type.aliasSymbol);
174
+ }
175
+ if (!(type.flags & TypeFlags.Object))
176
+ return undefined;
177
+ const objectFlags = type.objectFlags;
178
+ if (!(objectFlags & (ObjectFlags.Interface | ObjectFlags.Class)))
179
+ return undefined;
180
+ // A class, or an interface with a base type, is a reference to itself
181
+ // (for its `this` type). A generic one's instances are references to
182
+ // it instead, and are printed in place.
183
+ if (objectFlags & ObjectFlags.Reference && (type.target !== type || type.typeParameters?.length))
184
+ return undefined;
185
+ return this.isDefaultLibrary(type.symbol) ? undefined : this.nameOf(type.symbol);
186
+ }
187
+ /** The name a symbol is declared under, read off its declaration, so `export default interface Customer` is Customer; undefined when it has none to print. */
188
+ nameOf(symbol) {
189
+ const declaration = symbol.declarations?.[0];
190
+ const declared = declaration && this.ts.getNameOfDeclaration(declaration);
191
+ const name = declared && this.ts.isIdentifier(declared) ? declared.text : symbol.name;
192
+ return IDENTIFIER.test(name) && name !== "default" && !name.startsWith("__") ? name : undefined;
193
+ }
194
+ /**
195
+ * A name for a type that refers to itself and has none of its own: what it
196
+ * instantiates followed by its arguments (a JSON mapping of a Tree is
197
+ * `JsonOfTree`, a `Tree<string>` is `TreeString`), or `RecursiveType`.
198
+ */
199
+ recursiveNameOf(type) {
200
+ const { ObjectFlags, TypeFlags } = this.ts;
201
+ let instantiated;
202
+ if (type.aliasSymbol) {
203
+ instantiated = { symbol: type.aliasSymbol, typeArguments: type.aliasTypeArguments ?? [] };
204
+ }
205
+ else if (type.flags & TypeFlags.Object && type.objectFlags & ObjectFlags.Reference) {
206
+ const { target } = type;
207
+ const typeArguments = this.checker.getTypeArguments(type).slice(0, target.typeParameters?.length ?? 0);
208
+ instantiated = { symbol: target.symbol, typeArguments };
209
+ }
210
+ const base = instantiated && this.nameOf(instantiated.symbol);
211
+ if (!instantiated || !base)
212
+ return "RecursiveType";
213
+ const argumentNames = instantiated.typeArguments
214
+ .map((argument) => (argument.aliasSymbol && this.nameOf(argument.aliasSymbol)) ?? (argument.symbol && this.nameOf(argument.symbol)) ?? this.checker.typeToString(argument))
215
+ .filter((name) => IDENTIFIER.test(name));
216
+ return `${base}${argumentNames.map((name) => name[0].toUpperCase() + name.slice(1)).join("")}`;
217
+ }
218
+ printStructure(type, path) {
219
+ const { TypeFlags } = this.ts;
220
+ if (type.flags & TypeFlags.Union)
221
+ return this.printUnion(type, path);
222
+ if (type.flags & TypeFlags.Intersection)
223
+ return this.printIntersection(type, path);
224
+ return this.printObject(type, path);
225
+ }
226
+ printUnion(type, path) {
227
+ return this.printUnionOf(type.types, path);
228
+ }
229
+ printUnionOf(types, path) {
230
+ const members = [];
231
+ // boolean is the union of its two literals, and a union holding it
232
+ // holds them flattened in: they are put back together.
233
+ const booleans = new Set();
234
+ for (const member of types) {
235
+ if (member.flags & this.ts.TypeFlags.BooleanLiteral)
236
+ booleans.add(this.checker.typeToString(member));
237
+ else
238
+ members.push(this.print(member, path).text);
239
+ }
240
+ if (booleans.size === 2)
241
+ members.push("boolean");
242
+ else
243
+ members.push(...booleans);
244
+ members.sort((a, b) => unionMemberRank(a) - unionMemberRank(b) || byCodeUnits(a, b));
245
+ const oneLine = members.join(" | ");
246
+ if (oneLine.length <= UNION_LINE_WIDTH && !oneLine.includes("\n"))
247
+ return { text: oneLine, compound: true };
248
+ return { text: members.map((member) => `| ${indentFollowingLines(member, " ")}`).join("\n"), compound: true };
249
+ }
250
+ printIntersection(type, path) {
251
+ // Objects intersected are one object, and printed as the one they are.
252
+ if (type.types.every((member) => this.isPlainObject(member)))
253
+ return this.printMembers(type, path);
254
+ const members = type.types.map((member) => this.wrapped(this.print(member, path))).sort(byCodeUnits);
255
+ return { text: members.join(" & "), compound: true };
256
+ }
257
+ printObject(type, path) {
258
+ const { checker, ts } = this;
259
+ if (checker.isTupleType(type))
260
+ return this.printTuple(type, path);
261
+ if (checker.isArrayType(type)) {
262
+ const reference = type;
263
+ const element = this.print(checker.getTypeArguments(reference)[0], `${path}[]`);
264
+ const readonly = reference.target.symbol?.escapedName === "ReadonlyArray";
265
+ return { text: `${readonly ? "readonly " : ""}${this.wrapped(element)}[]`, compound: readonly };
266
+ }
267
+ if (this.isCallable(type))
268
+ return this.fail(path, `${checker.typeToString(type)}, a function, which is not data`);
269
+ if (this.isDefaultLibraryInterface(type)) {
270
+ const reference = type;
271
+ const parameterCount = type.objectFlags & ts.ObjectFlags.Reference ? reference.target.typeParameters?.length ?? 0 : 0;
272
+ const typeArguments = parameterCount ? checker.getTypeArguments(reference).slice(0, parameterCount) : [];
273
+ const name = checker.getFullyQualifiedName(type.symbol);
274
+ return atom(typeArguments.length ? `${name}<${typeArguments.map((argument) => this.print(argument, path).text).join(", ")}>` : name);
275
+ }
276
+ return this.printMembers(type, path);
277
+ }
278
+ printMembers(type, path) {
279
+ const { checker, ts } = this;
280
+ const lines = [];
281
+ for (const index of checker.getIndexInfosOfType(type)) {
282
+ const key = this.print(index.keyType, `${path}[key]`).text;
283
+ lines.push(this.labeledValue(`${index.isReadonly ? "readonly " : ""}[key: ${key}]:`, this.print(index.type, `${path}[${key}]`).text));
284
+ }
285
+ for (const property of [...checker.getPropertiesOfType(type)].sort(bySourceOrder)) {
286
+ const propertyPath = `${path}.${property.name}`;
287
+ const unprintable = this.unprintableProperty(property);
288
+ if (unprintable) {
289
+ this.fail(propertyPath, unprintable);
290
+ continue;
291
+ }
292
+ const optional = !!(property.flags & ts.SymbolFlags.Optional);
293
+ const printed = optional ? this.printOptionalMember(checker.getTypeOfSymbol(property), propertyPath) : this.print(checker.getTypeOfSymbol(property), propertyPath);
294
+ lines.push(this.labeledValue(`${this.keyOf(property.name)}${optional ? "?" : ""}:`, printed.text));
295
+ }
296
+ if (!lines.length)
297
+ return atom("{}");
298
+ return atom(`{\n${lines.map((line) => INDENT + indentFollowingLines(line, INDENT) + this.style.semicolon).join("\n")}\n}`);
299
+ }
300
+ /**
301
+ * An optional member's type as its source wrote it. Under
302
+ * exactOptionalPropertyTypes the compiler adds its own "missing"
303
+ * undefined, a different type from the `undefined` a source writes;
304
+ * printed as `| undefined` it would let the member be undefined, which
305
+ * the source does not.
306
+ */
307
+ printOptionalMember(type, path) {
308
+ if (!this.exactOptionalProperties || !(type.flags & this.ts.TypeFlags.Union))
309
+ return this.print(type, path);
310
+ const undefinedType = this.checker.getUndefinedType();
311
+ const written = type.types.filter((member) => !(member.flags & this.ts.TypeFlags.Undefined) || member === undefinedType);
312
+ if (written.length === type.types.length)
313
+ return this.print(type, path);
314
+ return written.length === 1 ? this.print(written[0], path) : this.printUnionOf(written, path);
315
+ }
316
+ printTuple(type, path) {
317
+ const { ElementFlags } = this.ts;
318
+ const { elementFlags, labeledElementDeclarations, readonly } = type.target;
319
+ const labelOf = (index) => {
320
+ const declaration = labeledElementDeclarations?.[index];
321
+ return declaration && this.ts.isIdentifier(declaration.name) ? declaration.name.text : undefined;
322
+ };
323
+ // Labels are all or nothing in a tuple.
324
+ const labeled = elementFlags.every((_, index) => labelOf(index) !== undefined);
325
+ const elements = this.checker.getTypeArguments(type).slice(0, elementFlags.length).map((element, index) => {
326
+ const flag = elementFlags[index];
327
+ const printed = this.print(element, `${path}[${index}]`);
328
+ const label = labeled ? `${labelOf(index)}` : "";
329
+ if (flag & ElementFlags.Rest)
330
+ return `...${label ? `${label}: ` : ""}${this.wrapped(printed)}[]`;
331
+ if (flag & ElementFlags.Variadic)
332
+ return `...${label ? `${label}: ` : ""}${printed.text}`;
333
+ if (flag & ElementFlags.Optional)
334
+ return label ? `${label}?: ${printed.text}` : `${this.wrapped(printed)}?`;
335
+ return label ? `${label}: ${printed.text}` : printed.text;
336
+ });
337
+ return { text: `${readonly ? "readonly " : ""}[${elements.join(", ")}]`, compound: readonly };
338
+ }
339
+ printTemplateLiteral(type, path) {
340
+ // A cooked text as template source: JSON's escapes, plus the two a template adds.
341
+ const escape = (text) => JSON.stringify(text).slice(1, -1).replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
342
+ const spans = type.types.map((span, index) => `\${${this.print(span, path).text}}${escape(type.texts[index + 1])}`);
343
+ return atom(`\`${escape(type.texts[0])}${spans.join("")}\``);
344
+ }
345
+ /** Why a property cannot be printed as a plain member, or undefined when it can. */
346
+ unprintableProperty(property) {
347
+ const { ts } = this;
348
+ if (String(property.escapedName).startsWith("__@"))
349
+ return "a symbol-keyed property, which only its declaration can name";
350
+ if (property.name.startsWith("#"))
351
+ return "a private field of a class, which only its declaration can hold";
352
+ const hidden = property.declarations?.some((declaration) => ts.getCombinedModifierFlags(declaration) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected));
353
+ return hidden ? "a private or protected member of a class, which only its declaration can hold" : undefined;
354
+ }
355
+ /** An object type printed member by member: not an array, a tuple, a function or a default library interface. */
356
+ isPlainObject(type) {
357
+ return !!(type.flags & this.ts.TypeFlags.Object)
358
+ && !this.checker.isArrayType(type) && !this.checker.isTupleType(type)
359
+ && !this.isCallable(type) && !this.isDefaultLibraryInterface(type);
360
+ }
361
+ isCallable(type) {
362
+ const { SignatureKind } = this.ts;
363
+ return this.checker.getSignaturesOfType(type, SignatureKind.Call).length > 0 || this.checker.getSignaturesOfType(type, SignatureKind.Construct).length > 0;
364
+ }
365
+ isDefaultLibraryInterface(type) {
366
+ const { SymbolFlags } = this.ts;
367
+ return !!type.symbol && !!(type.symbol.flags & (SymbolFlags.Interface | SymbolFlags.Class)) && this.isDefaultLibrary(type.symbol);
368
+ }
369
+ /** Declared in the default library, even where a package augments it (as @types/node does some globals). */
370
+ isDefaultLibrary(symbol) {
371
+ return !!symbol.declarations?.some((declaration) => this.program.isSourceFileDefaultLibrary(declaration.getSourceFile()));
372
+ }
373
+ wrapped(printed) {
374
+ if (!printed.compound)
375
+ return printed.text;
376
+ if (printed.text.startsWith("| "))
377
+ return `(\n${INDENT}${indentFollowingLines(printed.text, INDENT)}\n)`;
378
+ return `(${printed.text})`;
379
+ }
380
+ keyOf(name) {
381
+ return IDENTIFIER.test(name) ? name : this.quoted(name);
382
+ }
383
+ quoted(value) {
384
+ const doubleQuoted = JSON.stringify(value);
385
+ if (this.style.quote === "\"")
386
+ return doubleQuoted;
387
+ // Every double quote inside is escaped, so each \" is a quote and
388
+ // never the tail of an escaped backslash.
389
+ return `'${doubleQuoted.slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
390
+ }
391
+ takeName(base) {
392
+ let name = base;
393
+ for (let suffix = 2; this.takenNames.has(name); suffix++)
394
+ name = `${base}${suffix}`;
395
+ this.takenNames.add(name);
396
+ return name;
397
+ }
398
+ fail(path, reason) {
399
+ this.failures.push(`${path}: ${reason}`);
400
+ return atom("unknown");
401
+ }
402
+ }
@@ -0,0 +1,13 @@
1
+ /** What the parent asks for: which export of which module to digest, and the file to compare it with. */
2
+ export type FreshProcessRequest = {
3
+ moduleUrl: string;
4
+ exportName: string;
5
+ file: string;
6
+ };
7
+ /** The comparison, or why none could be made. */
8
+ export type FreshProcessVerdict = {
9
+ same: boolean;
10
+ lines: string[];
11
+ } | {
12
+ failure: string;
13
+ };
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from "fs";
2
+ import { describeSignatureChanges, readSignatureMap, VERIFY_REPORT_PREFIX } from "./writeApiSignatures.js";
3
+ const request = JSON.parse(process.argv[2] ?? "null");
4
+ const namespace = await import(request.moduleUrl);
5
+ const source = namespace[request.exportName];
6
+ let verdict;
7
+ if (typeof source?.apiSignatureEntries !== "function") {
8
+ verdict = { failure: `${request.moduleUrl} has no export "${request.exportName}" that lists API signatures: name the export holding the instance in exportName` };
9
+ }
10
+ else {
11
+ const { movedLines, summary } = describeSignatureChanges(await source.apiSignatureEntries(), readSignatureMap(readFileSync(request.file, "utf8")));
12
+ verdict = { same: movedLines.length === 0, lines: [summary, ...movedLines] };
13
+ }
14
+ // Waited for: a write to a pipe can still be pending when the process exits,
15
+ // and the parent reads the pipe.
16
+ await new Promise((resolveWrite) => process.stdout.write(`\n${VERIFY_REPORT_PREFIX}${JSON.stringify(verdict)}\n`, () => resolveWrite()));
17
+ // The app's module may hold the process open (a client's sockets, a timer),
18
+ // and this process exists for the one comparison.
19
+ process.exit(0);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The module a generator reads the instance from, as both of lambder/build's
3
+ * generators take it: a path relative to the working directory, or a file
4
+ * URL, as a URL (`new URL("../server/index.ts", import.meta.url)`) or as the
5
+ * string `import.meta.resolve()` answers.
6
+ */
7
+ export type LambderModuleLocation = string | URL;
8
+ /** The module as a file URL. A string that already is one is taken as one: resolved as a path, it would name a directory called "file:". */
9
+ export declare const moduleUrlOf: (module: LambderModuleLocation) => string;
10
+ /** The module as a path on disk. */
11
+ export declare const modulePathOf: (module: LambderModuleLocation) => string;
@@ -0,0 +1,6 @@
1
+ import { resolve } from "path";
2
+ import { fileURLToPath, pathToFileURL } from "url";
3
+ /** The module as a file URL. A string that already is one is taken as one: resolved as a path, it would name a directory called "file:". */
4
+ export const moduleUrlOf = (module) => typeof module === "string" && !/^file:/i.test(module) ? pathToFileURL(resolve(module)).href : new URL(module).href;
5
+ /** The module as a path on disk. */
6
+ export const modulePathOf = (module) => fileURLToPath(moduleUrlOf(module));
@@ -0,0 +1,78 @@
1
+ import { type LambderModuleLocation } from "./moduleLocation.js";
2
+ export type LambderApiContractFileOptions = {
3
+ /** The module that exports the Lambder instance, usually the server's entry: a path relative to the working directory, or a file URL. */
4
+ module: LambderModuleLocation;
5
+ /** The export that holds the instance, whose ApiContract is written out. Default: "default", the module's default export. */
6
+ exportName?: string;
7
+ /** The name the written module exports the contract type under. Default: "ApiContractType". */
8
+ typeName?: string;
9
+ /** The tsconfig.json the module compiles under, whose compiler options and path aliases resolve its imports. Relative to the working directory. Default: the nearest tsconfig.json at or above the module's directory. */
10
+ tsconfig?: string;
11
+ /** The TypeScript module to write. Relative to the working directory. */
12
+ file: string;
13
+ /** Write nothing, and answer whether the file on disk is what the contract prints now. Default: false. */
14
+ check?: boolean;
15
+ /** The comment the file opens with, one `//` line per line. It should name what generates the file. Default: a note naming writeApiContract(). */
16
+ header?: string;
17
+ /** Quotes in the generated module. Default: "double". */
18
+ quotes?: "single" | "double";
19
+ /** End the generated declarations and members with semicolons. Default: true. */
20
+ semicolons?: boolean;
21
+ };
22
+ export type LambderApiContractFileResult = {
23
+ /** False when the contract could not be read or printed, a check found the file stale, or the printed type was not the contract's. */
24
+ ok: boolean;
25
+ /** The absolute path. */
26
+ file: string;
27
+ /** How many APIs the contract holds (0 when it could not be read). */
28
+ count: number;
29
+ /** True when the file was (re)written; a file that already holds this text is left untouched. */
30
+ written: boolean;
31
+ /** APIs whose printed types differ from the file that was on disk, by name, counting the declarations each refers to. */
32
+ changed: string[];
33
+ added: string[];
34
+ removed: string[];
35
+ /** What happened, as lines to print: a summary, then one line per API that moved or per member that could not be printed. */
36
+ lines: string[];
37
+ };
38
+ /**
39
+ * Writes an app's API contract type to a TypeScript module as plain types,
40
+ * or checks the one on disk, and names the APIs whose types moved.
41
+ *
42
+ * The contract is the ApiContract property of the instance the module
43
+ * exports, read through the TypeScript compiler under the server's own
44
+ * tsconfig; nothing of the server runs. Every type in it is printed as the
45
+ * structure it resolves to: zod's inferences, mapped and conditional types and
46
+ * the server's own types become plain object types, unions and literals. The
47
+ * written module imports nothing, not even lambder, and exports one type
48
+ * alias, `typeName`. Only the default library's interfaces (Date) are printed
49
+ * by name. A non-generic named type is printed once, as a declaration of its
50
+ * own that the entries refer to.
51
+ *
52
+ * Anything with no plain form fails the call and names where it sits: a
53
+ * function, a symbol-keyed property, an enum, a class's private member, or a
54
+ * type parameter the contract leaves open. Property `readonly` modifiers are
55
+ * not carried over (they never decide assignability); readonly arrays and
56
+ * tuples are.
57
+ *
58
+ * ```ts
59
+ * import { writeApiContract } from "lambder/build";
60
+ *
61
+ * const result = await writeApiContract({
62
+ * module: "server/src/index.ts", // export const lambder = initLambder()...
63
+ * exportName: "lambder",
64
+ * file: "shared/generated/apiContract.generated.ts",
65
+ * check: process.argv.includes("--check"),
66
+ * });
67
+ * console.log(result.lines.join("\n"));
68
+ * process.exit(result.ok ? 0 : 1);
69
+ * ```
70
+ *
71
+ * A check compares the text, so the file should be left out of formatters;
72
+ * each declaration carries a `// prettier-ignore` line for Prettier. A write
73
+ * that changes the file first compiles the new text beside the server's
74
+ * sources and checks every entry against the contract both ways, and writes
75
+ * nothing when one differs. The file is written to a temporary file renamed
76
+ * over the old one, so a build reading it meanwhile never sees half of it.
77
+ */
78
+ export declare const writeApiContract: (options: LambderApiContractFileOptions) => Promise<LambderApiContractFileResult>;