ソースを参照

doc(backend): document modules, services, and service-scripts

KernelDeimos 11 ヶ月 前
コミット
f88c4a5c9c

+ 104 - 0
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'] () {}
+}
+```

+ 150 - 0
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: '<h1>Some Heading</h1>',
+                }),
+                new NotifCard({
+                    text: 'I am a card with some text',
+                    style: 'settings-card-success',
+                }),
+                new ActionCard({
+                    title: 'Open an Alert',
+                    button_text: 'Click Me',
+                    on_click: async () => {
+                        // Here we open an example window
+                        await UIAlert({
+                            message: 'Hello, Puter!',
+                        });
+                    }
+                })
+            ]
+        });
+
+        return component;
+    }
+});
+```