@proto-labs-inc/rig-core 0.2.5 → 0.3.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/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # @proto-labs-inc/rig-core
2
2
 
3
- The rig, framework-free: URL-state sync, the postMessage bridge to Proto's
4
- Frame, and comment mode. Prototypes use it through an adapter,
5
- `@proto-labs-inc/rig` for React or `@proto-labs-inc/rig-vue` for Vue.
6
- Comment capture lazy-imports `modern-screenshot` from the prototype's own
7
- dependencies; without it a comment still works, with no picture.
3
+ The stable loader and framework contract for Proto's hosted rig. React and Vue
4
+ adapters load the current compatible runtime through `loadRuntime(manifest,
5
+ signal)`, then use its store and lifecycle methods. The runtime owns state,
6
+ URL/history, bridge, themes, capture, and selection behavior.
7
+
8
+ Every opening checks the active release and verifies downloaded code by SHA-256.
9
+ There is no offline startup fallback. Existing running sessions survive connection
10
+ loss; opening or refreshing without a compatible runtime fails with Retry.
@@ -0,0 +1,36 @@
1
+ import type { Manifest } from "@proto-labs-inc/wire";
2
+ export declare const ADAPTER_CONTRACT = 1;
3
+ export declare const ADAPTER_VERSION = "0.3.0";
4
+ export interface RuntimeStore {
5
+ get(key: string): string | undefined;
6
+ set(key: string, value: string): void;
7
+ subscribe(listener: () => void): () => void;
8
+ }
9
+ export interface RuntimeOptions {
10
+ frameOrigin?: string;
11
+ tools: boolean;
12
+ release: string;
13
+ adapterVersion: string;
14
+ owner?: "desktop";
15
+ }
16
+ export interface RuntimeInstance {
17
+ store: RuntimeStore;
18
+ start(): void;
19
+ dispose(): void;
20
+ }
21
+ export interface RuntimeModule {
22
+ adapterContract: number;
23
+ createRuntime(manifest: Manifest, options: RuntimeOptions): RuntimeInstance;
24
+ }
25
+ export interface RuntimeRelease {
26
+ contract: number;
27
+ release: string;
28
+ sha256: string;
29
+ asset: string;
30
+ }
31
+ export interface RuntimePacket {
32
+ descriptor: RuntimeRelease;
33
+ source: string;
34
+ }
35
+ export declare function isRuntimeRelease(value: unknown): value is RuntimeRelease;
36
+ export declare function verifyRuntime(packet: RuntimePacket): Promise<void>;
@@ -0,0 +1,19 @@
1
+ export const ADAPTER_CONTRACT = 1;
2
+ export const ADAPTER_VERSION = "0.3.0";
3
+ export function isRuntimeRelease(value) {
4
+ if (!value || typeof value !== "object")
5
+ return false;
6
+ const r = value;
7
+ return r.contract === ADAPTER_CONTRACT && typeof r.release === "string" &&
8
+ typeof r.sha256 === "string" && /^[a-f0-9]{64}$/.test(r.sha256) &&
9
+ r.release === r.sha256 && r.asset === `/rig-runtime/${r.sha256}.js`;
10
+ }
11
+ export async function verifyRuntime(packet) {
12
+ if (!isRuntimeRelease(packet.descriptor) || typeof packet.source !== "string")
13
+ throw new Error("Incompatible rig runtime");
14
+ const bytes = new TextEncoder().encode(packet.source);
15
+ const hash = await crypto.subtle.digest("SHA-256", bytes);
16
+ const hex = [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, "0")).join("");
17
+ if (hex !== packet.descriptor.sha256)
18
+ throw new Error("Rig runtime integrity check failed");
19
+ }
package/dist/index.d.ts CHANGED
@@ -1,38 +1,5 @@
1
- /**
2
- * The rig's framework-free core: URL-state sync, the bridge, and the Frame tools
3
- * loader. Everything here is plain DOM + history + postMessage — no framework.
4
- * Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
5
- * this with their own idioms and nothing else.
6
- */
7
- import { type Manifest, type ResolvedPreviewTheme, type RigToFrame } from "@proto-labs-inc/wire";
1
+ /** Stable bootstrap and framework contract. Product behavior lives in rig-runtime. */
2
+ export { loadRuntime } from "./loader.js";
3
+ export { ADAPTER_CONTRACT, ADAPTER_VERSION } from "./contract.js";
4
+ export type { RuntimeInstance, RuntimeStore, RuntimeModule, RuntimeOptions } from "./contract.js";
8
5
  export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
