@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The one rule for the names generated modules export (docs/plan.md §9, §10): `generate`'s
|
|
2
|
+
// schema constant and `transpile`'s workflow constant.
|
|
3
|
+
|
|
4
|
+
const ASCII_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Words an ES module can't declare as a constant (reserved words, `arguments` and `eval` in
|
|
8
|
+
* strict code) or must not shadow (`undefined`, `NaN`, `Infinity`).
|
|
9
|
+
*/
|
|
10
|
+
const RESERVED: ReadonlySet<string> = new Set(
|
|
11
|
+
"break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield await let static implements interface package private protected public arguments eval undefined NaN Infinity".split(
|
|
12
|
+
" ",
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/** What the schema module (`index.ts`) imports. */
|
|
17
|
+
export const SCHEMA_MODULE_NAMES: readonly string[] = ["defineSchema", "TypedSchema", "Nodes"];
|
|
18
|
+
|
|
19
|
+
/** What a transpiled module imports or binds itself. */
|
|
20
|
+
export const TRANSPILED_MODULE_NAMES: readonly string[] = [
|
|
21
|
+
"workflow",
|
|
22
|
+
"Workflow",
|
|
23
|
+
"NodesOf",
|
|
24
|
+
"n",
|
|
25
|
+
"raw",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Why `name` can't be the exported constant of a generated module, or `undefined` when it can.
|
|
30
|
+
* One rule for both kinds: a schema module's export (`generate`) and a transpiled workflow's
|
|
31
|
+
* (`transpile`), which imports the schema's, so a schema named `raw` is refused when it is
|
|
32
|
+
* generated rather than when a workflow is first transpiled over it. The name must be an ASCII
|
|
33
|
+
* identifier, not a reserved word, and none of the names either module declares itself
|
|
34
|
+
* (`defineSchema`, `TypedSchema`, `Nodes`, `workflow`, `Workflow`, `NodesOf`, `n`, `raw`).
|
|
35
|
+
*/
|
|
36
|
+
export function exportNameProblem(name: string): string | undefined {
|
|
37
|
+
if (!ASCII_IDENTIFIER.test(name)) {
|
|
38
|
+
return "it isn't an identifier (ASCII letters, digits, _ and $, not starting with a digit)";
|
|
39
|
+
}
|
|
40
|
+
if (RESERVED.has(name)) return "JavaScript reserves it";
|
|
41
|
+
if (SCHEMA_MODULE_NAMES.includes(name) || TRANSPILED_MODULE_NAMES.includes(name)) {
|
|
42
|
+
const names = [...SCHEMA_MODULE_NAMES, ...TRANSPILED_MODULE_NAMES].join(", ");
|
|
43
|
+
return `the generated modules declare it themselves (${names})`;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether {@link exportNameProblem} finds nothing wrong with `name`. */
|
|
49
|
+
export function isExportName(name: string): boolean {
|
|
50
|
+
return exportNameProblem(name) === undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Expression layout, as oxfmt prints it (Prettier's rules for calls, objects and arrays), and
|
|
2
|
+
// the layout of the transpiled module's declaration.
|
|
3
|
+
|
|
4
|
+
import { type InputValue, isOutputRef, type JsonValue } from "@tscomfy/core";
|
|
5
|
+
import {
|
|
6
|
+
type Doc,
|
|
7
|
+
fill,
|
|
8
|
+
group,
|
|
9
|
+
groupId,
|
|
10
|
+
hardline,
|
|
11
|
+
ifBreak,
|
|
12
|
+
indent,
|
|
13
|
+
indentIfBreak,
|
|
14
|
+
join,
|
|
15
|
+
line,
|
|
16
|
+
PRINT_WIDTH,
|
|
17
|
+
softline,
|
|
18
|
+
textWidth,
|
|
19
|
+
} from "./doc.ts";
|
|
20
|
+
import { propertyKey, quote } from "./names.ts";
|
|
21
|
+
import { isList } from "./values.ts";
|
|
22
|
+
|
|
23
|
+
/** The names a transpiled module's declaration is written with. */
|
|
24
|
+
export interface DeclarationNames {
|
|
25
|
+
/** The exported constant. */
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** The schema module's export. */
|
|
28
|
+
readonly schemaName: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How `export const <name>: Workflow<…> = workflow(<schema>, <build> <block>);` is laid out, as
|
|
33
|
+
* Prettier lays out an assignment whose right side is a call with a function last: the function
|
|
34
|
+
* hugs the call while the line up to its `{` fits (with `});` for an empty block), else each
|
|
35
|
+
* argument takes a line of its own; the right side moves below `=` when that makes the call's
|
|
36
|
+
* first line fit.
|
|
37
|
+
*/
|
|
38
|
+
export type Layout = "hug" | "expand" | "belowHug" | "belowExpand";
|
|
39
|
+
|
|
40
|
+
/** The column the build function's statements start at, per layout. */
|
|
41
|
+
export const STATEMENT_INDENT: Record<Layout, number> = {
|
|
42
|
+
hug: 2,
|
|
43
|
+
expand: 4,
|
|
44
|
+
belowHug: 4,
|
|
45
|
+
belowExpand: 6,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function declarationLayout(
|
|
49
|
+
options: DeclarationNames,
|
|
50
|
+
build: string,
|
|
51
|
+
empty: boolean,
|
|
52
|
+
): Layout {
|
|
53
|
+
const left = declarationLeft(options);
|
|
54
|
+
const hug = `workflow(${options.schemaName}, ${build} ${empty ? "{});" : "{"}`;
|
|
55
|
+
if (textWidth(`${left} ${hug}`) <= PRINT_WIDTH) return "hug";
|
|
56
|
+
if (textWidth(`${left} workflow(`) <= PRINT_WIDTH) return "expand";
|
|
57
|
+
return textWidth(` ${hug}`) <= PRINT_WIDTH ? "belowHug" : "belowExpand";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function declarationLeft(options: DeclarationNames): string {
|
|
61
|
+
return `export const ${options.name}: Workflow<NodesOf<typeof ${options.schemaName}>> =`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function declarationDoc(
|
|
65
|
+
options: DeclarationNames,
|
|
66
|
+
build: string,
|
|
67
|
+
block: Doc,
|
|
68
|
+
layout: Layout,
|
|
69
|
+
): Doc {
|
|
70
|
+
const { schemaName } = options;
|
|
71
|
+
const left = declarationLeft(options);
|
|
72
|
+
const hugged: Doc = [`workflow(${schemaName}, `, build, " ", block, ");"];
|
|
73
|
+
const expanded: Doc = [
|
|
74
|
+
"workflow(",
|
|
75
|
+
indent([hardline, schemaName, ",", hardline, build, " ", block, ","]),
|
|
76
|
+
hardline,
|
|
77
|
+
");",
|
|
78
|
+
];
|
|
79
|
+
switch (layout) {
|
|
80
|
+
case "hug":
|
|
81
|
+
return [left, " ", hugged];
|
|
82
|
+
case "expand":
|
|
83
|
+
return [left, " ", expanded];
|
|
84
|
+
case "belowHug":
|
|
85
|
+
return [left, indent([hardline, hugged])];
|
|
86
|
+
case "belowExpand":
|
|
87
|
+
return [left, indent([hardline, expanded])];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A call: arguments on one line, or one per line. */
|
|
92
|
+
export function callDoc(callee: string, args: readonly Doc[]): Doc {
|
|
93
|
+
return group([
|
|
94
|
+
callee,
|
|
95
|
+
"(",
|
|
96
|
+
indent([softline, join([",", line], args)]),
|
|
97
|
+
ifBreak(","),
|
|
98
|
+
softline,
|
|
99
|
+
")",
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `const x = …;`, breaking inside the call rather than after `=`. */
|
|
104
|
+
export function statementDoc(left: string, call: Doc): Doc {
|
|
105
|
+
const id = groupId("statement");
|
|
106
|
+
return group([left, " =", group(indent(line), id), indentIfBreak([call, ";"], id)]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function objectDoc(properties: readonly Doc[]): Doc {
|
|
110
|
+
if (properties.length === 0) return "{}";
|
|
111
|
+
return group(["{", indent([line, join([",", line], properties)]), ifBreak(","), line, "}"]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* An array. Prettier always breaks an array of two or more objects or arrays that each have
|
|
116
|
+
* more than one element.
|
|
117
|
+
*/
|
|
118
|
+
export function arrayDoc(items: readonly Doc[], breaks = false): Doc {
|
|
119
|
+
if (items.length === 0) return "[]";
|
|
120
|
+
if (breaks) return ["[", indent([hardline, join([",", hardline], items)]), ",", hardline, "]"];
|
|
121
|
+
return group(["[", indent([softline, join([",", line], items)]), ifBreak(","), softline, "]"]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `key: value`; a scalar stays on the key's line, anything else may move below it. */
|
|
125
|
+
export function propertyDoc(key: string, value: Doc): Doc {
|
|
126
|
+
// Prettier never breaks after a key narrower than five columns (tab width plus three).
|
|
127
|
+
const short = textWidth(key) < 5;
|
|
128
|
+
if (short || (typeof value === "string" && !value.startsWith('"') && !value.startsWith("'"))) {
|
|
129
|
+
return [key, ": ", value];
|
|
130
|
+
}
|
|
131
|
+
const id = groupId("property");
|
|
132
|
+
return group([key, ":", group(indent(line), id), indentIfBreak(value, id)]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A number literal as Prettier writes it: exponents without `+` (`1e21`, not `1e+21`). */
|
|
136
|
+
function numberText(value: number): string {
|
|
137
|
+
return Object.is(value, -0) ? "-0" : String(value).replace("e+", "e");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function scalarDoc(value: string | number | bigint | boolean | null): Doc {
|
|
141
|
+
if (typeof value === "string") return quote(value);
|
|
142
|
+
if (typeof value === "bigint") return `${String(value)}n`;
|
|
143
|
+
if (typeof value === "number") return numberText(value);
|
|
144
|
+
return String(value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** "object" or "array" for an element with more than one entry. */
|
|
148
|
+
function many(item: InputValue | JsonValue): "object" | "array" | undefined {
|
|
149
|
+
if (isList(item)) return item.length > 1 ? "array" : undefined;
|
|
150
|
+
if (typeof item === "object" && item !== null && !isOutputRef(item)) {
|
|
151
|
+
return Object.keys(item).length > 1 ? "object" : undefined;
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether Prettier breaks an array whatever its width: two or more elements, all objects with
|
|
158
|
+
* more than one key or all arrays with more than one element.
|
|
159
|
+
*/
|
|
160
|
+
export function breaksArray(items: readonly (InputValue | JsonValue)[]): boolean {
|
|
161
|
+
const first = items[0] === undefined ? undefined : many(items[0]);
|
|
162
|
+
return items.length > 1 && first !== undefined && items.every((item) => many(item) === first);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A JSON value as an expression. */
|
|
166
|
+
export function jsonDoc(value: JsonValue): Doc {
|
|
167
|
+
if (isList(value)) {
|
|
168
|
+
return isNumberList(value)
|
|
169
|
+
? numbersDoc(value)
|
|
170
|
+
: arrayDoc(value.map(jsonDoc), breaksArray(value));
|
|
171
|
+
}
|
|
172
|
+
if (typeof value === "object" && value !== null) {
|
|
173
|
+
return objectDoc(
|
|
174
|
+
Object.entries(value).map(([key, item]) => propertyDoc(objectKey(key), jsonDoc(item))),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return scalarDoc(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A list of two or more numbers, which Prettier prints concisely, as many per line as fit. */
|
|
181
|
+
export function isNumberList(
|
|
182
|
+
value: readonly (InputValue | JsonValue)[],
|
|
183
|
+
): value is readonly number[] {
|
|
184
|
+
return value.length > 1 && value.every((item) => typeof item === "number");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function numbersDoc(values: readonly number[]): Doc {
|
|
188
|
+
const items = values.map((value, index) =>
|
|
189
|
+
index < values.length - 1 ? `${numberText(value)},` : numberText(value),
|
|
190
|
+
);
|
|
191
|
+
// The trailing comma belongs to the last item, so the last line's fit counts it.
|
|
192
|
+
return group(["[", indent([softline, fill(items, ",")]), softline, "]"]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A key for an object literal. `__proto__` is computed, since a plain `__proto__:` would set
|
|
197
|
+
* the prototype rather than make a property.
|
|
198
|
+
*/
|
|
199
|
+
export function objectKey(name: string): string {
|
|
200
|
+
return name === "__proto__" ? '["__proto__"]' : propertyKey(name);
|
|
201
|
+
}
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Diagnostic, hashSchema, type Schema } from "@tscomfy/core";
|
|
2
|
+
import { emitSchemaModule } from "./emit-schema-module.ts";
|
|
3
|
+
import { emitTypes } from "./emit-types.ts";
|
|
4
|
+
|
|
5
|
+
/** The version written into generated headers. A test keeps it equal to package.json. */
|
|
6
|
+
export const CODEGEN_VERSION = "0.1.0";
|
|
7
|
+
|
|
8
|
+
/** A file to write, as data: codegen never touches the filesystem. */
|
|
9
|
+
export interface GeneratedFile {
|
|
10
|
+
/** Relative to the output directory. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly contents: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Options for {@link generate}. */
|
|
16
|
+
export interface GenerateOptions {
|
|
17
|
+
/** The exported schema constant, e.g. `local`: one `exportNameProblem` finds nothing wrong with. */
|
|
18
|
+
readonly exportName: string;
|
|
19
|
+
/**
|
|
20
|
+
* The extension `index.ts` uses to import `nodes.ts`: `.js` (the default) works under
|
|
21
|
+
* `nodenext` and `bundler` resolution; `.ts` needs `allowImportingTsExtensions` or
|
|
22
|
+
* `rewriteRelativeImportExtensions`.
|
|
23
|
+
*/
|
|
24
|
+
readonly importExtension?: ".js" | ".ts";
|
|
25
|
+
/** The command that regenerates the files, recorded in their headers. */
|
|
26
|
+
readonly command?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The result of {@link generate}. */
|
|
30
|
+
export interface GenerateResult {
|
|
31
|
+
/** `nodes.ts` and `index.ts`. */
|
|
32
|
+
readonly files: readonly GeneratedFile[];
|
|
33
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates the files for one schema: `nodes.ts` (types only) and
|
|
38
|
+
* `index.ts` (the typed runtime schema). Pure and deterministic. Throws a `TypeError` for an
|
|
39
|
+
* export name `exportNameProblem` refuses.
|
|
40
|
+
*/
|
|
41
|
+
export function generate(schema: Schema, options: GenerateOptions): GenerateResult {
|
|
42
|
+
const header = [
|
|
43
|
+
`Generated by @tscomfy/codegen ${CODEGEN_VERSION}. Do not edit: regenerate instead.`,
|
|
44
|
+
`Schema ${hashSchema(schema)}${schema.comfyVersion === undefined ? "" : `, ComfyUI ${schema.comfyVersion}`}.`,
|
|
45
|
+
...(options.command === undefined ? [] : [`Regenerate with: ${options.command}`]),
|
|
46
|
+
];
|
|
47
|
+
const types = emitTypes(schema, header);
|
|
48
|
+
const nodesModule = `./nodes${options.importExtension ?? ".js"}`;
|
|
49
|
+
return {
|
|
50
|
+
files: [
|
|
51
|
+
{ path: "nodes.ts", contents: types.contents },
|
|
52
|
+
{
|
|
53
|
+
path: "index.ts",
|
|
54
|
+
contents: emitSchemaModule(schema, options.exportName, nodesModule, header),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
diagnostics: types.diagnostics,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @tscomfy/codegen
|
|
3
|
+
*
|
|
4
|
+
* Turns a normalized tscomfy schema into generated TypeScript (node types and
|
|
5
|
+
* the runtime schema module) and transpiles workflows to code. Pure: returns
|
|
6
|
+
* files as data and never touches the filesystem.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
CODEGEN_VERSION,
|
|
11
|
+
generate,
|
|
12
|
+
type GeneratedFile,
|
|
13
|
+
type GenerateOptions,
|
|
14
|
+
type GenerateResult,
|
|
15
|
+
} from "./generate.ts";
|
|
16
|
+
export { exportNameProblem } from "./export-name.ts";
|
|
17
|
+
export {
|
|
18
|
+
transpile,
|
|
19
|
+
transpileNameProblem,
|
|
20
|
+
type TranspileOptions,
|
|
21
|
+
type TranspileResult,
|
|
22
|
+
} from "./transpile.ts";
|
package/src/names.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Identifiers and literal text for generated code. ComfyUI names (class types, input names,
|
|
2
|
+
// option keys) are arbitrary strings: 328 class names in a real dump are not identifiers. They
|
|
3
|
+
// stay exact as property keys and are sanitized only where TypeScript needs an identifier.
|
|
4
|
+
|
|
5
|
+
// ECMAScript IdentifierName, which oxfmt leaves unquoted as a key (`café`, `日本`).
|
|
6
|
+
const IDENTIFIER = /^[\p{ID_Start}$_][\p{ID_Continue}$\u200c\u200d]*$/u;
|
|
7
|
+
|
|
8
|
+
/** A property key: the name itself when it is an identifier, else a string literal. */
|
|
9
|
+
export function propertyKey(name: string): string {
|
|
10
|
+
return IDENTIFIER.test(name) ? name : quote(name);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A string literal as oxfmt writes it: double quotes, unless the text holds more double quotes
|
|
15
|
+
* than single ones, then single quotes. Other escapes are `JSON.stringify`'s.
|
|
16
|
+
*/
|
|
17
|
+
export function quote(text: string): string {
|
|
18
|
+
const doubled = JSON.stringify(text);
|
|
19
|
+
const doubles = text.split('"').length - 1;
|
|
20
|
+
const singles = text.split("'").length - 1;
|
|
21
|
+
if (doubles <= singles) return doubled;
|
|
22
|
+
const inner = doubled.slice(1, -1).replaceAll('\\"', '"').replaceAll("'", "\\'");
|
|
23
|
+
return `'${inner}'`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LINE_TERMINATOR = /\r\n|[\r\n\u2028\u2029]/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lines of server text for a comment: split at every line terminator (which a comment can't
|
|
30
|
+
* hold raw without oxfmt rewriting it) and with `*\/` escaped, so no name or tooltip can close
|
|
31
|
+
* the comment and inject code.
|
|
32
|
+
*/
|
|
33
|
+
export function commentLines(text: string): string[] {
|
|
34
|
+
return text.split(LINE_TERMINATOR).map((each) => each.replaceAll("*/", "*\\/").trimEnd());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Server text for a one-line comment. */
|
|
38
|
+
export function commentText(text: string): string {
|
|
39
|
+
return commentLines(text).join(" ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `//` comment lines; line terminators in the text can't end the comment early. */
|
|
43
|
+
export function headerLines(header: readonly string[]): string[] {
|
|
44
|
+
return header.map((text) => `// ${commentText(text)}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A PascalCase identifier part from an arbitrary name: the runs of letters and digits, each
|
|
49
|
+
* capitalized (`sampler_name` → `SamplerName`, `easy kSampler` → `EasyKSampler`). Never empty
|
|
50
|
+
* and never starts with a digit.
|
|
51
|
+
*/
|
|
52
|
+
export function pascal(name: string, fallback: string): string {
|
|
53
|
+
const words = name.match(/[A-Za-z0-9]+/g) ?? [];
|
|
54
|
+
const joined = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
55
|
+
if (joined === "") return fallback;
|
|
56
|
+
return /^[0-9]/.test(joined) ? `${fallback}${joined}` : joined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Hands out unique names in one namespace, in call order. A taken base gets the first free
|
|
61
|
+
* numeric suffix (`Mode`, `Mode2`, …), so names are stable for a given schema.
|
|
62
|
+
*/
|
|
63
|
+
export class NameRegistry {
|
|
64
|
+
readonly #taken = new Set<string>();
|
|
65
|
+
/** Per base, the first suffix not yet tried: names are never given back, so none below is free. */
|
|
66
|
+
readonly #next = new Map<string, number>();
|
|
67
|
+
|
|
68
|
+
constructor(reserved: readonly string[] = []) {
|
|
69
|
+
for (const name of reserved) this.#taken.add(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A fresh name based on `base`, now taken. */
|
|
73
|
+
claim(base: string): string {
|
|
74
|
+
let suffix = this.#next.get(base) ?? 1;
|
|
75
|
+
let name = suffix === 1 ? base : `${base}${String(suffix)}`;
|
|
76
|
+
while (this.#taken.has(name)) {
|
|
77
|
+
suffix++;
|
|
78
|
+
name = `${base}${String(suffix)}`;
|
|
79
|
+
}
|
|
80
|
+
this.#next.set(base, suffix + 1);
|
|
81
|
+
this.#taken.add(name);
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Whether `name` is taken. */
|
|
86
|
+
has(name: string): boolean {
|
|
87
|
+
return this.#taken.has(name);
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/rules.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Input rules the generated types are written with (docs/plan.md §6.2, §7.2), shared by the type
|
|
2
|
+
// emitter, which writes them, and the transpiler, which must decide by the same rules whether
|
|
3
|
+
// the types can express a node.
|
|
4
|
+
|
|
5
|
+
import { type InputDef, sendsInputValue } from "@tscomfy/core";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether a property named `name` must be required: TypeScript checks an optional property
|
|
9
|
+
* against the `Object` member of that name, which every object literal has.
|
|
10
|
+
*/
|
|
11
|
+
export function isObjectMember(name: string): boolean {
|
|
12
|
+
return OBJECT_MEMBERS.has(name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Members of TypeScript's `Object` interface, which every object type has. */
|
|
16
|
+
const OBJECT_MEMBERS: ReadonlySet<string> = new Set([
|
|
17
|
+
"constructor",
|
|
18
|
+
"toString",
|
|
19
|
+
"toLocaleString",
|
|
20
|
+
"valueOf",
|
|
21
|
+
"hasOwnProperty",
|
|
22
|
+
"isPrototypeOf",
|
|
23
|
+
"propertyIsEnumerable",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The field a DynamicGroup row's type requires beyond the required ones (§7.2): a row is counted
|
|
28
|
+
* only when one of its keys is sent, so when no field is required or has a default, the first
|
|
29
|
+
* one must be given, or `{}` would send no row at all.
|
|
30
|
+
*/
|
|
31
|
+
export function forcedRowField(fields: readonly InputDef[]): InputDef | undefined {
|
|
32
|
+
const sendable = fields.filter(sendsInputValue);
|
|
33
|
+
return sendable.some((field) => isRequired(field) || hasDefault(field)) ? undefined : sendable[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether an input must be given: the backend requires it and no default fills it in.
|
|
38
|
+
* Autogrow and DynamicGroup are required by their minimum alone: the backend enforces it even
|
|
39
|
+
* when the input itself is optional. An input named after a member of `Object` is always
|
|
40
|
+
* required: TypeScript checks an optional property against the member every object literal
|
|
41
|
+
* has (`constructor: Function`), so leaving it out would fail to compile.
|
|
42
|
+
*/
|
|
43
|
+
export function isRequired(input: InputDef): boolean {
|
|
44
|
+
if (isObjectMember(input.name)) return true;
|
|
45
|
+
switch (input.kind) {
|
|
46
|
+
case "autogrow":
|
|
47
|
+
return input.template.required && input.min > 0 && input.slots.length > 0;
|
|
48
|
+
case "dynamicGroup":
|
|
49
|
+
return input.min > 0;
|
|
50
|
+
case "combo":
|
|
51
|
+
// A forceInput combo has no widget, so nothing fills its default in.
|
|
52
|
+
return input.required && (input.default === undefined || input.forceInput === true);
|
|
53
|
+
case "int":
|
|
54
|
+
case "float":
|
|
55
|
+
case "string":
|
|
56
|
+
case "boolean":
|
|
57
|
+
case "value":
|
|
58
|
+
return input.required && input.default === undefined;
|
|
59
|
+
case "socket":
|
|
60
|
+
case "dynamicCombo":
|
|
61
|
+
case "matchType":
|
|
62
|
+
case "unknown":
|
|
63
|
+
return input.required;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether the exporter sends a value for the input when none is given. */
|
|
68
|
+
export function hasDefault(input: InputDef): boolean {
|
|
69
|
+
switch (input.kind) {
|
|
70
|
+
case "combo":
|
|
71
|
+
return input.default !== undefined && input.forceInput !== true;
|
|
72
|
+
case "int":
|
|
73
|
+
case "float":
|
|
74
|
+
case "string":
|
|
75
|
+
case "boolean":
|
|
76
|
+
case "value":
|
|
77
|
+
return input.default !== undefined;
|
|
78
|
+
case "socket":
|
|
79
|
+
case "autogrow":
|
|
80
|
+
case "dynamicCombo":
|
|
81
|
+
case "dynamicGroup":
|
|
82
|
+
case "matchType":
|
|
83
|
+
case "unknown":
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|