lastriko 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +58 -0
- package/dist/__tests__/integration/ws-flow.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/ws-flow.integration.test.js +249 -0
- package/dist/__tests__/integration/ws-flow.integration.test.js.map +1 -0
- package/dist/__tests__/integration/ws-flow.test.d.ts +1 -0
- package/dist/__tests__/integration/ws-flow.test.js +70 -0
- package/dist/__tests__/integration/ws-flow.test.js.map +1 -0
- package/dist/client/events.d.ts +6 -0
- package/dist/client/events.js +114 -0
- package/dist/client/events.js.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/swap.d.ts +4 -0
- package/dist/client/swap.js +47 -0
- package/dist/client/swap.js.map +1 -0
- package/dist/client/ws.d.ts +11 -0
- package/dist/client/ws.js +111 -0
- package/dist/client/ws.js.map +1 -0
- package/dist/components/context.d.ts +51 -0
- package/dist/components/context.js +595 -0
- package/dist/components/context.js.map +1 -0
- package/dist/components/context.test.d.ts +1 -0
- package/dist/components/context.test.js +69 -0
- package/dist/components/context.test.js.map +1 -0
- package/dist/components/id.d.ts +3 -0
- package/dist/components/id.js +9 -0
- package/dist/components/id.js.map +1 -0
- package/dist/components/registry.d.ts +2 -0
- package/dist/components/registry.js +97 -0
- package/dist/components/registry.js.map +1 -0
- package/dist/components/types.d.ts +390 -0
- package/dist/components/types.js +2 -0
- package/dist/components/types.js.map +1 -0
- package/dist/engine/executor.d.ts +20 -0
- package/dist/engine/executor.js +84 -0
- package/dist/engine/executor.js.map +1 -0
- package/dist/engine/executor.test.d.ts +1 -0
- package/dist/engine/executor.test.js +79 -0
- package/dist/engine/executor.test.js.map +1 -0
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/lifecycle.d.ts +16 -0
- package/dist/engine/lifecycle.js +44 -0
- package/dist/engine/lifecycle.js.map +1 -0
- package/dist/engine/lifecycle.test.d.ts +1 -0
- package/dist/engine/lifecycle.test.js +15 -0
- package/dist/engine/lifecycle.test.js.map +1 -0
- package/dist/engine/messages.d.ts +95 -0
- package/dist/engine/messages.js +21 -0
- package/dist/engine/messages.js.map +1 -0
- package/dist/engine/renderer.components.test.d.ts +1 -0
- package/dist/engine/renderer.components.test.js +302 -0
- package/dist/engine/renderer.components.test.js.map +1 -0
- package/dist/engine/renderer.d.ts +4 -0
- package/dist/engine/renderer.js +408 -0
- package/dist/engine/renderer.js.map +1 -0
- package/dist/engine/renderer.test.d.ts +1 -0
- package/dist/engine/renderer.test.js +80 -0
- package/dist/engine/renderer.test.js.map +1 -0
- package/dist/engine/server.d.ts +43 -0
- package/dist/engine/server.js +402 -0
- package/dist/engine/server.js.map +1 -0
- package/dist/engine/server.test.d.ts +1 -0
- package/dist/engine/server.test.js +149 -0
- package/dist/engine/server.test.js.map +1 -0
- package/dist/engine/shell.d.ts +7 -0
- package/dist/engine/shell.js +25 -0
- package/dist/engine/shell.js.map +1 -0
- package/dist/engine/theme-path.d.ts +11 -0
- package/dist/engine/theme-path.js +32 -0
- package/dist/engine/theme-path.js.map +1 -0
- package/dist/engine/theme-path.test.d.ts +1 -0
- package/dist/engine/theme-path.test.js +52 -0
- package/dist/engine/theme-path.test.js.map +1 -0
- package/dist/engine/watcher.d.ts +8 -0
- package/dist/engine/watcher.js +27 -0
- package/dist/engine/watcher.js.map +1 -0
- package/dist/engine/websocket.d.ts +24 -0
- package/dist/engine/websocket.hub.test.d.ts +1 -0
- package/dist/engine/websocket.hub.test.js +75 -0
- package/dist/engine/websocket.hub.test.js.map +1 -0
- package/dist/engine/websocket.js +119 -0
- package/dist/engine/websocket.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/registry.d.ts +11 -0
- package/dist/plugins/registry.js +61 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/plugins/registry.test.d.ts +1 -0
- package/dist/plugins/registry.test.js +44 -0
- package/dist/plugins/registry.test.js.map +1 -0
- package/dist/plugins/types.d.ts +30 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/style.css +134 -0
- package/dist/theme/lastriko.css +134 -0
- package/package.json +60 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/**
|
|
6
|
+
* Resolve `lastriko.css` for `GET /style.css`.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
* 1. `LASTRIKO_THEME_CSS` — absolute or cwd-relative path (must exist).
|
|
10
|
+
* 2. `packages/core/src/theme/lastriko.css` under `process.cwd()` (monorepo dev from repo root).
|
|
11
|
+
* 3. `../theme/lastriko.css` next to this module (`src/engine` or `dist/engine` after build).
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` if no readable file is found (caller should respond with 500, not throw).
|
|
14
|
+
*/
|
|
15
|
+
export function resolveThemeCssPath(cwd) {
|
|
16
|
+
const fromEnv = process.env.LASTRIKO_THEME_CSS?.trim();
|
|
17
|
+
if (fromEnv) {
|
|
18
|
+
const abs = fromEnv.startsWith('/')
|
|
19
|
+
? fromEnv
|
|
20
|
+
: join(cwd, fromEnv);
|
|
21
|
+
return existsSync(abs) ? abs : null;
|
|
22
|
+
}
|
|
23
|
+
const monorepoDev = join(cwd, 'packages/core/src/theme/lastriko.css');
|
|
24
|
+
if (existsSync(monorepoDev))
|
|
25
|
+
return monorepoDev;
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const nextToEngine = join(here, '../theme/lastriko.css');
|
|
28
|
+
if (existsSync(nextToEngine))
|
|
29
|
+
return nextToEngine;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=theme-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-path.js","sourceRoot":"","sources":["../../src/engine/theme-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAC;IACvD,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YACjC,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACvB,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACtC,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,sCAAsC,CAAC,CAAC;IACtE,IAAI,UAAU,CAAC,WAAW,CAAC;QACzB,OAAO,WAAW,CAAC;IAErB,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,uBAAuB,CAAC,CAAC;IACzD,IAAI,UAAU,CAAC,YAAY,CAAC;QAC1B,OAAO,YAAY,CAAC;IAEtB,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { resolveThemeCssPath } from './theme-path';
|
|
6
|
+
describe('resolveThemeCssPath', () => {
|
|
7
|
+
const prevEnv = process.env.LASTRIKO_THEME_CSS;
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (prevEnv === undefined)
|
|
10
|
+
delete process.env.LASTRIKO_THEME_CSS;
|
|
11
|
+
else
|
|
12
|
+
process.env.LASTRIKO_THEME_CSS = prevEnv;
|
|
13
|
+
});
|
|
14
|
+
it('returns monorepo dev path when cwd is repo root and packages/core tree exists', () => {
|
|
15
|
+
const cwd = join(__dirname, '../../../../');
|
|
16
|
+
const p = resolveThemeCssPath(cwd);
|
|
17
|
+
expect(p).not.toBeNull();
|
|
18
|
+
expect(p).toContain('packages/core/src/theme/lastriko.css');
|
|
19
|
+
expect(readFileSync(p, 'utf8')).toContain(':root');
|
|
20
|
+
});
|
|
21
|
+
it('uses LASTRIKO_THEME_CSS when set to an existing absolute path', () => {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), 'lk-theme-'));
|
|
23
|
+
const custom = join(dir, 'custom.css');
|
|
24
|
+
writeFileSync(custom, 'body{}');
|
|
25
|
+
process.env.LASTRIKO_THEME_CSS = custom;
|
|
26
|
+
expect(resolveThemeCssPath('/')).toBe(custom);
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
it('returns null when LASTRIKO_THEME_CSS points to a missing file', () => {
|
|
30
|
+
process.env.LASTRIKO_THEME_CSS = join(tmpdir(), 'nonexistent-lastriko-theme.css');
|
|
31
|
+
expect(resolveThemeCssPath(process.cwd())).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
it('resolves next to engine module when monorepo layout is absent', () => {
|
|
34
|
+
const isolated = mkdtempSync(join(tmpdir(), 'lk-cwd-'));
|
|
35
|
+
const p = resolveThemeCssPath(isolated);
|
|
36
|
+
expect(p).not.toBeNull();
|
|
37
|
+
expect(readFileSync(p, 'utf8')).toContain(':root');
|
|
38
|
+
rmSync(isolated, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
it('prefers LASTRIKO_THEME_CSS over monorepo path when both exist', () => {
|
|
41
|
+
const dir = mkdtempSync(join(tmpdir(), 'lk-pref-'));
|
|
42
|
+
const fakeRoot = join(dir, 'repo');
|
|
43
|
+
mkdirSync(join(fakeRoot, 'packages/core/src/theme'), { recursive: true });
|
|
44
|
+
writeFileSync(join(fakeRoot, 'packages/core/src/theme/lastriko.css'), '/* mono */');
|
|
45
|
+
const custom = join(dir, 'winner.css');
|
|
46
|
+
writeFileSync(custom, '/* winner */');
|
|
47
|
+
process.env.LASTRIKO_THEME_CSS = custom;
|
|
48
|
+
expect(resolveThemeCssPath(fakeRoot)).toBe(custom);
|
|
49
|
+
rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=theme-path.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-path.test.js","sourceRoot":"","sources":["../../src/engine/theme-path.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Watcher {
|
|
2
|
+
stop: () => Promise<void> | void;
|
|
3
|
+
}
|
|
4
|
+
export declare function createWatcher(onReload: () => void, debounceMs?: number): {
|
|
5
|
+
scheduleReload: () => void;
|
|
6
|
+
stop: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function startWatcher(_scriptPath: string, onReload: () => void, debounceMs?: number): Promise<Watcher>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function createWatcher(onReload, debounceMs = 50) {
|
|
2
|
+
let timer;
|
|
3
|
+
return {
|
|
4
|
+
scheduleReload() {
|
|
5
|
+
if (timer) {
|
|
6
|
+
clearTimeout(timer);
|
|
7
|
+
}
|
|
8
|
+
timer = setTimeout(() => {
|
|
9
|
+
timer = undefined;
|
|
10
|
+
onReload();
|
|
11
|
+
}, debounceMs);
|
|
12
|
+
},
|
|
13
|
+
stop() {
|
|
14
|
+
if (timer) {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
timer = undefined;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function startWatcher(_scriptPath, onReload, debounceMs = 50) {
|
|
22
|
+
const watcher = createWatcher(onReload, debounceMs);
|
|
23
|
+
return {
|
|
24
|
+
stop: watcher.stop,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/engine/watcher.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,aAAa,CAAC,QAAoB,EAAE,UAAU,GAAG,EAAE;IAIjE,IAAI,KAAgD,CAAC;IAErD,OAAO;QACL,cAAc;YACZ,IAAI,KAAK,EAAE,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;YACD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBACtB,KAAK,GAAG,SAAS,CAAC;gBAClB,QAAQ,EAAE,CAAC;YACb,CAAC,EAAE,UAAU,CAAC,CAAC;QACjB,CAAC;QACD,IAAI;YACF,IAAI,KAAK,EAAE,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,KAAK,GAAG,SAAS,CAAC;YACpB,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,WAAmB,EACnB,QAAoB,EACpB,UAAU,GAAG,EAAE;IAEf,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACpD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI;KACnB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AppDefinition } from './executor';
|
|
2
|
+
import type { ConnectionScope } from '../components/types';
|
|
3
|
+
export interface WebSocketLike {
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
}
|
|
6
|
+
export interface SocketConnection {
|
|
7
|
+
socket: WebSocketLike;
|
|
8
|
+
scope: ConnectionScope;
|
|
9
|
+
}
|
|
10
|
+
export declare class WebSocketHub {
|
|
11
|
+
private readonly appDef;
|
|
12
|
+
private readonly connections;
|
|
13
|
+
private currentTheme;
|
|
14
|
+
constructor(appDef: AppDefinition);
|
|
15
|
+
addConnection(socket: WebSocketLike): ConnectionScope;
|
|
16
|
+
removeConnection(socket: WebSocketLike): void;
|
|
17
|
+
handleRawMessage(socket: WebSocketLike, raw: unknown): void;
|
|
18
|
+
handleHotReload(): void;
|
|
19
|
+
private handleMessage;
|
|
20
|
+
private handleReady;
|
|
21
|
+
private handleEvent;
|
|
22
|
+
private send;
|
|
23
|
+
}
|
|
24
|
+
export declare function createWebSocketHub(appDef: AppDefinition): WebSocketHub;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createWebSocketHub } from './websocket';
|
|
3
|
+
function captureSocket() {
|
|
4
|
+
const sent = [];
|
|
5
|
+
return {
|
|
6
|
+
socket: {
|
|
7
|
+
send(data) {
|
|
8
|
+
sent.push(data);
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
sent,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function parseSent(sent) {
|
|
15
|
+
return sent.map((s) => JSON.parse(s));
|
|
16
|
+
}
|
|
17
|
+
describe('webSocketHub', () => {
|
|
18
|
+
it('applies RESIZE to connection viewport', () => {
|
|
19
|
+
const { socket, sent } = captureSocket();
|
|
20
|
+
const hub = createWebSocketHub({
|
|
21
|
+
title: 't',
|
|
22
|
+
callback: () => { },
|
|
23
|
+
});
|
|
24
|
+
const scope = hub.addConnection(socket);
|
|
25
|
+
hub.handleRawMessage(socket, JSON.stringify({ type: 'RESIZE', payload: { width: 400, height: 300 } }));
|
|
26
|
+
expect(scope.viewport).toEqual({ width: 400, height: 300 });
|
|
27
|
+
expect(sent.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
it('broadcasts hot reload and re-runs app', () => {
|
|
30
|
+
let runs = 0;
|
|
31
|
+
const { socket, sent } = captureSocket();
|
|
32
|
+
const hub = createWebSocketHub({
|
|
33
|
+
title: 'hot',
|
|
34
|
+
callback: () => {
|
|
35
|
+
runs++;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
hub.addConnection(socket);
|
|
39
|
+
hub.handleRawMessage(socket, JSON.stringify({
|
|
40
|
+
type: 'READY',
|
|
41
|
+
payload: { viewport: { width: 1, height: 1 }, theme: 'light' },
|
|
42
|
+
}));
|
|
43
|
+
expect(runs).toBe(1);
|
|
44
|
+
const before = sent.length;
|
|
45
|
+
hub.handleHotReload();
|
|
46
|
+
expect(runs).toBe(2);
|
|
47
|
+
expect(sent.length).toBeGreaterThan(before);
|
|
48
|
+
});
|
|
49
|
+
it('sends ERROR when app callback throws on READY', () => {
|
|
50
|
+
const { socket, sent } = captureSocket();
|
|
51
|
+
const hub = createWebSocketHub({
|
|
52
|
+
title: 'bad',
|
|
53
|
+
callback: () => {
|
|
54
|
+
throw new Error('boom');
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
hub.addConnection(socket);
|
|
58
|
+
hub.handleRawMessage(socket, JSON.stringify({
|
|
59
|
+
type: 'READY',
|
|
60
|
+
payload: { viewport: { width: 1, height: 1 }, theme: null },
|
|
61
|
+
}));
|
|
62
|
+
const msgs = parseSent(sent);
|
|
63
|
+
expect(msgs.some((m) => m.type === 'ERROR')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('ignores messages from unknown sockets', () => {
|
|
66
|
+
const { socket, sent } = captureSocket();
|
|
67
|
+
const hub = createWebSocketHub({ title: 'x', callback: () => { } });
|
|
68
|
+
hub.handleRawMessage(socket, JSON.stringify({
|
|
69
|
+
type: 'READY',
|
|
70
|
+
payload: { viewport: { width: 1, height: 1 }, theme: null },
|
|
71
|
+
}));
|
|
72
|
+
expect(sent).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=websocket.hub.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.hub.test.js","sourceRoot":"","sources":["../../src/engine/websocket.hub.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { executeApp, handleClientEvent } from './executor';
|
|
2
|
+
import { parseClientMessage, serializeServerMessage } from './messages';
|
|
3
|
+
import { createConnectionScope } from '../components/registry';
|
|
4
|
+
export class WebSocketHub {
|
|
5
|
+
appDef;
|
|
6
|
+
connections = new Map();
|
|
7
|
+
currentTheme = 'light';
|
|
8
|
+
constructor(appDef) {
|
|
9
|
+
this.appDef = appDef;
|
|
10
|
+
}
|
|
11
|
+
addConnection(socket) {
|
|
12
|
+
const scope = createConnectionScope(undefined, {
|
|
13
|
+
send: (message) => this.send(socket, message),
|
|
14
|
+
});
|
|
15
|
+
this.send(socket, {
|
|
16
|
+
type: 'TOAST',
|
|
17
|
+
payload: {
|
|
18
|
+
type: 'info',
|
|
19
|
+
message: `__connection_id__:${scope.id}`,
|
|
20
|
+
duration: 50,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
this.connections.set(socket, { socket, scope });
|
|
24
|
+
return scope;
|
|
25
|
+
}
|
|
26
|
+
removeConnection(socket) {
|
|
27
|
+
const entry = this.connections.get(socket);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
entry.scope.destroy();
|
|
32
|
+
this.connections.delete(socket);
|
|
33
|
+
}
|
|
34
|
+
handleRawMessage(socket, raw) {
|
|
35
|
+
const parsed = parseClientMessage(raw);
|
|
36
|
+
if (!parsed) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.handleMessage(socket, parsed);
|
|
40
|
+
}
|
|
41
|
+
handleHotReload() {
|
|
42
|
+
for (const { socket, scope } of this.connections.values()) {
|
|
43
|
+
this.send(socket, {
|
|
44
|
+
type: 'TOAST',
|
|
45
|
+
payload: { message: 'Reloading...', type: 'info', duration: 1200 },
|
|
46
|
+
});
|
|
47
|
+
scope.clearTransientState();
|
|
48
|
+
const result = executeApp(this.appDef, scope, this.currentTheme);
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
this.send(socket, {
|
|
51
|
+
type: 'ERROR',
|
|
52
|
+
payload: { message: result.error.message, stack: result.error.stack },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
handleMessage(socket, message) {
|
|
58
|
+
const entry = this.connections.get(socket);
|
|
59
|
+
if (!entry) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
switch (message.type) {
|
|
63
|
+
case 'READY':
|
|
64
|
+
this.handleReady(entry, message);
|
|
65
|
+
break;
|
|
66
|
+
case 'EVENT':
|
|
67
|
+
this.handleEvent(entry, message);
|
|
68
|
+
break;
|
|
69
|
+
case 'THEME_CHANGE':
|
|
70
|
+
this.currentTheme = message.payload.mode;
|
|
71
|
+
entry.scope.theme = this.currentTheme;
|
|
72
|
+
this.send(socket, { type: 'THEME', payload: { mode: this.currentTheme } });
|
|
73
|
+
break;
|
|
74
|
+
case 'RESIZE':
|
|
75
|
+
entry.scope.viewport = {
|
|
76
|
+
width: message.payload.width,
|
|
77
|
+
height: message.payload.height,
|
|
78
|
+
};
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
handleReady(entry, message) {
|
|
83
|
+
this.currentTheme = message.payload.theme ?? this.currentTheme;
|
|
84
|
+
entry.scope.theme = this.currentTheme;
|
|
85
|
+
entry.scope.viewport = {
|
|
86
|
+
width: message.payload.viewport.width,
|
|
87
|
+
height: message.payload.viewport.height,
|
|
88
|
+
};
|
|
89
|
+
const result = executeApp(this.appDef, entry.scope, this.currentTheme);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
this.send(entry.socket, {
|
|
92
|
+
type: 'ERROR',
|
|
93
|
+
payload: { message: result.error.message, stack: result.error.stack },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
handleEvent(entry, message) {
|
|
98
|
+
const { scope } = entry;
|
|
99
|
+
try {
|
|
100
|
+
handleClientEvent(scope, message.payload);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.send(entry.socket, {
|
|
104
|
+
type: 'TOAST',
|
|
105
|
+
payload: {
|
|
106
|
+
type: 'error',
|
|
107
|
+
message: error instanceof Error ? error.message : String(error),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
send(socket, message) {
|
|
113
|
+
socket.send(serializeServerMessage(message));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function createWebSocketHub(appDef) {
|
|
117
|
+
return new WebSocketHub(appDef);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/engine/websocket.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LastrikoPlugin } from './plugins/types';
|
|
2
|
+
import { type RuntimeConfig, type RunningServer } from './engine/server';
|
|
3
|
+
import type { AppCallback } from './components/types';
|
|
4
|
+
export interface AppConfig extends RuntimeConfig {
|
|
5
|
+
plugins?: LastrikoPlugin[];
|
|
6
|
+
toolbar?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface AppOptions {
|
|
9
|
+
plugins?: LastrikoPlugin[];
|
|
10
|
+
server?: RuntimeConfig;
|
|
11
|
+
}
|
|
12
|
+
export interface RunningApp {
|
|
13
|
+
stop: () => Promise<void>;
|
|
14
|
+
server: RunningServer;
|
|
15
|
+
}
|
|
16
|
+
export declare function defineConfig(config: AppConfig): AppConfig;
|
|
17
|
+
export declare function app(title: string, callback: AppCallback, opts?: AppOptions): Promise<RunningApp>;
|
|
18
|
+
export type { ConnectionScope as Connection, ComponentHandle, ButtonHandle, ButtonCallbackHandle, InputHandle, TextHandle, TextInputHandle, NumberInputHandle, SliderHandle, ToggleHandle, SelectHandle, FileUploadHandle, UploadedFile, SelectOption, ButtonOpts, TextInputOpts, NumberInputOpts, SliderOpts, ToggleOpts, SelectOpts, FileUploadOpts, TableHandle, TableRow, TableRowHandle as RowHandle, MetricHandle, MetricOpts, ProgressHandle, ProgressOpts, StreamHandle, StreamTextOpts, ChatHandle, ChatUIOptions, PromptEditorHandle, PromptEditorOpts, ShellRegions, ShellOpts, GridOpts, TabsOpts, TabDef, ToastOpts, AlertOpts, LoadingOpts, UIContext, AppCallback, } from './components/types';
|
|
19
|
+
export type { LastrikoPlugin } from './plugins/types';
|
|
20
|
+
export { createWebSocketHub } from './engine/websocket';
|
|
21
|
+
export { createWatcher, startWatcher } from './engine/watcher';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { createServer } from './engine/server';
|
|
3
|
+
export function defineConfig(config) {
|
|
4
|
+
return config;
|
|
5
|
+
}
|
|
6
|
+
export async function app(title, callback, opts = {}) {
|
|
7
|
+
const plugins = opts.plugins;
|
|
8
|
+
const server = await createServer({
|
|
9
|
+
title,
|
|
10
|
+
plugins,
|
|
11
|
+
app: callback,
|
|
12
|
+
...(opts.server ?? {}),
|
|
13
|
+
});
|
|
14
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
15
|
+
console.info(`[lastriko] Ready at http://${server.host}:${server.port}`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
server,
|
|
19
|
+
stop: async () => {
|
|
20
|
+
await server.stop();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export { createWebSocketHub } from './engine/websocket';
|
|
25
|
+
export { createWatcher, startWatcher } from './engine/watcher';
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,YAAY,EAA0C,MAAM,iBAAiB,CAAC;AAkBvF,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,KAAa,EAAE,QAAqB,EAAE,OAAmB,EAAE;IACnF,MAAM,OAAO,GAAG,IAAI,CAAC,OAA6E,CAAC;IACnG,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;QAChC,KAAK;QACL,OAAO;QACP,GAAG,EAAE,QAAQ;QACb,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;KACvB,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QACpC,OAAO,CAAC,IAAI,CAAC,8BAA8B,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO;QACL,MAAM;QACN,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC;AAiDD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LastrikoPlugin, PluginContext } from './types';
|
|
2
|
+
export declare class PluginRegistry {
|
|
3
|
+
private readonly plugins;
|
|
4
|
+
private readonly ordered;
|
|
5
|
+
constructor(initial?: LastrikoPlugin[]);
|
|
6
|
+
register(plugin: LastrikoPlugin): void;
|
|
7
|
+
setupAll(context?: PluginContext): Promise<void>;
|
|
8
|
+
teardownAll(): Promise<void>;
|
|
9
|
+
getPlugin(name: string): Pick<LastrikoPlugin, 'name' | 'version'> | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function createPluginRegistry(initial?: LastrikoPlugin[]): PluginRegistry;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { atom } from 'nanostores';
|
|
2
|
+
export class PluginRegistry {
|
|
3
|
+
plugins = new Map();
|
|
4
|
+
ordered = [];
|
|
5
|
+
constructor(initial = []) {
|
|
6
|
+
for (const plugin of initial) {
|
|
7
|
+
this.register(plugin);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
register(plugin) {
|
|
11
|
+
if (this.plugins.has(plugin.name)) {
|
|
12
|
+
throw new Error(`Duplicate plugin name: ${plugin.name}`);
|
|
13
|
+
}
|
|
14
|
+
this.plugins.set(plugin.name, plugin);
|
|
15
|
+
this.ordered.push(plugin);
|
|
16
|
+
}
|
|
17
|
+
async setupAll(context) {
|
|
18
|
+
const ctx = context ?? createDefaultPluginContext(this);
|
|
19
|
+
for (const plugin of this.ordered) {
|
|
20
|
+
await plugin.setup(ctx);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async teardownAll() {
|
|
24
|
+
for (const plugin of [...this.ordered].reverse()) {
|
|
25
|
+
await plugin.teardown?.();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
getPlugin(name) {
|
|
29
|
+
const plugin = this.plugins.get(name);
|
|
30
|
+
if (!plugin) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
name: plugin.name,
|
|
35
|
+
version: plugin.version,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function createPluginRegistry(initial = []) {
|
|
40
|
+
return new PluginRegistry(initial);
|
|
41
|
+
}
|
|
42
|
+
function createDefaultPluginContext(registry) {
|
|
43
|
+
return {
|
|
44
|
+
registerComponent: () => { },
|
|
45
|
+
registerRoute: () => { },
|
|
46
|
+
registerMiddleware: () => { },
|
|
47
|
+
getStore: (_key, initialValue) => atom(initialValue),
|
|
48
|
+
onConnection: (_handler) => { },
|
|
49
|
+
onDisconnect: (_handler) => { },
|
|
50
|
+
onEvent: (_handler) => { },
|
|
51
|
+
config: {},
|
|
52
|
+
logger: {
|
|
53
|
+
info: () => { },
|
|
54
|
+
warn: () => { },
|
|
55
|
+
error: () => { },
|
|
56
|
+
debug: () => { },
|
|
57
|
+
},
|
|
58
|
+
getPlugin: (name) => registry.getPlugin(name),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/plugins/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAqB,MAAM,YAAY,CAAC;AAIrD,MAAM,OAAO,cAAc;IACR,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC5C,OAAO,GAAqB,EAAE,CAAC;IAEhD,YAAY,UAA4B,EAAE;QACxC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,MAAsB;QAC7B,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAuB;QACpC,MAAM,GAAG,GAAG,OAAO,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC;QACxD,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW;QACf,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;YACjD,MAAM,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,SAAS,CAAC,IAAY;QACpB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;IACJ,CAAC;CACF;AAED,MAAM,UAAU,oBAAoB,CAAC,UAA4B,EAAE;IACjE,OAAO,IAAI,cAAc,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,0BAA0B,CAAC,QAAwB;IAC1D,OAAO;QACL,iBAAiB,EAAE,GAAG,EAAE,GAAE,CAAC;QAC3B,aAAa,EAAE,GAAG,EAAE,GAAE,CAAC;QACvB,kBAAkB,EAAE,GAAG,EAAE,GAAE,CAAC;QAC5B,QAAQ,EAAE,CAAI,IAAY,EAAE,YAAe,EAAmB,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC;QACnF,YAAY,EAAE,CAAC,QAA0C,EAAE,EAAE,GAAE,CAAC;QAChE,YAAY,EAAE,CAAC,QAA0C,EAAE,EAAE,GAAE,CAAC;QAChE,OAAO,EAAE,CAAC,QAA0I,EAAE,EAAE,GAAE,CAAC;QAC3J,MAAM,EAAE,EAAE;QACV,MAAM,EAAE;YACN,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;YACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;YACd,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;YACf,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;SAChB;QACD,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC;KAC9C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { createPluginRegistry } from './registry';
|
|
3
|
+
function createPlugin(name) {
|
|
4
|
+
return {
|
|
5
|
+
name,
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
setup: async () => { },
|
|
8
|
+
teardown: async () => { },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
describe('plugin registry', () => {
|
|
12
|
+
test('setup and teardown execute in expected order', async () => {
|
|
13
|
+
const order = [];
|
|
14
|
+
const a = {
|
|
15
|
+
name: 'a',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
setup: async () => {
|
|
18
|
+
order.push('setup-a');
|
|
19
|
+
},
|
|
20
|
+
teardown: async () => {
|
|
21
|
+
order.push('teardown-a');
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const b = {
|
|
25
|
+
name: 'b',
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
setup: async () => {
|
|
28
|
+
order.push('setup-b');
|
|
29
|
+
},
|
|
30
|
+
teardown: async () => {
|
|
31
|
+
order.push('teardown-b');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const registry = createPluginRegistry([a, b]);
|
|
35
|
+
await registry.setupAll();
|
|
36
|
+
await registry.teardownAll();
|
|
37
|
+
expect(order).toEqual(['setup-a', 'setup-b', 'teardown-b', 'teardown-a']);
|
|
38
|
+
expect(registry.getPlugin('a')).toEqual({ name: 'a', version: '1.0.0' });
|
|
39
|
+
});
|
|
40
|
+
test('duplicate plugin names are rejected', () => {
|
|
41
|
+
expect(() => createPluginRegistry([createPlugin('x'), createPlugin('x')])).toThrow(/Duplicate plugin name/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=registry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../../src/plugins/registry.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGlD,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;QACrB,QAAQ,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;KACzB,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,IAAI,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAmB;YACxB,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,OAAO;YAChB,KAAK,EAAE,KAAK,IAAI,EAAE;gBAChB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;YACD,QAAQ,EAAE,KAAK,IAAI,EAAE;gBACnB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC3B,CAAC;SACF,CAAC;QACF,MAAM,CAAC,GAAmB;YACxB,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,OAAO;YAChB,KAAK,EAAE,KAAK,IAAI,EAAE;gBAChB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;YACD,QAAQ,EAAE,KAAK,IAAI,EAAE;gBACnB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC3B,CAAC;SACF,CAAC;QAEF,MAAM,QAAQ,GAAG,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC1B,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;QAE7B,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;QAC1E,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,GAAG,EAAE,CAAC,oBAAoB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAChF,uBAAuB,CACxB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WritableAtom } from 'nanostores';
|
|
2
|
+
import type { ComponentHandle, ConnectionScope } from '../components/types';
|
|
3
|
+
import type { ClientEventPayload } from '../engine/messages';
|
|
4
|
+
export interface PluginLogger {
|
|
5
|
+
info(msg: string, data?: Record<string, unknown>): void;
|
|
6
|
+
warn(msg: string, data?: Record<string, unknown>): void;
|
|
7
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
8
|
+
debug(msg: string, data?: Record<string, unknown>): void;
|
|
9
|
+
}
|
|
10
|
+
export interface RouteHandler {
|
|
11
|
+
(request: Request): Response | Promise<Response>;
|
|
12
|
+
}
|
|
13
|
+
export interface PluginContext {
|
|
14
|
+
registerComponent<T extends ComponentHandle<Record<string, unknown>, unknown>>(_name: string, _definition: unknown): void;
|
|
15
|
+
registerRoute(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, handler: RouteHandler): void;
|
|
16
|
+
registerMiddleware(_fn: (request: Request) => Response | Promise<Response> | void): void;
|
|
17
|
+
getStore<T>(key: string, initialValue: T): WritableAtom<T>;
|
|
18
|
+
onConnection(handler: (scope: ConnectionScope) => void): void;
|
|
19
|
+
onDisconnect(handler: (scope: ConnectionScope) => void): void;
|
|
20
|
+
onEvent(handler: (scope: ConnectionScope, event: ClientEventPayload) => boolean | void): void;
|
|
21
|
+
config: Record<string, unknown>;
|
|
22
|
+
logger: PluginLogger;
|
|
23
|
+
getPlugin(name: string): Pick<LastrikoPlugin, 'name' | 'version'> | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface LastrikoPlugin {
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
setup(ctx: PluginContext): void | Promise<void>;
|
|
29
|
+
teardown?(): void | Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/plugins/types.ts"],"names":[],"mappings":""}
|