lambder 8.0.2 → 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 (53) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +7 -2
  3. package/dist/build/ContractTypePrinter.d.ts +85 -0
  4. package/dist/build/ContractTypePrinter.js +402 -0
  5. package/dist/build/moduleLocation.d.ts +11 -0
  6. package/dist/build/moduleLocation.js +6 -0
  7. package/dist/build/writeApiContract.d.ts +78 -0
  8. package/dist/build/writeApiContract.js +302 -0
  9. package/dist/build/writeApiSignatures.d.ts +32 -27
  10. package/dist/build/writeApiSignatures.js +37 -42
  11. package/dist/build/writeFileAtomically.d.ts +8 -0
  12. package/dist/build/writeFileAtomically.js +22 -0
  13. package/dist/build.d.ts +8 -3
  14. package/dist/build.js +6 -3
  15. package/dist/client/LambderUploadRunner.d.ts +96 -0
  16. package/dist/client/LambderUploadRunner.js +234 -0
  17. package/dist/client.d.ts +4 -0
  18. package/dist/client.js +4 -0
  19. package/dist/core/Lambder.d.ts +9 -10
  20. package/dist/core/Lambder.js +9 -10
  21. package/dist/index.d.ts +11 -1
  22. package/dist/index.js +8 -0
  23. package/dist/mock/lambderMockMswHandler.d.ts +10 -4
  24. package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
  25. package/dist/mock/lambderMockUploadMswHandler.js +28 -0
  26. package/dist/mock.d.ts +3 -0
  27. package/dist/mock.js +4 -0
  28. package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
  29. package/dist/shared/contracts/LambderUploadBucket.js +74 -0
  30. package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
  31. package/dist/shared/util/LambderContentDisposition.js +13 -0
  32. package/dist/shared/util/LambderTextDigest.d.ts +7 -5
  33. package/dist/shared/util/LambderTextDigest.js +11 -5
  34. package/dist/shared/wire/LambderApiContract.d.ts +10 -40
  35. package/dist/shared/wire/LambderApiRefusal.d.ts +6 -0
  36. package/dist/shared/wire/LambderApiRefusal.js +6 -0
  37. package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
  38. package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
  39. package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
  40. package/dist/shared/wire/LambderUploadRefusal.js +18 -0
  41. package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
  42. package/dist/shared/wire/LambderUploadSchemas.js +30 -0
  43. package/dist/stores/LambderDdbSdk.js +1 -5
  44. package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
  45. package/dist/stores/LambderMemoryUploadBucket.js +219 -0
  46. package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
  47. package/dist/stores/LambderS3UploadBucket.js +144 -0
  48. package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
  49. package/dist/stores/LambderSdkInstallHint.js +14 -0
  50. package/dist/testing/LambderTestApp.d.ts +4 -4
  51. package/dist/testing.d.ts +2 -0
  52. package/dist/testing.js +1 -0
  53. package/package.json +15 -1
package/CHANGELOG.md CHANGED
@@ -9,7 +9,121 @@ sit on its first published patch, and later patches list only what they changed.
9
9
  Releases up to 3.2.6 carry git tags; the ones after it were published without
10
10
  one, so versions are not cross-linked to tag comparisons here.
11
11
 
