diff --git a/package.json b/package.json index 52302fc8..afff315f 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "stablestudio-plugin-stability": "yarn workspace @stability/stablestudio-plugin-stability", "stablestudio-plugin-webgpu": "yarn workspace @stability/stablestudio-plugin-webgpu", "stablestudio-plugin-webui": "yarn workspace @stability/stablestudio-plugin-webui", + "stablestudio-plugin-comfy": "yarn workspace @stability/stablestudio-plugin-comfy", "stablestudio-ui": "yarn workspace @stability/stablestudio-ui", "dev:use-example-plugin": "cross-env VITE_USE_EXAMPLE_PLUGIN=true yarn dev", + "dev:use-comfy-plugin": "cross-env VITE_USE_COMFY_PLUGIN=true yarn dev", "dev": "yarn workspaces foreach --all --interlaced --verbose --parallel --jobs unlimited run dev", "build": "yarn workspaces foreach --all --interlaced --verbose --jobs unlimited run build", "clean": "yarn workspaces foreach --all --interlaced --verbose --parallel --jobs unlimited run clean && rimraf node_modules" diff --git a/packages/stablestudio-plugin-comfyui/.eslintrc.json b/packages/stablestudio-plugin-comfyui/.eslintrc.json new file mode 100644 index 00000000..cbd7eb46 --- /dev/null +++ b/packages/stablestudio-plugin-comfyui/.eslintrc.json @@ -0,0 +1 @@ +{ "extends": ["../../.eslintrc.json"] } diff --git a/packages/stablestudio-plugin-comfyui/.gitignore b/packages/stablestudio-plugin-comfyui/.gitignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/packages/stablestudio-plugin-comfyui/.gitignore @@ -0,0 +1 @@ +lib diff --git a/packages/stablestudio-plugin-comfyui/LICENSE b/packages/stablestudio-plugin-comfyui/LICENSE new file mode 100644 index 00000000..c539a924 --- /dev/null +++ b/packages/stablestudio-plugin-comfyui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 hlky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/stablestudio-plugin-comfyui/README.md b/packages/stablestudio-plugin-comfyui/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/stablestudio-plugin-comfyui/package.json b/packages/stablestudio-plugin-comfyui/package.json new file mode 100644 index 00000000..b10eac7f --- /dev/null +++ b/packages/stablestudio-plugin-comfyui/package.json @@ -0,0 +1,39 @@ +{ + "name": "@stability/stablestudio-plugin-comfy", + "version": "0.0.0", + "license": "MIT", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "files": [ + "lib" + ], + "scripts": { + "clean": "rimraf lib && rimraf node_modules", + "build:types": "ttsc --project tsconfig.json", + "build:javascript": "tsx scripts/Build.ts", + "build": "yarn build:types && yarn build:javascript", + "dev": "nodemon --watch src --ext ts,tsx,json --exec \"yarn build\"" + }, + "dependencies": { + "@stability/stablestudio-plugin": "workspace:^" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.33.1", + "@typescript-eslint/parser": "^5.33.1", + "eslint": "8.22.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-markdown": "^3.0.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "nodemon": "^2.0.20", + "prettier": "^2.7.1", + "rimraf": "^3.0.2", + "ts-node": "^10.9.1", + "tsx": "^3.12.1", + "ttypescript": "^1.5.13", + "typescript": "4.8.4", + "typescript-transform-paths": "^3.4.4" + } +} diff --git a/packages/stablestudio-plugin-comfyui/scripts/Build.ts b/packages/stablestudio-plugin-comfyui/scripts/Build.ts new file mode 100644 index 00000000..20e45e5d --- /dev/null +++ b/packages/stablestudio-plugin-comfyui/scripts/Build.ts @@ -0,0 +1,21 @@ +import * as ESBuild from "esbuild"; + +const main = async () => { + try { + await ESBuild.build({ + entryPoints: ["src/index.ts"], + outdir: "lib", + bundle: true, + sourcemap: true, + minify: true, + splitting: true, + format: "esm", + target: ["esnext"], + }); + } catch (error) { + console.error(error); + process.exit(1); + } +}; + +main(); diff --git a/packages/stablestudio-plugin-comfyui/src/index.ts b/packages/stablestudio-plugin-comfyui/src/index.ts new file mode 100644 index 00000000..9a1ed72c --- /dev/null +++ b/packages/stablestudio-plugin-comfyui/src/index.ts @@ -0,0 +1,572 @@ +import * as StableStudio from "@stability/stablestudio-plugin"; + +const basicWorkflow = ( + prompt: string, + negative_prompt: string, + model: string, + width: number, + height: number, + seed: number, + steps: number, + sampler: string, + cfgScale: number +) => { + return { + "3": { + class_type: "KSampler", + inputs: { + cfg: cfgScale, + denoise: 1, + latent_image: ["5", 0], + model: ["4", 0], + negative: ["7", 0], + positive: ["6", 0], + sampler_name: sampler, + scheduler: "normal", + seed: seed, + steps: steps, + }, + }, + "4": { + class_type: "CheckpointLoaderSimple", + inputs: { + ckpt_name: model, + }, + }, + "5": { + class_type: "EmptyLatentImage", + inputs: { + batch_size: 1, + height: height, + width: width, + }, + }, + "6": { + class_type: "CLIPTextEncode", + inputs: { + clip: ["4", 1], + text: prompt, + }, + }, + "7": { + class_type: "CLIPTextEncode", + inputs: { + clip: ["4", 1], + text: negative_prompt, + }, + }, + "8": { + class_type: "VAEDecode", + inputs: { + samples: ["3", 0], + vae: ["4", 2], + }, + }, + "9": { + class_type: "SaveImage", + inputs: { + filename_prefix: "ComfyUI", + images: ["8", 0], + }, + }, + }; +}; + +const basicWorkflowImage = ( + prompt: string, + negative_prompt: string, + model: string, + seed: number, + steps: number, + sampler: string, + cfgScale: number, + imageFilename: string, + imageStrength: number +) => { + imageStrength = Math.max(0, Math.min(0.99, imageStrength)); + return { + "3": { + class_type: "KSampler", + inputs: { + cfg: cfgScale, + denoise: 1 - imageStrength, + latent_image: ["11", 0], + model: ["4", 0], + negative: ["7", 0], + positive: ["6", 0], + sampler_name: sampler, + scheduler: "normal", + seed: seed, + steps: steps, + }, + }, + "4": { + class_type: "CheckpointLoaderSimple", + inputs: { + ckpt_name: model, + }, + }, + "6": { + class_type: "CLIPTextEncode", + inputs: { + clip: ["4", 1], + text: prompt, + }, + }, + "7": { + class_type: "CLIPTextEncode", + inputs: { + clip: ["4", 1], + text: negative_prompt, + }, + }, + "8": { + class_type: "VAEDecode", + inputs: { + samples: ["3", 0], + vae: ["4", 2], + }, + }, + "9": { + class_type: "SaveImage", + inputs: { + filename_prefix: "ComfyUI", + images: ["8", 0], + }, + }, + "10": { + class_type: "LoadImage", + inputs: { + image: imageFilename, + "choose file to upload": "image", + }, + }, + "11": { + class_type: "VAEEncode", + inputs: { + pixels: ["10", 0], + vae: ["4", 2], + }, + }, + }; +}; + +const basicWorkflowInpaint = ( + prompt: string, + negative_prompt: string, + model: string, + seed: number, + steps: number, + sampler: string, + cfgScale: number, + imageFilename: string, + imageStrength: number, + maskFilename: string +) => { + imageStrength = Math.max(0, Math.min(0.99, imageStrength)); + return { + "3": { + class_type: "KSampler", + inputs: { + cfg: cfgScale, + denoise: 1 - imageStrength, + latent_image: ["17", 0], + model: ["4", 0], + negative: ["7", 0], + positive: ["6", 0], + sampler_name: sampler, + scheduler: "normal", + seed: seed, + steps: steps, + }, + }, + "4": { + class_type: "CheckpointLoaderSimple", + inputs: { + ckpt_name: model, + }, + }, + "6": { + class_type: "CLIPTextEncode", + inputs: { + clip: ["4", 1], + text: prompt, + }, + }, + "7": { + class_type: "CLIPTextEncode", + inputs: { + clip: ["4", 1], + text: negative_prompt, + }, + }, + "8": { + class_type: "VAEDecode", + inputs: { + samples: ["3", 0], + vae: ["4", 2], + }, + }, + "9": { + class_type: "SaveImage", + inputs: { + filename_prefix: "ComfyUI", + images: ["8", 0], + }, + }, + "10": { + class_type: "LoadImage", + inputs: { + image: imageFilename, + "choose file to upload": "image", + }, + }, + "17": { + class_type: "VAEEncodeForInpaint", + inputs: { + grow_mask_by: 0, + mask: ["21", 0], + pixels: ["10", 0], + vae: ["4", 2], + }, + }, + "20": { + class_type: "LoadImageMask", + inputs: { + channel: "red", + "choose file to upload": "image", + image: maskFilename, + }, + }, + "21": { + class_type: "InvertMask", + inputs: { + mask: ["20", 0], + }, + }, + }; +}; + +const getModels = async (apiUrl: string) => { + const response = await fetch(`${apiUrl}/object_info`); + if (response.ok) { + const json = await response.json(); + return json.CheckpointLoaderSimple.input.required.ckpt_name[0]; + } else { + return []; + } +}; + +const getSamplers = async (apiUrl: string) => { + const response = await fetch(`${apiUrl}/object_info`); + if (response.ok) { + const json = await response.json(); + return json.KSampler.input.required.sampler_name[0]; + } else { + return []; + } +}; + +const getQueue = async (apiUrl: string) => { + const response = await fetch(`${apiUrl}/queue`); + if (response.ok) { + const json = await response.json(); + return { + pending: json.queue_pending.length, + running: json.queue_running.length, + }; + } else { + return { + pending: 0, + running: 0, + }; + } +}; + +const postQueue = async (apiUrl: string, data: any) => { + const response = await fetch(`${apiUrl}/prompt`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ prompt: data }), + }); + if (response.ok) { + const json = await response.json(); + return { + promptId: json.prompt_id, + }; + } else { + return { + error: "Error", + }; + } +}; + +const promptIdToImage = async ( + apiUrl: string, + promptId: string, + input: StableStudio.StableDiffusionInput +) => { + const response = await fetch(`${apiUrl}/history`); + if (response.ok) { + let json = await response.json(); + if (json.hasOwnProperty(promptId)) { + const prompt = json[promptId]["outputs"][9]["images"]; + const images = []; + for (const image of prompt) { + images.push({ + id: `${Math.random() * 10000000}`, + createdAt: new Date(), + blob: await fetch( + `${apiUrl}/view?filename=${image["filename"]}&subfolder=${image["subfolder"]}&type=output` + ).then((r) => r.blob()), + input: input, + } as StableStudio.StableDiffusionImage); + } + console.log(images); + return images; + } else { + return undefined; + } + } else { + return undefined; + } +}; + +const uploadImage = async (apiUrl: string, image: Blob, filename: string) => { + const blob = new Blob([image], { type: "image/png" }); + const formData = new FormData(); + formData.append("image", blob, filename); + const response = await fetch(`${apiUrl}/upload/image`, { + method: "POST", + body: formData, + }); + if (response.ok) { + const json = await response.json(); + return { + name: json.name, + subfolder: json.subfolder, + type: json.type, + }; + } else { + return { + error: "Error", + }; + } +}; + +const randomFilename = () => { + return ( + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + ); +}; + +const randomSeed = () => { + return Math.floor(Math.random() * 1000000000); +}; + +const getDefaultInput = () => ({ + width: 512, + height: 512, + + cfgScale: 7, + steps: 30, + sampler: { id: "0", name: "euler" }, + model: "0", //hack to select first model from list + strength: 0.75, +}); + +export const createPlugin = StableStudio.createPlugin<{ + settings: { + apiUrl: StableStudio.PluginSettingString; + }; +}>(({ get, set }) => ({ + manifest: { + name: "ComfyUI", + author: "hlky", + link: "https://github.com/comfyanonymous/ComfyUI", + icon: `${window.location.origin}/DummyImage.png`, + version: "0.0.1", + license: "MIT", + description: "ComfyUI plugin for StableStudio", + }, + getStableDiffusionSamplers: async () => { + return await getSamplers(get().settings.apiUrl.value as string).then( + (samplers) => + samplers.map((s: string) => ({ + id: `${samplers.indexOf(s)}`, + name: s, + })) + ); + }, + + getStableDiffusionModels: async () => { + const models = getModels(get().settings.apiUrl.value as string).then( + (models) => + models.map((model: string) => ({ + id: `${models.indexOf(model)}`, + name: model, + })) + ); + return models; + }, + + createStableDiffusionImages: async (options) => { + const count = options?.count ?? 1; + const apiUrl = get().settings.apiUrl.value as string; + const models = await getModels(apiUrl); + const model = models[options?.input?.model as string]; + const defaultStableDiffusionInput = getDefaultInput(); + const input = { + ...defaultStableDiffusionInput, + ...options?.input, + }; + input.model = model; + if (!input.model) { + return undefined; + } + + const prompt = + input.prompts + ?.filter((p) => p.weight && p.weight > 0) + .map((p) => `(${p.text}:${p.weight})`) + .join(" ") ?? ""; + const negative_prompt = + input.prompts + ?.filter((p) => p.weight && p.weight < 0) + .map((p) => `(${p.text}:${p.weight})`) + .join(" ") ?? "text"; + + const width = input.width ?? 512; + const height = input.height ?? 512; + const seed = input.seed === 0 ? randomSeed() : input.seed ?? randomSeed(); + const steps = input.steps ?? 30; + const sampler = input.sampler?.name ?? "euler"; + const cfgScale = input.cfgScale ?? 8; + let workflow: any = basicWorkflow( + prompt, + negative_prompt, + model, + width, + height, + seed, + steps, + sampler, + cfgScale + ); + const initImageFilename = `${randomFilename()}.png`; + const maskImageFilename = `${randomFilename()}.png`; + if (input.initialImage) { + const image = await uploadImage( + apiUrl, + input.initialImage.blob!, + initImageFilename + ); + if (image.error) { + return undefined; + } + workflow = basicWorkflowImage( + prompt, + negative_prompt, + model, + seed, + steps, + sampler, + cfgScale, + initImageFilename, + input.initialImage.weight ?? 0.75 + ); + } + if (input.maskImage) { + const image = await uploadImage( + apiUrl, + input.maskImage.blob!, + maskImageFilename + ); + if (image.error) { + return undefined; + } + workflow = basicWorkflowInpaint( + prompt, + negative_prompt, + model, + seed, + steps, + sampler, + cfgScale, + initImageFilename, + input.initialImage!.weight ?? 0.75, + maskImageFilename + ); + } + let promptIds = []; + let images: StableStudio.StableDiffusionImages = { id: "", images: [] }; + //messy, would be nicer to return images as they are ready + for (let i = 0; i < count; i++) { + workflow["3"].inputs.seed = seed + i; + const promptId = await postQueue(apiUrl, workflow); + if (!promptId) { + return undefined; + } + + promptIds.push(promptId); + } + for (let i = 0; i < count; i++) { + const promptId = promptIds[i].promptId; + let image = await promptIdToImage(apiUrl, promptId, { + ...input, + seed: seed + i, + }); + while (image === undefined) { + await new Promise((r) => setTimeout(r, 1000)); + image = await promptIdToImage(apiUrl, promptId, { + ...input, + seed: seed + i, + }); + } + images.images!.push(image[0]); + } + + return images; + }, + + getStatus: async () => { + const apiUrl = get().settings.apiUrl.value; + if (!apiUrl) { + return { + indicator: "error", + text: "API URL not set", + }; + } else { + const queue = await getQueue(apiUrl); + return { + indicator: "success", + text: + "Queue: " + queue.pending + " pending, " + queue.running + " running", + }; + } + }, + + getStableDiffusionDefaultCount: () => 1, + + getStableDiffusionDefaultInput: () => getDefaultInput(), + + settings: { + apiUrl: { + type: "string" as const, + default: "http://127.0.0.1:8188", + placeholder: "API URL", + required: true, + value: "http://127.0.0.1:8188", + }, + }, + + setSetting: (key, value) => + set(({ settings }) => ({ + settings: { + [key]: { ...settings[key], value: value as string }, + }, + })), +})); diff --git a/packages/stablestudio-plugin-comfyui/tsconfig.json b/packages/stablestudio-plugin-comfyui/tsconfig.json new file mode 100644 index 00000000..536c6bf7 --- /dev/null +++ b/packages/stablestudio-plugin-comfyui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"], + "exclude": ["node_modules"], + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "noUncheckedIndexedAccess": false, + + "outDir": "./lib", + "baseUrl": "./", + "paths": { + "~/*": ["./src/*"] + }, + + "plugins": [ + { "transform": "typescript-transform-paths" }, + { "transform": "typescript-transform-paths", "afterDeclarations": true } + ] + } +} diff --git a/packages/stablestudio-plugin-stability/src/index.ts b/packages/stablestudio-plugin-stability/src/index.ts index ae1ae2d1..418ede97 100644 --- a/packages/stablestudio-plugin-stability/src/index.ts +++ b/packages/stablestudio-plugin-stability/src/index.ts @@ -34,6 +34,8 @@ const getStableDiffusionDefaultInputFromPrompt = (prompt: string) => ({ cfgScale: 7, steps: 50, + + strength: 0.75, }); export const createPlugin = StableStudio.createPlugin<{ diff --git a/packages/stablestudio-ui/src/Editor/Dream/Render/index.tsx b/packages/stablestudio-ui/src/Editor/Dream/Render/index.tsx index c608ca4f..3e51b335 100644 --- a/packages/stablestudio-ui/src/Editor/Dream/Render/index.tsx +++ b/packages/stablestudio-ui/src/Editor/Dream/Render/index.tsx @@ -20,6 +20,9 @@ export namespace Render { entity, 1 ); + //largest entity size + const width = Math.max(...entities.map((e) => e.width)); + const height = Math.max(...entities.map((e) => e.height)); console.log(data); @@ -39,6 +42,8 @@ export namespace Render { weight: 1, } : null; + input.width = width; + input.height = height; }); create({ diff --git a/packages/stablestudio-ui/src/Editor/Dream/index.tsx b/packages/stablestudio-ui/src/Editor/Dream/index.tsx index 9970d434..6c42aed5 100644 --- a/packages/stablestudio-ui/src/Editor/Dream/index.tsx +++ b/packages/stablestudio-ui/src/Editor/Dream/index.tsx @@ -24,9 +24,9 @@ export function Dream({ id }: { id: ID }) { 4 as const; + const {getStableDiffusionDefaultCount} = Plugin.get(); + export const preset = () => getStableDiffusionDefaultCount?.() ?? 4; export const get = (): number => store.getState().count; export const set = (count: number) => store.getState().setCount(count); diff --git a/packages/stablestudio-ui/src/Generation/Image/Create/index.tsx b/packages/stablestudio-ui/src/Generation/Image/Create/index.tsx index 382ba814..e2882bb6 100644 --- a/packages/stablestudio-ui/src/Generation/Image/Create/index.tsx +++ b/packages/stablestudio-ui/src/Generation/Image/Create/index.tsx @@ -52,8 +52,8 @@ export namespace Create { onStarted(); await Throttle.wait(); - - const initImg = await Generation.Image.Input.resizeInit(input); + //resizing here appears to be causing issues with the output (on ComfyUI plugin at least) + const initImg = undefined;//await Generation.Image.Input.resizeInit(input); const pluginInput = await Generation.Image.Input.toInput( !initImg ? input diff --git a/packages/stablestudio-ui/src/Generation/Image/Model/StableDiffusionV1/index.ts b/packages/stablestudio-ui/src/Generation/Image/Model/StableDiffusionV1/index.ts index f3ee9268..a0f744fd 100644 --- a/packages/stablestudio-ui/src/Generation/Image/Model/StableDiffusionV1/index.ts +++ b/packages/stablestudio-ui/src/Generation/Image/Model/StableDiffusionV1/index.ts @@ -60,7 +60,7 @@ export namespace StableDiffusionV1 { steps: pluginDefaultInput?.steps ?? 50, seed: pluginDefaultInput?.seed ?? 0, guidance: false, - strength: 1, + strength: pluginDefaultInput?.strength ?? 0.75, mask: null, init: null, extras: { diff --git a/packages/stablestudio-ui/src/Generation/Image/Sampler/Samplers.tsx b/packages/stablestudio-ui/src/Generation/Image/Sampler/Samplers.tsx index cfabbbcd..b8960227 100644 --- a/packages/stablestudio-ui/src/Generation/Image/Sampler/Samplers.tsx +++ b/packages/stablestudio-ui/src/Generation/Image/Sampler/Samplers.tsx @@ -19,5 +19,5 @@ export namespace Samplers { }; export const useAreEnabled = () => - Plugin.use(({ getStableDiffusionModels }) => !!getStableDiffusionModels); + Plugin.use(({ getStableDiffusionSamplers }) => !!getStableDiffusionSamplers); } diff --git a/packages/stablestudio-ui/src/Plugin/index.tsx b/packages/stablestudio-ui/src/Plugin/index.tsx index 31976d87..9c9b7ff7 100644 --- a/packages/stablestudio-ui/src/Plugin/index.tsx +++ b/packages/stablestudio-ui/src/Plugin/index.tsx @@ -1,6 +1,7 @@ import * as StableStudio from "@stability/stablestudio-plugin"; import * as StableStudioPluginExample from "@stability/stablestudio-plugin-example"; import * as StableStudioPluginStability from "@stability/stablestudio-plugin-stability"; +import * as StableStudioPluginComfy from "@stability/stablestudio-plugin-comfy"; import { Environment } from "~/Environment"; import { Generation } from "~/Generation"; @@ -115,6 +116,8 @@ namespace State { const { createPlugin: createRootPlugin } = Environment.get("USE_EXAMPLE_PLUGIN") === "true" ? StableStudioPluginExample + : Environment.get("USE_COMFY_PLUGIN") === "true" + ? StableStudioPluginComfy : StableStudioPluginStability; return { diff --git a/yarn.lock b/yarn.lock index 9ddec844..9baa3f28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1131,6 +1131,31 @@ __metadata: languageName: node linkType: hard +"@stability/stablestudio-plugin-comfy@workspace:packages/stablestudio-plugin-comfyui": + version: 0.0.0-use.local + resolution: "@stability/stablestudio-plugin-comfy@workspace:packages/stablestudio-plugin-comfyui" + dependencies: + "@stability/stablestudio-plugin": "workspace:^" + "@typescript-eslint/eslint-plugin": ^5.33.1 + "@typescript-eslint/parser": ^5.33.1 + eslint: 8.22.0 + eslint-config-prettier: ^8.5.0 + eslint-plugin-import: ^2.26.0 + eslint-plugin-markdown: ^3.0.0 + eslint-plugin-prettier: ^4.2.1 + eslint-plugin-react: ^7.30.1 + eslint-plugin-react-hooks: ^4.6.0 + nodemon: ^2.0.20 + prettier: ^2.7.1 + rimraf: ^3.0.2 + ts-node: ^10.9.1 + tsx: ^3.12.1 + ttypescript: ^1.5.13 + typescript: 4.8.4 + typescript-transform-paths: ^3.4.4 + languageName: unknown + linkType: soft + "@stability/stablestudio-plugin-example@workspace:^, @stability/stablestudio-plugin-example@workspace:packages/stablestudio-plugin-example": version: 0.0.0-use.local resolution: "@stability/stablestudio-plugin-example@workspace:packages/stablestudio-plugin-example"