@coder/pixel-storybook 0.1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/api.d.ts +56 -0
- package/dist/api.js +238 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +102 -0
- package/dist/checkDifferences.d.ts +63 -0
- package/dist/checkDifferences.js +67 -0
- package/dist/compare/compare.d.ts +5 -0
- package/dist/compare/compare.js +80 -0
- package/dist/compare/pixelmatch.d.ts +7 -0
- package/dist/compare/pixelmatch.js +46 -0
- package/dist/compare/utils.d.ts +2 -0
- package/dist/compare/utils.js +18 -0
- package/dist/compare/worker.d.ts +1 -0
- package/dist/compare/worker.js +6 -0
- package/dist/concurrency.d.ts +19 -0
- package/dist/concurrency.js +61 -0
- package/dist/config.d.ts +3795 -0
- package/dist/config.js +472 -0
- package/dist/configHelper.d.ts +2 -0
- package/dist/configHelper.js +34 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/crawler/storybook.d.ts +51 -0
- package/dist/crawler/storybook.js +317 -0
- package/dist/crawler/utils.d.ts +6 -0
- package/dist/crawler/utils.js +20 -0
- package/dist/createShots.d.ts +30 -0
- package/dist/createShots.js +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +26 -0
- package/dist/log.js +99 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +186 -0
- package/dist/schemas.d.ts +174 -0
- package/dist/schemas.js +73 -0
- package/dist/shard.d.ts +3 -0
- package/dist/shard.js +17 -0
- package/dist/shots/shots.d.ts +3 -0
- package/dist/shots/shots.js +196 -0
- package/dist/shots/utils.d.ts +33 -0
- package/dist/shots/utils.js +177 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/dist/upload.d.ts +10 -0
- package/dist/upload.js +32 -0
- package/dist/uploadStorybook.d.ts +2 -0
- package/dist/uploadStorybook.js +56 -0
- package/dist/utils.d.ts +50 -0
- package/dist/utils.js +194 -0
- package/package.json +64 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { config, isPlatformModeConfig } from '../config.js';
|
|
4
|
+
import { notSupported } from '../constants.js';
|
|
5
|
+
import { log } from '../log.js';
|
|
6
|
+
import { generateLabel, selectBreakpoints } from '../shots/utils.js';
|
|
7
|
+
import { launchBrowser } from '../utils.js';
|
|
8
|
+
export const getStoryBookUrl = (url) => {
|
|
9
|
+
if (url.startsWith('http://') ||
|
|
10
|
+
url.startsWith('https://') ||
|
|
11
|
+
url.startsWith('file://')) {
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
if (url.startsWith('/')) {
|
|
15
|
+
return `file://${url}`;
|
|
16
|
+
}
|
|
17
|
+
return `file://${path.normalize(path.join(process.cwd(), url))}`;
|
|
18
|
+
};
|
|
19
|
+
export const getIframeUrl = (url) => url.endsWith('/') ? `${url}iframe.html` : `${url}/iframe.html`;
|
|
20
|
+
export const collectStoriesViaWindowApi = async (context, url, isIframeUrl) => {
|
|
21
|
+
const page = await context.newPage();
|
|
22
|
+
const iframeUrl = isIframeUrl
|
|
23
|
+
? getStoryBookUrl(url)
|
|
24
|
+
: getIframeUrl(getStoryBookUrl(url));
|
|
25
|
+
await page.goto(iframeUrl);
|
|
26
|
+
await page.waitForFunction(() => window.__STORYBOOK_PREVIEW__, null, {
|
|
27
|
+
timeout: config.timeouts.fetchStories,
|
|
28
|
+
});
|
|
29
|
+
// Storybook >= 8 expose a new preview API that has a `ready` method to be awaited before proceeding
|
|
30
|
+
const isV8OrAbove = await page.evaluate(async () => {
|
|
31
|
+
const { __STORYBOOK_PREVIEW__: api } = window;
|
|
32
|
+
return api.ready !== undefined;
|
|
33
|
+
});
|
|
34
|
+
if (isV8OrAbove) {
|
|
35
|
+
// SB v8 and above
|
|
36
|
+
await page.evaluate(async () => {
|
|
37
|
+
const { __STORYBOOK_PREVIEW__: api } = window;
|
|
38
|
+
if (api.ready) {
|
|
39
|
+
await api.ready();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// SB v7 and below
|
|
45
|
+
await page.waitForFunction(() => window.__STORYBOOK_CLIENT_API__, null, {
|
|
46
|
+
timeout: config.timeouts.fetchStories,
|
|
47
|
+
});
|
|
48
|
+
await page.evaluate(async () => {
|
|
49
|
+
const { __STORYBOOK_CLIENT_API__: api } = window;
|
|
50
|
+
if (api.storyStore) {
|
|
51
|
+
await api.storyStore.cacheAllCSFFiles?.();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const result = await page.evaluate(async () => {
|
|
56
|
+
const parseParameters = (parameters, level = 0) => {
|
|
57
|
+
if (level > 10) {
|
|
58
|
+
return 'UNSUPPORTED_DEPTH';
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(parameters)) {
|
|
61
|
+
// @ts-expect-error FIXME
|
|
62
|
+
return parameters.map((value) => parseParameters(value, level + 1));
|
|
63
|
+
}
|
|
64
|
+
if (typeof parameters === 'string' ||
|
|
65
|
+
typeof parameters === 'number' ||
|
|
66
|
+
typeof parameters === 'boolean' ||
|
|
67
|
+
parameters === undefined ||
|
|
68
|
+
typeof parameters === 'function' ||
|
|
69
|
+
parameters instanceof RegExp ||
|
|
70
|
+
parameters instanceof Date ||
|
|
71
|
+
parameters === null) {
|
|
72
|
+
return parameters;
|
|
73
|
+
}
|
|
74
|
+
if (typeof parameters === 'object' && parameters !== null) {
|
|
75
|
+
// @ts-expect-error FIXME
|
|
76
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
77
|
+
return Object.keys(parameters).reduce((acc, key) => {
|
|
78
|
+
// @ts-expect-error FIXME
|
|
79
|
+
acc[key] = parseParameters(parameters[key], level + 1);
|
|
80
|
+
return acc;
|
|
81
|
+
}, {});
|
|
82
|
+
}
|
|
83
|
+
return 'UNSUPPORTED_TYPE';
|
|
84
|
+
};
|
|
85
|
+
const mapStories = (stories) => stories.map((story) => {
|
|
86
|
+
const parameters = parseParameters(story.parameters);
|
|
87
|
+
return {
|
|
88
|
+
id: story.id,
|
|
89
|
+
kind: story.kind,
|
|
90
|
+
story: story.story,
|
|
91
|
+
importPath: parameters?.fileName,
|
|
92
|
+
parameters,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
const { __STORYBOOK_PREVIEW__: previewApi, __STORYBOOK_CLIENT_API__: clientApi, } = window;
|
|
96
|
+
let stories = [];
|
|
97
|
+
if (previewApi.extract) {
|
|
98
|
+
const items = await previewApi.extract();
|
|
99
|
+
stories = mapStories(Object.values(items));
|
|
100
|
+
}
|
|
101
|
+
else if (clientApi.raw) {
|
|
102
|
+
// Fallback for 6.4 and below
|
|
103
|
+
stories = mapStories(clientApi.raw());
|
|
104
|
+
}
|
|
105
|
+
return { stories };
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
export const collectStoriesViaStoriesJson = async (context, url) => {
|
|
110
|
+
const storiesJsonUrl = url.endsWith('/')
|
|
111
|
+
? `${url}stories.json`
|
|
112
|
+
: `${url}/stories.json`;
|
|
113
|
+
let storiesJson;
|
|
114
|
+
if (storiesJsonUrl.startsWith('file://')) {
|
|
115
|
+
try {
|
|
116
|
+
const file = readFileSync(storiesJsonUrl.slice(7));
|
|
117
|
+
storiesJson = JSON.parse(file.toString());
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error(`Cannot load file ${storiesJsonUrl}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const result = await context.request.get(storiesJsonUrl);
|
|
125
|
+
storiesJson = (await result.json());
|
|
126
|
+
}
|
|
127
|
+
if (typeof storiesJson.stories === 'object') {
|
|
128
|
+
return {
|
|
129
|
+
stories: Object.values(storiesJson.stories),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`Cannot load resource ${storiesJsonUrl}`);
|
|
133
|
+
};
|
|
134
|
+
export const collectStories = async (url) => {
|
|
135
|
+
const browser = await launchBrowser();
|
|
136
|
+
const context = await browser.newContext();
|
|
137
|
+
try {
|
|
138
|
+
log.process('info', 'general', 'Trying to collect stories via window object');
|
|
139
|
+
const result = await collectStoriesViaWindowApi(context, url);
|
|
140
|
+
await browser.close();
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
log.process('info', 'general', 'Fallback to /stories.json');
|
|
145
|
+
log.process('error', 'general', error);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const result = await collectStoriesViaStoriesJson(context, url);
|
|
149
|
+
await browser.close();
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
await browser.close();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const generateBrowserConfig = (story) => {
|
|
158
|
+
const browserConfig = config.configureBrowser?.({
|
|
159
|
+
...story,
|
|
160
|
+
shotMode: 'storybook',
|
|
161
|
+
});
|
|
162
|
+
if (story.parameters?.viewport && browserConfig) {
|
|
163
|
+
browserConfig.viewport ??= {
|
|
164
|
+
width: 1280,
|
|
165
|
+
height: 720,
|
|
166
|
+
};
|
|
167
|
+
browserConfig.viewport = {
|
|
168
|
+
...browserConfig.viewport,
|
|
169
|
+
...story.parameters.viewport,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return browserConfig;
|
|
173
|
+
};
|
|
174
|
+
const generateStoryUrl = (iframeUrl, storyId, args, breakpoint) => {
|
|
175
|
+
let url = `${iframeUrl}?id=${storyId}&viewMode=story`;
|
|
176
|
+
if (args) {
|
|
177
|
+
const argsString = Object.entries(args)
|
|
178
|
+
.map(([key, value]) => `${key}:${value}`)
|
|
179
|
+
.join(';');
|
|
180
|
+
url += `&args=${argsString}`;
|
|
181
|
+
}
|
|
182
|
+
if (breakpoint !== undefined) {
|
|
183
|
+
url += `&width=${breakpoint}`;
|
|
184
|
+
}
|
|
185
|
+
return url;
|
|
186
|
+
};
|
|
187
|
+
const kebabCase = (str) => (str ?? '')
|
|
188
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
189
|
+
.replace(/[\s_]+/g, '-')
|
|
190
|
+
.toLowerCase();
|
|
191
|
+
const generateFilename = (kind, story, prefix, suffix) => {
|
|
192
|
+
return [prefix, kebabCase(kind), kebabCase(story), kebabCase(suffix)]
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.join('--');
|
|
195
|
+
};
|
|
196
|
+
export const generateStorybookShotItems = (baseUrl, stories, mask, modeBreakpoints, browser) => {
|
|
197
|
+
const iframeUrl = getIframeUrl(getStoryBookUrl(baseUrl));
|
|
198
|
+
return stories
|
|
199
|
+
.filter((story) => story.parameters?.lostpixel?.disable !== true)
|
|
200
|
+
.filter((story) => story.parameters?.storyshots?.disable !== true)
|
|
201
|
+
.filter((story) => config.filterShot
|
|
202
|
+
? config.filterShot({ ...story, shotMode: 'storybook' })
|
|
203
|
+
: true)
|
|
204
|
+
.flatMap((story) => {
|
|
205
|
+
const shotName = config.shotNameGenerator?.({ ...story, shotMode: 'storybook' }) ??
|
|
206
|
+
generateFilename(story.kind, story.story);
|
|
207
|
+
let label = generateLabel({ browser });
|
|
208
|
+
let fileNameWithExt = `${shotName}${label}.png`;
|
|
209
|
+
const baseShotItem = {
|
|
210
|
+
shotMode: 'storybook',
|
|
211
|
+
id: `${story.id}${label}`,
|
|
212
|
+
shotName: `${shotName}${label}`,
|
|
213
|
+
importPath: story.importPath,
|
|
214
|
+
url: generateStoryUrl(iframeUrl, story.id, story.parameters?.lostpixel?.args),
|
|
215
|
+
filePathBaseline: isPlatformModeConfig(config)
|
|
216
|
+
? notSupported
|
|
217
|
+
: path.join(config.imagePathBaseline, fileNameWithExt),
|
|
218
|
+
filePathCurrent: path.join(config.imagePathCurrent, fileNameWithExt),
|
|
219
|
+
filePathDifference: isPlatformModeConfig(config)
|
|
220
|
+
? notSupported
|
|
221
|
+
: path.join(config.imagePathDifference, fileNameWithExt),
|
|
222
|
+
browserConfig: generateBrowserConfig(story),
|
|
223
|
+
threshold: story.parameters?.lostpixel?.threshold ?? config.threshold,
|
|
224
|
+
waitBeforeScreenshot: story.parameters?.lostpixel?.waitBeforeScreenshot ??
|
|
225
|
+
config.waitBeforeScreenshot,
|
|
226
|
+
stabilizeBeforeScreenshot: story.parameters?.lostpixel?.stabilizeBeforeScreenshot ??
|
|
227
|
+
config.stabilizeBeforeScreenshot,
|
|
228
|
+
mask: [...(mask ?? []), ...(story.parameters?.lostpixel?.mask ?? [])],
|
|
229
|
+
elementLocator: story.parameters?.lostpixel?.elementLocator ??
|
|
230
|
+
config?.storybookShots?.elementLocator ??
|
|
231
|
+
'',
|
|
232
|
+
waitForSelector: config?.storybookShots?.waitForSelector,
|
|
233
|
+
};
|
|
234
|
+
const storyLevelBreakpoints = story.parameters?.lostpixel?.breakpoints ?? [];
|
|
235
|
+
const breakpoints = selectBreakpoints(config.breakpoints, modeBreakpoints, storyLevelBreakpoints);
|
|
236
|
+
let shotItems = [];
|
|
237
|
+
if (!breakpoints || breakpoints.length === 0) {
|
|
238
|
+
shotItems = [baseShotItem];
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
shotItems = breakpoints.map((breakpoint) => {
|
|
242
|
+
label = generateLabel({ breakpoint, browser });
|
|
243
|
+
fileNameWithExt = `${shotName}${label}.png`;
|
|
244
|
+
return {
|
|
245
|
+
...baseShotItem,
|
|
246
|
+
id: `${story.id}${label}`,
|
|
247
|
+
shotName: `${shotName}${label}`,
|
|
248
|
+
breakpoint,
|
|
249
|
+
breakpointGroup: story.id,
|
|
250
|
+
filePathBaseline: isPlatformModeConfig(config)
|
|
251
|
+
? notSupported
|
|
252
|
+
: path.join(config.imagePathBaseline, fileNameWithExt),
|
|
253
|
+
filePathCurrent: path.join(config.imagePathCurrent, fileNameWithExt),
|
|
254
|
+
filePathDifference: isPlatformModeConfig(config)
|
|
255
|
+
? notSupported
|
|
256
|
+
: path.join(config.imagePathDifference, fileNameWithExt),
|
|
257
|
+
viewport: {
|
|
258
|
+
width: breakpoint,
|
|
259
|
+
height: undefined,
|
|
260
|
+
},
|
|
261
|
+
url: generateStoryUrl(iframeUrl, story.id, story.parameters?.lostpixel?.args, breakpoint),
|
|
262
|
+
browserConfig: generateBrowserConfig({
|
|
263
|
+
...story,
|
|
264
|
+
parameters: {
|
|
265
|
+
...story.parameters,
|
|
266
|
+
viewport: {
|
|
267
|
+
width: breakpoint,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const extraShots = story.parameters?.lostpixel?.extraShots?.flatMap((snapshot) => {
|
|
275
|
+
const combinedArgs = {
|
|
276
|
+
...story.parameters?.lostpixel?.args,
|
|
277
|
+
...snapshot.args,
|
|
278
|
+
};
|
|
279
|
+
const snapshotShotName = generateFilename(story.kind, story.story, snapshot.prefix, snapshot.suffix);
|
|
280
|
+
return (breakpoints?.length === 0 ? [undefined] : breakpoints).map((breakpoint) => {
|
|
281
|
+
label = generateLabel({ breakpoint, browser });
|
|
282
|
+
fileNameWithExt = `${snapshotShotName}${label}.png`;
|
|
283
|
+
return {
|
|
284
|
+
...baseShotItem,
|
|
285
|
+
id: `${story.id}${label}-${snapshot.name ?? 'snapshot'}`,
|
|
286
|
+
shotName: `${snapshotShotName}${label}`,
|
|
287
|
+
breakpoint,
|
|
288
|
+
breakpointGroup: story.id,
|
|
289
|
+
filePathBaseline: isPlatformModeConfig(config)
|
|
290
|
+
? notSupported
|
|
291
|
+
: path.join(config.imagePathBaseline, fileNameWithExt),
|
|
292
|
+
filePathCurrent: path.join(config.imagePathCurrent, fileNameWithExt),
|
|
293
|
+
filePathDifference: isPlatformModeConfig(config)
|
|
294
|
+
? notSupported
|
|
295
|
+
: path.join(config.imagePathDifference, fileNameWithExt),
|
|
296
|
+
url: generateStoryUrl(iframeUrl, story.id, combinedArgs, breakpoint),
|
|
297
|
+
viewport: breakpoint
|
|
298
|
+
? {
|
|
299
|
+
width: breakpoint,
|
|
300
|
+
height: undefined,
|
|
301
|
+
}
|
|
302
|
+
: undefined,
|
|
303
|
+
browserConfig: generateBrowserConfig({
|
|
304
|
+
...story,
|
|
305
|
+
parameters: {
|
|
306
|
+
...story.parameters,
|
|
307
|
+
viewport: {
|
|
308
|
+
width: breakpoint,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}) ?? [];
|
|
315
|
+
return [...shotItems, ...extraShots];
|
|
316
|
+
});
|
|
317
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { getPort } from 'get-port-please';
|
|
3
|
+
import handler from 'serve-handler';
|
|
4
|
+
export const launchStaticWebServer = async (basePath) => {
|
|
5
|
+
const port = await getPort({
|
|
6
|
+
random: true,
|
|
7
|
+
});
|
|
8
|
+
const server = http.createServer(async (request, response) => {
|
|
9
|
+
return handler(request, response, {
|
|
10
|
+
public: basePath.startsWith('file://') ? basePath.slice(7) : basePath,
|
|
11
|
+
cleanUrls: false,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
server.listen(port);
|
|
15
|
+
return {
|
|
16
|
+
server,
|
|
17
|
+
port,
|
|
18
|
+
url: `http://localhost:${port}`,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const createShots: () => Promise<{
|
|
2
|
+
shotMode: "storybook";
|
|
3
|
+
id: string;
|
|
4
|
+
shotName: string;
|
|
5
|
+
url: string;
|
|
6
|
+
filePathBaseline: string;
|
|
7
|
+
filePathCurrent: string;
|
|
8
|
+
filePathDifference: string;
|
|
9
|
+
threshold: number;
|
|
10
|
+
browserConfig?: import("playwright-core").BrowserContextOptions | undefined;
|
|
11
|
+
waitBeforeScreenshot?: number | undefined;
|
|
12
|
+
stabilizeBeforeScreenshot?: boolean | undefined;
|
|
13
|
+
stabilization?: {
|
|
14
|
+
status: "stable" | "fonts-not-ready" | "images-not-ready" | "unstable";
|
|
15
|
+
elapsedMs: number;
|
|
16
|
+
capMs: number;
|
|
17
|
+
} | undefined;
|
|
18
|
+
importPath?: string | undefined;
|
|
19
|
+
mask?: {
|
|
20
|
+
selector: string;
|
|
21
|
+
}[] | undefined;
|
|
22
|
+
viewport?: {
|
|
23
|
+
width: number;
|
|
24
|
+
height?: number | undefined;
|
|
25
|
+
} | undefined;
|
|
26
|
+
breakpoint?: number | undefined;
|
|
27
|
+
breakpointGroup?: string | undefined;
|
|
28
|
+
elementLocator?: string | undefined;
|
|
29
|
+
waitForSelector?: string | undefined;
|
|
30
|
+
}[]>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mapLimit } from './concurrency.js';
|
|
2
|
+
import { config, isPlatformModeConfig } from './config.js';
|
|
3
|
+
import { collectStories, generateStorybookShotItems, } from './crawler/storybook.js';
|
|
4
|
+
import { launchStaticWebServer } from './crawler/utils.js';
|
|
5
|
+
import { log } from './log.js';
|
|
6
|
+
import { takeScreenShots } from './shots/shots.js';
|
|
7
|
+
import { getBrowsers, removeFilesInFolder } from './utils.js';
|
|
8
|
+
export const createShots = async () => {
|
|
9
|
+
const { storybookShots, imagePathCurrent } = config;
|
|
10
|
+
const shotItems = [];
|
|
11
|
+
removeFilesInFolder(imagePathCurrent);
|
|
12
|
+
if (!isPlatformModeConfig(config)) {
|
|
13
|
+
removeFilesInFolder(config.imagePathDifference);
|
|
14
|
+
}
|
|
15
|
+
const browsers = getBrowsers();
|
|
16
|
+
if (storybookShots) {
|
|
17
|
+
const { storybookUrl, mask } = storybookShots;
|
|
18
|
+
log.process('info', 'general', `\n=== [Storybook Mode] ${storybookUrl} ===\n`);
|
|
19
|
+
let storybookWebUrl = storybookUrl;
|
|
20
|
+
let localServer;
|
|
21
|
+
if (!storybookUrl.startsWith('http://') &&
|
|
22
|
+
!storybookUrl.startsWith('https://')) {
|
|
23
|
+
const staticWebServer = await launchStaticWebServer(storybookUrl);
|
|
24
|
+
storybookWebUrl = staticWebServer.url;
|
|
25
|
+
localServer = staticWebServer.server;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const collection = await collectStories(storybookWebUrl);
|
|
29
|
+
if (!collection?.stories || collection.stories.length === 0) {
|
|
30
|
+
throw new Error('Error: Stories not found');
|
|
31
|
+
}
|
|
32
|
+
log.process('info', 'general', `Found ${collection.stories.length} stories`);
|
|
33
|
+
await mapLimit(browsers, browsers.length, async (browser) => {
|
|
34
|
+
const items = generateStorybookShotItems(storybookWebUrl, collection.stories, mask, storybookShots.breakpoints, browsers.length > 1 ? browser : undefined);
|
|
35
|
+
const filterItemsToCheck = 'filterItemsToCheck' in config
|
|
36
|
+
? config.filterItemsToCheck
|
|
37
|
+
: undefined;
|
|
38
|
+
const filteredShotItems = filterItemsToCheck
|
|
39
|
+
? items.filter((item) => filterItemsToCheck(item))
|
|
40
|
+
: items;
|
|
41
|
+
shotItems.push(...items);
|
|
42
|
+
log.process('info', 'general', `Prepared ${filteredShotItems.length} stories for screenshots on ${browser.name()}`);
|
|
43
|
+
await takeScreenShots(filteredShotItems, browser);
|
|
44
|
+
});
|
|
45
|
+
localServer?.close();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
localServer?.close();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
log.process('info', 'general', 'Screenshots done!');
|
|
52
|
+
}
|
|
53
|
+
return shotItems;
|
|
54
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './config.js';
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ShotMode } from './types.js';
|
|
2
|
+
type LogLevel = 'info' | 'error' | 'warn' | 'debug';
|
|
3
|
+
type LogEntry = {
|
|
4
|
+
timestamp: Date;
|
|
5
|
+
level: LogLevel;
|
|
6
|
+
item?: {
|
|
7
|
+
shotMode: ShotMode;
|
|
8
|
+
uniqueItemId: string;
|
|
9
|
+
itemIndex: number;
|
|
10
|
+
totalItems: number;
|
|
11
|
+
};
|
|
12
|
+
source: 'process' | 'browser';
|
|
13
|
+
context: 'general' | 'api' | 'console' | 'network' | 'timeout' | 'config';
|
|
14
|
+
content: unknown[];
|
|
15
|
+
};
|
|
16
|
+
export type LogMemory = LogEntry[];
|
|
17
|
+
export declare const logMemory: LogMemory;
|
|
18
|
+
export declare const log: {
|
|
19
|
+
item: (item: LogEntry["item"]) => {
|
|
20
|
+
process(level: LogEntry["level"], context: LogEntry["context"], ...content: unknown[]): void;
|
|
21
|
+
browser(level: LogEntry["level"], context: LogEntry["context"], ...content: unknown[]): void;
|
|
22
|
+
};
|
|
23
|
+
process(level: LogEntry["level"], context: LogEntry["context"], ...content: unknown[]): void;
|
|
24
|
+
browser(level: LogEntry["level"], context: LogEntry["context"], ...content: unknown[]): void;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const logMemory = [];
|
|
2
|
+
const serializeError = (error) => ({
|
|
3
|
+
message: error.message,
|
|
4
|
+
name: error.name,
|
|
5
|
+
stack: error.stack,
|
|
6
|
+
});
|
|
7
|
+
const serializeErrors = (content) => content.map((item) => {
|
|
8
|
+
if (item instanceof Error) {
|
|
9
|
+
return serializeError(item);
|
|
10
|
+
}
|
|
11
|
+
return item;
|
|
12
|
+
});
|
|
13
|
+
const renderLog = (entry) => {
|
|
14
|
+
if (entry.source === 'browser' && entry.context === 'console') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (entry.source === 'browser' && entry.context === 'network') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { log } = console;
|
|
21
|
+
const logPrefix = [];
|
|
22
|
+
if (entry.item) {
|
|
23
|
+
logPrefix.push(`[${entry.item.itemIndex + 1}/${entry.item.totalItems}]`);
|
|
24
|
+
}
|
|
25
|
+
if (!['general', 'api', 'config'].includes(entry.context)) {
|
|
26
|
+
logPrefix.push(`[${entry.context}]`);
|
|
27
|
+
}
|
|
28
|
+
if (entry.level === 'error') {
|
|
29
|
+
logPrefix.push('❌');
|
|
30
|
+
}
|
|
31
|
+
if (entry.item?.uniqueItemId) {
|
|
32
|
+
log(...logPrefix, ...entry.content, `(${entry.item?.uniqueItemId})`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
log(...logPrefix, ...entry.content);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export const log = {
|
|
39
|
+
item: (item) => ({
|
|
40
|
+
process(level, context, ...content) {
|
|
41
|
+
const entry = {
|
|
42
|
+
timestamp: new Date(),
|
|
43
|
+
level,
|
|
44
|
+
item,
|
|
45
|
+
source: 'process',
|
|
46
|
+
context,
|
|
47
|
+
content,
|
|
48
|
+
};
|
|
49
|
+
renderLog(entry);
|
|
50
|
+
logMemory.push({
|
|
51
|
+
...entry,
|
|
52
|
+
content: serializeErrors(content),
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
browser(level, context, ...content) {
|
|
56
|
+
const entry = {
|
|
57
|
+
timestamp: new Date(),
|
|
58
|
+
level,
|
|
59
|
+
item,
|
|
60
|
+
source: 'browser',
|
|
61
|
+
context,
|
|
62
|
+
content,
|
|
63
|
+
};
|
|
64
|
+
renderLog(entry);
|
|
65
|
+
logMemory.push({
|
|
66
|
+
...entry,
|
|
67
|
+
content: serializeErrors(content),
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
process(level, context, ...content) {
|
|
72
|
+
const entry = {
|
|
73
|
+
timestamp: new Date(),
|
|
74
|
+
level,
|
|
75
|
+
source: 'process',
|
|
76
|
+
context,
|
|
77
|
+
content,
|
|
78
|
+
};
|
|
79
|
+
renderLog(entry);
|
|
80
|
+
logMemory.push({
|
|
81
|
+
...entry,
|
|
82
|
+
content: serializeErrors(content),
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
browser(level, context, ...content) {
|
|
86
|
+
const entry = {
|
|
87
|
+
timestamp: new Date(),
|
|
88
|
+
level,
|
|
89
|
+
source: 'browser',
|
|
90
|
+
context,
|
|
91
|
+
content,
|
|
92
|
+
};
|
|
93
|
+
renderLog(entry);
|
|
94
|
+
logMemory.push({
|
|
95
|
+
...entry,
|
|
96
|
+
content: serializeErrors(content),
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
};
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GenerateOnlyModeConfig, PlatformModeConfig } from './config.js';
|
|
2
|
+
export declare const runner: (config: GenerateOnlyModeConfig) => Promise<void>;
|
|
3
|
+
export declare const getPlatformApiToken: (config: PlatformModeConfig) => Promise<string>;
|
|
4
|
+
export declare const platformRunner: (config: PlatformModeConfig, apiToken: string) => Promise<void>;
|