@svelgo/core 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/index.html ADDED
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SvelGo (Vite dev only)</title>
7
+ </head>
8
+ <body>
9
+ <!-- This file is only used by the Vite dev server for standalone testing.
10
+ In production, the Go server serves its own HTML shell. -->
11
+ <div id="svelgo-root"></div>
12
+ <script>
13
+ window.__SVELGO_PAGE_ID__ = "dev-page";
14
+ window.__SVELGO_STATE__ = "";
15
+ window.__SVELGO_MANIFEST__ = [];
16
+ </script>
17
+ <script type="module" src="/src/main.ts"></script>
18
+ </body>
19
+ </html>
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@svelgo/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ "./runtime/client": "./src/runtime/client.ts",
7
+ "./runtime/ws": "./src/runtime/ws.ts",
8
+ "./runtime/state": "./src/runtime/state.ts",
9
+ "./runtime/proto": "./src/runtime/proto.ts",
10
+ "./runtime/registry": "./src/runtime/registry.ts",
11
+ "./ui_descriptor.json": "./src/runtime/ui_descriptor.json"
12
+ },
13
+ "scripts": {
14
+ "dev": "vite",
15
+ "build": "vite build",
16
+ "preview": "vite preview"
17
+ },
18
+ "devDependencies": {
19
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
20
+ "protobufjs-cli": "^2.0.0",
21
+ "svelte": "^5.0.0",
22
+ "typescript": "^5.0.0",
23
+ "vite": "^6.3.0"
24
+ },
25
+ "dependencies": {
26
+ "protobufjs": "^7.4.0"
27
+ }
28
+ }
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import type { Writable } from 'svelte/store'
3
+ import { getComponentStore } from '../runtime/state'
4
+ import { sendEvent } from '../runtime/ws'
5
+
6
+ let { id }: { id: string } = $props()
7
+
8
+ let state: Record<string, unknown> = $state({})
9
+ $effect(() => {
10
+ const store = getComponentStore(id) as Writable<Record<string, unknown>>
11
+ return store.subscribe(s => { state = s })
12
+ })
13
+ </script>
14
+
15
+ <button
16
+ onclick={() => sendEvent(id, 'click')}
17
+ disabled={!!state.disabled}
18
+ style="
19
+ background-color: {state.disabled ? '#9ca3af' : '#3b82f6'};
20
+ color: white;
21
+ padding: 0.75rem 1.5rem;
22
+ border: none;
23
+ border-radius: 0.5rem;
24
+ font-size: 1rem;
25
+ cursor: {state.disabled ? 'not-allowed' : 'pointer'};
26
+ opacity: {state.disabled ? '0.6' : '1'};
27
+ transition: background-color 0.15s ease;
28
+ "
29
+ >
30
+ {state.label ?? ''}
31
+ </button>
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import type { Writable } from 'svelte/store'
3
+ import { getComponentStore } from '../runtime/state'
4
+
5
+ let { id }: { id: string } = $props()
6
+
7
+ let state: Record<string, unknown> = $state({})
8
+ $effect(() => {
9
+ const store = getComponentStore(id) as Writable<Record<string, unknown>>
10
+ return store.subscribe(s => { state = s })
11
+ })
12
+ </script>
13
+
14
+ <span style="font-size: 1rem; color: #1f2937;">
15
+ {state.text ?? ''}
16
+ </span>
@@ -0,0 +1,9 @@
1
+ import { registerComponent } from './registry'
2
+ import { registerComponentDecoder, root } from './proto'
3
+ import Button from '../component/Button.svelte'
4
+ import Label from '../component/Label.svelte'
5
+
6
+ registerComponent('svelgo.Button', Button)
7
+ registerComponent('svelgo.Label', Label)
8
+ registerComponentDecoder('svelgo.Button', root.lookupType('ui.ButtonState'))
9
+ registerComponentDecoder('svelgo.Label', root.lookupType('ui.LabelState'))
@@ -0,0 +1,68 @@
1
+ import { mount } from 'svelte'
2
+ import { ComponentRegistry } from './registry'
3
+ import { initComponentState } from './state'
4
+ import { decodePageState, decodeComponentState } from './proto'
5
+ import { openWebSocket } from './ws'
6
+ import './builtins'
7
+
8
+ interface ManifestEntry {
9
+ id: string
10
+ type: string
11
+ slot: string
12
+ }
13
+
14
+ // Typed shape of the decoded PageState protobuf message.
15
+ interface DecodedPageState {
16
+ pageId: string
17
+ components: DecodedComponentState[]
18
+ }
19
+
20
+ interface DecodedComponentState {
21
+ id: string
22
+ type: string
23
+ stateBytes: Uint8Array
24
+ }
25
+
26
+ // Typed shape of the globals injected by the Go HTML shell.
27
+ interface SvelGoWindow {
28
+ __SVELGO_PAGE_ID__: string
29
+ __SVELGO_STATE__: string
30
+ __SVELGO_MANIFEST__: ManifestEntry[]
31
+ __SVELGO_DEBUG__: boolean
32
+ }
33
+
34
+ export function bootstrap() {
35
+ const w = window as unknown as SvelGoWindow
36
+ const pageId = w.__SVELGO_PAGE_ID__
37
+ const stateBlob = w.__SVELGO_STATE__
38
+ const manifest = w.__SVELGO_MANIFEST__
39
+
40
+ // Decode the protobuf state blob and initialise per-component Svelte stores
41
+ if (stateBlob) {
42
+ const pageState = decodePageState(stateBlob) as unknown as DecodedPageState
43
+ if (w.__SVELGO_DEBUG__) {
44
+ console.debug('[svelgo init] page state', pageState)
45
+ }
46
+ for (const cs of (pageState.components ?? [])) {
47
+ const decoded = decodeComponentState(cs.type, cs.stateBytes)
48
+ initComponentState(cs.id, decoded)
49
+ }
50
+ }
51
+
52
+ // Mount each Svelte component into the root div
53
+ const root = document.getElementById('svelgo-root')!
54
+ for (const entry of manifest) {
55
+ const Ctor = ComponentRegistry[entry.type]
56
+ if (!Ctor) {
57
+ console.warn(`SvelGo: unknown component type "${entry.type}"`)
58
+ continue
59
+ }
60
+ const target = document.createElement('div')
61
+ target.dataset.svelgoSlot = entry.id
62
+ root.appendChild(target)
63
+ mount(Ctor, { target, props: { id: entry.id } })
64
+ }
65
+
66
+ // Open the WebSocket for live state updates
67
+ openWebSocket(pageId)
68
+ }
@@ -0,0 +1,44 @@
1
+ import protobuf from 'protobufjs/light'
2
+ import descriptor from './ui_descriptor.json'
3
+
4
+ export const root = protobuf.Root.fromJSON(descriptor as protobuf.INamespace)
5
+
6
+ const PageStateMsg = root.lookupType('ui.PageState')
7
+ const ClientEventMsg = root.lookupType('ui.ClientEvent')
8
+ const StateUpdateMsg = root.lookupType('ui.StateUpdate')
9
+
10
+ // Map component type → protobuf message type for decoding state_bytes.
11
+ // Applications register their component decoders by calling registerComponentDecoder().
12
+ const componentTypes: Record<string, protobuf.Type> = {}
13
+
14
+ export function registerComponentDecoder(typeName: string, msgType: protobuf.Type) {
15
+ componentTypes[typeName] = msgType
16
+ }
17
+
18
+ // Decode the base64 protobuf blob injected into the HTML shell
19
+ export function decodePageState(base64blob: string): protobuf.Message {
20
+ const bytes = Uint8Array.from(atob(base64blob), c => c.charCodeAt(0))
21
+ return PageStateMsg.decode(bytes)
22
+ }
23
+
24
+ // Decode a component's state_bytes using its declared type
25
+ export function decodeComponentState(type: string, stateBytes: Uint8Array): Record<string, unknown> {
26
+ const MsgType = componentTypes[type]
27
+ if (!MsgType) {
28
+ console.warn(`Unknown component type: ${type}`)
29
+ return {}
30
+ }
31
+ const msg = MsgType.decode(stateBytes)
32
+ return MsgType.toObject(msg, { defaults: true, longs: String, enums: String }) as Record<string, unknown>
33
+ }
34
+
35
+ // Decode a binary WebSocket frame as a StateUpdate
36
+ export function decodeStateUpdate(buffer: ArrayBuffer): protobuf.Message {
37
+ return StateUpdateMsg.decode(new Uint8Array(buffer))
38
+ }
39
+
40
+ // Encode a ClientEvent to send over the WebSocket
41
+ export function encodeClientEvent(payload: Record<string, unknown>): Uint8Array {
42
+ const msg = ClientEventMsg.create(payload)
43
+ return ClientEventMsg.encode(msg).finish()
44
+ }
@@ -0,0 +1,15 @@
1
+ import type { ComponentType } from 'svelte'
2
+
3
+ // Maps component type strings (from Go) to Svelte component constructors.
4
+ // Applications register their components by calling registerComponent().
5
+ export const ComponentRegistry: Record<string, ComponentType> = {}
6
+
7
+ export function registerComponent(typeName: string, ctor: ComponentType) {
8
+ if (ComponentRegistry[typeName] && ComponentRegistry[typeName] !== ctor) {
9
+ console.warn(
10
+ `SvelGo: registerComponent("${typeName}") is overwriting an existing registration. ` +
11
+ `This is likely a mistake — each type name should be registered exactly once.`
12
+ )
13
+ }
14
+ ComponentRegistry[typeName] = ctor
15
+ }
@@ -0,0 +1,22 @@
1
+ import { writable } from 'svelte/store'
2
+
3
+ type Store = ReturnType<typeof writable<Record<string, unknown>>>
4
+
5
+ const stores = new Map<string, Store>()
6
+
7
+ export function initComponentState(id: string, initialState: Record<string, unknown>) {
8
+ stores.set(id, writable(initialState))
9
+ }
10
+
11
+ export function getComponentStore(id: string): Store {
12
+ const store = stores.get(id)
13
+ if (!store) throw new Error(`No state store for component "${id}"`)
14
+ return store
15
+ }
16
+
17
+ export function updateComponentState(id: string, patch: Record<string, unknown>) {
18
+ const store = stores.get(id)
19
+ if (store) {
20
+ store.update(current => ({ ...current, ...patch }))
21
+ }
22
+ }
@@ -0,0 +1,93 @@
1
+ {
2
+ "nested": {
3
+ "ui": {
4
+ "options": {
5
+ "go_package": "github.com/hawkhero/svelgo/gen/ui"
6
+ },
7
+ "nested": {
8
+ "PageState": {
9
+ "fields": {
10
+ "pageId": {
11
+ "type": "string",
12
+ "id": 1
13
+ },
14
+ "components": {
15
+ "rule": "repeated",
16
+ "type": "ComponentState",
17
+ "id": 2
18
+ }
19
+ }
20
+ },
21
+ "ComponentState": {
22
+ "fields": {
23
+ "id": {
24
+ "type": "string",
25
+ "id": 1
26
+ },
27
+ "type": {
28
+ "type": "string",
29
+ "id": 2
30
+ },
31
+ "stateBytes": {
32
+ "type": "bytes",
33
+ "id": 3
34
+ }
35
+ }
36
+ },
37
+ "ClientEvent": {
38
+ "fields": {
39
+ "pageId": {
40
+ "type": "string",
41
+ "id": 1
42
+ },
43
+ "componentId": {
44
+ "type": "string",
45
+ "id": 2
46
+ },
47
+ "eventType": {
48
+ "type": "string",
49
+ "id": 3
50
+ },
51
+ "payload": {
52
+ "type": "bytes",
53
+ "id": 4
54
+ }
55
+ }
56
+ },
57
+ "StateUpdate": {
58
+ "fields": {
59
+ "pageId": {
60
+ "type": "string",
61
+ "id": 1
62
+ },
63
+ "updatedComponents": {
64
+ "rule": "repeated",
65
+ "type": "ComponentState",
66
+ "id": 2
67
+ }
68
+ }
69
+ },
70
+ "ButtonState": {
71
+ "fields": {
72
+ "label": {
73
+ "type": "string",
74
+ "id": 1
75
+ },
76
+ "disabled": {
77
+ "type": "bool",
78
+ "id": 2
79
+ }
80
+ }
81
+ },
82
+ "LabelState": {
83
+ "fields": {
84
+ "text": {
85
+ "type": "string",
86
+ "id": 1
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,44 @@
1
+ import { decodeStateUpdate, decodeComponentState, encodeClientEvent } from './proto'
2
+ import { updateComponentState } from './state'
3
+
4
+ // Typed shape of the decoded StateUpdate protobuf message.
5
+ interface DecodedStateUpdate {
6
+ pageId: string
7
+ updatedComponents: DecodedComponentState[]
8
+ }
9
+
10
+ interface DecodedComponentState {
11
+ id: string
12
+ type: string
13
+ stateBytes: Uint8Array
14
+ }
15
+
16
+ const debug = () => (window as unknown as { __SVELGO_DEBUG__: boolean }).__SVELGO_DEBUG__ === true
17
+
18
+ let socket: WebSocket
19
+
20
+ export function openWebSocket(pageId: string) {
21
+ const url = `ws://${location.host}/ws?page-id=${encodeURIComponent(pageId)}`
22
+ socket = new WebSocket(url)
23
+ socket.binaryType = 'arraybuffer'
24
+
25
+ socket.onmessage = (evt) => {
26
+ const update = decodeStateUpdate(evt.data) as unknown as DecodedStateUpdate
27
+ if (debug()) console.debug('[svelgo ws ←]', update)
28
+ for (const cs of (update.updatedComponents ?? [])) {
29
+ const decoded = decodeComponentState(cs.type, cs.stateBytes)
30
+ updateComponentState(cs.id, decoded)
31
+ }
32
+ }
33
+
34
+ socket.onerror = (err) => console.error('SvelGo WebSocket error:', err)
35
+ socket.onclose = () => console.log('SvelGo WebSocket closed')
36
+ }
37
+
38
+ export function sendEvent(componentId: string, eventType: string, payload: Uint8Array = new Uint8Array()) {
39
+ if (!socket || socket.readyState !== WebSocket.OPEN) return
40
+ const pageId = (window as unknown as { __SVELGO_PAGE_ID__: string }).__SVELGO_PAGE_ID__
41
+ const bytes = encodeClientEvent({ pageId, componentId, eventType, payload })
42
+ if (debug()) console.debug('[svelgo ws →]', { pageId, componentId, eventType })
43
+ socket.send(bytes)
44
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
2
+ import { defineConfig } from 'vite'
3
+ import { resolve } from 'path'
4
+
5
+ export default defineConfig({
6
+ plugins: [svelte()],
7
+ build: {
8
+ // Emit manifest.json so the Go asset resolver can find hashed filenames
9
+ manifest: true,
10
+ rollupOptions: {
11
+ input: resolve(__dirname, 'src/main.ts'),
12
+ },
13
+ // Output directly into the Go embed directory
14
+ outDir: resolve(__dirname, '../static'),
15
+ emptyOutDir: true,
16
+ },
17
+ server: {
18
+ // Allow the Go server to load scripts cross-origin in dev mode
19
+ cors: true,
20
+ },
21
+ })