@storyshelf/core 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/README.md +127 -0
- package/dist/adapter/auth.d.mts +50 -0
- package/dist/adapter/auth.mjs +10 -0
- package/dist/adapter/auth.mjs.map +1 -0
- package/dist/adapter/capture-runner.d.mts +70 -0
- package/dist/adapter/capture-runner.mjs +4 -0
- package/dist/adapter/database.d.mts +43 -0
- package/dist/adapter/database.mjs +4 -0
- package/dist/adapter/git-host/helpers.d.mts +12 -0
- package/dist/adapter/git-host/helpers.mjs +29 -0
- package/dist/adapter/git-host/helpers.mjs.map +1 -0
- package/dist/adapter/git-host.d.mts +46 -0
- package/dist/adapter/git-host.mjs +4 -0
- package/dist/adapter/storage.d.mts +22 -0
- package/dist/adapter/storage.mjs +4 -0
- package/dist/adapters/capture-queue.d.mts +49 -0
- package/dist/adapters/metadata.d.mts +24 -0
- package/dist/adapters/webhook-events.mjs +39 -0
- package/dist/adapters/webhook-events.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +29 -0
- package/dist/capture/adapter.mjs +13 -0
- package/dist/capture/adapter.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +23 -0
- package/dist/capture/orchestrator.mjs +103 -0
- package/dist/capture/orchestrator.mjs.map +1 -0
- package/dist/capture/pipeline.d.mts +41 -0
- package/dist/capture/pipeline.mjs +119 -0
- package/dist/capture/pipeline.mjs.map +1 -0
- package/dist/capture/queue.d.mts +35 -0
- package/dist/capture/queue.mjs +96 -0
- package/dist/capture/queue.mjs.map +1 -0
- package/dist/capture/status-fanout.mjs +66 -0
- package/dist/capture/status-fanout.mjs.map +1 -0
- package/dist/capture/storybook.d.mts +14 -0
- package/dist/capture/storybook.mjs +40 -0
- package/dist/capture/storybook.mjs.map +1 -0
- package/dist/config.d.mts +68 -0
- package/dist/config.mjs +69 -0
- package/dist/config.mjs.map +1 -0
- package/dist/ddl.d.mts +7 -0
- package/dist/ddl.mjs +155 -0
- package/dist/ddl.mjs.map +1 -0
- package/dist/diff/engine.d.mts +8 -0
- package/dist/diff/engine.mjs +79 -0
- package/dist/diff/engine.mjs.map +1 -0
- package/dist/diff/options.d.mts +40 -0
- package/dist/diff/options.mjs +15 -0
- package/dist/diff/options.mjs.map +1 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.mjs +327 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger.d.mts +39 -0
- package/dist/logger.mjs +35 -0
- package/dist/logger.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +39 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +38 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/models/baseline.d.mts +20 -0
- package/dist/models/baseline.mjs +89 -0
- package/dist/models/baseline.mjs.map +1 -0
- package/dist/models/build.d.mts +35 -0
- package/dist/models/build.mjs +77 -0
- package/dist/models/build.mjs.map +1 -0
- package/dist/models/comment.d.mts +20 -0
- package/dist/models/comment.mjs +42 -0
- package/dist/models/comment.mjs.map +1 -0
- package/dist/models/label.d.mts +43 -0
- package/dist/models/label.mjs +107 -0
- package/dist/models/label.mjs.map +1 -0
- package/dist/models/member.d.mts +27 -0
- package/dist/models/member.mjs +54 -0
- package/dist/models/member.mjs.map +1 -0
- package/dist/models/project.d.mts +42 -0
- package/dist/models/project.mjs +78 -0
- package/dist/models/project.mjs.map +1 -0
- package/dist/models/snapshot.d.mts +46 -0
- package/dist/models/snapshot.mjs +75 -0
- package/dist/models/snapshot.mjs.map +1 -0
- package/dist/models/status-config.mjs +61 -0
- package/dist/models/status-config.mjs.map +1 -0
- package/dist/models/token.d.mts +33 -0
- package/dist/models/token.mjs +60 -0
- package/dist/models/token.mjs.map +1 -0
- package/dist/models/webhook.d.mts +37 -0
- package/dist/models/webhook.mjs +67 -0
- package/dist/models/webhook.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +333 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +437 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +196 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/login.mjs +77 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +228 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +168 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +111 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects.mjs +178 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +90 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +191 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +149 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +124 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +150 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +95 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +149 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/retention/purge.d.mts +28 -0
- package/dist/retention/purge.mjs +66 -0
- package/dist/retention/purge.mjs.map +1 -0
- package/dist/routers/admin.mjs +41 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +24 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/auth.mjs +77 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +87 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +162 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +89 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/helpers.mjs +76 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +18 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +77 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +54 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +101 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/projects.mjs +121 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/schemas.mjs +188 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings.mjs +235 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +143 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +100 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/tokens.mjs +80 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +142 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +89 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/schema.d.mts +4372 -0
- package/dist/schema.mjs +158 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/store.mjs +18 -0
- package/dist/store.mjs.map +1 -0
- package/dist/types.d.mts +18 -0
- package/dist/types.mjs +49 -0
- package/dist/types.mjs.map +1 -0
- package/dist/ui/components.mjs +127 -0
- package/dist/ui/components.mjs.map +1 -0
- package/dist/ui/document.d.mts +8 -0
- package/dist/ui/document.mjs +338 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/styles.mjs +173 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/theme.mjs +42 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/urls.d.mts +19 -0
- package/dist/urls.mjs +23 -0
- package/dist/urls.mjs.map +1 -0
- package/dist/utils/encrypt.mjs +49 -0
- package/dist/utils/encrypt.mjs.map +1 -0
- package/dist/utils/hash.mjs +22 -0
- package/dist/utils/hash.mjs.map +1 -0
- package/dist/utils/paths.d.mts +11 -0
- package/dist/utils/paths.mjs +23 -0
- package/dist/utils/paths.mjs.map +1 -0
- package/dist/utils/ulid.d.mts +8 -0
- package/dist/utils/ulid.mjs +41 -0
- package/dist/utils/ulid.mjs.map +1 -0
- package/package.json +162 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../models/build.mjs";
|
|
5
|
+
import { ProjectModel } from "../models/project.mjs";
|
|
6
|
+
import { storybookZipPath } from "../utils/paths.mjs";
|
|
7
|
+
import { persistCapture } from "./pipeline.mjs";
|
|
8
|
+
import { StorybookAdapter } from "./storybook.mjs";
|
|
9
|
+
import { DEFAULT_VIEWPORTS } from "./adapter.mjs";
|
|
10
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
11
|
+
import { join, resolve, sep } from "node:path";
|
|
12
|
+
import AdmZip from "adm-zip";
|
|
13
|
+
//#region src/capture/orchestrator.ts
|
|
14
|
+
async function executeCaptureJob(input, options) {
|
|
15
|
+
const builds = new BuildModel(options.db);
|
|
16
|
+
const { build, project } = await loadTarget(options, input.buildId);
|
|
17
|
+
const logger = options.logger?.child({
|
|
18
|
+
buildId: input.buildId,
|
|
19
|
+
reqId: input.reqId
|
|
20
|
+
});
|
|
21
|
+
await builds.setStatus(build.id, "capturing");
|
|
22
|
+
const startTime = performance.now();
|
|
23
|
+
let storybookDir;
|
|
24
|
+
try {
|
|
25
|
+
const extractStart = performance.now();
|
|
26
|
+
storybookDir = await extractStorybook(options, project.id, build.id);
|
|
27
|
+
const extractDuration = performance.now() - extractStart;
|
|
28
|
+
logger?.info({ durationMs: Math.round(extractDuration) }, "storybook extracted");
|
|
29
|
+
const stories = await new StorybookAdapter().discover(storybookDir);
|
|
30
|
+
const viewports = options.viewports ?? DEFAULT_VIEWPORTS;
|
|
31
|
+
const renderStart = performance.now();
|
|
32
|
+
const result = await options.runner.render({
|
|
33
|
+
buildId: build.id,
|
|
34
|
+
storybookDir,
|
|
35
|
+
stories,
|
|
36
|
+
viewports,
|
|
37
|
+
logger
|
|
38
|
+
});
|
|
39
|
+
const renderDuration = performance.now() - renderStart;
|
|
40
|
+
logger?.info({
|
|
41
|
+
durationMs: Math.round(renderDuration),
|
|
42
|
+
storyCount: stories.length
|
|
43
|
+
}, "stories rendered");
|
|
44
|
+
const persistStart = performance.now();
|
|
45
|
+
await persistCapture({
|
|
46
|
+
db: options.db,
|
|
47
|
+
storage: options.storage,
|
|
48
|
+
project,
|
|
49
|
+
build,
|
|
50
|
+
viewports,
|
|
51
|
+
captures: result.captures,
|
|
52
|
+
logger
|
|
53
|
+
}, new Set(result.failures.map((failure) => failure.storyId)));
|
|
54
|
+
const persistDuration = performance.now() - persistStart;
|
|
55
|
+
logger?.info({ durationMs: Math.round(persistDuration) }, "capture persisted");
|
|
56
|
+
const totalDuration = performance.now() - startTime;
|
|
57
|
+
logger?.info({ durationMs: Math.round(totalDuration) }, "capture completed");
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const totalDuration = performance.now() - startTime;
|
|
60
|
+
logger?.error({
|
|
61
|
+
durationMs: Math.round(totalDuration),
|
|
62
|
+
err: error
|
|
63
|
+
}, "capture failed");
|
|
64
|
+
await builds.setStatus(build.id, "failed").catch((markError) => {
|
|
65
|
+
logger?.error({ err: markError }, "failed to mark build failed after capture error");
|
|
66
|
+
});
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function loadTarget(options, buildId) {
|
|
71
|
+
const build = await new BuildModel(options.db).get(buildId);
|
|
72
|
+
if (!build) throw new Error(`Build not found: ${buildId}`);
|
|
73
|
+
const project = await new ProjectModel(options.db).get(build.projectId);
|
|
74
|
+
if (!project) throw new Error(`Project not found: ${build.projectId}`);
|
|
75
|
+
return {
|
|
76
|
+
build,
|
|
77
|
+
project
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function blockedTarget(root, entryName) {
|
|
81
|
+
const candidate = resolve(join(root, entryName));
|
|
82
|
+
return candidate !== root && !candidate.startsWith(root + sep);
|
|
83
|
+
}
|
|
84
|
+
function assertNoTraversal(zip, root) {
|
|
85
|
+
for (const entry of zip.getEntries()) if (blockedTarget(root, entry.entryName)) throw new Error(`Blocked path traversal in uploaded Storybook: ${entry.entryName}`);
|
|
86
|
+
}
|
|
87
|
+
async function extractStorybook(options, projectId, buildId) {
|
|
88
|
+
const targetDir = join(options.scratchDir, projectId, "builds", buildId, "storybook");
|
|
89
|
+
const root = resolve(targetDir);
|
|
90
|
+
const zip = new AdmZip(await options.storage.read(storybookZipPath(projectId, buildId)));
|
|
91
|
+
assertNoTraversal(zip, root);
|
|
92
|
+
await rm(targetDir, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
force: true
|
|
95
|
+
});
|
|
96
|
+
await mkdir(targetDir, { recursive: true });
|
|
97
|
+
zip.extractAllTo(targetDir, true);
|
|
98
|
+
return targetDir;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { executeCaptureJob };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=orchestrator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.mjs","names":[],"sources":["../../src/capture/orchestrator.ts"],"sourcesContent":["import { mkdir, rm } from \"node:fs/promises\";\nimport { join, resolve, sep } from \"node:path\";\n\nimport AdmZip from \"adm-zip\";\nimport type { Logger } from \"pino\";\n\nimport type { CaptureRunner } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport type { Build, Project } from \"../schema.ts\";\nimport { storybookZipPath } from \"../utils/paths.ts\";\nimport { persistCapture } from \"./pipeline.ts\";\nimport { StorybookAdapter } from \"./storybook.ts\";\nimport { DEFAULT_VIEWPORTS } from \"./adapter.ts\";\nimport type { Viewport } from \"./adapter.ts\";\n\nexport interface CaptureJobOptions {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n runner: CaptureRunner;\n scratchDir: string;\n viewports?: Viewport[];\n logger?: Logger;\n}\nexport async function executeCaptureJob(input: { buildId: string; reqId?: string }, options: CaptureJobOptions): Promise<void> {\n const builds = new BuildModel(options.db);\n const { build, project } = await loadTarget(options, input.buildId);\n const logger = options.logger?.child({ buildId: input.buildId, reqId: input.reqId });\n await builds.setStatus(build.id, \"capturing\");\n\n const startTime = performance.now();\n let storybookDir: string | undefined;\n try {\n const extractStart = performance.now();\n storybookDir = await extractStorybook(options, project.id, build.id);\n const extractDuration = performance.now() - extractStart;\n logger?.info({ durationMs: Math.round(extractDuration) }, \"storybook extracted\");\n\n const adapter = new StorybookAdapter();\n const stories = await adapter.discover(storybookDir);\n const viewports = options.viewports ?? DEFAULT_VIEWPORTS;\n\n const renderStart = performance.now();\n const result = await options.runner.render({ buildId: build.id, storybookDir, stories, viewports, logger });\n const renderDuration = performance.now() - renderStart;\n logger?.info({ durationMs: Math.round(renderDuration), storyCount: stories.length }, \"stories rendered\");\n\n const persistStart = performance.now();\n await persistCapture(\n {\n db: options.db,\n storage: options.storage,\n project,\n build,\n viewports,\n captures: result.captures,\n logger,\n },\n new Set(result.failures.map((failure) => failure.storyId)),\n );\n const persistDuration = performance.now() - persistStart;\n logger?.info({ durationMs: Math.round(persistDuration) }, \"capture persisted\");\n\n const totalDuration = performance.now() - startTime;\n logger?.info({ durationMs: Math.round(totalDuration) }, \"capture completed\");\n } catch (error) {\n const totalDuration = performance.now() - startTime;\n logger?.error({ durationMs: Math.round(totalDuration), err: error }, \"capture failed\");\n await builds.setStatus(build.id, \"failed\").catch((markError: unknown) => {\n logger?.error({ err: markError }, \"failed to mark build failed after capture error\");\n });\n throw error;\n }\n}\n\nasync function loadTarget(options: CaptureJobOptions, buildId: string): Promise<{ build: Build; project: Project }> {\n const build = await new BuildModel(options.db).get(buildId);\n if (!build) {\n throw new Error(`Build not found: ${buildId}`);\n }\n const project = await new ProjectModel(options.db).get(build.projectId);\n if (!project) {\n throw new Error(`Project not found: ${build.projectId}`);\n }\n return { build, project };\n}\n\nfunction blockedTarget(root: string, entryName: string): boolean {\n const candidate = resolve(join(root, entryName));\n return candidate !== root && !candidate.startsWith(root + sep);\n}\n\nfunction assertNoTraversal(zip: AdmZip, root: string): void {\n for (const entry of zip.getEntries()) {\n if (blockedTarget(root, entry.entryName)) {\n throw new Error(`Blocked path traversal in uploaded Storybook: ${entry.entryName}`);\n }\n }\n}\n\nasync function extractStorybook(options: CaptureJobOptions, projectId: string, buildId: string): Promise<string> {\n const targetDir = join(options.scratchDir, projectId, \"builds\", buildId, \"storybook\");\n const root = resolve(targetDir);\n const zip = new AdmZip(await options.storage.read(storybookZipPath(projectId, buildId)));\n assertNoTraversal(zip, root);\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(targetDir, { recursive: true });\n zip.extractAllTo(targetDir, true);\n return targetDir;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { Viewport } from "./adapter.mjs";
|
|
4
|
+
import { RenderedSnapshot } from "../adapter/capture-runner.mjs";
|
|
5
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
6
|
+
import { StorageAdapter } from "../adapter/storage.mjs";
|
|
7
|
+
import { Build, Project } from "../schema.mjs";
|
|
8
|
+
import { Logger } from "pino";
|
|
9
|
+
//#region src/capture/pipeline.d.ts
|
|
10
|
+
/** Persistence inputs for a completed capture run. */
|
|
11
|
+
interface CaptureContext {
|
|
12
|
+
/** Database adapter. */
|
|
13
|
+
db: DatabaseAdapter;
|
|
14
|
+
/** Storage adapter. */
|
|
15
|
+
storage: StorageAdapter;
|
|
16
|
+
/** The project being captured. */
|
|
17
|
+
project: Project;
|
|
18
|
+
/** The build being captured. */
|
|
19
|
+
build: Build;
|
|
20
|
+
/** Viewports at which stories were captured. */
|
|
21
|
+
viewports: Viewport[];
|
|
22
|
+
/** Screenshot buffers produced by the capture renderer. */
|
|
23
|
+
captures: RenderedSnapshot[];
|
|
24
|
+
/** Optional logger for capture diagnostics. */
|
|
25
|
+
logger?: Logger;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Persist a completed capture run: write screenshots, diff against the branch
|
|
29
|
+
* baseline, create snapshots, and finalize the build.
|
|
30
|
+
*
|
|
31
|
+
* Rendering is performed by a pure `CaptureRunner`; this function owns only
|
|
32
|
+
* storage writes and record keeping, so it can run against any renderer.
|
|
33
|
+
*
|
|
34
|
+
* @param ctx - Capture context.
|
|
35
|
+
* @param renderFailedStoryIds - Story ids that the renderer could not capture.
|
|
36
|
+
* They are excluded from the persisted set and force the build to `failed`.
|
|
37
|
+
*/
|
|
38
|
+
declare function persistCapture(ctx: CaptureContext, renderFailedStoryIds?: ReadonlySet<string>): Promise<void>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { CaptureContext, persistCapture };
|
|
41
|
+
//# sourceMappingURL=pipeline.d.mts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../models/build.mjs";
|
|
5
|
+
import { diffPath, screenshotPath } from "../utils/paths.mjs";
|
|
6
|
+
import { BaselineModel } from "../models/baseline.mjs";
|
|
7
|
+
import { SnapshotModel } from "../models/snapshot.mjs";
|
|
8
|
+
import { diffImages } from "../diff/engine.mjs";
|
|
9
|
+
import { DEFAULT_DIFF_OPTIONS } from "../diff/options.mjs";
|
|
10
|
+
//#region src/capture/pipeline.ts
|
|
11
|
+
/**
|
|
12
|
+
* Persist a completed capture run: write screenshots, diff against the branch
|
|
13
|
+
* baseline, create snapshots, and finalize the build.
|
|
14
|
+
*
|
|
15
|
+
* Rendering is performed by a pure `CaptureRunner`; this function owns only
|
|
16
|
+
* storage writes and record keeping, so it can run against any renderer.
|
|
17
|
+
*
|
|
18
|
+
* @param ctx - Capture context.
|
|
19
|
+
* @param renderFailedStoryIds - Story ids that the renderer could not capture.
|
|
20
|
+
* They are excluded from the persisted set and force the build to `failed`.
|
|
21
|
+
*/
|
|
22
|
+
async function persistCapture(ctx, renderFailedStoryIds = /* @__PURE__ */ new Set()) {
|
|
23
|
+
const failedStoryIds = new Set(renderFailedStoryIds);
|
|
24
|
+
await Promise.all(ctx.captures.map(async (capture) => {
|
|
25
|
+
try {
|
|
26
|
+
await persistSnapshot(ctx, capture);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
failedStoryIds.add(capture.story.id);
|
|
29
|
+
ctx.logger?.error({
|
|
30
|
+
storyId: capture.story.id,
|
|
31
|
+
viewport: capture.viewportName,
|
|
32
|
+
err: error
|
|
33
|
+
}, "capture failed for story");
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds);
|
|
37
|
+
}
|
|
38
|
+
function viewportByName(ctx, name) {
|
|
39
|
+
return ctx.viewports.find((v) => v.name === name) ?? {
|
|
40
|
+
name,
|
|
41
|
+
width: 0,
|
|
42
|
+
height: 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function persistSnapshot(ctx, capture) {
|
|
46
|
+
const viewport = viewportByName(ctx, capture.viewportName);
|
|
47
|
+
const screenshot = screenshotPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);
|
|
48
|
+
await ctx.storage.write(screenshot, capture.screenshot);
|
|
49
|
+
const baseline = await resolveBaseline(ctx, capture.story.id, capture.viewportName);
|
|
50
|
+
if (!baseline) {
|
|
51
|
+
await createWithoutBaseline(ctx, capture, viewport, screenshot);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await createWithBaseline(ctx, capture, viewport, screenshot, baseline.screenshotPath);
|
|
55
|
+
}
|
|
56
|
+
async function resolveBaseline(ctx, storyId, viewport) {
|
|
57
|
+
return await new BaselineModel(ctx.db, ctx.storage).resolve(ctx.project.id, storyId, viewport, ctx.build.gitBranch, ctx.project.gitDefaultBranch);
|
|
58
|
+
}
|
|
59
|
+
async function createWithoutBaseline(ctx, capture, viewport, screenshot) {
|
|
60
|
+
const snapshots = new SnapshotModel(ctx.db);
|
|
61
|
+
const status = ctx.build.isDefault ? "approved" : "new";
|
|
62
|
+
const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {
|
|
63
|
+
storyId: capture.story.id,
|
|
64
|
+
storyName: capture.story.name,
|
|
65
|
+
storyTitle: capture.story.title,
|
|
66
|
+
storyImportPath: capture.story.importPath ?? "",
|
|
67
|
+
viewportName: capture.viewportName,
|
|
68
|
+
viewportWidth: viewport.width,
|
|
69
|
+
viewportHeight: viewport.height,
|
|
70
|
+
screenshotPath: screenshot
|
|
71
|
+
});
|
|
72
|
+
await snapshots.setStatus(snapshot.id, status);
|
|
73
|
+
if (ctx.build.isDefault) await new BaselineModel(ctx.db, ctx.storage).upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshot.id, screenshot);
|
|
74
|
+
}
|
|
75
|
+
async function createWithBaseline(ctx, capture, viewport, screenshot, baselinePath) {
|
|
76
|
+
const current = await ctx.storage.read(screenshot);
|
|
77
|
+
const previous = await ctx.storage.read(baselinePath);
|
|
78
|
+
const options = {
|
|
79
|
+
...DEFAULT_DIFF_OPTIONS,
|
|
80
|
+
pixelThreshold: ctx.project.pixelThreshold,
|
|
81
|
+
maxDiffRatio: ctx.project.maxDiffRatio
|
|
82
|
+
};
|
|
83
|
+
const result = diffImages(previous, current, options);
|
|
84
|
+
const snapshots = new SnapshotModel(ctx.db);
|
|
85
|
+
const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {
|
|
86
|
+
storyId: capture.story.id,
|
|
87
|
+
storyName: capture.story.name,
|
|
88
|
+
storyTitle: capture.story.title,
|
|
89
|
+
storyImportPath: capture.story.importPath ?? "",
|
|
90
|
+
viewportName: capture.viewportName,
|
|
91
|
+
viewportWidth: viewport.width,
|
|
92
|
+
viewportHeight: viewport.height,
|
|
93
|
+
screenshotPath: screenshot
|
|
94
|
+
});
|
|
95
|
+
const status = result.passed ? "unchanged" : "changed";
|
|
96
|
+
const diff = diffPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);
|
|
97
|
+
if (!result.passed && result.diffImage) await ctx.storage.write(diff, result.diffImage);
|
|
98
|
+
await snapshots.update(snapshot.id, {
|
|
99
|
+
status,
|
|
100
|
+
diffPath: !result.passed && result.diffImage ? diff : null,
|
|
101
|
+
diffPixels: result.diffPixels,
|
|
102
|
+
diffRatio: result.diffRatio,
|
|
103
|
+
diffPassed: result.passed
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function finalize(ctx, storyIds, failedStoryIds) {
|
|
107
|
+
const builds = new BuildModel(ctx.db);
|
|
108
|
+
const build = await builds.updateCounts(ctx.build.id);
|
|
109
|
+
const hasCaptures = storyIds.size > 0;
|
|
110
|
+
let status = "reviewing";
|
|
111
|
+
if (failedStoryIds.size > 0) status = "failed";
|
|
112
|
+
else if (hasCaptures && build.changedCount === 0) status = "approved";
|
|
113
|
+
await builds.setStatus(ctx.build.id, status);
|
|
114
|
+
if (ctx.build.isDefault && hasCaptures) await new BaselineModel(ctx.db, ctx.storage).removeOrphans(ctx.project.id, new Set(storyIds));
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { persistCapture };
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=pipeline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.mjs","names":[],"sources":["../../src/capture/pipeline.ts"],"sourcesContent":["import type { Logger } from \"pino\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { RenderedSnapshot } from \"../adapters/capture-runner.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { diffImages } from \"../diff/engine.ts\";\nimport { DEFAULT_DIFF_OPTIONS } from \"../diff/options.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline, Build, Project } from \"../schema.ts\";\nimport type { BuildStatus } from \"../types.ts\";\nimport { diffPath, screenshotPath } from \"../utils/paths.ts\";\nimport type { Viewport } from \"./adapter.ts\";\n\n/** Persistence inputs for a completed capture run. */\nexport interface CaptureContext {\n /** Database adapter. */\n db: DatabaseAdapter;\n /** Storage adapter. */\n storage: StorageAdapter;\n /** The project being captured. */\n project: Project;\n /** The build being captured. */\n build: Build;\n /** Viewports at which stories were captured. */\n viewports: Viewport[];\n /** Screenshot buffers produced by the capture renderer. */\n captures: RenderedSnapshot[];\n /** Optional logger for capture diagnostics. */\n logger?: Logger;\n}\n\n/**\n * Persist a completed capture run: write screenshots, diff against the branch\n * baseline, create snapshots, and finalize the build.\n *\n * Rendering is performed by a pure `CaptureRunner`; this function owns only\n * storage writes and record keeping, so it can run against any renderer.\n *\n * @param ctx - Capture context.\n * @param renderFailedStoryIds - Story ids that the renderer could not capture.\n * They are excluded from the persisted set and force the build to `failed`.\n */\nexport async function persistCapture(ctx: CaptureContext, renderFailedStoryIds: ReadonlySet<string> = new Set()): Promise<void> {\n const failedStoryIds = new Set(renderFailedStoryIds);\n await Promise.all(\n ctx.captures.map(async (capture) => {\n try {\n await persistSnapshot(ctx, capture);\n } catch (error) {\n // A failure in one story/viewport must not abort the other persists.\n failedStoryIds.add(capture.story.id);\n ctx.logger?.error(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for story\",\n );\n }\n }),\n );\n await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds);\n}\n\nfunction viewportByName(ctx: CaptureContext, name: string): Viewport {\n return ctx.viewports.find((v) => v.name === name) ?? { name, width: 0, height: 0 };\n}\n\nasync function persistSnapshot(ctx: CaptureContext, capture: RenderedSnapshot): Promise<void> {\n const viewport = viewportByName(ctx, capture.viewportName);\n const screenshot = screenshotPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);\n await ctx.storage.write(screenshot, capture.screenshot);\n\n const baseline = await resolveBaseline(ctx, capture.story.id, capture.viewportName);\n if (!baseline) {\n await createWithoutBaseline(ctx, capture, viewport, screenshot);\n return;\n }\n await createWithBaseline(ctx, capture, viewport, screenshot, baseline.screenshotPath);\n}\n\nasync function resolveBaseline(ctx: CaptureContext, storyId: string, viewport: string): Promise<Baseline | null> {\n const baselines = new BaselineModel(ctx.db, ctx.storage);\n return await baselines.resolve(ctx.project.id, storyId, viewport, ctx.build.gitBranch, ctx.project.gitDefaultBranch);\n}\n\nasync function createWithoutBaseline(ctx: CaptureContext, capture: RenderedSnapshot, viewport: Viewport, screenshot: string): Promise<void> {\n const snapshots = new SnapshotModel(ctx.db);\n const status = ctx.build.isDefault ? \"approved\" : \"new\";\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n await snapshots.setStatus(snapshot.id, status);\n\n if (ctx.build.isDefault) {\n const baselines = new BaselineModel(ctx.db, ctx.storage);\n await baselines.upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshot.id, screenshot);\n }\n}\n\nasync function createWithBaseline(ctx: CaptureContext, capture: RenderedSnapshot, viewport: Viewport, screenshot: string, baselinePath: string): Promise<void> {\n const current = await ctx.storage.read(screenshot);\n const previous = await ctx.storage.read(baselinePath);\n const options = { ...DEFAULT_DIFF_OPTIONS, pixelThreshold: ctx.project.pixelThreshold, maxDiffRatio: ctx.project.maxDiffRatio };\n const result = diffImages(previous, current, options);\n\n const snapshots = new SnapshotModel(ctx.db);\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n\n const status = result.passed ? \"unchanged\" : \"changed\";\n const diff = diffPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);\n if (!result.passed && result.diffImage) {\n await ctx.storage.write(diff, result.diffImage);\n }\n await snapshots.update(snapshot.id, {\n status,\n diffPath: !result.passed && result.diffImage ? diff : null,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n });\n}\n\nasync function finalize(ctx: CaptureContext, storyIds: ReadonlySet<string>, failedStoryIds: ReadonlySet<string>): Promise<void> {\n const builds = new BuildModel(ctx.db);\n const build = await builds.updateCounts(ctx.build.id);\n const hasCaptures = storyIds.size > 0;\n let status: BuildStatus = \"reviewing\";\n if (failedStoryIds.size > 0) {\n status = \"failed\";\n } else if (hasCaptures && build.changedCount === 0) {\n status = \"approved\";\n }\n await builds.setStatus(ctx.build.id, status);\n\n if (ctx.build.isDefault && hasCaptures) {\n const baselines = new BaselineModel(ctx.db, ctx.storage);\n await baselines.removeOrphans(ctx.project.id, new Set(storyIds));\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4CA,eAAsB,eAAe,KAAqB,uCAA4C,IAAI,IAAI,GAAkB;CAC9H,MAAM,iBAAiB,IAAI,IAAI,oBAAoB;CACnD,MAAM,QAAQ,IACZ,IAAI,SAAS,IAAI,OAAO,YAAY;EAClC,IAAI;GACF,MAAM,gBAAgB,KAAK,OAAO;EACpC,SAAS,OAAO;GAEd,eAAe,IAAI,QAAQ,MAAM,EAAE;GACnC,IAAI,QAAQ,MACV;IAAE,SAAS,QAAQ,MAAM;IAAI,UAAU,QAAQ;IAAc,KAAK;GAAM,GACxE,0BACF;EACF;CACF,CAAC,CACH;CACA,MAAM,SAAS,KAAK,IAAI,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,cAAc;AAClF;AAEA,SAAS,eAAe,KAAqB,MAAwB;CACnE,OAAO,IAAI,UAAU,MAAM,MAAM,EAAE,SAAS,IAAI,KAAK;EAAE;EAAM,OAAO;EAAG,QAAQ;CAAE;AACnF;AAEA,eAAe,gBAAgB,KAAqB,SAA0C;CAC5F,MAAM,WAAW,eAAe,KAAK,QAAQ,YAAY;CACzD,MAAM,aAAa,eAAe,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI,QAAQ,MAAM,IAAI,QAAQ,YAAY;CACtG,MAAM,IAAI,QAAQ,MAAM,YAAY,QAAQ,UAAU;CAEtD,MAAM,WAAW,MAAM,gBAAgB,KAAK,QAAQ,MAAM,IAAI,QAAQ,YAAY;CAClF,IAAI,CAAC,UAAU;EACb,MAAM,sBAAsB,KAAK,SAAS,UAAU,UAAU;EAC9D;CACF;CACA,MAAM,mBAAmB,KAAK,SAAS,UAAU,YAAY,SAAS,cAAc;AACtF;AAEA,eAAe,gBAAgB,KAAqB,SAAiB,UAA4C;CAE/G,OAAO,MAAM,IADS,cAAc,IAAI,IAAI,IAAI,OAC3B,CAAC,CAAC,QAAQ,IAAI,QAAQ,IAAI,SAAS,UAAU,IAAI,MAAM,WAAW,IAAI,QAAQ,gBAAgB;AACrH;AAEA,eAAe,sBAAsB,KAAqB,SAA2B,UAAoB,YAAmC;CAC1I,MAAM,YAAY,IAAI,cAAc,IAAI,EAAE;CAC1C,MAAM,SAAS,IAAI,MAAM,YAAY,aAAa;CAClD,MAAM,WAAW,MAAM,UAAU,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI;EACpE,SAAS,QAAQ,MAAM;EACvB,WAAW,QAAQ,MAAM;EACzB,YAAY,QAAQ,MAAM;EAC1B,iBAAiB,QAAQ,MAAM,cAAc;EAC7C,cAAc,QAAQ;EACtB,eAAe,SAAS;EACxB,gBAAgB,SAAS;EACzB,gBAAgB;CAClB,CAAC;CACD,MAAM,UAAU,UAAU,SAAS,IAAI,MAAM;CAE7C,IAAI,IAAI,MAAM,WAEZ,MAAM,IADgB,cAAc,IAAI,IAAI,IAAI,OAClC,CAAC,CAAC,OAAO,IAAI,QAAQ,IAAI,QAAQ,MAAM,IAAI,QAAQ,cAAc,IAAI,MAAM,WAAW,SAAS,IAAI,UAAU;AAE/H;AAEA,eAAe,mBAAmB,KAAqB,SAA2B,UAAoB,YAAoB,cAAqC;CAC7J,MAAM,UAAU,MAAM,IAAI,QAAQ,KAAK,UAAU;CACjD,MAAM,WAAW,MAAM,IAAI,QAAQ,KAAK,YAAY;CACpD,MAAM,UAAU;EAAE,GAAG;EAAsB,gBAAgB,IAAI,QAAQ;EAAgB,cAAc,IAAI,QAAQ;CAAa;CAC9H,MAAM,SAAS,WAAW,UAAU,SAAS,OAAO;CAEpD,MAAM,YAAY,IAAI,cAAc,IAAI,EAAE;CAC1C,MAAM,WAAW,MAAM,UAAU,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI;EACpE,SAAS,QAAQ,MAAM;EACvB,WAAW,QAAQ,MAAM;EACzB,YAAY,QAAQ,MAAM;EAC1B,iBAAiB,QAAQ,MAAM,cAAc;EAC7C,cAAc,QAAQ;EACtB,eAAe,SAAS;EACxB,gBAAgB,SAAS;EACzB,gBAAgB;CAClB,CAAC;CAED,MAAM,SAAS,OAAO,SAAS,cAAc;CAC7C,MAAM,OAAO,SAAS,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI,QAAQ,MAAM,IAAI,QAAQ,YAAY;CAC1F,IAAI,CAAC,OAAO,UAAU,OAAO,WAC3B,MAAM,IAAI,QAAQ,MAAM,MAAM,OAAO,SAAS;CAEhD,MAAM,UAAU,OAAO,SAAS,IAAI;EAClC;EACA,UAAU,CAAC,OAAO,UAAU,OAAO,YAAY,OAAO;EACtD,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,YAAY,OAAO;CACrB,CAAC;AACH;AAEA,eAAe,SAAS,KAAqB,UAA+B,gBAAoD;CAC9H,MAAM,SAAS,IAAI,WAAW,IAAI,EAAE;CACpC,MAAM,QAAQ,MAAM,OAAO,aAAa,IAAI,MAAM,EAAE;CACpD,MAAM,cAAc,SAAS,OAAO;CACpC,IAAI,SAAsB;CAC1B,IAAI,eAAe,OAAO,GACxB,SAAS;MACJ,IAAI,eAAe,MAAM,iBAAiB,GAC/C,SAAS;CAEX,MAAM,OAAO,UAAU,IAAI,MAAM,IAAI,MAAM;CAE3C,IAAI,IAAI,MAAM,aAAa,aAEzB,MAAM,IADgB,cAAc,IAAI,IAAI,IAAI,OAClC,CAAC,CAAC,cAAc,IAAI,QAAQ,IAAI,IAAI,IAAI,QAAQ,CAAC;AAEnE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { CaptureJob, CaptureQueue, QueueEntry } from "../adapters/capture-queue.mjs";
|
|
4
|
+
import { Logger } from "pino";
|
|
5
|
+
//#region src/capture/queue.d.ts
|
|
6
|
+
interface InMemoryCaptureQueueOptions {
|
|
7
|
+
concurrency: number;
|
|
8
|
+
runJob: (job: CaptureJob) => Promise<void>;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
}
|
|
11
|
+
declare class InMemoryCaptureQueue implements CaptureQueue {
|
|
12
|
+
private readonly options;
|
|
13
|
+
readonly metadata: {
|
|
14
|
+
readonly name: "In-Memory Queue";
|
|
15
|
+
readonly version: string;
|
|
16
|
+
readonly description: "In-process capture queue";
|
|
17
|
+
readonly kind: "memory";
|
|
18
|
+
};
|
|
19
|
+
private readonly entries;
|
|
20
|
+
private running;
|
|
21
|
+
private readonly waiting;
|
|
22
|
+
private readonly inFlight;
|
|
23
|
+
constructor(options: InMemoryCaptureQueueOptions);
|
|
24
|
+
enqueue(job: CaptureJob): Promise<void>;
|
|
25
|
+
status(buildId: string): Promise<QueueEntry | null>;
|
|
26
|
+
active(): Promise<QueueEntry[]>;
|
|
27
|
+
recent(limit: number): Promise<QueueEntry[]>;
|
|
28
|
+
private process;
|
|
29
|
+
private log;
|
|
30
|
+
private track;
|
|
31
|
+
private acquire;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { InMemoryCaptureQueue, InMemoryCaptureQueueOptions };
|
|
35
|
+
//# sourceMappingURL=queue.d.mts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/capture/queue.ts
|
|
5
|
+
var InMemoryCaptureQueue = class {
|
|
6
|
+
options;
|
|
7
|
+
metadata = {
|
|
8
|
+
name: "In-Memory Queue",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
description: "In-process capture queue",
|
|
11
|
+
kind: "memory"
|
|
12
|
+
};
|
|
13
|
+
entries = /* @__PURE__ */ new Map();
|
|
14
|
+
running = 0;
|
|
15
|
+
waiting = [];
|
|
16
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
async enqueue(job) {
|
|
21
|
+
const entry = this.track(job.buildId);
|
|
22
|
+
this.log(job)?.info("capture queued");
|
|
23
|
+
this.inFlight.set(job.buildId, this.process(job, entry));
|
|
24
|
+
await Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
async status(buildId) {
|
|
27
|
+
return await Promise.resolve(this.entries.get(buildId) ?? null);
|
|
28
|
+
}
|
|
29
|
+
async active() {
|
|
30
|
+
return await Promise.resolve([...this.entries.values()].filter((entry) => entry.status === "queued" || entry.status === "running").toSorted((a, b) => a.queuedAt.localeCompare(b.queuedAt)));
|
|
31
|
+
}
|
|
32
|
+
async recent(limit) {
|
|
33
|
+
return await Promise.resolve([...this.entries.values()].toSorted((a, b) => b.queuedAt.localeCompare(a.queuedAt)).slice(0, limit));
|
|
34
|
+
}
|
|
35
|
+
async process(job, entry) {
|
|
36
|
+
const log = this.log(job);
|
|
37
|
+
await this.acquire();
|
|
38
|
+
entry.status = "running";
|
|
39
|
+
entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
40
|
+
log?.info("capture started");
|
|
41
|
+
try {
|
|
42
|
+
await this.options.runJob(job);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
entry.status = "failed";
|
|
45
|
+
entry.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
46
|
+
entry.error = error instanceof Error ? error.message : "Capture failed";
|
|
47
|
+
log?.error({ err: error }, "capture failed");
|
|
48
|
+
} finally {
|
|
49
|
+
this.running -= 1;
|
|
50
|
+
this.waiting.shift()?.();
|
|
51
|
+
if (entry.status !== "failed") {
|
|
52
|
+
entry.status = "completed";
|
|
53
|
+
entry.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
54
|
+
log?.info("capture completed");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
log(job) {
|
|
59
|
+
return this.options.logger?.child({
|
|
60
|
+
buildId: job.buildId,
|
|
61
|
+
reqId: job.reqId
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
track(buildId) {
|
|
65
|
+
const existing = this.entries.get(buildId);
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.status = "queued";
|
|
68
|
+
existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
69
|
+
existing.startedAt = void 0;
|
|
70
|
+
existing.finishedAt = void 0;
|
|
71
|
+
existing.error = void 0;
|
|
72
|
+
return existing;
|
|
73
|
+
}
|
|
74
|
+
const entry = {
|
|
75
|
+
buildId,
|
|
76
|
+
status: "queued",
|
|
77
|
+
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
78
|
+
};
|
|
79
|
+
this.entries.set(buildId, entry);
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
async acquire() {
|
|
83
|
+
if (this.running < this.options.concurrency) {
|
|
84
|
+
this.running += 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
this.waiting.push(resolve);
|
|
89
|
+
});
|
|
90
|
+
await this.acquire();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
export { InMemoryCaptureQueue };
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=queue.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.mjs","names":[],"sources":["../../src/capture/queue.ts"],"sourcesContent":["import type { Logger } from \"pino\";\n\nimport type { CaptureJob, CaptureQueue, QueueEntry } from \"../adapters/capture-queue.ts\";\n\ndeclare const __PKG_VERSION__: string;\n\nexport interface InMemoryCaptureQueueOptions {\n concurrency: number;\n runJob: (job: CaptureJob) => Promise<void>;\n logger?: Logger;\n}\nexport class InMemoryCaptureQueue implements CaptureQueue {\n readonly metadata = {\n name: \"In-Memory Queue\",\n version: typeof __PKG_VERSION__ === \"undefined\" ? \"0.0.0\" : __PKG_VERSION__, // oxlint-disable-line unicorn/no-typeof-undefined\n description: \"In-process capture queue\",\n kind: \"memory\",\n } as const;\n\n private readonly entries = new Map<string, QueueEntry>();\n private running = 0;\n private readonly waiting: (() => void)[] = [];\n private readonly inFlight = new Map<string, Promise<void>>();\n\n constructor(private readonly options: InMemoryCaptureQueueOptions) {}\n\n async enqueue(job: CaptureJob): Promise<void> {\n const entry = this.track(job.buildId);\n this.log(job)?.info(\"capture queued\");\n this.inFlight.set(job.buildId, this.process(job, entry));\n await Promise.resolve();\n }\n\n async status(buildId: string): Promise<QueueEntry | null> {\n return await Promise.resolve(this.entries.get(buildId) ?? null);\n }\n\n async active(): Promise<QueueEntry[]> {\n return await Promise.resolve([...this.entries.values()]\n .filter((entry) => entry.status === \"queued\" || entry.status === \"running\")\n .toSorted((a, b) => a.queuedAt.localeCompare(b.queuedAt)));\n }\n\n async recent(limit: number): Promise<QueueEntry[]> {\n return await Promise.resolve([...this.entries.values()].toSorted((a, b) => b.queuedAt.localeCompare(a.queuedAt)).slice(0, limit));\n }\n\n private async process(job: CaptureJob, entry: QueueEntry): Promise<void> {\n const log = this.log(job);\n await this.acquire();\n entry.status = \"running\";\n entry.startedAt = new Date().toISOString();\n log?.info(\"capture started\");\n try {\n await this.options.runJob(job);\n } catch (error) {\n entry.status = \"failed\";\n entry.finishedAt = new Date().toISOString();\n entry.error = error instanceof Error ? error.message : \"Capture failed\";\n log?.error({ err: error }, \"capture failed\");\n } finally {\n this.running -= 1;\n this.waiting.shift()?.();\n if (entry.status !== \"failed\") {\n entry.status = \"completed\";\n entry.finishedAt = new Date().toISOString();\n log?.info(\"capture completed\");\n }\n }\n }\n\n private log(job: CaptureJob): Logger | undefined {\n return this.options.logger?.child({ buildId: job.buildId, reqId: job.reqId });\n }\n\n private track(buildId: string): QueueEntry {\n const existing = this.entries.get(buildId);\n if (existing) {\n existing.status = \"queued\";\n existing.queuedAt = new Date().toISOString();\n existing.startedAt = undefined;\n existing.finishedAt = undefined;\n existing.error = undefined;\n return existing;\n }\n const entry: QueueEntry = { buildId, status: \"queued\", queuedAt: new Date().toISOString() };\n this.entries.set(buildId, entry);\n return entry;\n }\n\n private async acquire(): Promise<void> {\n if (this.running < this.options.concurrency) {\n this.running += 1;\n return;\n }\n await new Promise<void>((resolve) => {\n this.waiting.push(resolve);\n });\n await this.acquire();\n }\n}"],"mappings":";;;;AAWA,IAAa,uBAAb,MAA0D;CAa3B;CAZ7B,WAAoB;EAClB,MAAM;EACN,SAAA;EACA,aAAa;EACb,MAAM;CACR;CAEA,0BAA2B,IAAI,IAAwB;CACvD,UAAkB;CAClB,UAA2C,CAAC;CAC5C,2BAA4B,IAAI,IAA2B;CAE3D,YAAY,SAAuD;EAAtC,KAAA,UAAA;CAAuC;CAEpE,MAAM,QAAQ,KAAgC;EAC5C,MAAM,QAAQ,KAAK,MAAM,IAAI,OAAO;EACpC,KAAK,IAAI,GAAG,CAAC,EAAE,KAAK,gBAAgB;EACpC,KAAK,SAAS,IAAI,IAAI,SAAS,KAAK,QAAQ,KAAK,KAAK,CAAC;EACvD,MAAM,QAAQ,QAAQ;CACxB;CAEA,MAAM,OAAO,SAA6C;EACxD,OAAO,MAAM,QAAQ,QAAQ,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;CAChE;CAEA,MAAM,SAAgC;EACpC,OAAO,MAAM,QAAQ,QAAQ,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,CACpD,QAAQ,UAAU,MAAM,WAAW,YAAY,MAAM,WAAW,SAAS,CAAC,CAC1E,UAAU,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,QAAQ,CAAC,CAAC;CAC7D;CAEA,MAAM,OAAO,OAAsC;EACjD,OAAO,MAAM,QAAQ,QAAQ,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,CAAC,UAAU,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC;CAClI;CAEA,MAAc,QAAQ,KAAiB,OAAkC;EACvE,MAAM,MAAM,KAAK,IAAI,GAAG;EACxB,MAAM,KAAK,QAAQ;EACnB,MAAM,SAAS;EACf,MAAM,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;EACzC,KAAK,KAAK,iBAAiB;EAC3B,IAAI;GACF,MAAM,KAAK,QAAQ,OAAO,GAAG;EAC/B,SAAS,OAAO;GACd,MAAM,SAAS;GACf,MAAM,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;GAC1C,MAAM,QAAQ,iBAAiB,QAAQ,MAAM,UAAU;GACvD,KAAK,MAAM,EAAE,KAAK,MAAM,GAAG,gBAAgB;EAC7C,UAAU;GACR,KAAK,WAAW;GAChB,KAAK,QAAQ,MAAM,CAAC,GAAG;GACvB,IAAI,MAAM,WAAW,UAAU;IAC7B,MAAM,SAAS;IACf,MAAM,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;IAC1C,KAAK,KAAK,mBAAmB;GAC/B;EACF;CACF;CAEA,IAAY,KAAqC;EAC/C,OAAO,KAAK,QAAQ,QAAQ,MAAM;GAAE,SAAS,IAAI;GAAS,OAAO,IAAI;EAAM,CAAC;CAC9E;CAEA,MAAc,SAA6B;EACzC,MAAM,WAAW,KAAK,QAAQ,IAAI,OAAO;EACzC,IAAI,UAAU;GACZ,SAAS,SAAS;GAClB,SAAS,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;GAC3C,SAAS,YAAY,KAAA;GACrB,SAAS,aAAa,KAAA;GACtB,SAAS,QAAQ,KAAA;GACjB,OAAO;EACT;EACA,MAAM,QAAoB;GAAE;GAAS,QAAQ;GAAU,2BAAU,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE;EAC1F,KAAK,QAAQ,IAAI,SAAS,KAAK;EAC/B,OAAO;CACT;CAEA,MAAc,UAAyB;EACrC,IAAI,KAAK,UAAU,KAAK,QAAQ,aAAa;GAC3C,KAAK,WAAW;GAChB;EACF;EACA,MAAM,IAAI,SAAe,YAAY;GACnC,KAAK,QAAQ,KAAK,OAAO;EAC3B,CAAC;EACD,MAAM,KAAK,QAAQ;CACrB;AACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { StatusConfigModel } from "../models/status-config.mjs";
|
|
5
|
+
import { buildCommentMarkdown } from "../adapter/git-host/helpers.mjs";
|
|
6
|
+
//#region src/capture/status-fanout.ts
|
|
7
|
+
async function postStatusesForBuild(opts) {
|
|
8
|
+
if (opts.providers.length === 0) return;
|
|
9
|
+
const model = new StatusConfigModel(opts.db, opts.secret);
|
|
10
|
+
const rows = await model.list(opts.project.id);
|
|
11
|
+
const ctx = `storyshelf/${opts.project.slug}`;
|
|
12
|
+
await Promise.allSettled(rows.map(async (row) => {
|
|
13
|
+
const provider = opts.providers.find((p) => p.metadata.kind === row.provider);
|
|
14
|
+
if (!provider) {
|
|
15
|
+
opts.logger?.warn({ provider: row.provider }, "no provider registered for status config");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(row.config);
|
|
21
|
+
provider.metadata.schema.parse(parsed);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
opts.logger?.warn({
|
|
24
|
+
err: error,
|
|
25
|
+
provider: row.provider
|
|
26
|
+
}, "invalid status config");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let token;
|
|
30
|
+
try {
|
|
31
|
+
token = model.decryptToken(row);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
opts.logger?.warn({ err: error }, "failed to decrypt status token");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const adapter = provider.create({
|
|
37
|
+
config: parsed,
|
|
38
|
+
token,
|
|
39
|
+
logger: opts.logger
|
|
40
|
+
});
|
|
41
|
+
await adapter.setStatus({
|
|
42
|
+
context: ctx,
|
|
43
|
+
gitSha: opts.sha,
|
|
44
|
+
status: opts.status,
|
|
45
|
+
url: opts.url
|
|
46
|
+
});
|
|
47
|
+
if (adapter.upsertComment) {
|
|
48
|
+
const markdown = buildCommentMarkdown(opts.status, opts.url, ctx);
|
|
49
|
+
await adapter.upsertComment({
|
|
50
|
+
sha: opts.sha,
|
|
51
|
+
url: opts.url,
|
|
52
|
+
status: opts.status,
|
|
53
|
+
markdown
|
|
54
|
+
}).catch((error) => {
|
|
55
|
+
opts.logger?.warn({
|
|
56
|
+
err: error,
|
|
57
|
+
provider: row.provider
|
|
58
|
+
}, "failed to upsert comment");
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { postStatusesForBuild };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=status-fanout.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-fanout.mjs","names":[],"sources":["../../src/capture/status-fanout.ts"],"sourcesContent":["import type { Logger } from \"pino\";\n\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport type { CheckStatus, GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport { buildCommentMarkdown } from \"../adapters/git-host/helpers.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { Project } from \"../schema.ts\";\n\nasync function postStatusesForBuild(opts: {\n db: DatabaseAdapter;\n project: Project;\n sha: string;\n status: CheckStatus;\n url: string;\n providers: GitHostProvider[];\n secret: string | undefined;\n logger?: Logger;\n}): Promise<void> {\n if (opts.providers.length === 0) {\n return;\n }\n const model = new StatusConfigModel(opts.db, opts.secret);\n const rows = await model.list(opts.project.id);\n const ctx = `storyshelf/${opts.project.slug}`;\n await Promise.allSettled(\n rows.map(async (row) => {\n const provider = opts.providers.find((p) => p.metadata.kind === row.provider);\n if (!provider) {\n opts.logger?.warn({ provider: row.provider }, \"no provider registered for status config\");\n return;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(row.config);\n provider.metadata.schema.parse(parsed);\n } catch (error: unknown) {\n opts.logger?.warn({ err: error, provider: row.provider }, \"invalid status config\");\n return;\n }\n let token: string;\n try {\n token = model.decryptToken(row);\n } catch (error: unknown) {\n opts.logger?.warn({ err: error }, \"failed to decrypt status token\");\n return;\n }\n const adapter = provider.create({ config: parsed, token, logger: opts.logger });\n await adapter.setStatus({ context: ctx, gitSha: opts.sha, status: opts.status, url: opts.url });\n if (adapter.upsertComment) {\n const markdown = buildCommentMarkdown(opts.status, opts.url, ctx);\n await adapter\n .upsertComment({ sha: opts.sha, url: opts.url, status: opts.status, markdown })\n .catch((error: unknown) => {\n opts.logger?.warn({ err: error, provider: row.provider }, \"failed to upsert comment\");\n });\n }\n }),\n );\n}\n\nexport { postStatusesForBuild };\n"],"mappings":";;;;;;AAQA,eAAe,qBAAqB,MASlB;CAChB,IAAI,KAAK,UAAU,WAAW,GAC5B;CAEF,MAAM,QAAQ,IAAI,kBAAkB,KAAK,IAAI,KAAK,MAAM;CACxD,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK,QAAQ,EAAE;CAC7C,MAAM,MAAM,cAAc,KAAK,QAAQ;CACvC,MAAM,QAAQ,WACZ,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,WAAW,KAAK,UAAU,MAAM,MAAM,EAAE,SAAS,SAAS,IAAI,QAAQ;EAC5E,IAAI,CAAC,UAAU;GACb,KAAK,QAAQ,KAAK,EAAE,UAAU,IAAI,SAAS,GAAG,0CAA0C;GACxF;EACF;EACA,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,IAAI,MAAM;GAC9B,SAAS,SAAS,OAAO,MAAM,MAAM;EACvC,SAAS,OAAgB;GACvB,KAAK,QAAQ,KAAK;IAAE,KAAK;IAAO,UAAU,IAAI;GAAS,GAAG,uBAAuB;GACjF;EACF;EACA,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,aAAa,GAAG;EAChC,SAAS,OAAgB;GACvB,KAAK,QAAQ,KAAK,EAAE,KAAK,MAAM,GAAG,gCAAgC;GAClE;EACF;EACA,MAAM,UAAU,SAAS,OAAO;GAAE,QAAQ;GAAQ;GAAO,QAAQ,KAAK;EAAO,CAAC;EAC9E,MAAM,QAAQ,UAAU;GAAE,SAAS;GAAK,QAAQ,KAAK;GAAK,QAAQ,KAAK;GAAQ,KAAK,KAAK;EAAI,CAAC;EAC9F,IAAI,QAAQ,eAAe;GACzB,MAAM,WAAW,qBAAqB,KAAK,QAAQ,KAAK,KAAK,GAAG;GAChE,MAAM,QACH,cAAc;IAAE,KAAK,KAAK;IAAK,KAAK,KAAK;IAAK,QAAQ,KAAK;IAAQ;GAAS,CAAC,CAAC,CAC9E,OAAO,UAAmB;IACzB,KAAK,QAAQ,KAAK;KAAE,KAAK;KAAO,UAAU,IAAI;IAAS,GAAG,0BAA0B;GACtF,CAAC;EACL;CACF,CAAC,CACH;AACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { StoryEntry, StorySourceAdapter } from "./adapter.mjs";
|
|
4
|
+
//#region src/capture/storybook.d.ts
|
|
5
|
+
declare class StorybookAdapter implements StorySourceAdapter {
|
|
6
|
+
readonly name = "storybook";
|
|
7
|
+
readonly screenshotSelector = "#storybook-root";
|
|
8
|
+
discover(source: string): Promise<StoryEntry[]>;
|
|
9
|
+
buildUrl(baseUrl: string, storyId: string): string;
|
|
10
|
+
private readIndex;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { StorybookAdapter };
|
|
14
|
+
//# sourceMappingURL=storybook.d.mts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
//#region src/capture/storybook.ts
|
|
7
|
+
var StorybookAdapter = class {
|
|
8
|
+
name = "storybook";
|
|
9
|
+
screenshotSelector = "#storybook-root";
|
|
10
|
+
async discover(source) {
|
|
11
|
+
const index = await this.readIndex(source);
|
|
12
|
+
return Object.values(index.entries).filter((entry) => entry.type !== "docs").map((entry) => ({
|
|
13
|
+
id: entry.id,
|
|
14
|
+
title: entry.title,
|
|
15
|
+
name: entry.name,
|
|
16
|
+
importPath: entry.importPath,
|
|
17
|
+
tags: entry.tags,
|
|
18
|
+
type: entry.type === "docs" ? "docs" : "story"
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
buildUrl(baseUrl, storyId) {
|
|
22
|
+
return `${baseUrl}/iframe.html?id=${encodeURIComponent(storyId)}&viewMode=story`;
|
|
23
|
+
}
|
|
24
|
+
async readIndex(source) {
|
|
25
|
+
const found = (await Promise.all(["index.json", "stories.json"].map(async (name) => {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await readFile(join(source, name), "utf8");
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}))).find((index) => index !== null);
|
|
33
|
+
if (found) return found;
|
|
34
|
+
throw new Error(`No Storybook index found in ${source}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { StorybookAdapter };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=storybook.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/capture/storybook.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport type { StoryEntry, StorySourceAdapter } from \"./adapter.ts\";\n\ninterface StorybookIndex {\n v: number;\n entries: Record<string, { id: string; name: string; title: string; importPath?: string; tags?: string[]; type: string }>;\n}\n\nexport class StorybookAdapter implements StorySourceAdapter {\n readonly name = \"storybook\";\n readonly screenshotSelector = \"#storybook-root\";\n\n async discover(source: string): Promise<StoryEntry[]> {\n const index = await this.readIndex(source);\n return Object.values(index.entries)\n .filter((entry) => entry.type !== \"docs\")\n .map((entry) => ({\n id: entry.id,\n title: entry.title,\n name: entry.name,\n importPath: entry.importPath,\n tags: entry.tags,\n type: entry.type === \"docs\" ? \"docs\" : \"story\",\n }));\n }\n\n // eslint-disable-next-line class-methods-use-this\n buildUrl(baseUrl: string, storyId: string): string {\n return `${baseUrl}/iframe.html?id=${encodeURIComponent(storyId)}&viewMode=story`;\n }\n\n // Private helper; invoked via `this` from discover.\n // eslint-disable-next-line class-methods-use-this\n private async readIndex(source: string): Promise<StorybookIndex> {\n const candidates = [\"index.json\", \"stories.json\"];\n const results = await Promise.all(\n candidates.map(async (name) => {\n try {\n const raw = await readFile(join(source, name), \"utf8\");\n return JSON.parse(raw) as StorybookIndex;\n } catch {\n return null;\n }\n }),\n );\n const found = results.find((index) => index !== null);\n if (found) {\n return found;\n }\n throw new Error(`No Storybook index found in ${source}`);\n }\n}\n"],"mappings":";;;;;;AAUA,IAAa,mBAAb,MAA4D;CAC1D,OAAgB;CAChB,qBAA8B;CAE9B,MAAM,SAAS,QAAuC;EACpD,MAAM,QAAQ,MAAM,KAAK,UAAU,MAAM;EACzC,OAAO,OAAO,OAAO,MAAM,OAAO,CAAC,CAChC,QAAQ,UAAU,MAAM,SAAS,MAAM,CAAC,CACxC,KAAK,WAAW;GACf,IAAI,MAAM;GACV,OAAO,MAAM;GACb,MAAM,MAAM;GACZ,YAAY,MAAM;GAClB,MAAM,MAAM;GACZ,MAAM,MAAM,SAAS,SAAS,SAAS;EACzC,EAAE;CACN;CAGA,SAAS,SAAiB,SAAyB;EACjD,OAAO,GAAG,QAAQ,kBAAkB,mBAAmB,OAAO,EAAE;CAClE;CAIA,MAAc,UAAU,QAAyC;EAY/D,MAAM,SAAQ,MAVQ,QAAQ,IAC5B,CAFkB,cAAc,cAEvB,CAAC,CAAC,IAAI,OAAO,SAAS;GAC7B,IAAI;IACF,MAAM,MAAM,MAAM,SAAS,KAAK,QAAQ,IAAI,GAAG,MAAM;IACrD,OAAO,KAAK,MAAM,GAAG;GACvB,QAAQ;IACN,OAAO;GACT;EACF,CAAC,CACH,EAAA,CACsB,MAAM,UAAU,UAAU,IAAI;EACpD,IAAI,OACF,OAAO;EAET,MAAM,IAAI,MAAM,+BAA+B,QAAQ;CACzD;AACF"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { AuthAdapter } from "./adapter/auth.mjs";
|
|
4
|
+
import { CaptureQueue } from "./adapters/capture-queue.mjs";
|
|
5
|
+
import { CaptureRunner } from "./adapter/capture-runner.mjs";
|
|
6
|
+
import { DatabaseAdapter } from "./adapter/database.mjs";
|
|
7
|
+
import { GitHostProvider } from "./adapter/git-host.mjs";
|
|
8
|
+
import { StorageAdapter } from "./adapter/storage.mjs";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { Logger } from "pino";
|
|
11
|
+
//#region src/config.d.ts
|
|
12
|
+
interface BrandTheme {
|
|
13
|
+
accent: string;
|
|
14
|
+
surface: {
|
|
15
|
+
base: string;
|
|
16
|
+
card: string;
|
|
17
|
+
};
|
|
18
|
+
text: {
|
|
19
|
+
primary: string;
|
|
20
|
+
secondary: string;
|
|
21
|
+
};
|
|
22
|
+
border: string;
|
|
23
|
+
status: {
|
|
24
|
+
approved: string;
|
|
25
|
+
new: string;
|
|
26
|
+
rejected: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface UIConfig {
|
|
30
|
+
name?: string;
|
|
31
|
+
logo?: string;
|
|
32
|
+
favicon?: string;
|
|
33
|
+
lightTheme?: BrandTheme;
|
|
34
|
+
darkTheme?: BrandTheme;
|
|
35
|
+
}
|
|
36
|
+
declare const shelfConfigSchema: z.ZodObject<{
|
|
37
|
+
secret: z.ZodOptional<z.ZodString>;
|
|
38
|
+
publishedBaseDomain: z.ZodOptional<z.ZodString>;
|
|
39
|
+
captureConcurrency: z.ZodOptional<z.ZodNumber>;
|
|
40
|
+
scratchDir: z.ZodOptional<z.ZodString>;
|
|
41
|
+
purgeTtlDays: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
viewports: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
43
|
+
name: z.ZodString;
|
|
44
|
+
width: z.ZodNumber;
|
|
45
|
+
height: z.ZodNumber;
|
|
46
|
+
}, z.core.$strip>>>;
|
|
47
|
+
adapters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
48
|
+
name: z.ZodString;
|
|
49
|
+
version: z.ZodString;
|
|
50
|
+
description: z.ZodOptional<z.ZodString>;
|
|
51
|
+
kind: z.ZodString;
|
|
52
|
+
}, z.core.$strip>>>;
|
|
53
|
+
}, z.core.$strict>;
|
|
54
|
+
type ShelfConfig = z.infer<typeof shelfConfigSchema>;
|
|
55
|
+
interface ShelfOptions {
|
|
56
|
+
database: DatabaseAdapter;
|
|
57
|
+
storage: StorageAdapter;
|
|
58
|
+
captureRunner?: CaptureRunner;
|
|
59
|
+
captureQueue?: CaptureQueue;
|
|
60
|
+
auth?: AuthAdapter;
|
|
61
|
+
gitHosts?: GitHostProvider[];
|
|
62
|
+
logger?: Logger;
|
|
63
|
+
ui?: UIConfig;
|
|
64
|
+
config?: ShelfConfig;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { BrandTheme, ShelfConfig, ShelfOptions, UIConfig, shelfConfigSchema };
|
|
68
|
+
//# sourceMappingURL=config.d.mts.map
|