@shenora/react 0.8.0 → 0.9.1

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 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';
@@ -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.8.0",
3
+ "version": "0.9.1",
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",