@zmdb/compiler 1.0.0-beta.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 (172) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +40 -0
  3. package/dist/codegen/index.d.ts +50 -0
  4. package/dist/codegen/index.d.ts.map +1 -0
  5. package/dist/codegen/index.js +364 -0
  6. package/dist/codegen/index.js.map +1 -0
  7. package/dist/codegen/scan.d.ts +105 -0
  8. package/dist/codegen/scan.d.ts.map +1 -0
  9. package/dist/codegen/scan.js +450 -0
  10. package/dist/codegen/scan.js.map +1 -0
  11. package/dist/codegen/witness.d.ts +76 -0
  12. package/dist/codegen/witness.d.ts.map +1 -0
  13. package/dist/codegen/witness.js +690 -0
  14. package/dist/codegen/witness.js.map +1 -0
  15. package/dist/config/contract.d.ts +41 -0
  16. package/dist/config/contract.d.ts.map +1 -0
  17. package/dist/config/contract.js +5 -0
  18. package/dist/config/contract.js.map +1 -0
  19. package/dist/config/index.d.ts +53 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +277 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/index.zmdb.generated.d.ts +12 -0
  24. package/dist/config/index.zmdb.generated.js +28 -0
  25. package/dist/config/index.zmdb.witness.d.ts +3 -0
  26. package/dist/config/index.zmdb.witness.d.ts.map +1 -0
  27. package/dist/config/index.zmdb.witness.js +20 -0
  28. package/dist/config/index.zmdb.witness.js.map +1 -0
  29. package/dist/configured-plugin.d.ts +11 -0
  30. package/dist/configured-plugin.d.ts.map +1 -0
  31. package/dist/configured-plugin.js +27 -0
  32. package/dist/configured-plugin.js.map +1 -0
  33. package/dist/emit/index.d.ts +103 -0
  34. package/dist/emit/index.d.ts.map +1 -0
  35. package/dist/emit/index.js +1356 -0
  36. package/dist/emit/index.js.map +1 -0
  37. package/dist/errors.d.ts +7 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +2 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +46 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +254 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lint/ast.d.ts +17 -0
  46. package/dist/lint/ast.d.ts.map +1 -0
  47. package/dist/lint/ast.js +77 -0
  48. package/dist/lint/ast.js.map +1 -0
  49. package/dist/lint/host-types.d.ts +9 -0
  50. package/dist/lint/host-types.d.ts.map +1 -0
  51. package/dist/lint/host-types.js +2 -0
  52. package/dist/lint/host-types.js.map +1 -0
  53. package/dist/lint/index.d.ts +23 -0
  54. package/dist/lint/index.d.ts.map +1 -0
  55. package/dist/lint/index.js +53 -0
  56. package/dist/lint/index.js.map +1 -0
  57. package/dist/lint/rules/no-distributed-nullable-tags.d.ts +3 -0
  58. package/dist/lint/rules/no-distributed-nullable-tags.d.ts.map +1 -0
  59. package/dist/lint/rules/no-distributed-nullable-tags.js +71 -0
  60. package/dist/lint/rules/no-distributed-nullable-tags.js.map +1 -0
  61. package/dist/lint/rules/no-empty-patch.d.ts +3 -0
  62. package/dist/lint/rules/no-empty-patch.d.ts.map +1 -0
  63. package/dist/lint/rules/no-empty-patch.js +26 -0
  64. package/dist/lint/rules/no-empty-patch.js.map +1 -0
  65. package/dist/lint/rules/no-interpolated-sql.d.ts +3 -0
  66. package/dist/lint/rules/no-interpolated-sql.d.ts.map +1 -0
  67. package/dist/lint/rules/no-interpolated-sql.js +35 -0
  68. package/dist/lint/rules/no-interpolated-sql.js.map +1 -0
  69. package/dist/lint/rules/no-unbounded-find.d.ts +3 -0
  70. package/dist/lint/rules/no-unbounded-find.d.ts.map +1 -0
  71. package/dist/lint/rules/no-unbounded-find.js +26 -0
  72. package/dist/lint/rules/no-unbounded-find.js.map +1 -0
  73. package/dist/lint/rules/no-unknown-json-column.d.ts +3 -0
  74. package/dist/lint/rules/no-unknown-json-column.d.ts.map +1 -0
  75. package/dist/lint/rules/no-unknown-json-column.js +35 -0
  76. package/dist/lint/rules/no-unknown-json-column.js.map +1 -0
  77. package/dist/lint/rules/require-sql-on-number.d.ts +3 -0
  78. package/dist/lint/rules/require-sql-on-number.d.ts.map +1 -0
  79. package/dist/lint/rules/require-sql-on-number.js +34 -0
  80. package/dist/lint/rules/require-sql-on-number.js.map +1 -0
  81. package/dist/lint/types.d.ts +12 -0
  82. package/dist/lint/types.d.ts.map +1 -0
  83. package/dist/lint/types.js +2 -0
  84. package/dist/lint/types.js.map +1 -0
  85. package/dist/metro/metro.d.ts +22 -0
  86. package/dist/metro/metro.d.ts.map +1 -0
  87. package/dist/metro/metro.js +242 -0
  88. package/dist/metro/metro.js.map +1 -0
  89. package/dist/protobuf/decode.d.ts +20 -0
  90. package/dist/protobuf/decode.d.ts.map +1 -0
  91. package/dist/protobuf/decode.js +289 -0
  92. package/dist/protobuf/decode.js.map +1 -0
  93. package/dist/protobuf/descriptor.d.ts +16 -0
  94. package/dist/protobuf/descriptor.d.ts.map +1 -0
  95. package/dist/protobuf/descriptor.js +303 -0
  96. package/dist/protobuf/descriptor.js.map +1 -0
  97. package/dist/protobuf/encode.d.ts +20 -0
  98. package/dist/protobuf/encode.d.ts.map +1 -0
  99. package/dist/protobuf/encode.js +203 -0
  100. package/dist/protobuf/encode.js.map +1 -0
  101. package/dist/protobuf/grpc-ir.d.ts +16 -0
  102. package/dist/protobuf/grpc-ir.d.ts.map +1 -0
  103. package/dist/protobuf/grpc-ir.js +2 -0
  104. package/dist/protobuf/grpc-ir.js.map +1 -0
  105. package/dist/protobuf/plan.d.ts +42 -0
  106. package/dist/protobuf/plan.d.ts.map +1 -0
  107. package/dist/protobuf/plan.js +158 -0
  108. package/dist/protobuf/plan.js.map +1 -0
  109. package/dist/reflect/callsites.d.ts +40 -0
  110. package/dist/reflect/callsites.d.ts.map +1 -0
  111. package/dist/reflect/callsites.js +153 -0
  112. package/dist/reflect/callsites.js.map +1 -0
  113. package/dist/reflect/index.d.ts +106 -0
  114. package/dist/reflect/index.d.ts.map +1 -0
  115. package/dist/reflect/index.js +1421 -0
  116. package/dist/reflect/index.js.map +1 -0
  117. package/dist/reflect/session.d.ts +118 -0
  118. package/dist/reflect/session.d.ts.map +1 -0
  119. package/dist/reflect/session.js +228 -0
  120. package/dist/reflect/session.js.map +1 -0
  121. package/dist/testing/index.d.ts +69 -0
  122. package/dist/testing/index.d.ts.map +1 -0
  123. package/dist/testing/index.js +210 -0
  124. package/dist/testing/index.js.map +1 -0
  125. package/dist/transform/index.d.ts +74 -0
  126. package/dist/transform/index.d.ts.map +1 -0
  127. package/dist/transform/index.js +572 -0
  128. package/dist/transform/index.js.map +1 -0
  129. package/dist/unplugin/index.d.ts +53 -0
  130. package/dist/unplugin/index.d.ts.map +1 -0
  131. package/dist/unplugin/index.js +130 -0
  132. package/dist/unplugin/index.js.map +1 -0
  133. package/dist/unplugin/inline-bench.d.ts +29 -0
  134. package/dist/unplugin/inline-bench.d.ts.map +1 -0
  135. package/dist/unplugin/inline-bench.js +82 -0
  136. package/dist/unplugin/inline-bench.js.map +1 -0
  137. package/package.json +110 -0
  138. package/src/codegen/index.ts +451 -0
  139. package/src/codegen/scan.ts +574 -0
  140. package/src/codegen/witness.ts +819 -0
  141. package/src/config/contract.ts +48 -0
  142. package/src/config/index.ts +384 -0
  143. package/src/config/index.zmdb.generated.d.ts +12 -0
  144. package/src/config/index.zmdb.generated.js +28 -0
  145. package/src/config/index.zmdb.witness.ts +22 -0
  146. package/src/configured-plugin.ts +35 -0
  147. package/src/emit/index.ts +1496 -0
  148. package/src/errors.ts +6 -0
  149. package/src/index.ts +336 -0
  150. package/src/lint/ast.ts +96 -0
  151. package/src/lint/host-types.ts +9 -0
  152. package/src/lint/index.ts +80 -0
  153. package/src/lint/rules/no-distributed-nullable-tags.ts +76 -0
  154. package/src/lint/rules/no-empty-patch.ts +26 -0
  155. package/src/lint/rules/no-interpolated-sql.ts +45 -0
  156. package/src/lint/rules/no-unbounded-find.ts +26 -0
  157. package/src/lint/rules/no-unknown-json-column.ts +36 -0
  158. package/src/lint/rules/require-sql-on-number.ts +34 -0
  159. package/src/lint/types.ts +11 -0
  160. package/src/metro/metro.ts +313 -0
  161. package/src/protobuf/decode.ts +375 -0
  162. package/src/protobuf/descriptor.ts +353 -0
  163. package/src/protobuf/encode.ts +277 -0
  164. package/src/protobuf/grpc-ir.ts +17 -0
  165. package/src/protobuf/plan.ts +244 -0
  166. package/src/reflect/callsites.ts +198 -0
  167. package/src/reflect/index.ts +1723 -0
  168. package/src/reflect/session.ts +276 -0
  169. package/src/testing/index.ts +298 -0
  170. package/src/transform/index.ts +690 -0
  171. package/src/unplugin/index.ts +182 -0
  172. package/src/unplugin/inline-bench.ts +92 -0
