@storyshelf/core 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/auth.mjs.map +1 -1
- package/dist/adapter/git-host/helpers.d.mts +1 -0
- package/dist/adapter/git-host/helpers.mjs +1 -0
- package/dist/adapter/git-host/helpers.mjs.map +1 -1
- package/dist/adapters/capture-queue.d.mts +2 -0
- package/dist/adapters/webhook-events.mjs +8 -0
- package/dist/adapters/webhook-events.mjs.map +1 -1
- package/dist/capture/adapter.d.mts +2 -0
- package/dist/capture/adapter.mjs +3 -0
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/orchestrator.d.mts +7 -0
- package/dist/capture/orchestrator.mjs +6 -0
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/queue.d.mts +2 -0
- package/dist/capture/queue.mjs +1 -0
- package/dist/capture/queue.mjs.map +1 -1
- package/dist/capture/status-fanout.mjs +1 -0
- package/dist/capture/status-fanout.mjs.map +1 -1
- package/dist/capture/storybook.d.mts +1 -0
- package/dist/capture/storybook.mjs +1 -0
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/config.d.mts +27 -20
- package/dist/config.mjs +14 -0
- package/dist/config.mjs.map +1 -1
- package/dist/ddl.d.mts +1 -0
- package/dist/ddl.mjs +1 -0
- package/dist/ddl.mjs.map +1 -1
- package/dist/diff/engine.d.mts +8 -0
- package/dist/diff/engine.mjs +8 -0
- package/dist/diff/engine.mjs.map +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +6 -1
- package/dist/index.mjs.map +1 -1
- package/dist/logger.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -0
- package/dist/middleware/csrf.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +1 -0
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/models/baseline.d.mts +1 -0
- package/dist/models/baseline.mjs +3 -1
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build.d.mts +3 -0
- package/dist/models/build.mjs +3 -1
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/comment.d.mts +2 -0
- package/dist/models/comment.mjs +3 -1
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/label.mjs +2 -1
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.mjs +2 -1
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/project.d.mts +1 -0
- package/dist/models/project.mjs +2 -1
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/snapshot.d.mts +1 -0
- package/dist/models/snapshot.mjs +2 -1
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/status-config.mjs +2 -1
- package/dist/models/status-config.mjs.map +1 -1
- package/dist/models/token.mjs +2 -1
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/webhook.d.mts +1 -0
- package/dist/models/webhook.mjs +2 -1
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +1 -0
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +1 -0
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/compute-jobs.mjs +2 -0
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -0
- package/dist/pages/login.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +1 -0
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -0
- package/dist/pages/project-create.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +1 -0
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects.mjs +1 -0
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/root.mjs +1 -0
- package/dist/pages/root.mjs.map +1 -1
- package/dist/pages/settings-general.mjs +1 -0
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +1 -0
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-members.mjs +1 -0
- package/dist/pages/settings-members.mjs.map +1 -1
- package/dist/pages/settings-status.mjs +1 -0
- package/dist/pages/settings-status.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +1 -0
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -0
- package/dist/pages/settings-tokens.mjs.map +1 -1
- package/dist/pages/settings-webhooks.mjs +1 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -1
- package/dist/retention/purge.d.mts +3 -0
- package/dist/retention/purge.mjs +2 -1
- package/dist/retention/purge.mjs.map +1 -1
- package/dist/routers/admin.mjs +1 -0
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +1 -0
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -0
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/builds.handlers.mjs +10 -0
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +1 -0
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/comments.mjs +1 -0
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/helpers.mjs +7 -1
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +2 -0
- package/dist/routers/htmx.mjs.map +1 -1
- package/dist/routers/labels.mjs +1 -0
- package/dist/routers/labels.mjs.map +1 -1
- package/dist/routers/media.mjs +1 -0
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +1 -0
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/projects.mjs +1 -0
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/schemas.mjs +25 -0
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/settings.mjs +2 -0
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/snapshots.mjs +1 -0
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/status-configs.mjs +1 -0
- package/dist/routers/status-configs.mjs.map +1 -1
- package/dist/routers/storybook.mjs +1 -0
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +1 -0
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +1 -0
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +1 -0
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/schema-tables.mjs +166 -0
- package/dist/schema-tables.mjs.map +1 -0
- package/dist/schema.d.mts +194 -4420
- package/dist/schema.mjs +26 -138
- package/dist/schema.mjs.map +1 -1
- package/dist/store.mjs +2 -0
- package/dist/store.mjs.map +1 -1
- package/dist/types.d.mts +12 -0
- package/dist/types.mjs +8 -0
- package/dist/types.mjs.map +1 -1
- package/dist/ui/components.mjs +5 -0
- package/dist/ui/components.mjs.map +1 -1
- package/dist/ui/document.d.mts +1 -0
- package/dist/ui/document.mjs +1 -0
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/styles.mjs +1 -0
- package/dist/ui/styles.mjs.map +1 -1
- package/dist/ui/theme.mjs +2 -0
- package/dist/ui/theme.mjs.map +1 -1
- package/dist/urls.d.mts +8 -0
- package/dist/urls.mjs +7 -0
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/hash.mjs +3 -0
- package/dist/utils/hash.mjs.map +1 -1
- package/dist/utils/paths.d.mts +5 -0
- package/dist/utils/paths.mjs +5 -0
- package/dist/utils/paths.mjs.map +1 -1
- package/dist/utils/ulid.d.mts +2 -0
- package/dist/utils/ulid.mjs +2 -0
- package/dist/utils/ulid.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/adapters/auth.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/adapters/auth.ts"],"sourcesContent":["/**\n * Auth adapter interface: authenticate users and resolve project access.\n */\nimport type { AdapterMetadata } from \"./metadata.ts\";\nimport type { ProjectRole, SiteRole } from \"../types.ts\";\n\n/** Shared session cookie name used by all auth adapters (ADR 0008). */\nexport const SESSION_COOKIE = \"storyshelf_session\";\n\n/** An authenticated user of the platform. */\nexport interface AuthUser {\n /** User ID. */\n id: string;\n /** User email. */\n email: string;\n /** Display name. */\n name: string;\n /** Optional avatar URL. */\n avatarUrl?: string;\n /** Site-wide role. */\n role: SiteRole;\n}\n\n/** Data passed to an auth adapter when an OAuth callback is received. */\nexport interface AuthCallback {\n /** Provider name. */\n provider: string;\n /** Authorization code from the provider. */\n code: string;\n /** Anti-CSRF state value. */\n state: string;\n}\n\n/** Access controls granted to a user for a specific project. */\nexport interface ProjectAccess {\n /** Project role. */\n role: ProjectRole;\n}\n\n/** Pluggable authentication abstraction (ADR 0008). */\nexport interface AuthAdapter {\n /** Adapter identity. */\n readonly metadata?: AdapterMetadata;\n /** Resolve the current user from a request, or null if unauthenticated. */\n check(request: Request): Promise<AuthUser | null>;\n /** Create a session for a user and return a session token. */\n createSession(user: AuthUser): Promise<string>;\n /** Destroy a session by its id. */\n destroySession(sessionId: string): Promise<void>;\n /** Handle an OAuth callback and return the resolved user, if supported. */\n handleCallback?(callback: AuthCallback): Promise<AuthUser | null>;\n}\n"],"mappings":";;;;;AAOA,MAAa,iBAAiB"}
|
|
@@ -6,6 +6,7 @@ import { CheckStatus } from "../git-host.mjs";
|
|
|
6
6
|
declare function describeStatus(status: CheckStatus): string;
|
|
7
7
|
/** Markdown comment body helpers — single comment per build, idempotent via marker. */
|
|
8
8
|
declare function commentMarker(url: string): string;
|
|
9
|
+
/** Build the Markdown body for a StoryShelf status comment. */
|
|
9
10
|
declare function buildCommentMarkdown(status: CheckStatus, url: string, context: string): string;
|
|
10
11
|
//#endregion
|
|
11
12
|
export { buildCommentMarkdown, commentMarker, describeStatus };
|
|
@@ -15,6 +15,7 @@ function describeStatus(status) {
|
|
|
15
15
|
function commentMarker(url) {
|
|
16
16
|
return `<!-- storyshelf:${url} -->`;
|
|
17
17
|
}
|
|
18
|
+
/** Build the Markdown body for a StoryShelf status comment. */
|
|
18
19
|
function buildCommentMarkdown(status, url, context) {
|
|
19
20
|
switch (status) {
|
|
20
21
|
case "pending": return `Visual tests pending for \`${context}\` — [View build](${url})`;
|
|
@@ -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 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
|
|
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\n/** Build the Markdown body for a StoryShelf status comment. */\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;;AAGA,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"}
|
|
@@ -2,7 +2,9 @@ import __tsdown_shims_path from 'node:path';
|
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { AdapterMetadata } from "./metadata.mjs";
|
|
4
4
|
//#region src/adapters/capture-queue.d.ts
|
|
5
|
+
/** Lifecycle status of a queued capture job. */
|
|
5
6
|
type JobStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
7
|
+
/** Observable state of a single capture queue entry. */
|
|
6
8
|
interface QueueEntry {
|
|
7
9
|
buildId: string;
|
|
8
10
|
status: JobStatus;
|
|
@@ -17,6 +17,14 @@ async function sendWebhook(url, secret, event) {
|
|
|
17
17
|
body
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Deliver an event to every subscribed webhook of a project.
|
|
22
|
+
*
|
|
23
|
+
* @param db - Database adapter for loading subscriptions.
|
|
24
|
+
* @param projectId - Project whose webhooks receive the event.
|
|
25
|
+
* @param event - Event name (e.g. "baseline:created").
|
|
26
|
+
* @param data - Event payload.
|
|
27
|
+
*/
|
|
20
28
|
async function emitWebhookEvent(db, projectId, event, data) {
|
|
21
29
|
const webhooks = await new WebhookModel(db).list(projectId);
|
|
22
30
|
const eventPayload = {
|
|
@@ -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 // Webhook delivery failures are non-fatal\n }\n }),\n );\n}\n"],"mappings":";;;;;;
|
|
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\n/** Outbound webhook payload delivered to subscribers. */\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\n/**\n * Deliver an event to every subscribed webhook of a project.\n *\n * @param db - Database adapter for loading subscriptions.\n * @param projectId - Project whose webhooks receive the event.\n * @param event - Event name (e.g. \"baseline:created\").\n * @param data - Event payload.\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":";;;;;;AAYA,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;;;;;;;;;AAUA,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,7 @@
|
|
|
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
|
+
/** Per-story capture parameters from the Storybook index. */
|
|
4
5
|
interface StoryParameters {
|
|
5
6
|
disableSnapshot?: boolean;
|
|
6
7
|
delay?: number;
|
|
@@ -31,6 +32,7 @@ interface Viewport {
|
|
|
31
32
|
width: number;
|
|
32
33
|
height: number;
|
|
33
34
|
}
|
|
35
|
+
/** Default viewport used when none is configured. */
|
|
34
36
|
declare const DEFAULT_VIEWPORTS: Viewport[];
|
|
35
37
|
//#endregion
|
|
36
38
|
export { DEFAULT_VIEWPORTS, StoryEntry, StoryParameters, StorySourceAdapter, Viewport };
|
package/dist/capture/adapter.mjs
CHANGED
|
@@ -2,12 +2,14 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
//#region src/capture/adapter.ts
|
|
5
|
+
/** Return whether a story is marked flaky (failures stay non-blocking). */
|
|
5
6
|
function isFlakyStory(entry) {
|
|
6
7
|
if (entry.parameters?.flakyTest) return true;
|
|
7
8
|
const tags = entry.tags ?? [];
|
|
8
9
|
for (const t of tags) if (t.toLowerCase() === "flaky-test") return true;
|
|
9
10
|
return false;
|
|
10
11
|
}
|
|
12
|
+
/** Return whether a story is excluded from snapshot capture. */
|
|
11
13
|
function isDisabledStory(entry) {
|
|
12
14
|
if (entry.parameters?.disableSnapshot) return true;
|
|
13
15
|
const tags = entry.tags ?? [];
|
|
@@ -17,6 +19,7 @@ function isDisabledStory(entry) {
|
|
|
17
19
|
}
|
|
18
20
|
return false;
|
|
19
21
|
}
|
|
22
|
+
/** Default viewport used when none is configured. */
|
|
20
23
|
const DEFAULT_VIEWPORTS = [{
|
|
21
24
|
name: "desktop",
|
|
22
25
|
width: 1280,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.mjs","names":[],"sources":["../../src/capture/adapter.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"adapter.mjs","names":[],"sources":["../../src/capture/adapter.ts"],"sourcesContent":["/** Per-story capture parameters from the Storybook index. */\nexport interface StoryParameters {\n disableSnapshot?: boolean;\n delay?: number;\n diffThreshold?: number;\n pauseAnimationAtEnd?: boolean;\n flakyTest?: boolean;\n}\n\n/** Return whether a story is marked flaky (failures stay non-blocking). */\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\n/** Return whether a story is excluded from snapshot capture. */\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\n/** Default viewport used when none is configured. */\nexport const DEFAULT_VIEWPORTS: Viewport[] = [{ name: \"desktop\", width: 1280, height: 720 }];\n"],"mappings":";;;;;AAUA,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;;AAGA,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;;AA6BA,MAAa,oBAAgC,CAAC;CAAE,MAAM;CAAW,OAAO;CAAM,QAAQ;AAAI,CAAC"}
|
|
@@ -6,6 +6,7 @@ import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
|
6
6
|
import { StorageAdapter } from "../adapter/storage.mjs";
|
|
7
7
|
import { Logger } from "pino";
|
|
8
8
|
//#region src/capture/orchestrator.d.ts
|
|
9
|
+
/** Inputs for running a capture job against a Storybook build. */
|
|
9
10
|
interface CaptureJobOptions {
|
|
10
11
|
db: DatabaseAdapter;
|
|
11
12
|
storage: StorageAdapter;
|
|
@@ -14,6 +15,12 @@ interface CaptureJobOptions {
|
|
|
14
15
|
viewports?: Viewport[];
|
|
15
16
|
logger?: Logger;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Run the full capture for a build: extract, render, persist, and finalize.
|
|
20
|
+
*
|
|
21
|
+
* @param input - Build id plus the originating request id.
|
|
22
|
+
* @param options - Adapters, scratch dir, viewports, and logger.
|
|
23
|
+
*/
|
|
17
24
|
declare function executeCaptureJob(input: {
|
|
18
25
|
buildId: string;
|
|
19
26
|
reqId?: string;
|
|
@@ -11,6 +11,12 @@ import { mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
|
11
11
|
import { join, relative, resolve, sep } from "node:path";
|
|
12
12
|
import AdmZip from "adm-zip";
|
|
13
13
|
//#region src/capture/orchestrator.ts
|
|
14
|
+
/**
|
|
15
|
+
* Run the full capture for a build: extract, render, persist, and finalize.
|
|
16
|
+
*
|
|
17
|
+
* @param input - Build id plus the originating request id.
|
|
18
|
+
* @param options - Adapters, scratch dir, viewports, and logger.
|
|
19
|
+
*/
|
|
14
20
|
async function executeCaptureJob(input, options) {
|
|
15
21
|
const builds = new BuildModel(options.db);
|
|
16
22
|
const { build, project } = await loadTarget(options, input.buildId);
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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\n/** Inputs for running a capture job against a Storybook build. */\nexport interface CaptureJobOptions {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n runner: CaptureRunner;\n scratchDir: string;\n viewports?: Viewport[];\n logger?: Logger;\n}\n/**\n * Run the full capture for a build: extract, render, persist, and finalize.\n *\n * @param input - Build id plus the originating request id.\n * @param options - Adapters, scratch dir, viewports, and 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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"}
|
package/dist/capture/queue.d.mts
CHANGED
|
@@ -3,11 +3,13 @@ import __tsdown_shims_url from 'node:url';
|
|
|
3
3
|
import { CaptureJob, CaptureQueue, QueueEntry } from "../adapters/capture-queue.mjs";
|
|
4
4
|
import { Logger } from "pino";
|
|
5
5
|
//#region src/capture/queue.d.ts
|
|
6
|
+
/** Options for the in-process capture queue. */
|
|
6
7
|
interface InMemoryCaptureQueueOptions {
|
|
7
8
|
concurrency: number;
|
|
8
9
|
runJob: (job: CaptureJob) => Promise<void>;
|
|
9
10
|
logger?: Logger;
|
|
10
11
|
}
|
|
12
|
+
/** In-process capture queue with bounded concurrency. */
|
|
11
13
|
declare class InMemoryCaptureQueue implements CaptureQueue {
|
|
12
14
|
private readonly options;
|
|
13
15
|
readonly metadata: {
|
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 | 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":"
|
|
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\n/** Options for the in-process capture queue. */\nexport interface InMemoryCaptureQueueOptions {\n concurrency: number;\n runJob: (job: CaptureJob) => Promise<void>;\n logger?: Logger;\n}\n/** In-process capture queue with bounded concurrency. */\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":";;;;;AAaA,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,6 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { StatusConfigModel } from "../models/status-config.mjs";
|
|
5
5
|
import { buildCommentMarkdown } from "../adapter/git-host/helpers.mjs";
|
|
6
6
|
//#region src/capture/status-fanout.ts
|
|
7
|
+
/** Post build check statuses (and review comments) to every configured git provider. */
|
|
7
8
|
async function postStatusesForBuild(opts) {
|
|
8
9
|
if (opts.providers.length === 0) return;
|
|
9
10
|
const model = new StatusConfigModel(opts.db, opts.secret);
|
|
@@ -1 +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":"
|
|
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\n/** Post build check statuses (and review comments) to every configured git provider. */\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":";;;;;;;AASA,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"}
|
|
@@ -2,6 +2,7 @@ import __tsdown_shims_path from 'node:path';
|
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { StoryEntry, StorySourceAdapter } from "./adapter.mjs";
|
|
4
4
|
//#region src/capture/storybook.d.ts
|
|
5
|
+
/** Discovers stories from a built Storybook via its index file. */
|
|
5
6
|
declare class StorybookAdapter implements StorySourceAdapter {
|
|
6
7
|
readonly name = "storybook";
|
|
7
8
|
readonly screenshotSelector = "#storybook-root";
|
|
@@ -11,6 +11,7 @@ function mergeParameters(entry) {
|
|
|
11
11
|
};
|
|
12
12
|
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
13
13
|
}
|
|
14
|
+
/** Discovers stories from a built Storybook via its index file. */
|
|
14
15
|
var StorybookAdapter = class {
|
|
15
16
|
name = "storybook";
|
|
16
17
|
screenshotSelector = "#storybook-root";
|
|
@@ -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, 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
|
|
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\n/** Discovers stories from a built Storybook via its index file. */\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;;AAGA,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.d.mts
CHANGED
|
@@ -9,6 +9,7 @@ import { StorageAdapter } from "./adapter/storage.mjs";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { Logger } from "pino";
|
|
11
11
|
//#region src/config.d.ts
|
|
12
|
+
/** Brand color theme for the server-rendered UI. */
|
|
12
13
|
interface BrandTheme {
|
|
13
14
|
accent: string;
|
|
14
15
|
surface: {
|
|
@@ -26,6 +27,7 @@ interface BrandTheme {
|
|
|
26
27
|
rejected: string;
|
|
27
28
|
};
|
|
28
29
|
}
|
|
30
|
+
/** Branding overrides for the server-rendered UI. */
|
|
29
31
|
interface UIConfig {
|
|
30
32
|
name?: string;
|
|
31
33
|
logo?: string;
|
|
@@ -33,25 +35,30 @@ interface UIConfig {
|
|
|
33
35
|
lightTheme?: BrandTheme;
|
|
34
36
|
darkTheme?: BrandTheme;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
38
|
+
/** A viewport in which stories are captured. */
|
|
39
|
+
interface ShelfViewport {
|
|
40
|
+
name: string;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
}
|
|
44
|
+
/** Metadata snapshot of a configured adapter. */
|
|
45
|
+
interface AdapterSnapshot {
|
|
46
|
+
name: string;
|
|
47
|
+
version: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
kind: string;
|
|
50
|
+
}
|
|
51
|
+
/** Shelf-level configuration (validated by {@link shelfConfigSchema}). */
|
|
52
|
+
interface ShelfConfig {
|
|
53
|
+
secret?: string;
|
|
54
|
+
publishedBaseDomain?: string;
|
|
55
|
+
captureConcurrency?: number;
|
|
56
|
+
scratchDir?: string;
|
|
57
|
+
purgeTtlDays?: number;
|
|
58
|
+
viewports?: ShelfViewport[];
|
|
59
|
+
adapters?: Record<string, AdapterSnapshot>;
|
|
60
|
+
}
|
|
61
|
+
/** Adapter and configuration options for creating the shelf router. */
|
|
55
62
|
interface ShelfOptions {
|
|
56
63
|
database: DatabaseAdapter;
|
|
57
64
|
storage: StorageAdapter;
|
|
@@ -64,5 +71,5 @@ interface ShelfOptions {
|
|
|
64
71
|
config?: ShelfConfig;
|
|
65
72
|
}
|
|
66
73
|
//#endregion
|
|
67
|
-
export { BrandTheme, ShelfConfig, ShelfOptions,
|
|
74
|
+
export { AdapterSnapshot, BrandTheme, ShelfConfig, ShelfOptions, ShelfViewport, UIConfig };
|
|
68
75
|
//# sourceMappingURL=config.d.mts.map
|
package/dist/config.mjs
CHANGED
|
@@ -31,6 +31,7 @@ const adapterSnapshotSchema = z.object({
|
|
|
31
31
|
description: z.string().optional(),
|
|
32
32
|
kind: z.string()
|
|
33
33
|
});
|
|
34
|
+
/** Zod schema validating the shelf-level configuration. */
|
|
34
35
|
const shelfConfigSchema = z.object({
|
|
35
36
|
secret: z.string().min(1).optional(),
|
|
36
37
|
publishedBaseDomain: z.string().optional(),
|
|
@@ -40,6 +41,7 @@ const shelfConfigSchema = z.object({
|
|
|
40
41
|
viewports: z.array(viewportSchema).min(1, "at least one viewport required").optional(),
|
|
41
42
|
adapters: z.record(z.string(), adapterSnapshotSchema).optional()
|
|
42
43
|
}).strict();
|
|
44
|
+
/** Zod schema validating the UI branding configuration. */
|
|
43
45
|
const uiConfigSchema = z.object({
|
|
44
46
|
name: z.string().optional(),
|
|
45
47
|
logo: z.string().url().optional(),
|
|
@@ -47,6 +49,12 @@ const uiConfigSchema = z.object({
|
|
|
47
49
|
lightTheme: brandThemeSchema.optional(),
|
|
48
50
|
darkTheme: brandThemeSchema.optional()
|
|
49
51
|
}).strict();
|
|
52
|
+
/**
|
|
53
|
+
* Parse and validate a raw shelf-level configuration object.
|
|
54
|
+
*
|
|
55
|
+
* @param config - Unvalidated configuration record.
|
|
56
|
+
* @returns The validated shelf configuration.
|
|
57
|
+
*/
|
|
50
58
|
function validateConfig(config) {
|
|
51
59
|
const result = shelfConfigSchema.safeParse(config);
|
|
52
60
|
if (!result.success) {
|
|
@@ -55,6 +63,12 @@ function validateConfig(config) {
|
|
|
55
63
|
}
|
|
56
64
|
return result.data;
|
|
57
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse and validate a raw UI branding configuration object.
|
|
68
|
+
*
|
|
69
|
+
* @param config - Unvalidated UI configuration record.
|
|
70
|
+
* @returns The validated UI configuration.
|
|
71
|
+
*/
|
|
58
72
|
function validateUiConfig(config) {
|
|
59
73
|
const result = uiConfigSchema.safeParse(config);
|
|
60
74
|
if (!result.success) {
|
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 // 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\
|
|
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\n/** Brand color theme for the server-rendered UI. */\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\n/** Branding overrides for the server-rendered UI. */\nexport interface UIConfig {\n name?: string;\n logo?: string;\n favicon?: string;\n lightTheme?: BrandTheme;\n darkTheme?: BrandTheme;\n}\n\n/** A viewport in which stories are captured. */\nexport interface ShelfViewport {\n name: string;\n width: number;\n height: number;\n}\n\n/** Metadata snapshot of a configured adapter. */\nexport interface AdapterSnapshot {\n name: string;\n version: string;\n description?: string;\n kind: string;\n}\n\n/** Shelf-level configuration (validated by {@link shelfConfigSchema}). */\nexport interface ShelfConfig {\n secret?: string;\n publishedBaseDomain?: string;\n captureConcurrency?: number;\n scratchDir?: string;\n purgeTtlDays?: number;\n viewports?: ShelfViewport[];\n adapters?: Record<string, AdapterSnapshot>;\n}\n\nconst viewportSchema: z.ZodType<ShelfViewport> = 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.ZodType<AdapterSnapshot> = z.object({\n name: z.string(),\n version: z.string(),\n description: z.string().optional(),\n kind: z.string(),\n});\n\n/** Zod schema validating the shelf-level configuration. */\nexport const shelfConfigSchema: z.ZodType<ShelfConfig> = 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\n/** Zod schema validating the UI branding configuration. */\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\n/**\n * Parse and validate a raw shelf-level configuration object.\n *\n * @param config - Unvalidated configuration record.\n * @returns The validated shelf configuration.\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\n/**\n * Parse and validate a raw UI branding configuration object.\n *\n * @param config - Unvalidated UI configuration record.\n * @returns The validated UI configuration.\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\n/** Adapter and configuration options for creating the shelf router. */\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":";;;;;AAsDA,MAAM,iBAA2C,EAAE,OAAO;CACxD,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,wBAAoD,EAAE,OAAO;CACjE,MAAM,EAAE,OAAO;CACf,SAAS,EAAE,OAAO;CAClB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO;AACjB,CAAC;;AAGD,MAAa,oBAA4C,EACtD,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;;AAGV,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;;;;;;;AAQV,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;;;;;;;AAQA,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"}
|
package/dist/ddl.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
//#region src/ddl.d.ts
|
|
4
|
+
/** Canonical SQLite DDL for all StoryShelf tables and indexes. */
|
|
4
5
|
declare const DDL = "\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_status_configs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_viewport_idx ON snapshots (build_id, story_id, viewport_name);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n screenshot_path TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_viewport_branch_idx ON baselines (project_id, story_id, viewport_name, branch);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\n";
|
|
5
6
|
//#endregion
|
|
6
7
|
export { DDL };
|
package/dist/ddl.mjs
CHANGED
package/dist/ddl.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ddl.mjs","names":[],"sources":["../src/ddl.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"ddl.mjs","names":[],"sources":["../src/ddl.ts"],"sourcesContent":["/** Canonical SQLite DDL for all StoryShelf tables and indexes. */\nexport const DDL = `\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_status_configs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_viewport_idx ON snapshots (build_id, story_id, viewport_name);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n screenshot_path TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_viewport_branch_idx ON baselines (project_id, story_id, viewport_name, branch);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\n`;\n"],"mappings":";;;;;AACA,MAAa,MAAM"}
|
package/dist/diff/engine.d.mts
CHANGED
|
@@ -2,6 +2,14 @@ import __tsdown_shims_path from 'node:path';
|
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { DiffOptions, DiffResult } from "./options.mjs";
|
|
4
4
|
//#region src/diff/engine.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Compare baseline and current screenshots, returning pass/fail and a diff image.
|
|
7
|
+
*
|
|
8
|
+
* @param baseline - Baseline PNG buffer.
|
|
9
|
+
* @param current - Current capture PNG buffer.
|
|
10
|
+
* @param options - Pixel threshold and ratio limits.
|
|
11
|
+
* @returns The diff result with pixel counts and an optional overlay image.
|
|
12
|
+
*/
|
|
5
13
|
declare function diffImages(baseline: Buffer, current: Buffer, options: DiffOptions): DiffResult;
|
|
6
14
|
//#endregion
|
|
7
15
|
export { diffImages };
|
package/dist/diff/engine.mjs
CHANGED
|
@@ -4,6 +4,14 @@ import.meta.url;
|
|
|
4
4
|
import pixelmatch from "pixelmatch";
|
|
5
5
|
import { PNG } from "pngjs";
|
|
6
6
|
//#region src/diff/engine.ts
|
|
7
|
+
/**
|
|
8
|
+
* Compare baseline and current screenshots, returning pass/fail and a diff image.
|
|
9
|
+
*
|
|
10
|
+
* @param baseline - Baseline PNG buffer.
|
|
11
|
+
* @param current - Current capture PNG buffer.
|
|
12
|
+
* @param options - Pixel threshold and ratio limits.
|
|
13
|
+
* @returns The diff result with pixel counts and an optional overlay image.
|
|
14
|
+
*/
|
|
7
15
|
function diffImages(baseline, current, options) {
|
|
8
16
|
const baselinePng = PNG.sync.read(baseline);
|
|
9
17
|
const currentPng = PNG.sync.read(current);
|