9
- export declare const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
10
- /** Mark the page light or dark (data-proto-theme and the "dark" class) for
11
- * a prototype that styles both. The page's own CSS decides color-scheme:
12
- * forcing it here gave pages that only style light mode dark form fields
13
- * on a light page whenever the viewer's system was dark. */
14
- export declare function applyPreviewTheme(theme: ResolvedPreviewTheme): void;
15
- /** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
16
- export declare class RigStore {
17
- private manifest;
18
- private values;
19
- private listeners;
20
- constructor(manifest: Manifest);
21
- updateManifest(manifest: Manifest): void;
22
- get(key: string): string | undefined;
23
- set(key: string, value: string): void;
24
- setMany(entries: Array<[string, string]>): void;
25
- setState(id: string, variants: Record<string, string>): void;
26
- subscribe: (l: () => void) => () => void;
27
- /** Restore the exact URL, including removed selections, without writing a
28
- * new history entry while Back or Forward is traversing an existing one. */
29
- restoreUrl(): void;
30
- private syncUrl;
31
- }
32
- export declare function postToFrame(msg: RigToFrame): void;
33
- /**
34
- * Wire a store to the bridge: listen for Frame messages, announce the rig,
35
- * and report the boot-time state (the deep-link fix). Returns a disposer.
36
- * Call client-side only.
37
- */
38
- export declare function wireRig(store: RigStore, manifest: Manifest): () => void;
package/dist/index.js CHANGED
@@ -1,277 +1,4 @@
1
- /**
2
- * The rig's framework-free core: URL-state sync, the bridge, and the Frame tools
3
- * loader. Everything here is plain DOM + history + postMessage — no framework.
4
- * Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
5
- * this with their own idioms and nothing else.
6
- */
7
- import { PROTOCOL_VERSION, STATE_PARAM, variantParam, applyVariantSelection, isFrameToRig, } from "@proto-labs-inc/wire";
8
- import { loadFrameTools } from "./frame-tools-loader.js";
9
- import { observePageHistory } from "./page-history.js";
1
+ /** Stable bootstrap and framework contract. Product behavior lives in rig-runtime. */
2
+ export { loadRuntime } from "./loader.js";
3
+ export { ADAPTER_CONTRACT, ADAPTER_VERSION } from "./contract.js";
10
4
  export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
