diff --git a/nodecg-io-debug/dashboard/debug-helper.css b/nodecg-io-debug/dashboard/debug-helper.css
new file mode 100644
index 000000000..896410f3e
--- /dev/null
+++ b/nodecg-io-debug/dashboard/debug-helper.css
@@ -0,0 +1,174 @@
+.container {
+ display: inline-block;
+ border: 1px dashed rgb(64, 64, 64);
+ background-color: rgb(44, 44, 44);
+ padding: 0;
+ margin: 10px;
+ min-width: 200px;
+ min-height: 100px;
+ box-shadow: 6px 5px 15px 3px rgba(0, 0, 0, 0.52);
+}
+
+.containerHead {
+ display: block;
+ color: #92d6d6;
+ padding-top: 1px;
+ font-size: large;
+ border-bottom: 1px dashed rgb(64, 64, 64);
+ background-color: rgb(55, 55, 55);
+}
+
+.containerTitle {
+ font-weight: bold;
+}
+
+.containerEvent {
+ font-style: italic;
+ float: right;
+ margin-left: 30px;
+}
+
+.containerBody {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ font-size: large;
+}
+
+.containerHead,
+.containerBody {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+body {
+ float: left;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+button {
+ border: 1px solid gray;
+ color: white;
+ background-color: rgb(70, 70, 70);
+ font-size: large;
+}
+
+button:active {
+ background-color: rgb(90, 90, 90);
+}
+
+#event_click button,
+#event_bool button {
+ height: 80px;
+ width: 80px;
+}
+
+#event_number button {
+ height: 30px;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+
+.button1 {
+ background-color: #45586e;
+}
+.button2 {
+ background-color: #4c6c78;
+}
+.button3 {
+ background-color: #426161;
+}
+.button4 {
+ background-color: #4c786b;
+}
+.button5 {
+ background-color: #456e57;
+}
+
+.button1:active {
+ background-color: #506780;
+}
+.button2:active {
+ background-color: #577c8a;
+}
+.button3:active {
+ background-color: #4e7373;
+}
+.button4:active {
+ background-color: #578a7b;
+}
+.button5:active {
+ background-color: #508065;
+}
+
+.smallInfo {
+ font-size: smaller;
+}
+
+input[type="number"] {
+ font-size: larger;
+ color: white;
+ font-family: "Consolas", sans-serif;
+ background-color: rgb(70, 70, 70);
+ border: 1px solid rgb(20, 20, 20);
+ width: 80px;
+}
+
+.inlineContainer {
+ margin-top: 10px;
+}
+
+#number_input1_send,
+#text_oneline_send,
+#text_multiline_send,
+#list_list_send {
+ padding: 5px !important;
+ margin-left: 5px;
+}
+
+input[type="range"] {
+ width: 75%;
+ margin-right: 20px;
+}
+
+input[type="color"] {
+ width: 80px;
+ height: 80px;
+}
+
+input[type="date"],
+input[type="datetime-local"] {
+ font-size: larger;
+ color: white;
+ background-color: rgb(70, 70, 70);
+ border: 1px solid rgb(20, 20, 20);
+}
+
+input[type="text"],
+textarea {
+ font-size: larger;
+ color: white;
+ background-color: rgb(70, 70, 70);
+ border: 1px solid rgb(20, 20, 20);
+ width: 200px;
+}
+
+.area {
+ resize: none;
+ height: 100px;
+}
+
+.lists {
+ resize: none;
+ height: 120px;
+}
+
+.flex-fill {
+ flex: 1;
+}
+
+#instanceMonaco {
+ height: 300px;
+ width: 500px;
+ font-size: larger;
+}
diff --git a/nodecg-io-debug/dashboard/debug-helper.html b/nodecg-io-debug/dashboard/debug-helper.html
new file mode 100644
index 000000000..fd196e717
--- /dev/null
+++ b/nodecg-io-debug/dashboard/debug-helper.html
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+ Clicks
+ onClick, onclick[1-5]
+
+
+
+
+
+
+
+
+
+
+
+
+ Numbers
+ onNumber
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (number sent automatically)
+
+
+
+
+
+
+ Ranges
+ onRange(0to100|0to1|M1to1)
+
+
+
+
+
+
+
+ Colors
+ onColor
+
+
+
+
+
+
+
+
+ Date
+ onDate
+
+
+
+
+
+
+
+ Booleans
+ onBool
+
+
+
+
+
+
+
+
+
+ Strings
+ onText
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lists
+ onList
+
+
+
+
+
+
+
(List entries are comma separated)
+
+
+
+
+
+
+
+
diff --git a/nodecg-io-debug/dashboard/debug-helper.js b/nodecg-io-debug/dashboard/debug-helper.js
new file mode 100644
index 000000000..be09559a5
--- /dev/null
+++ b/nodecg-io-debug/dashboard/debug-helper.js
@@ -0,0 +1,79 @@
+/* eslint-disable no-undef */
+
+// Buttons
+for (let i = 1; i <= 5; i++) {
+ document.querySelector(`#click_button${i}`).onclick = () => {
+ nodecg.sendMessage("onClick", i);
+ };
+}
+
+// Numbers
+for (let i = 0; i < 5; i++) {
+ const num = 10 ** i;
+ document.querySelector(`#number_button${num}`).onclick = () => {
+ nodecg.sendMessage("onNumber", num);
+ };
+}
+document.querySelector(`#number_input1_send`).onclick = () => {
+ const num = document.querySelector('#number_input1').value;
+ nodecg.sendMessage("onNumber", num);
+};
+document.querySelector(`#number_input2`).onchange = () => {
+ const num = document.querySelector('#number_input2').value;
+ nodecg.sendMessage("onNumber", num);
+};
+
+// Ranges
+for (const range of ["0to100", "0to1", "M1to1"]) {
+ document.querySelector("#range_" + range).addEventListener("change", (e) => {
+ const num = e.target.value;
+ nodecg.sendMessage(`onRange${range}`, num);
+ });
+}
+
+// Color
+document.querySelector("#color_color").addEventListener("input", (e) => {
+ const color = e.target.value;
+ nodecg.sendMessage(`onColor`, color);
+});
+
+// Dates
+for (const element of ["#date_date", "#date_datetime"]) {
+ document.querySelector(element).addEventListener("change", (e) => {
+ const date = e.target.value;
+ nodecg.sendMessage(`onDate`, date);
+ });
+}
+
+// Booleans
+document.querySelector("#bool_false").onclick = () => {
+ nodecg.sendMessage("onBool", false);
+};
+document.querySelector("#bool_true").onclick = () => {
+ nodecg.sendMessage("onBool", true);
+};
+
+// Text
+for (const element of ["oneline", "multiline"]) {
+ document.querySelector(`#text_${element}_send`).onclick = () => {
+ const value = document.querySelector(`#text_${element}`).value;
+ nodecg.sendMessage("onText", value);
+ };
+}
+
+// Lists
+document.querySelector("#list_list_send").onclick = () => {
+ const value = document.querySelector("#list_list").value;
+ nodecg.sendMessage("onList", value);
+};
+
+// JSON
+document.querySelector("#json_send").onclick = () => {
+ const jsonString = window.debugMonacoEditor.getValue();
+ try {
+ const json = JSON.parse(jsonString);
+ nodecg.sendMessage("onJSON", json);
+ } catch (e) {
+ nodecg.log.error(`Cannot send invalid json: ${e}`)
+ }
+};
\ No newline at end of file
diff --git a/nodecg-io-debug/extension/debugHelper.ts b/nodecg-io-debug/extension/debugHelper.ts
new file mode 100644
index 000000000..34390acbe
--- /dev/null
+++ b/nodecg-io-debug/extension/debugHelper.ts
@@ -0,0 +1,130 @@
+import { EventEmitter } from "events";
+import { NodeCG } from "nodecg/types/server";
+
+export interface Color {
+ red: number;
+ green: number;
+ blue: number;
+}
+
+export class DebugHelper extends EventEmitter {
+ constructor(nodecg: NodeCG) {
+ super();
+ nodecg.log.info("DebugHelper is ready to help debugging.");
+
+ // Registering all listeners and defining redirection
+ nodecg.listenFor("onClick", (value) => {
+ this.emit("onClick");
+ this.emit(`onClick${value}`);
+ });
+
+ nodecg.listenFor("onNumber", (value) => {
+ this.emit("onNumber", parseInt(value));
+ });
+
+ for (const range of ["0to100", "0to1", "M1to1"]) {
+ nodecg.listenFor(`onRange${range}`, (value) => {
+ this.emit(`onRange${range}`, parseFloat(value));
+ });
+ }
+
+ nodecg.listenFor("onColor", (value) => {
+ this.emit("onColor", DebugHelper.hexToRGB(value));
+ });
+
+ nodecg.listenFor("onDate", (value) => {
+ this.emit("onDate", new Date(value));
+ });
+
+ nodecg.listenFor("onBool", (value) => {
+ this.emit("onBool", value);
+ });
+
+ nodecg.listenFor("onText", (value) => {
+ this.emit("onText", value);
+ });
+
+ nodecg.listenFor("onList", (value) => {
+ const list = (value as string).split(",");
+ this.emit("onList", list);
+ });
+
+ nodecg.listenFor("onJSON", (value) => {
+ this.emit("onJSON", value);
+ });
+ }
+
+ private static hexToRGB(hex: string): Color {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result
+ ? {
+ red: parseInt(result[1], 16),
+ green: parseInt(result[2], 16),
+ blue: parseInt(result[3], 16),
+ }
+ : { red: 0, green: 0, blue: 0 };
+ }
+
+ static createClient(nodecg: NodeCG): DebugHelper {
+ return new DebugHelper(nodecg);
+ }
+
+ // Custom register handler functions
+
+ onClick(listener: () => void): void {
+ this.on("onClick", listener);
+ }
+ onClick1(listener: () => void): void {
+ this.on("onClick1", listener);
+ }
+ onClick2(listener: () => void): void {
+ this.on("onClick2", listener);
+ }
+ onClick3(listener: () => void): void {
+ this.on("onClick3", listener);
+ }
+ onClick4(listener: () => void): void {
+ this.on("onClick4", listener);
+ }
+ onClick5(listener: () => void): void {
+ this.on("onClick5", listener);
+ }
+
+ onNumber(listener: (value: number) => void): void {
+ this.on("onNumber", listener);
+ }
+
+ onRange0to100(listener: (value: number) => void): void {
+ this.on("onRange0to100", listener);
+ }
+ onRange0to1(listener: (value: number) => void): void {
+ this.on("onRange0to1", listener);
+ }
+ onRangeM1to1(listener: (value: number) => void): void {
+ this.on("onRangeM1to1", listener);
+ }
+
+ onColor(listener: (value: Color) => void): void {
+ this.on("onColor", listener);
+ }
+
+ onDate(listener: (value: Date) => void): void {
+ this.on("onDate", listener);
+ }
+
+ onBool(listener: (value: boolean) => void): void {
+ this.on("onBool", listener);
+ }
+
+ onText(listener: (value: string) => void): void {
+ this.on("onText", listener);
+ }
+
+ onList(listener: (value: Array) => void): void {
+ this.on("onList", listener);
+ }
+
+ onJSON(listener: (value: unknown) => void): void {
+ this.on("onJSON", listener);
+ }
+}
diff --git a/nodecg-io-debug/extension/index.ts b/nodecg-io-debug/extension/index.ts
new file mode 100644
index 000000000..dd5bc1202
--- /dev/null
+++ b/nodecg-io-debug/extension/index.ts
@@ -0,0 +1,35 @@
+import { NodeCG } from "nodecg/types/server";
+import { Result, emptySuccess, success, ServiceBundle } from "nodecg-io-core";
+import { DebugHelper } from "./debugHelper";
+
+export type DebugConfig = {
+ // Nothing to configure
+};
+
+export { DebugHelper } from "./debugHelper";
+
+module.exports = (nodecg: NodeCG) => {
+ new DebugService(nodecg, "debug", __dirname, "../schema.json").register();
+};
+
+class DebugService extends ServiceBundle {
+ async validateConfig(_: DebugConfig): Promise> {
+ return emptySuccess();
+ }
+
+ async createClient(_: DebugConfig): Promise> {
+ const client = DebugHelper.createClient(this.nodecg);
+ this.nodecg.log.info("Successfully created debug helper.");
+ return success(client);
+ }
+
+ stopClient(_: DebugHelper): void {
+ this.nodecg.log.info("Successfully stopped debug client.");
+ }
+
+ removeHandlers(client: DebugHelper): void {
+ client.removeAllListeners();
+ }
+
+ requiresNoConfig = true;
+}
diff --git a/nodecg-io-debug/package-lock.json b/nodecg-io-debug/package-lock.json
new file mode 100644
index 000000000..f79fd2286
--- /dev/null
+++ b/nodecg-io-debug/package-lock.json
@@ -0,0 +1,27 @@
+{
+ "name": "nodecg-io-debug",
+ "version": "0.2.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "version": "0.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "monaco-editor": "^0.23.0"
+ }
+ },
+ "node_modules/monaco-editor": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.23.0.tgz",
+ "integrity": "sha512-q+CP5zMR/aFiMTE9QlIavGyGicKnG2v/H8qVvybLzeFsARM8f6G9fL0sMST2tyVYCwDKkGamZUI6647A0jR/Lg=="
+ }
+ },
+ "dependencies": {
+ "monaco-editor": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.23.0.tgz",
+ "integrity": "sha512-q+CP5zMR/aFiMTE9QlIavGyGicKnG2v/H8qVvybLzeFsARM8f6G9fL0sMST2tyVYCwDKkGamZUI6647A0jR/Lg=="
+ }
+ }
+}
diff --git a/nodecg-io-debug/package.json b/nodecg-io-debug/package.json
new file mode 100644
index 000000000..53ee2e3af
--- /dev/null
+++ b/nodecg-io-debug/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "nodecg-io-debug",
+ "version": "0.2.0",
+ "description": "Debug helper service that helps to easily trigger your code for debugging purposes.",
+ "homepage": "https://nodecg.io/samples/debug",
+ "author": {
+ "name": "CodeOverflow team",
+ "url": "https://github.com/codeoverflow-org"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/codeoverflow-org/nodecg-io.git",
+ "directory": "nodecg-io-debug"
+ },
+ "main": "extension",
+ "scripts": {
+ "build": "tsc -b",
+ "watch": "tsc -b -w",
+ "clean": "tsc -b --clean"
+ },
+ "keywords": [
+ "nodecg-io",
+ "nodecg-bundle"
+ ],
+ "nodecg": {
+ "compatibleRange": "^1.1.1",
+ "bundleDependencies": {
+ "nodecg-io-core": "^0.2.0"
+ },
+ "dashboardPanels": [
+ {
+ "name": "nodecg-io-debug-helper",
+ "title": "Debug Helper",
+ "file": "debug-helper.html",
+ "fullbleed": true,
+ "headerColor": "#527878"
+ }
+ ],
+ "mount": [
+ {
+ "directory": "node_modules",
+ "endpoint": "monaco"
+ }
+ ]
+ },
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^14.14.33",
+ "nodecg": "^1.8.1",
+ "typescript": "^4.2.3"
+ },
+ "dependencies": {
+ "nodecg-io-core": "^0.2.0",
+ "monaco-editor": "^0.23.0"
+ }
+}
diff --git a/nodecg-io-debug/schema.json b/nodecg-io-debug/schema.json
new file mode 100644
index 000000000..21dca84a4
--- /dev/null
+++ b/nodecg-io-debug/schema.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {},
+ "required": []
+}
diff --git a/nodecg-io-debug/tsconfig.json b/nodecg-io-debug/tsconfig.json
new file mode 100644
index 000000000..1c8405620
--- /dev/null
+++ b/nodecg-io-debug/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.common.json"
+}
diff --git a/samples/debug/extension/index.ts b/samples/debug/extension/index.ts
new file mode 100644
index 000000000..046e95114
--- /dev/null
+++ b/samples/debug/extension/index.ts
@@ -0,0 +1,53 @@
+import { NodeCG } from "nodecg/types/server";
+import { DebugHelper } from "nodecg-io-debug";
+import { requireService } from "nodecg-io-core";
+
+module.exports = function (nodecg: NodeCG) {
+ nodecg.log.info("Sample bundle for the debug service started.");
+
+ const debug = requireService(nodecg, "debug");
+
+ debug?.onAvailable((debug) => {
+ nodecg.log.info("Debug service available.");
+
+ debug.onClick(() => {
+ nodecg.log.info(`Received in 'onClick'`);
+ });
+
+ debug.onNumber((value) => {
+ nodecg.log.info(`Received in 'onNumber' with number: ${value}`);
+ });
+
+ debug.onRange0to100((value) => {
+ nodecg.log.info(`Received in 'onRange0to100' with value: ${value}`);
+ });
+
+ debug.onColor((value) => {
+ nodecg.log.info(`Received in 'onColor' with [red,green,blue]: [${value.red},${value.green},${value.blue}]`);
+ });
+
+ debug.onDate((value) => {
+ nodecg.log.info(`Received in 'onDate' with date: ${value}`);
+ });
+
+ debug.onBool((value) => {
+ nodecg.log.info(`Received in 'onBool' with boolean: ${value}`);
+ });
+
+ debug.onText((value) => {
+ nodecg.log.info(`Received in 'onText' with string: ${value}`);
+ });
+
+ debug.onList((value) => {
+ nodecg.log.info(`Received in 'onList' with entries: [${value.join(",")}]`);
+ });
+
+ debug.onJSON((value) => {
+ nodecg.log.info(`Received in 'onJSON' with JSON: ${JSON.stringify(value)}`);
+ });
+ });
+
+ debug?.onUnavailable(() => {
+ nodecg.log.info("Debug service unavailable.");
+ });
+};
diff --git a/samples/debug/package.json b/samples/debug/package.json
new file mode 100644
index 000000000..ef1d599a7
--- /dev/null
+++ b/samples/debug/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "debug",
+ "version": "0.2.0",
+ "private": true,
+ "nodecg": {
+ "compatibleRange": "^1.1.1",
+ "bundleDependencies": {
+ "nodecg-io-debug": "^0.2.0"
+ }
+ },
+ "scripts": {
+ "build": "tsc -b",
+ "watch": "tsc -b -w",
+ "clean": "tsc -b --clean"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "^14.14.33",
+ "nodecg": "^1.8.1",
+ "nodecg-io-core": "^0.2.0",
+ "nodecg-io-debug": "^0.2.0",
+ "typescript": "^4.2.3"
+ }
+}
diff --git a/samples/debug/tsconfig.json b/samples/debug/tsconfig.json
new file mode 100644
index 000000000..c8bb01bee
--- /dev/null
+++ b/samples/debug/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../tsconfig.common.json"
+}