From 8b1c8ba9ee15386b95a6ccac0baea85b11b7c353 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 29 May 2023 18:19:09 -0500 Subject: [PATCH 1/7] Render markdown in frontend --- package.json | 1 + pnpm-lock.yaml | 39 +++- src/lib/convertVanillaWorkflow.ts | 50 +++-- src/lib/nodes/widgets/ComfyMarkdownNode.ts | 57 ++++++ src/lib/nodes/widgets/ComfyWidgetNode.ts | 13 +- src/lib/nodes/widgets/index.ts | 1 + src/lib/widgets/MarkdownWidget.svelte | 227 +++++++++++++++++++++ 7 files changed, 366 insertions(+), 22 deletions(-) create mode 100644 src/lib/nodes/widgets/ComfyMarkdownNode.ts create mode 100644 src/lib/widgets/MarkdownWidget.svelte diff --git a/package.json b/package.json index 7dce293..947df10 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@codemirror/search": "^6.2.2", "@codemirror/state": "^6.1.2", "@codemirror/view": "^6.4.1", + "@dogagenc/svelte-markdown": "^0.2.4", "@gradio/accordion": "workspace:*", "@gradio/atoms": "workspace:*", "@gradio/button": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 460e56e..860c670 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@codemirror/view': specifier: ^6.4.1 version: 6.11.0 + '@dogagenc/svelte-markdown': + specifier: ^0.2.4 + version: 0.2.4(svelte@3.58.0) '@gradio/accordion': specifier: workspace:* version: link:gradio/js/accordion @@ -148,6 +151,9 @@ importers: svelte-floating-ui: specifier: ^1.5.2 version: 1.5.2 + svelte-markdown: + specifier: ^0.2.3 + version: 0.2.3(svelte@3.58.0) svelte-preprocess: specifier: ^5.0.3 version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) @@ -1357,7 +1363,6 @@ packages: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 @@ -1501,6 +1506,16 @@ packages: w3c-keyname: 2.2.6 dev: false + /@dogagenc/svelte-markdown@0.2.4(svelte@3.58.0): + resolution: {integrity: sha512-UmmHHZ7rilAbBYiNsxuL5d8Ac79EhFXrhjsUNr30BPzn+T7ohJR8kHMFjDYDQc0tOQOfKbICvkPAQ6cprqS3Eg==} + peerDependencies: + svelte: ^3.0.0 + dependencies: + '@types/marked': 4.3.1 + marked: 4.3.0 + svelte: 3.58.0 + dev: false + /@esbuild/android-arm64@0.17.18: resolution: {integrity: sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==} engines: {node: '>=12'} @@ -2423,6 +2438,10 @@ packages: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true + /@types/marked@4.3.1: + resolution: {integrity: sha512-vSSbKZFbNktrQ15v7o1EaH78EbWV+sPQbPjHG+Cp8CaNcPFUEfjZ0Iml/V0bFDwsTlYe8o6XC5Hfdp91cqPV2g==} + dev: false + /@types/node@10.17.60: resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} dev: false @@ -3098,8 +3117,6 @@ packages: '@codemirror/search': 6.4.0 '@codemirror/state': 6.2.0 '@codemirror/view': 6.11.0 - transitivePeerDependencies: - - '@lezer/common' dev: false /codemirror@6.0.1(@lezer/common@1.0.2): @@ -5481,6 +5498,12 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: false + /marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + dev: false + /md5-hex@3.0.1: resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} engines: {node: '>=8'} @@ -6690,6 +6713,16 @@ packages: dependencies: svelte: 3.58.0 + /svelte-markdown@0.2.3(svelte@3.58.0): + resolution: {integrity: sha512-2h680NzTXnAD0CXhxe3GeHl6W+ayG4iKQRl+BIDRw+R0mUE0OiNxP1Vt8Rn+aWevB/LBiBIPCAwvL+0BkG057A==} + peerDependencies: + svelte: ^3.0.0 + dependencies: + '@types/marked': 4.3.1 + marked: 4.3.0 + svelte: 3.58.0 + dev: false + /svelte-preprocess@4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4): resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==} engines: {node: '>= 9.11.2'} diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts index 8d97517..b777711 100644 --- a/src/lib/convertVanillaWorkflow.ts +++ b/src/lib/convertVanillaWorkflow.ts @@ -68,7 +68,7 @@ function getConnectionPos(node: SerializedLGraphNode, is_input: boolean, slotNum return out; } -function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node: SerializedLGraphNode, slotIndex: number, isInput: boolean, widgetNodeType: string, value: any): [ComfyWidgetNode, SerializedComfyWidgetNode] { +function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, widgetNodeType: string, value: any, node?: SerializedLGraphNode, slotIndex?: number, isInput?: boolean): [ComfyWidgetNode, SerializedComfyWidgetNode] { const comfyWidgetNode = LiteGraph.createNode(widgetNodeType); comfyWidgetNode.flags.collapsed = true; const size: Vector2 = [0, 0]; @@ -85,12 +85,15 @@ function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node: const serWidgetNode = comfyWidgetNode.serialize() as SerializedComfyWidgetNode; serWidgetNode.comfyValue = value; serWidgetNode.shownOutputProperties = {}; - getConnectionPos(node, isInput, slotIndex, serWidgetNode.pos); - if (isInput) - serWidgetNode.pos[0] -= size[0] - 20; - else - serWidgetNode.pos[0] += 20; - serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2; + + if (node != null) { + getConnectionPos(node, isInput, slotIndex, serWidgetNode.pos); + if (isInput) + serWidgetNode.pos[0] -= size[0] - 20; + else + serWidgetNode.pos[0] += 20; + serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2; + } if (widgetNodeType === "ui/text" && typeof value === "string" && value.indexOf("\n") != -1) { const lineCount = countNewLines(value); @@ -260,11 +263,12 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode( vanillaWorkflow, + widgetNodeType, + value, node, 0, // first output on the PrimitiveNode - false, // this is an output slot index - widgetNodeType, - value); + false // this is an output slot index + ); // Set the UI node's min/max/step from the node def configureWidgetNodeProperties(serWidgetNode, widgetOpts) @@ -381,6 +385,20 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork removeSerializedNode(vanillaWorkflow, node); continue } + else if (node.type === "Note") { + const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode( + vanillaWorkflow, + "ui/markdown", + node.widgets_values[0] + ); + serWidgetNode.pos = [node.pos[0], node.pos[1]] + + const group = layoutState.addContainer(left, { title: "" }) + layoutState.addWidget(group, comfyWidgetNode) + + removeSerializedNode(vanillaWorkflow, node); + continue + } const def = ComfyApp.knownBackendNodes[node.type]; if (def == null) { @@ -449,11 +467,12 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode( vanillaWorkflow, + widgetNodeType, + value, node, connInputIndex, - true, - widgetNodeType, - value); + true + ); configureWidgetNodeProperties(serWidgetNode, inputOpts) @@ -492,11 +511,12 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork // Let's create a gallery for this output node and hook it up const [comfyGalleryNode, serGalleryNode] = createSerializedWidgetNode( vanillaWorkflow, + "ui/gallery", + [], node, connOutputIndex, false, - "ui/gallery", - []); + ); if (group == null) group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type }) diff --git a/src/lib/nodes/widgets/ComfyMarkdownNode.ts b/src/lib/nodes/widgets/ComfyMarkdownNode.ts new file mode 100644 index 0000000..8d41bf5 --- /dev/null +++ b/src/lib/nodes/widgets/ComfyMarkdownNode.ts @@ -0,0 +1,57 @@ +import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout } from "@litegraph-ts/core"; + +import MarkdownWidget from "$lib/widgets/MarkdownWidget.svelte"; +import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode"; + +export interface ComfyMarkdownProperties extends ComfyWidgetProperties { +} + +export default class ComfyMarkdownNode extends ComfyWidgetNode { + override properties: ComfyMarkdownProperties = { + tags: [], + defaultValue: false, + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "store", type: BuiltInSlotType.ACTION } + ], + outputs: [ + { name: "value", type: "string" }, + { name: "changed", type: BuiltInSlotType.EVENT }, + ] + } + + override svelteComponentType = MarkdownWidget; + override defaultValue = ""; + + constructor(name?: string) { + super(name, "") + } + + override createDisplayWidget(): ITextWidget { + const widget = this.addWidget( + "text", + "Value", + "", + (v: string) => { + if (v == null || v === this.getValue()) { + return; + } + this.setValue(v); + }, + { + multiline: true, + inputStyle: { fontFamily: "monospace" } + } + ) + return widget; + } +} + +LiteGraph.registerNodeType({ + class: ComfyMarkdownNode, + title: "UI.Markdown", + desc: "Displays Markdown in the UI", + type: "ui/markdown" +}) diff --git a/src/lib/nodes/widgets/ComfyWidgetNode.ts b/src/lib/nodes/widgets/ComfyWidgetNode.ts index a8787cb..a0c3c8a 100644 --- a/src/lib/nodes/widgets/ComfyWidgetNode.ts +++ b/src/lib/nodes/widgets/ComfyWidgetNode.ts @@ -106,13 +106,18 @@ export default abstract class ComfyWidgetNode extends ComfyGraphNode { this.value = writable(value) this.color ||= color.color this.bgColor ||= color.bgColor - this.displayWidget = this.addWidget( + this.displayWidget = this.createDisplayWidget(); + this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this)) + } + + protected createDisplayWidget(): ITextWidget { + const widget = this.addWidget( "text", "Value", "" - ); - this.displayWidget.disabled = true; // prevent editing - this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this)) + ) + widget.disabled = true; // prevent editing + return widget; } addPropertyAsOutput(propertyName: string, type: string) { diff --git a/src/lib/nodes/widgets/index.ts b/src/lib/nodes/widgets/index.ts index 130149c..d1b6c09 100644 --- a/src/lib/nodes/widgets/index.ts +++ b/src/lib/nodes/widgets/index.ts @@ -9,3 +9,4 @@ export { default as ComfyRadioNode } from "./ComfyRadioNode" export { default as ComfyNumberNode } from "./ComfyNumberNode" export { default as ComfyTextNode } from "./ComfyTextNode" export { default as ComfyMultiRegionNode } from "./ComfyMultiRegionNode" +export { default as ComfyMarkdownNode } from "./ComfyMarkdownNode" diff --git a/src/lib/widgets/MarkdownWidget.svelte b/src/lib/widgets/MarkdownWidget.svelte new file mode 100644 index 0000000..c41a607 --- /dev/null +++ b/src/lib/widgets/MarkdownWidget.svelte @@ -0,0 +1,227 @@ + + +
+ {#key $attrsChanged} + {#if widget !== null && node !== null} + + + + {/if} + {/key} +
+ + From cba6e6e47cb5a8fa57c00e203d721884fb5dc4d6 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 29 May 2023 18:23:29 -0500 Subject: [PATCH 2/7] Don't render markdown embedded HTML --- src/lib/widgets/MarkdownWidget.svelte | 8 +++++++- src/lib/widgets/markdown/NullMarkdownRenderer.svelte | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/lib/widgets/markdown/NullMarkdownRenderer.svelte diff --git a/src/lib/widgets/MarkdownWidget.svelte b/src/lib/widgets/MarkdownWidget.svelte index c41a607..1a328b0 100644 --- a/src/lib/widgets/MarkdownWidget.svelte +++ b/src/lib/widgets/MarkdownWidget.svelte @@ -4,6 +4,8 @@ import { Block } from "@gradio/atoms"; import type { ComfyMarkdownNode } from "$lib/nodes/widgets"; import SvelteMarkdown from "svelte-markdown" + import NullMarkdownRenderer from "./markdown/NullMarkdownRenderer.svelte" + import { SvelteComponentDev } from "svelte/internal"; export let widget: WidgetLayout | null = null; export let isMobile: boolean = false; @@ -12,6 +14,10 @@ let nodeValue: Writable = writable(""); let attrsChanged: Writable = writable(0); + let renderers: Record = { + "html": NullMarkdownRenderer + } + $: widget && setNodeValue(widget); function setNodeValue(widget: WidgetLayout) { @@ -27,7 +33,7 @@ {#key $attrsChanged} {#if widget !== null && node !== null} - + {/if} {/key} diff --git a/src/lib/widgets/markdown/NullMarkdownRenderer.svelte b/src/lib/widgets/markdown/NullMarkdownRenderer.svelte new file mode 100644 index 0000000..70f162e --- /dev/null +++ b/src/lib/widgets/markdown/NullMarkdownRenderer.svelte @@ -0,0 +1,7 @@ + + +
From 97bc7ce6ba9203014f22533f1f0e8685d87d8456 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 29 May 2023 18:29:04 -0500 Subject: [PATCH 3/7] Update litegraph --- litegraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litegraph b/litegraph index 9b8d28d..7c38fa4 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 9b8d28d3e2b3bc69a09b55b2d41a9bd53f57e37d +Subproject commit 7c38fa4aedb33c960806efad6d700ec8ca67649f From 8890e45b668816b0c4b906b775166acec1be2654 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 29 May 2023 18:45:08 -0500 Subject: [PATCH 4/7] Display mask on image upload --- src/lib/components/ImageUpload.svelte | 13 +++++++++++++ src/lib/widgets/ImageUploadWidget.svelte | 11 ++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/lib/components/ImageUpload.svelte b/src/lib/components/ImageUpload.svelte index 224ad73..85358df 100644 --- a/src/lib/components/ImageUpload.svelte +++ b/src/lib/components/ImageUpload.svelte @@ -18,6 +18,7 @@ export let elem_classes: string[] = [] export let style: string = "" export let label: string = "" + export let mask: ComfyImageLocation | null; // let propsChanged: Writable | null = null; let dragging = false; let pending_upload = false; @@ -172,6 +173,15 @@ bind:naturalWidth={imgWidth} bind:naturalHeight={imgHeight} /> + {#key mask} + {#if mask} + + {firstImage.filename} + {/if} + {/key} {:else} 0; $: if (!hasImage) { editMask = false; } + let mask: ComfyImageLocation | null; + $: if (hasImage && canMask) { + mask = $nodeValue[0].children?.find(i => i.tags.includes("mask"))?.comfyUIFile; + } + else { + mask = null; + } + const MASK_FILENAME: string = "ComfyBoxMask.png" async function onMaskReleased(e: CustomEvent) { @@ -122,6 +129,7 @@ // TODO other child image types preserved here? image.children = []; } + mask = null; if (maskCanvasComp) { maskCanvasComp.clearStrokes(); } @@ -232,6 +240,7 @@ Date: Mon, 29 May 2023 18:49:44 -0500 Subject: [PATCH 5/7] Fix --- pnpm-lock.yaml | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 860c670..07f31e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,9 +151,6 @@ importers: svelte-floating-ui: specifier: ^1.5.2 version: 1.5.2 - svelte-markdown: - specifier: ^0.2.3 - version: 0.2.3(svelte@3.58.0) svelte-preprocess: specifier: ^5.0.3 version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) @@ -1363,6 +1360,7 @@ packages: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 @@ -3117,6 +3115,8 @@ packages: '@codemirror/search': 6.4.0 '@codemirror/state': 6.2.0 '@codemirror/view': 6.11.0 + transitivePeerDependencies: + - '@lezer/common' dev: false /codemirror@6.0.1(@lezer/common@1.0.2): @@ -6713,16 +6713,6 @@ packages: dependencies: svelte: 3.58.0 - /svelte-markdown@0.2.3(svelte@3.58.0): - resolution: {integrity: sha512-2h680NzTXnAD0CXhxe3GeHl6W+ayG4iKQRl+BIDRw+R0mUE0OiNxP1Vt8Rn+aWevB/LBiBIPCAwvL+0BkG057A==} - peerDependencies: - svelte: ^3.0.0 - dependencies: - '@types/marked': 4.3.1 - marked: 4.3.0 - svelte: 3.58.0 - dev: false - /svelte-preprocess@4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4): resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==} engines: {node: '>= 9.11.2'} From ac53ba226b7067b8bf985535656b5fe4a6edc4eb Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 29 May 2023 18:52:48 -0500 Subject: [PATCH 6/7] Fix --- src/lib/widgets/MarkdownWidget.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/widgets/MarkdownWidget.svelte b/src/lib/widgets/MarkdownWidget.svelte index 1a328b0..f99524b 100644 --- a/src/lib/widgets/MarkdownWidget.svelte +++ b/src/lib/widgets/MarkdownWidget.svelte @@ -3,7 +3,7 @@ import { get, type Writable, writable } from "svelte/store"; import { Block } from "@gradio/atoms"; import type { ComfyMarkdownNode } from "$lib/nodes/widgets"; - import SvelteMarkdown from "svelte-markdown" + import SvelteMarkdown from "@dogagenc/svelte-markdown" import NullMarkdownRenderer from "./markdown/NullMarkdownRenderer.svelte" import { SvelteComponentDev } from "svelte/internal"; From 5270c6750e8d1c4ecf61ae9b9b542a2bb53c692e Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 29 May 2023 18:59:11 -0500 Subject: [PATCH 7/7] Fix --- package.json | 1 + pnpm-lock.yaml | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 947df10..670e59f 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "framework7": "^8.0.3", "framework7-svelte": "^8.0.3", "img-comparison-slider": "^8.0.0", + "marked": "^5.0.3", "pollen-css": "^4.6.2", "radix-icons-svelte": "^1.2.1", "style-mod": "^4.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07f31e6..9f8345e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: img-comparison-slider: specifier: ^8.0.0 version: 8.0.0 + marked: + specifier: ^5.0.3 + version: 5.0.3 pollen-css: specifier: ^4.6.2 version: 4.6.2 @@ -1360,7 +1363,6 @@ packages: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 @@ -3115,8 +3117,6 @@ packages: '@codemirror/search': 6.4.0 '@codemirror/state': 6.2.0 '@codemirror/view': 6.11.0 - transitivePeerDependencies: - - '@lezer/common' dev: false /codemirror@6.0.1(@lezer/common@1.0.2): @@ -5504,6 +5504,12 @@ packages: hasBin: true dev: false + /marked@5.0.3: + resolution: {integrity: sha512-KUONa43Uk74uUNWMxh6lfaNYmSAsRMiDAaX8QBCCRVXzEufR0zX6T33vrGbvTnQLL02ungDM3KSzZtO+chJaHg==} + engines: {node: '>= 18'} + hasBin: true + dev: false + /md5-hex@3.0.1: resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} engines: {node: '>=8'}