From 9aef281ad25c656c0aab5c97e1b971441ea8dd5d Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Mon, 22 Sep 2025 18:34:50 -0400 Subject: [PATCH] dev: add runtime modules for imports and exports RuntimeModule is a new construct which represents the import/export mechanism for Puter extensions. This can be thought of as analogous to node's Module class. These are called "runtime modules" because extensions have less control of what they're consuming than a typical npm module; i.e. `runtime.import('database')` will provide a version of 'database' that's compatible with the importing extension, but the version used can't be locked or otherwise determined by the extension. --- extensions/exports_something.js | 5 +++ extensions/imports_something.js | 3 ++ src/backend/src/Kernel.js | 9 +++++ src/backend/src/extension/RuntimeModule.js | 30 ++++++++++++++++ .../src/extension/RuntimeModuleRegistry.js | 34 +++++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 extensions/exports_something.js create mode 100644 extensions/imports_something.js create mode 100644 src/backend/src/extension/RuntimeModule.js create mode 100644 src/backend/src/extension/RuntimeModuleRegistry.js diff --git a/extensions/exports_something.js b/extensions/exports_something.js new file mode 100644 index 00000000..53c8424e --- /dev/null +++ b/extensions/exports_something.js @@ -0,0 +1,5 @@ +//@puter priority -1 +console.log('exporting something...'); +runtime.exports = { + testval: 5 +}; diff --git a/extensions/imports_something.js b/extensions/imports_something.js new file mode 100644 index 00000000..15eaa304 --- /dev/null +++ b/extensions/imports_something.js @@ -0,0 +1,3 @@ +console.log('importing something...'); +const { testval } = runtime.import('exports_something'); +console.log(testval); diff --git a/src/backend/src/Kernel.js b/src/backend/src/Kernel.js index c805b013..786ed87e 100644 --- a/src/backend/src/Kernel.js +++ b/src/backend/src/Kernel.js @@ -32,6 +32,8 @@ const { prependToJSFiles } = require("./kernel/modutil"); const uuid = require('uuid'); const readline = require("node:readline/promises"); +const { RuntimeModuleRegistry } = require("./extension/RuntimeModuleRegistry"); +const { RuntimeModule } = require("./extension/RuntimeModule"); const { quot } = libs.string; @@ -50,6 +52,8 @@ class Kernel extends AdvancedBase { this.entry_path = entry_path; this.extensionExports = {}; this.registry = {}; + + this.runtimeModuleRegistry = new RuntimeModuleRegistry(); } add_module (module) { @@ -380,6 +384,7 @@ class Kernel extends AdvancedBase { `const { use: puter } = globalThis.__puter_extension_globals__.useapi;`, `const extension = globalThis.__puter_extension_globals__` + `.extensionObjectRegistry[${JSON.stringify(extension_id)}];`, + `const runtime = extension.runtime;`, `const config = extension.config;`, `const registry = extension.registry;`, `const register = registry.register;`, @@ -392,6 +397,10 @@ class Kernel extends AdvancedBase { const mod = new ExtensionModule(); mod.extension = new Extension(); + + const runtimeModule = new RuntimeModule({ name: mod_name }); + this.runtimeModuleRegistry.register(runtimeModule); + mod.extension.runtime = runtimeModule; mod_entry.module = mod; diff --git a/src/backend/src/extension/RuntimeModule.js b/src/backend/src/extension/RuntimeModule.js new file mode 100644 index 00000000..ab6f11d7 --- /dev/null +++ b/src/backend/src/extension/RuntimeModule.js @@ -0,0 +1,30 @@ +const { AdvancedBase } = require("@heyputer/putility"); + +class RuntimeModule extends AdvancedBase { + constructor (options = {}) { + super(); + this.exports_ = undefined; + this.exports_is_set_ = false; + this.remappings = options.remappings ?? {}; + + this.name = options.name ?? undefined; + } + set exports (value) { + this.exports_is_set_ = true; + this.exports_ = value; + } + get exports () { + if ( this.exports_is_set_ === false && this.defer ) { + this.exports = this.defer(); + } + return this.exports_; + } + import (name) { + if ( this.remappings.hasOwnProperty(name) ) { + name = this.remappings[name]; + } + return this.runtimeModuleRegistry.exportsOf(name); + } +} + +module.exports = { RuntimeModule }; diff --git a/src/backend/src/extension/RuntimeModuleRegistry.js b/src/backend/src/extension/RuntimeModuleRegistry.js new file mode 100644 index 00000000..fe28c844 --- /dev/null +++ b/src/backend/src/extension/RuntimeModuleRegistry.js @@ -0,0 +1,34 @@ +const { AdvancedBase } = require("@heyputer/putility"); +const { RuntimeModule } = require("./RuntimeModule"); + +class RuntimeModuleRegistry extends AdvancedBase { + constructor () { + super(); + this.modules_ = {}; + } + + register (extensionModule, options = {}) { + if ( ! (extensionModule instanceof RuntimeModule) ) { + throw new Error(`expected a RuntimeModule, but got: ${ + extensionModule?.constructor?.name ?? typeof extensionModule})`); + } + const uniqueName = options.as ?? extensionModule.name ?? require('uuid').v4(); + if ( this.modules_.hasOwnProperty(uniqueName) ) { + throw new Error(`duplicate runtime module: ${uniqueName}`); + } + console.log(`registering with name... ${uniqueName}`); + this.modules_[uniqueName] = extensionModule; + extensionModule.runtimeModuleRegistry = this; + } + + exportsOf (name) { + if ( ! this.modules_[name] ) { + throw new Error(`could not find runtime module: ${name}`); + } + return this.modules_[name].exports; + } +} + +module.exports = { + RuntimeModuleRegistry +};