diff --git a/src/Workspace.ts b/src/Workspace.ts index f22fba5..3148a47 100644 --- a/src/Workspace.ts +++ b/src/Workspace.ts @@ -1,13 +1,29 @@ +import * as engine from "./index.js"; +import * as stateMachine from "@mixery/state-machine"; import { LiveNote } from "./midi/Note.js"; +import { Addon } from "./addons/Addon.js"; +import { ExternalAddonLoader } from "./addons/ExternalAddonLoader.js"; export class Workspace { + addonRequires = new Map(); + addons: Addon[] = []; + public constructor( public readonly audioContext: BaseAudioContext - ) {} + ) { + this.addonRequires.set("@mixery/engine", engine); + this.addonRequires.set("@mixery/state-machine", stateMachine); + } private _lastNoteId = 0; public autoNoteId(note: LiveNote) { note.id = this._lastNoteId++; } + + async loadAddonFromUrl(url: string) { + let addon = await ExternalAddonLoader.loadFromUrl(this, url); + this.addons.push(addon); + return addon; + } } \ No newline at end of file diff --git a/src/addons/Addon.ts b/src/addons/Addon.ts index 8ad3fa5..2e597f3 100644 --- a/src/addons/Addon.ts +++ b/src/addons/Addon.ts @@ -8,6 +8,11 @@ export interface AddonInfo { description?: string; } +export interface AddonMetadata extends AddonInfo { + id: string; + main: string; +} + export type AddonFactory = (addon: Addon) => any; export class Addon { diff --git a/src/addons/ExternalAddonLoader.ts b/src/addons/ExternalAddonLoader.ts new file mode 100644 index 0000000..c5d7de0 --- /dev/null +++ b/src/addons/ExternalAddonLoader.ts @@ -0,0 +1,46 @@ +import { Addon, AddonMetadata } from "../addons/Addon.js"; +import { Workspace } from "../Workspace.js"; + +export class ExternalAddonLoader { + metadata: AddonMetadata; + addon: Addon; + + constructor( + public readonly workspace: Workspace + ) {} + + static async loadFromUrl(workspace: Workspace, url: string) { + // Security risk: Loading addons from URLs allows scripts to have access to your + // data (projects, presets, etc) that is stored inside Mixery. + + if (url.endsWith("/")) url = url.substring(0, url.length - 1); + const metadata: AddonMetadata = await (await fetch(`${url}/addon.metadata.json`)).json(); + const script = await (await fetch(`${url}/${metadata.main}`)).text(); + const loader = new ExternalAddonLoader(workspace); + + loader.metadata = metadata; + loader.createAddonInstance(); + loader.runScript(script); + return loader.addon; + } + + require(name: string) { + let module = this.workspace.addonRequires.get(name); + if (!module) throw new Error(`Can't find module ${name} (Have you registered your module using Workspace#addonRequires.set() yet?).`); + return module; + } + + createAddonInstance() { + this.addon = new Addon(this.metadata.id, this.metadata); + } + + runScript(script: string) { + let func = new Function("require", "id", "metadata", "addon", script); + func( + (name: string) => this.require(name), + this.metadata.id, + this.metadata, + this.addon + ); + } +} \ No newline at end of file diff --git a/src/test.workspace.ts b/src/test.workspace.ts index 0dd41e9..cc91d8b 100644 --- a/src/test.workspace.ts +++ b/src/test.workspace.ts @@ -78,4 +78,7 @@ function render() { ctx.stroke(); ctx.closePath(); } -window.requestAnimationFrame(render); \ No newline at end of file +window.requestAnimationFrame(render); + +// Expose some methods +globalThis.loadAddonFromUrl = (url: string) => workspace.loadAddonFromUrl(url); \ No newline at end of file