11
- const RIG_VERSION = "0.2.5";
12
- export const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
13
- /** Mark the page light or dark (data-proto-theme and the "dark" class) for
14
- * a prototype that styles both. The page's own CSS decides color-scheme:
15
- * forcing it here gave pages that only style light mode dark form fields
16
- * on a light page whenever the viewer's system was dark. */
17
- export function applyPreviewTheme(theme) {
18
- if (typeof document === "undefined")
19
- return;
20
- const root = document.documentElement;
21
- root.dataset.protoTheme = theme;
22
- root.classList.toggle("dark", theme === "dark");
23
- }
24
- function resolvedSystemTheme() {
25
- return window.matchMedia("(prefers-color-scheme: dark)").matches
26
- ? "dark"
27
- : "light";
28
- }
29
- function searchForFrame() {
30
- const params = new URLSearchParams(window.location.search);
31
- params.delete(INTERNAL_PREVIEW_THEME_PARAM);
32
- const search = params.toString();
33
- return search ? `?${search}` : "";
34
- }
35
- /** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
36
- export class RigStore {
37
- manifest;
38
- values = new Map();
39
- listeners = new Set();
40
- constructor(manifest) {
41
- this.manifest = manifest;
42
- if (typeof window === "undefined")
43
- return;
44
- const params = new URLSearchParams(window.location.search);
45
- const bootTheme = params.get(INTERNAL_PREVIEW_THEME_PARAM);
46
- applyPreviewTheme(bootTheme === "light" || bootTheme === "dark"
47
- ? bootTheme
48
- : resolvedSystemTheme());
49
- for (const [k, v] of params) {
50
- if (k === STATE_PARAM || k.startsWith("v."))
51
- this.values.set(k, v);
52
- }
53
- const state = manifest.states.find((s) => s.id === this.values.get(STATE_PARAM));
54
- let hydrated = false;
55
- for (const [component, variant] of Object.entries(state?.variants ?? {})) {
56
- const key = variantParam(component);
57
- if (this.values.has(key))
58
- continue;
59
- this.values.set(key, variant);
60
- params.set(key, variant);
61
- hydrated = true;
62
- }
63
- if (hydrated) {
64
- window.history.replaceState(null, "", `?${params.toString()}${window.location.hash || ""}`);
65
- }
66
- }
67
- updateManifest(manifest) {
68
- this.manifest = manifest;
69
- }
70
- get(key) {
71
- return this.values.get(key);
72
- }
73
- set(key, value) {
74
- if (key.startsWith("v.")) {
75
- const params = new URLSearchParams([...this.values]);
76
- if (!applyVariantSelection(this.manifest.variantSets, params, key.slice(2), value))
77
- return;
78
- this.setMany([...params.entries()]);
79
- return;
80
- }
81
- this.setMany([[key, value]]);
82
- }
83
- setMany(entries) {
84
- let changed = false;
85
- for (const [key, value] of entries) {
86
- if (this.values.get(key) === value)
87
- continue;
88
- this.values.set(key, value);
89
- changed = true;
90
- }
91
- if (!changed)
92
- return;
93
- this.syncUrl();
94
- this.listeners.forEach((l) => l());
95
- }
96
- setState(id, variants) {
97
- const next = new Map([[STATE_PARAM, id]]);
98
- for (const [component, variant] of Object.entries(variants)) {
99
- next.set(variantParam(component), variant);
100
- }
101
- let changed = this.values.size !== next.size;
102
- if (!changed) {
103
- for (const [key, value] of next) {
104
- if (this.values.get(key) !== value) {
105
- changed = true;
106
- break;
107
- }
108
- }
109
- }
110
- if (!changed)
111
- return;
112
- this.values = next;
113
- this.syncUrl();
114
- this.listeners.forEach((l) => l());
115
- }
116
- subscribe = (l) => {
117
- this.listeners.add(l);
118
- return () => {
119
- this.listeners.delete(l);
120
- };
121
- };
122
- /** Restore the exact URL, including removed selections, without writing a
123
- * new history entry while Back or Forward is traversing an existing one. */
124
- restoreUrl() {
125
- const next = new Map();
126
- for (const [key, value] of new URLSearchParams(window.location.search)) {
127
- if (key === STATE_PARAM || key.startsWith("v."))
128
- next.set(key, value);
129
- }
130
- const changed = next.size !== this.values.size ||
131
- [...next].some(([key, value]) => this.values.get(key) !== value);
132
- if (!changed)
133
- return;
134
- this.values = next;
135
- this.listeners.forEach((listener) => listener());
136
- }
137
- syncUrl() {
138
- const params = new URLSearchParams(window.location.search);
139
- for (const key of [...params.keys()]) {
140
- if (key === STATE_PARAM || key.startsWith("v."))
141
- params.delete(key);
142
- }
143
- for (const [k, v] of this.values)
144
- params.set(k, v);
145
- const search = `?${params.toString()}${window.location.hash || ""}`;
146
- window.history.replaceState(null, "", search);
147
- }
148
- }
149
- export function postToFrame(msg) {
150
- // P0: the Frame's origin isn't known to the prototype; "*" is acceptable
151
- // because nothing sensitive crosses the bridge. Revisit with link protection.
152
- if (typeof window !== "undefined" && window.parent !== window)
153
- window.parent.postMessage(msg, "*");
154
- }
155
- async function waitForCaptureReady() {
156
- const pendingImages = [...document.images]
157
- .filter((image) => !image.complete)
158
- .map((image) => new Promise((resolve) => {
159
- image.addEventListener("load", () => resolve(), { once: true });
160
- image.addEventListener("error", () => resolve(), { once: true });
161
- }));
162
- const assetsReady = Promise.all([document.fonts.ready, ...pendingImages]);
163
- await Promise.race([
164
- assetsReady,
165
- new Promise((resolve) => window.setTimeout(resolve, 3000)),
166
- ]);
167
- // requestAnimationFrame may be suspended for an invisible iframe. The timer
168
- // also lets entrance transitions and canvas-backed backgrounds reach their
169
- // resting state before the gallery captures them.
170
- await new Promise((resolve) => window.setTimeout(resolve, 1500));
171
- }
172
- async function captureThumbnail() {
173
- const restingState = document.createElement("style");
174
- restingState.textContent = `
175
- *, *::before, *::after {
176
- animation-delay: 0s !important;
177
- animation-duration: 0s !important;
178
- transition-delay: 0s !important;
179
- transition-duration: 0s !important;
180
- }
181
- .opacity-0,
182
- [style*="opacity: 0"] {
183
- opacity: 1 !important;
184
- transform: none !important;
185
- }
186
- `;
187
- document.head.append(restingState);
188
- try {
189
- for (const animation of document.getAnimations()) {
190
- try {
191
- animation.finish();
192
- }
193
- catch {
194
- animation.cancel();
195
- }
196
- }
197
- await waitForCaptureReady();
198
- // Optional peer provided by the prototype, not the rig.
199
- const mod = (await import("modern-screenshot"));
200
- const bodyBackground = window.getComputedStyle(document.body).backgroundColor;
201
- const htmlBackground = window.getComputedStyle(document.documentElement).backgroundColor;
202
- const backgroundColor = bodyBackground !== "rgba(0, 0, 0, 0)" ? bodyBackground : htmlBackground;
203
- return await mod.domToDataUrl(document.documentElement, {
204
- backgroundColor,
205
- timeout: 5000,
206
- width: window.innerWidth,
207
- height: window.innerHeight,
208
- scale: 1,
209
- });
210
- }
211
- catch {
212
- return null;
213
- }
214
- finally {
215
- restingState.remove();
216
- }
217
- }
218
- /**
219
- * Wire a store to the bridge: listen for Frame messages, announce the rig,
220
- * and report the boot-time state (the deep-link fix). Returns a disposer.
221
- * Call client-side only.
222
- */
223
- export function wireRig(store, manifest) {
224
- store.updateManifest(manifest);
225
- const disposeHistory = observePageHistory(() => {
226
- store.restoreUrl();
227
- postToFrame({ type: "state-changed", search: searchForFrame() });
228
- });
229
- const disposeTools = loadFrameTools(manifest.frameOrigin);
230
- let controlledTheme = false;
231
- const media = window.matchMedia("(prefers-color-scheme: dark)");
232
- const onSystemTheme = () => {
233
- if (!controlledTheme)
234
- applyPreviewTheme(resolvedSystemTheme());
235
- };
236
- media.addEventListener("change", onSystemTheme);
237
- const onMessage = (e) => {
238
- if (e.source !== window.parent)
239
- return;
240
- if (!isFrameToRig(e.data))
241
- return; // tolerate unknown messages silently
242
- if (e.data.type === "set-state") {
243
- const state = manifest.states.find((s) => s.id === e.data.id);
244
- store.setState(e.data.id, state?.variants ?? {});
245
- }
246
- else if (e.data.type === "set-theme") {
247
- controlledTheme = true;
248
- applyPreviewTheme(e.data.theme);
249
- }
250
- else if (e.data.type === "set-variant")
251
- store.set(variantParam(e.data.component), e.data.variant);
252
- };
253
- window.addEventListener("message", onMessage);
254
- postToFrame({
255
- type: "hello",
256
- protocolVersion: PROTOCOL_VERSION,
257
- rigVersion: RIG_VERSION,
258
- name: manifest.name,
259
- });
260
- postToFrame({ type: "state-changed", search: searchForFrame() });
261
- const thumbnailRequestId = new URLSearchParams(window.location.search).get("_protoThumbnail");
262
- if (thumbnailRequestId) {
263
- void captureThumbnail().then((image) => {
264
- postToFrame({
265
- type: "thumbnail-capture",
266
- image,
267
- requestId: thumbnailRequestId,
268
- });
269
- });
270
- }
271
- return () => {
272
- disposeHistory();
273
- media.removeEventListener("change", onSystemTheme);
274
- window.removeEventListener("message", onMessage);
275
- disposeTools();
276
- };
277
- }
@@ -0,0 +1,3 @@
1
+ import type { Manifest } from "@proto-labs-inc/wire";
2
+ import { type RuntimeInstance } from "./contract.js";
3
+ export declare function loadRuntime(manifest: Manifest, signal: AbortSignal): Promise<RuntimeInstance>;
package/dist/loader.js ADDED
@@ -0,0 +1,124 @@
1
+ import { ADAPTER_CONTRACT, ADAPTER_VERSION, verifyRuntime } from "./contract.js";
2
+ const HOST_KEY = Symbol.for("proto.rig.host");
3
+ const RUNNING_KEY = Symbol.for("proto.rig.running");
4
+ const LOADER_KEY = Symbol.for("proto.rig.loader");
5
+ const global = globalThis;
6
+ // Native desktop authorization supplies the backend, never a query parameter.
7
+ if (typeof window !== "undefined")
8
+ global[LOADER_KEY] = true;
9
+ function trustedOrigin(value) {
10
+ const url = new URL(value);
11
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
12
+ if (url.origin !== value || (url.protocol !== "https:" && !(url.protocol === "http:" && local)))
13
+ throw new Error("Invalid rig origin");
14
+ return url.origin;
15
+ }
16
+ async function resolveHost(manifest, signal) {
17
+ const native = global[HOST_KEY];
18
+ if (native)
19
+ return { ...native, origin: trustedOrigin(native.origin) };
20
+ if (!document.referrer && window.name.startsWith("prototype-page:")) {
21
+ return new Promise((resolve, reject) => {
22
+ const finish = () => { clearTimeout(timer); window.removeEventListener("proto:rig-host", ready); signal.removeEventListener("abort", abort); };
23
+ const ready = () => { const host = global[HOST_KEY]; if (host) {
24
+ finish();
25
+ try {
26
+ resolve({ ...host, origin: trustedOrigin(host.origin) });
27
+ }
28
+ catch (error) {
29
+ reject(error);
30
+ }
31
+ } };
32
+ const abort = () => { finish(); reject(signal.reason); };
33
+ const timer = window.setTimeout(() => { finish(); reject(new Error("Update Proto desktop to load this rig")); }, 15_000);
34
+ window.addEventListener("proto:rig-host", ready);
35
+ signal.addEventListener("abort", abort, { once: true });
36
+ ready();
37
+ });
38
+ }
39
+ if (!manifest.frameOrigin)
40
+ throw new Error("The prototype has no configured Frame origin");
41
+ const origin = trustedOrigin(manifest.frameOrigin);
42
+ const parentMatches = Boolean(document.referrer && new URL(document.referrer).origin === origin);
43
+ return { origin, tools: window.parent !== window && parentMatches && window.name === "prototype-page" };
44
+ }
45
+ async function requestRuntime(host, signal) {
46
+ // Embedded previews ask their authenticated parent to fetch public code. This
47
+ // also avoids HTTPS prototypes making private-network requests to localhost.
48
+ if (window.parent !== window && host.tools) {
49
+ return new Promise((resolve, reject) => {
50
+ const channel = new MessageChannel();
51
+ const finish = () => { clearTimeout(timer); channel.port1.close(); signal.removeEventListener("abort", abort); };
52
+ const abort = () => { finish(); reject(signal.reason); };
53
+ const timer = window.setTimeout(() => { finish(); reject(new Error("Rig runtime request timed out")); }, 15_000);
54
+ signal.addEventListener("abort", abort, { once: true });
55
+ channel.port1.onmessage = event => { finish(); if (event.data?.status === "ready")
56
+ resolve(event.data.packet);
57
+ else
58
+ reject(new Error("Rig runtime unavailable")); };
59
+ window.parent.postMessage({ type: "request-rig-runtime", contract: ADAPTER_CONTRACT }, host.origin, [channel.port2]);
60
+ });
61
+ }
62
+ const response = await fetch(`${host.origin}/rig-runtime/active.json`, { credentials: "omit", cache: "no-store", redirect: "error", signal });
63
+ if (!response.ok)
64
+ throw new Error("Rig release unavailable");
65
+ const descriptor = await response.json();
66
+ // Validate the descriptor before using any path from the network.
67
+ const { isRuntimeRelease } = await import("./contract.js");
68
+ if (!isRuntimeRelease(descriptor))
69
+ throw new Error("Incompatible rig release");
70
+ const asset = await fetch(`${host.origin}${descriptor.asset}`, { credentials: "omit", redirect: "error", signal });
71
+ if (!asset.ok)
72
+ throw new Error("Rig runtime unavailable");
73
+ return { descriptor, source: await asset.text() };
74
+ }
75
+ export async function loadRuntime(manifest, signal) {
76
+ const host = await resolveHost(manifest, signal);
77
+ signal.throwIfAborted();
78
+ const activate = async (packet) => {
79
+ await verifyRuntime(packet);
80
+ signal.throwIfAborted();
81
+ const url = URL.createObjectURL(new Blob([packet.source], { type: "text/javascript" }));
82
+ let instance;
83
+ try {
84
+ const runtime = await import(/* webpackIgnore: true */ /* @vite-ignore */ url);
85
+ signal.throwIfAborted();
86
+ if (runtime.adapterContract !== ADAPTER_CONTRACT || typeof runtime.createRuntime !== "function")
87
+ throw new Error("Incompatible rig module");
88
+ instance = runtime.createRuntime(manifest, { frameOrigin: host.origin, tools: host.tools, release: packet.descriptor.release, adapterVersion: ADAPTER_VERSION, owner: host.owner });
89
+ if (global[RUNNING_KEY])
90
+ throw new Error("A rig runtime is already mounted");
91
+ const mounted = instance;
92
+ global[RUNNING_KEY] = mounted;
93
+ const dispose = mounted.dispose.bind(mounted);
94
+ mounted.dispose = () => {
95
+ try {
96
+ dispose();
97
+ }
98
+ finally {
99
+ if (global[RUNNING_KEY] === mounted)
100
+ delete global[RUNNING_KEY];
101
+ }
102
+ };
103
+ instance.start();
104
+ return instance;
105
+ }
106
+ catch (error) {
107
+ instance?.dispose();
108
+ throw error;
109
+ }
110
+ finally {
111
+ URL.revokeObjectURL(url);
112
+ }
113
+ };
114
+ try {
115
+ const candidate = await requestRuntime(host, AbortSignal.any([signal, AbortSignal.timeout(8_000)]));
116
+ return await activate(candidate);
117
+ }
118
+ catch (error) {
119
+ signal.throwIfAborted();
120
+ if (window.parent !== window)
121
+ window.parent.postMessage({ type: "rig-runtime-status", status: "unavailable" }, host.origin);
122
+ throw error;
123
+ }
124
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proto-labs-inc/rig-core",
3
- "version": "0.2.5",
4
- "description": "Proto's rig, framework-free: URL-state sync, the postMessage bridge to the Frame, and comment mode.",
3
+ "version": "0.3.0",
4
+ "description": "Stable hosted rig loader and framework adapter contract.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -19,20 +19,11 @@
19
19
  "files": [
20
20
  "dist"
21
21
  ],
22
- "peerDependencies": {
23
- "modern-screenshot": ">=4"
24
- },
25
- "peerDependenciesMeta": {
26
- "modern-screenshot": {
27
- "optional": true
28
- }
29
- },
30
22
  "dependencies": {
31
- "@proto-labs-inc/wire": "^0.2.4"
23
+ "@proto-labs-inc/wire": "^0.3.0"
32
24
  },
33
25
  "devDependencies": {
34
- "typescript": "~6.0.2",
35
- "modern-screenshot": "^4.7.0"
26
+ "typescript": "~6.0.2"
36
27
  },
37
28
  "publishConfig": {
38
29
  "access": "public"
@@ -1,2 +0,0 @@
1
- /** Stable bootstrap only. Selection behavior is deployed with the Frame app. */
2
- export declare function loadFrameTools(frameOrigin?: string): () => void;
@@ -1,83 +0,0 @@
1
- /** Stable bootstrap only. Selection behavior is deployed with the Frame app. */
2
- export function loadFrameTools(frameOrigin) {
3
- if (window.parent === window)
4
- return () => { };
5
- let disposed = false;
6
- let closeRequest;
7
- let unmount;
8
- const report = (status, origin = "*") => {
9
- if (!disposed)
10
- window.parent.postMessage({ type: "frame-tools-status", status }, origin);
11
- };
12
- const dispose = () => { disposed = true; closeRequest?.(); unmount?.(); };
13
- if (!frameOrigin) {
14
- report("unavailable");
15
- return dispose;
16
- }
17
- let origin;
18
- try {
19
- const url = new URL(frameOrigin);
20
- const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
21
- if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
22
- throw new Error("Invalid Frame origin");
23
- if (url.username || url.password || url.origin !== frameOrigin)
24
- throw new Error("Expected an origin");
25
- origin = url.origin;
26
- // The origin is pinned in the prototype, never taken from a message or URL parameter.
27
- if (!document.referrer || new URL(document.referrer).origin !== origin)
28
- return dispose;
29
- }
30
- catch {
31
- report("unavailable");
32
- return dispose;
33
- }
34
- report("loading", origin);
35
- // The Frame fetches its own public module. A published HTTPS prototype
36
- // never needs to reach into a localhost or private-network Frame.
37
- const channel = new MessageChannel();
38
- const source = new Promise((resolve, reject) => {
39
- const finish = () => {
40
- clearTimeout(timer);
41
- channel.port1.close();
42
- closeRequest = undefined;
43
- };
44
- const timer = setTimeout(() => {
45
- finish();
46
- reject(new Error("Selection runtime request timed out"));
47
- }, 15_000);
48
- closeRequest = () => {
49
- finish();
50
- reject(new Error("Selection runtime request disposed"));
51
- };
52
- channel.port1.onmessage = (event) => {
53
- finish();
54
- const reply = event.data;
55
- if (reply?.status === "ready" && typeof reply.source === "string")
56
- resolve(reply.source);
57
- else
58
- reject(new Error("Selection runtime unavailable"));
59
- };
60
- window.parent.postMessage({ type: "request-frame-tools" }, origin, [channel.port2]);
61
- });
62
- void source.then(async (text) => {
63
- if (disposed)
64
- return;
65
- const url = URL.createObjectURL(new Blob([text], { type: "text/javascript" }));
66
- try {
67
- const runtime = await import(/* webpackIgnore: true */ /* @vite-ignore */ url);
68
- if (disposed)
69
- return;
70
- unmount = runtime.mountFrameTools({ frameOrigin: origin });
71
- report("ready", origin);
72
- }
73
- finally {
74
- URL.revokeObjectURL(url);
75
- }
76
- }).catch((error) => {
77
- if (disposed)
78
- return;
79
- console.warn("Proto selection tools failed to load", error);
80
- report("unavailable", origin);
81
- });
82
- return dispose;
83
- }
@@ -1,4 +0,0 @@
1
- /** Every URL change in a prototype is a step the browser bar can revisit.
2
- * Keep state-only history writes intact: routers use those for bookkeeping.
3
- * Install after boot-time URL hydration, which is not a navigation. */
4
- export declare function observePageHistory(onNavigate: () => void): () => void;
@@ -1,38 +0,0 @@
1
- /** Every URL change in a prototype is a step the browser bar can revisit.
2
- * Keep state-only history writes intact: routers use those for bookkeeping.
3
- * Install after boot-time URL hydration, which is not a navigation. */
4
- export function observePageHistory(onNavigate) {
5
- const history = window.history;
6
- const push = history.pushState;
7
- const replace = history.replaceState;
8
- let currentUrl = window.location.href;
9
- const changed = () => {
10
- const url = window.location.href;
11
- if (url === currentUrl)
12
- return;
13
- currentUrl = url;
14
- onNavigate();
15
- };
16
- const pushState = function (data, unused, url) {
17
- push.call(this, data, unused, url);
18
- changed();
19
- };
20
- const replaceState = function (data, unused, url) {
21
- const next = url == null ? window.location.href : new URL(url, window.location.href).href;
22
- const write = next === window.location.href ? replace : push;
23
- write.call(this, data, unused, url);
24
- changed();
25
- };
26
- history.pushState = pushState;
27
- history.replaceState = replaceState;
28
- window.addEventListener("popstate", changed);
29
- window.addEventListener("hashchange", changed);
30
- return () => {
31
- if (history.pushState === pushState)
32
- history.pushState = push;
33
- if (history.replaceState === replaceState)
34
- history.replaceState = replace;
35
- window.removeEventListener("popstate", changed);
36
- window.removeEventListener("hashchange", changed);
37
- };
38
- }