12
- ## [8.0.1] - 2026-09-25
12
+ ## [8.1.1] - 2026-09-26
13
+
14
+ Two additions, and three breaking changes a minor line carries here on
15
+ purpose. An app's contract can now be written out as a generated file of
16
+ plain types for its clients to import, which does by default what
17
+ `LambderFlattenContract` asked each app to spell out, so that helper is gone.
18
+ And direct uploads, files posted from the browser straight to S3 on tickets
19
+ the server signs, are part of the framework.
20
+
21
+ ### Added
22
+
23
+ - **`writeApiContract` in `lambder/build`: the contract as a generated file.**
24
+ A client that imports `typeof lambder.ApiContract` from the server compiles
25
+ the server to get it, every endpoint's schemas and the libraries they infer
26
+ through included, and reads it as the intersection chaining built. In a
27
+ 193-endpoint app that was 17 of the frontend check's 19 million type
28
+ instantiations and 2.4 of its 4.8 GB. `writeApiContract` reads the
29
+ `ApiContract` of the instance a module exports (`module`, `exportName`)
30
+ through the TypeScript compiler, under the server's own tsconfig and
31
+ without running any of it, and writes the contract as one object type with
32
+ plain members to a module that imports nothing. The app declares nothing
33
+ for it, and its clients (a frontend, another service's
34
+ `LambderInvokeCaller`, its own tests through `lambderTestApp`) import the
35
+ type from there: the same frontend check fell to 1.8 million
36
+ instantiations and 2.4 GB.
37
+ - Every type is printed as the structure it resolves to. The default
38
+ library's interfaces (`Date`) keep their names, a non-generic named type
39
+ (an alias, an interface, a class) is printed once as a declaration the
40
+ entries refer to, which is also how a recursive type refers to itself,
41
+ and properties keep the order they are written in, so the file changes
42
+ only when an API does.
43
+ - Anything with no plain form fails the call and names where it sits: a
44
+ function, a symbol key, an enum, a class's private member, an open type
45
+ parameter, and a type a compile error left unresolved, wherever in the
46
+ server's sources the error is.
47
+ - A write compiles the new text beside the server's sources and checks each
48
+ entry against the contract in both directions before it touches the file.
49
+ `check: true` writes nothing and fails a stale file. Both name the APIs
50
+ that moved, counting a change to a shared declaration against every API
51
+ that reaches it.
52
+ - `typescript` 5.4 or later is an optional peer dependency, loaded only when
53
+ `writeApiContract` runs. It needs the compiler API, so 5.x or 6.x:
54
+ TypeScript 7 ships none, and the call says so when it finds a 7.
55
+
56
+ See [the contract as a generated file](./docs/apis.md#the-contract-as-a-generated-file).
57
+
58
+ - **Direct uploads.** A file too large for an API payload goes from the
59
+ browser straight to object storage, on a ticket the app's endpoint signs
60
+ for exactly that file: its key, byte size, content type and SHA-256, all
61
+ enforced by storage, and verified by the server before the app's record
62
+ counts it as uploaded.
63
+ - `LambderUploadBucket`, the interface a bucket implements: sign a ticket,
64
+ verify what arrived, sign a download link, and read, write, copy and
65
+ delete objects for the rest of their life. `LambderS3UploadBucket`
66
+ implements it over an S3 presigned POST whose policy pins every fact;
67
+ `@aws-sdk/s3-presigned-post` and `@aws-sdk/s3-request-presigner` join
68
+ `@aws-sdk/client-s3` as optional peers, each loaded on first use.
69
+ `writeObject` sends the checksum it is given or has the SDK compute one.
70
+ - Lifetimes at both levels: `ticketLifetimeSeconds` and
71
+ `downloadLifetimeSeconds` on the bucket, `lifetimeSeconds` on a ticket or
72
+ a link, each held to S3's seven days. What a stored object carries,
73
+ `object` on a ticket or a write: tags (how an object gets a time to live,
74
+ through a lifecycle rule), metadata, `Cache-Control` and a
75
+ `Content-Disposition`, all pinned in the ticket's policy. A download link
76
+ takes its own `contentDisposition`, to save a file under its name.
77
+ - `LambderUploadRunner` in `lambder/client`, the browser half: checks the
78
+ file against the rule, hashes it, asks for a ticket, posts it with
79
+ progress, tries again after a dropped, failing, timed-out or stalled
80
+ connection with a growing wait, asks for a new ticket when storage says
81
+ the old one expired, stops on an abort signal it also hands to the app's
82
+ own calls, and has the server confirm. A failure is a `LambderUploadError`
83
+ whose `reason` a screen words. It posts over XMLHttpRequest for progress,
84
+ and over fetch where that is all there is.
85
+ - `LambderMemoryUploadBucket`, the same bucket in memory, holding a post to
86
+ the rules S3 holds a presigned POST to and refusing with S3's statuses and
87
+ XML errors, so a runner takes the same path against it as against S3; and
88
+ `lambderMockUploadMswHandler` in `lambder/mock`, which puts it behind MSW
89
+ for a mock app's uploads.
90
+ - `LambderUploadFileFactsSchema` and `LambderUploadTicketSchema`, the zod
91
+ schemas of the two shapes that cross the app's own API; and
92
+ `checkUploadRule` and `refuseUnacceptedUpload`, for a bucket of an app's
93
+ own to refuse a file as Lambder's do.
94
+ - Three refusal codes for a file a rule does not accept:
95
+ `lambder/upload-empty`, `lambder/upload-type-rejected` and
96
+ `lambder/upload-too-large`.
97
+
98
+ See [Direct uploads](./docs/uploads.md).
99
+
100
+ ### Removed
101
+
102
+ - **`LambderFlattenContract`.** It collapsed the chained contract into an
103
+ interface an app had to declare by hand for its clients' type checks to stay
104
+ cheap. A client that needs that now imports the contract `writeApiContract`
105
+ generates, which is flat by construction, and the server's own reads of its
106
+ contract cost it little. Replace
107
+ `export interface ApiContractType extends LambderFlattenContract<typeof lambder.ApiContract> {}`
108
+ with `export type ApiContractType = typeof lambder.ApiContract`, or drop it
109
+ and point the clients at the generated file.
110
+
111
+ ### Changed
112
+
113
+ - **`writeApiSignatures({ module, exportName, file })`.** It takes the module
114
+ that exports the instance, as `writeApiContract` does, and imports it,
115
+ rather than the instance and then its module again for the fresh-process
116
+ check. That check now runs by default; `verifyInFreshProcess: false` skips
117
+ it. Replace `writeApiSignatures(lambder, { file, verifyInFreshProcess: {
118
+ module, exportName } })` with `writeApiSignatures({ module, exportName,
119
+ file })`. Both generators take `module` as a path or a file URL
120
+ (`LambderModuleLocation`).
121
+ - **`LambderMswModule` names `http.all` beside `http.post`.** It is the one
122
+ description of the msw module both mock adapters take, the API's and an
123
+ upload bucket's. The real `msw` module fits it as before; a hand-built
124
+ stand-in for it needs an `all` as well.
125
+
126
+ ## [8.0.2] - 2026-09-25
13
127
 
14
128
  A major, out of a review of 7.3.1. Most of it closes holes: session writes
15
129
  that could undo a logout or a password change, a login any website could
package/README.md CHANGED
@@ -67,6 +67,10 @@ const company = await caller.api("getCompany", { slug: "acme" });
67
67
  - **Frontend hosting.** Serve a build from a folder, S3, R2 or any HTTP
68
68
  origin, with an app shell rendered through a build-pipeline-safe template
69
69
  engine.
70
+ - **Direct uploads.** Files go from the browser straight to S3 on tickets
71
+ that pin their size, type and SHA-256, with a browser runner that hashes,
72
+ retries and reports progress, and a memory bucket that holds tests and the
73
+ mock to the same rules.
70
74
  - **Runs anywhere Lambda does.** API Gateway REST APIs (payload v1), HTTP APIs
71
75
  (payload v2) and Lambda Function URLs; the payload format is detected per
72
76
  event.
@@ -112,7 +116,7 @@ The package ships five entry points; pick by where the code runs:
112
116
  | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
113
117
  | `lambder/mock` | Browser and Node, in development and tests | `LambderMockApp`, the mock runtime: your typed contract served from mock handlers over the real API pipeline and memory stores |
114
118
  | `lambder/testing` | Node, in tests | `lambderTestApp`: your real instance under test in this process, memory stores put under it in place, simulated browsers with typed callers in front of it, and the outcome assertions |
115
- | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance |
119
+ | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiContract`: the contract as plain types a client compiles instead of the server |
116
120
 
117
121
  Frontends and shared isomorphic packages should import from `lambder/client`
118
122
  only; the entry's module graph contains no AWS SDK, Node built-ins, or server
@@ -156,6 +160,7 @@ guide that matches what you are building. The full index lives in
156
160
  | [The API core](./docs/api-core.md) | `LambderApiPipeline`: the one pipeline the server and the mock runtime run, the store interfaces, the transports |
157
161
  | [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression, transports |
158
162
  | [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
163
+ | [Direct uploads](./docs/uploads.md) | Files the browser posts straight to S3 with tickets the server signs, verified before they count |
159
164
  | [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
160
165
  | [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, on-demand languages, runtime dictionaries |
161
166
  | [The mock runtime](./docs/mock.md) | `LambderMockApp`: the typed contract served from mock handlers over the real pipeline, in the browser and in tests |
@@ -184,7 +189,7 @@ review of 7.3.1: session writes that cannot undo a logout, API calls that must
184
189
  be JSON, output schemas applied at runtime, rate limits that count IPv6 callers
185
190
  by their /64 and custom keys after the guards, idempotency keys bound to the
186
191
  request they were first sent with, and the same behavior on every gateway and
187
- in the mock. Every break and what to do about it is in the 8.0.1 entry. The
192
+ in the mock. Every break and what to do about it is in the 8.0.2 entry. The
188
193
  compiler finds most of them. Fourteen it cannot are named there: hand-built
189
194
  calls without a JSON Content-Type, hand-built answers without `apiVersion`,
190
195
  handlers whose payload does not match their output schema, code that decoded
@@ -0,0 +1,85 @@
1
+ import type ts from "typescript";
2
+ /** How the printed module is punctuated. */
3
+ export type ContractPrintStyle = {
4
+ quote: "'" | "\"";
5
+ semicolon: "" | ";";
6
+ };
7
+ /** The contract as text: each entry by name, and the named types the entries refer to. */
8
+ export type PrintedContract = {
9
+ entries: {
10
+ name: string;
11
+ text: string;
12
+ }[];
13
+ declarations: {
14
+ name: string;
15
+ text: string;
16
+ }[];
17
+ /** Where the contract holds something with no plain form, and what it is. */
18
+ failures: string[];
19
+ };
20
+ export declare class ContractTypePrinter {
21
+ private readonly ts;
22
+ private readonly program;
23
+ private readonly checker;
24
+ private readonly style;
25
+ private readonly failures;
26
+ /** Every named declaration by the type it stands for; its text is null while it is being printed. */
27
+ private readonly declarations;
28
+ /** Names a declaration may not take: the default library's, and the contract's own. */
29
+ private readonly takenNames;
30
+ /** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
31
+ private readonly inProgress;
32
+ /** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
33
+ private readonly exactOptionalProperties;
34
+ constructor(ts: typeof import("typescript"), program: ts.Program, checker: ts.TypeChecker, style: ContractPrintStyle, contractName: string);
35
+ /** Prints each member of the contract type, sorted by name, and every declaration they refer to. */
36
+ printContract(contract: ts.Type): PrintedContract;
37
+ /**
38
+ * `label value`, with a union too long for one line starting on the next
39
+ * line, one member per line: what a property, an index signature and a
40
+ * declaration all print through.
41
+ */
42
+ labeledValue(label: string, value: string): string;
43
+ private print;
44
+ /** A union, an intersection or an object: printed in place, or as a reference to a declaration of its own. */
45
+ private printComposite;
46
+ /** The name a type is declared under, when it is one to print as a declaration: non-generic, and not the default library's. */
47
+ private declaredNameOf;
48
+ /** 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. */
49
+ private nameOf;
50
+ /**
51
+ * A name for a type that refers to itself and has none of its own: what it
52
+ * instantiates followed by its arguments (a JSON mapping of a Tree is
53
+ * `JsonOfTree`, a `Tree<string>` is `TreeString`), or `RecursiveType`.
54
+ */
55
+ private recursiveNameOf;
56
+ private printStructure;
57
+ private printUnion;
58
+ private printUnionOf;
59
+ private printIntersection;
60
+ private printObject;
61
+ private printMembers;
62
+ /**
63
+ * An optional member's type as its source wrote it. Under
64
+ * exactOptionalPropertyTypes the compiler adds its own "missing"
65
+ * undefined, a different type from the `undefined` a source writes;
66
+ * printed as `| undefined` it would let the member be undefined, which
67
+ * the source does not.
68
+ */
69
+ private printOptionalMember;
70
+ private printTuple;
71
+ private printTemplateLiteral;
72
+ /** Why a property cannot be printed as a plain member, or undefined when it can. */
73
+ private unprintableProperty;
74
+ /** An object type printed member by member: not an array, a tuple, a function or a default library interface. */
75
+ private isPlainObject;
76
+ private isCallable;
77
+ private isDefaultLibraryInterface;
78
+ /** Declared in the default library, even where a package augments it (as @types/node does some globals). */
79
+ private isDefaultLibrary;
80
+ private wrapped;
81
+ keyOf(name: string): string;
82
+ private quoted;
83
+ private takeName;
84
+ private fail;
85
+ }
@@ -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,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;