@web-applets/sdk 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +157 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.js +151 -0
- package/dist/context.d.ts +24 -0
- package/dist/context.js +103 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +21 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +33 -0
- package/package.json +30 -0
package/README.md
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# Web Applets
|
2
|
+
|
3
|
+
> An open SDK to create interoperable actions & views for agents – _a web of capabilities!_
|
4
|
+
|
5
|
+
## What is it?
|
6
|
+
|
7
|
+
Web Applets is a specification for modular, local web software that can be read and used by both humans and machines. Web Applets aims to be an interoperabe application layer for agents – instead of chatbots that can only interact in plain text, Web Applets allow them to actuate real software, read the results, and render rich, graphical views in response.
|
8
|
+
|
9
|
+
Did we mention it's _interoperable_? We think the future of software should be open & collaborative, not locked down to a single platform.
|
10
|
+
|
11
|
+
## Getting started
|
12
|
+
|
13
|
+
First, clone this repo and run `npm run install`. There are a few sample applets included in `/applets`. To install these applets and start the playground, run `npm run playground`.
|
14
|
+
|
15
|
+
The fastest way to create a new applet is by duplicating one of the applet folders in `/applets`. The folder title will be part of the URL of your applet, so make sure it doesn't include any spaces or other non-URL-safe characters.
|
16
|
+
|
17
|
+
Inside your applet folder, you'll find a basic web app setup:
|
18
|
+
|
19
|
+
- `public/manifest.json`: This file describes the Applet, and tells the model what actions are available and what parameters each action takes
|
20
|
+
- `index.html`: Much like a website, this holds the main page for your applet
|
21
|
+
- `main.js`: Declares functions that respond to each action, and a render function that updates the view based on state
|
22
|
+
|
23
|
+
> Want to use React? Svelte? Vue? – No problem, just install the dependencies and create an app the way you normally would in a website. So long as you're receiving the action events, it will all just work.
|
24
|
+
|
25
|
+
Let's say we want our applet to respond to a "set_name" action and render the user's name. In our `manifest.json` file we can write:
|
26
|
+
|
27
|
+
```js
|
28
|
+
{
|
29
|
+
// ...
|
30
|
+
"actions": [
|
31
|
+
{
|
32
|
+
"id": "set_name",
|
33
|
+
"description": "Sets the name of the user to be greeted",
|
34
|
+
"params": {
|
35
|
+
"name": {
|
36
|
+
"type": "string",
|
37
|
+
"description": "The name of the user"
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
]
|
42
|
+
}
|
43
|
+
```
|
44
|
+
|
45
|
+
Now let's update `main.ts` to assign an action handler:
|
46
|
+
|
47
|
+
```js
|
48
|
+
// First, import the SDK
|
49
|
+
import { appletContext } from '../../sdk/src';
|
50
|
+
|
51
|
+
// Now connect to the applet runtime
|
52
|
+
const applet = appletContext.connect();
|
53
|
+
|
54
|
+
// Attach the action handler, and update the state
|
55
|
+
applet.setActionHandler('set_name', ({ name }) => {
|
56
|
+
applet.setState({ name });
|
57
|
+
});
|
58
|
+
```
|
59
|
+
|
60
|
+
When this state updates, it will inform the client which can then store the state somewhere, for example in a database so the applet will persist between uses.
|
61
|
+
|
62
|
+
Finally, we need to render the applet whenever a render signal is received. Again in `main.ts`:
|
63
|
+
|
64
|
+
```js
|
65
|
+
// ...
|
66
|
+
|
67
|
+
applet.onrender = () => {
|
68
|
+
document.body.innerText = `Hello, ${applet.state.name}!`;
|
69
|
+
};
|
70
|
+
```
|
71
|
+
|
72
|
+
Now if you run `npm run playground` from the project root, you should be able to test out your new applet action directly. This applet will now work in any environment where the SDK is installed.
|
73
|
+
|
74
|
+
![A screenshot showing the 'playground' editing UI, with a web applets showing 'Hello, Web Applets'](docs/assets/web-applets-playground.png)
|
75
|
+
|
76
|
+
## Integrating Web Applets into your client
|
77
|
+
|
78
|
+
Integrating Web Applets is just as easy as creating one. First, in your project, make sure you have the sdk installed:
|
79
|
+
|
80
|
+
```bash
|
81
|
+
npm install @unternet/web-applets
|
82
|
+
```
|
83
|
+
|
84
|
+
In your code, you can import the applets client:
|
85
|
+
|
86
|
+
```js
|
87
|
+
import { applets } from '@unternet/web-applets';
|
88
|
+
```
|
89
|
+
|
90
|
+
Now you can create a new applet from a URL:
|
91
|
+
|
92
|
+
```js
|
93
|
+
const applet = await applets.load(
|
94
|
+
`https://unternet.co/applets/helloworld.applet`
|
95
|
+
);
|
96
|
+
applet.onstateupdated = (state) => console.log(state);
|
97
|
+
applet.dispatchAction('set_name', { name: 'Web Applets' });
|
98
|
+
// console.log: { name: "Web Applets" }
|
99
|
+
```
|
100
|
+
|
101
|
+
The above applet is actually running headless, but we can get it to display by attaching it to an iframe. For the loading step, instead run:
|
102
|
+
|
103
|
+
```js
|
104
|
+
const container = document.createElement('iframe');
|
105
|
+
document.body.appendChild(container);
|
106
|
+
const applet = await applets.load(
|
107
|
+
`https://unternet.co/applets/helloworld.applet`,
|
108
|
+
container
|
109
|
+
);
|
110
|
+
```
|
111
|
+
|
112
|
+
To load pre-existing saved state into an applet, simply set the state property:
|
113
|
+
|
114
|
+
```js
|
115
|
+
applet.state = { name: 'Ada Lovelace' };
|
116
|
+
// console.log: { name: "Ada Lovelace" }
|
117
|
+
```
|
118
|
+
|
119
|
+
It may also be helpful to check available applets at a domain, or in your local public folder if you've downloaded a set of Web Applets you want your product to use. For that you can extract the applet headers from the App Manifest at the public root (`/manifest.json`), and see the available applets and a shorthand for the actions you can take in them. This is automatically created when you build your applets.
|
120
|
+
|
121
|
+
```js
|
122
|
+
const headers = await applets.getHeaders('/');
|
123
|
+
```
|
124
|
+
|
125
|
+
This headers object looks like:
|
126
|
+
|
127
|
+
```js
|
128
|
+
[
|
129
|
+
{
|
130
|
+
name: 'Hello World',
|
131
|
+
description: 'Displays a greeting to the user.',
|
132
|
+
url: '/applets/helloworld.applet',
|
133
|
+
actions: [
|
134
|
+
{
|
135
|
+
id: 'set_name',
|
136
|
+
description: 'Sets the name of the user to be greeted',
|
137
|
+
params: {
|
138
|
+
name: 'The name of the user',
|
139
|
+
},
|
140
|
+
},
|
141
|
+
],
|
142
|
+
},
|
143
|
+
// ...
|
144
|
+
];
|
145
|
+
```
|
146
|
+
|
147
|
+
You can use it to present a quick summary of available tools to your model, and then decide on an applet and action to use.
|
148
|
+
|
149
|
+
## Feedback & Community
|
150
|
+
|
151
|
+
## License
|
152
|
+
|
153
|
+
[MIT](./LICENSE.md)
|
154
|
+
|
155
|
+
---
|
156
|
+
|
157
|
+
Built by [Unternet](https://unternet.co).
|
package/dist/client.d.ts
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
import { AppletAction, AppletHeader, AppletMessage, ActionParams, AppletManifest } from './types';
|
2
|
+
export declare function getHeaders(url: string): Promise<AppletHeader[]>;
|
3
|
+
export declare function getManifests(url: string): Promise<any[]>;
|
4
|
+
export declare function load(url: string, container: HTMLIFrameElement): Promise<Applet>;
|
5
|
+
export declare class Applet<T = unknown> extends EventTarget {
|
6
|
+
#private;
|
7
|
+
actions: AppletAction[];
|
8
|
+
manifest: AppletManifest;
|
9
|
+
container: HTMLIFrameElement;
|
10
|
+
constructor();
|
11
|
+
get state(): T;
|
12
|
+
set state(state: T);
|
13
|
+
toJson(): {
|
14
|
+
[k: string]: any;
|
15
|
+
};
|
16
|
+
resizeContainer(dimensions: {
|
17
|
+
height: number;
|
18
|
+
width: number;
|
19
|
+
}): void;
|
20
|
+
onstateupdated(event: CustomEvent): void;
|
21
|
+
disconnect(): void;
|
22
|
+
dispatchAction(actionId: string, params: ActionParams): Promise<AppletMessage<any>>;
|
23
|
+
}
|
24
|
+
export declare function loadManifest(url: string): Promise<AppletManifest>;
|
package/dist/client.js
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
6
|
+
};
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
|
+
};
|
12
|
+
var _Applet_state;
|
13
|
+
import { AppletMessage, } from './types';
|
14
|
+
const hiddenRoot = document.createElement('div');
|
15
|
+
hiddenRoot.style.display = 'none';
|
16
|
+
document.body.appendChild(hiddenRoot);
|
17
|
+
export async function getHeaders(url) {
|
18
|
+
url = parseUrl(url);
|
19
|
+
try {
|
20
|
+
const request = await fetch(`${url}/manifest.json`);
|
21
|
+
const appManifest = await request.json();
|
22
|
+
const appletHeaders = appManifest.applets;
|
23
|
+
return appletHeaders ?? [];
|
24
|
+
}
|
25
|
+
catch {
|
26
|
+
return [];
|
27
|
+
}
|
28
|
+
}
|
29
|
+
export async function getManifests(url) {
|
30
|
+
url = parseUrl(url);
|
31
|
+
const request = await fetch(`${url}/manifest.json`);
|
32
|
+
const headers = (await request.json()).applets;
|
33
|
+
const manifests = await Promise.all(headers.map(async (header) => {
|
34
|
+
const appletUrl = parseUrl(header.url);
|
35
|
+
const request = await fetch(`${appletUrl}/manifest.json`);
|
36
|
+
return await request.json();
|
37
|
+
}));
|
38
|
+
return manifests ?? [];
|
39
|
+
}
|
40
|
+
export async function load(url, container) {
|
41
|
+
url = parseUrl(url);
|
42
|
+
const manifest = await loadManifest(`${url}`);
|
43
|
+
const applet = new Applet();
|
44
|
+
applet.manifest = manifest;
|
45
|
+
applet.actions = manifest.actions; // let the events set this later
|
46
|
+
applet.container = container;
|
47
|
+
container.src = applet.manifest.entrypoint;
|
48
|
+
if (!container.isConnected)
|
49
|
+
hiddenRoot.appendChild(container);
|
50
|
+
return new Promise((resolve) => {
|
51
|
+
window.addEventListener('message', (message) => {
|
52
|
+
if (message.source !== container.contentWindow)
|
53
|
+
return;
|
54
|
+
if (message.data.type === 'ready')
|
55
|
+
resolve(applet);
|
56
|
+
});
|
57
|
+
});
|
58
|
+
}
|
59
|
+
export class Applet extends EventTarget {
|
60
|
+
constructor() {
|
61
|
+
super();
|
62
|
+
this.actions = [];
|
63
|
+
_Applet_state.set(this, void 0);
|
64
|
+
window.addEventListener('message', (message) => {
|
65
|
+
if (message.source !== this.container.contentWindow)
|
66
|
+
return;
|
67
|
+
if (message.data.type === 'state') {
|
68
|
+
__classPrivateFieldSet(this, _Applet_state, message.data.state, "f");
|
69
|
+
this.dispatchEvent(new CustomEvent('stateupdated', { detail: message.data.detail }));
|
70
|
+
this.onstateupdated(message.data.state);
|
71
|
+
}
|
72
|
+
if (message.data.type === 'resize') {
|
73
|
+
this.resizeContainer(message.data.dimensions);
|
74
|
+
}
|
75
|
+
this.container.contentWindow?.postMessage({
|
76
|
+
type: 'resolve',
|
77
|
+
id: message.data.id,
|
78
|
+
}, '*');
|
79
|
+
});
|
80
|
+
}
|
81
|
+
get state() {
|
82
|
+
return __classPrivateFieldGet(this, _Applet_state, "f");
|
83
|
+
}
|
84
|
+
set state(state) {
|
85
|
+
__classPrivateFieldSet(this, _Applet_state, state, "f");
|
86
|
+
const stateMessage = new AppletMessage('state', { state });
|
87
|
+
this.container.contentWindow?.postMessage(stateMessage.toJson(), '*');
|
88
|
+
}
|
89
|
+
toJson() {
|
90
|
+
return Object.fromEntries(Object.entries(this).filter(([_, value]) => {
|
91
|
+
try {
|
92
|
+
JSON.stringify(value);
|
93
|
+
return true;
|
94
|
+
}
|
95
|
+
catch {
|
96
|
+
return false;
|
97
|
+
}
|
98
|
+
}));
|
99
|
+
}
|
100
|
+
resizeContainer(dimensions) {
|
101
|
+
this.container.style.height = `${dimensions.height}px`;
|
102
|
+
// if (!this.#styleOverrides) {
|
103
|
+
// this.#container.style.height = `${dimensions.height}px`;
|
104
|
+
// }
|
105
|
+
}
|
106
|
+
onstateupdated(event) { }
|
107
|
+
disconnect() { }
|
108
|
+
async dispatchAction(actionId, params) {
|
109
|
+
const requestMessage = new AppletMessage('action', {
|
110
|
+
actionId,
|
111
|
+
params,
|
112
|
+
});
|
113
|
+
this.container.contentWindow?.postMessage(requestMessage.toJson(), '*');
|
114
|
+
return new Promise((resolve) => {
|
115
|
+
const listener = (messageEvent) => {
|
116
|
+
if (messageEvent.source !== this.container.contentWindow)
|
117
|
+
return;
|
118
|
+
const responseMessage = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
119
|
+
if (responseMessage.type === 'resolve' &&
|
120
|
+
responseMessage.id === requestMessage.id) {
|
121
|
+
window.removeEventListener('message', listener);
|
122
|
+
resolve(responseMessage);
|
123
|
+
}
|
124
|
+
};
|
125
|
+
window.addEventListener('message', listener);
|
126
|
+
});
|
127
|
+
}
|
128
|
+
}
|
129
|
+
_Applet_state = new WeakMap();
|
130
|
+
function parseUrl(url, base) {
|
131
|
+
if (['http', 'https'].includes(url.split('://')[0])) {
|
132
|
+
return url;
|
133
|
+
}
|
134
|
+
let path = url;
|
135
|
+
if (path.startsWith('/'))
|
136
|
+
path = path.slice(1);
|
137
|
+
if (path.endsWith('/'))
|
138
|
+
path = path.slice(0, -1);
|
139
|
+
url = `${base || window.location.origin}/${path}`;
|
140
|
+
return url;
|
141
|
+
}
|
142
|
+
export async function loadManifest(url) {
|
143
|
+
url = parseUrl(url);
|
144
|
+
const request = await fetch(`${url}/manifest.json`);
|
145
|
+
const appletManifest = await request.json();
|
146
|
+
if (appletManifest.type !== 'applet') {
|
147
|
+
throw new Error("URL doesn't point to a valid applet manifest.");
|
148
|
+
}
|
149
|
+
appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
|
150
|
+
return appletManifest;
|
151
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { ActionHandlerDict, AppletState, AppletMessage, AppletMessageType, AppletMessageCallback, ActionParams, ActionHandler } from './types';
|
2
|
+
/**
|
3
|
+
* Context
|
4
|
+
*/
|
5
|
+
export declare class AppletContext<StateType = AppletState> extends EventTarget {
|
6
|
+
client: AppletClient;
|
7
|
+
actionHandlers: ActionHandlerDict;
|
8
|
+
state: StateType;
|
9
|
+
connect(): this;
|
10
|
+
setActionHandler<T extends ActionParams>(actionId: string, handler: ActionHandler<T>): void;
|
11
|
+
setState(state: StateType): Promise<void>;
|
12
|
+
onload(): Promise<void> | void;
|
13
|
+
onready(): Promise<void> | void;
|
14
|
+
onrender(): void;
|
15
|
+
}
|
16
|
+
/**
|
17
|
+
* Client
|
18
|
+
*/
|
19
|
+
declare class AppletClient {
|
20
|
+
on(messageType: AppletMessageType, callback: AppletMessageCallback): void;
|
21
|
+
send(message: AppletMessage): Promise<void>;
|
22
|
+
}
|
23
|
+
export declare const appletContext: AppletContext<AppletState>;
|
24
|
+
export {};
|
package/dist/context.js
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
import { AppletMessage, } from './types';
|
2
|
+
/**
|
3
|
+
* Context
|
4
|
+
*/
|
5
|
+
export class AppletContext extends EventTarget {
|
6
|
+
constructor() {
|
7
|
+
super(...arguments);
|
8
|
+
this.actionHandlers = {};
|
9
|
+
}
|
10
|
+
connect() {
|
11
|
+
this.client = new AppletClient();
|
12
|
+
const startup = async () => {
|
13
|
+
await this.onload();
|
14
|
+
this.client.send(new AppletMessage('ready'));
|
15
|
+
this.dispatchEvent(new CustomEvent('ready'));
|
16
|
+
await this.onready();
|
17
|
+
};
|
18
|
+
if (document.readyState === 'complete' ||
|
19
|
+
document.readyState === 'interactive') {
|
20
|
+
setTimeout(startup, 1);
|
21
|
+
}
|
22
|
+
else {
|
23
|
+
window.addEventListener('DOMContentLoaded', startup);
|
24
|
+
}
|
25
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
26
|
+
for (let entry of entries) {
|
27
|
+
const message = new AppletMessage('resize', {
|
28
|
+
dimensions: {
|
29
|
+
width: entry.contentRect.width,
|
30
|
+
height: entry.contentRect.height,
|
31
|
+
},
|
32
|
+
});
|
33
|
+
this.client.send(message);
|
34
|
+
}
|
35
|
+
});
|
36
|
+
resizeObserver.observe(document.querySelector('html'));
|
37
|
+
this.client.on('state', (message) => {
|
38
|
+
if (!isStateMessage(message)) {
|
39
|
+
throw new TypeError("Message doesn't match type StateMessage");
|
40
|
+
}
|
41
|
+
this.setState(message.state);
|
42
|
+
});
|
43
|
+
this.client.on('action', async (message) => {
|
44
|
+
if (!isActionMessage(message)) {
|
45
|
+
throw new TypeError("Message doesn't match type AppletMessage.");
|
46
|
+
}
|
47
|
+
if (Object.keys(this.actionHandlers).includes(message.actionId)) {
|
48
|
+
await this.actionHandlers[message.actionId](message.params);
|
49
|
+
}
|
50
|
+
message.resolve();
|
51
|
+
});
|
52
|
+
return this;
|
53
|
+
}
|
54
|
+
setActionHandler(actionId, handler) {
|
55
|
+
this.actionHandlers[actionId] = handler;
|
56
|
+
}
|
57
|
+
async setState(state) {
|
58
|
+
const message = new AppletMessage('state', { state });
|
59
|
+
await this.client.send(message);
|
60
|
+
this.state = state;
|
61
|
+
this.dispatchEvent(new CustomEvent('render'));
|
62
|
+
this.onrender(); // TODO: Should come from client? Or stay here, and only activate if mounted? Need a control for mounting.
|
63
|
+
}
|
64
|
+
onload() { }
|
65
|
+
onready() { }
|
66
|
+
onrender() { }
|
67
|
+
}
|
68
|
+
function isActionMessage(message) {
|
69
|
+
return message.type === 'action';
|
70
|
+
}
|
71
|
+
function isStateMessage(message) {
|
72
|
+
return message.type === 'state';
|
73
|
+
}
|
74
|
+
/**
|
75
|
+
* Client
|
76
|
+
*/
|
77
|
+
class AppletClient {
|
78
|
+
on(messageType, callback) {
|
79
|
+
window.addEventListener('message', (messageEvent) => {
|
80
|
+
if (messageEvent.data.type !== messageType)
|
81
|
+
return;
|
82
|
+
const message = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
83
|
+
message.resolve = () => {
|
84
|
+
window.parent.postMessage(new AppletMessage('resolve', { id: message.id }), '*');
|
85
|
+
};
|
86
|
+
callback(message);
|
87
|
+
});
|
88
|
+
}
|
89
|
+
send(message) {
|
90
|
+
window.parent.postMessage(message.toJson(), '*');
|
91
|
+
return new Promise((resolve) => {
|
92
|
+
const listener = (messageEvent) => {
|
93
|
+
if (messageEvent.data.type === 'resolve' &&
|
94
|
+
messageEvent.data.id === message.id) {
|
95
|
+
window.removeEventListener('message', listener);
|
96
|
+
resolve();
|
97
|
+
}
|
98
|
+
};
|
99
|
+
window.addEventListener('message', listener);
|
100
|
+
});
|
101
|
+
}
|
102
|
+
}
|
103
|
+
export const appletContext = new AppletContext();
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
export interface AppletManifest {
|
2
|
+
type: 'applet';
|
3
|
+
name: string;
|
4
|
+
description: string;
|
5
|
+
icon?: string;
|
6
|
+
entrypoint: string;
|
7
|
+
actions: AppletAction[];
|
8
|
+
}
|
9
|
+
export interface AppletAction {
|
10
|
+
id: string;
|
11
|
+
description: string;
|
12
|
+
params?: ActionParamSchema;
|
13
|
+
}
|
14
|
+
export interface AppletHeader {
|
15
|
+
name: string;
|
16
|
+
description: string;
|
17
|
+
url: string;
|
18
|
+
params: {
|
19
|
+
[key: string]: string;
|
20
|
+
};
|
21
|
+
}
|
22
|
+
export type AppletState = Record<string, Serializable>;
|
23
|
+
export type ActionParamSchema = Record<string, {
|
24
|
+
description: string;
|
25
|
+
type: 'string';
|
26
|
+
}>;
|
27
|
+
export type ActionParams = Record<string, unknown>;
|
28
|
+
export type ActionHandlerDict = {
|
29
|
+
[key: string]: ActionHandler<any>;
|
30
|
+
};
|
31
|
+
export type ActionHandler<T extends ActionParams> = (params: T) => void | Promise<void>;
|
32
|
+
export type AnyAppletMessage = AppletMessage | AppletStateMessage | AppletActionMessage;
|
33
|
+
export interface AppletStateMessage<T = any> extends AppletMessage {
|
34
|
+
type: 'state';
|
35
|
+
state: T;
|
36
|
+
}
|
37
|
+
export interface AppletActionMessage<T = any> extends AppletMessage {
|
38
|
+
type: 'action';
|
39
|
+
actionId: string;
|
40
|
+
params: T;
|
41
|
+
}
|
42
|
+
export declare class AppletMessage<T = any> {
|
43
|
+
type: AppletMessageType;
|
44
|
+
id: string;
|
45
|
+
timeStamp: number;
|
46
|
+
constructor(type: AppletMessageType, values?: T);
|
47
|
+
toJson(): {
|
48
|
+
[k: string]: any;
|
49
|
+
};
|
50
|
+
resolve(): void;
|
51
|
+
}
|
52
|
+
export type AppletMessageType = 'action' | 'render' | 'state' | 'ready' | 'resolve' | 'resize';
|
53
|
+
export type AppletMessageCallback = (message: AnyAppletMessage) => void;
|
54
|
+
type Serializable = string | number | boolean | null | Serializable[] | {
|
55
|
+
[key: string]: Serializable;
|
56
|
+
};
|
57
|
+
export {};
|
package/dist/types.js
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
export class AppletMessage {
|
2
|
+
constructor(type, values) {
|
3
|
+
this.timeStamp = Date.now();
|
4
|
+
this.type = type;
|
5
|
+
this.id = crypto.randomUUID();
|
6
|
+
if (values)
|
7
|
+
Object.assign(this, values);
|
8
|
+
}
|
9
|
+
toJson() {
|
10
|
+
return Object.fromEntries(Object.entries(this).filter(([_, value]) => {
|
11
|
+
try {
|
12
|
+
JSON.stringify(value);
|
13
|
+
return true;
|
14
|
+
}
|
15
|
+
catch {
|
16
|
+
return false;
|
17
|
+
}
|
18
|
+
}));
|
19
|
+
}
|
20
|
+
resolve() { }
|
21
|
+
}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
function parseUrl(url, base) {
|
2
|
+
if (['http', 'https'].includes(url.split('://')[0])) {
|
3
|
+
return url;
|
4
|
+
}
|
5
|
+
let path = url;
|
6
|
+
if (path.startsWith('/'))
|
7
|
+
path = path.slice(1);
|
8
|
+
if (path.endsWith('/'))
|
9
|
+
path = path.slice(0, -1);
|
10
|
+
url = `${base || window.location.origin}/${path}`;
|
11
|
+
return url;
|
12
|
+
}
|
13
|
+
export async function getAppletsList(url) {
|
14
|
+
url = parseUrl(url);
|
15
|
+
try {
|
16
|
+
const request = await fetch(`${url}/manifest.json`);
|
17
|
+
const appManifest = await request.json();
|
18
|
+
return appManifest.applets ? appManifest.applets : [];
|
19
|
+
}
|
20
|
+
catch {
|
21
|
+
return [];
|
22
|
+
}
|
23
|
+
}
|
24
|
+
export async function loadAppletManifest(url) {
|
25
|
+
url = parseUrl(url);
|
26
|
+
const request = await fetch(`${url}/manifest.json`);
|
27
|
+
const appletManifest = await request.json();
|
28
|
+
if (appletManifest.type !== 'applet') {
|
29
|
+
throw new Error("URL doesn't point to a valid applet manifest.");
|
30
|
+
}
|
31
|
+
appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
|
32
|
+
return appletManifest;
|
33
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
{
|
2
|
+
"name": "@web-applets/sdk",
|
3
|
+
"version": "0.0.4",
|
4
|
+
"description": "The Web Applets SDK, for creating & hosting Web Applets.",
|
5
|
+
"author": "Rupert Manfredi <rupert@unternet.co>",
|
6
|
+
"license": "MIT",
|
7
|
+
"type": "module",
|
8
|
+
"main": "dist/index.js",
|
9
|
+
"types": "dist/index.d.ts",
|
10
|
+
"files": [
|
11
|
+
"dist"
|
12
|
+
],
|
13
|
+
"publishConfig": {
|
14
|
+
"access": "public"
|
15
|
+
},
|
16
|
+
"repository": {
|
17
|
+
"type": "git",
|
18
|
+
"url": "git+https://github.com/unternet-co/web-applets.git"
|
19
|
+
},
|
20
|
+
"scripts": {
|
21
|
+
"build": "tsc && cp ../README.md ./README.md",
|
22
|
+
"prepublishOnly": "npm run build"
|
23
|
+
},
|
24
|
+
"devDependencies": {
|
25
|
+
"typescript": "^5.6.2"
|
26
|
+
},
|
27
|
+
"dependencies": {
|
28
|
+
"vite": "^5.4.7"
|
29
|
+
}
|
30
|
+
}
|