diff --git a/packages/backend/doc/contributors/modules.md b/packages/backend/doc/contributors/modules.md new file mode 100644 index 00000000..5d00e4aa --- /dev/null +++ b/packages/backend/doc/contributors/modules.md @@ -0,0 +1,104 @@ +# Puter Kernel Moduels and Services + +## Modules + +A Puter kernel module is simply a collection of services that run when +the module is installed. You can find an example of this in the +`run-selfhosted.js` script at the root of the Puter monorepo. + +Here is the relevant excerpt in `run-selfhosted.js` at the time of +writing this documentation: + +```javascript +const { + Kernel, + CoreModule, + DatabaseModule, + LocalDiskStorageModule, + SelfHostedModule +} = (await import('@heyputer/backend')).default; + +console.log('kerne', Kernel); +const k = new Kernel(); +k.add_module(new CoreModule()); +k.add_module(new DatabaseModule()); +k.add_module(new LocalDiskStorageModule()); +k.add_module(new SelfHostedModule()); +k.boot(); +``` + +A few modules are added to Puter before booting. If you want to install +your own modules into Puter you can edit this file for self-hosted runs +or create your own script that boots Puter. This makes it possible to +have deployments of Puter with custom functionality. + +To function properly, Puter needs **CoreModule**, a database module, +and a storage module. + +A module extends +[AdvancedBase](../../../puter-js-common/README.md) +and implements +an `install` method. The install method has one parameter, a +[Context](../../src/util/context.js) +object containing all the values kernel modules have access to. This +includes the `services` +[Container](../../src/services/Container.js`). + +A module adds services to Puter.eA typical module may look something +like this: + +```javascript +class MyPuterModule extends AdvancedBase { + async install (context) { + const services = context.get('services'); + + const MyService = require('./path/to/MyService.js'); + services.registerService('my-service', MyService, { + some_options: 'for-my-service', + }); + } +} +``` + +## Services + +Services extend +[BaseService](../../src/services/BaseService.js) +and provide additional functionality for Puter. They can add HTTP +endpoints and register objects with other services. + +When implementing a service it is important to understand +Puter's [boot sequence](./boot-sequence.md) + +A typical service may look like this: + +```javascript +class MyService extends BaseService { + static MODULES = { + // Use node's `require` function to populate this object; + // this makes these available to `this.require` and offers + // dependency-injection for unit testing. + ['some-module']: require('some-module') + } + + // Do not override the constructor of BaseService - use this instead! + async _construct () { + this.my_list = []; + } + + // This method is called after _construct has been called on all + // other services. + async _init () { + const services = this.services; + + // We can get the instances of other services here + const svc_otherService = services.get('other-service'); + } + + // The service container can listen on the "service event bus" + async ['__on_boot.consolidation'] () {} + async ['__on_boot.activation'] () {} + async ['__on_start.webserver'] () {} + async ['__on_install.routes'] () {} +} +``` diff --git a/packages/backend/doc/contributors/service-scripts.md b/packages/backend/doc/contributors/service-scripts.md new file mode 100644 index 00000000..9fdea19e --- /dev/null +++ b/packages/backend/doc/contributors/service-scripts.md @@ -0,0 +1,150 @@ +> **NOTICE:** This documentation is new and might contain errors. +> Feel free to open a Github issue if you run into any problems. + +# Service Scripts + +## What is a Service Script? + +Service scripts allow backend services to provide client-side code that +runs in Puter's GUI. This is useful if you want to make a mod or plugin +for Puter that has backend functionality. For example, you might want +to add a tab to the settings panel to make use of or configure the service. + +Service scripts are made possible by the `puter-homepage` service, which +allows you to register URLs for additional javascript files Puter's +GUI should load. + +## ES Modules - A Problem of Ordering + +In browsers, script tags with `type=module` implicitly behave according +to those with the `defer` attribute. This means after the DOM is loaded +the scripts will run in the order in which they appear in the document. + +Relying on this execution order however does not work. This is because +`import` is implicitly asynchronous. Effectively, this means these +scripts will execute in arbitrary order if they all have imports. + +In a situation where all the client-side code is bundled with rollup +or webpack this is not an issue as you typically only have one +entry script. To facilitate loading service scripts, which are not +bundled with the GUI, we require that service scripts call the global +`service_script` function to access the API for service scripts. + +## Providing a Service Script + +For a service to provide a service script, it simply needs to serve +static files (the "service script") on some URL, and register that +URL with the `puter-homepage` service. + +In this example below we use builtin functionality of express to serve +static files. + +```javascript +class MyService extends BaseService { + async _init () { + // First we tell `puter-homepage` that we're going to be serving + // a javascript file which we want to be included when the GUI + // loads. + const svc_puterHomepage = this.services.get('puter-homepage'); + svc_puterHomepage.register_script('/my-service-script/main.js'); + } + + async ['__on_install.routes'] (_, { app }) { + // Here we ask express to serve our script. This is made possible + // by WebServerService which provides the `app` object when it + // emits the 'install.routes` event. + app.use('/my-service-script', + express.static( + PathBuilder.add(__dirname).add('gui').build() + ) + ); + } +} +``` + +## A Simple Service Script + + + +```javascript +import SomeModule from "./SomeModule.js"; + +service_script(api => { + api.on_ready(() => { + // This callback is invoked when the GUI is ready + + // We can use api.get() to import anything exposed to + // service scripts by Puter's GUI; for example: + const Button = api.use('ui.components.Button'); + // ^ Here we get Puter's Button component, which is made + // available to service scripts. + }); +}); +``` + +## Adding a Settings Tab + +Starting with the following example: + +```javascript +import MySettingsTab from "./MySettingsTab.js"; + +globalThis.service_script(api => { + api.on_ready(() => { + const svc_settings = globalThis.services.get('settings'); + svc_settings.register_tab(MySettingsTab(api)); + }); +}); +``` + +The module **MySettingsTab** exports a function for scoping the `api` +object, and that function returns a settings tab. The settings tab is +an object with a specific format that Puter's settings window understands. + +Here are the contents of `MySettingsTab.js`: + +```javascript +import MyWindow from "./MyWindow.js"; + +export default api => ({ + id: 'my-settings-tab', + title_i18n_key: 'My Settings Tab', + icon: 'shield.svg', + factory: () => { + const NotifCard = api.use('ui.component.NotifCard'); + const ActionCard = api.use('ui.component.ActionCard'); + const JustHTML = api.use('ui.component.JustHTML'); + const Flexer = api.use('ui.component.Flexer'); + const UIAlert = api.use('ui.window.UIAlert'); + + // The root component for our settings tab will be a "flexer", + // which by default displays its child components in a vertical + // layout. + const component = new Flexer({ + children: [ + // We can insert raw HTML as a component + new JustHTML({ + no_shadow: true, // use CSS for settings window + html: '