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.
This commit is contained in:
KernelDeimos
2025-09-22 18:34:50 -04:00
committed by KernelDeimos
parent 860388c3cb
commit 9aef281ad2
5 changed files with 81 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
//@puter priority -1
console.log('exporting something...');
runtime.exports = {
testval: 5
};

View File

@@ -0,0 +1,3 @@
console.log('importing something...');
const { testval } = runtime.import('exports_something');
console.log(testval);

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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
};