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.
Files changed (101) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +58 -0
  3. package/dist/__tests__/integration/ws-flow.integration.test.d.ts +1 -0
  4. package/dist/__tests__/integration/ws-flow.integration.test.js +249 -0
  5. package/dist/__tests__/integration/ws-flow.integration.test.js.map +1 -0
  6. package/dist/__tests__/integration/ws-flow.test.d.ts +1 -0
  7. package/dist/__tests__/integration/ws-flow.test.js +70 -0
  8. package/dist/__tests__/integration/ws-flow.test.js.map +1 -0
  9. package/dist/client/events.d.ts +6 -0
  10. package/dist/client/events.js +114 -0
  11. package/dist/client/events.js.map +1 -0
  12. package/dist/client/index.d.ts +2 -0
  13. package/dist/client/index.js +5 -0
  14. package/dist/client/index.js.map +1 -0
  15. package/dist/client/swap.d.ts +4 -0
  16. package/dist/client/swap.js +47 -0
  17. package/dist/client/swap.js.map +1 -0
  18. package/dist/client/ws.d.ts +11 -0
  19. package/dist/client/ws.js +111 -0
  20. package/dist/client/ws.js.map +1 -0
  21. package/dist/components/context.d.ts +51 -0
  22. package/dist/components/context.js +595 -0
  23. package/dist/components/context.js.map +1 -0
  24. package/dist/components/context.test.d.ts +1 -0
  25. package/dist/components/context.test.js +69 -0
  26. package/dist/components/context.test.js.map +1 -0
  27. package/dist/components/id.d.ts +3 -0
  28. package/dist/components/id.js +9 -0
  29. package/dist/components/id.js.map +1 -0
  30. package/dist/components/registry.d.ts +2 -0
  31. package/dist/components/registry.js +97 -0
  32. package/dist/components/registry.js.map +1 -0
  33. package/dist/components/types.d.ts +390 -0
  34. package/dist/components/types.js +2 -0
  35. package/dist/components/types.js.map +1 -0
  36. package/dist/engine/executor.d.ts +20 -0
  37. package/dist/engine/executor.js +84 -0
  38. package/dist/engine/executor.js.map +1 -0
  39. package/dist/engine/executor.test.d.ts +1 -0
  40. package/dist/engine/executor.test.js +79 -0
  41. package/dist/engine/executor.test.js.map +1 -0
  42. package/dist/engine/index.d.ts +6 -0
  43. package/dist/engine/index.js +7 -0
  44. package/dist/engine/index.js.map +1 -0
  45. package/dist/engine/lifecycle.d.ts +16 -0
  46. package/dist/engine/lifecycle.js +44 -0
  47. package/dist/engine/lifecycle.js.map +1 -0
  48. package/dist/engine/lifecycle.test.d.ts +1 -0
  49. package/dist/engine/lifecycle.test.js +15 -0
  50. package/dist/engine/lifecycle.test.js.map +1 -0
  51. package/dist/engine/messages.d.ts +95 -0
  52. package/dist/engine/messages.js +21 -0
  53. package/dist/engine/messages.js.map +1 -0
  54. package/dist/engine/renderer.components.test.d.ts +1 -0
  55. package/dist/engine/renderer.components.test.js +302 -0
  56. package/dist/engine/renderer.components.test.js.map +1 -0
  57. package/dist/engine/renderer.d.ts +4 -0
  58. package/dist/engine/renderer.js +408 -0
  59. package/dist/engine/renderer.js.map +1 -0
  60. package/dist/engine/renderer.test.d.ts +1 -0
  61. package/dist/engine/renderer.test.js +80 -0
  62. package/dist/engine/renderer.test.js.map +1 -0
  63. package/dist/engine/server.d.ts +43 -0
  64. package/dist/engine/server.js +402 -0
  65. package/dist/engine/server.js.map +1 -0
  66. package/dist/engine/server.test.d.ts +1 -0
  67. package/dist/engine/server.test.js +149 -0
  68. package/dist/engine/server.test.js.map +1 -0
  69. package/dist/engine/shell.d.ts +7 -0
  70. package/dist/engine/shell.js +25 -0
  71. package/dist/engine/shell.js.map +1 -0
  72. package/dist/engine/theme-path.d.ts +11 -0
  73. package/dist/engine/theme-path.js +32 -0
  74. package/dist/engine/theme-path.js.map +1 -0
  75. package/dist/engine/theme-path.test.d.ts +1 -0
  76. package/dist/engine/theme-path.test.js +52 -0
  77. package/dist/engine/theme-path.test.js.map +1 -0
  78. package/dist/engine/watcher.d.ts +8 -0
  79. package/dist/engine/watcher.js +27 -0
  80. package/dist/engine/watcher.js.map +1 -0
  81. package/dist/engine/websocket.d.ts +24 -0
  82. package/dist/engine/websocket.hub.test.d.ts +1 -0
  83. package/dist/engine/websocket.hub.test.js +75 -0
  84. package/dist/engine/websocket.hub.test.js.map +1 -0
  85. package/dist/engine/websocket.js +119 -0
  86. package/dist/engine/websocket.js.map +1 -0
  87. package/dist/index.d.ts +21 -0
  88. package/dist/index.js +26 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/plugins/registry.d.ts +11 -0
  91. package/dist/plugins/registry.js +61 -0
  92. package/dist/plugins/registry.js.map +1 -0
  93. package/dist/plugins/registry.test.d.ts +1 -0
  94. package/dist/plugins/registry.test.js +44 -0
  95. package/dist/plugins/registry.test.js.map +1 -0
  96. package/dist/plugins/types.d.ts +30 -0
  97. package/dist/plugins/types.js +2 -0
  98. package/dist/plugins/types.js.map +1 -0
  99. package/dist/style.css +134 -0
  100. package/dist/theme/lastriko.css +134 -0
  101. 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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/plugins/types.ts"],"names":[],"mappings":""}