mirror of
https://github.com/HeyPuter/puter.git
synced 2025-01-23 14:20:22 +08:00
doc(backend): document modules, services, and service-scripts
This commit is contained in:
parent
6ef283d7ef
commit
f88c4a5c9c
104
packages/backend/doc/contributors/modules.md
Normal file
104
packages/backend/doc/contributors/modules.md
Normal file
@ -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
packages/backend/doc/contributors/service-scripts.md
Normal file
150
packages/backend/doc/contributors/service-scripts.md
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user