@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/dist/rules.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
import { sendsInputValue } from "@tscomfy/core";
|
|
5
|
+
/**
|
|
6
|
+
* Whether a property named `name` must be required: TypeScript checks an optional property
|
|
7
|
+
* against the `Object` member of that name, which every object literal has.
|
|
8
|
+
*/
|
|
9
|
+
export function isObjectMember(name) {
|
|
10
|
+
return OBJECT_MEMBERS.has(name);
|
|
11
|
+
}
|
|
12
|
+
/** Members of TypeScript's `Object` interface, which every object type has. */
|
|
13
|
+
const OBJECT_MEMBERS = new Set([
|
|
14
|
+
"constructor",
|
|
15
|
+
"toString",
|
|
16
|
+
"toLocaleString",
|
|
17
|
+
"valueOf",
|
|
18
|
+
"hasOwnProperty",
|
|
19
|
+
"isPrototypeOf",
|
|
20
|
+
"propertyIsEnumerable",
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* The field a DynamicGroup row's type requires beyond the required ones (§7.2): a row is counted
|
|
24
|
+
* only when one of its keys is sent, so when no field is required or has a default, the first
|
|
25
|
+
* one must be given, or `{}` would send no row at all.
|
|
26
|
+
*/
|
|
27
|
+
export function forcedRowField(fields) {
|
|
28
|
+
const sendable = fields.filter(sendsInputValue);
|
|
29
|
+
return sendable.some((field) => isRequired(field) || hasDefault(field)) ? undefined : sendable[0];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether an input must be given: the backend requires it and no default fills it in.
|
|
33
|
+
* Autogrow and DynamicGroup are required by their minimum alone: the backend enforces it even
|
|
34
|
+
* when the input itself is optional. An input named after a member of `Object` is always
|
|
35
|
+
* required: TypeScript checks an optional property against the member every object literal
|
|
36
|
+
* has (`constructor: Function`), so leaving it out would fail to compile.
|
|
37
|
+
*/
|
|
38
|
+
export function isRequired(input) {
|
|
39
|
+
if (isObjectMember(input.name))
|
|
40
|
+
return true;
|
|
41
|
+
switch (input.kind) {
|
|
42
|
+
case "autogrow":
|
|
43
|
+
return input.template.required && input.min > 0 && input.slots.length > 0;
|
|
44
|
+
case "dynamicGroup":
|
|
45
|
+
return input.min > 0;
|
|
46
|
+
case "combo":
|
|
47
|
+
// A forceInput combo has no widget, so nothing fills its default in.
|
|
48
|
+
return input.required && (input.default === undefined || input.forceInput === true);
|
|
49
|
+
case "int":
|
|
50
|
+
case "float":
|
|
51
|
+
case "string":
|
|
52
|
+
case "boolean":
|
|
53
|
+
case "value":
|
|
54
|
+
return input.required && input.default === undefined;
|
|
55
|
+
case "socket":
|
|
56
|
+
case "dynamicCombo":
|
|
57
|
+
case "matchType":
|
|
58
|
+
case "unknown":
|
|
59
|
+
return input.required;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Whether the exporter sends a value for the input when none is given. */
|
|
63
|
+
export function hasDefault(input) {
|
|
64
|
+
switch (input.kind) {
|
|
65
|
+
case "combo":
|
|
66
|
+
return input.default !== undefined && input.forceInput !== true;
|
|
67
|
+
case "int":
|
|
68
|
+
case "float":
|
|
69
|
+
case "string":
|
|
70
|
+
case "boolean":
|
|
71
|
+
case "value":
|
|
72
|
+
return input.default !== undefined;
|
|
73
|
+
case "socket":
|
|
74
|
+
case "autogrow":
|
|
75
|
+
case "dynamicCombo":
|
|
76
|
+
case "dynamicGroup":
|
|
77
|
+
case "matchType":
|
|
78
|
+
case "unknown":
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,8FAA8F;AAC9F,gCAAgC;AAEhC,OAAO,EAAiB,eAAe,EAAE,MAAM,eAAe,CAAC;AAE/D;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,+EAA+E;AAC/E,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC;IAClD,aAAa;IACb,UAAU;IACV,gBAAgB;IAChB,SAAS;IACT,gBAAgB;IAChB,eAAe;IACf,sBAAsB;CACvB,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAA2B;IACxD,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAChD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AACpG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,KAAe;IACxC,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,UAAU;YACb,OAAO,KAAK,CAAC,QAAQ,CAAC,QAAQ,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5E,KAAK,cAAc;YACjB,OAAO,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;QACvB,KAAK,OAAO;YACV,qEAAqE;YACrE,OAAO,KAAK,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC;QACtF,KAAK,KAAK,CAAC;QACX,KAAK,OAAO,CAAC;QACb,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS,CAAC;QACf,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC;QACvD,KAAK,QAAQ,CAAC;QACd,KAAK,cAAc,CAAC;QACpB,KAAK,WAAW,CAAC;QACjB,KAAK,SAAS;YACZ,OAAO,KAAK,CAAC,QAAQ,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAC,KAAe;IACxC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;QAClE,KAAK,KAAK,CAAC;QACX,KAAK,OAAO,CAAC;QACb,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS,CAAC;QACf,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC;QACrC,KAAK,QAAQ,CAAC;QACd,KAAK,UAAU,CAAC;QAChB,KAAK,cAAc,CAAC;QACpB,KAAK,cAAc,CAAC;QACpB,KAAK,WAAW,CAAC;QACjB,KAAK,SAAS;YACZ,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Diagnostic, type Workflow } from "@tscomfy/core";
|
|
2
|
+
/** Options for {@link transpile}. */
|
|
3
|
+
export interface TranspileOptions {
|
|
4
|
+
/** The exported constant holding the workflow, e.g. `txt2img`. */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** Where the generated schema is imported from, e.g. `./comfy/local/index.js`. */
|
|
7
|
+
readonly schemaModule: string;
|
|
8
|
+
/** The schema module's export, e.g. `local`. */
|
|
9
|
+
readonly schemaName: string;
|
|
10
|
+
/** Inline a node used once into its use, unless that node inlines another. Default `true`. */
|
|
11
|
+
readonly inline?: boolean;
|
|
12
|
+
/** Extra header lines, e.g. the command that transpiled the file. */
|
|
13
|
+
readonly header?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/** What {@link transpile} returns. */
|
|
16
|
+
export interface TranspileResult {
|
|
17
|
+
/** The module source, formatted as oxfmt formats it. */
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builder code for `workflow`: an ES module exporting the workflow, built with
|
|
23
|
+
* the generated types over `options.schemaName`. Nodes keep their ids and titles and come in
|
|
24
|
+
* dependency order; inputs equal to their default are left out, since the exporter fills them
|
|
25
|
+
* in. Running it and exporting gives the same prompt as `toApi(workflow)`, except that ids that
|
|
26
|
+
* aren't integers (`40:31`) come in dependency order, where the workflow lists one before a node
|
|
27
|
+
* it depends on. Pure and deterministic.
|
|
28
|
+
*
|
|
29
|
+
* Reports `W_TRANSPILED_AS_RAW` for a node the types can't express, which is written as
|
|
30
|
+
* `raw(…)`, and `E_DEPENDENCY_CYCLE` and `E_DANGLING_LINK` for what no program can build: those
|
|
31
|
+
* nodes, or links, are left out.
|
|
32
|
+
*/
|
|
33
|
+
export declare function transpile(workflow: Workflow, options: TranspileOptions): TranspileResult;
|
|
34
|
+
/**
|
|
35
|
+
* Why {@link transpile} refuses these names, or `undefined` when it takes them: each must pass
|
|
36
|
+
* `exportNameProblem`, they must differ, and the declaration
|
|
37
|
+
* `export const <name>: Workflow<NodesOf<typeof <schemaName>>> =` must fit on one line.
|
|
38
|
+
*/
|
|
39
|
+
export declare function transpileNameProblem(options: Pick<TranspileOptions, "name" | "schemaName">): string | undefined;
|
|
40
|
+
//# sourceMappingURL=transpile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transpile.d.ts","sourceRoot":"","sources":["../src/transpile.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,UAAU,EAUf,KAAK,QAAQ,EACd,MAAM,eAAe,CAAC;AA2BvB,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gDAAgD;IAChD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAC;CAC7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAIxF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,YAAY,CAAC,GACrD,MAAM,GAAG,SAAS,CAiBpB"}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// Workflow → TypeScript (docs/plan.md §10): the builder code that rebuilds a workflow over the
|
|
2
|
+
// generated types. Nodes the types can express become `n.Class(…)` calls; the rest become
|
|
3
|
+
// `raw(…)` with their wire keys, so the program always rebuilds the same prompt.
|
|
4
|
+
//
|
|
5
|
+
// Whether the types can express a node is decided by the same rules `emit-types.ts` writes the
|
|
6
|
+
// types with (`typing.ts`, §6.2, §6.3), so the output compiles: a test type-checks it for every
|
|
7
|
+
// mock and for the corpus. The layout is oxfmt's (`expressions.ts`).
|
|
8
|
+
import { createDiagnostic, exportApi, isOutputRef, stringify, } from "@tscomfy/core";
|
|
9
|
+
import { hardline, indent, join, print, PRINT_WIDTH, textWidth } from "./doc.js";
|
|
10
|
+
import { exportNameProblem, isExportName, TRANSPILED_MODULE_NAMES } from "./export-name.js";
|
|
11
|
+
import { arrayDoc, breaksArray, callDoc, declarationDoc, declarationLayout, declarationLeft, isNumberList, jsonDoc, numbersDoc, objectDoc, objectKey, propertyDoc, scalarDoc, STATEMENT_INDENT, statementDoc, } from "./expressions.js";
|
|
12
|
+
import { CODEGEN_VERSION } from "./generate.js";
|
|
13
|
+
import { headerLines, NameRegistry, quote } from "./names.js";
|
|
14
|
+
import { hasDefault, isRequired } from "./rules.js";
|
|
15
|
+
import { Typing } from "./typing.js";
|
|
16
|
+
import { isList, isPlainObject, references } from "./values.js";
|
|
17
|
+
/**
|
|
18
|
+
* Builder code for `workflow`: an ES module exporting the workflow, built with
|
|
19
|
+
* the generated types over `options.schemaName`. Nodes keep their ids and titles and come in
|
|
20
|
+
* dependency order; inputs equal to their default are left out, since the exporter fills them
|
|
21
|
+
* in. Running it and exporting gives the same prompt as `toApi(workflow)`, except that ids that
|
|
22
|
+
* aren't integers (`40:31`) come in dependency order, where the workflow lists one before a node
|
|
23
|
+
* it depends on. Pure and deterministic.
|
|
24
|
+
*
|
|
25
|
+
* Reports `W_TRANSPILED_AS_RAW` for a node the types can't express, which is written as
|
|
26
|
+
* `raw(…)`, and `E_DEPENDENCY_CYCLE` and `E_DANGLING_LINK` for what no program can build: those
|
|
27
|
+
* nodes, or links, are left out.
|
|
28
|
+
*/
|
|
29
|
+
export function transpile(workflow, options) {
|
|
30
|
+
const problem = transpileNameProblem(options);
|
|
31
|
+
if (problem !== undefined)
|
|
32
|
+
throw new TypeError(`transpile: ${problem}`);
|
|
33
|
+
return new Transpiler(workflow, options).run();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Why {@link transpile} refuses these names, or `undefined` when it takes them: each must pass
|
|
37
|
+
* `exportNameProblem`, they must differ, and the declaration
|
|
38
|
+
* `export const <name>: Workflow<NodesOf<typeof <schemaName>>> =` must fit on one line.
|
|
39
|
+
*/
|
|
40
|
+
export function transpileNameProblem(options) {
|
|
41
|
+
for (const [label, value] of [
|
|
42
|
+
["name", options.name],
|
|
43
|
+
["schema name", options.schemaName],
|
|
44
|
+
]) {
|
|
45
|
+
const problem = exportNameProblem(value);
|
|
46
|
+
if (problem !== undefined)
|
|
47
|
+
return `the ${label} ${JSON.stringify(value)} won't do: ${problem}`;
|
|
48
|
+
}
|
|
49
|
+
if (options.name === options.schemaName) {
|
|
50
|
+
return `the name and the schema name are both ${JSON.stringify(options.name)}`;
|
|
51
|
+
}
|
|
52
|
+
const left = declarationLeft(options);
|
|
53
|
+
// Export names are ASCII, so their length is their width.
|
|
54
|
+
if (textWidth(left) > PRINT_WIDTH) {
|
|
55
|
+
return `the declaration \`${left}\` is wider than ${String(PRINT_WIDTH)} columns`;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
class Transpiler {
|
|
60
|
+
#workflow;
|
|
61
|
+
#options;
|
|
62
|
+
#diagnostics = [];
|
|
63
|
+
#plans = new Map();
|
|
64
|
+
#references = new Map();
|
|
65
|
+
#inlined = new Set();
|
|
66
|
+
#usedSlots = new Map();
|
|
67
|
+
#names;
|
|
68
|
+
#usesN = false;
|
|
69
|
+
#usesRaw = false;
|
|
70
|
+
#layout = "hug";
|
|
71
|
+
constructor(workflow, options) {
|
|
72
|
+
this.#workflow = workflow;
|
|
73
|
+
this.#options = options;
|
|
74
|
+
this.#names = new NameRegistry([...TRANSPILED_MODULE_NAMES, options.name, options.schemaName]);
|
|
75
|
+
}
|
|
76
|
+
run() {
|
|
77
|
+
const nodes = [...this.#workflow.graph.nodes.values()];
|
|
78
|
+
// Before modes: a bypassed or muted raw node is written with its own inputs.
|
|
79
|
+
const exported = exportApi(this.#workflow, { validate: false, meta: false, modes: false });
|
|
80
|
+
// What the exporter can't send, the program can't build either.
|
|
81
|
+
this.#diagnostics.push(...exported.diagnostics);
|
|
82
|
+
const order = this.#order(nodes);
|
|
83
|
+
// A link to an output a node doesn't have can only be written as raw(…).output(slot).
|
|
84
|
+
const missing = new Map();
|
|
85
|
+
for (const node of nodes) {
|
|
86
|
+
for (const ref of references(node.inputs)) {
|
|
87
|
+
const source = this.#workflow.graph.nodes.get(ref.node);
|
|
88
|
+
const outputs = source === undefined ? undefined : this.#workflow.schema.nodes[source.classType]?.outputs;
|
|
89
|
+
if (outputs !== undefined && outputs[ref.slot] === undefined) {
|
|
90
|
+
missing.set(ref.node, ref.slot);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// In dependency order, so a node's sources are decided first: a raw source is `Out<"*">`.
|
|
95
|
+
const typing = new Typing(this.#workflow, (id) => this.#plans.get(id)?.mode);
|
|
96
|
+
for (const node of order) {
|
|
97
|
+
const definition = Object.hasOwn(this.#workflow.schema.nodes, node.classType)
|
|
98
|
+
? this.#workflow.schema.nodes[node.classType]
|
|
99
|
+
: undefined;
|
|
100
|
+
const slot = missing.get(node.id);
|
|
101
|
+
const reason = node.raw || definition === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: slot === undefined
|
|
104
|
+
? typing.problem(node, definition)
|
|
105
|
+
: `output ${String(slot)} is linked, but it has no such output`;
|
|
106
|
+
if (reason !== undefined) {
|
|
107
|
+
this.#diagnostics.push(createDiagnostic("W_TRANSPILED_AS_RAW", [node.id], `Node ${node.id} (${node.classType}) is written as raw(…): ${reason}.`));
|
|
108
|
+
}
|
|
109
|
+
this.#plans.set(node.id, {
|
|
110
|
+
node,
|
|
111
|
+
definition,
|
|
112
|
+
mode: node.raw || definition === undefined || reason !== undefined ? "raw" : "typed",
|
|
113
|
+
wire: exported.prompt[node.id]?.inputs ?? {},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const plans = order.flatMap((node) => this.#plans.get(node.id) ?? []);
|
|
117
|
+
this.#usesN = plans.some((plan) => plan.mode === "typed");
|
|
118
|
+
this.#usesRaw = plans.some((plan) => plan.mode === "raw");
|
|
119
|
+
// Known before any statement: how deep the statements sit decides their layout.
|
|
120
|
+
this.#layout = declarationLayout(this.#options, this.#build(), plans.length === 0);
|
|
121
|
+
const uses = this.#uses(plans);
|
|
122
|
+
const statements = [];
|
|
123
|
+
for (const plan of plans) {
|
|
124
|
+
const statement = this.#statement(plan, uses);
|
|
125
|
+
if (statement !== undefined)
|
|
126
|
+
statements.push(statement);
|
|
127
|
+
}
|
|
128
|
+
return { code: this.#module(statements), diagnostics: this.#diagnostics };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Dependency order, ties broken by graph order. No program can build a cycle, or a node that
|
|
132
|
+
* depends on one: they are reported and left out.
|
|
133
|
+
*/
|
|
134
|
+
#order(nodes) {
|
|
135
|
+
const index = new Map(nodes.map((node, position) => [node.id, position]));
|
|
136
|
+
const dependents = new Map();
|
|
137
|
+
const waiting = new Map();
|
|
138
|
+
for (const node of nodes) {
|
|
139
|
+
const sources = new Set(references(node.inputs).flatMap((ref) => (index.has(ref.node) ? [ref.node] : [])));
|
|
140
|
+
waiting.set(node.id, sources.size);
|
|
141
|
+
for (const source of sources) {
|
|
142
|
+
const list = dependents.get(source);
|
|
143
|
+
if (list === undefined)
|
|
144
|
+
dependents.set(source, [node.id]);
|
|
145
|
+
else
|
|
146
|
+
list.push(node.id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const ready = new MinHeap();
|
|
150
|
+
for (const node of nodes)
|
|
151
|
+
if (waiting.get(node.id) === 0)
|
|
152
|
+
ready.push(index.get(node.id) ?? 0);
|
|
153
|
+
const order = [];
|
|
154
|
+
for (let at = ready.pop(); at !== undefined; at = ready.pop()) {
|
|
155
|
+
const node = nodes[at];
|
|
156
|
+
if (node === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
order.push(node);
|
|
159
|
+
for (const dependent of dependents.get(node.id) ?? []) {
|
|
160
|
+
const left = (waiting.get(dependent) ?? 0) - 1;
|
|
161
|
+
waiting.set(dependent, left);
|
|
162
|
+
if (left === 0)
|
|
163
|
+
ready.push(index.get(dependent) ?? 0);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const left = nodes.filter((node) => (waiting.get(node.id) ?? 0) > 0);
|
|
167
|
+
const cyclic = inCycles(left);
|
|
168
|
+
for (const node of left) {
|
|
169
|
+
this.#diagnostics.push(createDiagnostic("E_DEPENDENCY_CYCLE", [node.id], cyclic.has(node.id)
|
|
170
|
+
? `Node ${node.id} (${node.classType}) depends on itself through its links; leaving it out.`
|
|
171
|
+
: `Node ${node.id} (${node.classType}) depends on nodes in a cycle; leaving it out.`));
|
|
172
|
+
}
|
|
173
|
+
return order;
|
|
174
|
+
}
|
|
175
|
+
/** How often each output (`id`, `slot`) is used by what will be printed. */
|
|
176
|
+
#uses(plans) {
|
|
177
|
+
const uses = new Map();
|
|
178
|
+
for (const plan of plans) {
|
|
179
|
+
// A raw node prints its exported inputs, which may drop what the exporter can't send.
|
|
180
|
+
const refs = plan.mode === "raw"
|
|
181
|
+
? Object.values(plan.wire).flatMap((value) => isLink(value) ? [{ node: value[0], slot: value[1] }] : [])
|
|
182
|
+
: references(plan.node.inputs);
|
|
183
|
+
for (const ref of refs) {
|
|
184
|
+
const key = slotKey(ref.node, ref.slot);
|
|
185
|
+
uses.set(key, (uses.get(key) ?? 0) + 1);
|
|
186
|
+
const slots = this.#usedSlots.get(ref.node);
|
|
187
|
+
if (slots === undefined)
|
|
188
|
+
this.#usedSlots.set(ref.node, new Set([ref.slot]));
|
|
189
|
+
else
|
|
190
|
+
slots.add(ref.slot);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return uses;
|
|
194
|
+
}
|
|
195
|
+
/** The statement for one node, or `undefined` when it is inlined into its only use. */
|
|
196
|
+
#statement(plan, uses) {
|
|
197
|
+
const { node, definition } = plan;
|
|
198
|
+
const options = this.#nodeOptions(node);
|
|
199
|
+
if (plan.mode === "raw" || definition === undefined) {
|
|
200
|
+
const call = callDoc("raw", [quote(node.classType), this.#wireObject(plan), options]);
|
|
201
|
+
const slots = this.#usedSlots.get(node.id);
|
|
202
|
+
if (slots === undefined)
|
|
203
|
+
return [call, ";"];
|
|
204
|
+
const name = this.#claim(camel(node.classType, "node"));
|
|
205
|
+
for (const slot of slots) {
|
|
206
|
+
this.#references.set(slotKey(node.id, slot), [name, ".output(", String(slot), ")"]);
|
|
207
|
+
}
|
|
208
|
+
return statementDoc(`const ${name}`, call);
|
|
209
|
+
}
|
|
210
|
+
const callee = /^[A-Za-z_$][\w$]*$/.test(node.classType)
|
|
211
|
+
? `n.${node.classType}`
|
|
212
|
+
: `n[${quote(node.classType)}]`;
|
|
213
|
+
const call = callDoc(callee, [this.#inputsObject(definition.inputs, node.inputs).doc, options]);
|
|
214
|
+
const { outputs } = definition;
|
|
215
|
+
const used = outputs.filter((output) => (uses.get(slotKey(node.id, output.index)) ?? 0) > 0);
|
|
216
|
+
if (used.length === 0)
|
|
217
|
+
return [call, ";"];
|
|
218
|
+
if (outputs.length === 1) {
|
|
219
|
+
const key = slotKey(node.id, 0);
|
|
220
|
+
// Used once, by a node written later, and inlining nothing itself: written in place. Not
|
|
221
|
+
// with an id that isn't an array index: such keys keep the order nodes are created in.
|
|
222
|
+
if (this.#options.inline !== false &&
|
|
223
|
+
isArrayIndex(node.id) &&
|
|
224
|
+
uses.get(key) === 1 &&
|
|
225
|
+
references(node.inputs).every((ref) => !this.#inlined.has(ref.node))) {
|
|
226
|
+
this.#inlined.add(node.id);
|
|
227
|
+
this.#references.set(key, call);
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const name = this.#claim(camel(outputs[0]?.name ?? node.classType, "output"));
|
|
231
|
+
this.#references.set(key, name);
|
|
232
|
+
return statementDoc(`const ${name}`, call);
|
|
233
|
+
}
|
|
234
|
+
// Several outputs: destructure the ones used, by their names, while the pattern fits on its
|
|
235
|
+
// line (Prettier would break a longer one); otherwise index one variable.
|
|
236
|
+
const last = Math.max(...used.map((output) => output.index));
|
|
237
|
+
const bindings = outputs
|
|
238
|
+
.slice(0, last + 1)
|
|
239
|
+
.map((output) => (used.includes(output) ? this.#claim(camel(output.name, "output")) : ""));
|
|
240
|
+
const pattern = `const [${bindings.join(", ")}] =`;
|
|
241
|
+
if (STATEMENT_INDENT[this.#layout] + textWidth(pattern) <= PRINT_WIDTH) {
|
|
242
|
+
for (const [index, binding] of bindings.entries()) {
|
|
243
|
+
if (binding !== "")
|
|
244
|
+
this.#references.set(slotKey(node.id, index), binding);
|
|
245
|
+
}
|
|
246
|
+
return statementDoc(`const [${bindings.join(", ")}]`, call);
|
|
247
|
+
}
|
|
248
|
+
const name = this.#claim(camel(node.classType, "node"));
|
|
249
|
+
for (const output of used) {
|
|
250
|
+
this.#references.set(slotKey(node.id, output.index), [name, "[", String(output.index), "]"]);
|
|
251
|
+
}
|
|
252
|
+
return statementDoc(`const ${name}`, call);
|
|
253
|
+
}
|
|
254
|
+
#claim(base) {
|
|
255
|
+
// A word no export may be gets a suffix, except the module's own names, which the registry
|
|
256
|
+
// holds: those get a number, like any other taken name.
|
|
257
|
+
const usable = isExportName(base) || TRANSPILED_MODULE_NAMES.includes(base);
|
|
258
|
+
return this.#names.claim(usable ? base : `${base}Node`);
|
|
259
|
+
}
|
|
260
|
+
/** `{ id: "5" }`, and the title and mode if there are. */
|
|
261
|
+
#nodeOptions(node) {
|
|
262
|
+
return objectDoc([
|
|
263
|
+
propertyDoc("id", quote(node.id)),
|
|
264
|
+
...(node.title === undefined ? [] : [propertyDoc("title", quote(node.title))]),
|
|
265
|
+
...(node.mode === undefined ? [] : [propertyDoc("mode", quote(node.mode))]),
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Inputs as an object literal, in the order given (the exporter orders them by the schema).
|
|
270
|
+
* Values equal to their default are left out at every level, as the exporter fills them in.
|
|
271
|
+
* Also returns how many properties were written.
|
|
272
|
+
*/
|
|
273
|
+
#inputsObject(definitions, inputs) {
|
|
274
|
+
const known = new Map(definitions.map((input) => [input.name, input]));
|
|
275
|
+
const properties = [];
|
|
276
|
+
for (const [name, value] of Object.entries(inputs)) {
|
|
277
|
+
const definition = known.get(name);
|
|
278
|
+
// The exporter fills defaults in; but what the types require must be written.
|
|
279
|
+
if (definition !== undefined && !isRequired(definition) && equalsDefault(definition, value)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const key = objectKey(name);
|
|
283
|
+
const doc = this.#value(value, definition);
|
|
284
|
+
// `{ model }` rather than `{ model: model }`.
|
|
285
|
+
properties.push(doc === key ? key : propertyDoc(key, doc));
|
|
286
|
+
}
|
|
287
|
+
return { doc: objectDoc(properties), count: properties.length };
|
|
288
|
+
}
|
|
289
|
+
/** A structured value as an expression. */
|
|
290
|
+
#value(value, definition) {
|
|
291
|
+
if (isOutputRef(value))
|
|
292
|
+
return this.#reference(value.node, value.slot);
|
|
293
|
+
if (isList(value)) {
|
|
294
|
+
if (definition?.kind === "dynamicGroup") {
|
|
295
|
+
const rows = value.map((row) => isPlainObject(row)
|
|
296
|
+
? this.#inputsObject(definition.fields, row)
|
|
297
|
+
: { doc: this.#value(row, undefined), count: 0 });
|
|
298
|
+
// Rows as printed, defaults left out, decide whether the list must break.
|
|
299
|
+
const breaks = rows.length > 1 && rows.every((row) => row.count > 1);
|
|
300
|
+
return arrayDoc(rows.map((row) => row.doc), breaks);
|
|
301
|
+
}
|
|
302
|
+
const element = definition?.kind === "autogrow" ? definition.template : undefined;
|
|
303
|
+
if (isNumberList(value))
|
|
304
|
+
return numbersDoc(value);
|
|
305
|
+
return arrayDoc(value.map((item) => this.#value(item, element)), breaksArray(value));
|
|
306
|
+
}
|
|
307
|
+
if (isPlainObject(value)) {
|
|
308
|
+
if (definition?.kind === "dynamicCombo" && typeof value["option"] === "string") {
|
|
309
|
+
const key = value["option"];
|
|
310
|
+
const chosen = definition.options.find((option) => option.key === key);
|
|
311
|
+
const children = value["inputs"];
|
|
312
|
+
const inputs = isPlainObject(children)
|
|
313
|
+
? this.#inputsObject(chosen?.inputs ?? [], children)
|
|
314
|
+
: undefined;
|
|
315
|
+
// With every option input at its default, the key alone says the same.
|
|
316
|
+
if (inputs === undefined || inputs.count === 0)
|
|
317
|
+
return quote(key);
|
|
318
|
+
return objectDoc([propertyDoc("option", quote(key)), propertyDoc("inputs", inputs.doc)]);
|
|
319
|
+
}
|
|
320
|
+
const element = definition?.kind === "autogrow" ? definition.template : undefined;
|
|
321
|
+
return objectDoc(Object.entries(value).map(([key, item]) => propertyDoc(objectKey(key), this.#value(item, element))));
|
|
322
|
+
}
|
|
323
|
+
return scalarDoc(value);
|
|
324
|
+
}
|
|
325
|
+
/** A raw node's inputs: wire keys, links as references, lists unwrapped (raw wraps them). */
|
|
326
|
+
#wireObject(plan) {
|
|
327
|
+
return objectDoc(Object.entries(plan.wire).flatMap(([key, value]) => {
|
|
328
|
+
if (isLink(value)) {
|
|
329
|
+
const [node, slot] = value;
|
|
330
|
+
if (!this.#workflow.graph.nodes.has(node)) {
|
|
331
|
+
this.#dangling(plan.node.id, key, node);
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
return [propertyDoc(objectKey(key), this.#reference(node, slot))];
|
|
335
|
+
}
|
|
336
|
+
const unwrapped = isWrappedList(value) ? value["__value__"] : value;
|
|
337
|
+
return [propertyDoc(objectKey(key), jsonDoc(unwrapped ?? null))];
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
#reference(node, slot) {
|
|
341
|
+
const reference = this.#references.get(slotKey(node, slot));
|
|
342
|
+
// Sources are written before their uses, and left-out nodes have no uses written.
|
|
343
|
+
if (reference === undefined)
|
|
344
|
+
throw new Error(`transpile: no reference for ${node}:${String(slot)}`);
|
|
345
|
+
return reference;
|
|
346
|
+
}
|
|
347
|
+
#dangling(id, key, node) {
|
|
348
|
+
this.#diagnostics.push(createDiagnostic("E_DANGLING_LINK", [id, "inputs", key], `Input ${key} of node ${id} links to node ${node}, which the workflow doesn't have; leaving it out.`));
|
|
349
|
+
}
|
|
350
|
+
/** The build function's head: `({ n, raw }) =>` with what the statements use. */
|
|
351
|
+
#build() {
|
|
352
|
+
const context = [...(this.#usesN ? ["n"] : []), ...(this.#usesRaw ? ["raw"] : [])];
|
|
353
|
+
return `(${context.length === 0 ? "" : `{ ${context.join(", ")} }`}) =>`;
|
|
354
|
+
}
|
|
355
|
+
#module(statements) {
|
|
356
|
+
const header = [
|
|
357
|
+
`Transpiled by @tscomfy/codegen ${CODEGEN_VERSION}.`,
|
|
358
|
+
...(this.#options.header ?? []),
|
|
359
|
+
];
|
|
360
|
+
const { schemaName } = this.#options;
|
|
361
|
+
const block = statements.length === 0
|
|
362
|
+
? "{}"
|
|
363
|
+
: ["{", indent([hardline, join(hardline, statements)]), hardline, "}"];
|
|
364
|
+
return [
|
|
365
|
+
...headerLines(header),
|
|
366
|
+
"",
|
|
367
|
+
'import { type NodesOf, type Workflow, workflow } from "@tscomfy/core";',
|
|
368
|
+
`import { ${schemaName} } from ${quote(this.#options.schemaModule)};`,
|
|
369
|
+
"",
|
|
370
|
+
print(declarationDoc(this.#options, this.#build(), block, this.#layout), PRINT_WIDTH),
|
|
371
|
+
"",
|
|
372
|
+
].join("\n");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** The nodes of `nodes` that lie on a cycle among them (Tarjan's algorithm, iteratively). */
|
|
376
|
+
function inCycles(nodes) {
|
|
377
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
378
|
+
const edges = new Map(nodes.map((node) => [
|
|
379
|
+
node.id,
|
|
380
|
+
[...new Set(references(node.inputs).map((ref) => ref.node))].filter((id) => ids.has(id)),
|
|
381
|
+
]));
|
|
382
|
+
const order = new Map();
|
|
383
|
+
const low = new Map();
|
|
384
|
+
const stack = [];
|
|
385
|
+
const onStack = new Set();
|
|
386
|
+
const cyclic = new Set();
|
|
387
|
+
let counter = 0;
|
|
388
|
+
for (const start of ids) {
|
|
389
|
+
if (order.has(start))
|
|
390
|
+
continue;
|
|
391
|
+
const frames = [{ id: start, next: 0 }];
|
|
392
|
+
order.set(start, counter);
|
|
393
|
+
low.set(start, counter++);
|
|
394
|
+
stack.push(start);
|
|
395
|
+
onStack.add(start);
|
|
396
|
+
while (frames.length > 0) {
|
|
397
|
+
const frame = frames.at(-1);
|
|
398
|
+
if (frame === undefined)
|
|
399
|
+
break;
|
|
400
|
+
const targets = edges.get(frame.id) ?? [];
|
|
401
|
+
const target = targets[frame.next++];
|
|
402
|
+
if (target !== undefined) {
|
|
403
|
+
if (!order.has(target)) {
|
|
404
|
+
order.set(target, counter);
|
|
405
|
+
low.set(target, counter++);
|
|
406
|
+
stack.push(target);
|
|
407
|
+
onStack.add(target);
|
|
408
|
+
frames.push({ id: target, next: 0 });
|
|
409
|
+
}
|
|
410
|
+
else if (onStack.has(target)) {
|
|
411
|
+
low.set(frame.id, Math.min(low.get(frame.id) ?? 0, order.get(target) ?? 0));
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
frames.pop();
|
|
416
|
+
const parent = frames.at(-1);
|
|
417
|
+
if (parent !== undefined) {
|
|
418
|
+
low.set(parent.id, Math.min(low.get(parent.id) ?? 0, low.get(frame.id) ?? 0));
|
|
419
|
+
}
|
|
420
|
+
if (low.get(frame.id) !== order.get(frame.id))
|
|
421
|
+
continue;
|
|
422
|
+
const component = [];
|
|
423
|
+
for (let id = stack.pop(); id !== undefined; id = stack.pop()) {
|
|
424
|
+
onStack.delete(id);
|
|
425
|
+
component.push(id);
|
|
426
|
+
if (id === frame.id)
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const self = (edges.get(frame.id) ?? []).includes(frame.id);
|
|
430
|
+
if (component.length > 1 || self)
|
|
431
|
+
for (const id of component)
|
|
432
|
+
cyclic.add(id);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return cyclic;
|
|
436
|
+
}
|
|
437
|
+
/** Whether a key is an array index, which objects order numerically rather than by creation. */
|
|
438
|
+
function isArrayIndex(key) {
|
|
439
|
+
return /^(?:0|[1-9][0-9]{0,9})$/.test(key) && Number(key) < 2 ** 32 - 1;
|
|
440
|
+
}
|
|
441
|
+
/** A binary min-heap of graph positions, for the dependency order's ready list. */
|
|
442
|
+
class MinHeap {
|
|
443
|
+
#items = [];
|
|
444
|
+
push(value) {
|
|
445
|
+
const items = this.#items;
|
|
446
|
+
items.push(value);
|
|
447
|
+
for (let at = items.length - 1; at > 0;) {
|
|
448
|
+
const parent = (at - 1) >> 1;
|
|
449
|
+
const [up, down] = [items[at] ?? 0, items[parent] ?? 0];
|
|
450
|
+
if (down <= up)
|
|
451
|
+
break;
|
|
452
|
+
items[at] = down;
|
|
453
|
+
items[parent] = up;
|
|
454
|
+
at = parent;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
pop() {
|
|
458
|
+
const items = this.#items;
|
|
459
|
+
const top = items[0];
|
|
460
|
+
const last = items.pop();
|
|
461
|
+
if (top === undefined || last === undefined || items.length === 0)
|
|
462
|
+
return top;
|
|
463
|
+
items[0] = last;
|
|
464
|
+
for (let at = 0;;) {
|
|
465
|
+
const [left, right] = [2 * at + 1, 2 * at + 2];
|
|
466
|
+
let smallest = at;
|
|
467
|
+
if (left < items.length && (items[left] ?? 0) < (items[smallest] ?? 0))
|
|
468
|
+
smallest = left;
|
|
469
|
+
if (right < items.length && (items[right] ?? 0) < (items[smallest] ?? 0))
|
|
470
|
+
smallest = right;
|
|
471
|
+
if (smallest === at)
|
|
472
|
+
break;
|
|
473
|
+
const [a, b] = [items[at] ?? 0, items[smallest] ?? 0];
|
|
474
|
+
items[at] = b;
|
|
475
|
+
items[smallest] = a;
|
|
476
|
+
at = smallest;
|
|
477
|
+
}
|
|
478
|
+
return top;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/** Whether a value equals what the exporter would send for the input when left out. */
|
|
482
|
+
function equalsDefault(input, value) {
|
|
483
|
+
if (!hasDefault(input) || isOutputRef(value))
|
|
484
|
+
return false;
|
|
485
|
+
const fallback = "default" in input ? input.default : undefined;
|
|
486
|
+
return fallback !== undefined && stringify(fallback) === stringify(toJson(value));
|
|
487
|
+
}
|
|
488
|
+
/** A structured value without references, as JSON; references compare as `null`. */
|
|
489
|
+
function toJson(value) {
|
|
490
|
+
if (isOutputRef(value))
|
|
491
|
+
return null;
|
|
492
|
+
if (isList(value))
|
|
493
|
+
return value.map(toJson);
|
|
494
|
+
if (isPlainObject(value)) {
|
|
495
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toJson(item)]));
|
|
496
|
+
}
|
|
497
|
+
return value;
|
|
498
|
+
}
|
|
499
|
+
function slotKey(node, slot) {
|
|
500
|
+
return `${node}\u0000${String(slot)}`;
|
|
501
|
+
}
|
|
502
|
+
function isLink(value) {
|
|
503
|
+
return (isList(value) &&
|
|
504
|
+
value.length === 2 &&
|
|
505
|
+
typeof value[0] === "string" &&
|
|
506
|
+
typeof value[1] === "number");
|
|
507
|
+
}
|
|
508
|
+
function isWrappedList(value) {
|
|
509
|
+
return (typeof value === "object" &&
|
|
510
|
+
value !== null &&
|
|
511
|
+
!isList(value) &&
|
|
512
|
+
Object.keys(value).length === 1 &&
|
|
513
|
+
isList(value["__value__"]));
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* A camelCase variable name from a ComfyUI name, split at case changes: `LATENT` → `latent`,
|
|
517
|
+
* `VAEDecode` → `vaeDecode`, `CLIP_VISION` → `clipVision`. Never empty, never a leading digit.
|
|
518
|
+
*/
|
|
519
|
+
function camel(name, fallback) {
|
|
520
|
+
const words = name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+/g) ?? [];
|
|
521
|
+
const joined = words
|
|
522
|
+
.map((word, index) => {
|
|
523
|
+
const lower = word.toLowerCase();
|
|
524
|
+
return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
525
|
+
})
|
|
526
|
+
.join("");
|
|
527
|
+
if (joined === "")
|
|
528
|
+
return fallback;
|
|
529
|
+
return /^[0-9]/.test(joined) ? `${fallback}${joined}` : joined;
|
|
530
|
+
}
|
|
531
|
+
//# sourceMappingURL=transpile.js.map
|