@tscomfy/codegen 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +25 -2
- package/dist/doc.d.ts +78 -0
- package/dist/doc.d.ts.map +1 -0
- package/dist/doc.js +335 -0
- package/dist/doc.js.map +1 -0
- package/dist/emit-schema-module.d.ts +11 -0
- package/dist/emit-schema-module.d.ts.map +1 -0
- package/dist/emit-schema-module.js +39 -0
- package/dist/emit-schema-module.js.map +1 -0
- package/dist/emit-types.d.ts +15 -0
- package/dist/emit-types.d.ts.map +1 -0
- package/dist/emit-types.js +718 -0
- package/dist/emit-types.js.map +1 -0
- package/dist/export-name.d.ts +16 -0
- package/dist/export-name.d.ts.map +1 -0
- package/dist/export-name.js +43 -0
- package/dist/export-name.js.map +1 -0
- package/dist/expressions.d.ts +51 -0
- package/dist/expressions.d.ts.map +1 -0
- package/dist/expressions.js +147 -0
- package/dist/expressions.js.map +1 -0
- package/dist/generate.d.ts +35 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +30 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/names.d.ts +36 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +81 -0
- package/dist/names.js.map +1 -0
- package/dist/rules.d.ts +23 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +82 -0
- package/dist/rules.js.map +1 -0
- package/dist/transpile.d.ts +40 -0
- package/dist/transpile.d.ts.map +1 -0
- package/dist/transpile.js +531 -0
- package/dist/transpile.js.map +1 -0
- package/dist/typing.d.ts +18 -0
- package/dist/typing.d.ts.map +1 -0
- package/dist/typing.js +387 -0
- package/dist/typing.js.map +1 -0
- package/dist/values.d.ts +15 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +22 -0
- package/dist/values.js.map +1 -0
- package/package.json +46 -4
- package/src/doc.ts +390 -0
- package/src/emit-schema-module.ts +47 -0
- package/src/emit-types.ts +880 -0
- package/src/export-name.ts +51 -0
- package/src/expressions.ts +201 -0
- package/src/generate.ts +59 -0
- package/src/index.ts +22 -0
- package/src/names.ts +89 -0
- package/src/rules.ts +86 -0
- package/src/transpile.ts +639 -0
- package/src/typing.ts +456 -0
- package/src/values.ts +28 -0
package/src/doc.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// A small document printer in the style of Wadler's "prettier printer", with Prettier's
|
|
2
|
+
// group/indent/line/ifBreak/indentIfBreak semantics. Generated code is built as documents so its
|
|
3
|
+
// line breaks match what oxfmt would produce for the constructs codegen emits (tested by
|
|
4
|
+
// formatting the generated fixtures and the corpus output).
|
|
5
|
+
|
|
6
|
+
/** A printable document. Arrays concatenate. */
|
|
7
|
+
export type Doc = string | readonly Doc[] | Group | Indent | Line | IfBreak | IndentIfBreak | Fill;
|
|
8
|
+
|
|
9
|
+
/** Names a group so other documents can follow its decision. */
|
|
10
|
+
export interface GroupId {
|
|
11
|
+
readonly description: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Group {
|
|
15
|
+
readonly kind: "group";
|
|
16
|
+
readonly contents: Doc;
|
|
17
|
+
readonly id: GroupId | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Indent {
|
|
21
|
+
readonly kind: "indent";
|
|
22
|
+
readonly contents: Doc;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Line {
|
|
26
|
+
readonly kind: "line";
|
|
27
|
+
/** `line`: a space when flat; `soft`: nothing when flat; `hard`: always a newline. */
|
|
28
|
+
readonly mode: "line" | "soft" | "hard";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IfBreak {
|
|
32
|
+
readonly kind: "ifBreak";
|
|
33
|
+
readonly broken: Doc;
|
|
34
|
+
readonly flat: Doc;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface IndentIfBreak {
|
|
38
|
+
readonly kind: "indentIfBreak";
|
|
39
|
+
readonly contents: Doc;
|
|
40
|
+
readonly group: GroupId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Fill {
|
|
44
|
+
readonly kind: "fill";
|
|
45
|
+
readonly contents: readonly string[];
|
|
46
|
+
/** Printed after the last item when broken, and counted in its fit. */
|
|
47
|
+
readonly trailing: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prints on one line if the whole group fits, else breaks every line directly inside it. With
|
|
52
|
+
* an `id`, {@link indentIfBreak} can follow the decision.
|
|
53
|
+
*/
|
|
54
|
+
export function group(contents: Doc, id?: GroupId): Doc {
|
|
55
|
+
return { kind: "group", contents, id };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A fresh group id. */
|
|
59
|
+
export function groupId(description: string): GroupId {
|
|
60
|
+
return { description };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Prettier's `fill` over text separated by `line`s: as many items on each line as fit. An item
|
|
65
|
+
* goes on the current line when it fits there together with the item before it.
|
|
66
|
+
*/
|
|
67
|
+
export function fill(contents: readonly string[], trailing = ""): Doc {
|
|
68
|
+
return { kind: "fill", contents, trailing };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Indents the lines inside by one level. */
|
|
72
|
+
export function indent(contents: Doc): Doc {
|
|
73
|
+
return { kind: "indent", contents };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Indents `contents` only if the group `id` broke (Prettier's assignment layout). */
|
|
77
|
+
export function indentIfBreak(contents: Doc, id: GroupId): Doc {
|
|
78
|
+
return { kind: "indentIfBreak", contents, group: id };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A space, or a newline when its group breaks. */
|
|
82
|
+
export const line: Doc = { kind: "line", mode: "line" };
|
|
83
|
+
/** Nothing, or a newline when its group breaks. */
|
|
84
|
+
export const softline: Doc = { kind: "line", mode: "soft" };
|
|
85
|
+
/** Always a newline; its groups break. */
|
|
86
|
+
export const hardline: Doc = { kind: "line", mode: "hard" };
|
|
87
|
+
|
|
88
|
+
/** `broken` when the enclosing group breaks, else `flat`. */
|
|
89
|
+
export function ifBreak(broken: Doc, flat: Doc = ""): Doc {
|
|
90
|
+
return { kind: "ifBreak", broken, flat };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `docs` with `separator` between them. */
|
|
94
|
+
export function join(separator: Doc, docs: readonly Doc[]): Doc {
|
|
95
|
+
return docs.flatMap((doc, index) => (index === 0 ? [doc] : [separator, doc]));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `left = right` as Prettier lays out assignments: `right` stays on the line while its head
|
|
100
|
+
* fits, else it moves to the next line, indented.
|
|
101
|
+
*/
|
|
102
|
+
export function assignment(left: Doc, right: Doc): Doc {
|
|
103
|
+
const id = groupId("assignment");
|
|
104
|
+
return group([left, " =", group(indent(line), id), indentIfBreak(right, id)]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type Mode = "flat" | "break";
|
|
108
|
+
|
|
109
|
+
interface Command {
|
|
110
|
+
readonly indent: number;
|
|
111
|
+
readonly mode: Mode;
|
|
112
|
+
readonly doc: Doc;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Columns the generated code is laid out for: oxfmt's default, and this repo's setting. */
|
|
116
|
+
export const PRINT_WIDTH = 100;
|
|
117
|
+
|
|
118
|
+
/** Prints a document at `width` columns with two-space indentation. */
|
|
119
|
+
export function print(doc: Doc, width: number): string {
|
|
120
|
+
// Chunks, not one growing string: trimming trailing spaces must stay O(1) per line.
|
|
121
|
+
const output: string[] = [];
|
|
122
|
+
const modes = new Map<GroupId, Mode>();
|
|
123
|
+
let column = 0;
|
|
124
|
+
const stack: Command[] = [{ indent: 0, mode: "break", doc }];
|
|
125
|
+
for (let command = stack.pop(); command !== undefined; command = stack.pop()) {
|
|
126
|
+
const { indent: level, mode, doc: current } = command;
|
|
127
|
+
if (typeof current === "string") {
|
|
128
|
+
output.push(current);
|
|
129
|
+
column += textWidth(current);
|
|
130
|
+
} else if (isDocArray(current)) {
|
|
131
|
+
for (let index = current.length - 1; index >= 0; index--) {
|
|
132
|
+
const part = current[index];
|
|
133
|
+
if (part !== undefined) stack.push({ indent: level, mode, doc: part });
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
switch (current.kind) {
|
|
137
|
+
case "group": {
|
|
138
|
+
const flat =
|
|
139
|
+
mode === "flat" ||
|
|
140
|
+
(!containsHardline(current) &&
|
|
141
|
+
fits({ indent: level, mode: "flat", doc: current.contents }, stack, width - column));
|
|
142
|
+
const chosen = flat ? "flat" : "break";
|
|
143
|
+
if (current.id !== undefined) modes.set(current.id, chosen);
|
|
144
|
+
stack.push({ indent: level, mode: chosen, doc: current.contents });
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "indent":
|
|
148
|
+
stack.push({ indent: level + 1, mode, doc: current.contents });
|
|
149
|
+
break;
|
|
150
|
+
case "indentIfBreak":
|
|
151
|
+
stack.push({
|
|
152
|
+
indent: modes.get(current.group) === "break" ? level + 1 : level,
|
|
153
|
+
mode,
|
|
154
|
+
doc: current.contents,
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
case "ifBreak":
|
|
158
|
+
stack.push({
|
|
159
|
+
indent: level,
|
|
160
|
+
mode,
|
|
161
|
+
doc: mode === "break" ? current.broken : current.flat,
|
|
162
|
+
});
|
|
163
|
+
break;
|
|
164
|
+
case "fill": {
|
|
165
|
+
const broken = mode === "break";
|
|
166
|
+
const items = current.contents.map((item, index) =>
|
|
167
|
+
broken && index === current.contents.length - 1 ? `${item}${current.trailing}` : item,
|
|
168
|
+
);
|
|
169
|
+
for (const [index, item] of items.entries()) {
|
|
170
|
+
const next = items[index + 1];
|
|
171
|
+
const start = column;
|
|
172
|
+
output.push(item);
|
|
173
|
+
column += textWidth(item);
|
|
174
|
+
if (next === undefined) break;
|
|
175
|
+
if (!broken || start + textWidth(item) + 1 + textWidth(next) <= width) {
|
|
176
|
+
output.push(" ");
|
|
177
|
+
column += 1;
|
|
178
|
+
} else {
|
|
179
|
+
output.push("\n", " ".repeat(level));
|
|
180
|
+
column = level * 2;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "line":
|
|
186
|
+
if (mode === "flat" && current.mode !== "hard") {
|
|
187
|
+
if (current.mode === "line") {
|
|
188
|
+
output.push(" ");
|
|
189
|
+
column += 1;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
trimTrailingSpaces(output);
|
|
193
|
+
output.push("\n", " ".repeat(level));
|
|
194
|
+
column = level * 2;
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return output.join("");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Columns a string takes in a terminal, as oxfmt measures them: East Asian wide and fullwidth
|
|
205
|
+
* characters and emoji take two, combining marks and format characters none.
|
|
206
|
+
*/
|
|
207
|
+
export function textWidth(text: string): number {
|
|
208
|
+
// Pure ASCII is the overwhelmingly common case.
|
|
209
|
+
if (/^[\x20-\x7e]*$/.test(text)) return text.length;
|
|
210
|
+
let width = 0;
|
|
211
|
+
for (const char of text) {
|
|
212
|
+
const code = char.codePointAt(0) ?? 0;
|
|
213
|
+
if (ZERO_WIDTH.test(char)) continue;
|
|
214
|
+
width += isWide(code) ? 2 : 1;
|
|
215
|
+
}
|
|
216
|
+
return width;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
|
|
220
|
+
|
|
221
|
+
/** East Asian Wide and Fullwidth ranges (Unicode 15), and emoji presentation blocks. */
|
|
222
|
+
const WIDE_RANGES: readonly (readonly [number, number])[] = [
|
|
223
|
+
[0x1100, 0x115f],
|
|
224
|
+
[0x231a, 0x231b],
|
|
225
|
+
[0x2329, 0x232a],
|
|
226
|
+
[0x23e9, 0x23ec],
|
|
227
|
+
[0x23f0, 0x23f0],
|
|
228
|
+
[0x23f3, 0x23f3],
|
|
229
|
+
[0x25fd, 0x25fe],
|
|
230
|
+
[0x2614, 0x2615],
|
|
231
|
+
[0x2648, 0x2653],
|
|
232
|
+
[0x267f, 0x267f],
|
|
233
|
+
[0x2693, 0x2693],
|
|
234
|
+
[0x26a1, 0x26a1],
|
|
235
|
+
[0x26aa, 0x26ab],
|
|
236
|
+
[0x26bd, 0x26be],
|
|
237
|
+
[0x26c4, 0x26c5],
|
|
238
|
+
[0x26ce, 0x26ce],
|
|
239
|
+
[0x26d4, 0x26d4],
|
|
240
|
+
[0x26ea, 0x26ea],
|
|
241
|
+
[0x26f2, 0x26f3],
|
|
242
|
+
[0x26f5, 0x26f5],
|
|
243
|
+
[0x26fa, 0x26fa],
|
|
244
|
+
[0x26fd, 0x26fd],
|
|
245
|
+
[0x2705, 0x2705],
|
|
246
|
+
[0x270a, 0x270b],
|
|
247
|
+
[0x2728, 0x2728],
|
|
248
|
+
[0x274c, 0x274c],
|
|
249
|
+
[0x274e, 0x274e],
|
|
250
|
+
[0x2753, 0x2755],
|
|
251
|
+
[0x2757, 0x2757],
|
|
252
|
+
[0x2795, 0x2797],
|
|
253
|
+
[0x27b0, 0x27b0],
|
|
254
|
+
[0x27bf, 0x27bf],
|
|
255
|
+
[0x2b1b, 0x2b1c],
|
|
256
|
+
[0x2b50, 0x2b50],
|
|
257
|
+
[0x2b55, 0x2b55],
|
|
258
|
+
[0x2e80, 0x303e],
|
|
259
|
+
[0x3041, 0x33ff],
|
|
260
|
+
[0x3400, 0x4dbf],
|
|
261
|
+
[0x4e00, 0x9fff],
|
|
262
|
+
[0xa000, 0xa4cf],
|
|
263
|
+
[0xa960, 0xa97f],
|
|
264
|
+
[0xac00, 0xd7a3],
|
|
265
|
+
[0xf900, 0xfaff],
|
|
266
|
+
[0xfe10, 0xfe19],
|
|
267
|
+
[0xfe30, 0xfe6f],
|
|
268
|
+
[0xff00, 0xff60],
|
|
269
|
+
[0xffe0, 0xffe6],
|
|
270
|
+
[0x16fe0, 0x16fe4],
|
|
271
|
+
[0x17000, 0x18aff],
|
|
272
|
+
[0x1b000, 0x1b2ff],
|
|
273
|
+
[0x1f004, 0x1f004],
|
|
274
|
+
[0x1f0cf, 0x1f0cf],
|
|
275
|
+
[0x1f18e, 0x1f18e],
|
|
276
|
+
[0x1f191, 0x1f19a],
|
|
277
|
+
[0x1f200, 0x1f251],
|
|
278
|
+
[0x1f300, 0x1f64f],
|
|
279
|
+
[0x1f680, 0x1f6ff],
|
|
280
|
+
[0x1f7e0, 0x1f7eb],
|
|
281
|
+
[0x1f90c, 0x1f9ff],
|
|
282
|
+
[0x1fa70, 0x1faff],
|
|
283
|
+
[0x20000, 0x2fffd],
|
|
284
|
+
[0x30000, 0x3fffd],
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
function isWide(code: number): boolean {
|
|
288
|
+
return WIDE_RANGES.some(([from, to]) => code >= from && code <= to);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Drops spaces at the end of the output; a newline before them is a blank line to keep. */
|
|
292
|
+
function trimTrailingSpaces(output: string[]): void {
|
|
293
|
+
for (let last = output.at(-1); last !== undefined; last = output.at(-1)) {
|
|
294
|
+
const trimmed = last.replace(/ +$/, "");
|
|
295
|
+
if (trimmed !== "") {
|
|
296
|
+
output[output.length - 1] = trimmed;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
output.pop();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const hardlines = new WeakMap<Group, boolean>();
|
|
304
|
+
|
|
305
|
+
/** Whether a group must break because it holds a hard line; cached, as groups nest deeply. */
|
|
306
|
+
function containsHardline(target: Group): boolean {
|
|
307
|
+
let known = hardlines.get(target);
|
|
308
|
+
if (known === undefined) {
|
|
309
|
+
known = hasHardline(target.contents);
|
|
310
|
+
hardlines.set(target, known);
|
|
311
|
+
}
|
|
312
|
+
return known;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Whether `next` fits in the remaining `width`, together with whatever follows it on the same
|
|
317
|
+
* line: the rest of the stack, up to its first line break in break mode.
|
|
318
|
+
*/
|
|
319
|
+
function fits(next: Command, rest: readonly Command[], width: number): boolean {
|
|
320
|
+
let remaining = width;
|
|
321
|
+
const pending: Command[] = [next];
|
|
322
|
+
let restIndex = rest.length - 1;
|
|
323
|
+
while (remaining >= 0) {
|
|
324
|
+
let command = pending.pop();
|
|
325
|
+
if (command === undefined) {
|
|
326
|
+
if (restIndex < 0) return true;
|
|
327
|
+
command = rest[restIndex--];
|
|
328
|
+
if (command === undefined) return true;
|
|
329
|
+
}
|
|
330
|
+
const { mode, doc } = command;
|
|
331
|
+
if (typeof doc === "string") {
|
|
332
|
+
remaining -= textWidth(doc);
|
|
333
|
+
} else if (isDocArray(doc)) {
|
|
334
|
+
for (let index = doc.length - 1; index >= 0; index--) {
|
|
335
|
+
const part = doc[index];
|
|
336
|
+
if (part !== undefined) pending.push({ indent: command.indent, mode, doc: part });
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
switch (doc.kind) {
|
|
340
|
+
case "group":
|
|
341
|
+
case "indent":
|
|
342
|
+
case "indentIfBreak":
|
|
343
|
+
pending.push({ indent: command.indent, mode, doc: doc.contents });
|
|
344
|
+
break;
|
|
345
|
+
case "ifBreak":
|
|
346
|
+
pending.push({
|
|
347
|
+
indent: command.indent,
|
|
348
|
+
mode,
|
|
349
|
+
doc: mode === "break" ? doc.broken : doc.flat,
|
|
350
|
+
});
|
|
351
|
+
break;
|
|
352
|
+
case "fill":
|
|
353
|
+
if (mode === "break") {
|
|
354
|
+
remaining -= textWidth(doc.contents[0] ?? "");
|
|
355
|
+
if (doc.contents.length > 1) return true;
|
|
356
|
+
} else {
|
|
357
|
+
remaining -= textWidth(doc.contents.join(" "));
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
case "line":
|
|
361
|
+
if (mode === "break" || doc.mode === "hard") return true;
|
|
362
|
+
if (doc.mode === "line") remaining -= 1;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function hasHardline(doc: Doc): boolean {
|
|
371
|
+
if (typeof doc === "string") return false;
|
|
372
|
+
if (isDocArray(doc)) return doc.some(hasHardline);
|
|
373
|
+
switch (doc.kind) {
|
|
374
|
+
case "line":
|
|
375
|
+
return doc.mode === "hard";
|
|
376
|
+
case "fill":
|
|
377
|
+
return false;
|
|
378
|
+
case "ifBreak":
|
|
379
|
+
return hasHardline(doc.broken) || hasHardline(doc.flat);
|
|
380
|
+
case "group":
|
|
381
|
+
case "indent":
|
|
382
|
+
case "indentIfBreak":
|
|
383
|
+
return hasHardline(doc.contents);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// `Array.isArray` doesn't narrow readonly arrays out of a union.
|
|
388
|
+
function isDocArray(doc: Doc): doc is readonly Doc[] {
|
|
389
|
+
return Array.isArray(doc);
|
|
390
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Schema, stringify } from "@tscomfy/core";
|
|
2
|
+
import { assignment, group, ifBreak, indent, print, PRINT_WIDTH, softline } from "./doc.ts";
|
|
3
|
+
import { exportNameProblem } from "./export-name.ts";
|
|
4
|
+
import { headerLines, quote } from "./names.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emits the runtime schema module, `index.ts` (docs/plan.md §6.1, D13): the schema as one string
|
|
8
|
+
* literal loaded by `defineSchema`, typed by the `Nodes` interface of `nodes.ts`.
|
|
9
|
+
*
|
|
10
|
+
* `exportName` is the exported constant, e.g. `local`; `nodesModule` the import specifier of the
|
|
11
|
+
* types file, e.g. `./nodes.js`. Throws a `TypeError` when `exportNameProblem` finds a problem
|
|
12
|
+
* with `exportName`.
|
|
13
|
+
*/
|
|
14
|
+
export function emitSchemaModule(
|
|
15
|
+
schema: Schema,
|
|
16
|
+
exportName: string,
|
|
17
|
+
nodesModule: string,
|
|
18
|
+
header: readonly string[] = [],
|
|
19
|
+
): string {
|
|
20
|
+
const problem = exportNameProblem(exportName);
|
|
21
|
+
if (problem !== undefined) {
|
|
22
|
+
throw new TypeError(`emitSchemaModule: can't export ${JSON.stringify(exportName)}: ${problem}`);
|
|
23
|
+
}
|
|
24
|
+
const lines = [
|
|
25
|
+
...headerLines(header),
|
|
26
|
+
...(header.length > 0 ? [""] : []),
|
|
27
|
+
'import { defineSchema, type TypedSchema } from "@tscomfy/core";',
|
|
28
|
+
`import type { Nodes } from ${JSON.stringify(nodesModule)};`,
|
|
29
|
+
"",
|
|
30
|
+
"/** The node definitions this module was generated from, typed by `Nodes`. */",
|
|
31
|
+
print(
|
|
32
|
+
assignment(`export const ${exportName}: TypedSchema<Nodes>`, [
|
|
33
|
+
group([
|
|
34
|
+
"defineSchema<Nodes>(",
|
|
35
|
+
indent([softline, quote(stringify(schema))]),
|
|
36
|
+
ifBreak(","),
|
|
37
|
+
softline,
|
|
38
|
+
")",
|
|
39
|
+
]),
|
|
40
|
+
";",
|
|
41
|
+
]),
|
|
42
|
+
PRINT_WIDTH,
|
|
43
|
+
),
|
|
44
|
+
"",
|
|
45
|
+
];
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|