@xeonr/renderer-sdk 1.5.0 → 1.5.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/dist/react/dom.d.ts +1 -0
- package/dist/react/dom.d.ts.map +1 -1
- package/dist/react/dom.js +9 -0
- package/dist/react/dom.js.map +1 -1
- package/dist/react/useRendererClient.d.ts.map +1 -1
- package/dist/react/useRendererClient.js +5 -7
- package/dist/react/useRendererClient.js.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/renderer.css +34 -0
- package/src/client.ts +0 -765
- package/src/index.ts +0 -47
- package/src/protocol.ts +0 -286
- package/src/react/RendererErrorBoundary.tsx +0 -95
- package/src/react/dom.ts +0 -53
- package/src/react/index.ts +0 -5
- package/src/react/useRendererClient.ts +0 -250
- package/src/react/useReportFatalError.tsx +0 -71
- package/src/resources.ts +0 -119
- package/src/types.ts +0 -156
- package/tsconfig.json +0 -18
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
-
import { RendererClient } from '../client.js';
|
|
3
|
-
import type { RendererClientOptions } from '../client.js';
|
|
4
|
-
import type { RendererApiAdapter, RendererConfig, RendererScope, RenderingType, InitPayload } from '../types.js';
|
|
5
|
-
|
|
6
|
-
export interface UseRendererClientOptions extends RendererClientOptions {}
|
|
7
|
-
|
|
8
|
-
// One RendererClient per page. Multiple `useRendererClient()` calls
|
|
9
|
-
// (different components subscribing to the same state) all share the
|
|
10
|
-
// singleton — previously each call spawned its own client, which
|
|
11
|
-
// caused duplicate `window.error` / `unhandledrejection` listeners
|
|
12
|
-
// (so every crash got reported N times), duplicate `uplim:ready`
|
|
13
|
-
// pings, and state divergence between instances.
|
|
14
|
-
//
|
|
15
|
-
// Page lifetime: the iframe is full-reloaded for HMR / test, so the
|
|
16
|
-
// module-level singleton lives exactly as long as it should. SDK-level
|
|
17
|
-
// HMR (developer working on the SDK in monorepo, hot-replacing this
|
|
18
|
-
// module) is handled via `import.meta.hot.dispose` at the bottom of
|
|
19
|
-
// the file.
|
|
20
|
-
let sharedClient: RendererClient | null = null;
|
|
21
|
-
let sharedClientOptionsKey: string | null = null;
|
|
22
|
-
|
|
23
|
-
function optionsKey(options: UseRendererClientOptions | undefined): string {
|
|
24
|
-
if (!options) return '';
|
|
25
|
-
// Stable order so { a, b } and { b, a } produce the same key. Used
|
|
26
|
-
// only for the dev-mode mismatch warning — never for behavior.
|
|
27
|
-
const entries = Object.entries(options).sort(([a], [b]) => a.localeCompare(b));
|
|
28
|
-
return JSON.stringify(entries);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getSharedClient(options: UseRendererClientOptions | undefined): RendererClient {
|
|
32
|
-
if (sharedClient) {
|
|
33
|
-
const key = optionsKey(options);
|
|
34
|
-
// Best-effort dev nudge: if a later mount passes different
|
|
35
|
-
// options than the first one, those options are silently
|
|
36
|
-
// ignored. Only flag when keys differ AND the later caller
|
|
37
|
-
// passed non-empty options (passing nothing is the common
|
|
38
|
-
// case for downstream subscribers).
|
|
39
|
-
if (key && sharedClientOptionsKey !== null && key !== sharedClientOptionsKey) {
|
|
40
|
-
// eslint-disable-next-line no-console
|
|
41
|
-
console.warn(
|
|
42
|
-
'[uplim] useRendererClient called with different options after first call; ' +
|
|
43
|
-
'later options are ignored (the client is a per-page singleton).',
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
return sharedClient;
|
|
47
|
-
}
|
|
48
|
-
sharedClient = new RendererClient(options);
|
|
49
|
-
sharedClientOptionsKey = optionsKey(options);
|
|
50
|
-
return sharedClient;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// SDK-dev HMR cleanup: when this module is hot-replaced (e.g. someone
|
|
54
|
-
// editing the SDK with the renderer harness running), detach the old
|
|
55
|
-
// client's listeners before the new module evaluates. Prod / non-HMR
|
|
56
|
-
// builds skip the entire block — `import.meta.hot` is undefined.
|
|
57
|
-
const maybeHot = (import.meta as ImportMeta & { hot?: { dispose: (cb: () => void) => void } }).hot;
|
|
58
|
-
if (maybeHot) {
|
|
59
|
-
maybeHot.dispose(() => {
|
|
60
|
-
if (sharedClient) {
|
|
61
|
-
sharedClient.destroy();
|
|
62
|
-
sharedClient = null;
|
|
63
|
-
sharedClientOptionsKey = null;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Apply the host's theme to the renderer document. Two distinct effects:
|
|
70
|
-
*
|
|
71
|
-
* - `documentElement.dataset.theme` — surfaces the theme as a
|
|
72
|
-
* `[data-theme="dark"]` selector for renderer-authored CSS.
|
|
73
|
-
* - `documentElement.style.colorScheme` — tells the browser to render
|
|
74
|
-
* scrollbars, form controls, and (critically) the iframe's default
|
|
75
|
-
* canvas in the matching mode. Without this, a cross-origin iframe
|
|
76
|
-
* defaults to a white canvas regardless of what the host's
|
|
77
|
-
* `color-scheme` is — so `background: transparent` in the renderer
|
|
78
|
-
* reveals white, not the host's dark surface. This is the fix for
|
|
79
|
-
* "renderer shows light mode when host is dark".
|
|
80
|
-
*/
|
|
81
|
-
function applyThemeToDocument(theme: 'light' | 'dark'): void {
|
|
82
|
-
if (typeof document === 'undefined') return;
|
|
83
|
-
document.documentElement.dataset.theme = theme;
|
|
84
|
-
document.documentElement.style.colorScheme = theme;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface UseRendererClientResult {
|
|
88
|
-
/** Whether the host has sent the init message. */
|
|
89
|
-
connected: boolean;
|
|
90
|
-
/** What resource the renderer is scoped to. */
|
|
91
|
-
scope: RendererScope | null;
|
|
92
|
-
/** How the renderer is being displayed. */
|
|
93
|
-
renderingType: RenderingType | null;
|
|
94
|
-
/** The current integration access token. */
|
|
95
|
-
token: string | null;
|
|
96
|
-
/** Unix timestamp (ms) when the token expires. */
|
|
97
|
-
tokenExpiresAt: number | null;
|
|
98
|
-
/** The current host theme. */
|
|
99
|
-
theme: 'light' | 'dark';
|
|
100
|
-
/** The parsed config.json from the renderer archive. */
|
|
101
|
-
config: RendererConfig | null;
|
|
102
|
-
/** Which entrypoint was loaded (dashboard or portal). */
|
|
103
|
-
entrypoint: 'dashboard' | 'portal' | null;
|
|
104
|
-
/** Base URL for upl.im API calls. */
|
|
105
|
-
apiBaseUrl: string | null;
|
|
106
|
-
/** Pre-configured API adapter for use with `getUploadClientWithEnv()`. */
|
|
107
|
-
apiAdapter: RendererApiAdapter;
|
|
108
|
-
/**
|
|
109
|
-
* Pre-loaded resource snapshot (typically the Upload proto's JSON
|
|
110
|
-
* form for upload-scoped renderers). Renderers should cast to the
|
|
111
|
-
* expected proto JSON type and use this directly to avoid an extra
|
|
112
|
-
* GetUpload round trip on first paint. `null` when the host didn't
|
|
113
|
-
* supply one (older hosts, folder/virtual-file scopes); fall back
|
|
114
|
-
* to a fetch in that case.
|
|
115
|
-
*/
|
|
116
|
-
resource: unknown;
|
|
117
|
-
/** Current hash path within the renderer (e.g. '/settings'). Updated reactively on hash changes. */
|
|
118
|
-
path: string;
|
|
119
|
-
|
|
120
|
-
/** Request the host to open a specific upload. */
|
|
121
|
-
openUpload: (uploadId: string) => void;
|
|
122
|
-
/** Request a fresh integration access token. */
|
|
123
|
-
requestToken: () => void;
|
|
124
|
-
/** Request the host to close this renderer (modal mode). */
|
|
125
|
-
close: () => void;
|
|
126
|
-
/**
|
|
127
|
-
* When the renderer's `config.json` has `deferReady: true`, call
|
|
128
|
-
* this once the renderer's data is loaded and the UI is ready to
|
|
129
|
-
* be visible. The host overlay stays up until this fires (or until
|
|
130
|
-
* the SDK's safety timeout elapses — see
|
|
131
|
-
* `RendererClientOptions.readyTimeoutMs`). No-op when `deferReady`
|
|
132
|
-
* isn't set in config.
|
|
133
|
-
*/
|
|
134
|
-
markReady: () => void;
|
|
135
|
-
|
|
136
|
-
/** The underlying RendererClient instance for advanced use. */
|
|
137
|
-
client: RendererClient;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* React hook for building custom renderers.
|
|
142
|
-
*
|
|
143
|
-
* Creates a `RendererClient` instance, manages its lifecycle, and exposes
|
|
144
|
-
* reactive state that triggers re-renders when the host sends messages.
|
|
145
|
-
*
|
|
146
|
-
* ```tsx
|
|
147
|
-
* import { useRendererClient } from '@xeonr/renderer-sdk/react';
|
|
148
|
-
*
|
|
149
|
-
* function App() {
|
|
150
|
-
* const { connected, scope, renderingType, theme, token } = useRendererClient();
|
|
151
|
-
*
|
|
152
|
-
* if (!connected) return <div>Connecting...</div>;
|
|
153
|
-
*
|
|
154
|
-
* return (
|
|
155
|
-
* <div data-theme={theme}>
|
|
156
|
-
* <pre>{JSON.stringify(scope, null, 2)}</pre>
|
|
157
|
-
* </div>
|
|
158
|
-
* );
|
|
159
|
-
* }
|
|
160
|
-
* ```
|
|
161
|
-
*/
|
|
162
|
-
export function useRendererClient(options?: UseRendererClientOptions): UseRendererClientResult {
|
|
163
|
-
const client = getSharedClient(options);
|
|
164
|
-
|
|
165
|
-
// Initial state read directly from the client — handles late
|
|
166
|
-
// subscribers (a second `useRendererClient` mount that happens
|
|
167
|
-
// after init has already fired). Without this, the late mount
|
|
168
|
-
// would sit at `connected: false` forever because onInit only
|
|
169
|
-
// fires for NEW init events, not for already-received state.
|
|
170
|
-
const [connected, setConnected] = useState<boolean>(() => client.isConnected());
|
|
171
|
-
const [scope, setScope] = useState<RendererScope | null>(() => client.getScope());
|
|
172
|
-
const [renderingType, setRenderingType] = useState<RenderingType | null>(() => client.getRenderingType());
|
|
173
|
-
const [token, setToken] = useState<string | null>(() => client.getToken());
|
|
174
|
-
const [tokenExpiresAt, setTokenExpiresAt] = useState<number | null>(() => client.getTokenExpiresAt());
|
|
175
|
-
const [theme, setTheme] = useState<'light' | 'dark'>(() => client.getTheme());
|
|
176
|
-
const [config, setConfig] = useState<RendererConfig | null>(() => client.getConfig());
|
|
177
|
-
const [entrypoint, setEntrypoint] = useState<'dashboard' | 'portal' | null>(() => client.getEntrypoint());
|
|
178
|
-
const [apiBaseUrl, setApiBaseUrl] = useState<string | null>(() => client.getApiBaseUrl());
|
|
179
|
-
const [resource, setResource] = useState<unknown>(() => client.getResource());
|
|
180
|
-
const [path, setPath] = useState<string>(() => client.getPath());
|
|
181
|
-
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
const unsubInit = client.onInit((payload: InitPayload) => {
|
|
184
|
-
setConnected(true);
|
|
185
|
-
setScope(payload.scope);
|
|
186
|
-
setRenderingType(payload.renderingType);
|
|
187
|
-
setToken(payload.token);
|
|
188
|
-
setTokenExpiresAt(payload.tokenExpiresAt);
|
|
189
|
-
setTheme(payload.theme);
|
|
190
|
-
setConfig(payload.config);
|
|
191
|
-
setEntrypoint(payload.entrypoint);
|
|
192
|
-
setApiBaseUrl(payload.apiBaseUrl);
|
|
193
|
-
setResource(payload.resource ?? null);
|
|
194
|
-
setPath(client.getPath());
|
|
195
|
-
applyThemeToDocument(payload.theme);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const unsubTheme = client.onThemeChange((newTheme) => {
|
|
199
|
-
setTheme(newTheme);
|
|
200
|
-
applyThemeToDocument(newTheme);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const unsubToken = client.onTokenRefresh((newToken, newExpiresAt) => {
|
|
204
|
-
setToken(newToken);
|
|
205
|
-
setTokenExpiresAt(newExpiresAt);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const unsubNavigate = client.onNavigate((newPath) => {
|
|
209
|
-
setPath(newPath);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
return () => {
|
|
213
|
-
unsubInit();
|
|
214
|
-
unsubTheme();
|
|
215
|
-
unsubToken();
|
|
216
|
-
unsubNavigate();
|
|
217
|
-
// NB: do NOT destroy() here — the client is a per-page
|
|
218
|
-
// singleton, not a per-component instance. Component
|
|
219
|
-
// unmounts (route changes / strict-mode double-mount /
|
|
220
|
-
// React render-time tearing) shouldn't kill the shared
|
|
221
|
-
// state. The iframe full-reload tears the singleton down
|
|
222
|
-
// implicitly when the page goes away; SDK-level HMR is
|
|
223
|
-
// handled by import.meta.hot.dispose at the top of file.
|
|
224
|
-
};
|
|
225
|
-
}, [client]);
|
|
226
|
-
|
|
227
|
-
const methods = useMemo(() => ({
|
|
228
|
-
openUpload: (uploadId: string) => client.openUpload(uploadId),
|
|
229
|
-
requestToken: () => client.requestToken(),
|
|
230
|
-
close: () => client.close(),
|
|
231
|
-
markReady: () => client.signalReady(),
|
|
232
|
-
apiAdapter: client.getApiAdapter(),
|
|
233
|
-
}), [client]);
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
connected,
|
|
237
|
-
scope,
|
|
238
|
-
renderingType,
|
|
239
|
-
token,
|
|
240
|
-
tokenExpiresAt,
|
|
241
|
-
theme,
|
|
242
|
-
config,
|
|
243
|
-
entrypoint,
|
|
244
|
-
apiBaseUrl,
|
|
245
|
-
resource,
|
|
246
|
-
path,
|
|
247
|
-
client,
|
|
248
|
-
...methods,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Hook for surfacing async failures to `<RendererErrorBoundary>` (and from
|
|
5
|
-
* there to the host's crash overlay + telemetry).
|
|
6
|
-
*
|
|
7
|
-
* Renderers shouldn't render their own "Upload not found" / "Failed to
|
|
8
|
-
* load" inline UI — those are still crashes from a telemetry standpoint,
|
|
9
|
-
* and the host already owns a polished overlay with reload + fallback
|
|
10
|
-
* actions. This hook lets a renderer take an error caught in an async
|
|
11
|
-
* context (fetch `.catch()`, useEffect cleanup, etc.) and route it to
|
|
12
|
-
* the host instead of rendering a half-broken inline state.
|
|
13
|
-
*
|
|
14
|
-
* Pattern:
|
|
15
|
-
*
|
|
16
|
-
* ```tsx
|
|
17
|
-
* const reportFatal = useReportFatalError();
|
|
18
|
-
* useEffect(() => {
|
|
19
|
-
* client.getUpload(...).catch(reportFatal);
|
|
20
|
-
* }, []);
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* Mechanics: we keep an error in component state. When set, the *next*
|
|
24
|
-
* render throws it — React's error-handling pipeline catches the throw
|
|
25
|
-
* and bubbles to the nearest boundary. From there
|
|
26
|
-
* `RendererErrorBoundary.componentDidCatch` reports the crash to the
|
|
27
|
-
* host and renders nothing, and the host overlay (Reload / Copy /
|
|
28
|
-
* Fallback) takes over the visible iframe area.
|
|
29
|
-
*
|
|
30
|
-
* Why state-then-throw rather than a direct `throw err`? You can't
|
|
31
|
-
* throw to React from async code — React only sees throws that happen
|
|
32
|
-
* during render or in event handlers. Routing through state ensures
|
|
33
|
-
* the throw lands inside the render phase.
|
|
34
|
-
*
|
|
35
|
-
* The returned callback is stable across renders so it's safe to drop
|
|
36
|
-
* straight into a `.catch()`.
|
|
37
|
-
*/
|
|
38
|
-
export function useReportFatalError(): (err: unknown) => void {
|
|
39
|
-
// Generic state slot — we never read the value, only use the
|
|
40
|
-
// updater. We narrow to `never` so TypeScript stops us from
|
|
41
|
-
// accidentally using it as data anywhere.
|
|
42
|
-
const [, setState] = useState<never>();
|
|
43
|
-
return useCallback((err: unknown) => {
|
|
44
|
-
// Functional updater throws — React invokes it during the next
|
|
45
|
-
// render attempt, which puts the throw inside the render phase
|
|
46
|
-
// where boundaries can see it.
|
|
47
|
-
setState(() => {
|
|
48
|
-
if (err instanceof Error) {
|
|
49
|
-
throw err;
|
|
50
|
-
}
|
|
51
|
-
// Wrap non-Error throws so the boundary always sees a real
|
|
52
|
-
// Error instance with a meaningful .name / .message. Without
|
|
53
|
-
// this, `client.reportCrash` would synthesise a generic
|
|
54
|
-
// 'UnknownError' which loses the original payload's shape.
|
|
55
|
-
if (typeof err === 'string') {
|
|
56
|
-
throw new Error(err);
|
|
57
|
-
}
|
|
58
|
-
if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') {
|
|
59
|
-
const wrapped = new Error(err.message);
|
|
60
|
-
if ('name' in err && typeof err.name === 'string') {
|
|
61
|
-
wrapped.name = err.name;
|
|
62
|
-
}
|
|
63
|
-
if ('stack' in err && typeof err.stack === 'string') {
|
|
64
|
-
wrapped.stack = err.stack;
|
|
65
|
-
}
|
|
66
|
-
throw wrapped;
|
|
67
|
-
}
|
|
68
|
-
throw new Error('Unknown renderer error');
|
|
69
|
-
});
|
|
70
|
-
}, []);
|
|
71
|
-
}
|
package/src/resources.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed resource accessors that read from the host-seeded `resource`
|
|
3
|
-
* cache first and fall back to a real API call when the cache is
|
|
4
|
-
* absent or stale.
|
|
5
|
-
*
|
|
6
|
-
* Lives in its own file so the imports against `@xeonr/uploads-protocol`
|
|
7
|
-
* and `@xeonr/uploads-sdk` are kept out of `client.ts`. Those packages
|
|
8
|
-
* are declared as optional peer dependencies — a renderer that never
|
|
9
|
-
* calls `getUpload()` / `getFolder()` / `getBucket()` doesn't need to
|
|
10
|
-
* install them. The fetch helpers throw at call time when the deps are
|
|
11
|
-
* missing.
|
|
12
|
-
*/
|
|
13
|
-
import { fromJson } from '@bufbuild/protobuf';
|
|
14
|
-
import {
|
|
15
|
-
BucketUploadsService,
|
|
16
|
-
BucketFoldersService,
|
|
17
|
-
UploadSchema,
|
|
18
|
-
FolderSchema,
|
|
19
|
-
type Upload,
|
|
20
|
-
type Folder,
|
|
21
|
-
} from '@xeonr/uploads-protocol/uplim/api/v1/uploads_pb';
|
|
22
|
-
import {
|
|
23
|
-
BucketsService,
|
|
24
|
-
BucketSchema,
|
|
25
|
-
type Bucket,
|
|
26
|
-
} from '@xeonr/uploads-protocol/uplim/api/v1/buckets_pb';
|
|
27
|
-
import { getUploadClientWithEnv } from '@xeonr/uploads-sdk/api/base';
|
|
28
|
-
import type { RendererApiAdapter, RendererScope } from './types.js';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Best-effort decoder for the host's pre-loaded `resource` field. The
|
|
32
|
-
* host serialises via `toJson(UploadSchema, …)`/etc and posts the JSON
|
|
33
|
-
* across the iframe boundary, so we run it back through `fromJson` to
|
|
34
|
-
* land at a real Message instance with the right prototype methods.
|
|
35
|
-
*
|
|
36
|
-
* Returns null on any failure (schema mismatch, missing fields, etc.)
|
|
37
|
-
* so callers fall through to a fresh fetch.
|
|
38
|
-
*/
|
|
39
|
-
export function decodeUploadResource(value: unknown): Upload | null {
|
|
40
|
-
if (!value || typeof value !== 'object') return null;
|
|
41
|
-
try {
|
|
42
|
-
return fromJson(UploadSchema, value as Parameters<typeof fromJson<typeof UploadSchema>>[1]);
|
|
43
|
-
} catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function decodeFolderResource(value: unknown): Folder | null {
|
|
49
|
-
if (!value || typeof value !== 'object') return null;
|
|
50
|
-
try {
|
|
51
|
-
return fromJson(FolderSchema, value as Parameters<typeof fromJson<typeof FolderSchema>>[1]);
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function decodeBucketResource(value: unknown): Bucket | null {
|
|
58
|
-
if (!value || typeof value !== 'object') return null;
|
|
59
|
-
try {
|
|
60
|
-
return fromJson(BucketSchema, value as Parameters<typeof fromJson<typeof BucketSchema>>[1]);
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Fetch the upload for the current scope via the API. Caller already
|
|
68
|
-
* narrowed the scope; we just translate to the BucketUploadsService
|
|
69
|
-
* RPC. Throws when the API returns no upload (treated as a not-found
|
|
70
|
-
* by the renderer).
|
|
71
|
-
*/
|
|
72
|
-
export async function fetchUpload(
|
|
73
|
-
scope: RendererScope,
|
|
74
|
-
apiAdapter: RendererApiAdapter,
|
|
75
|
-
): Promise<Upload> {
|
|
76
|
-
if (scope.type !== 'upload') {
|
|
77
|
-
throw new Error('fetchUpload requires an upload-typed scope');
|
|
78
|
-
}
|
|
79
|
-
const client = getUploadClientWithEnv(BucketUploadsService, apiAdapter);
|
|
80
|
-
const res = await client.getUpload({
|
|
81
|
-
bucketRef: { type: { case: 'bucketId', value: scope.bucketId } },
|
|
82
|
-
uploadRef: { type: { case: 'uploadId', value: scope.uploadId } },
|
|
83
|
-
});
|
|
84
|
-
if (!res.upload) throw new Error('Upload not found');
|
|
85
|
-
return res.upload;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function fetchFolder(
|
|
89
|
-
scope: RendererScope,
|
|
90
|
-
apiAdapter: RendererApiAdapter,
|
|
91
|
-
): Promise<Folder> {
|
|
92
|
-
// Folder scopes ('folder' or 'virtual-file' with a folderId) carry
|
|
93
|
-
// the identifier; everything else is a misuse.
|
|
94
|
-
let folderId: string | undefined;
|
|
95
|
-
if (scope.type === 'folder') folderId = scope.folderId;
|
|
96
|
-
else if (scope.type === 'virtual-file') folderId = scope.folderId;
|
|
97
|
-
if (!folderId) {
|
|
98
|
-
throw new Error('fetchFolder requires a folder-typed scope with a folderId');
|
|
99
|
-
}
|
|
100
|
-
const client = getUploadClientWithEnv(BucketFoldersService, apiAdapter);
|
|
101
|
-
const res = await client.getFolder({
|
|
102
|
-
bucketRef: { type: { case: 'bucketId', value: scope.bucketId } },
|
|
103
|
-
folderRef: { type: { case: 'folderId', value: folderId } },
|
|
104
|
-
});
|
|
105
|
-
if (!res.folder) throw new Error('Folder not found');
|
|
106
|
-
return res.folder;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export async function fetchBucket(
|
|
110
|
-
scope: RendererScope,
|
|
111
|
-
apiAdapter: RendererApiAdapter,
|
|
112
|
-
): Promise<Bucket> {
|
|
113
|
-
const client = getUploadClientWithEnv(BucketsService, apiAdapter);
|
|
114
|
-
const res = await client.getBucket({
|
|
115
|
-
bucketRef: { type: { case: 'bucketId', value: scope.bucketId } },
|
|
116
|
-
});
|
|
117
|
-
if (!res.bucket) throw new Error('Bucket not found');
|
|
118
|
-
return res.bucket;
|
|
119
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* config.json schema — lives at the root of a renderer archive.
|
|
3
|
-
* Validated server-side on upload; provided to the renderer in the init message.
|
|
4
|
-
*/
|
|
5
|
-
export interface RendererConfig {
|
|
6
|
-
version: 1;
|
|
7
|
-
/** Permissions the renderer requests from the host. */
|
|
8
|
-
permissions?: RendererPermission[];
|
|
9
|
-
sandbox?: {
|
|
10
|
-
allowPopups?: boolean;
|
|
11
|
-
allowForms?: boolean;
|
|
12
|
-
allowDownloads?: boolean;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Extra origins this renderer needs to reach via `fetch` / `XMLHttpRequest`
|
|
16
|
-
* / WebSocket. Surfaced to the browser as additional entries in the CSP
|
|
17
|
-
* `connect-src` directive. The upl.im API + auth hosts are always allowed
|
|
18
|
-
* by the renderer-proxy; only list extras here (e.g. a third-party API
|
|
19
|
-
* the renderer integrates with).
|
|
20
|
-
*
|
|
21
|
-
* Each entry is a fully-qualified origin like `https://api.example.com`
|
|
22
|
-
* or a wildcarded host like `https://*.example.com`. No paths, no
|
|
23
|
-
* trailing slashes. Origins are validated at deploy time.
|
|
24
|
-
*/
|
|
25
|
-
connectHosts?: string[];
|
|
26
|
-
/**
|
|
27
|
-
* Short build identifier the build pipeline stamps into config.json
|
|
28
|
-
* (e.g. the renderer-plugin-vite hash of the output directory).
|
|
29
|
-
* Surfaced in crash telemetry as a separate dimension so we can
|
|
30
|
-
* distinguish "crashed in build A" from "crashed in build B" even
|
|
31
|
-
* when both builds share the same archive_upload_id (the developer
|
|
32
|
-
* republished without bumping the archive). Bounded to 64 chars.
|
|
33
|
-
*/
|
|
34
|
-
buildHash?: string;
|
|
35
|
-
/**
|
|
36
|
-
* When true, the SDK holds `uplim:ack` until the renderer explicitly
|
|
37
|
-
* calls `markReady()` from `useRendererClient()`. The host's loading
|
|
38
|
-
* overlay stays visible until that signal arrives, so the user
|
|
39
|
-
* never sees a blank canvas between bridge ack and the renderer's
|
|
40
|
-
* first paint.
|
|
41
|
-
*
|
|
42
|
-
* Leave unset (or `false`) for fast renderers where the gap is
|
|
43
|
-
* invisible — that's the right default and lowest-friction pattern
|
|
44
|
-
* (image, code, plain markdown). Set `true` when the renderer takes
|
|
45
|
-
* perceptible time to settle (large file fetch, video decode,
|
|
46
|
-
* multi-page paginate) and the blank-canvas flash is noticeable.
|
|
47
|
-
*
|
|
48
|
-
* The SDK auto-acks after 30s (configurable via
|
|
49
|
-
* `RendererClientOptions.readyTimeoutMs`) and logs a warning if
|
|
50
|
-
* `markReady()` never fires — so a forgotten signal can't trap
|
|
51
|
-
* users behind the overlay.
|
|
52
|
-
*/
|
|
53
|
-
deferReady?: boolean;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type RendererPermission =
|
|
57
|
-
| 'createFolder'
|
|
58
|
-
| 'openUpload';
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* What resource the renderer is scoped to.
|
|
62
|
-
* Sent from host to iframe in the init message.
|
|
63
|
-
*/
|
|
64
|
-
export type RendererScope =
|
|
65
|
-
| RendererBucketScope
|
|
66
|
-
| RendererFolderScope
|
|
67
|
-
| RendererUploadScope
|
|
68
|
-
| RendererVirtualFileScope;
|
|
69
|
-
|
|
70
|
-
export interface RendererBucketScope {
|
|
71
|
-
type: 'bucket';
|
|
72
|
-
bucketId: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface RendererFolderScope {
|
|
76
|
-
type: 'folder';
|
|
77
|
-
bucketId: string;
|
|
78
|
-
folderId: string;
|
|
79
|
-
path: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface RendererUploadScope {
|
|
83
|
-
type: 'upload';
|
|
84
|
-
bucketId: string;
|
|
85
|
-
uploadId: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface RendererVirtualFileScope {
|
|
89
|
-
type: 'virtual-file';
|
|
90
|
-
bucketId: string;
|
|
91
|
-
folderId?: string;
|
|
92
|
-
path: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* How the renderer is being displayed within the dashboard entrypoint.
|
|
97
|
-
* A single renderer may handle multiple rendering types.
|
|
98
|
-
*/
|
|
99
|
-
export type RenderingType =
|
|
100
|
-
| 'bucket-renderer'
|
|
101
|
-
| 'folder-renderer'
|
|
102
|
-
| 'preview-modal';
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* API adapter compatible with `IUploadAdapterEnv` from `@xeonr/uploads-sdk`.
|
|
106
|
-
* Pass this to `getUploadClientWithEnv()` to get a pre-configured API client
|
|
107
|
-
* that handles authentication and token refresh automatically.
|
|
108
|
-
*/
|
|
109
|
-
export interface RendererApiAdapter {
|
|
110
|
-
hostname?: string;
|
|
111
|
-
tokenHelper?: () => Promise<string | null>;
|
|
112
|
-
onAuthenticationExpired?: (retryCount: number) => Promise<boolean>;
|
|
113
|
-
onAuthenticationFailed?: () => Promise<any>;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Payload received by the renderer on initialization. */
|
|
117
|
-
export interface InitPayload {
|
|
118
|
-
/** Protocol version. */
|
|
119
|
-
version: 1;
|
|
120
|
-
/** Integration access token scoped to the integration's client. */
|
|
121
|
-
token: string;
|
|
122
|
-
/** Unix timestamp (ms) when the token expires. */
|
|
123
|
-
tokenExpiresAt: number;
|
|
124
|
-
/** Current host theme. */
|
|
125
|
-
theme: 'light' | 'dark';
|
|
126
|
-
/** Which entrypoint was loaded. */
|
|
127
|
-
entrypoint: 'dashboard' | 'portal';
|
|
128
|
-
/** What resource the renderer is scoped to. */
|
|
129
|
-
scope: RendererScope;
|
|
130
|
-
/** How the renderer is being displayed (dashboard only). */
|
|
131
|
-
renderingType: RenderingType;
|
|
132
|
-
/** Base URL for upl.im API calls. */
|
|
133
|
-
apiBaseUrl: string;
|
|
134
|
-
/** The parsed config.json from the renderer archive. */
|
|
135
|
-
config: RendererConfig;
|
|
136
|
-
/** Initial hash path to navigate to, derived from the host URL fragment. */
|
|
137
|
-
initialPath?: string;
|
|
138
|
-
/**
|
|
139
|
-
* Pre-loaded resource snapshot the host already had on hand when it
|
|
140
|
-
* mounted the iframe — typically the `Upload` proto (in its JSON
|
|
141
|
-
* encoding) for upload-scoped renderers, or `Folder` for folder
|
|
142
|
-
* scopes. Renderers can consume this directly to avoid an extra
|
|
143
|
-
* GetUpload / GetFolder round trip on first paint.
|
|
144
|
-
*
|
|
145
|
-
* Shape is intentionally loose (`unknown`) because the SDK is
|
|
146
|
-
* deliberately decoupled from the @xeonr/uploads-protocol types —
|
|
147
|
-
* renderers cast to the proto JSON type (e.g. `UploadJson`) on use.
|
|
148
|
-
* Treat as advisory: if absent or stale, fall back to a fresh fetch.
|
|
149
|
-
*
|
|
150
|
-
* Stays in sync via the existing `resource_url`-style fetches that
|
|
151
|
-
* the renderer triggers when the host posts an updated init (e.g.
|
|
152
|
-
* after edit / replace flows). For now hosts only populate the
|
|
153
|
-
* field for upload-typed scopes; future hosts may extend.
|
|
154
|
-
*/
|
|
155
|
-
resource?: unknown;
|
|
156
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"declarationMap": true,
|
|
8
|
-
"sourceMap": true,
|
|
9
|
-
"outDir": "./dist",
|
|
10
|
-
"rootDir": "./src",
|
|
11
|
-
"jsx": "react-jsx",
|
|
12
|
-
"strict": true,
|
|
13
|
-
"esModuleInterop": true,
|
|
14
|
-
"skipLibCheck": true,
|
|
15
|
-
"forceConsistentCasingInFileNames": true
|
|
16
|
-
},
|
|
17
|
-
"include": ["src"]
|
|
18
|
-
}
|
package/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["./src/client.ts","./src/index.ts","./src/protocol.ts","./src/types.ts","./src/react/index.ts","./src/react/userendererclient.ts"],"version":"5.9.3"}
|