@@ -0,0 +1,690 @@
1
+ // The three files project compilation writes beside a source file, as text.
2
+ // Generated banners name the compiler package so checked-in artifacts identify their
3
+ // current owner without implying that the retired executable still exists.
4
+ //
5
+ // Nothing here runs the compiler or the emitter. It is the text half of project compilation:
6
+ // given the entries `scan.ts` found, produce the witness module, and — given the transformer's
7
+ // output for that witness — produce the JavaScript and declarations that go with it. Keeping
8
+ // it pure is what makes it testable without a `tsgo` process.
9
+ //
10
+ // ## Why three files
11
+ //
12
+ // The rewrite is destructive. After a run the source says `zmdbIsUser(data)` and the
13
+ // `is<User>(data)` it came from is gone, so the type argument — the only input the whole
14
+ // pipeline has — would be gone with it. The **witness** keeps it, in a form the consumer's
15
+ // own `tsc` checks: a wrapper per entry, written against the runtime API. That makes a
16
+ // renamed or deleted `User` a build error in a generated file, rather than a compiled
17
+ // validator that quietly keeps describing a type nobody declares any more.
18
+ //
19
+ // The witness is also the transform input, and its output cannot be the artifact: the
20
+ // emitted helpers are untyped JavaScript (`function _zmdbFreeze(_v)`), which under
21
+ // `noImplicitAny` is an error per parameter. So the artifact is a **`.js`** — the emitter's
22
+ // output needs no annotations because nothing typechecks it — plus a **`.d.ts`** carrying
23
+ // the signatures. That split pays for itself twice over: there is not one cast anywhere in
24
+ // the generated code, and `schemaOf<T>()`'s phantom (`TaggedSchema<T>`'s `unique symbol`
25
+ // slot, which no object literal can satisfy) is *declared* in the `.d.ts` rather than
26
+ // asserted into existence.
27
+ //
28
+ // ## How the JavaScript is extracted
29
+ //
30
+ // By sentinel comments, and only because this module wrote every line around them. The
31
+ // transformer replaces call spans and prepends its prelude; it never moves a statement and
32
+ // never touches a comment. So the text between `/*zmdb:begin:NAME*/` and
33
+ // `/*zmdb:end:NAME*/` is the wrapper with its call inlined, and it still starts with the
34
+ // exact signature line this module generated — which is what makes swapping that line for
35
+ // its JavaScript form a string operation rather than a parse.
36
+ import { SyntaxKind } from 'typescript/unstable/ast';
37
+ import { isIdentifier, isImportDeclaration, isNamedImports, isNamespaceImport, isPropertyAccessExpression, isStringLiteral, } from 'typescript/unstable/ast/is';
38
+ import { Rewriter } from '../transform/index.js';
39
+ /** Wraps the header and the import block: everything the `.js` does not want. */
40
+ const IMPORTS_OPEN = '/*zmdb:imports*/';
41
+ const IMPORTS_CLOSE = '/*zmdb:/imports*/';
42
+ function begin(name) {
43
+ return `/*zmdb:begin:${name}*/`;
44
+ }
45
+ function end(name) {
46
+ return `/*zmdb:end:${name}*/`;
47
+ }
48
+ /** The type each callee's signature needs beyond the type argument itself. */
49
+ const SUPPORT_TYPES = {
50
+ validate: ['ValidateResult'],
51
+ validateShallow: ['ValidateResult'],
52
+ toJsonSchema: ['JsonSchemaObject'],
53
+ schemaOf: ['TaggedSchema'],
54
+ toolFor: ['ToolOptions', 'ToolProvider', 'ToolSpecFor'],
55
+ loadGrpcService: ['GrpcLoadedService'],
56
+ };
57
+ const TS_EXTENSIONS = [
58
+ // source extension, runtime extension, declaration extension
59
+ ['.mts', '.mjs', '.d.mts'],
60
+ ['.cts', '.cjs', '.d.cts'],
61
+ ['.tsx', '.js', '.d.ts'],
62
+ ['.ts', '.js', '.d.ts'],
63
+ ];
64
+ /**
65
+ * Where the three files go for one source file.
66
+ *
67
+ * Beside the source, one set per source file, which is a deliberate departure from a single
68
+ * per-project module. Two things fall out of it and both are worth more than the tidiness:
69
+ * the witness's relative imports are the source's own relative imports unchanged (no path
70
+ * arithmetic, and none of the ways that gets it wrong), and two files that both write
71
+ * `is<Row>` cannot collide over the export name `zmdbIsRow`.
72
+ *
73
+ * The runtime extension follows the source's, so a `.mts` file gets a `.mjs` — the module
74
+ * kind of the generated file has to match the module kind of the file importing it.
75
+ */
76
+ export function artifactPaths(sourcePath) {
77
+ for (const [source, runtime, declaration] of TS_EXTENSIONS) {
78
+ if (!sourcePath.endsWith(source))
79
+ continue;
80
+ const stem = sourcePath.slice(0, -source.length);
81
+ return {
82
+ witness: `${stem}.zmdb.witness${source === '.tsx' ? '.ts' : source}`,
83
+ js: `${stem}.zmdb.generated${runtime}`,
84
+ dts: `${stem}.zmdb.generated${declaration}`,
85
+ specifier: `./${base(stem)}.zmdb.generated${runtime}`,
86
+ };
87
+ }
88
+ return {
89
+ witness: `${sourcePath}.zmdb.witness.ts`,
90
+ js: `${sourcePath}.zmdb.generated.js`,
91
+ dts: `${sourcePath}.zmdb.generated.d.ts`,
92
+ specifier: `./${base(sourcePath)}.zmdb.generated.js`,
93
+ };
94
+ }
95
+ function base(path) {
96
+ const cut = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
97
+ return cut === -1 ? path : path.slice(cut + 1);
98
+ }
99
+ /** Whether a path is something a previous run wrote. The scan must not recurse into these. */
100
+ export function isGeneratedPath(path) {
101
+ return /\.zmdb\.(?:witness|generated)\.[cm]?[jt]sx?$|\.zmdb\.generated\.d\.[cm]?ts$/.test(path);
102
+ }
103
+ /**
104
+ * The wrapper's shape for one entry.
105
+ *
106
+ * Every callee becomes a *function*, including the two — `toJsonSchema<T>()` and
107
+ * `schemaOf<T>()` — that take no argument and could have been a `const`. Not for efficiency:
108
+ * the emitter hoists both to one deeply frozen module-level literal and the wrapper returns a
109
+ * reference to it, so a `const` would compile to the same bytes. It is so that the generated
110
+ * module has the same shape as the call it replaced. `document()` was a call in the source and
111
+ * stays one, which is what keeps the rewrite a rename rather than a refactor.
112
+ */
113
+ function signature(entry) {
114
+ const type = entry.typeText;
115
+ const typeArguments = entry.depthText === undefined ? type : `${type}, ${entry.depthText}`;
116
+ const captured = entry.argumentsText?.join(', ');
117
+ switch (entry.callee) {
118
+ case 'is':
119
+ case 'equals':
120
+ return {
121
+ parameters: 'value: unknown',
122
+ returns: `value is ${type}`,
123
+ plain: 'value',
124
+ call: `${entry.callee}<${type}>(value)`,
125
+ };
126
+ case 'isShallow':
127
+ return {
128
+ parameters: 'value: unknown',
129
+ returns: `value is ${type}`,
130
+ plain: 'value',
131
+ call: `isShallow<${typeArguments}>(value)`,
132
+ };
133
+ case 'assert':
134
+ case 'assertEquals':
135
+ return { parameters: 'value: unknown', returns: type, plain: 'value', call: `${entry.callee}<${type}>(value)` };
136
+ case 'assertShallow':
137
+ return {
138
+ parameters: 'value: unknown',
139
+ returns: type,
140
+ plain: 'value',
141
+ call: `assertShallow<${typeArguments}>(value)`,
142
+ };
143
+ case 'validate':
144
+ return {
145
+ parameters: 'value: unknown',
146
+ returns: `ValidateResult<${type}>`,
147
+ plain: 'value',
148
+ call: `validate<${type}>(value)`,
149
+ };
150
+ case 'validateShallow':
151
+ return {
152
+ parameters: 'value: unknown',
153
+ returns: `ValidateResult<${type}>`,
154
+ plain: 'value',
155
+ call: `validateShallow<${typeArguments}>(value)`,
156
+ };
157
+ case 'random':
158
+ return { parameters: '', returns: type, plain: '', call: `random<${type}>()` };
159
+ case 'toJsonSchema':
160
+ return { parameters: '', returns: 'JsonSchemaObject', plain: '', call: `toJsonSchema<${type}>()` };
161
+ case 'schemaOf':
162
+ return { parameters: '', returns: `TaggedSchema<${type}>`, plain: '', call: `schemaOf<${type}>()` };
163
+ case 'toolFor':
164
+ return {
165
+ typeParameters: '<P extends ToolProvider>',
166
+ parameters: 'provider: P, name: string, opts?: ToolOptions',
167
+ returns: 'ToolSpecFor[P]',
168
+ plain: 'provider, name, opts',
169
+ call: `toolFor<${type}, P>(provider, name, opts)`,
170
+ };
171
+ case 'grpcDescriptor':
172
+ return {
173
+ parameters: '',
174
+ returns: 'string',
175
+ plain: '',
176
+ call: `grpcDescriptor<${type}>(${captured ?? ''})`,
177
+ };
178
+ case 'loadGrpcService':
179
+ return {
180
+ parameters: '',
181
+ returns: `GrpcLoadedService<${type}>`,
182
+ plain: '',
183
+ call: `loadGrpcService<${type}>(${captured ?? ''})`,
184
+ };
185
+ case 'protoDescriptor':
186
+ return { parameters: '', returns: 'string', plain: '', call: `protoDescriptor<${type}>()` };
187
+ case 'protoDecode':
188
+ return {
189
+ parameters: 'bytes: Uint8Array',
190
+ returns: type,
191
+ plain: 'bytes',
192
+ call: `protoDecode<${type}>(bytes)`,
193
+ };
194
+ case 'protoEncode':
195
+ return {
196
+ parameters: `value: ${type}`,
197
+ returns: 'Uint8Array',
198
+ plain: 'value',
199
+ call: `protoEncode<${type}>(value)`,
200
+ };
201
+ default:
202
+ // `CALLEES` is the only source of `callee`, and every member of it is above. A new one
203
+ // added there and not here would otherwise generate a wrapper that calls nothing.
204
+ throw new Error(`@zmdb/compiler has no wrapper for \`${entry.callee}<T>()\``);
205
+ }
206
+ }
207
+ /** `export function NAME(value: unknown): value is User {` — the line the `.js` swaps out. */
208
+ function tsOpening(entry) {
209
+ const { parameters, returns, typeParameters = '' } = signature(entry);
210
+ return `export function ${entry.name}${typeParameters}(${parameters}): ${returns} {`;
211
+ }
212
+ // -----------------------------------------------------------------------------
213
+ // Imports
214
+ // -----------------------------------------------------------------------------
215
+ function typeImportLines(imports, style) {
216
+ const named = new Map();
217
+ const lines = [];
218
+ for (const entry of imports) {
219
+ if (entry.kind === 'default') {
220
+ lines.push(`import type ${entry.local} from ${quote(entry.specifier, style)};`);
221
+ }
222
+ else if (entry.kind === 'namespace') {
223
+ lines.push(`import type * as ${entry.local} from ${quote(entry.specifier, style)};`);
224
+ }
225
+ else {
226
+ const clause = entry.original === entry.local ? entry.local : `${entry.original} as ${entry.local}`;
227
+ const group = named.get(entry.specifier);
228
+ if (group)
229
+ group.push(clause);
230
+ else
231
+ named.set(entry.specifier, [clause]);
232
+ }
233
+ }
234
+ for (const [specifier, clauses] of named) {
235
+ lines.push(`import type { ${clauses.toSorted().join(', ')} } from ${quote(specifier, style)};`);
236
+ }
237
+ return lines.toSorted();
238
+ }
239
+ /**
240
+ * The runtime API the witness calls, and the types its signatures mention.
241
+ *
242
+ * Values come from the specifier the *source* used, never from an implementation package: a
243
+ * project that installed only `zmdb` has no direct `@zmdb/validator` dependency. Support
244
+ * types normally share that entry. The one deliberate split is root `schemaOf`: the curated
245
+ * root keeps the callable, while `TaggedSchema` belongs to the complete `zmdb/schema` concern.
246
+ */
247
+ function calleeImportLines(entries, sources, style) {
248
+ const values = new Map();
249
+ const types = new Map();
250
+ const into = (map, specifier, name) => {
251
+ const group = map.get(specifier);
252
+ if (group)
253
+ group.add(name);
254
+ else
255
+ map.set(specifier, new Set([name]));
256
+ };
257
+ for (const entry of entries) {
258
+ const specifier = sources.get(entry.callee) ?? '@zmdb/validator';
259
+ into(values, specifier, entry.callee);
260
+ const support = SUPPORT_TYPES[entry.callee];
261
+ if (support) {
262
+ const supportSpecifier = entry.callee === 'schemaOf' && specifier === 'zmdb' ? 'zmdb/schema' : specifier;
263
+ for (const name of support)
264
+ into(types, supportSpecifier, name);
265
+ }
266
+ }
267
+ const lines = [];
268
+ for (const [specifier, names] of values) {
269
+ lines.push(`import { ${[...names].toSorted().join(', ')} } from ${quote(specifier, style)};`);
270
+ }
271
+ for (const [specifier, names] of types) {
272
+ lines.push(`import type { ${[...names].toSorted().join(', ')} } from ${quote(specifier, style)};`);
273
+ }
274
+ return lines.toSorted();
275
+ }
276
+ /** The support-type imports on their own, for the `.d.ts`, which calls nothing. */
277
+ function supportTypeLines(entries, sources, style) {
278
+ return calleeImportLines(entries, sources, style).filter(line => line.startsWith('import type '));
279
+ }
280
+ /**
281
+ * A module specifier, quoted the way the file it is going into quotes them.
282
+ *
283
+ * Cosmetic in isolation, load-bearing in aggregate: a consumer runs a formatter over its
284
+ * tree, and a generated file that disagrees with it is a diff on every `--check`.
285
+ */
286
+ function quote(specifier, style) {
287
+ if (style !== "'")
288
+ return JSON.stringify(specifier);
289
+ return `'${specifier.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
290
+ }
291
+ /**
292
+ * Which quote the source file uses for its own imports. Single unless it says otherwise —
293
+ * the majority style, and the one a file with no imports at all is most likely to want.
294
+ */
295
+ export function quoteStyle(sourceFile) {
296
+ for (const node of sourceFile.statements) {
297
+ if (!isImportDeclaration(node))
298
+ continue;
299
+ const specifier = node.moduleSpecifier;
300
+ if (!isStringLiteral(specifier))
301
+ continue;
302
+ const char = sourceFile.text[specifier.getStart()];
303
+ if (char === '"' || char === "'")
304
+ return char;
305
+ }
306
+ return "'";
307
+ }
308
+ export function witnessSource(input) {
309
+ const { sourceName, entries, typeImports, calleeSources, style } = input;
310
+ const parts = [
311
+ IMPORTS_OPEN,
312
+ `// Generated by @zmdb/compiler from ${sourceName}. Checked, not edited.`,
313
+ '//',
314
+ `// One wrapper per validation call ${sourceName} used to make, written against the runtime`,
315
+ '// API so that the compiler still sees each type argument. That is the point of the file: it',
316
+ '// is what the project compiler reads on the next run to regenerate the compiled module, and it is',
317
+ '// what turns a renamed or deleted type into a build error here instead of a validator that',
318
+ '// silently checks a shape nobody declares.',
319
+ '//',
320
+ '// Editing it by hand does nothing. An entry disappears when the source stops referring to',
321
+ "// its export name, so deleting a call in the source is how you delete a validator; there's",
322
+ '// no bookkeeping to keep in step.',
323
+ ];
324
+ const imports = [...typeImportLines(typeImports, style), ...calleeImportLines(entries, calleeSources, style)];
325
+ if (imports.length > 0)
326
+ parts.push('', ...imports);
327
+ parts.push(IMPORTS_CLOSE);
328
+ for (const entry of entries) {
329
+ const { call } = signature(entry);
330
+ parts.push('', begin(entry.name), tsOpening(entry), ` return ${call};`, '}', end(entry.name));
331
+ }
332
+ return `${parts.join('\n')}\n`;
333
+ }
334
+ /**
335
+ * The `.js` and the `.d.ts`, assembled from the transformed witness.
336
+ *
337
+ * Only the wrapper bodies come from the transform; every other line is regenerated here from
338
+ * the same `Entry` list the witness was built from. So the two files cannot disagree about a
339
+ * signature, and neither can drift from the witness.
340
+ */
341
+ export function generatedModules(input) {
342
+ const { sourceName, entries, transformed } = input;
343
+ const header = (tail) => [
344
+ `// Generated by @zmdb/compiler from ${sourceName}. Do not edit.`,
345
+ '//',
346
+ `// The validators, compiled. Every check below was emitted from a type in ${sourceName}'s`,
347
+ `// witness module by \`@zmdb/compiler\`, so there is no schema to walk at runtime and`,
348
+ '// nothing to look up: the shape is the control flow.',
349
+ '//',
350
+ ...tail,
351
+ ].join('\n');
352
+ const jsHeader = header([
353
+ '// JavaScript rather than TypeScript on purpose — the emitted helpers are untyped, and a',
354
+ '// generated file that needed `any` to typecheck would be a worse trade than one that is',
355
+ '// simply not typechecked. The declarations live in the sidecar `.d.ts`.',
356
+ '//',
357
+ '// Formatters and linters should skip it. The emitter writes a check as one long expression',
358
+ '// because that is what it is; reformatting would leave the next compiler check',
359
+ '// permanently out of date against a file nothing was wrong with.',
360
+ ]);
361
+ const dtsHeader = header([
362
+ '// This half is the types: the signatures the compiler reads when the source imports the',
363
+ '// module next door. Nothing here is checked against the implementation, and nothing needs',
364
+ '// to be — the witness makes the same claims against the runtime API, and *that* is checked.',
365
+ ]);
366
+ const preludeEnd = firstMarker(transformed, entries);
367
+ let prelude = transformed.slice(0, preludeEnd);
368
+ const open = prelude.indexOf(IMPORTS_OPEN);
369
+ const close = prelude.indexOf(IMPORTS_CLOSE);
370
+ if (open !== -1 && close !== -1) {
371
+ // The header and the witness's own imports. Both are TypeScript-only by now: every call
372
+ // that needed the runtime API has been inlined, so the import would load a validator
373
+ // nobody calls, and the type imports are not syntax a `.js` file may contain.
374
+ prelude = prelude.slice(0, open) + prelude.slice(close + IMPORTS_CLOSE.length);
375
+ }
376
+ const bodies = entries.map(entry => javascriptFor(entry, transformed));
377
+ const declarations = entries.map(entry => {
378
+ const { parameters, returns, typeParameters = '' } = signature(entry);
379
+ return `export declare function ${entry.name}${typeParameters}(${parameters}): ${returns};`;
380
+ });
381
+ const dtsImports = [
382
+ ...typeImportLines(input.typeImports, input.style),
383
+ ...supportTypeLines(entries, input.calleeSources, input.style),
384
+ ];
385
+ return {
386
+ js: `${jsHeader}\n${join([prelude.trim(), ...bodies])}\n`,
387
+ dts: `${dtsHeader}\n${join([dtsImports.join('\n'), ...declarations])}\n`,
388
+ };
389
+ }
390
+ function join(blocks) {
391
+ return blocks.filter(block => block.length > 0).join('\n\n');
392
+ }
393
+ /** Where the wrappers start, which is where the prelude ends. */
394
+ function firstMarker(transformed, entries) {
395
+ let earliest = transformed.length;
396
+ for (const entry of entries) {
397
+ const at = transformed.indexOf(begin(entry.name));
398
+ if (at !== -1 && at < earliest)
399
+ earliest = at;
400
+ }
401
+ return earliest;
402
+ }
403
+ /**
404
+ * One wrapper, as JavaScript.
405
+ *
406
+ * The sentinel comments bound it and the signature line is known verbatim, so this is two
407
+ * string operations. A missing marker means the transformer moved or dropped a statement,
408
+ * which it has no path to do — so it throws rather than writing a module with a hole in it.
409
+ */
410
+ function javascriptFor(entry, transformed) {
411
+ const from = transformed.indexOf(begin(entry.name));
412
+ const to = transformed.indexOf(end(entry.name));
413
+ if (from === -1 || to === -1 || to < from) {
414
+ throw new Error(`the transformed witness lost the wrapper for ${entry.name}`);
415
+ }
416
+ const chunk = transformed.slice(from + begin(entry.name).length, to).trim();
417
+ const opening = tsOpening(entry);
418
+ if (!chunk.startsWith(opening)) {
419
+ throw new Error(`the transformed witness rewrote the signature of ${entry.name}`);
420
+ }
421
+ const { plain } = signature(entry);
422
+ return `export function ${entry.name}(${plain}) {${chunk.slice(opening.length)}`;
423
+ }
424
+ /**
425
+ * `is<User>(data)` → `zmdbIsUser(data)`, plus the import that makes it resolve.
426
+ *
427
+ * The whole call is replaced rather than just its callee, because the type argument has to
428
+ * go with it and `is<User>` is one span in the middle of another. Arguments are read through
429
+ * the `Rewriter`, so `assert<A>(is<B>(x))` picks up the inner rewrite instead of carrying a
430
+ * stale copy of it.
431
+ */
432
+ export function rewriteSource(input) {
433
+ const { sourceFile, code, sites, specifier, entries, calleeSources, style } = input;
434
+ const rewriter = new Rewriter(code);
435
+ // Back to front, for the same reason the transformer does it: an earlier offset is only
436
+ // valid while everything after it is untouched.
437
+ for (const { site, entry } of sites.toReversed()) {
438
+ const args = site.node.arguments;
439
+ const first = args[0];
440
+ const last = args[args.length - 1];
441
+ const inner = entry.argumentsText === undefined && first && last ? rewriter.slice(first.getStart(), last.end) : '';
442
+ rewriter.replace(site.node.getStart(), site.node.end, `${entry.name}(${inner})`);
443
+ }
444
+ const names = entries.map(entry => entry.name);
445
+ const statement = names.length === 0 ? '' : `import { ${names.join(', ')} } from ${quote(specifier, style)};`;
446
+ // The import edits come after the call edits and are all at lower offsets, so no applied
447
+ // edit lies inside one of their spans and the `Rewriter`'s arithmetic is unaffected.
448
+ const owned = new Set(calleeSources.values());
449
+ const compiled = new Set(sites.map(({ site }) => site.binding));
450
+ const context = {
451
+ specifier,
452
+ statement,
453
+ names,
454
+ owned,
455
+ compiled,
456
+ style,
457
+ live: liveIdentifiers(sourceFile, sites),
458
+ };
459
+ for (const edit of importEdits(sourceFile, context).toSorted((a, b) => b.start - a.start)) {
460
+ rewriter.replace(edit.start, edit.end, edit.text);
461
+ }
462
+ return rewriter.text;
463
+ }
464
+ /**
465
+ * Every name the file still uses after the rewrite, as names rather than as text.
466
+ *
467
+ * This decides whether a callee's import is now dead, and it has to be an AST walk. Searching
468
+ * the text for `is` finds it in `import { is } from 'zmdb'`, which is the statement being
469
+ * judged, and finds it in a comment — this file's own fixture said "the type argument *is* the
470
+ * input" and kept a compiled-away import alive on the strength of it. A regex has no way to
471
+ * know which of those is a reference and the parser already does.
472
+ *
473
+ * Three things are therefore skipped: import declarations, because an import is not a use; the
474
+ * head of each compiled call, because `is<User>` is what is going away, while its *arguments*
475
+ * are walked, so an `assert` passed as a value inside one keeps its import; and the property
476
+ * half of `a.b`, because `payload.is` names a property, not the imported function.
477
+ */
478
+ function liveIdentifiers(sourceFile, sites) {
479
+ const compiled = new Map();
480
+ for (const { site } of sites)
481
+ compiled.set(site.node, site.node.arguments);
482
+ const names = new Set();
483
+ const walk = (node) => {
484
+ if (isImportDeclaration(node))
485
+ return;
486
+ const args = compiled.get(node);
487
+ if (args) {
488
+ for (const argument of args)
489
+ walk(argument);
490
+ return;
491
+ }
492
+ if (isIdentifier(node)) {
493
+ names.add(node.text);
494
+ return;
495
+ }
496
+ if (isPropertyAccessExpression(node)) {
497
+ walk(node.expression);
498
+ return;
499
+ }
500
+ node.forEachChild(walk);
501
+ };
502
+ sourceFile.forEachChild(walk);
503
+ return names;
504
+ }
505
+ /**
506
+ * How far a deleted statement's deletion should reach: through the end of its line, and
507
+ * through the blank lines after it when they are no longer separating anything.
508
+ *
509
+ * The line itself is not optional — leaving the newline behind puts a blank line where the
510
+ * statement was, and no later run would remove it, so the file would be permanently one line
511
+ * emptier than the formatter wants it. The blank lines after are the same problem one level
512
+ * up: deleting the only import above a blank line leaves the file starting with that blank
513
+ * line, so a paragraph break that now divides nothing goes with the paragraph.
514
+ */
515
+ function throughLine(text, start, stop) {
516
+ let at = skipSpaces(text, stop);
517
+ if (at >= text.length || text[at] !== '\n')
518
+ return stop;
519
+ at += 1;
520
+ if (!blankAbove(text, start))
521
+ return at;
522
+ while (at < text.length && text[skipSpaces(text, at)] === '\n')
523
+ at = skipSpaces(text, at) + 1;
524
+ return at;
525
+ }
526
+ function skipSpaces(text, from) {
527
+ let at = from;
528
+ while (at < text.length && (text[at] === ' ' || text[at] === '\t' || text[at] === '\r'))
529
+ at += 1;
530
+ return at;
531
+ }
532
+ /** Whether `start`'s line is the first in the file or has a blank line above it. */
533
+ function blankAbove(text, start) {
534
+ let at = start - 1;
535
+ while (at >= 0 && (text[at] === ' ' || text[at] === '\t' || text[at] === '\r'))
536
+ at -= 1;
537
+ if (at < 0)
538
+ return true;
539
+ if (text[at] !== '\n')
540
+ return false;
541
+ at -= 1;
542
+ while (at >= 0 && (text[at] === ' ' || text[at] === '\t' || text[at] === '\r'))
543
+ at -= 1;
544
+ return at < 0 || text[at] === '\n';
545
+ }
546
+ /** The local names a `{ … }` import clause binds. Aliases are not something codegen writes. */
547
+ function importedNames(bindings) {
548
+ if (!bindings || !isNamedImports(bindings))
549
+ return [];
550
+ return bindings.elements.filter(element => isIdentifier(element.name)).map(element => element.name.text);
551
+ }
552
+ /** Whether an import already binds exactly `wanted`, in any order. */
553
+ function satisfies(found, wanted) {
554
+ return found.length === wanted.length && wanted.every(name => found.includes(name));
555
+ }
556
+ /**
557
+ * Add the generated import, and take away the ones the rewrite just orphaned.
558
+ *
559
+ * The second half is not tidiness: `import { is } from 'zmdb'` with no `is` left in the file
560
+ * is an error under `noUnusedLocals`, so a codegen that left it behind would break the build
561
+ * it was supposed to speed up. Only bindings this rewrite could have orphaned are considered
562
+ * — the callee names, and a namespace or default binding of a module a callee came
563
+ * from — and only when the name is referenced nowhere in what is left.
564
+ */
565
+ function importEdits(sourceFile, context) {
566
+ const { specifier, statement, names, owned, compiled, style, live } = context;
567
+ const edits = [];
568
+ let existing;
569
+ // Where the generated import can go, and where it cannot. The new statement goes after the
570
+ // last import that is *still there* — anchoring it to one this pass is deleting would put an
571
+ // insertion inside a deleted span, and the deletion would eat it.
572
+ const surviving = [];
573
+ const removals = [];
574
+ for (const node of sourceFile.statements) {
575
+ if (!isImportDeclaration(node))
576
+ continue;
577
+ surviving.push(node.end);
578
+ /** Delete the statement whole: it exists only to import something now compiled away. */
579
+ const remove = () => {
580
+ surviving.pop();
581
+ const through = throughLine(sourceFile.text, node.getStart(), node.end);
582
+ removals.push({ at: edits.length, start: node.getStart(), nodeEnd: node.end, end: through });
583
+ edits.push({ start: node.getStart(), end: through, text: '' });
584
+ };
585
+ const moduleSpecifier = node.moduleSpecifier;
586
+ if (!isStringLiteral(moduleSpecifier))
587
+ continue;
588
+ if (moduleSpecifier.text === specifier) {
589
+ // A previous run's import. Its *names* are what get compared, never its text — see
590
+ // `satisfies` below.
591
+ existing = {
592
+ start: node.getStart(),
593
+ end: node.end,
594
+ withLine: throughLine(sourceFile.text, node.getStart(), node.end),
595
+ names: importedNames(node.importClause?.namedBindings),
596
+ };
597
+ continue;
598
+ }
599
+ const clause = node.importClause;
600
+ if (!clause)
601
+ continue;
602
+ const dead = (name) => !live.has(name);
603
+ const bindings = clause.namedBindings;
604
+ const namespaceName = bindings && isNamespaceImport(bindings) && isIdentifier(bindings.name) ? bindings.name.text : undefined;
605
+ const defaultName = clause.name && isIdentifier(clause.name) ? clause.name.text : undefined;
606
+ if (owned.has(moduleSpecifier.text)) {
607
+ const single = namespaceName ?? defaultName;
608
+ if (single !== undefined && (!bindings || !isNamedImports(bindings)) && dead(single)) {
609
+ remove();
610
+ continue;
611
+ }
612
+ }
613
+ if (!bindings || !isNamedImports(bindings))
614
+ continue;
615
+ const keep = [];
616
+ let dropped = false;
617
+ for (const element of bindings.elements) {
618
+ if (!isIdentifier(element.name))
619
+ continue;
620
+ const local = element.name.text;
621
+ if (compiled.has(local) && dead(local)) {
622
+ dropped = true;
623
+ continue;
624
+ }
625
+ const original = element.propertyName && isIdentifier(element.propertyName) ? element.propertyName.text : local;
626
+ // `import { type A, B }` — the per-specifier modifier has to survive the rebuild, or
627
+ // a type-only binding becomes a value import of something that does not exist at runtime.
628
+ const modifier = element.isTypeOnly ? 'type ' : '';
629
+ keep.push(`${modifier}${original === local ? local : `${original} as ${local}`}`);
630
+ }
631
+ if (!dropped)
632
+ continue;
633
+ if (keep.length === 0 && defaultName === undefined) {
634
+ remove();
635
+ }
636
+ else {
637
+ const prefix = defaultName === undefined ? '' : `${defaultName}, `;
638
+ const kind = clause.phaseModifier === SyntaxKind.TypeKeyword ? 'import type' : 'import';
639
+ edits.push({
640
+ start: node.getStart(),
641
+ end: node.end,
642
+ text: keep.length === 0
643
+ ? `${kind} ${defaultName ?? ''} from ${quote(moduleSpecifier.text, style)};`
644
+ : `${kind} ${prefix}{ ${keep.join(', ')} } from ${quote(moduleSpecifier.text, style)};`,
645
+ });
646
+ }
647
+ }
648
+ if (statement.length === 0) {
649
+ // Nothing left to import. An existing import from a previous run is now dead, and
650
+ // leaving it would keep a generated module alive that nothing references.
651
+ if (existing)
652
+ edits.push({ start: existing.start, end: existing.withLine, text: '' });
653
+ return edits;
654
+ }
655
+ if (existing) {
656
+ // Only when it says something different. The one thing this statement has to get right is
657
+ // which names come from the generated module, and a consumer's formatter owns everything
658
+ // else about it: `printWidth` wraps a seven-name import across eight lines, `sortImports`
659
+ // reorders the names, and neither is a reason to rewrite the file. Comparing text here
660
+ // would put compiler check mode and `fmt --check` in a loop that neither can win.
661
+ if (!satisfies(existing.names, names))
662
+ edits.push({ start: existing.start, end: existing.end, text: statement });
663
+ return edits;
664
+ }
665
+ const anchor = surviving.at(-1);
666
+ if (anchor !== undefined) {
667
+ edits.push({ start: anchor, end: anchor, text: `\n${statement}` });
668
+ return edits;
669
+ }
670
+ const first = removals.toSorted((a, b) => a.start - b.start)[0];
671
+ if (first) {
672
+ // Every import the file had was orphaned by the rewrite — the common case for a module whose
673
+ // only import was the validator. So the generated one takes the first one's place *as* that
674
+ // deletion: an insertion at the same offset would be two edits claiming one byte, with no
675
+ // rule saying which of them won. The trailing whitespace the deletion swallowed comes back
676
+ // with it, so the file keeps the paragraph break it had.
677
+ edits[first.at] = {
678
+ start: first.start,
679
+ end: first.end,
680
+ text: `${statement}${sourceFile.text.slice(first.nodeEnd, first.end)}`,
681
+ };
682
+ return edits;
683
+ }
684
+ // No imports at all. After the leading comment of the first statement rather than at
685
+ // offset zero, so a file's licence header stays at the top of it.
686
+ const at = sourceFile.statements[0]?.getStart() ?? 0;
687
+ edits.push({ start: at, end: at, text: `${statement}\n\n` });
688
+ return edits;
689
+ }
690
+ //# sourceMappingURL=witness.js.map