@shenora/react 0.8.0 → 0.9.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/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/media.d.ts +65 -0
- package/dist/media.js +83 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +13 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export { WindowCommands, useWindowMaximized, type WindowResizeEdge, type Caption
|
|
|
10
10
|
export { useDropZone, DROP_ZONE_MODULE, type DropZoneFileDrop, type UseDropZoneOptions, } from './useDropZone.js';
|
|
11
11
|
export { useShenora, useShenoraEvent, useShenoraQuery, type ShenoraQueryResult } from './hooks.js';
|
|
12
12
|
export { installDevInterceptor, type DevInterceptorOptions, type DevIpcEntry, type DevEventEntry, } from './devInterceptor.js';
|
|
13
|
+
export { mediaUrl, encodeMediaPayload, decodeMediaPayload } from './media.js';
|
package/dist/index.js
CHANGED
|
@@ -19,3 +19,6 @@ export { WindowCommands, useWindowMaximized, } from './windowCommands.js';
|
|
|
19
19
|
export { useDropZone, DROP_ZONE_MODULE, } from './useDropZone.js';
|
|
20
20
|
export { useShenora, useShenoraEvent, useShenoraQuery } from './hooks.js';
|
|
21
21
|
export { installDevInterceptor, } from './devInterceptor.js';
|
|
22
|
+
// Addressing local content the page cannot reach itself. A pure function, not a hook — building the URL
|
|
23
|
+
// needs no React, and a `useMediaSource` can follow if an adopter wants load/error state.
|
|
24
|
+
export { mediaUrl, encodeMediaPayload, decodeMediaPayload } from './media.js';
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building the URL that lets a page render LOCAL content — media, images, documents, exports — that it
|
|
3
|
+
* cannot reach directly.
|
|
4
|
+
*
|
|
5
|
+
* The host answers these through its resource interceptor; this module only builds the address, which is
|
|
6
|
+
* why it is a pure function and not a hook. A hook (`useMediaSource`) can follow if an adopter wants
|
|
7
|
+
* load/error state, but nothing needs one to start.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build a URL the host's resource interceptor will answer: `<route>?<base64url of JSON>`.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ **The result is RELATIVE, and that is the whole point.** Written as a path rather than with a scheme, it
|
|
13
|
+
* resolves against whatever origin the page is already served from — which means the browser hands the host
|
|
14
|
+
* a URL each platform can actually decode:
|
|
15
|
+
*
|
|
16
|
+
* | shell | the same call resolves to |
|
|
17
|
+
* |---|---|
|
|
18
|
+
* | iOS | `app://0.0.0.1/media?…` — the app scheme, the only thing iOS intercepts |
|
|
19
|
+
* | Android | `https://0.0.0.1/media?…` — Android's media pipeline REFUSES a non-standard scheme |
|
|
20
|
+
* | desktop | the app's virtual host |
|
|
21
|
+
*
|
|
22
|
+
* Both fixed forms fail on exactly one platform, in opposite directions, and registering the scheme rescues
|
|
23
|
+
* neither: iOS cannot register a handler for `https`, and Android cannot register a scheme at all. Measured
|
|
24
|
+
* on devices — so if you are tempted to hardcode `app://`, that is why not.
|
|
25
|
+
*
|
|
26
|
+
* The PAYLOAD is opaque to this package: whatever you pass is JSON-encoded and handed to your own host-side
|
|
27
|
+
* route, which decodes it. That keeps the kit out of your addressing scheme — a filename, an id, a container
|
|
28
|
+
* preference, a cache key, several of them. The kit encodes; you decide what it means.
|
|
29
|
+
*
|
|
30
|
+
* base64**url** specifically, so `+`, `/` and `=` cannot survive into a query string and be re-interpreted
|
|
31
|
+
* by anything that parses URLs along the way.
|
|
32
|
+
*
|
|
33
|
+
* ⚠ An encoded payload costs debuggability — you cannot read the URL in a log any more. Have the host log
|
|
34
|
+
* what it DECODED to: the response body cannot say, because an error body would leak paths.
|
|
35
|
+
*
|
|
36
|
+
* @param payload Anything JSON-serialisable. Your host route decides what the shape means.
|
|
37
|
+
* @param route The reserved path the host answers on. **Must not collide with a real asset in your bundle** —
|
|
38
|
+
* this shadows it. Defaults to `media`.
|
|
39
|
+
* @returns A relative URL, e.g. `/media?eyJzcmMiOiJjbGlwLm1wNCJ9`.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const shell = useShellInfo();
|
|
44
|
+
* const canServe = shell?.capabilities.includes(ShellCapabilities.localFiles);
|
|
45
|
+
* return canServe
|
|
46
|
+
* ? <video src={mediaUrl({ src: id })} controls playsInline />
|
|
47
|
+
* : <button onClick={openExternally}>Open</button>;
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function mediaUrl(payload: unknown, route?: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The payload encoding on its own, for a caller that builds its own URL but wants the same wire format —
|
|
53
|
+
* and so a host-side decoder has one documented thing to mirror.
|
|
54
|
+
*
|
|
55
|
+
* `TextEncoder` before `btoa` because `btoa` throws on any character above U+00FF: a payload carrying a
|
|
56
|
+
* non-ASCII title or path would fail at the call site, which is a poor way to discover an encoding choice.
|
|
57
|
+
*/
|
|
58
|
+
export declare function encodeMediaPayload(payload: unknown): string;
|
|
59
|
+
/**
|
|
60
|
+
* Decode what {@link encodeMediaPayload} produced. Here for tests and for a page that round-trips its own
|
|
61
|
+
* URLs; the real decoder is host-side, in whatever language the shell is written in.
|
|
62
|
+
*
|
|
63
|
+
* Padding is restored before decoding — base64url drops `=`, and `atob` requires it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function decodeMediaPayload<T = unknown>(encoded: string): T;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building the URL that lets a page render LOCAL content — media, images, documents, exports — that it
|
|
3
|
+
* cannot reach directly.
|
|
4
|
+
*
|
|
5
|
+
* The host answers these through its resource interceptor; this module only builds the address, which is
|
|
6
|
+
* why it is a pure function and not a hook. A hook (`useMediaSource`) can follow if an adopter wants
|
|
7
|
+
* load/error state, but nothing needs one to start.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build a URL the host's resource interceptor will answer: `<route>?<base64url of JSON>`.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ **The result is RELATIVE, and that is the whole point.** Written as a path rather than with a scheme, it
|
|
13
|
+
* resolves against whatever origin the page is already served from — which means the browser hands the host
|
|
14
|
+
* a URL each platform can actually decode:
|
|
15
|
+
*
|
|
16
|
+
* | shell | the same call resolves to |
|
|
17
|
+
* |---|---|
|
|
18
|
+
* | iOS | `app://0.0.0.1/media?…` — the app scheme, the only thing iOS intercepts |
|
|
19
|
+
* | Android | `https://0.0.0.1/media?…` — Android's media pipeline REFUSES a non-standard scheme |
|
|
20
|
+
* | desktop | the app's virtual host |
|
|
21
|
+
*
|
|
22
|
+
* Both fixed forms fail on exactly one platform, in opposite directions, and registering the scheme rescues
|
|
23
|
+
* neither: iOS cannot register a handler for `https`, and Android cannot register a scheme at all. Measured
|
|
24
|
+
* on devices — so if you are tempted to hardcode `app://`, that is why not.
|
|
25
|
+
*
|
|
26
|
+
* The PAYLOAD is opaque to this package: whatever you pass is JSON-encoded and handed to your own host-side
|
|
27
|
+
* route, which decodes it. That keeps the kit out of your addressing scheme — a filename, an id, a container
|
|
28
|
+
* preference, a cache key, several of them. The kit encodes; you decide what it means.
|
|
29
|
+
*
|
|
30
|
+
* base64**url** specifically, so `+`, `/` and `=` cannot survive into a query string and be re-interpreted
|
|
31
|
+
* by anything that parses URLs along the way.
|
|
32
|
+
*
|
|
33
|
+
* ⚠ An encoded payload costs debuggability — you cannot read the URL in a log any more. Have the host log
|
|
34
|
+
* what it DECODED to: the response body cannot say, because an error body would leak paths.
|
|
35
|
+
*
|
|
36
|
+
* @param payload Anything JSON-serialisable. Your host route decides what the shape means.
|
|
37
|
+
* @param route The reserved path the host answers on. **Must not collide with a real asset in your bundle** —
|
|
38
|
+
* this shadows it. Defaults to `media`.
|
|
39
|
+
* @returns A relative URL, e.g. `/media?eyJzcmMiOiJjbGlwLm1wNCJ9`.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const shell = useShellInfo();
|
|
44
|
+
* const canServe = shell?.capabilities.includes(ShellCapabilities.localFiles);
|
|
45
|
+
* return canServe
|
|
46
|
+
* ? <video src={mediaUrl({ src: id })} controls playsInline />
|
|
47
|
+
* : <button onClick={openExternally}>Open</button>;
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function mediaUrl(payload, route = 'media') {
|
|
51
|
+
if (typeof route !== 'string' || route.length === 0) {
|
|
52
|
+
throw new Error('mediaUrl: route must be a non-empty string.');
|
|
53
|
+
}
|
|
54
|
+
const path = route.startsWith('/') ? route : `/${route}`;
|
|
55
|
+
return `${path}?${encodeMediaPayload(payload)}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The payload encoding on its own, for a caller that builds its own URL but wants the same wire format —
|
|
59
|
+
* and so a host-side decoder has one documented thing to mirror.
|
|
60
|
+
*
|
|
61
|
+
* `TextEncoder` before `btoa` because `btoa` throws on any character above U+00FF: a payload carrying a
|
|
62
|
+
* non-ASCII title or path would fail at the call site, which is a poor way to discover an encoding choice.
|
|
63
|
+
*/
|
|
64
|
+
export function encodeMediaPayload(payload) {
|
|
65
|
+
const bytes = new TextEncoder().encode(JSON.stringify(payload));
|
|
66
|
+
let binary = '';
|
|
67
|
+
for (const byte of bytes)
|
|
68
|
+
binary += String.fromCharCode(byte);
|
|
69
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Decode what {@link encodeMediaPayload} produced. Here for tests and for a page that round-trips its own
|
|
73
|
+
* URLs; the real decoder is host-side, in whatever language the shell is written in.
|
|
74
|
+
*
|
|
75
|
+
* Padding is restored before decoding — base64url drops `=`, and `atob` requires it.
|
|
76
|
+
*/
|
|
77
|
+
export function decodeMediaPayload(encoded) {
|
|
78
|
+
let padded = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
|
79
|
+
padded += '='.repeat((4 - (padded.length % 4)) % 4);
|
|
80
|
+
const binary = atob(padded);
|
|
81
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
82
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
83
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -105,6 +105,19 @@ export declare const ShellCapabilities: {
|
|
|
105
105
|
readonly savePicker: "savePicker";
|
|
106
106
|
readonly secondaryWindows: "secondaryWindows";
|
|
107
107
|
readonly tray: "tray";
|
|
108
|
+
/**
|
|
109
|
+
* The host can serve LOCAL FILES to this page — media, images, documents, exports — through its resource
|
|
110
|
+
* interceptor. Pair it with {@link mediaUrl}.
|
|
111
|
+
*
|
|
112
|
+
* A page cannot reach a local file itself on any shell (`file://` is blocked from a virtual-host origin,
|
|
113
|
+
* and would be the wrong answer anyway), so branch on this and fall back rather than rendering a player
|
|
114
|
+
* that can never load.
|
|
115
|
+
*
|
|
116
|
+
* ⚠ It says the host CAN serve, not what: routes, payload shape and allowed roots are the app's. And it
|
|
117
|
+
* deliberately tells you nothing about the URL SCHEME — {@link mediaUrl} is relative precisely so each
|
|
118
|
+
* shell supplies its own, and knowing it would put you back to branching on platform.
|
|
119
|
+
*/
|
|
120
|
+
readonly localFiles: "localFiles";
|
|
108
121
|
};
|
|
109
122
|
/** The response envelope the host returns for an {@link IpcRequest}. */
|
|
110
123
|
export interface IpcResponse<TData = unknown> {
|
package/dist/types.js
CHANGED
|
@@ -63,4 +63,17 @@ export const ShellCapabilities = {
|
|
|
63
63
|
savePicker: 'savePicker',
|
|
64
64
|
secondaryWindows: 'secondaryWindows',
|
|
65
65
|
tray: 'tray',
|
|
66
|
+
/**
|
|
67
|
+
* The host can serve LOCAL FILES to this page — media, images, documents, exports — through its resource
|
|
68
|
+
* interceptor. Pair it with {@link mediaUrl}.
|
|
69
|
+
*
|
|
70
|
+
* A page cannot reach a local file itself on any shell (`file://` is blocked from a virtual-host origin,
|
|
71
|
+
* and would be the wrong answer anyway), so branch on this and fall back rather than rendering a player
|
|
72
|
+
* that can never load.
|
|
73
|
+
*
|
|
74
|
+
* ⚠ It says the host CAN serve, not what: routes, payload shape and allowed roots are the app's. And it
|
|
75
|
+
* deliberately tells you nothing about the URL SCHEME — {@link mediaUrl} is relative precisely so each
|
|
76
|
+
* shell supplies its own, and knowing it would put you back to branching on platform.
|
|
77
|
+
*/
|
|
78
|
+
localFiles: 'localFiles',
|
|
66
79
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shenora/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jiarong Gu",
|