lambder 8.0.2 → 8.1.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 +143 -1
- package/README.md +7 -2
- package/dist/build/ContractTypePrinter.d.ts +115 -0
- package/dist/build/ContractTypePrinter.js +460 -0
- package/dist/build/moduleLocation.d.ts +11 -0
- package/dist/build/moduleLocation.js +6 -0
- package/dist/build/writeApiContract.d.ts +79 -0
- package/dist/build/writeApiContract.js +303 -0
- package/dist/build/writeApiSignatures.d.ts +32 -27
- package/dist/build/writeApiSignatures.js +37 -42
- package/dist/build/writeFileAtomically.d.ts +8 -0
- package/dist/build/writeFileAtomically.js +22 -0
- package/dist/build.d.ts +8 -3
- package/dist/build.js +6 -3
- package/dist/client/LambderUploadRunner.d.ts +96 -0
- package/dist/client/LambderUploadRunner.js +234 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +4 -0
- package/dist/core/Lambder.d.ts +9 -10
- package/dist/core/Lambder.js +9 -10
- package/dist/index.d.ts +11 -1
- package/dist/index.js +8 -0
- package/dist/mock/lambderMockMswHandler.d.ts +10 -4
- package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
- package/dist/mock/lambderMockUploadMswHandler.js +28 -0
- package/dist/mock.d.ts +3 -0
- package/dist/mock.js +4 -0
- package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
- package/dist/shared/contracts/LambderUploadBucket.js +74 -0
- package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
- package/dist/shared/util/LambderContentDisposition.js +13 -0
- package/dist/shared/util/LambderTextDigest.d.ts +7 -5
- package/dist/shared/util/LambderTextDigest.js +11 -5
- package/dist/shared/util/escapeXmlText.d.ts +8 -0
- package/dist/shared/util/escapeXmlText.js +8 -0
- package/dist/shared/wire/LambderApiContract.d.ts +10 -40
- package/dist/shared/wire/LambderApiRefusal.d.ts +6 -0
- package/dist/shared/wire/LambderApiRefusal.js +6 -0
- package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
- package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
- package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
- package/dist/shared/wire/LambderUploadRefusal.js +18 -0
- package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
- package/dist/shared/wire/LambderUploadSchemas.js +30 -0
- package/dist/stores/LambderDdbSdk.js +1 -5
- package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
- package/dist/stores/LambderMemoryUploadBucket.js +219 -0
- package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
- package/dist/stores/LambderS3UploadBucket.js +144 -0
- package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
- package/dist/stores/LambderSdkInstallHint.js +14 -0
- package/dist/testing/LambderTestApp.d.ts +4 -4
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +1 -0
- package/package.json +15 -1
|
@@ -0,0 +1,460 @@
|
|
|
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 declaration by the type it stands for. */
|
|
41
|
+
declarations = new Map();
|
|
42
|
+
/** The name each declaration is printed under, settled from all of them; empty on the pass that finds them. */
|
|
43
|
+
settledNames = new Map();
|
|
44
|
+
/** Names a declaration may not take: the default library's, and the contract's own. */
|
|
45
|
+
reservedNames = new Set();
|
|
46
|
+
/** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
|
|
47
|
+
inProgress = new Set();
|
|
48
|
+
/** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
|
|
49
|
+
exactOptionalProperties;
|
|
50
|
+
constructor(ts, program, checker, style, contractName) {
|
|
51
|
+
this.ts = ts;
|
|
52
|
+
this.program = program;
|
|
53
|
+
this.checker = checker;
|
|
54
|
+
this.style = style;
|
|
55
|
+
this.reservedNames.add(contractName);
|
|
56
|
+
this.exactOptionalProperties = !!program.getCompilerOptions().exactOptionalPropertyTypes;
|
|
57
|
+
// A declaration named after a global the printed text refers to by
|
|
58
|
+
// name (Date) would shadow it.
|
|
59
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
60
|
+
if (!program.isSourceFileDefaultLibrary(sourceFile))
|
|
61
|
+
continue;
|
|
62
|
+
for (const statement of sourceFile.statements) {
|
|
63
|
+
if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isModuleDeclaration(statement))
|
|
64
|
+
&& statement.name && ts.isIdentifier(statement.name))
|
|
65
|
+
this.reservedNames.add(statement.name.text);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Prints each member of the contract type, sorted by name, and every
|
|
71
|
+
* declaration they refer to.
|
|
72
|
+
*
|
|
73
|
+
* In two passes: the first finds every type that needs a declaration,
|
|
74
|
+
* and the second prints with their names settled from all of them. Named
|
|
75
|
+
* as the printer met them, two types wanting one name would trade it
|
|
76
|
+
* whenever the APIs were registered in another order, or a union's
|
|
77
|
+
* members created in another, and the file would move with no API
|
|
78
|
+
* changed.
|
|
79
|
+
*/
|
|
80
|
+
printContract(contract) {
|
|
81
|
+
const members = [...this.checker.getPropertiesOfType(contract)].sort((a, b) => byCodeUnits(a.name, b.name));
|
|
82
|
+
const printEntries = () => members.map((entry) => ({ name: entry.name, text: this.print(this.checker.getTypeOfSymbol(entry), this.keyOf(entry.name)).text }));
|
|
83
|
+
printEntries();
|
|
84
|
+
this.settledNames = this.settleNames();
|
|
85
|
+
this.failures = [];
|
|
86
|
+
this.declarations = new Map();
|
|
87
|
+
const entries = printEntries();
|
|
88
|
+
const declarations = [...this.declarations.values()]
|
|
89
|
+
.map(({ name, text }) => ({ name, text: text ?? "never" }))
|
|
90
|
+
.sort((a, b) => byCodeUnits(a.name, b.name));
|
|
91
|
+
return { entries, declarations, failures: this.failures };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A name for every declaration the first pass found. Of the types that
|
|
95
|
+
* want one name, the one declared first (by file, then position) keeps
|
|
96
|
+
* it, and the others take the lowest free number after it once every
|
|
97
|
+
* type has claimed its own name, so a number never takes the name
|
|
98
|
+
* another type is declared under. Types declared at one place (two
|
|
99
|
+
* instantiations of a generic) keep the order the entries reached them
|
|
100
|
+
* in, by entry name.
|
|
101
|
+
*/
|
|
102
|
+
settleNames() {
|
|
103
|
+
const wanting = [...this.declarations].sort(([, a], [, b]) => byCodeUnits(a.base, b.base) || byCodeUnits(a.origin, b.origin));
|
|
104
|
+
const taken = new Set(this.reservedNames);
|
|
105
|
+
const settled = new Map();
|
|
106
|
+
const numbered = [];
|
|
107
|
+
for (const [type, { base }] of wanting) {
|
|
108
|
+
if (taken.has(base)) {
|
|
109
|
+
numbered.push([type, base]);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
taken.add(base);
|
|
113
|
+
settled.set(type, base);
|
|
114
|
+
}
|
|
115
|
+
for (const [type, base] of numbered) {
|
|
116
|
+
let suffix = 2;
|
|
117
|
+
while (taken.has(`${base}${suffix}`))
|
|
118
|
+
suffix++;
|
|
119
|
+
taken.add(`${base}${suffix}`);
|
|
120
|
+
settled.set(type, `${base}${suffix}`);
|
|
121
|
+
}
|
|
122
|
+
return settled;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `label value`, with a union too long for one line starting on the next
|
|
126
|
+
* line, one member per line: what a property, an index signature and a
|
|
127
|
+
* declaration all print through.
|
|
128
|
+
*/
|
|
129
|
+
labeledValue(label, value) {
|
|
130
|
+
return value.startsWith("| ") ? `${label}\n${INDENT}${indentFollowingLines(value, INDENT)}` : `${label} ${value}`;
|
|
131
|
+
}
|
|
132
|
+
print(type, path) {
|
|
133
|
+
const { TypeFlags } = this.ts;
|
|
134
|
+
const flags = type.flags;
|
|
135
|
+
// The compiler's own `any` is the one a source wrote (or inferred);
|
|
136
|
+
// any other is the error type a compile error leaves behind.
|
|
137
|
+
if (flags & TypeFlags.Any) {
|
|
138
|
+
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");
|
|
139
|
+
}
|
|
140
|
+
if (flags & TypeFlags.Unknown)
|
|
141
|
+
return atom("unknown");
|
|
142
|
+
if (flags & TypeFlags.Never)
|
|
143
|
+
return atom("never");
|
|
144
|
+
if (flags & TypeFlags.String)
|
|
145
|
+
return atom("string");
|
|
146
|
+
if (flags & TypeFlags.Number)
|
|
147
|
+
return atom("number");
|
|
148
|
+
if (flags & TypeFlags.BigInt)
|
|
149
|
+
return atom("bigint");
|
|
150
|
+
if (flags & TypeFlags.Boolean)
|
|
151
|
+
return atom("boolean");
|
|
152
|
+
if (flags & TypeFlags.Void)
|
|
153
|
+
return atom("void");
|
|
154
|
+
if (flags & TypeFlags.Undefined)
|
|
155
|
+
return atom("undefined");
|
|
156
|
+
if (flags & TypeFlags.Null)
|
|
157
|
+
return atom("null");
|
|
158
|
+
if (flags & TypeFlags.ESSymbol)
|
|
159
|
+
return atom("symbol");
|
|
160
|
+
if (flags & TypeFlags.NonPrimitive)
|
|
161
|
+
return atom("object");
|
|
162
|
+
// Before the literals: an enum member is a literal type as well.
|
|
163
|
+
if (flags & TypeFlags.EnumLike)
|
|
164
|
+
return this.fail(path, `${this.checker.typeToString(type)}, an enum, which only its declaration can name`);
|
|
165
|
+
if (flags & TypeFlags.StringLiteral)
|
|
166
|
+
return atom(this.quoted(type.value));
|
|
167
|
+
if (flags & TypeFlags.NumberLiteral)
|
|
168
|
+
return atom(String(type.value));
|
|
169
|
+
if (flags & TypeFlags.BigIntLiteral) {
|
|
170
|
+
const { negative, base10Value } = type.value;
|
|
171
|
+
return atom(`${negative ? "-" : ""}${base10Value}n`);
|
|
172
|
+
}
|
|
173
|
+
if (flags & TypeFlags.BooleanLiteral)
|
|
174
|
+
return atom(this.checker.typeToString(type));
|
|
175
|
+
if (flags & TypeFlags.UniqueESSymbol)
|
|
176
|
+
return this.fail(path, "a unique symbol, which only its declaration can name");
|
|
177
|
+
if (flags & TypeFlags.TemplateLiteral)
|
|
178
|
+
return this.printTemplateLiteral(type, path);
|
|
179
|
+
if (flags & TypeFlags.StringMapping) {
|
|
180
|
+
const mapping = type;
|
|
181
|
+
return atom(`${mapping.symbol.name}<${this.print(mapping.type, path).text}>`);
|
|
182
|
+
}
|
|
183
|
+
if (flags & (TypeFlags.Union | TypeFlags.Intersection | TypeFlags.Object))
|
|
184
|
+
return this.printComposite(type, path);
|
|
185
|
+
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)`);
|
|
186
|
+
}
|
|
187
|
+
/** A union, an intersection or an object: printed in place, or as a reference to a declaration of its own. */
|
|
188
|
+
printComposite(type, path) {
|
|
189
|
+
const known = this.declarations.get(type);
|
|
190
|
+
if (known)
|
|
191
|
+
return atom(known.name);
|
|
192
|
+
const own = this.ownDeclarationOf(type);
|
|
193
|
+
if (own) {
|
|
194
|
+
// Registered before the body is printed, so a reference to itself
|
|
195
|
+
// inside the body finds the name.
|
|
196
|
+
const declaration = this.declare(type, own);
|
|
197
|
+
declaration.text = this.printStructure(type, path).text;
|
|
198
|
+
return atom(declaration.name);
|
|
199
|
+
}
|
|
200
|
+
if (this.inProgress.has(type))
|
|
201
|
+
return atom(this.declare(type, this.recursiveDeclarationOf(type)).name);
|
|
202
|
+
this.inProgress.add(type);
|
|
203
|
+
const printed = this.printStructure(type, path);
|
|
204
|
+
this.inProgress.delete(type);
|
|
205
|
+
// Named while its body was being printed: it refers to itself.
|
|
206
|
+
const recursive = this.declarations.get(type);
|
|
207
|
+
if (!recursive)
|
|
208
|
+
return printed;
|
|
209
|
+
recursive.text = printed.text;
|
|
210
|
+
return atom(recursive.name);
|
|
211
|
+
}
|
|
212
|
+
/** A type's declaration, under the name it is settled to once the first pass has settled them. */
|
|
213
|
+
declare(type, { base, origin }) {
|
|
214
|
+
const declaration = { base, origin, name: this.settledNames.get(type) ?? base, text: null };
|
|
215
|
+
this.declarations.set(type, declaration);
|
|
216
|
+
return declaration;
|
|
217
|
+
}
|
|
218
|
+
/** The name a type is declared under and where, when it is one to print as a declaration: non-generic, and not the default library's. */
|
|
219
|
+
ownDeclarationOf(type) {
|
|
220
|
+
const { ObjectFlags, TypeFlags } = this.ts;
|
|
221
|
+
if (type.aliasSymbol) {
|
|
222
|
+
return type.aliasTypeArguments?.length || this.isDefaultLibrary(type.aliasSymbol) ? undefined : this.declaredAs(type.aliasSymbol);
|
|
223
|
+
}
|
|
224
|
+
if (!(type.flags & TypeFlags.Object))
|
|
225
|
+
return undefined;
|
|
226
|
+
const objectFlags = type.objectFlags;
|
|
227
|
+
if (!(objectFlags & (ObjectFlags.Interface | ObjectFlags.Class)))
|
|
228
|
+
return undefined;
|
|
229
|
+
// A class, or an interface with a base type, is a reference to itself
|
|
230
|
+
// (for its `this` type). A generic one's instances are references to
|
|
231
|
+
// it instead, and are printed in place.
|
|
232
|
+
if (objectFlags & ObjectFlags.Reference && (type.target !== type || type.typeParameters?.length))
|
|
233
|
+
return undefined;
|
|
234
|
+
return this.isDefaultLibrary(type.symbol) ? undefined : this.declaredAs(type.symbol);
|
|
235
|
+
}
|
|
236
|
+
/** A symbol's name to declare a type under, and where the symbol is declared. */
|
|
237
|
+
declaredAs(symbol) {
|
|
238
|
+
const base = this.nameOf(symbol);
|
|
239
|
+
return base === undefined ? undefined : { base, origin: this.originOf(symbol) };
|
|
240
|
+
}
|
|
241
|
+
/** Where a symbol is declared, as text that orders by file and then position; empty for one declared nowhere. */
|
|
242
|
+
originOf(symbol) {
|
|
243
|
+
const declaration = symbol?.declarations?.[0];
|
|
244
|
+
return declaration ? `${declaration.getSourceFile().fileName}\0${String(declaration.pos).padStart(10, "0")}` : "";
|
|
245
|
+
}
|
|
246
|
+
/** 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. */
|
|
247
|
+
nameOf(symbol) {
|
|
248
|
+
const declaration = symbol.declarations?.[0];
|
|
249
|
+
const declared = declaration && this.ts.getNameOfDeclaration(declaration);
|
|
250
|
+
const name = declared && this.ts.isIdentifier(declared) ? declared.text : symbol.name;
|
|
251
|
+
return IDENTIFIER.test(name) && name !== "default" && !name.startsWith("__") ? name : undefined;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A name for a type that refers to itself and has none of its own: what it
|
|
255
|
+
* instantiates followed by its arguments (a JSON mapping of a Tree is
|
|
256
|
+
* `JsonOfTree`, a `Tree<string>` is `TreeString`), or `RecursiveType`.
|
|
257
|
+
* Its origin is where what it instantiates is declared, then where each
|
|
258
|
+
* argument is, so two instantiations named alike are told apart by their
|
|
259
|
+
* arguments.
|
|
260
|
+
*/
|
|
261
|
+
recursiveDeclarationOf(type) {
|
|
262
|
+
const { ObjectFlags, TypeFlags } = this.ts;
|
|
263
|
+
let instantiated;
|
|
264
|
+
if (type.aliasSymbol) {
|
|
265
|
+
instantiated = { symbol: type.aliasSymbol, typeArguments: type.aliasTypeArguments ?? [] };
|
|
266
|
+
}
|
|
267
|
+
else if (type.flags & TypeFlags.Object && type.objectFlags & ObjectFlags.Reference) {
|
|
268
|
+
const { target } = type;
|
|
269
|
+
const typeArguments = this.checker.getTypeArguments(type).slice(0, target.typeParameters?.length ?? 0);
|
|
270
|
+
instantiated = { symbol: target.symbol, typeArguments };
|
|
271
|
+
}
|
|
272
|
+
const base = instantiated && this.nameOf(instantiated.symbol);
|
|
273
|
+
if (!instantiated || !base)
|
|
274
|
+
return { base: "RecursiveType", origin: this.originOf(type.symbol) };
|
|
275
|
+
const argumentNames = instantiated.typeArguments
|
|
276
|
+
.map((argument) => (argument.aliasSymbol && this.nameOf(argument.aliasSymbol)) ?? (argument.symbol && this.nameOf(argument.symbol)) ?? this.checker.typeToString(argument))
|
|
277
|
+
.filter((name) => IDENTIFIER.test(name));
|
|
278
|
+
return {
|
|
279
|
+
base: `${base}${argumentNames.map((name) => name[0].toUpperCase() + name.slice(1)).join("")}`,
|
|
280
|
+
origin: [instantiated.symbol, ...instantiated.typeArguments.map((argument) => argument.aliasSymbol ?? argument.symbol)].map((symbol) => this.originOf(symbol)).join("\n"),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
printStructure(type, path) {
|
|
284
|
+
const { TypeFlags } = this.ts;
|
|
285
|
+
if (type.flags & TypeFlags.Union)
|
|
286
|
+
return this.printUnion(type, path);
|
|
287
|
+
if (type.flags & TypeFlags.Intersection)
|
|
288
|
+
return this.printIntersection(type, path);
|
|
289
|
+
return this.printObject(type, path);
|
|
290
|
+
}
|
|
291
|
+
printUnion(type, path) {
|
|
292
|
+
return this.printUnionOf(type.types, path);
|
|
293
|
+
}
|
|
294
|
+
printUnionOf(types, path) {
|
|
295
|
+
const members = [];
|
|
296
|
+
// boolean is the union of its two literals, and a union holding it
|
|
297
|
+
// holds them flattened in: they are put back together.
|
|
298
|
+
const booleans = new Set();
|
|
299
|
+
for (const member of types) {
|
|
300
|
+
if (member.flags & this.ts.TypeFlags.BooleanLiteral)
|
|
301
|
+
booleans.add(this.checker.typeToString(member));
|
|
302
|
+
else
|
|
303
|
+
members.push(this.print(member, path).text);
|
|
304
|
+
}
|
|
305
|
+
if (booleans.size === 2)
|
|
306
|
+
members.push("boolean");
|
|
307
|
+
else
|
|
308
|
+
members.push(...booleans);
|
|
309
|
+
members.sort((a, b) => unionMemberRank(a) - unionMemberRank(b) || byCodeUnits(a, b));
|
|
310
|
+
const oneLine = members.join(" | ");
|
|
311
|
+
if (oneLine.length <= UNION_LINE_WIDTH && !oneLine.includes("\n"))
|
|
312
|
+
return { text: oneLine, compound: true };
|
|
313
|
+
return { text: members.map((member) => `| ${indentFollowingLines(member, " ")}`).join("\n"), compound: true };
|
|
314
|
+
}
|
|
315
|
+
printIntersection(type, path) {
|
|
316
|
+
// Objects intersected are one object, and printed as the one they are.
|
|
317
|
+
if (type.types.every((member) => this.isPlainObject(member)))
|
|
318
|
+
return this.printMembers(type, path);
|
|
319
|
+
const members = type.types.map((member) => this.wrapped(this.print(member, path))).sort(byCodeUnits);
|
|
320
|
+
return { text: members.join(" & "), compound: true };
|
|
321
|
+
}
|
|
322
|
+
printObject(type, path) {
|
|
323
|
+
const { checker, ts } = this;
|
|
324
|
+
if (checker.isTupleType(type))
|
|
325
|
+
return this.printTuple(type, path);
|
|
326
|
+
if (checker.isArrayType(type)) {
|
|
327
|
+
const reference = type;
|
|
328
|
+
const element = this.print(checker.getTypeArguments(reference)[0], `${path}[]`);
|
|
329
|
+
const readonly = reference.target.symbol?.escapedName === "ReadonlyArray";
|
|
330
|
+
return { text: `${readonly ? "readonly " : ""}${this.wrapped(element)}[]`, compound: readonly };
|
|
331
|
+
}
|
|
332
|
+
if (this.isCallable(type))
|
|
333
|
+
return this.fail(path, `${checker.typeToString(type)}, a function, which is not data`);
|
|
334
|
+
if (this.isDefaultLibraryInterface(type)) {
|
|
335
|
+
const reference = type;
|
|
336
|
+
const parameterCount = type.objectFlags & ts.ObjectFlags.Reference ? reference.target.typeParameters?.length ?? 0 : 0;
|
|
337
|
+
const typeArguments = parameterCount ? checker.getTypeArguments(reference).slice(0, parameterCount) : [];
|
|
338
|
+
const name = checker.getFullyQualifiedName(type.symbol);
|
|
339
|
+
return atom(typeArguments.length ? `${name}<${typeArguments.map((argument) => this.print(argument, path).text).join(", ")}>` : name);
|
|
340
|
+
}
|
|
341
|
+
return this.printMembers(type, path);
|
|
342
|
+
}
|
|
343
|
+
printMembers(type, path) {
|
|
344
|
+
const { checker, ts } = this;
|
|
345
|
+
const lines = [];
|
|
346
|
+
for (const index of checker.getIndexInfosOfType(type)) {
|
|
347
|
+
const key = this.print(index.keyType, `${path}[key]`).text;
|
|
348
|
+
lines.push(this.labeledValue(`${index.isReadonly ? "readonly " : ""}[key: ${key}]:`, this.print(index.type, `${path}[${key}]`).text));
|
|
349
|
+
}
|
|
350
|
+
for (const property of [...checker.getPropertiesOfType(type)].sort(bySourceOrder)) {
|
|
351
|
+
const propertyPath = `${path}.${property.name}`;
|
|
352
|
+
const unprintable = this.unprintableProperty(property);
|
|
353
|
+
if (unprintable) {
|
|
354
|
+
this.fail(propertyPath, unprintable);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const optional = !!(property.flags & ts.SymbolFlags.Optional);
|
|
358
|
+
const printed = optional ? this.printOptionalMember(checker.getTypeOfSymbol(property), propertyPath) : this.print(checker.getTypeOfSymbol(property), propertyPath);
|
|
359
|
+
lines.push(this.labeledValue(`${this.keyOf(property.name)}${optional ? "?" : ""}:`, printed.text));
|
|
360
|
+
}
|
|
361
|
+
if (!lines.length)
|
|
362
|
+
return atom("{}");
|
|
363
|
+
return atom(`{\n${lines.map((line) => INDENT + indentFollowingLines(line, INDENT) + this.style.semicolon).join("\n")}\n}`);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* An optional member's type as its source wrote it. Under
|
|
367
|
+
* exactOptionalPropertyTypes the compiler adds its own "missing"
|
|
368
|
+
* undefined, a different type from the `undefined` a source writes;
|
|
369
|
+
* printed as `| undefined` it would let the member be undefined, which
|
|
370
|
+
* the source does not.
|
|
371
|
+
*/
|
|
372
|
+
printOptionalMember(type, path) {
|
|
373
|
+
if (!this.exactOptionalProperties || !(type.flags & this.ts.TypeFlags.Union))
|
|
374
|
+
return this.print(type, path);
|
|
375
|
+
const undefinedType = this.checker.getUndefinedType();
|
|
376
|
+
const written = type.types.filter((member) => !(member.flags & this.ts.TypeFlags.Undefined) || member === undefinedType);
|
|
377
|
+
if (written.length === type.types.length)
|
|
378
|
+
return this.print(type, path);
|
|
379
|
+
return written.length === 1 ? this.print(written[0], path) : this.printUnionOf(written, path);
|
|
380
|
+
}
|
|
381
|
+
printTuple(type, path) {
|
|
382
|
+
const { ElementFlags } = this.ts;
|
|
383
|
+
const { elementFlags, labeledElementDeclarations, readonly } = type.target;
|
|
384
|
+
const labelOf = (index) => {
|
|
385
|
+
const declaration = labeledElementDeclarations?.[index];
|
|
386
|
+
return declaration && this.ts.isIdentifier(declaration.name) ? declaration.name.text : undefined;
|
|
387
|
+
};
|
|
388
|
+
// Labels are all or nothing in a tuple.
|
|
389
|
+
const labeled = elementFlags.every((_, index) => labelOf(index) !== undefined);
|
|
390
|
+
const elements = this.checker.getTypeArguments(type).slice(0, elementFlags.length).map((element, index) => {
|
|
391
|
+
const flag = elementFlags[index];
|
|
392
|
+
const printed = this.print(element, `${path}[${index}]`);
|
|
393
|
+
const label = labeled ? `${labelOf(index)}` : "";
|
|
394
|
+
if (flag & ElementFlags.Rest)
|
|
395
|
+
return `...${label ? `${label}: ` : ""}${this.wrapped(printed)}[]`;
|
|
396
|
+
if (flag & ElementFlags.Variadic)
|
|
397
|
+
return `...${label ? `${label}: ` : ""}${printed.text}`;
|
|
398
|
+
if (flag & ElementFlags.Optional)
|
|
399
|
+
return label ? `${label}?: ${printed.text}` : `${this.wrapped(printed)}?`;
|
|
400
|
+
return label ? `${label}: ${printed.text}` : printed.text;
|
|
401
|
+
});
|
|
402
|
+
return { text: `${readonly ? "readonly " : ""}[${elements.join(", ")}]`, compound: readonly };
|
|
403
|
+
}
|
|
404
|
+
printTemplateLiteral(type, path) {
|
|
405
|
+
// A cooked text as template source: JSON's escapes, plus the two a template adds.
|
|
406
|
+
const escape = (text) => JSON.stringify(text).slice(1, -1).replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
407
|
+
const spans = type.types.map((span, index) => `\${${this.print(span, path).text}}${escape(type.texts[index + 1])}`);
|
|
408
|
+
return atom(`\`${escape(type.texts[0])}${spans.join("")}\``);
|
|
409
|
+
}
|
|
410
|
+
/** Why a property cannot be printed as a plain member, or undefined when it can. */
|
|
411
|
+
unprintableProperty(property) {
|
|
412
|
+
const { ts } = this;
|
|
413
|
+
if (String(property.escapedName).startsWith("__@"))
|
|
414
|
+
return "a symbol-keyed property, which only its declaration can name";
|
|
415
|
+
if (property.name.startsWith("#"))
|
|
416
|
+
return "a private field of a class, which only its declaration can hold";
|
|
417
|
+
const hidden = property.declarations?.some((declaration) => ts.getCombinedModifierFlags(declaration) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected));
|
|
418
|
+
return hidden ? "a private or protected member of a class, which only its declaration can hold" : undefined;
|
|
419
|
+
}
|
|
420
|
+
/** An object type printed member by member: not an array, a tuple, a function or a default library interface. */
|
|
421
|
+
isPlainObject(type) {
|
|
422
|
+
return !!(type.flags & this.ts.TypeFlags.Object)
|
|
423
|
+
&& !this.checker.isArrayType(type) && !this.checker.isTupleType(type)
|
|
424
|
+
&& !this.isCallable(type) && !this.isDefaultLibraryInterface(type);
|
|
425
|
+
}
|
|
426
|
+
isCallable(type) {
|
|
427
|
+
const { SignatureKind } = this.ts;
|
|
428
|
+
return this.checker.getSignaturesOfType(type, SignatureKind.Call).length > 0 || this.checker.getSignaturesOfType(type, SignatureKind.Construct).length > 0;
|
|
429
|
+
}
|
|
430
|
+
isDefaultLibraryInterface(type) {
|
|
431
|
+
const { SymbolFlags } = this.ts;
|
|
432
|
+
return !!type.symbol && !!(type.symbol.flags & (SymbolFlags.Interface | SymbolFlags.Class)) && this.isDefaultLibrary(type.symbol);
|
|
433
|
+
}
|
|
434
|
+
/** Declared in the default library, even where a package augments it (as @types/node does some globals). */
|
|
435
|
+
isDefaultLibrary(symbol) {
|
|
436
|
+
return !!symbol.declarations?.some((declaration) => this.program.isSourceFileDefaultLibrary(declaration.getSourceFile()));
|
|
437
|
+
}
|
|
438
|
+
wrapped(printed) {
|
|
439
|
+
if (!printed.compound)
|
|
440
|
+
return printed.text;
|
|
441
|
+
if (printed.text.startsWith("| "))
|
|
442
|
+
return `(\n${INDENT}${indentFollowingLines(printed.text, INDENT)}\n)`;
|
|
443
|
+
return `(${printed.text})`;
|
|
444
|
+
}
|
|
445
|
+
keyOf(name) {
|
|
446
|
+
return IDENTIFIER.test(name) ? name : this.quoted(name);
|
|
447
|
+
}
|
|
448
|
+
quoted(value) {
|
|
449
|
+
const doubleQuoted = JSON.stringify(value);
|
|
450
|
+
if (this.style.quote === "\"")
|
|
451
|
+
return doubleQuoted;
|
|
452
|
+
// Every double quote inside is escaped, so each \" is a quote and
|
|
453
|
+
// never the tail of an escaped backslash.
|
|
454
|
+
return `'${doubleQuoted.slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
|
|
455
|
+
}
|
|
456
|
+
fail(path, reason) {
|
|
457
|
+
this.failures.push(`${path}: ${reason}`);
|
|
458
|
+
return atom("unknown");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
@@ -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,79 @@
|
|
|
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; two that want one name are numbered by where
|
|
51
|
+
* each is declared, never by the order the APIs were registered in.
|
|
52
|
+
*
|
|
53
|
+
* Anything with no plain form fails the call and names where it sits: a
|
|
54
|
+
* function, a symbol-keyed property, an enum, a class's private member, or a
|
|
55
|
+
* type parameter the contract leaves open. Property `readonly` modifiers are
|
|
56
|
+
* not carried over (they never decide assignability); readonly arrays and
|
|
57
|
+
* tuples are.
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { writeApiContract } from "lambder/build";
|
|
61
|
+
*
|
|
62
|
+
* const result = await writeApiContract({
|
|
63
|
+
* module: "server/src/index.ts", // export const lambder = initLambder()...
|
|
64
|
+
* exportName: "lambder",
|
|
65
|
+
* file: "shared/generated/apiContract.generated.ts",
|
|
66
|
+
* check: process.argv.includes("--check"),
|
|
67
|
+
* });
|
|
68
|
+
* console.log(result.lines.join("\n"));
|
|
69
|
+
* process.exit(result.ok ? 0 : 1);
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* A check compares the text, so the file should be left out of formatters;
|
|
73
|
+
* each declaration carries a `// prettier-ignore` line for Prettier. A write
|
|
74
|
+
* that changes the file first compiles the new text beside the server's
|
|
75
|
+
* sources and checks every entry against the contract both ways, and writes
|
|
76
|
+
* nothing when one differs. The file is written to a temporary file renamed
|
|
77
|
+
* over the old one, so a build reading it meanwhile never sees half of it.
|
|
78
|
+
*/
|
|
79
|
+
export declare const writeApiContract: (options: LambderApiContractFileOptions) => Promise<LambderApiContractFileResult>;
|