@storyshelf/core 0.1.0 → 0.1.2
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 +9 -8
- package/dist/adapter/capture-runner.d.mts +4 -0
- package/dist/adapter/git-host/helpers.mjs +1 -1
- package/dist/adapter/git-host/helpers.mjs.map +1 -1
- package/dist/adapter/storage.d.mts +1 -0
- package/dist/adapters/webhook-events.mjs.map +1 -1
- package/dist/capture/adapter.d.mts +9 -1
- package/dist/capture/adapter.mjs +16 -1
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/orchestrator.mjs +37 -10
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +5 -3
- package/dist/capture/pipeline.mjs +18 -6
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/queue.mjs +1 -1
- package/dist/capture/queue.mjs.map +1 -1
- package/dist/capture/storybook.mjs +11 -3
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/config.mjs +2 -2
- package/dist/config.mjs.map +1 -1
- package/dist/ddl.d.mts +1 -1
- package/dist/ddl.mjs +2 -0
- package/dist/ddl.mjs.map +1 -1
- package/dist/diff/engine.mjs +2 -2
- package/dist/diff/engine.mjs.map +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/logger.d.mts +3 -3
- package/dist/logger.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -1
- package/dist/middleware/csrf.mjs.map +1 -1
- package/dist/models/build.d.mts +16 -1
- package/dist/models/build.mjs +19 -1
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/label.d.mts +6 -0
- package/dist/models/label.mjs +11 -0
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/project.mjs +1 -1
- package/dist/models/project.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +1 -1
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/label-detail.mjs +227 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +32 -42
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-members.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +148 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs.map +1 -1
- package/dist/pages/settings-webhooks.mjs.map +1 -1
- package/dist/pages/storybook.mjs +59 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +5 -3
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +4 -2
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/labels.mjs +33 -1
- package/dist/routers/labels.mjs.map +1 -1
- package/dist/routers/schemas.mjs +15 -4
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/settings.mjs +20 -0
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/storybook.mjs +112 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/ui.mjs +24 -2
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/schema.d.mts +68 -0
- package/dist/schema.mjs +2 -0
- package/dist/schema.mjs.map +1 -1
- package/dist/types.d.mts +2 -1
- package/dist/types.mjs.map +1 -1
- package/dist/ui/components.mjs.map +1 -1
- package/dist/ui/document.mjs +9 -0
- package/dist/ui/document.mjs.map +1 -1
- package/dist/utils/ulid.mjs +3 -3
- package/dist/utils/ulid.mjs.map +1 -1
- package/package.json +76 -67
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siddhant Gupta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -22,10 +22,10 @@ import { createShelfRouter } from "@storyshelf/core";
|
|
|
22
22
|
const app = createShelfRouter({
|
|
23
23
|
database, // DatabaseAdapter
|
|
24
24
|
storage, // StorageAdapter
|
|
25
|
-
|
|
26
|
-
auth,
|
|
27
|
-
|
|
28
|
-
logger,
|
|
25
|
+
captureRunner, // CaptureRunner (optional)
|
|
26
|
+
auth, // AuthAdapter (optional)
|
|
27
|
+
gitHosts, // GitHostProvider[] (optional)
|
|
28
|
+
logger, // pino Logger (optional; built internally if omitted)
|
|
29
29
|
ui: { name: "My Shelf" }, // UIConfig (optional)
|
|
30
30
|
config: { captureConcurrency: 2, purgeTtlDays: 30 }, // ShelfConfig (optional)
|
|
31
31
|
});
|
|
@@ -44,9 +44,10 @@ Assembles the router from the provided adapters. `ShelfOptions`:
|
|
|
44
44
|
| ------ | ---- | ----------- |
|
|
45
45
|
| `database` | `DatabaseAdapter` | **Required.** Data access. |
|
|
46
46
|
| `storage` | `StorageAdapter` | **Required.** Blob storage for screenshots, diffs, storybook archives. |
|
|
47
|
-
| `
|
|
47
|
+
| `captureRunner` | `CaptureRunner` | Optional. Enables the async capture pipeline (pure renderer). |
|
|
48
|
+
| `captureQueue` | `CaptureQueue` | Optional. Queue adapter; defaults to `InMemoryCaptureQueue`. |
|
|
48
49
|
| `auth` | `AuthAdapter` | Optional. Enables auth and the login UI. |
|
|
49
|
-
| `
|
|
50
|
+
| `gitHosts` | `GitHostProvider[]` | Optional. Git-host adapters (GitHub/GitLab) for status checks, merge gates, PR comments. |
|
|
50
51
|
| `logger` | `Logger` (pino) | Optional. Shared logger. Construct a fallback via `createShelfLogger()`. |
|
|
51
52
|
| `ui` | `UIConfig` | Optional. UI branding. |
|
|
52
53
|
| `config` | `ShelfConfig` | Optional. Server behavior. |
|
|
@@ -83,7 +84,7 @@ All adapters are constructor-injected (no AsyncLocalStorage). See `docs/architec
|
|
|
83
84
|
- `StorageAdapter` — `read`, `write`, `delete`, `exists`, `list(prefix)`.
|
|
84
85
|
- `AuthAdapter` — `check(request)`, `createSession(user)`, `destroySession(sessionId)`, optional `handleCallback(callback)`. Also exports `AuthUser`, `AuthCallback`, and the shared `SESSION_COOKIE`.
|
|
85
86
|
- `CaptureRunner` — a **pure capture renderer**: `render(input) => RenderResult`, `cancel(buildId)`. Also exports `JobStatus`, `RenderedSnapshot`, `RenderResult`.
|
|
86
|
-
- `
|
|
87
|
+
- `GitHostProvider` / `GitHostAdapter` — set commit status checks, detect merges, and upsert PR comments. Real providers ship in `@storyshelf/git-github` and `@storyshelf/git-gitlab`. Also exports `CheckStatus`.
|
|
87
88
|
|
|
88
89
|
### Logging
|
|
89
90
|
|
|
@@ -113,7 +114,7 @@ See `docs/architecture.md` and the ADRs in `docs/adr/`.
|
|
|
113
114
|
|
|
114
115
|
## Deployment targets
|
|
115
116
|
|
|
116
|
-
The core router is runtime-agnostic (Web `Request`/`Response`, `fetch`, `crypto`, `URL`). The only Node-specific
|
|
117
|
+
The core router is runtime-agnostic (Web `Request`/`Response`, `fetch`, `crypto`, `URL`). The only Node-specific piece is the in-process `InMemoryCaptureQueue`, which suits long-lived Node servers; serverless runtimes swap in a remote `CaptureQueue` (e.g. `@storyshelf/queue-sqs`) plus a separate worker. You assemble a server for any platform — `storyshelf server init` generates a scaffold with the adapters you choose:
|
|
117
118
|
|
|
118
119
|
| Platform | Database | Storage | Capture queue | Server entry |
|
|
119
120
|
|----------|----------|---------|---------------|--------------|
|
|
@@ -61,6 +61,10 @@ interface CaptureRunner {
|
|
|
61
61
|
viewports: Viewport[];
|
|
62
62
|
/** Optional logger for render-time diagnostics. */
|
|
63
63
|
logger?: Logger;
|
|
64
|
+
/** Whether to execute Storybook play functions before screenshot. */
|
|
65
|
+
executePlay?: boolean;
|
|
66
|
+
/** Timeout for play function execution in ms. */
|
|
67
|
+
playTimeoutMs?: number;
|
|
64
68
|
}): Promise<RenderResult>;
|
|
65
69
|
/** Cancel a pending or in-flight render for a build. */
|
|
66
70
|
cancel(buildId: string): Promise<void>;
|
|
@@ -20,7 +20,7 @@ function buildCommentMarkdown(status, url, context) {
|
|
|
20
20
|
case "pending": return `Visual tests pending for \`${context}\` — [View build](${url})`;
|
|
21
21
|
case "success": return `Visual tests passed for \`${context}\` — [View build](${url})`;
|
|
22
22
|
case "failure": return `Visual changes detected for \`${context}\` — [View build](${url})`;
|
|
23
|
-
default: return `Visual tests
|
|
23
|
+
default: return `Visual tests for \`${context}\` — [View build](${url})`;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.mjs","names":[],"sources":["../../../src/adapters/git-host/helpers.ts"],"sourcesContent":["import type { CheckStatus } from \"./index.ts\";\n\n/** Human description for a check status — shared across GitHub/GitLab. */\nexport function describeStatus(status: CheckStatus): string {\n switch (status) {\n case \"pending\": {\n return \"Visual tests pending\";\n }\n case \"success\": {\n return \"Visual tests passed\";\n }\n case \"failure\": {\n return \"Visual changes detected or tests failed\";\n }\n default: {\n return \"Visual changes detected or tests failed\";\n }\n }\n}\n\n/** Markdown comment body helpers — single comment per build, idempotent via marker. */\nexport function commentMarker(url: string): string {\n return `<!-- storyshelf:${url} -->`;\n}\n\nexport function buildCommentMarkdown(status: CheckStatus, url: string, context: string): string {\n switch (status) {\n case \"pending\": {\n return `Visual tests pending for \\`${context}\\` — [View build](${url})`;\n }\n case \"success\": {\n return `Visual tests passed for \\`${context}\\` — [View build](${url})`;\n }\n case \"failure\": {\n return `Visual changes detected for \\`${context}\\` — [View build](${url})`;\n }\n default: {\n
|
|
1
|
+
{"version":3,"file":"helpers.mjs","names":[],"sources":["../../../src/adapters/git-host/helpers.ts"],"sourcesContent":["import type { CheckStatus } from \"./index.ts\";\n\n/** Human description for a check status — shared across GitHub/GitLab. */\nexport function describeStatus(status: CheckStatus): string {\n switch (status) {\n case \"pending\": {\n return \"Visual tests pending\";\n }\n case \"success\": {\n return \"Visual tests passed\";\n }\n case \"failure\": {\n return \"Visual changes detected or tests failed\";\n }\n default: {\n return \"Visual changes detected or tests failed\";\n }\n }\n}\n\n/** Markdown comment body helpers — single comment per build, idempotent via marker. */\nexport function commentMarker(url: string): string {\n return `<!-- storyshelf:${url} -->`;\n}\n\nexport function buildCommentMarkdown(status: CheckStatus, url: string, context: string): string {\n switch (status) {\n case \"pending\": {\n return `Visual tests pending for \\`${context}\\` — [View build](${url})`;\n }\n case \"success\": {\n return `Visual tests passed for \\`${context}\\` — [View build](${url})`;\n }\n case \"failure\": {\n return `Visual changes detected for \\`${context}\\` — [View build](${url})`;\n }\n default: {\n return `Visual tests for \\`${context}\\` — [View build](${url})`;\n }\n }\n}\n"],"mappings":";;;;;AAGA,SAAgB,eAAe,QAA6B;CAC1D,QAAQ,QAAR;EACE,KAAK,WACH,OAAO;EAET,KAAK,WACH,OAAO;EAET,KAAK,WACH,OAAO;EAET,SACE,OAAO;CAEX;AACF;;AAGA,SAAgB,cAAc,KAAqB;CACjD,OAAO,mBAAmB,IAAI;AAChC;AAEA,SAAgB,qBAAqB,QAAqB,KAAa,SAAyB;CAC9F,QAAQ,QAAR;EACE,KAAK,WACH,OAAO,8BAA8B,QAAQ,oBAAoB,IAAI;EAEvE,KAAK,WACH,OAAO,6BAA6B,QAAQ,oBAAoB,IAAI;EAEtE,KAAK,WACH,OAAO,iCAAiC,QAAQ,oBAAoB,IAAI;EAE1E,SACE,OAAO,sBAAsB,QAAQ,oBAAoB,IAAI;CAEjE;AACF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { AdapterMetadata } from "../adapters/metadata.mjs";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
4
5
|
//#region src/adapters/storage.d.ts
|
|
5
6
|
/** Blob storage abstraction for screenshots, diffs and Storybook archives. */
|
|
6
7
|
interface StorageAdapter {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-events.mjs","names":[],"sources":["../../src/adapters/webhook-events.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"./database.ts\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { hmacSha256 } from \"../utils/hash.ts\";\n\nexport interface WebhookEvent {\n event: string;\n projectId: string;\n data: Record<string, unknown>;\n timestamp: string;\n}\n\nasync function sendWebhook(url: string, secret: string, event: WebhookEvent): Promise<void> {\n const body = JSON.stringify(event);\n const signature = hmacSha256(secret, body);\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-StoryShelf-Event\": event.event,\n \"X-StoryShelf-Signature\": signature,\n },\n body,\n });\n}\n\nexport async function emitWebhookEvent(\n db: DatabaseAdapter,\n projectId: string,\n event: string,\n data: Record<string, unknown>,\n): Promise<void> {\n const webhookModel = new WebhookModel(db);\n const webhooks = await webhookModel.list(projectId);\n const eventPayload: WebhookEvent = {\n event,\n projectId,\n data,\n timestamp: new Date().toISOString(),\n };\n\n await Promise.allSettled(\n webhooks.map(async (webhook) => {\n const events = WebhookModel.eventsOf(webhook);\n if (events.length > 0 && !events.includes(event)) {\n return;\n }\n try {\n await sendWebhook(webhook.url, webhook.secret, eventPayload);\n } catch {\n //
|
|
1
|
+
{"version":3,"file":"webhook-events.mjs","names":[],"sources":["../../src/adapters/webhook-events.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"./database.ts\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { hmacSha256 } from \"../utils/hash.ts\";\n\nexport interface WebhookEvent {\n event: string;\n projectId: string;\n data: Record<string, unknown>;\n timestamp: string;\n}\n\nasync function sendWebhook(url: string, secret: string, event: WebhookEvent): Promise<void> {\n const body = JSON.stringify(event);\n const signature = hmacSha256(secret, body);\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-StoryShelf-Event\": event.event,\n \"X-StoryShelf-Signature\": signature,\n },\n body,\n });\n}\n\nexport async function emitWebhookEvent(\n db: DatabaseAdapter,\n projectId: string,\n event: string,\n data: Record<string, unknown>,\n): Promise<void> {\n const webhookModel = new WebhookModel(db);\n const webhooks = await webhookModel.list(projectId);\n const eventPayload: WebhookEvent = {\n event,\n projectId,\n data,\n timestamp: new Date().toISOString(),\n };\n\n await Promise.allSettled(\n webhooks.map(async (webhook) => {\n const events = WebhookModel.eventsOf(webhook);\n if (events.length > 0 && !events.includes(event)) {\n return;\n }\n try {\n await sendWebhook(webhook.url, webhook.secret, eventPayload);\n } catch {\n // Webhook delivery failures are non-fatal\n }\n }),\n );\n}\n"],"mappings":";;;;;;AAWA,eAAe,YAAY,KAAa,QAAgB,OAAoC;CAC1F,MAAM,OAAO,KAAK,UAAU,KAAK;CACjC,MAAM,YAAY,WAAW,QAAQ,IAAI;CACzC,MAAM,MAAM,KAAK;EACf,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,sBAAsB,MAAM;GAC5B,0BAA0B;EAC5B;EACA;CACF,CAAC;AACH;AAEA,eAAsB,iBACpB,IACA,WACA,OACA,MACe;CAEf,MAAM,WAAW,MAAM,IADE,aAAa,EACJ,CAAC,CAAC,KAAK,SAAS;CAClD,MAAM,eAA6B;EACjC;EACA;EACA;EACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;CACpC;CAEA,MAAM,QAAQ,WACZ,SAAS,IAAI,OAAO,YAAY;EAC9B,MAAM,SAAS,aAAa,SAAS,OAAO;EAC5C,IAAI,OAAO,SAAS,KAAK,CAAC,OAAO,SAAS,KAAK,GAC7C;EAEF,IAAI;GACF,MAAM,YAAY,QAAQ,KAAK,QAAQ,QAAQ,YAAY;EAC7D,QAAQ,CAER;CACF,CAAC,CACH;AACF"}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
//#region src/capture/adapter.d.ts
|
|
4
|
+
interface StoryParameters {
|
|
5
|
+
disableSnapshot?: boolean;
|
|
6
|
+
delay?: number;
|
|
7
|
+
diffThreshold?: number;
|
|
8
|
+
pauseAnimationAtEnd?: boolean;
|
|
9
|
+
flakyTest?: boolean;
|
|
10
|
+
}
|
|
4
11
|
/** A single discoverable story or docs entry within a Storybook. */
|
|
5
12
|
interface StoryEntry {
|
|
6
13
|
id: string;
|
|
@@ -9,6 +16,7 @@ interface StoryEntry {
|
|
|
9
16
|
importPath?: string;
|
|
10
17
|
tags?: string[];
|
|
11
18
|
type: "story" | "docs";
|
|
19
|
+
parameters?: StoryParameters;
|
|
12
20
|
}
|
|
13
21
|
/** Adapter that discovers and renders stories from a Storybook build. */
|
|
14
22
|
interface StorySourceAdapter {
|
|
@@ -25,5 +33,5 @@ interface Viewport {
|
|
|
25
33
|
}
|
|
26
34
|
declare const DEFAULT_VIEWPORTS: Viewport[];
|
|
27
35
|
//#endregion
|
|
28
|
-
export { DEFAULT_VIEWPORTS, StoryEntry, StorySourceAdapter, Viewport };
|
|
36
|
+
export { DEFAULT_VIEWPORTS, StoryEntry, StoryParameters, StorySourceAdapter, Viewport };
|
|
29
37
|
//# sourceMappingURL=adapter.d.mts.map
|
package/dist/capture/adapter.mjs
CHANGED
|
@@ -2,12 +2,27 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
//#region src/capture/adapter.ts
|
|
5
|
+
function isFlakyStory(entry) {
|
|
6
|
+
if (entry.parameters?.flakyTest) return true;
|
|
7
|
+
const tags = entry.tags ?? [];
|
|
8
|
+
for (const t of tags) if (t.toLowerCase() === "flaky-test") return true;
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
function isDisabledStory(entry) {
|
|
12
|
+
if (entry.parameters?.disableSnapshot) return true;
|
|
13
|
+
const tags = entry.tags ?? [];
|
|
14
|
+
for (const t of tags) {
|
|
15
|
+
const lower = t.toLowerCase();
|
|
16
|
+
if (lower === "skip" || lower === "disable" || lower === "disable-snapshot") return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
5
20
|
const DEFAULT_VIEWPORTS = [{
|
|
6
21
|
name: "desktop",
|
|
7
22
|
width: 1280,
|
|
8
23
|
height: 720
|
|
9
24
|
}];
|
|
10
25
|
//#endregion
|
|
11
|
-
export { DEFAULT_VIEWPORTS };
|
|
26
|
+
export { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory };
|
|
12
27
|
|
|
13
28
|
//# sourceMappingURL=adapter.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.mjs","names":[],"sources":["../../src/capture/adapter.ts"],"sourcesContent":["/** A single discoverable story or docs entry within a Storybook. */\nexport interface StoryEntry {\n id: string;\n title: string;\n name: string;\n importPath?: string;\n tags?: string[];\n type: \"story\" | \"docs\";\n}\n\n/** Adapter that discovers and renders stories from a Storybook build. */\nexport interface StorySourceAdapter {\n name: string;\n discover(source: string): Promise<StoryEntry[]>;\n buildUrl(baseUrl: string, storyId: string): string;\n screenshotSelector?: string;\n}\n\n/** A viewport size at which stories are captured. */\nexport interface Viewport {\n name: string;\n width: number;\n height: number;\n}\n\nexport const DEFAULT_VIEWPORTS: Viewport[] = [{ name: \"desktop\", width: 1280, height: 720 }];\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"adapter.mjs","names":[],"sources":["../../src/capture/adapter.ts"],"sourcesContent":["export interface StoryParameters {\n disableSnapshot?: boolean;\n delay?: number;\n diffThreshold?: number;\n pauseAnimationAtEnd?: boolean;\n flakyTest?: boolean;\n}\n\nexport function isFlakyStory(entry: Pick<StoryEntry, \"tags\" | \"parameters\">): boolean {\n if (entry.parameters?.flakyTest) return true;\n const tags = entry.tags ?? [];\n for (const t of tags) {\n if (t.toLowerCase() === \"flaky-test\") return true;\n }\n return false;\n}\n\nexport function isDisabledStory(entry: Pick<StoryEntry, \"tags\" | \"parameters\">): boolean {\n if (entry.parameters?.disableSnapshot) return true;\n const tags = entry.tags ?? [];\n for (const t of tags) {\n const lower = t.toLowerCase();\n if (lower === \"skip\" || lower === \"disable\" || lower === \"disable-snapshot\") return true;\n }\n return false;\n}\n\n/** A single discoverable story or docs entry within a Storybook. */\nexport interface StoryEntry {\n id: string;\n title: string;\n name: string;\n importPath?: string;\n tags?: string[];\n type: \"story\" | \"docs\";\n parameters?: StoryParameters;\n}\n\n/** Adapter that discovers and renders stories from a Storybook build. */\nexport interface StorySourceAdapter {\n name: string;\n discover(source: string): Promise<StoryEntry[]>;\n buildUrl(baseUrl: string, storyId: string): string;\n screenshotSelector?: string;\n}\n\n/** A viewport size at which stories are captured. */\nexport interface Viewport {\n name: string;\n width: number;\n height: number;\n}\n\nexport const DEFAULT_VIEWPORTS: Viewport[] = [{ name: \"desktop\", width: 1280, height: 720 }];\n"],"mappings":";;;;AAQA,SAAgB,aAAa,OAAyD;CACpF,IAAI,MAAM,YAAY,WAAW,OAAO;CACxC,MAAM,OAAO,MAAM,QAAQ,CAAC;CAC5B,KAAK,MAAM,KAAK,MACd,IAAI,EAAE,YAAY,MAAM,cAAc,OAAO;CAE/C,OAAO;AACT;AAEA,SAAgB,gBAAgB,OAAyD;CACvF,IAAI,MAAM,YAAY,iBAAiB,OAAO;CAC9C,MAAM,OAAO,MAAM,QAAQ,CAAC;CAC5B,KAAK,MAAM,KAAK,MAAM;EACpB,MAAM,QAAQ,EAAE,YAAY;EAC5B,IAAI,UAAU,UAAU,UAAU,aAAa,UAAU,oBAAoB,OAAO;CACtF;CACA,OAAO;AACT;AA4BA,MAAa,oBAAgC,CAAC;CAAE,MAAM;CAAW,OAAO;CAAM,QAAQ;AAAI,CAAC"}
|
|
@@ -3,12 +3,12 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { BuildModel } from "../models/build.mjs";
|
|
5
5
|
import { ProjectModel } from "../models/project.mjs";
|
|
6
|
-
import { storybookZipPath } from "../utils/paths.mjs";
|
|
6
|
+
import { storybookDir, storybookZipPath } from "../utils/paths.mjs";
|
|
7
7
|
import { persistCapture } from "./pipeline.mjs";
|
|
8
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";
|
|
9
|
+
import { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from "./adapter.mjs";
|
|
10
|
+
import { mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
11
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
12
12
|
import AdmZip from "adm-zip";
|
|
13
13
|
//#region src/capture/orchestrator.ts
|
|
14
14
|
async function executeCaptureJob(input, options) {
|
|
@@ -20,27 +20,37 @@ async function executeCaptureJob(input, options) {
|
|
|
20
20
|
});
|
|
21
21
|
await builds.setStatus(build.id, "capturing");
|
|
22
22
|
const startTime = performance.now();
|
|
23
|
-
let
|
|
23
|
+
let extractedDir;
|
|
24
24
|
try {
|
|
25
25
|
const extractStart = performance.now();
|
|
26
|
-
|
|
26
|
+
extractedDir = await extractStorybook(options, project.id, build.id);
|
|
27
27
|
const extractDuration = performance.now() - extractStart;
|
|
28
28
|
logger?.info({ durationMs: Math.round(extractDuration) }, "storybook extracted");
|
|
29
|
-
const
|
|
29
|
+
const staticsStart = performance.now();
|
|
30
|
+
await persistStorybookStatics(options.storage, extractedDir, project.id, build.id);
|
|
31
|
+
logger?.info({ durationMs: Math.round(performance.now() - staticsStart) }, "storybook statics persisted");
|
|
32
|
+
const stories = (await new StorybookAdapter().discover(extractedDir)).filter((s) => !isDisabledStory(s));
|
|
30
33
|
const viewports = options.viewports ?? DEFAULT_VIEWPORTS;
|
|
31
34
|
const renderStart = performance.now();
|
|
32
35
|
const result = await options.runner.render({
|
|
33
36
|
buildId: build.id,
|
|
34
|
-
storybookDir,
|
|
37
|
+
storybookDir: extractedDir,
|
|
35
38
|
stories,
|
|
36
39
|
viewports,
|
|
37
|
-
logger
|
|
40
|
+
logger,
|
|
41
|
+
executePlay: project.executePlay ?? false,
|
|
42
|
+
playTimeoutMs: project.playTimeoutMs ?? 1e4
|
|
38
43
|
});
|
|
39
44
|
const renderDuration = performance.now() - renderStart;
|
|
40
45
|
logger?.info({
|
|
41
46
|
durationMs: Math.round(renderDuration),
|
|
42
47
|
storyCount: stories.length
|
|
43
48
|
}, "stories rendered");
|
|
49
|
+
const flakyStoryIds = new Set(stories.filter((s) => isFlakyStory(s)).map((s) => s.id));
|
|
50
|
+
const blockingFailed = /* @__PURE__ */ new Set();
|
|
51
|
+
const flakyFailed = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const f of result.failures) if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);
|
|
53
|
+
else blockingFailed.add(f.storyId);
|
|
44
54
|
const persistStart = performance.now();
|
|
45
55
|
await persistCapture({
|
|
46
56
|
db: options.db,
|
|
@@ -50,7 +60,7 @@ async function executeCaptureJob(input, options) {
|
|
|
50
60
|
viewports,
|
|
51
61
|
captures: result.captures,
|
|
52
62
|
logger
|
|
53
|
-
},
|
|
63
|
+
}, blockingFailed, flakyFailed);
|
|
54
64
|
const persistDuration = performance.now() - persistStart;
|
|
55
65
|
logger?.info({ durationMs: Math.round(persistDuration) }, "capture persisted");
|
|
56
66
|
const totalDuration = performance.now() - startTime;
|
|
@@ -97,6 +107,23 @@ async function extractStorybook(options, projectId, buildId) {
|
|
|
97
107
|
zip.extractAllTo(targetDir, true);
|
|
98
108
|
return targetDir;
|
|
99
109
|
}
|
|
110
|
+
async function walkFiles(dir) {
|
|
111
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
112
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
113
|
+
const full = join(dir, entry.name);
|
|
114
|
+
if (entry.isDirectory()) return await walkFiles(full);
|
|
115
|
+
return [full];
|
|
116
|
+
}))).flat();
|
|
117
|
+
}
|
|
118
|
+
async function persistStorybookStatics(storage, sourceDir, projectId, buildId) {
|
|
119
|
+
const root = resolve(sourceDir);
|
|
120
|
+
const destinationPrefix = storybookDir(projectId, buildId);
|
|
121
|
+
const files = await walkFiles(root);
|
|
122
|
+
await Promise.all(files.map(async (file) => {
|
|
123
|
+
const rel = relative(root, file);
|
|
124
|
+
await storage.write(`${destinationPrefix}/${rel}`, await readFile(file));
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
100
127
|
//#endregion
|
|
101
128
|
export { executeCaptureJob };
|
|
102
129
|
|
|
@@ -1 +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
|
|
1
|
+
{"version":3,"file":"orchestrator.mjs","names":[],"sources":["../../src/capture/orchestrator.ts"],"sourcesContent":["import { mkdir, readdir, readFile, rm } from \"node:fs/promises\";\nimport { join, relative, 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 { storybookDir, storybookZipPath } from \"../utils/paths.ts\";\nimport { persistCapture } from \"./pipeline.ts\";\nimport { StorybookAdapter } from \"./storybook.ts\";\nimport { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } 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 extractedDir: string | undefined;\n try {\n const extractStart = performance.now();\n extractedDir = await extractStorybook(options, project.id, build.id);\n const extractDuration = performance.now() - extractStart;\n logger?.info({ durationMs: Math.round(extractDuration) }, \"storybook extracted\");\n\n // Persist the extracted statics to storage so the published Storybook\n // (`storybookDir`) can be served after the scratch dir is cleaned up.\n const staticsStart = performance.now();\n await persistStorybookStatics(options.storage, extractedDir, project.id, build.id);\n logger?.info({ durationMs: Math.round(performance.now() - staticsStart) }, \"storybook statics persisted\");\n\n const adapter = new StorybookAdapter();\n const discovered = await adapter.discover(extractedDir);\n const stories = discovered.filter((s) => !isDisabledStory(s));\n const viewports = options.viewports ?? DEFAULT_VIEWPORTS;\n\n const renderStart = performance.now();\n const result = await options.runner.render({\n buildId: build.id,\n storybookDir: extractedDir,\n stories,\n viewports,\n logger,\n executePlay: project.executePlay ?? false,\n playTimeoutMs: project.playTimeoutMs ?? 10_000,\n });\n const renderDuration = performance.now() - renderStart;\n logger?.info({ durationMs: Math.round(renderDuration), storyCount: stories.length }, \"stories rendered\");\n\n const flakyStoryIds = new Set(stories.filter((s) => isFlakyStory(s)).map((s) => s.id));\n const blockingFailed = new Set<string>();\n const flakyFailed = new Set<string>();\n for (const f of result.failures) {\n if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);\n else blockingFailed.add(f.storyId);\n }\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 blockingFailed,\n flakyFailed,\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\nasync function walkFiles(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const nested = await Promise.all(\n entries.map(async (entry) => {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n return await walkFiles(full);\n }\n return [full];\n }),\n );\n return nested.flat();\n}\n\nasync function persistStorybookStatics(storage: StorageAdapter, sourceDir: string, projectId: string, buildId: string): Promise<void> {\n const root = resolve(sourceDir);\n const destinationPrefix = storybookDir(projectId, buildId);\n const files = await walkFiles(root);\n await Promise.all(\n files.map(async (file) => {\n const rel = relative(root, file);\n await storage.write(`${destinationPrefix}/${rel}`, await readFile(file));\n }),\n );\n}\n"],"mappings":"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"}
|
|
@@ -32,10 +32,12 @@ interface CaptureContext {
|
|
|
32
32
|
* storage writes and record keeping, so it can run against any renderer.
|
|
33
33
|
*
|
|
34
34
|
* @param ctx - Capture context.
|
|
35
|
-
* @param renderFailedStoryIds - Story ids that the renderer could not capture
|
|
36
|
-
*
|
|
35
|
+
* @param renderFailedStoryIds - Story ids that the renderer could not capture
|
|
36
|
+
* (blocking failures). They force the build to `failed`.
|
|
37
|
+
* @param flakyFailedStoryIds - Story ids that failed but are marked flaky
|
|
38
|
+
* (non-blocking). They are logged as warnings and do not block the build.
|
|
37
39
|
*/
|
|
38
|
-
declare function persistCapture(ctx: CaptureContext, renderFailedStoryIds?: ReadonlySet<string>): Promise<void>;
|
|
40
|
+
declare function persistCapture(ctx: CaptureContext, renderFailedStoryIds?: ReadonlySet<string>, flakyFailedStoryIds?: ReadonlySet<string>): Promise<void>;
|
|
39
41
|
//#endregion
|
|
40
42
|
export { CaptureContext, persistCapture };
|
|
41
43
|
//# sourceMappingURL=pipeline.d.mts.map
|
|
@@ -16,16 +16,26 @@ import { DEFAULT_DIFF_OPTIONS } from "../diff/options.mjs";
|
|
|
16
16
|
* storage writes and record keeping, so it can run against any renderer.
|
|
17
17
|
*
|
|
18
18
|
* @param ctx - Capture context.
|
|
19
|
-
* @param renderFailedStoryIds - Story ids that the renderer could not capture
|
|
20
|
-
*
|
|
19
|
+
* @param renderFailedStoryIds - Story ids that the renderer could not capture
|
|
20
|
+
* (blocking failures). They force the build to `failed`.
|
|
21
|
+
* @param flakyFailedStoryIds - Story ids that failed but are marked flaky
|
|
22
|
+
* (non-blocking). They are logged as warnings and do not block the build.
|
|
21
23
|
*/
|
|
22
|
-
async function persistCapture(ctx, renderFailedStoryIds = /* @__PURE__ */ new Set()) {
|
|
24
|
+
async function persistCapture(ctx, renderFailedStoryIds = /* @__PURE__ */ new Set(), flakyFailedStoryIds = /* @__PURE__ */ new Set()) {
|
|
23
25
|
const failedStoryIds = new Set(renderFailedStoryIds);
|
|
26
|
+
const flakyIds = new Set(flakyFailedStoryIds);
|
|
24
27
|
await Promise.all(ctx.captures.map(async (capture) => {
|
|
25
28
|
try {
|
|
26
29
|
await persistSnapshot(ctx, capture);
|
|
27
30
|
} catch (error) {
|
|
28
|
-
|
|
31
|
+
if (flakyIds.has(capture.story.id)) {
|
|
32
|
+
flakyIds.add(capture.story.id);
|
|
33
|
+
ctx.logger?.warn({
|
|
34
|
+
storyId: capture.story.id,
|
|
35
|
+
viewport: capture.viewportName,
|
|
36
|
+
err: error
|
|
37
|
+
}, "capture failed for flaky story (non-blocking)");
|
|
38
|
+
} else failedStoryIds.add(capture.story.id);
|
|
29
39
|
ctx.logger?.error({
|
|
30
40
|
storyId: capture.story.id,
|
|
31
41
|
viewport: capture.viewportName,
|
|
@@ -33,7 +43,8 @@ async function persistCapture(ctx, renderFailedStoryIds = /* @__PURE__ */ new Se
|
|
|
33
43
|
}, "capture failed for story");
|
|
34
44
|
}
|
|
35
45
|
}));
|
|
36
|
-
|
|
46
|
+
for (const id of flakyIds) if (!failedStoryIds.has(id)) ctx.logger?.warn({ storyId: id }, "flaky story failed (non-blocking)");
|
|
47
|
+
await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds, flakyIds);
|
|
37
48
|
}
|
|
38
49
|
function viewportByName(ctx, name) {
|
|
39
50
|
return ctx.viewports.find((v) => v.name === name) ?? {
|
|
@@ -103,13 +114,14 @@ async function createWithBaseline(ctx, capture, viewport, screenshot, baselinePa
|
|
|
103
114
|
diffPassed: result.passed
|
|
104
115
|
});
|
|
105
116
|
}
|
|
106
|
-
async function finalize(ctx, storyIds, failedStoryIds) {
|
|
117
|
+
async function finalize(ctx, storyIds, failedStoryIds, flakyFailedStoryIds = /* @__PURE__ */ new Set()) {
|
|
107
118
|
const builds = new BuildModel(ctx.db);
|
|
108
119
|
const build = await builds.updateCounts(ctx.build.id);
|
|
109
120
|
const hasCaptures = storyIds.size > 0;
|
|
110
121
|
let status = "reviewing";
|
|
111
122
|
if (failedStoryIds.size > 0) status = "failed";
|
|
112
123
|
else if (hasCaptures && build.changedCount === 0) status = "approved";
|
|
124
|
+
if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) ctx.logger?.warn({ flakyStoryIds: [...flakyFailedStoryIds] }, "flaky stories failed (non-blocking)");
|
|
113
125
|
await builds.setStatus(ctx.build.id, status);
|
|
114
126
|
if (ctx.build.isDefault && hasCaptures) await new BaselineModel(ctx.db, ctx.storage).removeOrphans(ctx.project.id, new Set(storyIds));
|
|
115
127
|
}
|
|
@@ -1 +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":"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"}
|
|
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 * (blocking failures). They force the build to `failed`.\n * @param flakyFailedStoryIds - Story ids that failed but are marked flaky\n * (non-blocking). They are logged as warnings and do not block the build.\n */\nexport async function persistCapture(\n ctx: CaptureContext,\n renderFailedStoryIds: ReadonlySet<string> = new Set(),\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const failedStoryIds = new Set(renderFailedStoryIds);\n const flakyIds = new Set(flakyFailedStoryIds);\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 // Flaky stories remain non-blocking even on persist failure.\n if (flakyIds.has(capture.story.id)) {\n flakyIds.add(capture.story.id);\n ctx.logger?.warn(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for flaky story (non-blocking)\",\n );\n } else {\n failedStoryIds.add(capture.story.id);\n }\n ctx.logger?.error(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for story\",\n );\n }\n }),\n );\n // Also log flaky render failures as warnings\n for (const id of flakyIds) {\n if (!failedStoryIds.has(id)) {\n ctx.logger?.warn({ storyId: id }, \"flaky story failed (non-blocking)\");\n }\n }\n await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds, flakyIds);\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(\n ctx: CaptureContext,\n storyIds: ReadonlySet<string>,\n failedStoryIds: ReadonlySet<string>,\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): 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 // Flaky failures do not block: log warning if any flaky stories failed\n if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) {\n ctx.logger?.warn({ flakyStoryIds: [...flakyFailedStoryIds] }, \"flaky stories failed (non-blocking)\");\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":"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"}
|
package/dist/capture/queue.mjs
CHANGED
|
@@ -1 +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:
|
|
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 | undefined;\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: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\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,SAAU,WAAuD,mBAAmB;EACpF,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"}
|
|
@@ -4,25 +4,33 @@ import.meta.url;
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
//#region src/capture/storybook.ts
|
|
7
|
+
function mergeParameters(entry) {
|
|
8
|
+
const merged = {
|
|
9
|
+
...entry.parameters?.chromatic,
|
|
10
|
+
...entry.parameters?.storyshelf
|
|
11
|
+
};
|
|
12
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
13
|
+
}
|
|
7
14
|
var StorybookAdapter = class {
|
|
8
15
|
name = "storybook";
|
|
9
16
|
screenshotSelector = "#storybook-root";
|
|
10
17
|
async discover(source) {
|
|
11
18
|
const index = await this.readIndex(source);
|
|
12
|
-
return Object.values(index.entries).filter((entry) => entry.type !== "docs").map((entry) => ({
|
|
19
|
+
return Object.values(index.entries).filter((entry) => entry.type !== "docs" && entry.subtype !== "test").map((entry) => ({
|
|
13
20
|
id: entry.id,
|
|
14
21
|
title: entry.title,
|
|
15
22
|
name: entry.name,
|
|
16
23
|
importPath: entry.importPath,
|
|
17
24
|
tags: entry.tags,
|
|
18
|
-
type: entry.type === "docs" ? "docs" : "story"
|
|
25
|
+
type: entry.type === "docs" ? "docs" : "story",
|
|
26
|
+
parameters: mergeParameters(entry)
|
|
19
27
|
}));
|
|
20
28
|
}
|
|
21
29
|
buildUrl(baseUrl, storyId) {
|
|
22
30
|
return `${baseUrl}/iframe.html?id=${encodeURIComponent(storyId)}&viewMode=story`;
|
|
23
31
|
}
|
|
24
32
|
async readIndex(source) {
|
|
25
|
-
const found = (await Promise.all(["
|
|
33
|
+
const found = (await Promise.all(["stories.json", "index.json"].map(async (name) => {
|
|
26
34
|
try {
|
|
27
35
|
const raw = await readFile(join(source, name), "utf8");
|
|
28
36
|
return JSON.parse(raw);
|
|
@@ -1 +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
|
|
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, StoryParameters, StorySourceAdapter } from \"./adapter.ts\";\n\ninterface StorybookIndex {\n v: number;\n entries: Record<\n string,\n {\n id: string;\n name: string;\n title: string;\n importPath?: string;\n tags?: string[];\n type: string;\n subtype?: string;\n parameters?: { chromatic?: StoryParameters; storyshelf?: StoryParameters };\n }\n >;\n}\n\nfunction mergeParameters(entry: {\n parameters?: { chromatic?: StoryParameters; storyshelf?: StoryParameters };\n}): StoryParameters | undefined {\n const merged: StoryParameters = {\n ...entry.parameters?.chromatic,\n ...entry.parameters?.storyshelf,\n };\n return Object.keys(merged).length > 0 ? merged : undefined;\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\" && entry.subtype !== \"test\")\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 parameters: mergeParameters(entry),\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 // eslint-disable-next-line class-methods-use-this\n private async readIndex(source: string): Promise<StorybookIndex> {\n const candidates: string[] = [\"stories.json\", \"index.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":";;;;;;AAsBA,SAAS,gBAAgB,OAEO;CAC9B,MAAM,SAA0B;EAC9B,GAAG,MAAM,YAAY;EACrB,GAAG,MAAM,YAAY;CACvB;CACA,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,SAAS,KAAA;AACnD;AAEA,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,UAAU,MAAM,YAAY,MAAM,CAAC,CACpE,KAAK,WAAW;GACf,IAAI,MAAM;GACV,OAAO,MAAM;GACb,MAAM,MAAM;GACZ,YAAY,MAAM;GAClB,MAAM,MAAM;GACZ,MAAM,MAAM,SAAS,SAAS,SAAS;GACvC,YAAY,gBAAgB,KAAK;EACnC,EAAE;CACN;CAGA,SAAS,SAAiB,SAAyB;EACjD,OAAO,GAAG,QAAQ,kBAAkB,mBAAmB,OAAO,EAAE;CAClE;CAGA,MAAc,UAAU,QAAyC;EAY/D,MAAM,SAAQ,MAVQ,QAAQ,IAC5B,CAF4B,gBAAgB,YAEnC,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"}
|
package/dist/config.mjs
CHANGED
|
@@ -42,8 +42,8 @@ const shelfConfigSchema = z.object({
|
|
|
42
42
|
}).strict();
|
|
43
43
|
const uiConfigSchema = z.object({
|
|
44
44
|
name: z.string().optional(),
|
|
45
|
-
logo: z.url().optional(),
|
|
46
|
-
favicon: z.url().optional(),
|
|
45
|
+
logo: z.string().url().optional(),
|
|
46
|
+
favicon: z.string().url().optional(),
|
|
47
47
|
lightTheme: brandThemeSchema.optional(),
|
|
48
48
|
darkTheme: brandThemeSchema.optional()
|
|
49
49
|
}).strict();
|
package/dist/config.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type { Logger } from \"pino\";\n\nimport type { AuthAdapter } from \"./adapters/auth.ts\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport type { CaptureRunner } from \"./adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"./adapters/database.ts\";\nimport type { GitHostProvider } from \"./adapters/git-host/index.ts\";\nimport type { StorageAdapter } from \"./adapters/storage.ts\";\n\nexport interface BrandTheme {\n accent: string;\n surface: { base: string; card: string };\n text: { primary: string; secondary: string };\n border: string;\n status: { approved: string; new: string; rejected: string };\n}\n\nexport interface UIConfig {\n name?: string;\n logo?: string;\n favicon?: string;\n lightTheme?: BrandTheme;\n darkTheme?: BrandTheme;\n}\n\nconst viewportSchema = z.object({\n name: z.string().min(1),\n width: z.number().int().positive(),\n height: z.number().int().positive(),\n});\n\nconst brandThemeSchema = z.object({\n accent: z.string(),\n surface: z.object({ base: z.string(), card: z.string() }),\n text: z.object({ primary: z.string(), secondary: z.string() }),\n border: z.string(),\n status: z.object({ approved: z.string(), new: z.string(), rejected: z.string() }),\n});\n\nconst adapterSnapshotSchema = z.object({\n name: z.string(),\n version: z.string(),\n description: z.string().optional(),\n kind: z.string(),\n});\n\nexport const shelfConfigSchema = z\n .object({\n secret: z.string().min(1).optional(),\n publishedBaseDomain: z.string().optional(),\n captureConcurrency: z.number().int().positive().optional(),\n scratchDir: z.string().optional(),\n purgeTtlDays: z.number().int().positive().optional(),\n viewports: z.array(viewportSchema).min(1, \"at least one viewport required\").optional(),\n adapters: z.record(z.string(), adapterSnapshotSchema).optional(),\n })\n .strict();\n\nexport const uiConfigSchema = z\n .object({\n name: z.string().optional(),\n logo: z.url().optional(),\n favicon: z.url().optional(),\n lightTheme: brandThemeSchema.optional(),\n darkTheme: brandThemeSchema.optional(),\n })\n .strict();\n\nexport type ShelfConfig = z.infer<typeof shelfConfigSchema>;\n\nexport function validateConfig(config: Record<string, unknown>): ShelfConfig {\n const result = shelfConfigSchema.safeParse(config);\n if (!result.success) {\n const issues = result.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n throw new Error(`Invalid ShelfConfig: ${issues}`);\n }\n return result.data;\n}\n\nexport function validateUiConfig(config: Record<string, unknown>): UIConfig {\n const result = uiConfigSchema.safeParse(config);\n if (!result.success) {\n const issues = result.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n throw new Error(`Invalid UIConfig: ${issues}`);\n }\n return result.data;\n}\n\nexport interface ShelfOptions {\n database: DatabaseAdapter;\n storage: StorageAdapter;\n captureRunner?: CaptureRunner;\n captureQueue?: CaptureQueue;\n auth?: AuthAdapter;\n gitHosts?: GitHostProvider[];\n logger?: Logger;\n ui?: UIConfig;\n config?: ShelfConfig;\n}\n"],"mappings":";;;;;AA0BA,MAAM,iBAAiB,EAAE,OAAO;CAC9B,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACtB,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CACjC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;AACpC,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO;CAChC,QAAQ,EAAE,OAAO;CACjB,SAAS,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,MAAM,EAAE,OAAO;CAAE,CAAC;CACxD,MAAM,EAAE,OAAO;EAAE,SAAS,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC;CAC7D,QAAQ,EAAE,OAAO;CACjB,QAAQ,EAAE,OAAO;EAAE,UAAU,EAAE,OAAO;EAAG,KAAK,EAAE,OAAO;EAAG,UAAU,EAAE,OAAO;CAAE,CAAC;AAClF,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;CACrC,MAAM,EAAE,OAAO;CACf,SAAS,EAAE,OAAO;CAClB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO;AACjB,CAAC;AAED,MAAa,oBAAoB,EAC9B,OAAO;CACN,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;CACnC,qBAAqB,EAAE,OAAO,CAAC,CAAC,SAAS;CACzC,oBAAoB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;CACzD,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;CAChC,cAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;CACnD,WAAW,EAAE,MAAM,cAAc,CAAC,CAAC,IAAI,GAAG,gCAAgC,CAAC,CAAC,SAAS;CACrF,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,qBAAqB,CAAC,CAAC,SAAS;AACjE,CAAC,CAAC,CACD,OAAO;AAEV,MAAa,iBAAiB,EAC3B,OAAO;CACN,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type { Logger } from \"pino\";\n\nimport type { AuthAdapter } from \"./adapters/auth.ts\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport type { CaptureRunner } from \"./adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"./adapters/database.ts\";\nimport type { GitHostProvider } from \"./adapters/git-host/index.ts\";\nimport type { StorageAdapter } from \"./adapters/storage.ts\";\n\nexport interface BrandTheme {\n accent: string;\n surface: { base: string; card: string };\n text: { primary: string; secondary: string };\n border: string;\n status: { approved: string; new: string; rejected: string };\n}\n\nexport interface UIConfig {\n name?: string;\n logo?: string;\n favicon?: string;\n lightTheme?: BrandTheme;\n darkTheme?: BrandTheme;\n}\n\nconst viewportSchema = z.object({\n name: z.string().min(1),\n width: z.number().int().positive(),\n height: z.number().int().positive(),\n});\n\nconst brandThemeSchema = z.object({\n accent: z.string(),\n surface: z.object({ base: z.string(), card: z.string() }),\n text: z.object({ primary: z.string(), secondary: z.string() }),\n border: z.string(),\n status: z.object({ approved: z.string(), new: z.string(), rejected: z.string() }),\n});\n\nconst adapterSnapshotSchema = z.object({\n name: z.string(),\n version: z.string(),\n description: z.string().optional(),\n kind: z.string(),\n});\n\nexport const shelfConfigSchema = z\n .object({\n secret: z.string().min(1).optional(),\n publishedBaseDomain: z.string().optional(),\n captureConcurrency: z.number().int().positive().optional(),\n scratchDir: z.string().optional(),\n purgeTtlDays: z.number().int().positive().optional(),\n viewports: z.array(viewportSchema).min(1, \"at least one viewport required\").optional(),\n adapters: z.record(z.string(), adapterSnapshotSchema).optional(),\n })\n .strict();\n\nexport const uiConfigSchema = z\n .object({\n name: z.string().optional(),\n // oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\n logo: z.string().url().optional(),\n // oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\n favicon: z.string().url().optional(),\n lightTheme: brandThemeSchema.optional(),\n darkTheme: brandThemeSchema.optional(),\n })\n .strict();\n\nexport type ShelfConfig = z.infer<typeof shelfConfigSchema>;\n\nexport function validateConfig(config: Record<string, unknown>): ShelfConfig {\n const result = shelfConfigSchema.safeParse(config);\n if (!result.success) {\n const issues = result.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n throw new Error(`Invalid ShelfConfig: ${issues}`);\n }\n return result.data;\n}\n\nexport function validateUiConfig(config: Record<string, unknown>): UIConfig {\n const result = uiConfigSchema.safeParse(config);\n if (!result.success) {\n const issues = result.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n throw new Error(`Invalid UIConfig: ${issues}`);\n }\n return result.data;\n}\n\nexport interface ShelfOptions {\n database: DatabaseAdapter;\n storage: StorageAdapter;\n captureRunner?: CaptureRunner;\n captureQueue?: CaptureQueue;\n auth?: AuthAdapter;\n gitHosts?: GitHostProvider[];\n logger?: Logger;\n ui?: UIConfig;\n config?: ShelfConfig;\n}\n"],"mappings":";;;;;AA0BA,MAAM,iBAAiB,EAAE,OAAO;CAC9B,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACtB,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CACjC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;AACpC,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO;CAChC,QAAQ,EAAE,OAAO;CACjB,SAAS,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,MAAM,EAAE,OAAO;CAAE,CAAC;CACxD,MAAM,EAAE,OAAO;EAAE,SAAS,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC;CAC7D,QAAQ,EAAE,OAAO;CACjB,QAAQ,EAAE,OAAO;EAAE,UAAU,EAAE,OAAO;EAAG,KAAK,EAAE,OAAO;EAAG,UAAU,EAAE,OAAO;CAAE,CAAC;AAClF,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;CACrC,MAAM,EAAE,OAAO;CACf,SAAS,EAAE,OAAO;CAClB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO;AACjB,CAAC;AAED,MAAa,oBAAoB,EAC9B,OAAO;CACN,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;CACnC,qBAAqB,EAAE,OAAO,CAAC,CAAC,SAAS;CACzC,oBAAoB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;CACzD,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;CAChC,cAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;CACnD,WAAW,EAAE,MAAM,cAAc,CAAC,CAAC,IAAI,GAAG,gCAAgC,CAAC,CAAC,SAAS;CACrF,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,qBAAqB,CAAC,CAAC,SAAS;AACjE,CAAC,CAAC,CACD,OAAO;AAEV,MAAa,iBAAiB,EAC3B,OAAO;CACN,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAE1B,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAEhC,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CACnC,YAAY,iBAAiB,SAAS;CACtC,WAAW,iBAAiB,SAAS;AACvC,CAAC,CAAC,CACD,OAAO;AAIV,SAAgB,eAAe,QAA8C;CAC3E,MAAM,SAAS,kBAAkB,UAAU,MAAM;CACjD,IAAI,CAAC,OAAO,SAAS;EACnB,MAAM,SAAS,OAAO,MAAM,OAAO,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,IAAI;EAC5F,MAAM,IAAI,MAAM,wBAAwB,QAAQ;CAClD;CACA,OAAO,OAAO;AAChB;AAEA,SAAgB,iBAAiB,QAA2C;CAC1E,MAAM,SAAS,eAAe,UAAU,MAAM;CAC9C,IAAI,CAAC,OAAO,SAAS;EACnB,MAAM,SAAS,OAAO,MAAM,OAAO,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,IAAI;EAC5F,MAAM,IAAI,MAAM,qBAAqB,QAAQ;CAC/C;CACA,OAAO,OAAO;AAChB"}
|