@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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/api.d.ts +56 -0
  4. package/dist/api.js +238 -0
  5. package/dist/bin.d.ts +2 -0
  6. package/dist/bin.js +102 -0
  7. package/dist/checkDifferences.d.ts +63 -0
  8. package/dist/checkDifferences.js +67 -0
  9. package/dist/compare/compare.d.ts +5 -0
  10. package/dist/compare/compare.js +80 -0
  11. package/dist/compare/pixelmatch.d.ts +7 -0
  12. package/dist/compare/pixelmatch.js +46 -0
  13. package/dist/compare/utils.d.ts +2 -0
  14. package/dist/compare/utils.js +18 -0
  15. package/dist/compare/worker.d.ts +1 -0
  16. package/dist/compare/worker.js +6 -0
  17. package/dist/concurrency.d.ts +19 -0
  18. package/dist/concurrency.js +61 -0
  19. package/dist/config.d.ts +3795 -0
  20. package/dist/config.js +472 -0
  21. package/dist/configHelper.d.ts +2 -0
  22. package/dist/configHelper.js +34 -0
  23. package/dist/constants.d.ts +2 -0
  24. package/dist/constants.js +2 -0
  25. package/dist/crawler/storybook.d.ts +51 -0
  26. package/dist/crawler/storybook.js +317 -0
  27. package/dist/crawler/utils.d.ts +6 -0
  28. package/dist/crawler/utils.js +20 -0
  29. package/dist/createShots.d.ts +30 -0
  30. package/dist/createShots.js +54 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -0
  33. package/dist/log.d.ts +26 -0
  34. package/dist/log.js +99 -0
  35. package/dist/runner.d.ts +4 -0
  36. package/dist/runner.js +186 -0
  37. package/dist/schemas.d.ts +174 -0
  38. package/dist/schemas.js +73 -0
  39. package/dist/shard.d.ts +3 -0
  40. package/dist/shard.js +17 -0
  41. package/dist/shots/shots.d.ts +3 -0
  42. package/dist/shots/shots.js +196 -0
  43. package/dist/shots/utils.d.ts +33 -0
  44. package/dist/shots/utils.js +177 -0
  45. package/dist/types.d.ts +12 -0
  46. package/dist/types.js +1 -0
  47. package/dist/upload.d.ts +10 -0
  48. package/dist/upload.js +32 -0
  49. package/dist/uploadStorybook.d.ts +2 -0
  50. package/dist/uploadStorybook.js +56 -0
  51. package/dist/utils.d.ts +50 -0
  52. package/dist/utils.js +194 -0
  53. 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,6 @@
1
+ import http from 'node:http';
2
+ export declare const launchStaticWebServer: (basePath: string) => Promise<{
3
+ server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
4
+ port: number;
5
+ url: string;
6
+ }>;
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ export * from './config.js';
2
+ export type { StoryParameters } from './crawler/storybook.js';
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
+ };
@@ -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>;