diff --git a/__build__.py b/__build__.py index 3ceae13..ba1ca04 100644 --- a/__build__.py +++ b/__build__.py @@ -6,26 +6,30 @@ from shutil import rmtree, copytree, ignore_patterns from glob import glob import time import re +import argparse from py.log import COLORS from py.config import RGTHREE_CONFIG start = time.time() +parser = argparse.ArgumentParser() +parser.add_argument("-t", "--with-tests", default=False, action="store_true") +args = parser.parse_args() + THIS_DIR = os.path.dirname(os.path.abspath(__file__)) DIR_SRC_WEB = os.path.abspath(f'{THIS_DIR}/src_web/') DIR_WEB = os.path.abspath(f'{THIS_DIR}/web/') DIR_WEB_COMFYUI = os.path.abspath(f'{DIR_WEB}/comfyui/') -rmtree(DIR_WEB) - def log_step(msg=None, status=None): """ Logs a step keeping track of timing and initial msg. """ global step_msg # pylint: disable=W0601 global step_start # pylint: disable=W0601 if msg: - step_msg = f'▻ [Starting] {msg}...' + tag=f'{COLORS["YELLOW"]}[ Notice ]' if status == 'Notice' else f'{COLORS["RESET"]}[Starting]' + step_msg = f'▻ {tag}{COLORS["RESET"]} {msg}...' step_start = time.time() print(step_msg, end="\r") elif status: @@ -34,12 +38,11 @@ def log_step(msg=None, status=None): status_msg=f'{COLORS["RED"]}⤫ {status}{COLORS["RESET"]}' else: status_msg=f'{COLORS["BRIGHT_GREEN"]}🗸 {status}{COLORS["RESET"]}' - print( - f'{step_msg.ljust(50, ".")} {COLORS["BRIGHT_GREEN"]}🗸 {status}{COLORS["RESET"]} ({step_time}s)' - ) + print(f'{step_msg.ljust(64, ".")} {status_msg} ({step_time}s)') log_step(msg='Copying web directory') +rmtree(DIR_WEB) copytree(DIR_SRC_WEB, DIR_WEB, ignore=ignore_patterns("typings*", "*.ts", "*.scss")) log_step(status="Done") @@ -47,6 +50,18 @@ log_step(msg='TypeScript') checked = subprocess.run(["node", "./node_modules/typescript/bin/tsc"], check=True) log_step(status="Done") +if args.with_tests: + log_step(msg='Removing directories (KEEPING TESTING)', status="Notice") +else: + log_step(msg='Removing uneeded directories') + test_path=os.path.join(DIR_WEB, 'comfyui', 'tests') + if os.path.exists(test_path): + rmtree(test_path) + rmtree(os.path.join(DIR_WEB, 'comfyui', 'testing')) +# Always remove the dummy scripts_comfy directory +rmtree(os.path.join(DIR_WEB, 'scripts_comfy')) +log_step(status="Done") + scsss = glob(os.path.join(DIR_SRC_WEB, "**", "*.scss"), recursive=True) log_step(msg=f'SASS for {len(scsss)} files') scsss = [i.replace(THIS_DIR, '.') for i in scsss] @@ -64,19 +79,19 @@ log_step(status="Done") # "src_web/common" directory, but then need to rewrite the comfyui JS files to load from # "../../rgthree/common" (which we map correctly in rgthree_server.py). log_step(msg='Cleaning Imports') -print('▻ [Starting] Cleaning Imports...', end="\r") -web_subfolders = [f.name for f in os.scandir(DIR_WEB) if f.is_dir()] -for subfolder in web_subfolders: - js_files = glob(os.path.join(DIR_WEB, subfolder, '*.js'), recursive=True) - for file in js_files: - with open(file, 'r', encoding="utf-8") as f: - filedata = f.read() - if subfolder == 'comfyui': - filedata = re.sub(r'(from\s+["\'])rgthree/', '\\1../../rgthree/', filedata) - else: - filedata = re.sub(r'(from\s+["\'])rgthree/', '\\1../', filedata) - with open(file, 'w', encoding="utf-8") as f: - f.write(filedata) +js_files = glob(os.path.join(DIR_WEB, '**', '*.js'), recursive=True) +for file in js_files: + rel_path = file.replace(f'{DIR_WEB}/', "") + with open(file, 'r', encoding="utf-8") as f: + filedata = f.read() + num = rel_path.count(os.sep) + if rel_path.startswith('comfyui'): + filedata = re.sub(r'(from\s+["\'])rgthree/', f'\\1{"../" * (num + 1)}rgthree/', filedata) + filedata = re.sub(r'(from\s+["\'])scripts/', f'\\1{"../" * (num + 1)}scripts/', filedata) + else: + filedata = re.sub(r'(from\s+["\'])rgthree/', f'\\1{"../" * num}', filedata) + with open(file, 'w', encoding="utf-8") as f: + f.write(filedata) log_step(status="Done") print(f'Finished all in {round(time.time() - start, 3)}s') diff --git a/py/log.py b/py/log.py index 0653020..9215e89 100644 --- a/py/log.py +++ b/py/log.py @@ -18,14 +18,14 @@ COLORS = { 'BRIGHT_CYAN': '\33[96m', 'BRIGHT_WHITE': '\33[97m', # Styles. - 'RESET': '\33[0m', - 'BOLD': '\33[1m', + 'RESET': '\33[00m', + 'BOLD': '\33[01m', 'NORMAL': '\33[22m', - 'ITALIC': '\33[3m', - 'UNDERLINE': '\33[4m', - 'BLINK': '\33[5m', - 'BLINK2': '\33[6m', - 'SELECTED': '\33[7m', + 'ITALIC': '\33[03m', + 'UNDERLINE': '\33[04m', + 'BLINK': '\33[05m', + 'BLINK2': '\33[06m', + 'SELECTED': '\33[07m', # Backgrounds 'BG_BLACK': '\33[40m', 'BG_RED': '\33[41m', diff --git a/src_web/comfyui/testing/comfyui_env.ts b/src_web/comfyui/testing/comfyui_env.ts new file mode 100644 index 0000000..7bbf4f1 --- /dev/null +++ b/src_web/comfyui/testing/comfyui_env.ts @@ -0,0 +1,67 @@ +import { app } from "scripts/app.js"; +import { NodeTypesString } from "../constants.js"; +import { wait } from "rgthree/common/shared_utils.js"; +import type { LGraphNode } from "typings/litegraph.js"; + +type addNodeOptions = { + placement?: string; +}; + +/** + * A testing environment to make setting up, clearing, and queuing more predictable in an + * integration test environment. + */ +export class ComfyUITestEnvironment { + private lastNode: LGraphNode | null = null; + private maxY = 0; + + constructor() {} + + async addNode(nodeString: string, options: addNodeOptions = {}) { + const [canvas, graph] = [app.canvas, app.graph]; + const node = LiteGraph.createNode(nodeString); + let x = 0; + let y = 30; + if (this.lastNode) { + const placement = options.placement || "right"; + if (placement === "under") { + x = this.lastNode.pos[0]; + y = this.lastNode.pos[1] + this.lastNode.size[1] + 30; + } else if (placement === "right") { + x = this.lastNode.pos[0] + this.lastNode.size[0] + 100; + y = this.lastNode.pos[1]; + } else if (placement === "start") { + x = 0; + y = this.maxY + 50; + } + } + canvas.graph.add(node); + node.pos = [x, y]; + canvas.selectNode(node); + app.graph.setDirtyCanvas(true, true); + await wait(); + this.lastNode = node; + this.maxY = Math.max(this.maxY, y + this.lastNode.size[1]); + return (this.lastNode = node); + } + + async clear() { + app.clean(); + app.graph.clear(); + const nodeConfig = await this.addNode(NodeTypesString.KSAMPLER_CONFIG); + const displayAny = await this.addNode(NodeTypesString.DISPLAY_ANY); + nodeConfig.widgets[0]!.value = Math.round(Math.random() * 100); + nodeConfig.connect(0, displayAny, 0); + await this.queuePrompt(); + app.clean(); + app.graph.clear(); + this.lastNode = null; + this.maxY = 0; + await wait(); + } + + async queuePrompt() { + await app.queuePrompt(); + await wait(150); + } +} diff --git a/src_web/comfyui/testing/runner.ts b/src_web/comfyui/testing/runner.ts new file mode 100644 index 0000000..4992733 --- /dev/null +++ b/src_web/comfyui/testing/runner.ts @@ -0,0 +1,133 @@ +/** + * @fileoverview A set of methods that mimic a bit of the Jasmine testing library, but simpler and + * more succinct for manipulating a comfy integration test. + */ +import { wait } from "rgthree/common/shared_utils.js"; + +type TestContext = { + label?: string; + beforeEach?: Function[]; +}; + +let contexts: TestContext[] = []; + +export function describe(label: string, fn: Function) { + return async () => { + await describeRun(label, fn); + }; +} + +export async function describeRun(label: string, fn: Function) { + await wait(); + contexts.push({ label }); + console.group(`[Start] ${contexts[contexts.length - 1]!.label}`); + await fn(); + contexts.pop(); + console.groupEnd(); +} + +export async function should(declaration: string, fn: Function) { + if (!contexts[contexts.length - 1]) { + throw Error("Called should outside of a describe."); + } + console.group(`...should ${declaration}`); + try { + for (const context of contexts) { + for (const beforeEachFn of context?.beforeEach || []) { + await beforeEachFn(); + } + } + await fn(); + } catch (e: any) { + fail(e); + } + console.groupEnd(); +} + +export async function beforeEach(fn: Function) { + if (!contexts[contexts.length - 1]) { + throw Error("Called beforeEach outside of a describe."); + } + const last = contexts[contexts.length - 1]!; + last.beforeEach = last?.beforeEach || []; + last.beforeEach.push(fn); +} + +export function fail(e: Error) { + log(`X Failure: ${e}`, "color:#600; background:#fdd; padding: 2px 6px;"); +} + +function log(msg: string, styles: string) { + if (styles) { + console.log(`%c ${msg}`, styles); + } else { + console.log(msg); + } +} + +class Expectation { + private propertyLabel: string | null = ""; + private expectedLabel: string | null = ""; + private expectedFn!: (v: any) => boolean; + private value: any; + + constructor(value: any) { + this.value = value; + } + + toBe(labelOrExpected: any, maybeExpected?: any) { + const expected = maybeExpected !== undefined ? maybeExpected : labelOrExpected; + this.propertyLabel = maybeExpected !== undefined ? labelOrExpected : null; + this.expectedLabel = JSON.stringify(expected); + this.expectedFn = (v) => v == expected; + return this.toBeEval(); + } + toBeUndefined(propertyLabel: string) { + this.expectedFn = (v) => v === undefined; + this.propertyLabel = propertyLabel || ""; + this.expectedLabel = "undefined"; + return this.toBeEval(true); + } + toBeNullOrUndefined(propertyLabel: string) { + this.expectedFn = (v) => v == null; + this.propertyLabel = propertyLabel || ""; + this.expectedLabel = "null or undefined"; + return this.toBeEval(true); + } + toBeTruthy(propertyLabel: string) { + this.expectedFn = (v) => !v; + this.propertyLabel = propertyLabel || ""; + this.expectedLabel = "truthy"; + return this.toBeEval(false); + } + toBeANumber(propertyLabel: string) { + this.expectedFn = (v) => typeof v === "number"; + this.propertyLabel = propertyLabel || ""; + this.expectedLabel = "a number"; + return this.toBeEval(); + } + toBeEval(strict = false) { + let evaluation = this.expectedFn(this.value); + let msg = `Expected ${this.propertyLabel ? this.propertyLabel + " to be " : ""}${ + this.expectedLabel + }`; + msg += evaluation ? "." : `, but was ${JSON.stringify(this.value)}`; + this.log(evaluation, msg); + return evaluation; + } + log(value: boolean, msg: string) { + if (value) { + log(`🗸 ${msg}`, "color:#060; background:#cec; padding: 2px 6px;"); + } else { + log(`X ${msg}`, "color:#600; background:#fdd; padding: 2px 6px;"); + } + } +} + +export function expect(value: any, msg?: string) { + const expectation = new Expectation(value); + if (msg) { + expectation.log(value, msg); + } + return expectation; +} diff --git a/src_web/scripts_comfy/README.md b/src_web/scripts_comfy/README.md new file mode 100644 index 0000000..9b5b422 --- /dev/null +++ b/src_web/scripts_comfy/README.md @@ -0,0 +1,6 @@ +Here lies dummy ts files that decalre/export ComfyUI's own scripts files as typed types w/o needing +to symlink to the actual implementation. + +Actual code in the comfyui/ directory can import these like `import {app} from "/scripts/app.js"` +and have access to `app` as the fully typed `ComfyApp`. The `__build__.py` script will rewrite these +to the relative browser path. \ No newline at end of file diff --git a/src_web/scripts_comfy/app.ts b/src_web/scripts_comfy/app.ts new file mode 100644 index 0000000..f829090 --- /dev/null +++ b/src_web/scripts_comfy/app.ts @@ -0,0 +1,7 @@ +import { ComfyApp } from "../typings/comfy"; + +/** + * A dummy ComfyApp that we can import from our code, which we'll rewrite later to the comfyui + * hosted app.js + */ +export declare const app: ComfyApp; diff --git a/src_web/typings/comfy.d.ts b/src_web/typings/comfy.d.ts index 4c0a35d..46246c7 100644 --- a/src_web/typings/comfy.d.ts +++ b/src_web/typings/comfy.d.ts @@ -1,11 +1,17 @@ -import type { LGraphNode, IWidget, SerializedLGraphNode, LGraph } from "./litegraph"; +import type { LGraphNode, IWidget, SerializedLGraphNode, LGraph, LGraphCanvas, LiteGraph as TLiteGraph } from "./litegraph"; import type {Constructor, SerializedGraph} from './index'; +declare global { + const LiteGraph: typeof TLiteGraph; +} + // @rgthree: Types on ComfyApp as needed. export interface ComfyApp { extensions: ComfyExtension[]; - queuePrompt(number: number, batchCount = 1): void; + async queuePrompt(number?: number, batchCount = 1): Promise; graph: LGraph; + canvas: LGraphCanvas; + clean() : void; registerExtension(extension: ComfyExtension): void; } @@ -70,11 +76,12 @@ export interface ComfyExtension { >; /** * Allows the extension to add additional handling to the node before it is registered with LGraph + * @rgthree changed nodeType from `typeof LGraphNode` to `ComfyNodeConstructor` * @param nodeType The node class (not an instance) * @param nodeData The original node object info config object * @param app The ComfyUI app instance */ - beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise; + beforeRegisterNodeDef?(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo, app: ComfyApp): Promise; /** * Allows the extension to register additional nodes with LGraph after standard nodes are added * @param app The ComfyUI app instance diff --git a/tsconfig.json b/tsconfig.json index 48c26e0..47e3cca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "paths": { "typings/*": ["src_web/typings/*"], "rgthree/common/*": ["src_web/common/*"], - "node_modules": ["node_modules/*"] + "node_modules": ["node_modules/*"], + "scripts/*": ["src_web/scripts_comfy/*"], }, "outDir": "web/", "removeComments": true, diff --git a/web/common/media/svgs.js b/web/common/media/svgs.js index f0983d6..ace21b2 100644 --- a/web/common/media/svgs.js +++ b/web/common/media/svgs.js @@ -1,156 +1,156 @@ import { createElement as $el } from "../utils_dom.js"; -export const logoRgthree = `