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