@onlook/storybook-plugin 0.4.0-beta.7 → 0.4.0-beta.9
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/cli/index.js +112 -19
- package/dist/index.d.ts +91 -12
- package/dist/index.js +1199 -97395
- package/dist/preset/index.d.ts +10 -0
- package/dist/preset/index.js +1341 -0
- package/dist/preview/index.d.ts +156 -0
- package/dist/preview/index.js +302 -0
- package/dist/screenshot-service/index.js +109 -15
- package/dist/screenshot-service/utils/browser/index.d.ts +0 -6
- package/dist/screenshot-service/utils/browser/index.js +76 -13
- package/dist/storybook-onlook-plugin-CigILdDb.d.ts +61 -0
- package/package.json +16 -5
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Advertised + embedded in every event payload. Mirrored CLI-side (U8). */
|
|
4
|
+
declare const ONLOOK_CONTAINMENT_CONTRACT_VERSION = 1;
|
|
5
|
+
/** Marker attached to every contained export, thrown error, and the global registry. */
|
|
6
|
+
type ContainedModuleMarker = {
|
|
7
|
+
modulePath: string;
|
|
8
|
+
reason: 'env-validation';
|
|
9
|
+
envKeys: string[];
|
|
10
|
+
schemaKind?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Handshake global, set by the preview annotations (in `beforeAll` and again
|
|
14
|
+
* defensively when the decorator first runs):
|
|
15
|
+
* `globalThis.__ONLOOK_CONTAINMENT_CONTRACT__ = { version: 1 }`.
|
|
16
|
+
*/
|
|
17
|
+
declare const ONLOOK_CONTAINMENT_CONTRACT_GLOBAL = "__ONLOOK_CONTAINMENT_CONTRACT__";
|
|
18
|
+
type OnlookContainmentContract = {
|
|
19
|
+
version: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Emitted when the error-boundary decorator catches a render-time throw and
|
|
23
|
+
* renders the "needs setup" card in place of the story.
|
|
24
|
+
*/
|
|
25
|
+
declare const ONLOOK_CONTAINED_EVENT = "onlook/contained";
|
|
26
|
+
/**
|
|
27
|
+
* Emitted once per story mount, after the boundary's children committed
|
|
28
|
+
* without throwing. The verifier must key `rendered` on THIS (plus absence of
|
|
29
|
+
* Storybook error events) — never on `STORY_RENDERED` alone, which Storybook
|
|
30
|
+
* fires even after its own boundary catches.
|
|
31
|
+
*/
|
|
32
|
+
declare const ONLOOK_RENDERED_EVENT = "onlook/rendered";
|
|
33
|
+
/**
|
|
34
|
+
* Tag-for-verifier re-emit of errors the boundary cannot catch (loader /
|
|
35
|
+
* `beforeEach` / module-load throws surface as Storybook's
|
|
36
|
+
* `storyThrewException` / `storyErrored` channel events, outside the React
|
|
37
|
+
* render). These remain overlay-not-card; no DOM repaint is attempted.
|
|
38
|
+
*/
|
|
39
|
+
declare const ONLOOK_UNCONTAINED_ERROR_EVENT = "onlook/uncontained-error";
|
|
40
|
+
type OnlookContainedPayload = {
|
|
41
|
+
storyId: string;
|
|
42
|
+
contractVersion: number;
|
|
43
|
+
/** Trimmed failure summary (error message). */
|
|
44
|
+
message: string;
|
|
45
|
+
/** Present when the failure is attributed to a contained module (U4). */
|
|
46
|
+
module?: ContainedModuleMarker;
|
|
47
|
+
};
|
|
48
|
+
type OnlookRenderedPayload = {
|
|
49
|
+
storyId: string;
|
|
50
|
+
contractVersion: number;
|
|
51
|
+
};
|
|
52
|
+
type OnlookUncontainedErrorPayload = {
|
|
53
|
+
/** Best-effort (read from the preview iframe URL); absent when unknown. */
|
|
54
|
+
storyId?: string;
|
|
55
|
+
contractVersion: number;
|
|
56
|
+
message: string;
|
|
57
|
+
/** Which Storybook channel event this re-emits. */
|
|
58
|
+
source: 'storyThrewException' | 'storyErrored';
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Render-state attribute, value `'rendered' | 'contained'`. Set on
|
|
62
|
+
* `#storybook-root` (falling back to `document.body` when absent) after each
|
|
63
|
+
* boundary commit, and ALSO carried on the card's own wrapper element when
|
|
64
|
+
* contained. Cleared on boundary unmount (story switch), so a stale value
|
|
65
|
+
* never leaks into the next story's mount — verifiers should match it
|
|
66
|
+
* together with `data-onlook-story-id`.
|
|
67
|
+
*/
|
|
68
|
+
declare const ONLOOK_RENDER_STATE_ATTR = "data-onlook-render-state";
|
|
69
|
+
/** Companion attribute carrying the story id the render state belongs to. */
|
|
70
|
+
declare const ONLOOK_RENDER_STORY_ATTR = "data-onlook-story-id";
|
|
71
|
+
/** Present (empty value) on the "needs setup" card's wrapper element. */
|
|
72
|
+
declare const ONLOOK_CONTAINED_CARD_ATTR = "data-onlook-contained-card";
|
|
73
|
+
type OnlookRenderState = 'rendered' | 'contained';
|
|
74
|
+
|
|
75
|
+
type ContainmentCardProps = {
|
|
76
|
+
storyId: string;
|
|
77
|
+
storyLabel: string;
|
|
78
|
+
message: string;
|
|
79
|
+
module?: ContainedModuleMarker;
|
|
80
|
+
};
|
|
81
|
+
declare function ContainmentCard(props: ContainmentCardProps): React.ReactElement;
|
|
82
|
+
type BoundaryProps = {
|
|
83
|
+
storyId: string;
|
|
84
|
+
storyLabel: string;
|
|
85
|
+
children?: React.ReactNode;
|
|
86
|
+
};
|
|
87
|
+
type BoundaryState = {
|
|
88
|
+
error: unknown;
|
|
89
|
+
hasError: boolean;
|
|
90
|
+
};
|
|
91
|
+
declare class OnlookContainmentBoundary extends React.Component<BoundaryProps, BoundaryState> {
|
|
92
|
+
private renderedSignalSent;
|
|
93
|
+
constructor(props: BoundaryProps);
|
|
94
|
+
static getDerivedStateFromError(error: unknown): BoundaryState;
|
|
95
|
+
componentDidCatch(error: unknown): void;
|
|
96
|
+
componentDidMount(): void;
|
|
97
|
+
componentDidUpdate(): void;
|
|
98
|
+
componentWillUnmount(): void;
|
|
99
|
+
private signalRenderedIfClean;
|
|
100
|
+
render(): React.ReactNode;
|
|
101
|
+
}
|
|
102
|
+
type StoryContextLike = {
|
|
103
|
+
id?: string;
|
|
104
|
+
title?: string;
|
|
105
|
+
name?: string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The project-level decorator delivered through the addon `./preview` entry.
|
|
109
|
+
* Keyed by story id so switching stories remounts the boundary (resetting
|
|
110
|
+
* caught-error state and re-arming the once-per-mount rendered signal).
|
|
111
|
+
*/
|
|
112
|
+
declare function onlookContainmentDecorator(Story: unknown, context?: StoryContextLike): React.ReactElement;
|
|
113
|
+
|
|
114
|
+
type ChannelLike = {
|
|
115
|
+
emit: (event: string, payload?: unknown) => void;
|
|
116
|
+
on?: (event: string, listener: (payload?: unknown) => void) => void;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The preview channel. Prefers `window.__STORYBOOK_ADDONS_CHANNEL__` (set by
|
|
120
|
+
* builder-vite's setup code before any addon preview annotation loads), then
|
|
121
|
+
* the global-backed preview addon store — but only when it already HAS a
|
|
122
|
+
* channel (calling `getChannel()` earlier would install a mock and clobber
|
|
123
|
+
* Storybook's own setup). `null` when Storybook hasn't wired one (e.g. tests,
|
|
124
|
+
* portable stories).
|
|
125
|
+
*/
|
|
126
|
+
declare function getPreviewChannel(): ChannelLike | null;
|
|
127
|
+
/**
|
|
128
|
+
* Snapshot tooling detection (Chromatic's documented `isChromatic()` check).
|
|
129
|
+
* The vite-plugin half of this package self-disables under `process.env.CI ||
|
|
130
|
+
* CHROMATIC`; the preview half can't see those env vars in the browser, so it
|
|
131
|
+
* keys on Chromatic's user agent / URL marker. When true we keep the boundary
|
|
132
|
+
* behavior (the card still renders deterministically) but skip the dynamic
|
|
133
|
+
* signals — channel events and root-element attribute writes — so snapshots
|
|
134
|
+
* stay byte-stable.
|
|
135
|
+
*/
|
|
136
|
+
declare function isSnapshotEnvironment(): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Attribute an error to a contained module (U4's env-containment stand-ins).
|
|
139
|
+
* Primary: the marker the stand-in attaches at `error.__onlookContained`.
|
|
140
|
+
* Fallback: scan the `globalThis.__ONLOOK_CONTAINED_MODULES__` registry for a
|
|
141
|
+
* marker whose modulePath appears in the error message (covers errors
|
|
142
|
+
* re-wrapped by customer code that drops custom properties).
|
|
143
|
+
*/
|
|
144
|
+
declare function attributeErrorToModule(error: unknown): ContainedModuleMarker | undefined;
|
|
145
|
+
declare function installUncontainedErrorRelay(channel?: ChannelLike | null): void;
|
|
146
|
+
/**
|
|
147
|
+
* The preview annotations' `beforeAll` — runs once when the preview boots,
|
|
148
|
+
* before any story (and before any loader can throw). Advertises the
|
|
149
|
+
* handshake global and installs the uncontained-error relay.
|
|
150
|
+
*/
|
|
151
|
+
declare function onlookBeforeAll(): void;
|
|
152
|
+
|
|
153
|
+
declare const decorators: (typeof onlookContainmentDecorator)[];
|
|
154
|
+
declare const beforeAll: typeof onlookBeforeAll;
|
|
155
|
+
|
|
156
|
+
export { ContainmentCard, ONLOOK_CONTAINED_CARD_ATTR, ONLOOK_CONTAINED_EVENT, ONLOOK_CONTAINMENT_CONTRACT_GLOBAL, ONLOOK_CONTAINMENT_CONTRACT_VERSION, ONLOOK_RENDERED_EVENT, ONLOOK_RENDER_STATE_ATTR, ONLOOK_RENDER_STORY_ATTR, ONLOOK_UNCONTAINED_ERROR_EVENT, type OnlookContainedPayload, OnlookContainmentBoundary, type OnlookContainmentContract, type OnlookRenderState, type OnlookRenderedPayload, type OnlookUncontainedErrorPayload, attributeErrorToModule, beforeAll, decorators, getPreviewChannel, installUncontainedErrorRelay, isSnapshotEnvironment, onlookBeforeAll, onlookContainmentDecorator };
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
// src/preview/containment-boundary.ts
|
|
4
|
+
|
|
5
|
+
// src/containment-contract/containment-contract.ts
|
|
6
|
+
var ONLOOK_CONTAINMENT_CONTRACT_VERSION = 1;
|
|
7
|
+
var ONLOOK_CONTAINED_MODULES_GLOBAL = "__ONLOOK_CONTAINED_MODULES__";
|
|
8
|
+
var ONLOOK_CONTAINED_PROP = "__onlookContained";
|
|
9
|
+
var ONLOOK_CONTAINMENT_CONTRACT_GLOBAL = "__ONLOOK_CONTAINMENT_CONTRACT__";
|
|
10
|
+
var ONLOOK_CONTAINED_EVENT = "onlook/contained";
|
|
11
|
+
var ONLOOK_RENDERED_EVENT = "onlook/rendered";
|
|
12
|
+
var ONLOOK_UNCONTAINED_ERROR_EVENT = "onlook/uncontained-error";
|
|
13
|
+
var ONLOOK_RENDER_STATE_ATTR = "data-onlook-render-state";
|
|
14
|
+
var ONLOOK_RENDER_STORY_ATTR = "data-onlook-story-id";
|
|
15
|
+
var ONLOOK_CONTAINED_CARD_ATTR = "data-onlook-contained-card";
|
|
16
|
+
|
|
17
|
+
// src/preview/signals.ts
|
|
18
|
+
var STORYBOOK_STORY_THREW_EXCEPTION = "storyThrewException";
|
|
19
|
+
var STORYBOOK_STORY_ERRORED = "storyErrored";
|
|
20
|
+
function globalScope() {
|
|
21
|
+
return globalThis;
|
|
22
|
+
}
|
|
23
|
+
function getPreviewChannel() {
|
|
24
|
+
const g = globalScope();
|
|
25
|
+
const direct = g.__STORYBOOK_ADDONS_CHANNEL__;
|
|
26
|
+
if (direct && typeof direct.emit === "function") return direct;
|
|
27
|
+
const store = g.__STORYBOOK_ADDONS_PREVIEW;
|
|
28
|
+
if (store && typeof store.hasChannel === "function" && store.hasChannel() && typeof store.getChannel === "function") {
|
|
29
|
+
return store.getChannel();
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function isSnapshotEnvironment() {
|
|
34
|
+
try {
|
|
35
|
+
const w = globalScope().window;
|
|
36
|
+
if (!w) return false;
|
|
37
|
+
if (/Chromatic/.test(w.navigator?.userAgent ?? "")) return true;
|
|
38
|
+
if (/chromatic=true/.test(w.location?.href ?? "")) return true;
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function exposeContract() {
|
|
44
|
+
globalScope()[ONLOOK_CONTAINMENT_CONTRACT_GLOBAL] = {
|
|
45
|
+
version: ONLOOK_CONTAINMENT_CONTRACT_VERSION
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function signalElement() {
|
|
49
|
+
if (typeof document === "undefined") return null;
|
|
50
|
+
return document.getElementById("storybook-root") ?? document.body ?? null;
|
|
51
|
+
}
|
|
52
|
+
function setRenderState(state, storyId) {
|
|
53
|
+
if (isSnapshotEnvironment()) return;
|
|
54
|
+
try {
|
|
55
|
+
const el = signalElement();
|
|
56
|
+
if (!el) return;
|
|
57
|
+
el.setAttribute(ONLOOK_RENDER_STATE_ATTR, state);
|
|
58
|
+
el.setAttribute(ONLOOK_RENDER_STORY_ATTR, storyId);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function clearRenderState(storyId) {
|
|
63
|
+
try {
|
|
64
|
+
const el = signalElement();
|
|
65
|
+
if (!el) return;
|
|
66
|
+
if (el.getAttribute(ONLOOK_RENDER_STORY_ATTR) !== storyId) return;
|
|
67
|
+
el.removeAttribute(ONLOOK_RENDER_STATE_ATTR);
|
|
68
|
+
el.removeAttribute(ONLOOK_RENDER_STORY_ATTR);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function emit(event, payload) {
|
|
73
|
+
if (isSnapshotEnvironment()) return;
|
|
74
|
+
try {
|
|
75
|
+
getPreviewChannel()?.emit(event, payload);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function emitContained(payload) {
|
|
80
|
+
emit(ONLOOK_CONTAINED_EVENT, payload);
|
|
81
|
+
}
|
|
82
|
+
function emitRendered(storyId) {
|
|
83
|
+
emit(ONLOOK_RENDERED_EVENT, {
|
|
84
|
+
storyId,
|
|
85
|
+
contractVersion: ONLOOK_CONTAINMENT_CONTRACT_VERSION
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function summarizeError(error, maxLength = 400) {
|
|
89
|
+
const raw = error instanceof Error ? error.message : typeof error === "string" ? error : String(error);
|
|
90
|
+
const oneline = raw.trim();
|
|
91
|
+
return oneline.length > maxLength ? `${oneline.slice(0, maxLength - 1)}\u2026` : oneline;
|
|
92
|
+
}
|
|
93
|
+
function attributeErrorToModule(error) {
|
|
94
|
+
if (error && typeof error === "object") {
|
|
95
|
+
const direct = error[ONLOOK_CONTAINED_PROP];
|
|
96
|
+
if (isMarker(direct)) return direct;
|
|
97
|
+
}
|
|
98
|
+
const registry = globalScope()[ONLOOK_CONTAINED_MODULES_GLOBAL];
|
|
99
|
+
if (!registry) return void 0;
|
|
100
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
101
|
+
if (!message) return void 0;
|
|
102
|
+
for (const candidate of Object.values(registry)) {
|
|
103
|
+
if (isMarker(candidate) && message.includes(candidate.modulePath)) {
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
function isMarker(value) {
|
|
110
|
+
return value !== null && typeof value === "object" && typeof value.modulePath === "string" && value.reason === "env-validation";
|
|
111
|
+
}
|
|
112
|
+
var INSTALLED_CHANNELS = /* @__PURE__ */ new WeakSet();
|
|
113
|
+
function currentStoryId() {
|
|
114
|
+
try {
|
|
115
|
+
const w = globalScope().window;
|
|
116
|
+
const search = w?.location?.search;
|
|
117
|
+
if (!search) return void 0;
|
|
118
|
+
return new URLSearchParams(search).get("id") ?? void 0;
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function relayPayload(source, raw) {
|
|
124
|
+
const obj = raw ?? {};
|
|
125
|
+
const message = typeof obj.message === "string" ? obj.message : typeof obj.description === "string" ? obj.description : typeof obj.title === "string" ? obj.title : summarizeError(raw);
|
|
126
|
+
const storyId = currentStoryId();
|
|
127
|
+
return {
|
|
128
|
+
...storyId ? { storyId } : {},
|
|
129
|
+
contractVersion: ONLOOK_CONTAINMENT_CONTRACT_VERSION,
|
|
130
|
+
message: summarizeError(message),
|
|
131
|
+
source
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function installUncontainedErrorRelay(channel = getPreviewChannel()) {
|
|
135
|
+
if (!channel || typeof channel.on !== "function") return;
|
|
136
|
+
if (INSTALLED_CHANNELS.has(channel)) return;
|
|
137
|
+
INSTALLED_CHANNELS.add(channel);
|
|
138
|
+
channel.on(STORYBOOK_STORY_THREW_EXCEPTION, (payload) => {
|
|
139
|
+
emit(ONLOOK_UNCONTAINED_ERROR_EVENT, relayPayload("storyThrewException", payload));
|
|
140
|
+
});
|
|
141
|
+
channel.on(STORYBOOK_STORY_ERRORED, (payload) => {
|
|
142
|
+
emit(ONLOOK_UNCONTAINED_ERROR_EVENT, relayPayload("storyErrored", payload));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function onlookBeforeAll() {
|
|
146
|
+
exposeContract();
|
|
147
|
+
installUncontainedErrorRelay();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/preview/containment-boundary.ts
|
|
151
|
+
var cardStyles = {
|
|
152
|
+
wrapper: {
|
|
153
|
+
boxSizing: "border-box",
|
|
154
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
155
|
+
color: "#1f2937",
|
|
156
|
+
background: "#fafaf9",
|
|
157
|
+
border: "1px solid #e7e5e4",
|
|
158
|
+
borderRadius: "12px",
|
|
159
|
+
padding: "20px 24px",
|
|
160
|
+
margin: "16px",
|
|
161
|
+
maxWidth: "560px",
|
|
162
|
+
lineHeight: 1.5
|
|
163
|
+
},
|
|
164
|
+
badge: {
|
|
165
|
+
display: "inline-block",
|
|
166
|
+
fontSize: "11px",
|
|
167
|
+
fontWeight: 600,
|
|
168
|
+
letterSpacing: "0.04em",
|
|
169
|
+
textTransform: "uppercase",
|
|
170
|
+
color: "#92400e",
|
|
171
|
+
background: "#fef3c7",
|
|
172
|
+
borderRadius: "999px",
|
|
173
|
+
padding: "2px 10px",
|
|
174
|
+
marginBottom: "10px"
|
|
175
|
+
},
|
|
176
|
+
title: {
|
|
177
|
+
fontSize: "15px",
|
|
178
|
+
fontWeight: 600,
|
|
179
|
+
margin: "0 0 6px"
|
|
180
|
+
},
|
|
181
|
+
detail: {
|
|
182
|
+
fontSize: "13px",
|
|
183
|
+
margin: "0 0 10px",
|
|
184
|
+
color: "#44403c"
|
|
185
|
+
},
|
|
186
|
+
message: {
|
|
187
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
188
|
+
fontSize: "12px",
|
|
189
|
+
whiteSpace: "pre-wrap",
|
|
190
|
+
wordBreak: "break-word",
|
|
191
|
+
color: "#7f1d1d",
|
|
192
|
+
background: "#fef2f2",
|
|
193
|
+
border: "1px solid #fee2e2",
|
|
194
|
+
borderRadius: "8px",
|
|
195
|
+
padding: "10px 12px",
|
|
196
|
+
margin: "0 0 10px"
|
|
197
|
+
},
|
|
198
|
+
footer: {
|
|
199
|
+
fontSize: "12px",
|
|
200
|
+
margin: 0,
|
|
201
|
+
color: "#78716c"
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var h = React.createElement;
|
|
205
|
+
function ContainmentCard(props) {
|
|
206
|
+
const { storyId, storyLabel, message, module } = props;
|
|
207
|
+
const moduleDetail = module ? h(
|
|
208
|
+
"p",
|
|
209
|
+
{ style: cardStyles.detail, key: "module" },
|
|
210
|
+
`This story imports `,
|
|
211
|
+
h("code", null, module.modulePath),
|
|
212
|
+
module.envKeys.length > 0 ? `, which validates environment variables at load time. Missing: ${module.envKeys.join(", ")}.` : ", which could not load in this environment."
|
|
213
|
+
) : null;
|
|
214
|
+
return h(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
[ONLOOK_CONTAINED_CARD_ATTR]: "",
|
|
218
|
+
[ONLOOK_RENDER_STATE_ATTR]: "contained",
|
|
219
|
+
[ONLOOK_RENDER_STORY_ATTR]: storyId,
|
|
220
|
+
role: "alert",
|
|
221
|
+
style: cardStyles.wrapper
|
|
222
|
+
},
|
|
223
|
+
h("span", { style: cardStyles.badge, key: "badge" }, "Needs setup"),
|
|
224
|
+
h("h2", { style: cardStyles.title, key: "title" }, `${storyLabel} can't render yet`),
|
|
225
|
+
moduleDetail,
|
|
226
|
+
message ? h("pre", { style: cardStyles.message, key: "message" }, message) : null,
|
|
227
|
+
h(
|
|
228
|
+
"p",
|
|
229
|
+
{ style: cardStyles.footer, key: "footer" },
|
|
230
|
+
"Onlook contained this error so the rest of Storybook keeps working."
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
var OnlookContainmentBoundary = class extends React.Component {
|
|
235
|
+
// `onlook/rendered` fires once per story mount — re-renders of a healthy
|
|
236
|
+
// story (arg updates) don't re-emit.
|
|
237
|
+
renderedSignalSent = false;
|
|
238
|
+
constructor(props) {
|
|
239
|
+
super(props);
|
|
240
|
+
this.state = { error: null, hasError: false };
|
|
241
|
+
}
|
|
242
|
+
static getDerivedStateFromError(error) {
|
|
243
|
+
return { error, hasError: true };
|
|
244
|
+
}
|
|
245
|
+
componentDidCatch(error) {
|
|
246
|
+
const module = attributeErrorToModule(error);
|
|
247
|
+
setRenderState("contained", this.props.storyId);
|
|
248
|
+
emitContained({
|
|
249
|
+
storyId: this.props.storyId,
|
|
250
|
+
contractVersion: ONLOOK_CONTAINMENT_CONTRACT_VERSION,
|
|
251
|
+
message: summarizeError(error),
|
|
252
|
+
...module ? { module } : {}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
componentDidMount() {
|
|
256
|
+
exposeContract();
|
|
257
|
+
this.signalRenderedIfClean();
|
|
258
|
+
}
|
|
259
|
+
componentDidUpdate() {
|
|
260
|
+
this.signalRenderedIfClean();
|
|
261
|
+
}
|
|
262
|
+
componentWillUnmount() {
|
|
263
|
+
clearRenderState(this.props.storyId);
|
|
264
|
+
}
|
|
265
|
+
signalRenderedIfClean() {
|
|
266
|
+
if (this.state.hasError || this.renderedSignalSent) return;
|
|
267
|
+
this.renderedSignalSent = true;
|
|
268
|
+
setRenderState("rendered", this.props.storyId);
|
|
269
|
+
emitRendered(this.props.storyId);
|
|
270
|
+
}
|
|
271
|
+
render() {
|
|
272
|
+
if (this.state.hasError) {
|
|
273
|
+
return h(ContainmentCard, {
|
|
274
|
+
storyId: this.props.storyId,
|
|
275
|
+
storyLabel: this.props.storyLabel,
|
|
276
|
+
message: summarizeError(this.state.error),
|
|
277
|
+
module: attributeErrorToModule(this.state.error)
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return this.props.children;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function storyLabelFrom(context) {
|
|
284
|
+
const title = context.title?.split("/").pop();
|
|
285
|
+
if (title && context.name) return `${title} \xB7 ${context.name}`;
|
|
286
|
+
return title ?? context.name ?? context.id ?? "This story";
|
|
287
|
+
}
|
|
288
|
+
function onlookContainmentDecorator(Story, context) {
|
|
289
|
+
const ctx = context ?? {};
|
|
290
|
+
const storyId = ctx.id ?? "unknown";
|
|
291
|
+
return h(
|
|
292
|
+
OnlookContainmentBoundary,
|
|
293
|
+
{ key: storyId, storyId, storyLabel: storyLabelFrom(ctx) },
|
|
294
|
+
h(Story)
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/preview/index.ts
|
|
299
|
+
var decorators = [onlookContainmentDecorator];
|
|
300
|
+
var beforeAll = onlookBeforeAll;
|
|
301
|
+
|
|
302
|
+
export { ContainmentCard, ONLOOK_CONTAINED_CARD_ATTR, ONLOOK_CONTAINED_EVENT, ONLOOK_CONTAINMENT_CONTRACT_GLOBAL, ONLOOK_CONTAINMENT_CONTRACT_VERSION, ONLOOK_RENDERED_EVENT, ONLOOK_RENDER_STATE_ATTR, ONLOOK_RENDER_STORY_ATTR, ONLOOK_UNCONTAINED_ERROR_EVENT, OnlookContainmentBoundary, attributeErrorToModule, beforeAll, decorators, getPreviewChannel, installUncontainedErrorRelay, isSnapshotEnvironment, onlookBeforeAll, onlookContainmentDecorator };
|
|
@@ -2,6 +2,9 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
import { createRequire as createRequire$1 } from 'module';
|
|
7
|
+
import { promisify } from 'util';
|
|
5
8
|
import { chromium } from 'playwright';
|
|
6
9
|
|
|
7
10
|
globalThis.require = createRequire(import.meta.url);
|
|
@@ -54,24 +57,84 @@ function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
|
|
|
54
57
|
};
|
|
55
58
|
saveManifest(manifest);
|
|
56
59
|
}
|
|
57
|
-
var
|
|
60
|
+
var execFileAsync = promisify(execFile);
|
|
61
|
+
var require2 = createRequire$1(import.meta.url);
|
|
62
|
+
var browserPromise = null;
|
|
63
|
+
var installPromise = null;
|
|
64
|
+
var isMissingExecutableError = (err) => {
|
|
65
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
66
|
+
return msg.includes("Executable doesn't exist") || msg.includes("Please run the following command to download new browsers") || // Older playwright revisions phrase the version-mismatch case differently.
|
|
67
|
+
/chromium-\d+\/chrome-linux\/chrome/i.test(msg);
|
|
68
|
+
};
|
|
69
|
+
async function installChromium() {
|
|
70
|
+
if (!installPromise) {
|
|
71
|
+
installPromise = (async () => {
|
|
72
|
+
const cliPath = require2.resolve("playwright-core/cli.js");
|
|
73
|
+
console.log(
|
|
74
|
+
`[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
|
|
75
|
+
);
|
|
76
|
+
try {
|
|
77
|
+
const { stdout, stderr } = await execFileAsync(
|
|
78
|
+
process.execPath,
|
|
79
|
+
[cliPath, "install", "--force", "chromium"],
|
|
80
|
+
{ timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
|
|
81
|
+
);
|
|
82
|
+
if (stdout)
|
|
83
|
+
console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
|
|
84
|
+
if (stderr)
|
|
85
|
+
console.log(
|
|
86
|
+
`[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
|
|
87
|
+
);
|
|
88
|
+
console.log("[STORYBOOK_PLUGIN] chromium installed");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
91
|
+
console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
|
|
92
|
+
throw new Error(`Playwright chromium install failed: ${detail}`);
|
|
93
|
+
} finally {
|
|
94
|
+
installPromise = null;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
}
|
|
98
|
+
return installPromise;
|
|
99
|
+
}
|
|
100
|
+
async function launchWithSelfHeal() {
|
|
101
|
+
try {
|
|
102
|
+
return await chromium.launch({ headless: true });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (!isMissingExecutableError(err)) throw err;
|
|
105
|
+
console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
|
|
106
|
+
await installChromium();
|
|
107
|
+
try {
|
|
108
|
+
return await chromium.launch({ headless: true });
|
|
109
|
+
} catch (postInstallErr) {
|
|
110
|
+
const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
|
|
111
|
+
console.error(
|
|
112
|
+
`[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
|
|
113
|
+
);
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
58
120
|
async function getBrowser() {
|
|
59
|
-
if (!
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
if (!browserPromise) {
|
|
122
|
+
browserPromise = launchWithSelfHeal().catch((err) => {
|
|
123
|
+
browserPromise = null;
|
|
124
|
+
throw err;
|
|
62
125
|
});
|
|
63
126
|
}
|
|
64
|
-
return
|
|
127
|
+
return browserPromise;
|
|
65
128
|
}
|
|
66
129
|
async function closeBrowser() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
130
|
+
const pending = browserPromise;
|
|
131
|
+
if (!pending) return;
|
|
132
|
+
browserPromise = null;
|
|
133
|
+
try {
|
|
134
|
+
const b = await pending;
|
|
135
|
+
await b.close();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("Error closing browser:", error);
|
|
75
138
|
}
|
|
76
139
|
}
|
|
77
140
|
function getScreenshotPath(storyId, theme) {
|
|
@@ -83,8 +146,8 @@ function screenshotExists(storyId, theme) {
|
|
|
83
146
|
return fs.existsSync(screenshotPath);
|
|
84
147
|
}
|
|
85
148
|
async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
86
|
-
const
|
|
87
|
-
const context = await
|
|
149
|
+
const browser = await getBrowser();
|
|
150
|
+
const context = await browser.newContext({
|
|
88
151
|
viewport: { width, height },
|
|
89
152
|
deviceScaleFactor: 2
|
|
90
153
|
});
|
|
@@ -189,6 +252,34 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
|
|
|
189
252
|
}
|
|
190
253
|
|
|
191
254
|
// src/screenshot-service/screenshot-service.ts
|
|
255
|
+
var ONBOOK_PROXY_TOKEN = process.env.ONBOOK_PROXY_TOKEN ?? null;
|
|
256
|
+
var ONBOOK_BROADCAST_URL = process.env.ONBOOK_BROADCAST_URL ?? null;
|
|
257
|
+
var PROGRESS_TIMEOUT_MS = 3e3;
|
|
258
|
+
async function broadcastScreenshotProgress(completed, total) {
|
|
259
|
+
if (!ONBOOK_PROXY_TOKEN || !ONBOOK_BROADCAST_URL) return;
|
|
260
|
+
const percent = total > 0 ? Math.min(100, Math.floor(completed / total * 100)) : null;
|
|
261
|
+
try {
|
|
262
|
+
await fetch(ONBOOK_BROADCAST_URL, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
Authorization: `Bearer ${ONBOOK_PROXY_TOKEN}`,
|
|
266
|
+
"Content-Type": "application/json"
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
event: {
|
|
270
|
+
type: "SANDBOX_PHASE_PROGRESS",
|
|
271
|
+
phase: "generating_screenshots",
|
|
272
|
+
completed,
|
|
273
|
+
total,
|
|
274
|
+
percent,
|
|
275
|
+
label: "Capturing stories"
|
|
276
|
+
}
|
|
277
|
+
}),
|
|
278
|
+
signal: AbortSignal.timeout(PROGRESS_TIMEOUT_MS)
|
|
279
|
+
});
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
192
283
|
var StoryTimeoutError = class extends Error {
|
|
193
284
|
constructor(storyId, timeoutMs) {
|
|
194
285
|
super(`Story ${storyId} timed out after ${timeoutMs / 1e3}s`);
|
|
@@ -228,6 +319,7 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
|
|
|
228
319
|
}
|
|
229
320
|
const storyTimeout = timeoutMs * 2 + 1e4;
|
|
230
321
|
let completed = 0;
|
|
322
|
+
void broadcastScreenshotProgress(0, displayTotal);
|
|
231
323
|
for (const batch of batches) {
|
|
232
324
|
await Promise.all(
|
|
233
325
|
batch.map(async (story) => {
|
|
@@ -297,8 +389,10 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
|
|
|
297
389
|
}
|
|
298
390
|
})
|
|
299
391
|
);
|
|
392
|
+
void broadcastScreenshotProgress(offset + completed, displayTotal);
|
|
300
393
|
}
|
|
301
394
|
await closeBrowser();
|
|
395
|
+
void broadcastScreenshotProgress(displayTotal, displayTotal);
|
|
302
396
|
console.log("Screenshot generation complete!");
|
|
303
397
|
}
|
|
304
398
|
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { Browser } from 'playwright';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Initialize browser instance
|
|
5
|
-
*/
|
|
6
3
|
declare function getBrowser(): Promise<Browser>;
|
|
7
|
-
/**
|
|
8
|
-
* Close browser instance
|
|
9
|
-
*/
|
|
10
4
|
declare function closeBrowser(): Promise<void>;
|
|
11
5
|
|
|
12
6
|
export { closeBrowser, getBrowser };
|