@shenora/react 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.md +91 -91
- package/package.json +61 -61
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Jiarong Gu
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jiarong Gu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
# @shenora/react
|
|
2
|
-
|
|
3
|
-
React client for [Shenora](https://github.com/JiarongGu/Shenora) desktop hosts (.NET + WinForms +
|
|
4
|
-
WebView2). The typed bridge between a React frontend and the Shenora host: correlated `invoke`
|
|
5
|
-
with timeouts and structured errors, the event hub host notifications stream into, typed module
|
|
6
|
-
services, React hooks, and a pluggable transport with a browser fallback so the UI can be
|
|
7
|
-
developed in a plain browser. Headless by design — no UI components, bring your own design
|
|
8
|
-
system. Versioned in lockstep with the `Shenora.*` NuGet packages.
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import {
|
|
12
|
-
getBridge, useShenoraEvent, useShenoraQuery, BaseModuleService, createShenoraStore,
|
|
13
|
-
} from '@shenora/react';
|
|
14
|
-
|
|
15
|
-
// Once at startup, after your listeners are attached: it starts notification delivery (anything the
|
|
16
|
-
// host buffered arrives in the first batch). Drop zones need no particular ordering against it — the
|
|
17
|
-
// host clears them when a new DOCUMENT loads, not on this handshake.
|
|
18
|
-
await getBridge().notifyReady();
|
|
19
|
-
|
|
20
|
-
// a typed service per backend module:
|
|
21
|
-
interface NoteRequests { GET_ALL: void; ADD: { title: string } }
|
|
22
|
-
class NoteService extends BaseModuleService<NoteRequests> {
|
|
23
|
-
constructor() { super('NOTES'); }
|
|
24
|
-
getAll() { return this.send<Note[]>('GET_ALL'); }
|
|
25
|
-
add(title: string) { return this.send<Note>('ADD', { payload: { title } }); }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// in components:
|
|
29
|
-
const { data, loading, refetch } = useShenoraQuery<Note[]>('NOTES', 'GET_ALL');
|
|
30
|
-
useShenoraEvent<Note>('NOTES', 'ADDED', (note) => refetch());
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Failed calls reject with `OperationError` — a structured `code` (an i18n key: translate
|
|
34
|
-
`errors.{code}`) plus interpolation `parameters`, never raw host exception text.
|
|
35
|
-
|
|
36
|
-
### Long-running work: post, then read a store
|
|
37
|
-
|
|
38
|
-
`invoke` awaits a correlated reply and carries a timeout, and its handler's synchronous segment runs
|
|
39
|
-
on the host's **UI thread** — so it is for calls that are quick and UI-thread-safe. Everything else
|
|
40
|
-
posts and streams results back as events:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
const useDeploy = createShenoraStore('DEPLOY', {
|
|
44
|
-
initial: { status: 'idle', lines: [] as string[] },
|
|
45
|
-
// Loaded on the FIRST subscriber, so a component that mounts mid-run isn't empty:
|
|
46
|
-
// events it missed cannot be replayed.
|
|
47
|
-
snapshot: { type: 'GET_STATE', apply: (s, d) => ({ ...s, ...(d as object) }) },
|
|
48
|
-
on: {
|
|
49
|
-
PROGRESS: (s, p: { line: string }) => ({ ...s, lines: [...s.lines, p.line] }),
|
|
50
|
-
ENDED: (s, p: { ok: boolean }) => ({ ...s, status: p.ok ? 'done' : 'failed' }),
|
|
51
|
-
},
|
|
52
|
-
actions: ({ post }) => ({ start: (cfg: unknown) => post('START', { payload: cfg }) }),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// any number of components share ONE subscription:
|
|
56
|
-
const status = useDeploy((s) => s.status);
|
|
57
|
-
useDeploy.actions.start({ env: 'prod' });
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Use the **store for shared or long-lived state** and **`useShenoraEvent` for a one-off reaction in a
|
|
61
|
-
single component**. A failed `post` has no promise to reject, so it is reported through the bridge's
|
|
62
|
-
`onPostError` rather than vanishing — it is bridge-wide, so wire it once at startup
|
|
63
|
-
(`getBridge()` takes no options):
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
configureBridge({ onPostError: (failure) => log.error(failure.module, failure.type, failure.error) });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Observing the whole stream
|
|
70
|
-
|
|
71
|
-
`useShenoraEvent` and `createShenoraStore` listen for an exact `(module, type)`. When the vocabulary
|
|
72
|
-
isn't knowable up front — plug-in-contributed events, a diagnostics tap, or an adoption shim keeping
|
|
73
|
-
a legacy "every host message" handler alive while features migrate one at a time — subscribe broadly
|
|
74
|
-
instead. Both mirror the host's `IEventBus` and return an unsubscribe:
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
const off = eventBus.subscribeToAll((event) => log.debug(event.module, event.type, event.payload));
|
|
78
|
-
eventBus.subscribeToModule('DEPLOY', (event) => audit(event)); // every type from one module
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Delivery is narrowest-first — exact pair, then module, then catch-all — so a broad observer never
|
|
82
|
-
runs ahead of the feature code it is observing. Prefer `subscribe` when you know the pair: a
|
|
83
|
-
catch-all wakes for every event on the bus.
|
|
84
|
-
|
|
85
|
-
Pure-UI development in a plain browser: pass a `fallback` to `configureBridge` (gated behind
|
|
86
|
-
`import.meta.env.DEV`) to answer requests with canned data. Other shells (WebSocket,
|
|
87
|
-
mobile/Capacitor) implement the small `ShenoraTransport` seam and speak the same envelopes.
|
|
88
|
-
For CDP-driven testing, `installDevInterceptor()` records IPC/event traffic into ring buffers
|
|
89
|
-
and exposes `window.__shenora.call()/waitEvent()`.
|
|
90
|
-
|
|
91
|
-
MIT © Jiarong Gu
|
|
1
|
+
# @shenora/react
|
|
2
|
+
|
|
3
|
+
React client for [Shenora](https://github.com/JiarongGu/Shenora) desktop hosts (.NET + WinForms +
|
|
4
|
+
WebView2). The typed bridge between a React frontend and the Shenora host: correlated `invoke`
|
|
5
|
+
with timeouts and structured errors, the event hub host notifications stream into, typed module
|
|
6
|
+
services, React hooks, and a pluggable transport with a browser fallback so the UI can be
|
|
7
|
+
developed in a plain browser. Headless by design — no UI components, bring your own design
|
|
8
|
+
system. Versioned in lockstep with the `Shenora.*` NuGet packages.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import {
|
|
12
|
+
getBridge, useShenoraEvent, useShenoraQuery, BaseModuleService, createShenoraStore,
|
|
13
|
+
} from '@shenora/react';
|
|
14
|
+
|
|
15
|
+
// Once at startup, after your listeners are attached: it starts notification delivery (anything the
|
|
16
|
+
// host buffered arrives in the first batch). Drop zones need no particular ordering against it — the
|
|
17
|
+
// host clears them when a new DOCUMENT loads, not on this handshake.
|
|
18
|
+
await getBridge().notifyReady();
|
|
19
|
+
|
|
20
|
+
// a typed service per backend module:
|
|
21
|
+
interface NoteRequests { GET_ALL: void; ADD: { title: string } }
|
|
22
|
+
class NoteService extends BaseModuleService<NoteRequests> {
|
|
23
|
+
constructor() { super('NOTES'); }
|
|
24
|
+
getAll() { return this.send<Note[]>('GET_ALL'); }
|
|
25
|
+
add(title: string) { return this.send<Note>('ADD', { payload: { title } }); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// in components:
|
|
29
|
+
const { data, loading, refetch } = useShenoraQuery<Note[]>('NOTES', 'GET_ALL');
|
|
30
|
+
useShenoraEvent<Note>('NOTES', 'ADDED', (note) => refetch());
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Failed calls reject with `OperationError` — a structured `code` (an i18n key: translate
|
|
34
|
+
`errors.{code}`) plus interpolation `parameters`, never raw host exception text.
|
|
35
|
+
|
|
36
|
+
### Long-running work: post, then read a store
|
|
37
|
+
|
|
38
|
+
`invoke` awaits a correlated reply and carries a timeout, and its handler's synchronous segment runs
|
|
39
|
+
on the host's **UI thread** — so it is for calls that are quick and UI-thread-safe. Everything else
|
|
40
|
+
posts and streams results back as events:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const useDeploy = createShenoraStore('DEPLOY', {
|
|
44
|
+
initial: { status: 'idle', lines: [] as string[] },
|
|
45
|
+
// Loaded on the FIRST subscriber, so a component that mounts mid-run isn't empty:
|
|
46
|
+
// events it missed cannot be replayed.
|
|
47
|
+
snapshot: { type: 'GET_STATE', apply: (s, d) => ({ ...s, ...(d as object) }) },
|
|
48
|
+
on: {
|
|
49
|
+
PROGRESS: (s, p: { line: string }) => ({ ...s, lines: [...s.lines, p.line] }),
|
|
50
|
+
ENDED: (s, p: { ok: boolean }) => ({ ...s, status: p.ok ? 'done' : 'failed' }),
|
|
51
|
+
},
|
|
52
|
+
actions: ({ post }) => ({ start: (cfg: unknown) => post('START', { payload: cfg }) }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// any number of components share ONE subscription:
|
|
56
|
+
const status = useDeploy((s) => s.status);
|
|
57
|
+
useDeploy.actions.start({ env: 'prod' });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use the **store for shared or long-lived state** and **`useShenoraEvent` for a one-off reaction in a
|
|
61
|
+
single component**. A failed `post` has no promise to reject, so it is reported through the bridge's
|
|
62
|
+
`onPostError` rather than vanishing — it is bridge-wide, so wire it once at startup
|
|
63
|
+
(`getBridge()` takes no options):
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
configureBridge({ onPostError: (failure) => log.error(failure.module, failure.type, failure.error) });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Observing the whole stream
|
|
70
|
+
|
|
71
|
+
`useShenoraEvent` and `createShenoraStore` listen for an exact `(module, type)`. When the vocabulary
|
|
72
|
+
isn't knowable up front — plug-in-contributed events, a diagnostics tap, or an adoption shim keeping
|
|
73
|
+
a legacy "every host message" handler alive while features migrate one at a time — subscribe broadly
|
|
74
|
+
instead. Both mirror the host's `IEventBus` and return an unsubscribe:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const off = eventBus.subscribeToAll((event) => log.debug(event.module, event.type, event.payload));
|
|
78
|
+
eventBus.subscribeToModule('DEPLOY', (event) => audit(event)); // every type from one module
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Delivery is narrowest-first — exact pair, then module, then catch-all — so a broad observer never
|
|
82
|
+
runs ahead of the feature code it is observing. Prefer `subscribe` when you know the pair: a
|
|
83
|
+
catch-all wakes for every event on the bus.
|
|
84
|
+
|
|
85
|
+
Pure-UI development in a plain browser: pass a `fallback` to `configureBridge` (gated behind
|
|
86
|
+
`import.meta.env.DEV`) to answer requests with canned data. Other shells (WebSocket,
|
|
87
|
+
mobile/Capacitor) implement the small `ShenoraTransport` seam and speak the same envelopes.
|
|
88
|
+
For CDP-driven testing, `installDevInterceptor()` records IPC/event traffic into ring buffers
|
|
89
|
+
and exposes `window.__shenora.call()/waitEvent()`.
|
|
90
|
+
|
|
91
|
+
MIT © Jiarong Gu
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@shenora/react",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "React client for Shenora desktop hosts: correlated invoke/send/subscribe over the WebView2 bridge, typed module services, hooks, and a browser fallback for pure-UI development.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Jiarong Gu",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/JiarongGu/Shenora.git",
|
|
10
|
-
"directory": "src/Shenora.React"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://github.com/JiarongGu/Shenora#readme",
|
|
13
|
-
"keywords": [
|
|
14
|
-
"shenora",
|
|
15
|
-
"webview2",
|
|
16
|
-
"desktop",
|
|
17
|
-
"ipc",
|
|
18
|
-
"react",
|
|
19
|
-
"winforms",
|
|
20
|
-
"dotnet"
|
|
21
|
-
],
|
|
22
|
-
"type": "module",
|
|
23
|
-
"main": "./dist/index.js",
|
|
24
|
-
"types": "./dist/index.d.ts",
|
|
25
|
-
"exports": {
|
|
26
|
-
".": {
|
|
27
|
-
"types": "./dist/index.d.ts",
|
|
28
|
-
"import": "./dist/index.js"
|
|
29
|
-
},
|
|
30
|
-
"./package.json": "./package.json"
|
|
31
|
-
},
|
|
32
|
-
"files": [
|
|
33
|
-
"dist",
|
|
34
|
-
"README.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"sideEffects": false,
|
|
38
|
-
"publishConfig": {
|
|
39
|
-
"access": "public"
|
|
40
|
-
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
43
|
-
"prepublishOnly": "npm run build",
|
|
44
|
-
"//typecheck": "The ONLY thing that type-checks the tests — `build` excludes them and vitest transpiles without checking, so `@ts-expect-error` assertions (which pin the typed-service generic) are inert without this. Run by dev.mjs verify.",
|
|
45
|
-
"typecheck": "tsc -p tsconfig.json",
|
|
46
|
-
"test": "vitest run",
|
|
47
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
48
|
-
},
|
|
49
|
-
"peerDependencies": {
|
|
50
|
-
"react": ">=18"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@testing-library/react": "^16.3.2",
|
|
54
|
-
"@types/react": "^19.2.17",
|
|
55
|
-
"jsdom": "^29.1.1",
|
|
56
|
-
"react": "^19.2.8",
|
|
57
|
-
"react-dom": "^19.2.8",
|
|
58
|
-
"typescript": "^5.9.0",
|
|
59
|
-
"vitest": "^3.2.0"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@shenora/react",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "React client for Shenora desktop hosts: correlated invoke/send/subscribe over the WebView2 bridge, typed module services, hooks, and a browser fallback for pure-UI development.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Jiarong Gu",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/JiarongGu/Shenora.git",
|
|
10
|
+
"directory": "src/Shenora.React"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/JiarongGu/Shenora#readme",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"shenora",
|
|
15
|
+
"webview2",
|
|
16
|
+
"desktop",
|
|
17
|
+
"ipc",
|
|
18
|
+
"react",
|
|
19
|
+
"winforms",
|
|
20
|
+
"dotnet"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
43
|
+
"prepublishOnly": "npm run build",
|
|
44
|
+
"//typecheck": "The ONLY thing that type-checks the tests — `build` excludes them and vitest transpiles without checking, so `@ts-expect-error` assertions (which pin the typed-service generic) are inert without this. Run by dev.mjs verify.",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@testing-library/react": "^16.3.2",
|
|
54
|
+
"@types/react": "^19.2.17",
|
|
55
|
+
"jsdom": "^29.1.1",
|
|
56
|
+
"react": "^19.2.8",
|
|
57
|
+
"react-dom": "^19.2.8",
|
|
58
|
+
"typescript": "^5.9.0",
|
|
59
|
+
"vitest": "^3.2.0"
|
|
60
|
+
}
|
|
61
|
+
}
|