@storyshelf/core 0.3.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/README.md +38 -31
  2. package/dist/adapter/capture-queue.d.mts +29 -1
  3. package/dist/adapter/capture-runner.d.mts +18 -2
  4. package/dist/adapter/capture-runner.mjs +12 -1
  5. package/dist/adapter/capture-runner.mjs.map +1 -0
  6. package/dist/adapter/database.d.mts +67 -17
  7. package/dist/adapter/database.mjs +22 -55
  8. package/dist/adapter/database.mjs.map +1 -1
  9. package/dist/adapter/email-sender.d.mts +27 -0
  10. package/dist/adapter/email-sender.mjs +4 -0
  11. package/dist/adapter/metadata.d.mts +21 -15
  12. package/dist/adapter/notifier.d.mts +7 -0
  13. package/dist/adapter/notifier.mjs +6 -0
  14. package/dist/adapter/setup.d.mts +61 -0
  15. package/dist/adapter/setup.mjs +29 -0
  16. package/dist/adapter/setup.mjs.map +1 -0
  17. package/dist/adapter/webhook-events.d.mts +29 -0
  18. package/dist/adapter/webhook-events.mjs +83 -0
  19. package/dist/adapter/webhook-events.mjs.map +1 -0
  20. package/dist/adapters/notifier/events.d.mts +26 -0
  21. package/dist/adapters/notifier/events.mjs +66 -0
  22. package/dist/adapters/notifier/events.mjs.map +1 -0
  23. package/dist/adapters/notifier/format.d.mts +14 -0
  24. package/dist/adapters/notifier/format.mjs +96 -0
  25. package/dist/adapters/notifier/format.mjs.map +1 -0
  26. package/dist/adapters/notifier/provider.d.mts +32 -0
  27. package/dist/adapters/notifier/types.d.mts +56 -0
  28. package/dist/adapters/setup-runner.d.mts +23 -0
  29. package/dist/adapters/setup-runner.mjs +149 -0
  30. package/dist/adapters/setup-runner.mjs.map +1 -0
  31. package/dist/adapters/validate.d.mts +30 -0
  32. package/dist/adapters/validate.mjs +313 -0
  33. package/dist/adapters/validate.mjs.map +1 -0
  34. package/dist/auth.d.mts +94 -0
  35. package/dist/auth.mjs +4 -0
  36. package/dist/brand-packs.d.mts +16 -0
  37. package/dist/brand-packs.mjs +130 -0
  38. package/dist/brand-packs.mjs.map +1 -0
  39. package/dist/capture/adapter.d.mts +51 -1
  40. package/dist/capture/adapter.mjs.map +1 -1
  41. package/dist/capture/affected.d.mts +16 -0
  42. package/dist/capture/affected.mjs +64 -0
  43. package/dist/capture/affected.mjs.map +1 -0
  44. package/dist/capture/diff-record.mjs +30 -0
  45. package/dist/capture/diff-record.mjs.map +1 -0
  46. package/dist/capture/dispatch.d.mts +36 -0
  47. package/dist/capture/dispatch.mjs +88 -31
  48. package/dist/capture/dispatch.mjs.map +1 -1
  49. package/dist/capture/guards.mjs +31 -0
  50. package/dist/capture/guards.mjs.map +1 -0
  51. package/dist/capture/orchestrator.d.mts +41 -6
  52. package/dist/capture/orchestrator.mjs +151 -28
  53. package/dist/capture/orchestrator.mjs.map +1 -1
  54. package/dist/capture/pipeline.d.mts +30 -16
  55. package/dist/capture/pipeline.mjs +143 -39
  56. package/dist/capture/pipeline.mjs.map +1 -1
  57. package/dist/capture/recovery.d.mts +21 -0
  58. package/dist/capture/recovery.mjs +83 -0
  59. package/dist/capture/recovery.mjs.map +1 -0
  60. package/dist/capture/rediff.d.mts +35 -0
  61. package/dist/capture/rediff.mjs +77 -0
  62. package/dist/capture/rediff.mjs.map +1 -0
  63. package/dist/capture/sizing.d.mts +58 -0
  64. package/dist/capture/sizing.mjs +119 -0
  65. package/dist/capture/sizing.mjs.map +1 -0
  66. package/dist/capture/skip-checks.mjs +3 -3
  67. package/dist/capture/skip-checks.mjs.map +1 -1
  68. package/dist/capture/statics.d.mts +38 -0
  69. package/dist/capture/statics.mjs +38 -4
  70. package/dist/capture/statics.mjs.map +1 -1
  71. package/dist/capture/status-fanout.mjs +7 -2
  72. package/dist/capture/status-fanout.mjs.map +1 -1
  73. package/dist/capture/storybook.d.mts +31 -2
  74. package/dist/capture/storybook.mjs +77 -2
  75. package/dist/capture/storybook.mjs.map +1 -1
  76. package/dist/capture.d.mts +10 -4
  77. package/dist/capture.mjs +7 -2
  78. package/dist/config.d.mts +121 -4
  79. package/dist/config.mjs +55 -8
  80. package/dist/config.mjs.map +1 -1
  81. package/dist/index.d.mts +5 -56
  82. package/dist/index.mjs +4 -284
  83. package/dist/logger.d.mts +26 -17
  84. package/dist/logger.mjs +10 -5
  85. package/dist/logger.mjs.map +1 -1
  86. package/dist/models/baseline-status.d.mts +29 -0
  87. package/dist/models/baseline-status.mjs +26 -0
  88. package/dist/models/baseline-status.mjs.map +1 -0
  89. package/dist/models/baseline.d.mts +33 -0
  90. package/dist/models/baseline.mjs +35 -13
  91. package/dist/models/baseline.mjs.map +1 -1
  92. package/dist/models/build.d.mts +87 -0
  93. package/dist/models/build.mjs +83 -35
  94. package/dist/models/build.mjs.map +1 -1
  95. package/dist/models/capture-attempt.d.mts +40 -0
  96. package/dist/models/capture-attempt.mjs +85 -0
  97. package/dist/models/capture-attempt.mjs.map +1 -0
  98. package/dist/models/capture-log.d.mts +21 -0
  99. package/dist/models/capture-log.mjs +44 -0
  100. package/dist/models/capture-log.mjs.map +1 -0
  101. package/dist/models/comment.d.mts +29 -0
  102. package/dist/models/comment.mjs +11 -8
  103. package/dist/models/comment.mjs.map +1 -1
  104. package/dist/models/content-ref.d.mts +21 -0
  105. package/dist/models/content-ref.mjs +49 -0
  106. package/dist/models/content-ref.mjs.map +1 -0
  107. package/dist/models/credentials.d.mts +36 -0
  108. package/dist/models/credentials.mjs +103 -0
  109. package/dist/models/credentials.mjs.map +1 -0
  110. package/dist/models/label.d.mts +58 -0
  111. package/dist/models/label.mjs +32 -27
  112. package/dist/models/label.mjs.map +1 -1
  113. package/dist/models/member.d.mts +34 -0
  114. package/dist/models/member.mjs +20 -11
  115. package/dist/models/member.mjs.map +1 -1
  116. package/dist/models/notification-channel.d.mts +52 -0
  117. package/dist/models/notification-channel.mjs +93 -0
  118. package/dist/models/notification-channel.mjs.map +1 -0
  119. package/dist/models/notification-subscription.d.mts +44 -0
  120. package/dist/models/notification-subscription.mjs +89 -0
  121. package/dist/models/notification-subscription.mjs.map +1 -0
  122. package/dist/models/project-group-mapping.d.mts +30 -0
  123. package/dist/models/project-group-mapping.mjs +46 -0
  124. package/dist/models/project-group-mapping.mjs.map +1 -0
  125. package/dist/models/project.d.mts +54 -0
  126. package/dist/models/project.mjs +15 -11
  127. package/dist/models/project.mjs.map +1 -1
  128. package/dist/models/snapshot.d.mts +58 -0
  129. package/dist/models/snapshot.mjs +13 -7
  130. package/dist/models/snapshot.mjs.map +1 -1
  131. package/dist/models/status-config.d.mts +38 -0
  132. package/dist/models/status-config.mjs +12 -11
  133. package/dist/models/status-config.mjs.map +1 -1
  134. package/dist/models/token.d.mts +44 -0
  135. package/dist/models/token.mjs +18 -15
  136. package/dist/models/token.mjs.map +1 -1
  137. package/dist/models/user.d.mts +42 -0
  138. package/dist/models/user.mjs +65 -0
  139. package/dist/models/user.mjs.map +1 -0
  140. package/dist/models/webhook.d.mts +50 -0
  141. package/dist/models/webhook.mjs +12 -10
  142. package/dist/models/webhook.mjs.map +1 -1
  143. package/dist/models.d.mts +22 -0
  144. package/dist/models.mjs +23 -0
  145. package/dist/paths.mjs +29 -0
  146. package/dist/paths.mjs.map +1 -1
  147. package/dist/retention.d.mts +45 -0
  148. package/dist/retention.mjs +145 -0
  149. package/dist/retention.mjs.map +1 -0
  150. package/dist/schema/baseline.d.mts +1 -0
  151. package/dist/schema/build.d.mts +8 -0
  152. package/dist/schema/capture-attempt.d.mts +38 -0
  153. package/dist/schema/comment.d.mts +1 -1
  154. package/dist/schema/content-ref.d.mts +13 -0
  155. package/dist/schema/member.d.mts +2 -0
  156. package/dist/schema/notification.d.mts +35 -0
  157. package/dist/schema/project-group-mapping.d.mts +15 -0
  158. package/dist/schema/project.d.mts +4 -0
  159. package/dist/schema/snapshot.d.mts +7 -0
  160. package/dist/schema/token.d.mts +2 -0
  161. package/dist/schema/user.d.mts +11 -1
  162. package/dist/schema.d.mts +9 -46
  163. package/dist/schema.mjs +1 -55
  164. package/dist/test-helpers/create-project.d.mts +15 -0
  165. package/dist/test-helpers/create-project.mjs +28 -0
  166. package/dist/test-helpers/create-project.mjs.map +1 -0
  167. package/dist/test-helpers/database-contract.d.mts +13 -0
  168. package/dist/test-helpers/database-contract.mjs +58 -0
  169. package/dist/test-helpers/database-contract.mjs.map +1 -0
  170. package/dist/test-helpers/fake-adapters.d.mts +14 -0
  171. package/dist/test-helpers/fake-adapters.mjs +20 -0
  172. package/dist/test-helpers/fake-adapters.mjs.map +1 -0
  173. package/dist/test-helpers/fake-database.d.mts +11 -0
  174. package/dist/test-helpers/fake-database.mjs +104 -0
  175. package/dist/test-helpers/fake-database.mjs.map +1 -0
  176. package/dist/test-helpers/fake-storage.d.mts +14 -0
  177. package/dist/test-helpers/fake-storage.mjs +49 -0
  178. package/dist/test-helpers/fake-storage.mjs.map +1 -0
  179. package/dist/test-helpers/fake-tables.mjs +276 -0
  180. package/dist/test-helpers/fake-tables.mjs.map +1 -0
  181. package/dist/test-helpers/git-contract.d.mts +11 -0
  182. package/dist/test-helpers/git-contract.mjs +34 -0
  183. package/dist/test-helpers/git-contract.mjs.map +1 -0
  184. package/dist/test-helpers/notifier-contract.d.mts +11 -0
  185. package/dist/test-helpers/notifier-contract.mjs +34 -0
  186. package/dist/test-helpers/notifier-contract.mjs.map +1 -0
  187. package/dist/test-helpers/queue-contract.d.mts +11 -0
  188. package/dist/test-helpers/queue-contract.mjs +64 -0
  189. package/dist/test-helpers/queue-contract.mjs.map +1 -0
  190. package/dist/test-helpers/runner-contract.d.mts +11 -0
  191. package/dist/test-helpers/runner-contract.mjs +32 -0
  192. package/dist/test-helpers/runner-contract.mjs.map +1 -0
  193. package/dist/test-helpers/sql-chunks.mjs +138 -0
  194. package/dist/test-helpers/sql-chunks.mjs.map +1 -0
  195. package/dist/test-helpers/storage-contract.d.mts +11 -0
  196. package/dist/test-helpers/storage-contract.mjs +87 -0
  197. package/dist/test-helpers/storage-contract.mjs.map +1 -0
  198. package/dist/test-helpers.d.mts +13 -0
  199. package/dist/test-helpers.mjs +14 -0
  200. package/dist/tracing.mjs +63 -0
  201. package/dist/tracing.mjs.map +1 -0
  202. package/dist/types.d.mts +38 -5
  203. package/dist/types.mjs +18 -5
  204. package/dist/types.mjs.map +1 -1
  205. package/dist/urls.d.mts +12 -1
  206. package/dist/urls.mjs +22 -3
  207. package/dist/urls.mjs.map +1 -1
  208. package/dist/utils/encrypt.d.mts +48 -0
  209. package/dist/utils/encrypt.mjs +67 -13
  210. package/dist/utils/encrypt.mjs.map +1 -1
  211. package/dist/utils/hash.d.mts +17 -0
  212. package/dist/utils/hash.mjs +8 -2
  213. package/dist/utils/hash.mjs.map +1 -1
  214. package/dist/utils/http.d.mts +58 -0
  215. package/dist/utils/http.mjs +147 -0
  216. package/dist/utils/http.mjs.map +1 -0
  217. package/dist/utils/mime.d.mts +9 -0
  218. package/dist/utils/mime.mjs +33 -0
  219. package/dist/utils/mime.mjs.map +1 -0
  220. package/dist/utils/path-security.d.mts +12 -0
  221. package/dist/utils/path-security.mjs +24 -0
  222. package/dist/utils/path-security.mjs.map +1 -0
  223. package/dist/utils/redact.d.mts +17 -0
  224. package/dist/utils/redact.mjs +42 -0
  225. package/dist/utils/redact.mjs.map +1 -0
  226. package/dist/utils/timing.d.mts +19 -0
  227. package/dist/utils/timing.mjs +46 -0
  228. package/dist/utils/timing.mjs.map +1 -0
  229. package/dist/utils/ulid.d.mts +10 -0
  230. package/dist/utils/ulid.mjs +8 -8
  231. package/dist/utils/ulid.mjs.map +1 -1
  232. package/dist/utils.d.mts +12 -0
  233. package/dist/utils.mjs +13 -0
  234. package/package.json +69 -28
  235. package/dist/adapter/auth.d.mts +0 -50
  236. package/dist/adapter/auth.mjs +0 -10
  237. package/dist/adapter/auth.mjs.map +0 -1
  238. package/dist/adapter/init.d.mts +0 -58
  239. package/dist/adapter/init.mjs +0 -93
  240. package/dist/adapter/init.mjs.map +0 -1
  241. package/dist/adapters/webhook-events.mjs +0 -55
  242. package/dist/adapters/webhook-events.mjs.map +0 -1
  243. package/dist/ddl.d.mts +0 -8
  244. package/dist/ddl.mjs +0 -158
  245. package/dist/ddl.mjs.map +0 -1
  246. package/dist/index.mjs.map +0 -1
  247. package/dist/middleware/auth-gate.mjs +0 -36
  248. package/dist/middleware/auth-gate.mjs.map +0 -1
  249. package/dist/middleware/csrf.mjs +0 -40
  250. package/dist/middleware/csrf.mjs.map +0 -1
  251. package/dist/middleware/rate-limit.mjs +0 -39
  252. package/dist/middleware/rate-limit.mjs.map +0 -1
  253. package/dist/middleware/request-log.mjs +0 -28
  254. package/dist/middleware/request-log.mjs.map +0 -1
  255. package/dist/middleware/store-scope.mjs +0 -29
  256. package/dist/middleware/store-scope.mjs.map +0 -1
  257. package/dist/pages/build-detail.mjs +0 -339
  258. package/dist/pages/build-detail.mjs.map +0 -1
  259. package/dist/pages/build-diff-comments.mjs +0 -113
  260. package/dist/pages/build-diff-comments.mjs.map +0 -1
  261. package/dist/pages/build-diff-header.mjs +0 -163
  262. package/dist/pages/build-diff-header.mjs.map +0 -1
  263. package/dist/pages/build-diff-nav.mjs +0 -66
  264. package/dist/pages/build-diff-nav.mjs.map +0 -1
  265. package/dist/pages/build-diff-viewer.mjs +0 -197
  266. package/dist/pages/build-diff-viewer.mjs.map +0 -1
  267. package/dist/pages/build-diff.mjs +0 -90
  268. package/dist/pages/build-diff.mjs.map +0 -1
  269. package/dist/pages/compute-jobs.mjs +0 -198
  270. package/dist/pages/compute-jobs.mjs.map +0 -1
  271. package/dist/pages/label-detail.mjs +0 -228
  272. package/dist/pages/label-detail.mjs.map +0 -1
  273. package/dist/pages/login.mjs +0 -78
  274. package/dist/pages/login.mjs.map +0 -1
  275. package/dist/pages/project-builds.mjs +0 -231
  276. package/dist/pages/project-builds.mjs.map +0 -1
  277. package/dist/pages/project-create.mjs +0 -169
  278. package/dist/pages/project-create.mjs.map +0 -1
  279. package/dist/pages/project-settings.mjs +0 -102
  280. package/dist/pages/project-settings.mjs.map +0 -1
  281. package/dist/pages/projects.mjs +0 -184
  282. package/dist/pages/projects.mjs.map +0 -1
  283. package/dist/pages/root.mjs +0 -94
  284. package/dist/pages/root.mjs.map +0 -1
  285. package/dist/pages/settings-general.mjs +0 -192
  286. package/dist/pages/settings-general.mjs.map +0 -1
  287. package/dist/pages/settings-labels.mjs +0 -150
  288. package/dist/pages/settings-labels.mjs.map +0 -1
  289. package/dist/pages/settings-members.mjs +0 -125
  290. package/dist/pages/settings-members.mjs.map +0 -1
  291. package/dist/pages/settings-status.mjs +0 -152
  292. package/dist/pages/settings-status.mjs.map +0 -1
  293. package/dist/pages/settings-tests.mjs +0 -149
  294. package/dist/pages/settings-tests.mjs.map +0 -1
  295. package/dist/pages/settings-tokens.mjs +0 -96
  296. package/dist/pages/settings-tokens.mjs.map +0 -1
  297. package/dist/pages/settings-webhooks.mjs +0 -150
  298. package/dist/pages/settings-webhooks.mjs.map +0 -1
  299. package/dist/pages/storybook.mjs +0 -96
  300. package/dist/pages/storybook.mjs.map +0 -1
  301. package/dist/retention/purge.mjs +0 -70
  302. package/dist/retention/purge.mjs.map +0 -1
  303. package/dist/routers/admin.mjs +0 -42
  304. package/dist/routers/admin.mjs.map +0 -1
  305. package/dist/routers/assets.mjs +0 -25
  306. package/dist/routers/assets.mjs.map +0 -1
  307. package/dist/routers/auth.mjs +0 -78
  308. package/dist/routers/auth.mjs.map +0 -1
  309. package/dist/routers/builds.handlers.mjs +0 -184
  310. package/dist/routers/builds.handlers.mjs.map +0 -1
  311. package/dist/routers/builds.mjs +0 -185
  312. package/dist/routers/builds.mjs.map +0 -1
  313. package/dist/routers/comments.mjs +0 -90
  314. package/dist/routers/comments.mjs.map +0 -1
  315. package/dist/routers/health.mjs +0 -137
  316. package/dist/routers/health.mjs.map +0 -1
  317. package/dist/routers/helpers.mjs +0 -82
  318. package/dist/routers/helpers.mjs.map +0 -1
  319. package/dist/routers/htmx.mjs +0 -20
  320. package/dist/routers/htmx.mjs.map +0 -1
  321. package/dist/routers/labels.mjs +0 -110
  322. package/dist/routers/labels.mjs.map +0 -1
  323. package/dist/routers/media.mjs +0 -55
  324. package/dist/routers/media.mjs.map +0 -1
  325. package/dist/routers/members.mjs +0 -102
  326. package/dist/routers/members.mjs.map +0 -1
  327. package/dist/routers/projects.mjs +0 -122
  328. package/dist/routers/projects.mjs.map +0 -1
  329. package/dist/routers/schemas.mjs +0 -224
  330. package/dist/routers/schemas.mjs.map +0 -1
  331. package/dist/routers/settings-general.mjs +0 -95
  332. package/dist/routers/settings-general.mjs.map +0 -1
  333. package/dist/routers/settings-labels.mjs +0 -49
  334. package/dist/routers/settings-labels.mjs.map +0 -1
  335. package/dist/routers/settings-members.mjs +0 -30
  336. package/dist/routers/settings-members.mjs.map +0 -1
  337. package/dist/routers/settings-status.mjs +0 -73
  338. package/dist/routers/settings-status.mjs.map +0 -1
  339. package/dist/routers/settings-tokens.mjs +0 -40
  340. package/dist/routers/settings-tokens.mjs.map +0 -1
  341. package/dist/routers/settings-webhooks.mjs +0 -63
  342. package/dist/routers/settings-webhooks.mjs.map +0 -1
  343. package/dist/routers/settings.handlers.mjs +0 -93
  344. package/dist/routers/settings.handlers.mjs.map +0 -1
  345. package/dist/routers/settings.mjs +0 -23
  346. package/dist/routers/settings.mjs.map +0 -1
  347. package/dist/routers/snapshots.mjs +0 -144
  348. package/dist/routers/snapshots.mjs.map +0 -1
  349. package/dist/routers/status-configs.mjs +0 -101
  350. package/dist/routers/status-configs.mjs.map +0 -1
  351. package/dist/routers/storybook.mjs +0 -120
  352. package/dist/routers/storybook.mjs.map +0 -1
  353. package/dist/routers/tokens.mjs +0 -81
  354. package/dist/routers/tokens.mjs.map +0 -1
  355. package/dist/routers/ui.mjs +0 -165
  356. package/dist/routers/ui.mjs.map +0 -1
  357. package/dist/routers/webhooks.mjs +0 -90
  358. package/dist/routers/webhooks.mjs.map +0 -1
  359. package/dist/schema/baseline.mjs +0 -22
  360. package/dist/schema/baseline.mjs.map +0 -1
  361. package/dist/schema/build.mjs +0 -29
  362. package/dist/schema/build.mjs.map +0 -1
  363. package/dist/schema/comment.mjs +0 -26
  364. package/dist/schema/comment.mjs.map +0 -1
  365. package/dist/schema/label.mjs +0 -30
  366. package/dist/schema/label.mjs.map +0 -1
  367. package/dist/schema/member.mjs +0 -19
  368. package/dist/schema/member.mjs.map +0 -1
  369. package/dist/schema/project.mjs +0 -25
  370. package/dist/schema/project.mjs.map +0 -1
  371. package/dist/schema/snapshot.mjs +0 -34
  372. package/dist/schema/snapshot.mjs.map +0 -1
  373. package/dist/schema/status-config.mjs +0 -20
  374. package/dist/schema/status-config.mjs.map +0 -1
  375. package/dist/schema/token.mjs +0 -19
  376. package/dist/schema/token.mjs.map +0 -1
  377. package/dist/schema/user.mjs +0 -19
  378. package/dist/schema/user.mjs.map +0 -1
  379. package/dist/schema/webhook.mjs +0 -20
  380. package/dist/schema/webhook.mjs.map +0 -1
  381. package/dist/schema.mjs.map +0 -1
  382. package/dist/store.mjs +0 -20
  383. package/dist/store.mjs.map +0 -1
  384. package/dist/ui/document.mjs +0 -348
  385. package/dist/ui/document.mjs.map +0 -1
  386. package/dist/ui/feedback.mjs +0 -24
  387. package/dist/ui/feedback.mjs.map +0 -1
  388. package/dist/ui/forms.mjs +0 -117
  389. package/dist/ui/forms.mjs.map +0 -1
  390. package/dist/ui/styles.mjs +0 -174
  391. package/dist/ui/styles.mjs.map +0 -1
  392. package/dist/ui/theme.mjs +0 -44
  393. package/dist/ui/theme.mjs.map +0 -1
@@ -0,0 +1,35 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DatabaseAdapter } from "../adapter/database.mjs";
4
+ import { StorageAdapter } from "../adapter/storage.mjs";
5
+ import { Logger } from "../logger.mjs";
6
+ import { Build } from "../schema/build.mjs";
7
+ import { Project } from "../schema/project.mjs";
8
+ //#region src/capture/rediff.d.ts
9
+ /** Dependencies for {@link rediffBuild}. */
10
+ interface RediffDeps {
11
+ db: DatabaseAdapter;
12
+ storage: StorageAdapter;
13
+ logger?: Logger;
14
+ }
15
+ /** Outcome of a re-diff run. */
16
+ interface RediffResult {
17
+ /** Snapshots diffed against their current baseline. */
18
+ rediffed: number;
19
+ /** Snapshots whose status changed (for example `changed` to `unchanged`). */
20
+ statusChanged: number;
21
+ /** Snapshot ids skipped because their stored screenshot is gone. */
22
+ missingScreenshots: string[];
23
+ }
24
+ /** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */
25
+ declare const REDIFFABLE_BUILD_STATUSES: ReadonlySet<string>;
26
+ /**
27
+ * Re-diff the build's undecided snapshots against the baselines that apply today.
28
+ *
29
+ * Approved and rejected snapshots are decisions and stay untouched; inherited snapshots were never
30
+ * rendered. The caller refreshes the build rollup afterwards.
31
+ */
32
+ declare function rediffBuild(deps: RediffDeps, project: Project, build: Build): Promise<RediffResult>;
33
+ //#endregion
34
+ export { REDIFFABLE_BUILD_STATUSES, RediffDeps, RediffResult, rediffBuild };
35
+ //# sourceMappingURL=rediff.d.mts.map
@@ -0,0 +1,77 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { diffPath } from "../paths.mjs";
5
+ import { BaselineModel } from "../models/baseline.mjs";
6
+ import { NO_BASELINE } from "../models/baseline-status.mjs";
7
+ import { SnapshotModel } from "../models/snapshot.mjs";
8
+ import { diffStoredScreenshots, writeDiffOverlay } from "./diff-record.mjs";
9
+ import { diffStatus } from "./pipeline.mjs";
10
+ //#region src/capture/rediff.ts
11
+ /** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */
12
+ const REDIFFABLE_BUILD_STATUSES = /* @__PURE__ */ new Set([
13
+ "reviewing",
14
+ "approved",
15
+ "rejected"
16
+ ]);
17
+ const REDIFFABLE_SNAPSHOT_STATUSES = /* @__PURE__ */ new Set([
18
+ "new",
19
+ "changed",
20
+ "unchanged"
21
+ ]);
22
+ /**
23
+ * Re-diff the build's undecided snapshots against the baselines that apply today.
24
+ *
25
+ * Approved and rejected snapshots are decisions and stay untouched; inherited snapshots were never
26
+ * rendered. The caller refreshes the build rollup afterwards.
27
+ */
28
+ async function rediffBuild(deps, project, build) {
29
+ const rows = (await new SnapshotModel(deps.db).listByBuild(build.id)).filter((row) => !row.inherited && REDIFFABLE_SNAPSHOT_STATUSES.has(row.status));
30
+ const outcomes = await Promise.all(rows.map(async (row) => await rediffSnapshot(deps, project, build, row)));
31
+ const result = {
32
+ rediffed: outcomes.filter((outcome) => outcome !== "missing").length,
33
+ statusChanged: outcomes.filter((outcome) => outcome === "changed").length,
34
+ missingScreenshots: rows.filter((_row, index) => outcomes[index] === "missing").map((row) => row.id)
35
+ };
36
+ deps.logger?.info({
37
+ buildId: build.id,
38
+ ...result
39
+ }, "build re-diffed against current baselines");
40
+ return result;
41
+ }
42
+ async function rediffSnapshot(deps, project, build, row) {
43
+ if (!await deps.storage.exists(row.screenshotPath)) return "missing";
44
+ const baseline = await new BaselineModel(deps.db).resolve(project.id, row.storyId, row.viewportName, build.gitBranch, project.gitDefaultBranch);
45
+ const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();
46
+ await new SnapshotModel(deps.db).update(row.id, patch);
47
+ return patch.status === row.status ? "same" : "changed";
48
+ }
49
+ function noBaselinePatch() {
50
+ return {
51
+ status: "new",
52
+ diffPath: null,
53
+ diffPixels: null,
54
+ diffRatio: null,
55
+ diffPassed: null,
56
+ baselineId: null,
57
+ baselineVersion: NO_BASELINE
58
+ };
59
+ }
60
+ async function diffPatch(deps, project, build, row, baseline) {
61
+ const result = await diffStoredScreenshots(deps.storage, project, baseline.screenshotPath, row.screenshotPath);
62
+ const target = diffPath(project.id, build.id, row.storyId, row.viewportName);
63
+ const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);
64
+ return {
65
+ status: diffStatus(result.passed, false),
66
+ diffPath: overlay,
67
+ diffPixels: result.diffPixels,
68
+ diffRatio: result.diffRatio,
69
+ diffPassed: result.passed,
70
+ baselineId: baseline.id,
71
+ baselineVersion: baseline.updatedAt
72
+ };
73
+ }
74
+ //#endregion
75
+ export { REDIFFABLE_BUILD_STATUSES, rediffBuild };
76
+
77
+ //# sourceMappingURL=rediff.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rediff.mjs","names":[],"sources":["../../src/capture/rediff.ts"],"sourcesContent":["/** Recompute a build's diffs against the baselines that apply now, without re-rendering. */\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { NO_BASELINE } from \"../models/baseline-status.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { Snapshot } from \"../schema/snapshot.ts\";\nimport { diffPath } from \"../utils/paths.ts\";\nimport { diffStoredScreenshots, writeDiffOverlay } from \"./diff-record.ts\";\nimport { diffStatus } from \"./pipeline.ts\";\n\n/** Dependencies for {@link rediffBuild}. */\nexport interface RediffDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n logger?: Logger;\n}\n\n/** Outcome of a re-diff run. */\nexport interface RediffResult {\n /** Snapshots diffed against their current baseline. */\n rediffed: number;\n /** Snapshots whose status changed (for example `changed` to `unchanged`). */\n statusChanged: number;\n /** Snapshot ids skipped because their stored screenshot is gone. */\n missingScreenshots: string[];\n}\n\n/** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */\nexport const REDIFFABLE_BUILD_STATUSES: ReadonlySet<string> = new Set([\n \"reviewing\",\n \"approved\",\n \"rejected\",\n]);\n\nconst REDIFFABLE_SNAPSHOT_STATUSES: ReadonlySet<string> = new Set([\"new\", \"changed\", \"unchanged\"]);\n\n/**\n * Re-diff the build's undecided snapshots against the baselines that apply today.\n *\n * Approved and rejected snapshots are decisions and stay untouched; inherited snapshots were never\n * rendered. The caller refreshes the build rollup afterwards.\n */\nexport async function rediffBuild(\n deps: RediffDeps,\n project: Project,\n build: Build,\n): Promise<RediffResult> {\n const snapshots = new SnapshotModel(deps.db);\n const rows = (await snapshots.listByBuild(build.id)).filter(\n (row) => !row.inherited && REDIFFABLE_SNAPSHOT_STATUSES.has(row.status),\n );\n const outcomes = await Promise.all(\n rows.map(async (row) => await rediffSnapshot(deps, project, build, row)),\n );\n const result: RediffResult = {\n rediffed: outcomes.filter((outcome) => outcome !== \"missing\").length,\n statusChanged: outcomes.filter((outcome) => outcome === \"changed\").length,\n missingScreenshots: rows\n .filter((_row, index) => outcomes[index] === \"missing\")\n .map((row) => row.id),\n };\n deps.logger?.info({ buildId: build.id, ...result }, \"build re-diffed against current baselines\");\n return result;\n}\n\ntype Outcome = \"same\" | \"changed\" | \"missing\";\n\nasync function rediffSnapshot(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n): Promise<Outcome> {\n if (!(await deps.storage.exists(row.screenshotPath))) {\n return \"missing\";\n }\n const baseline = await new BaselineModel(deps.db).resolve(\n project.id,\n row.storyId,\n row.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n );\n const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();\n await new SnapshotModel(deps.db).update(row.id, patch);\n return patch.status === row.status ? \"same\" : \"changed\";\n}\n\nfunction noBaselinePatch(): Partial<Snapshot> {\n return {\n status: \"new\",\n diffPath: null,\n diffPixels: null,\n diffRatio: null,\n diffPassed: null,\n baselineId: null,\n baselineVersion: NO_BASELINE,\n };\n}\n\nasync function diffPatch(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n baseline: Baseline,\n): Promise<Partial<Snapshot>> {\n const result = await diffStoredScreenshots(\n deps.storage,\n project,\n baseline.screenshotPath,\n row.screenshotPath,\n );\n const target = diffPath(project.id, build.id, row.storyId, row.viewportName);\n const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);\n return {\n status: diffStatus(result.passed, false),\n diffPath: overlay,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n baselineId: baseline.id,\n baselineVersion: baseline.updatedAt,\n };\n}\n"],"mappings":";;;;;;;;;;;AAiCA,MAAa,4CAAiD,IAAI,IAAI;CACpE;CACA;CACA;AACF,CAAC;AAED,MAAM,+CAAoD,IAAI,IAAI;CAAC;CAAO;CAAW;AAAW,CAAC;;;;;;;AAQjG,eAAsB,YACpB,MACA,SACA,OACuB;CAEvB,MAAM,QAAQ,MAAM,IADE,cAAc,KAAK,EACb,CAAC,CAAC,YAAY,MAAM,EAAE,EAAA,CAAG,QAClD,QAAQ,CAAC,IAAI,aAAa,6BAA6B,IAAI,IAAI,MAAM,CACxE;CACA,MAAM,WAAW,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,QAAQ,MAAM,eAAe,MAAM,SAAS,OAAO,GAAG,CAAC,CACzE;CACA,MAAM,SAAuB;EAC3B,UAAU,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EAC9D,eAAe,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EACnE,oBAAoB,KACjB,QAAQ,MAAM,UAAU,SAAS,WAAW,SAAS,CAAC,CACtD,KAAK,QAAQ,IAAI,EAAE;CACxB;CACA,KAAK,QAAQ,KAAK;EAAE,SAAS,MAAM;EAAI,GAAG;CAAO,GAAG,2CAA2C;CAC/F,OAAO;AACT;AAIA,eAAe,eACb,MACA,SACA,OACA,KACkB;CAClB,IAAI,CAAE,MAAM,KAAK,QAAQ,OAAO,IAAI,cAAc,GAChD,OAAO;CAET,MAAM,WAAW,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,QAChD,QAAQ,IACR,IAAI,SACJ,IAAI,cACJ,MAAM,WACN,QAAQ,gBACV;CACA,MAAM,QAAQ,WAAW,MAAM,UAAU,MAAM,SAAS,OAAO,KAAK,QAAQ,IAAI,gBAAgB;CAChG,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,KAAK;CACrD,OAAO,MAAM,WAAW,IAAI,SAAS,SAAS;AAChD;AAEA,SAAS,kBAAqC;CAC5C,OAAO;EACL,QAAQ;EACR,UAAU;EACV,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,YAAY;EACZ,iBAAiB;CACnB;AACF;AAEA,eAAe,UACb,MACA,SACA,OACA,KACA,UAC4B;CAC5B,MAAM,SAAS,MAAM,sBACnB,KAAK,SACL,SACA,SAAS,gBACT,IAAI,cACN;CACA,MAAM,SAAS,SAAS,QAAQ,IAAI,MAAM,IAAI,IAAI,SAAS,IAAI,YAAY;CAC3E,MAAM,UAAU,MAAM,iBAAiB,KAAK,SAAS,QAAQ,QAAQ,OAAO,MAAM;CAClF,OAAO;EACL,QAAQ,WAAW,OAAO,QAAQ,KAAK;EACvC,UAAU;EACV,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,YAAY,OAAO;EACnB,YAAY,SAAS;EACrB,iBAAiB,SAAS;CAC5B;AACF"}
@@ -0,0 +1,58 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Viewport } from "./adapter.mjs";
4
+ //#region src/capture/sizing.d.ts
5
+ /** Default min/max dimensions for cropped/fullPage screenshots (diff stability). */
6
+ declare const SIZING_DEFAULTS: {
7
+ readonly minWidth: 100;
8
+ readonly minHeight: 100;
9
+ readonly maxWidth: 2000;
10
+ readonly maxHeight: 4000;
11
+ readonly whitespaceThreshold: 0.6;
12
+ };
13
+ /** Bounding box of #storybook-root as returned by Playwright/Puppeteer. */
14
+ interface BoundingBox {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+ /** Options for computing the screenshot plan. */
21
+ interface SizingOptions {
22
+ /** Per-story override: false forces full viewport, true forces crop when possible. */
23
+ autoCrop?: boolean;
24
+ /** Whitespace fraction (0..1) above which we crop; default 0.6. */
25
+ whitespaceThreshold?: number;
26
+ /** Whether overflow should trigger fullPage; default true. */
27
+ fullPageOnOverflow?: boolean;
28
+ /** Min dimensions for the final PNG (clamp after padding). */
29
+ minWidth?: number;
30
+ minHeight?: number;
31
+ /** Max dimensions for the final PNG (clamp after padding). */
32
+ maxWidth?: number;
33
+ maxHeight?: number;
34
+ }
35
+ /** Result of the sizing decision — what the runner should do. */
36
+ interface ScreenshotPlan {
37
+ /** Viewport to set on the page before capture. */
38
+ viewport: Viewport;
39
+ /** Clip to use for page.screenshot({clip}). Undefined means full viewport. */
40
+ clip?: {
41
+ x: number;
42
+ y: number;
43
+ width: number;
44
+ height: number;
45
+ };
46
+ /** Whether to use fullPage screenshot (for overflow). */
47
+ fullPage: boolean;
48
+ }
49
+ /**
50
+ * Compute the screenshot plan for a single story×viewport.
51
+ * Single prod owner for sizing logic — runners import this, never duplicate.
52
+ */
53
+ declare function getScreenshotPlan(viewport: Viewport, box: BoundingBox | null, options?: SizingOptions): ScreenshotPlan;
54
+ /** Resolve viewports from project/server config with fallback to default (not sizing, just list). */
55
+ declare function resolveViewports(rawViewports: string | null | undefined, fallback: Viewport[], serverViewports?: Viewport[]): Viewport[];
56
+ //#endregion
57
+ export { BoundingBox, SIZING_DEFAULTS, ScreenshotPlan, SizingOptions, getScreenshotPlan, resolveViewports };
58
+ //# sourceMappingURL=sizing.d.mts.map
@@ -0,0 +1,119 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { sha256 } from "../utils/hash.mjs";
5
+ //#region src/capture/sizing.ts
6
+ /** Padding around the cropped component (8px each side). */
7
+ const PAD = 16;
8
+ const HALF_PAD = 8;
9
+ /** Default min/max dimensions for cropped/fullPage screenshots (diff stability). */
10
+ const SIZING_DEFAULTS = {
11
+ minWidth: 100,
12
+ minHeight: 100,
13
+ maxWidth: 2e3,
14
+ maxHeight: 4e3,
15
+ whitespaceThreshold: .6
16
+ };
17
+ /** Clamp value between min and max. */
18
+ function clamp(value, min, max) {
19
+ return Math.max(min, Math.min(max, value));
20
+ }
21
+ function isOverflowing(box, viewport) {
22
+ return box.width > viewport.width || box.height > viewport.height;
23
+ }
24
+ function whitespaceOf(box, viewport) {
25
+ const viewportArea = viewport.width * viewport.height;
26
+ const boxArea = box.width * box.height;
27
+ const coverage = viewportArea > 0 ? boxArea / viewportArea : 1;
28
+ return 1 - Math.min(coverage, 1);
29
+ }
30
+ function shouldCrop(box, viewport, opts, threshold) {
31
+ if (opts.autoCrop === true) return true;
32
+ return whitespaceOf(box, viewport) > threshold;
33
+ }
34
+ function paddedClip(box, minW, maxW, minH, maxH) {
35
+ const w = clamp(Math.ceil(box.width + PAD), minW, maxW);
36
+ const h = clamp(Math.ceil(box.height + PAD), minH, maxH);
37
+ return {
38
+ x: Math.max(0, box.x - HALF_PAD),
39
+ y: Math.max(0, box.y - HALF_PAD),
40
+ width: w,
41
+ height: h
42
+ };
43
+ }
44
+ /**
45
+ * Compute the screenshot plan for a single story×viewport.
46
+ * Single prod owner for sizing logic — runners import this, never duplicate.
47
+ */
48
+ function getScreenshotPlan(viewport, box, options = {}) {
49
+ if (options.autoCrop === false) return {
50
+ viewport,
51
+ fullPage: false
52
+ };
53
+ if (!box) return {
54
+ viewport,
55
+ fullPage: false
56
+ };
57
+ if (isOverflowing(box, viewport) && (options.fullPageOnOverflow ?? true)) return {
58
+ viewport,
59
+ fullPage: true
60
+ };
61
+ if (isOverflowing(box, viewport)) return grownViewportPlan(viewport, box, options);
62
+ return maybeCroppedPlan(viewport, box, options);
63
+ }
64
+ function grownViewportPlan(viewport, box, options) {
65
+ const maxH = options.maxHeight ?? SIZING_DEFAULTS.maxHeight;
66
+ const grownHeight = clamp(Math.ceil(box.height + PAD), viewport.height, maxH);
67
+ return {
68
+ viewport: {
69
+ ...viewport,
70
+ height: grownHeight
71
+ },
72
+ fullPage: false
73
+ };
74
+ }
75
+ function maybeCroppedPlan(viewport, box, options) {
76
+ if (!shouldCrop(box, viewport, options, options.whitespaceThreshold ?? SIZING_DEFAULTS.whitespaceThreshold)) return {
77
+ viewport,
78
+ fullPage: false
79
+ };
80
+ const minW = options.minWidth ?? SIZING_DEFAULTS.minWidth;
81
+ const minH = options.minHeight ?? SIZING_DEFAULTS.minHeight;
82
+ const clip = paddedClip(box, minW, options.maxWidth ?? SIZING_DEFAULTS.maxWidth, minH, options.maxHeight ?? SIZING_DEFAULTS.maxHeight);
83
+ return {
84
+ viewport,
85
+ clip: {
86
+ x: Math.round(clip.x),
87
+ y: Math.round(clip.y),
88
+ width: clip.width,
89
+ height: clip.height
90
+ },
91
+ fullPage: false
92
+ };
93
+ }
94
+ /** Resolve viewports from project/server config with fallback to default (not sizing, just list). */
95
+ function resolveViewports(rawViewports, fallback, serverViewports) {
96
+ if (serverViewports && serverViewports.length > 0) return serverViewports;
97
+ if (rawViewports) return parsedViewports(rawViewports) ?? fallback;
98
+ return fallback;
99
+ }
100
+ function parsedViewports(raw) {
101
+ try {
102
+ const parsed = JSON.parse(raw);
103
+ if (Array.isArray(parsed) && parsed.length > 0) return parsed;
104
+ } catch {}
105
+ return null;
106
+ }
107
+ /** Hash infra determinism: browser + viewports + sizing defaults. */
108
+ function infraHashFor(browser, viewports, defaults = SIZING_DEFAULTS) {
109
+ const payload = JSON.stringify({
110
+ browser,
111
+ viewports,
112
+ defaults
113
+ });
114
+ return sha256(payload);
115
+ }
116
+ //#endregion
117
+ export { SIZING_DEFAULTS, getScreenshotPlan, infraHashFor, resolveViewports };
118
+
119
+ //# sourceMappingURL=sizing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sizing.mjs","names":[],"sources":["../../src/capture/sizing.ts"],"sourcesContent":["import { sha256 } from \"../utils/hash.ts\";\nimport type { Viewport } from \"./adapter.ts\";\n\n/** Padding around the cropped component (8px each side). */\nconst PAD = 16;\nconst HALF_PAD = 8;\n\n/** Default min/max dimensions for cropped/fullPage screenshots (diff stability). */\nexport const SIZING_DEFAULTS = {\n minWidth: 100,\n minHeight: 100,\n maxWidth: 2000,\n maxHeight: 4000,\n whitespaceThreshold: 0.6,\n} as const;\n\n/** Bounding box of #storybook-root as returned by Playwright/Puppeteer. */\nexport interface BoundingBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** Options for computing the screenshot plan. */\nexport interface SizingOptions {\n /** Per-story override: false forces full viewport, true forces crop when possible. */\n autoCrop?: boolean;\n /** Whitespace fraction (0..1) above which we crop; default 0.6. */\n whitespaceThreshold?: number;\n /** Whether overflow should trigger fullPage; default true. */\n fullPageOnOverflow?: boolean;\n /** Min dimensions for the final PNG (clamp after padding). */\n minWidth?: number;\n minHeight?: number;\n /** Max dimensions for the final PNG (clamp after padding). */\n maxWidth?: number;\n maxHeight?: number;\n}\n\n/** Result of the sizing decision — what the runner should do. */\nexport interface ScreenshotPlan {\n /** Viewport to set on the page before capture. */\n viewport: Viewport;\n /** Clip to use for page.screenshot({clip}). Undefined means full viewport. */\n clip?: { x: number; y: number; width: number; height: number };\n /** Whether to use fullPage screenshot (for overflow). */\n fullPage: boolean;\n}\n\n/** Clamp value between min and max. */\nfunction clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, value));\n}\n\nfunction isOverflowing(box: BoundingBox, viewport: Viewport): boolean {\n return box.width > viewport.width || box.height > viewport.height;\n}\n\nfunction whitespaceOf(box: BoundingBox, viewport: Viewport): number {\n const viewportArea = viewport.width * viewport.height;\n const boxArea = box.width * box.height;\n const coverage = viewportArea > 0 ? boxArea / viewportArea : 1;\n return 1 - Math.min(coverage, 1);\n}\n\nfunction shouldCrop(\n box: BoundingBox,\n viewport: Viewport,\n opts: SizingOptions,\n threshold: number,\n): boolean {\n if (opts.autoCrop === true) return true;\n return whitespaceOf(box, viewport) > threshold;\n}\n\nfunction paddedClip(\n box: BoundingBox,\n minW: number,\n maxW: number,\n minH: number,\n maxH: number,\n): BoundingBox {\n const w = clamp(Math.ceil(box.width + PAD), minW, maxW);\n const h = clamp(Math.ceil(box.height + PAD), minH, maxH);\n return {\n x: Math.max(0, box.x - HALF_PAD),\n y: Math.max(0, box.y - HALF_PAD),\n width: w,\n height: h,\n };\n}\n\n/**\n * Compute the screenshot plan for a single story×viewport.\n * Single prod owner for sizing logic — runners import this, never duplicate.\n */\nexport function getScreenshotPlan(\n viewport: Viewport,\n box: BoundingBox | null,\n options: SizingOptions = {},\n): ScreenshotPlan {\n if (options.autoCrop === false) return { viewport, fullPage: false };\n if (!box) return { viewport, fullPage: false };\n if (isOverflowing(box, viewport) && (options.fullPageOnOverflow ?? true)) {\n return { viewport, fullPage: true };\n }\n if (isOverflowing(box, viewport)) {\n return grownViewportPlan(viewport, box, options);\n }\n return maybeCroppedPlan(viewport, box, options);\n}\n\nfunction grownViewportPlan(\n viewport: Viewport,\n box: BoundingBox,\n options: SizingOptions,\n): ScreenshotPlan {\n const maxH = options.maxHeight ?? SIZING_DEFAULTS.maxHeight;\n const grownHeight = clamp(Math.ceil(box.height + PAD), viewport.height, maxH);\n return { viewport: { ...viewport, height: grownHeight }, fullPage: false };\n}\n\nfunction maybeCroppedPlan(\n viewport: Viewport,\n box: BoundingBox,\n options: SizingOptions,\n): ScreenshotPlan {\n const threshold = options.whitespaceThreshold ?? SIZING_DEFAULTS.whitespaceThreshold;\n if (!shouldCrop(box, viewport, options, threshold)) return { viewport, fullPage: false };\n const minW = options.minWidth ?? SIZING_DEFAULTS.minWidth;\n const minH = options.minHeight ?? SIZING_DEFAULTS.minHeight;\n const maxW = options.maxWidth ?? SIZING_DEFAULTS.maxWidth;\n const maxH = options.maxHeight ?? SIZING_DEFAULTS.maxHeight;\n const clip = paddedClip(box, minW, maxW, minH, maxH);\n return {\n viewport,\n clip: { x: Math.round(clip.x), y: Math.round(clip.y), width: clip.width, height: clip.height },\n fullPage: false,\n };\n}\n\n/** Resolve viewports from project/server config with fallback to default (not sizing, just list). */\nexport function resolveViewports(\n rawViewports: string | null | undefined,\n fallback: Viewport[],\n serverViewports?: Viewport[],\n): Viewport[] {\n if (serverViewports && serverViewports.length > 0) return serverViewports;\n if (rawViewports) return parsedViewports(rawViewports) ?? fallback;\n return fallback;\n}\n\nfunction parsedViewports(raw: string): Viewport[] | null {\n try {\n const parsed = JSON.parse(raw) as Viewport[];\n if (Array.isArray(parsed) && parsed.length > 0) return parsed;\n } catch {\n // fallback\n }\n return null;\n}\n\n/** Hash infra determinism: browser + viewports + sizing defaults. */\nexport function infraHashFor(\n browser: string,\n viewports: Viewport[],\n defaults: typeof SIZING_DEFAULTS = SIZING_DEFAULTS,\n): string {\n const payload = JSON.stringify({ browser, viewports, defaults });\n return sha256(payload);\n}\n"],"mappings":"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"}
@@ -5,8 +5,8 @@ import { BuildModel } from "../models/build.mjs";
5
5
  import { StatusConfigModel } from "../models/status-config.mjs";
6
6
  //#region src/capture/skip-checks.ts
7
7
  /** Skip capture when another approved build already covers the same commit. */
8
- async function hasApprovedBuildForSha(db, projectId, sha, excludeBuildId) {
9
- return (await new BuildModel(db).list(projectId)).some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === "approved");
8
+ async function hasApprovedBuildForSha(db, tables, projectId, sha, excludeBuildId) {
9
+ return (await new BuildModel(db, tables).list(projectId)).some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === "approved");
10
10
  }
11
11
  function findRowProvider(providers, row) {
12
12
  return providers.find((p) => p.metadata.kind === row.provider);
@@ -65,7 +65,7 @@ async function checkRow(model, row, providers, sha, branch, logger) {
65
65
  /** True when any configured provider reports the commit already merged. */
66
66
  async function isAlreadyMerged(opts) {
67
67
  if (opts.providers.length === 0) return false;
68
- const model = new StatusConfigModel(opts.db, opts.secret);
68
+ const model = new StatusConfigModel(opts.db, opts.tables, opts.secret);
69
69
  const rows = await model.list(opts.projectId);
70
70
  for (const row of rows) if (await checkRow(model, row, opts.providers, opts.sha, opts.branch, opts.logger)) return true;
71
71
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"skip-checks.mjs","names":[],"sources":["../../src/capture/skip-checks.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport type { ProjectStatusConfig } from \"../schema/status-config.ts\";\n\n/** Skip capture when another approved build already covers the same commit. */\nexport async function hasApprovedBuildForSha(\n db: DatabaseAdapter,\n projectId: string,\n sha: string,\n excludeBuildId: string,\n): Promise<boolean> {\n const builds = await new BuildModel(db).list(projectId);\n return builds.some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === \"approved\");\n}\n\n/** Inputs for checking whether a commit is already merged upstream. */\nexport interface MergeCheck {\n providers: GitHostProvider[];\n sha: string;\n branch: string;\n secret: string | undefined;\n db: DatabaseAdapter;\n projectId: string;\n logger?: Logger;\n}\n\ninterface ParsedRow {\n provider: GitHostProvider;\n config: unknown;\n token: string;\n}\n\nfunction findRowProvider(\n providers: GitHostProvider[],\n row: { provider: string },\n): GitHostProvider | undefined {\n return providers.find((p) => p.metadata.kind === row.provider);\n}\n\nfunction decodeRowConfig(provider: GitHostProvider, configJson: string): unknown {\n try {\n const parsed: unknown = JSON.parse(configJson);\n provider.metadata.schema.parse(parsed);\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction decodeRowToken(model: StatusConfigModel, row: ProjectStatusConfig): string | null {\n try {\n return model.decryptToken(row);\n } catch {\n return null;\n }\n}\n\nfunction parseRow(\n model: StatusConfigModel,\n row: ProjectStatusConfig,\n providers: GitHostProvider[],\n): ParsedRow | null {\n const provider = findRowProvider(providers, row);\n if (!provider) {\n return null;\n }\n const parsed = decodeRowConfig(provider, row.config);\n if (parsed === null) {\n return null;\n }\n const token = decodeRowToken(model, row);\n if (token === null) {\n return null;\n }\n return { provider, config: parsed, token };\n}\n\nasync function checkRowMerged(\n parsed: ParsedRow,\n sha: string,\n branch: string,\n logger?: Logger,\n): Promise<boolean> {\n const adapter = parsed.provider.create({ config: parsed.config, token: parsed.token, logger });\n if (!adapter.isMerged) {\n return false;\n }\n try {\n return await adapter.isMerged({ sha, branch });\n } catch {\n // Ignore provider errors — do not skip on failure\n return false;\n }\n}\n\n/** Check one status-config row for an upstream merge. */\nasync function checkRow(\n model: StatusConfigModel,\n row: ProjectStatusConfig,\n providers: GitHostProvider[],\n sha: string,\n branch: string,\n logger?: Logger,\n): Promise<boolean> {\n const parsed = parseRow(model, row, providers);\n if (!parsed) {\n return false;\n }\n return await checkRowMerged(parsed, sha, branch, logger);\n}\n\n/** True when any configured provider reports the commit already merged. */\nexport async function isAlreadyMerged(opts: MergeCheck): Promise<boolean> {\n if (opts.providers.length === 0) {\n return false;\n }\n const model = new StatusConfigModel(opts.db, opts.secret);\n const rows = await model.list(opts.projectId);\n for (const row of rows) {\n // eslint-disable-next-line no-await-in-loop -- short-circuit on first merged status\n if (await checkRow(model, row, opts.providers, opts.sha, opts.branch, opts.logger)) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";;;;;;;AAQA,eAAsB,uBACpB,IACA,WACA,KACA,gBACkB;CAElB,QAAO,MADc,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,SAAS,EAAA,CACxC,MAAM,MAAM,EAAE,WAAW,OAAO,EAAE,OAAO,kBAAkB,EAAE,WAAW,UAAU;AAClG;AAmBA,SAAS,gBACP,WACA,KAC6B;CAC7B,OAAO,UAAU,MAAM,MAAM,EAAE,SAAS,SAAS,IAAI,QAAQ;AAC/D;AAEA,SAAS,gBAAgB,UAA2B,YAA6B;CAC/E,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,UAAU;EAC7C,SAAS,SAAS,OAAO,MAAM,MAAM;EACrC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,eAAe,OAA0B,KAAyC;CACzF,IAAI;EACF,OAAO,MAAM,aAAa,GAAG;CAC/B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,SACP,OACA,KACA,WACkB;CAClB,MAAM,WAAW,gBAAgB,WAAW,GAAG;CAC/C,IAAI,CAAC,UACH,OAAO;CAET,MAAM,SAAS,gBAAgB,UAAU,IAAI,MAAM;CACnD,IAAI,WAAW,MACb,OAAO;CAET,MAAM,QAAQ,eAAe,OAAO,GAAG;CACvC,IAAI,UAAU,MACZ,OAAO;CAET,OAAO;EAAE;EAAU,QAAQ;EAAQ;CAAM;AAC3C;AAEA,eAAe,eACb,QACA,KACA,QACA,QACkB;CAClB,MAAM,UAAU,OAAO,SAAS,OAAO;EAAE,QAAQ,OAAO;EAAQ,OAAO,OAAO;EAAO;CAAO,CAAC;CAC7F,IAAI,CAAC,QAAQ,UACX,OAAO;CAET,IAAI;EACF,OAAO,MAAM,QAAQ,SAAS;GAAE;GAAK;EAAO,CAAC;CAC/C,QAAQ;EAEN,OAAO;CACT;AACF;;AAGA,eAAe,SACb,OACA,KACA,WACA,KACA,QACA,QACkB;CAClB,MAAM,SAAS,SAAS,OAAO,KAAK,SAAS;CAC7C,IAAI,CAAC,QACH,OAAO;CAET,OAAO,MAAM,eAAe,QAAQ,KAAK,QAAQ,MAAM;AACzD;;AAGA,eAAsB,gBAAgB,MAAoC;CACxE,IAAI,KAAK,UAAU,WAAW,GAC5B,OAAO;CAET,MAAM,QAAQ,IAAI,kBAAkB,KAAK,IAAI,KAAK,MAAM;CACxD,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK,SAAS;CAC5C,KAAK,MAAM,OAAO,MAEhB,IAAI,MAAM,SAAS,OAAO,KAAK,KAAK,WAAW,KAAK,KAAK,KAAK,QAAQ,KAAK,MAAM,GAC/E,OAAO;CAGX,OAAO;AACT"}
1
+ {"version":3,"file":"skip-checks.mjs","names":[],"sources":["../../src/capture/skip-checks.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport { StatusConfigModel, type StatusConfigTables } from \"../models/status-config.ts\";\nimport type { ProjectStatusConfig } from \"../schema/status-config.ts\";\nimport type { SecretInput } from \"../utils/encrypt.ts\";\n\n/** Skip capture when another approved build already covers the same commit. */\nexport async function hasApprovedBuildForSha(\n db: DatabaseAdapter,\n tables: BuildTables,\n projectId: string,\n sha: string,\n excludeBuildId: string,\n): Promise<boolean> {\n const builds = await new BuildModel(db, tables).list(projectId);\n return builds.some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === \"approved\");\n}\n\n/** Inputs for checking whether a commit is already merged upstream. */\nexport interface MergeCheck {\n providers: GitHostProvider[];\n sha: string;\n branch: string;\n secret: SecretInput;\n db: DatabaseAdapter;\n tables: StatusConfigTables;\n projectId: string;\n logger?: Logger;\n}\n\ninterface ParsedRow {\n provider: GitHostProvider;\n config: unknown;\n token: string;\n}\n\nfunction findRowProvider(\n providers: GitHostProvider[],\n row: { provider: string },\n): GitHostProvider | undefined {\n return providers.find((p) => p.metadata.kind === row.provider);\n}\n\nfunction decodeRowConfig(provider: GitHostProvider, configJson: string): unknown {\n try {\n const parsed: unknown = JSON.parse(configJson);\n provider.metadata.schema.parse(parsed);\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction decodeRowToken(model: StatusConfigModel, row: ProjectStatusConfig): string | null {\n try {\n return model.decryptToken(row);\n } catch {\n return null;\n }\n}\n\nfunction parseRow(\n model: StatusConfigModel,\n row: ProjectStatusConfig,\n providers: GitHostProvider[],\n): ParsedRow | null {\n const provider = findRowProvider(providers, row);\n if (!provider) {\n return null;\n }\n const parsed = decodeRowConfig(provider, row.config);\n if (parsed === null) {\n return null;\n }\n const token = decodeRowToken(model, row);\n if (token === null) {\n return null;\n }\n return { provider, config: parsed, token };\n}\n\nasync function checkRowMerged(\n parsed: ParsedRow,\n sha: string,\n branch: string,\n logger?: Logger,\n): Promise<boolean> {\n const adapter = parsed.provider.create({ config: parsed.config, token: parsed.token, logger });\n if (!adapter.isMerged) {\n return false;\n }\n try {\n return await adapter.isMerged({ sha, branch });\n } catch {\n // Ignore provider errors — do not skip on failure\n return false;\n }\n}\n\n/** Check one status-config row for an upstream merge. */\nasync function checkRow(\n model: StatusConfigModel,\n row: ProjectStatusConfig,\n providers: GitHostProvider[],\n sha: string,\n branch: string,\n logger?: Logger,\n): Promise<boolean> {\n const parsed = parseRow(model, row, providers);\n if (!parsed) {\n return false;\n }\n return await checkRowMerged(parsed, sha, branch, logger);\n}\n\n/** True when any configured provider reports the commit already merged. */\nexport async function isAlreadyMerged(opts: MergeCheck): Promise<boolean> {\n if (opts.providers.length === 0) {\n return false;\n }\n const model = new StatusConfigModel(opts.db, opts.tables, opts.secret);\n const rows = await model.list(opts.projectId);\n for (const row of rows) {\n // eslint-disable-next-line no-await-in-loop -- short-circuit on first merged status\n if (await checkRow(model, row, opts.providers, opts.sha, opts.branch, opts.logger)) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";;;;;;;AASA,eAAsB,uBACpB,IACA,QACA,WACA,KACA,gBACkB;CAElB,QAAO,MADc,IAAI,WAAW,IAAI,MAAM,CAAC,CAAC,KAAK,SAAS,EAAA,CAChD,MAAM,MAAM,EAAE,WAAW,OAAO,EAAE,OAAO,kBAAkB,EAAE,WAAW,UAAU;AAClG;AAoBA,SAAS,gBACP,WACA,KAC6B;CAC7B,OAAO,UAAU,MAAM,MAAM,EAAE,SAAS,SAAS,IAAI,QAAQ;AAC/D;AAEA,SAAS,gBAAgB,UAA2B,YAA6B;CAC/E,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,UAAU;EAC7C,SAAS,SAAS,OAAO,MAAM,MAAM;EACrC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,eAAe,OAA0B,KAAyC;CACzF,IAAI;EACF,OAAO,MAAM,aAAa,GAAG;CAC/B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,SACP,OACA,KACA,WACkB;CAClB,MAAM,WAAW,gBAAgB,WAAW,GAAG;CAC/C,IAAI,CAAC,UACH,OAAO;CAET,MAAM,SAAS,gBAAgB,UAAU,IAAI,MAAM;CACnD,IAAI,WAAW,MACb,OAAO;CAET,MAAM,QAAQ,eAAe,OAAO,GAAG;CACvC,IAAI,UAAU,MACZ,OAAO;CAET,OAAO;EAAE;EAAU,QAAQ;EAAQ;CAAM;AAC3C;AAEA,eAAe,eACb,QACA,KACA,QACA,QACkB;CAClB,MAAM,UAAU,OAAO,SAAS,OAAO;EAAE,QAAQ,OAAO;EAAQ,OAAO,OAAO;EAAO;CAAO,CAAC;CAC7F,IAAI,CAAC,QAAQ,UACX,OAAO;CAET,IAAI;EACF,OAAO,MAAM,QAAQ,SAAS;GAAE;GAAK;EAAO,CAAC;CAC/C,QAAQ;EAEN,OAAO;CACT;AACF;;AAGA,eAAe,SACb,OACA,KACA,WACA,KACA,QACA,QACkB;CAClB,MAAM,SAAS,SAAS,OAAO,KAAK,SAAS;CAC7C,IAAI,CAAC,QACH,OAAO;CAET,OAAO,MAAM,eAAe,QAAQ,KAAK,QAAQ,MAAM;AACzD;;AAGA,eAAsB,gBAAgB,MAAoC;CACxE,IAAI,KAAK,UAAU,WAAW,GAC5B,OAAO;CAET,MAAM,QAAQ,IAAI,kBAAkB,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM;CACrE,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK,SAAS;CAC5C,KAAK,MAAM,OAAO,MAEhB,IAAI,MAAM,SAAS,OAAO,KAAK,KAAK,WAAW,KAAK,KAAK,KAAK,QAAQ,KAAK,MAAM,GAC/E,OAAO;CAGX,OAAO;AACT"}
@@ -0,0 +1,38 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DatabaseAdapter } from "../adapter/database.mjs";
4
+ import { StorageAdapter } from "../adapter/storage.mjs";
5
+ //#region src/capture/statics.d.ts
6
+ /**
7
+ * Shared Storybook statics handling: unzip a stored build into a scratch
8
+ * directory and persist the extracted files back to storage. Used by the
9
+ * capture orchestrator and by the upload path for opt-in inline extraction
10
+ * (`maxInlineUnzipSize`); both write identical keys, so either may run first.
11
+ */
12
+ /**
13
+ * Extract a build's stored zip into a dedicated scratch directory.
14
+ *
15
+ * @param storage - Storage holding the uploaded zip.
16
+ * @param scratchDir - Root under which per-build scratch dirs are created.
17
+ * @param projectId - Owning project id.
18
+ * @param buildId - Build whose zip to extract.
19
+ * @returns The extracted Storybook directory.
20
+ */
21
+ declare function extractStorybookToScratch(storage: StorageAdapter, scratchDir: string, projectId: string, buildId: string): Promise<string>;
22
+ /**
23
+ * Persist an extracted Storybook directory to storage for published serving.
24
+ *
25
+ * Uses content-addressed deduplication: each file is hashed and stored once
26
+ * under `content/<hash>`, with a per-build manifest at
27
+ * `${projectId}/builds/${buildId}/storybook/manifest.json`.
28
+ *
29
+ * @param storage - Destination storage adapter.
30
+ * @param sourceDir - Extracted Storybook directory.
31
+ * @param projectId - Owning project id.
32
+ * @param buildId - Build the files belong to.
33
+ * @param db - Optional database for content ref tracking (enables GC).
34
+ */
35
+ declare function persistStorybookStatics(storage: StorageAdapter, sourceDir: string, projectId: string, buildId: string, db?: DatabaseAdapter): Promise<void>;
36
+ //#endregion
37
+ export { extractStorybookToScratch, persistStorybookStatics };
38
+ //# sourceMappingURL=statics.d.mts.map
@@ -2,9 +2,9 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { storybookDir, storybookZipPath } from "../paths.mjs";
5
- import { createWriteStream } from "node:fs";
6
- import { mkdir, readFile, readdir, rm } from "node:fs/promises";
7
5
  import { dirname, join, relative, resolve, sep } from "node:path";
6
+ import { mkdir, readFile, readdir, rm } from "node:fs/promises";
7
+ import { createWriteStream } from "node:fs";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { Parse } from "unzipper";
10
10
  //#region src/capture/statics.ts
@@ -50,19 +50,53 @@ async function awaitWrites(writes) {
50
50
  /**
51
51
  * Persist an extracted Storybook directory to storage for published serving.
52
52
  *
53
+ * Uses content-addressed deduplication: each file is hashed and stored once
54
+ * under `content/<hash>`, with a per-build manifest at
55
+ * `${projectId}/builds/${buildId}/storybook/manifest.json`.
56
+ *
53
57
  * @param storage - Destination storage adapter.
54
58
  * @param sourceDir - Extracted Storybook directory.
55
59
  * @param projectId - Owning project id.
56
60
  * @param buildId - Build the files belong to.
61
+ * @param db - Optional database for content ref tracking (enables GC).
57
62
  */
58
- async function persistStorybookStatics(storage, sourceDir, projectId, buildId) {
63
+ async function persistStorybookStatics(storage, sourceDir, projectId, buildId, db) {
59
64
  const root = resolve(sourceDir);
60
65
  const destinationPrefix = storybookDir(projectId, buildId);
61
66
  const files = await walkFiles(root);
67
+ const manifest = {};
62
68
  await Promise.all(files.map(async (file) => {
63
69
  const rel = relative(root, file);
64
- await storage.write(`${destinationPrefix}/${rel}`, await readFile(file));
70
+ const buffer = await readFile(file);
71
+ const hash = await hashBuffer(buffer);
72
+ const contentPath = `content/${hash}`;
73
+ manifest[rel] = hash;
74
+ if (!await storage.exists(contentPath)) await storage.write(contentPath, buffer);
75
+ await storage.write(`${destinationPrefix}/${rel}`, buffer);
76
+ if (db) await upsertContentRef(db, hash);
65
77
  }));
78
+ await storage.write(`${destinationPrefix}/manifest.json`, Buffer.from(JSON.stringify(manifest)));
79
+ }
80
+ async function hashBuffer(buffer) {
81
+ const { createHash } = await import("node:crypto");
82
+ return createHash("sha256").update(buffer).digest("hex");
83
+ }
84
+ async function upsertContentRef(db, hash) {
85
+ try {
86
+ const contentRefs = db.tables.contentRefs;
87
+ const now = (/* @__PURE__ */ new Date()).toISOString();
88
+ const existing = await db.get(contentRefs, hash);
89
+ if (existing) await db.update(contentRefs, hash, {
90
+ refCount: existing.refCount + 1,
91
+ lastSeenAt: now
92
+ });
93
+ else await db.insert(contentRefs, {
94
+ hash,
95
+ refCount: 1,
96
+ lastSeenAt: now,
97
+ createdAt: now
98
+ });
99
+ } catch {}
66
100
  }
67
101
  function blockedTarget(root, entryName) {
68
102
  const candidate = resolve(join(root, entryName));
@@ -1 +1 @@
1
- {"version":3,"file":"statics.mjs","names":[],"sources":["../../src/capture/statics.ts"],"sourcesContent":["import { createWriteStream } from \"node:fs\";\nimport { mkdir, readdir, readFile, rm } from \"node:fs/promises\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport { Parse, type Entry } from \"unzipper\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { storybookDir, storybookZipPath } from \"../utils/paths.ts\";\n\n/**\n * Shared Storybook statics handling: unzip a stored build into a scratch\n * directory and persist the extracted files back to storage. Used by the\n * capture orchestrator and by the upload path for opt-in inline extraction\n * (`maxInlineUnzipSize`); both write identical keys, so either may run first.\n */\n\n/**\n * Extract a build's stored zip into a dedicated scratch directory.\n *\n * @param storage - Storage holding the uploaded zip.\n * @param scratchDir - Root under which per-build scratch dirs are created.\n * @param projectId - Owning project id.\n * @param buildId - Build whose zip to extract.\n * @returns The extracted Storybook directory.\n */\nexport async function extractStorybookToScratch(\n storage: StorageAdapter,\n scratchDir: string,\n projectId: string,\n buildId: string,\n): Promise<string> {\n const targetDir = join(scratchDir, projectId, \"builds\", buildId, \"storybook\");\n const root = await prepareTargetDir(targetDir);\n const source = await storage.readStream(storybookZipPath(projectId, buildId));\n const writes: Promise<void>[] = [];\n const parseError = await runParser(root, source, writes);\n if (parseError) {\n throw parseError;\n }\n await awaitWrites(writes);\n return targetDir;\n}\n\n/** Reset a scratch target directory, returning its resolved root. */\nasync function prepareTargetDir(targetDir: string): Promise<string> {\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(targetDir, { recursive: true });\n return resolve(targetDir);\n}\n\n/** Await streamed zip writes, throwing the first write failure. */\nasync function awaitWrites(writes: Promise<void>[]): Promise<void> {\n const outcomes = await Promise.allSettled(writes);\n const writeError = outcomes.find(\n (outcome): outcome is PromiseRejectedResult => outcome.status === \"rejected\",\n );\n if (writeError) {\n throw writeError.reason;\n }\n}\n\n/**\n * Persist an extracted Storybook directory to storage for published serving.\n *\n * @param storage - Destination storage adapter.\n * @param sourceDir - Extracted Storybook directory.\n * @param projectId - Owning project id.\n * @param buildId - Build the files belong to.\n */\nexport async function persistStorybookStatics(\n storage: StorageAdapter,\n sourceDir: string,\n projectId: string,\n buildId: string,\n): 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\nfunction blockedTarget(root: string, entryName: string): boolean {\n const candidate = resolve(join(root, entryName));\n return candidate !== root && !candidate.startsWith(root + sep);\n}\n\n/** Pipe one zip entry to disk, tracking the write for later aggregation. */\nfunction handleEntry(\n root: string,\n entry: Entry,\n writes: Promise<void>[],\n fail: (error: unknown) => void,\n): void {\n if (blockedTarget(root, entry.path)) {\n entry.autodrain();\n fail(new Error(`Blocked path traversal in uploaded Storybook: ${entry.path}`));\n return;\n }\n if (entry.type === \"Directory\") {\n entry.autodrain();\n return;\n }\n writeEntry(root, entry, writes);\n}\n\n/** Stream one zip entry to disk, tracking the write for later aggregation. */\nfunction writeEntry(root: string, entry: Entry, writes: Promise<void>[]): void {\n const target = join(root, entry.path);\n const done = (async (): Promise<void> => {\n await mkdir(dirname(target), { recursive: true });\n await pipeline(entry, createWriteStream(target));\n })();\n writes.push(done);\n done.catch(() => {}); // Intentionally empty — aggregated via allSettled by the caller\n}\n\n/** Run the unzipper parser, resolving with the first parse error (or null). */\n// oxlint-disable-next-line typescript/promise-function-async -- executor-style promise wrapper around events\nfunction runParser(\n root: string,\n source: import(\"node:stream\").Readable,\n writes: Promise<void>[],\n): Promise<unknown> {\n return new Promise((settle) => {\n const parser = Parse();\n const fail = (error: unknown): void => {\n source.destroy();\n settle(error);\n };\n parser.on(\"entry\", (entry: Entry) => {\n handleEntry(root, entry, writes, fail);\n });\n parser.on(\"error\", fail);\n parser.on(\"close\", () => {\n settle(null);\n });\n source.on(\"error\", fail);\n source.pipe(parser);\n });\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,eAAsB,0BACpB,SACA,YACA,WACA,SACiB;CACjB,MAAM,YAAY,KAAK,YAAY,WAAW,UAAU,SAAS,WAAW;CAC5E,MAAM,OAAO,MAAM,iBAAiB,SAAS;CAC7C,MAAM,SAAS,MAAM,QAAQ,WAAW,iBAAiB,WAAW,OAAO,CAAC;CAC5E,MAAM,SAA0B,CAAC;CACjC,MAAM,aAAa,MAAM,UAAU,MAAM,QAAQ,MAAM;CACvD,IAAI,YACF,MAAM;CAER,MAAM,YAAY,MAAM;CACxB,OAAO;AACT;;AAGA,eAAe,iBAAiB,WAAoC;CAClE,MAAM,GAAG,WAAW;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;CACpD,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAC1C,OAAO,QAAQ,SAAS;AAC1B;;AAGA,eAAe,YAAY,QAAwC;CAEjE,MAAM,cAAa,MADI,QAAQ,WAAW,MAAM,EAAA,CACpB,MACzB,YAA8C,QAAQ,WAAW,UACpE;CACA,IAAI,YACF,MAAM,WAAW;AAErB;;;;;;;;;AAUA,eAAsB,wBACpB,SACA,WACA,WACA,SACe;CACf,MAAM,OAAO,QAAQ,SAAS;CAC9B,MAAM,oBAAoB,aAAa,WAAW,OAAO;CACzD,MAAM,QAAQ,MAAM,UAAU,IAAI;CAClC,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,SAAS;EACxB,MAAM,MAAM,SAAS,MAAM,IAAI;EAC/B,MAAM,QAAQ,MAAM,GAAG,kBAAkB,GAAG,OAAO,MAAM,SAAS,IAAI,CAAC;CACzE,CAAC,CACH;AACF;AAEA,SAAS,cAAc,MAAc,WAA4B;CAC/D,MAAM,YAAY,QAAQ,KAAK,MAAM,SAAS,CAAC;CAC/C,OAAO,cAAc,QAAQ,CAAC,UAAU,WAAW,OAAO,GAAG;AAC/D;;AAGA,SAAS,YACP,MACA,OACA,QACA,MACM;CACN,IAAI,cAAc,MAAM,MAAM,IAAI,GAAG;EACnC,MAAM,UAAU;EAChB,qBAAK,IAAI,MAAM,iDAAiD,MAAM,MAAM,CAAC;EAC7E;CACF;CACA,IAAI,MAAM,SAAS,aAAa;EAC9B,MAAM,UAAU;EAChB;CACF;CACA,WAAW,MAAM,OAAO,MAAM;AAChC;;AAGA,SAAS,WAAW,MAAc,OAAc,QAA+B;CAC7E,MAAM,SAAS,KAAK,MAAM,MAAM,IAAI;CACpC,MAAM,QAAQ,YAA2B;EACvC,MAAM,MAAM,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;EAChD,MAAM,SAAS,OAAO,kBAAkB,MAAM,CAAC;CACjD,EAAA,CAAG;CACH,OAAO,KAAK,IAAI;CAChB,KAAK,YAAY,CAAC,CAAC;AACrB;;AAIA,SAAS,UACP,MACA,QACA,QACkB;CAClB,OAAO,IAAI,SAAS,WAAW;EAC7B,MAAM,SAAS,MAAM;EACrB,MAAM,QAAQ,UAAyB;GACrC,OAAO,QAAQ;GACf,OAAO,KAAK;EACd;EACA,OAAO,GAAG,UAAU,UAAiB;GACnC,YAAY,MAAM,OAAO,QAAQ,IAAI;EACvC,CAAC;EACD,OAAO,GAAG,SAAS,IAAI;EACvB,OAAO,GAAG,eAAe;GACvB,OAAO,IAAI;EACb,CAAC;EACD,OAAO,GAAG,SAAS,IAAI;EACvB,OAAO,KAAK,MAAM;CACpB,CAAC;AACH;AAEA,eAAe,UAAU,KAAgC;CACvD,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;CAU1D,QAAO,MATc,QAAQ,IAC3B,QAAQ,IAAI,OAAO,UAAU;EAC3B,MAAM,OAAO,KAAK,KAAK,MAAM,IAAI;EACjC,IAAI,MAAM,YAAY,GACpB,OAAO,MAAM,UAAU,IAAI;EAE7B,OAAO,CAAC,IAAI;CACd,CAAC,CACH,EAAA,CACc,KAAK;AACrB"}
1
+ {"version":3,"file":"statics.mjs","names":[],"sources":["../../src/capture/statics.ts"],"sourcesContent":["import { createWriteStream } from \"node:fs\";\nimport { mkdir, readdir, readFile, rm } from \"node:fs/promises\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport { Parse, type Entry } from \"unzipper\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { storybookDir, storybookZipPath } from \"../utils/paths.ts\";\n\n/**\n * Shared Storybook statics handling: unzip a stored build into a scratch\n * directory and persist the extracted files back to storage. Used by the\n * capture orchestrator and by the upload path for opt-in inline extraction\n * (`maxInlineUnzipSize`); both write identical keys, so either may run first.\n */\n\n/**\n * Extract a build's stored zip into a dedicated scratch directory.\n *\n * @param storage - Storage holding the uploaded zip.\n * @param scratchDir - Root under which per-build scratch dirs are created.\n * @param projectId - Owning project id.\n * @param buildId - Build whose zip to extract.\n * @returns The extracted Storybook directory.\n */\nexport async function extractStorybookToScratch(\n storage: StorageAdapter,\n scratchDir: string,\n projectId: string,\n buildId: string,\n): Promise<string> {\n const targetDir = join(scratchDir, projectId, \"builds\", buildId, \"storybook\");\n const root = await prepareTargetDir(targetDir);\n const source = await storage.readStream(storybookZipPath(projectId, buildId));\n const writes: Promise<void>[] = [];\n const parseError = await runParser(root, source, writes);\n if (parseError) {\n throw parseError;\n }\n await awaitWrites(writes);\n return targetDir;\n}\n\n/** Reset a scratch target directory, returning its resolved root. */\nasync function prepareTargetDir(targetDir: string): Promise<string> {\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(targetDir, { recursive: true });\n return resolve(targetDir);\n}\n\n/** Await streamed zip writes, throwing the first write failure. */\nasync function awaitWrites(writes: Promise<void>[]): Promise<void> {\n const outcomes = await Promise.allSettled(writes);\n const writeError = outcomes.find(\n (outcome): outcome is PromiseRejectedResult => outcome.status === \"rejected\",\n );\n if (writeError) {\n throw writeError.reason;\n }\n}\n\n/**\n * Persist an extracted Storybook directory to storage for published serving.\n *\n * Uses content-addressed deduplication: each file is hashed and stored once\n * under `content/<hash>`, with a per-build manifest at\n * `${projectId}/builds/${buildId}/storybook/manifest.json`.\n *\n * @param storage - Destination storage adapter.\n * @param sourceDir - Extracted Storybook directory.\n * @param projectId - Owning project id.\n * @param buildId - Build the files belong to.\n * @param db - Optional database for content ref tracking (enables GC).\n */\nexport async function persistStorybookStatics(\n storage: StorageAdapter,\n sourceDir: string,\n projectId: string,\n buildId: string,\n db?: DatabaseAdapter,\n): Promise<void> {\n const root = resolve(sourceDir);\n const destinationPrefix = storybookDir(projectId, buildId);\n const files = await walkFiles(root);\n const manifest: Record<string, string> = {};\n await Promise.all(\n files.map(async (file) => {\n const rel = relative(root, file);\n const buffer = await readFile(file);\n const hash = await hashBuffer(buffer);\n const contentPath = `content/${hash}`;\n manifest[rel] = hash;\n // Only write if not already exists (dedup)\n if (!(await storage.exists(contentPath))) {\n await storage.write(contentPath, buffer);\n }\n // Also write to legacy per-build path for backward compat (can be removed after migration)\n // and maintain content_refs for GC\n await storage.write(`${destinationPrefix}/${rel}`, buffer);\n if (db) {\n await upsertContentRef(db, hash);\n }\n }),\n );\n await storage.write(`${destinationPrefix}/manifest.json`, Buffer.from(JSON.stringify(manifest)));\n}\n\nasync function hashBuffer(buffer: Buffer): Promise<string> {\n const { createHash } = await import(\"node:crypto\");\n return createHash(\"sha256\").update(buffer).digest(\"hex\");\n}\n\nasync function upsertContentRef(db: DatabaseAdapter, hash: string): Promise<void> {\n try {\n const contentRefs = db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = (await db.get(contentRefs, hash)) as unknown as { refCount: number } | null;\n if (existing) {\n await db.update(contentRefs, hash, {\n refCount: existing.refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await db.insert(contentRefs, { hash, refCount: 1, lastSeenAt: now, createdAt: now });\n }\n } catch {\n // content_refs table may not exist yet (old DB) — ignore\n }\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\n/** Pipe one zip entry to disk, tracking the write for later aggregation. */\nfunction handleEntry(\n root: string,\n entry: Entry,\n writes: Promise<void>[],\n fail: (error: unknown) => void,\n): void {\n if (blockedTarget(root, entry.path)) {\n entry.autodrain();\n fail(new Error(`Blocked path traversal in uploaded Storybook: ${entry.path}`));\n return;\n }\n if (entry.type === \"Directory\") {\n entry.autodrain();\n return;\n }\n writeEntry(root, entry, writes);\n}\n\n/** Stream one zip entry to disk, tracking the write for later aggregation. */\nfunction writeEntry(root: string, entry: Entry, writes: Promise<void>[]): void {\n const target = join(root, entry.path);\n const done = (async (): Promise<void> => {\n await mkdir(dirname(target), { recursive: true });\n await pipeline(entry, createWriteStream(target));\n })();\n writes.push(done);\n done.catch(() => {}); // Intentionally empty — aggregated via allSettled by the caller\n}\n\n/** Run the unzipper parser, resolving with the first parse error (or null). */\n// oxlint-disable-next-line typescript/promise-function-async -- executor-style promise wrapper around events\nfunction runParser(\n root: string,\n source: import(\"node:stream\").Readable,\n writes: Promise<void>[],\n): Promise<unknown> {\n return new Promise((settle) => {\n const parser = Parse();\n const fail = (error: unknown): void => {\n source.destroy();\n settle(error);\n };\n parser.on(\"entry\", (entry: Entry) => {\n handleEntry(root, entry, writes, fail);\n });\n parser.on(\"error\", fail);\n parser.on(\"close\", () => {\n settle(null);\n });\n source.on(\"error\", fail);\n source.pipe(parser);\n });\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"],"mappings":"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"}
@@ -1,13 +1,18 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { StatusConfigModel } from "../models/status-config.mjs";
5
4
  import { buildCommentMarkdown } from "../adapter/git-host/helpers.mjs";
5
+ import { StatusConfigModel } from "../models/status-config.mjs";
6
+ import { LOCAL_SHA_PREFIX } from "@storyshelf/affected";
6
7
  //#region src/capture/status-fanout.ts
7
8
  /** Post build check statuses (and review comments) to every configured git provider. */
8
9
  async function postStatusesForBuild(opts) {
9
10
  if (opts.providers.length === 0) return;
10
- const model = new StatusConfigModel(opts.db, opts.secret);
11
+ if (opts.sha.startsWith(LOCAL_SHA_PREFIX)) {
12
+ opts.logger?.info({ sha: opts.sha }, "skipping git provider status for synthetic local identity");
13
+ return;
14
+ }
15
+ const model = new StatusConfigModel(opts.db, opts.tables, opts.secret);
11
16
  const rows = await model.list(opts.project.id);
12
17
  const ctx = `storyshelf/${opts.project.slug}`;
13
18
  await Promise.allSettled(rows.map(async (row) => {
@@ -1 +1 @@
1
- {"version":3,"file":"status-fanout.mjs","names":[],"sources":["../../src/capture/status-fanout.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { buildCommentMarkdown } from \"../adapters/git-host/helpers.ts\";\nimport type { CheckStatus, GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport type { Project } from \"../schema/project.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({\n context: ctx,\n gitSha: opts.sha,\n status: opts.status,\n url: opts.url,\n });\n if (adapter.upsertComment) {\n const markdown = buildCommentMarkdown(opts.status, opts.url, ctx);\n await adapter\n .upsertComment({ sha: opts.sha, url: opts.url, status: opts.status, markdown })\n .catch((error: unknown) => {\n opts.logger?.warn({ err: error, provider: row.provider }, \"failed to upsert comment\");\n });\n }\n }),\n );\n}\n\nexport { postStatusesForBuild };\n"],"mappings":";;;;;;;AAQA,eAAe,qBAAqB,MASlB;CAChB,IAAI,KAAK,UAAU,WAAW,GAC5B;CAEF,MAAM,QAAQ,IAAI,kBAAkB,KAAK,IAAI,KAAK,MAAM;CACxD,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK,QAAQ,EAAE;CAC7C,MAAM,MAAM,cAAc,KAAK,QAAQ;CACvC,MAAM,QAAQ,WACZ,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,WAAW,KAAK,UAAU,MAAM,MAAM,EAAE,SAAS,SAAS,IAAI,QAAQ;EAC5E,IAAI,CAAC,UAAU;GACb,KAAK,QAAQ,KAAK,EAAE,UAAU,IAAI,SAAS,GAAG,0CAA0C;GACxF;EACF;EACA,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,IAAI,MAAM;GAC9B,SAAS,SAAS,OAAO,MAAM,MAAM;EACvC,SAAS,OAAgB;GACvB,KAAK,QAAQ,KAAK;IAAE,KAAK;IAAO,UAAU,IAAI;GAAS,GAAG,uBAAuB;GACjF;EACF;EACA,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,aAAa,GAAG;EAChC,SAAS,OAAgB;GACvB,KAAK,QAAQ,KAAK,EAAE,KAAK,MAAM,GAAG,gCAAgC;GAClE;EACF;EACA,MAAM,UAAU,SAAS,OAAO;GAAE,QAAQ;GAAQ;GAAO,QAAQ,KAAK;EAAO,CAAC;EAC9E,MAAM,QAAQ,UAAU;GACtB,SAAS;GACT,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,KAAK,KAAK;EACZ,CAAC;EACD,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"}
1
+ {"version":3,"file":"status-fanout.mjs","names":[],"sources":["../../src/capture/status-fanout.ts"],"sourcesContent":["import { LOCAL_SHA_PREFIX } from \"@storyshelf/affected\";\nimport type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { buildCommentMarkdown } from \"../adapters/git-host/helpers.ts\";\nimport type { CheckStatus, GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport { StatusConfigModel, type StatusConfigTables } from \"../models/status-config.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { SecretInput } from \"../utils/encrypt.ts\";\n\n/** Post build check statuses (and review comments) to every configured git provider. */\nasync function postStatusesForBuild(opts: {\n db: DatabaseAdapter;\n tables: StatusConfigTables;\n project: Project;\n sha: string;\n status: CheckStatus;\n url: string;\n providers: GitHostProvider[];\n secret: SecretInput;\n logger?: Logger;\n}): Promise<void> {\n if (opts.providers.length === 0) {\n return;\n }\n if (opts.sha.startsWith(LOCAL_SHA_PREFIX)) {\n opts.logger?.info(\n { sha: opts.sha },\n \"skipping git provider status for synthetic local identity\",\n );\n return;\n }\n const model = new StatusConfigModel(opts.db, opts.tables, 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({\n context: ctx,\n gitSha: opts.sha,\n status: opts.status,\n url: opts.url,\n });\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":";;;;;;;;AAUA,eAAe,qBAAqB,MAUlB;CAChB,IAAI,KAAK,UAAU,WAAW,GAC5B;CAEF,IAAI,KAAK,IAAI,WAAW,gBAAgB,GAAG;EACzC,KAAK,QAAQ,KACX,EAAE,KAAK,KAAK,IAAI,GAChB,2DACF;EACA;CACF;CACA,MAAM,QAAQ,IAAI,kBAAkB,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM;CACrE,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;GACtB,SAAS;GACT,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,KAAK,KAAK;EACZ,CAAC;EACD,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"}
@@ -1,15 +1,44 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- import { StoryEntry, StorySourceAdapter } from "./adapter.mjs";
3
+ import { StoryEntry, StoryParameters, StorySourceAdapter, StoryViewportConfig, Viewport } from "./adapter.mjs";
4
+ import { DepGraph } from "@storyshelf/affected";
4
5
  //#region src/capture/storybook.d.ts
6
+ /** Story parameters as serialized by the Storybook index. */
7
+ interface StorybookParameters {
8
+ chromatic?: StoryParameters;
9
+ storyshelf?: StoryParameters;
10
+ viewport?: StoryViewportConfig;
11
+ }
12
+ /** Merge the `chromatic` and `storyshelf` parameter layers (`storyshelf` wins). */
13
+ declare function mergeParameters(entry: {
14
+ parameters?: StorybookParameters;
15
+ }): StoryParameters | undefined;
16
+ /**
17
+ * The Storybook `MINIMAL_VIEWPORTS` snapshot used to resolve a story's
18
+ * `defaultViewport` name at capture time. Dimensions are the documented
19
+ * stable set; inline `parameters.viewport.viewports` always win over these.
20
+ */
21
+ declare const STORYBOOK_BUILTIN_VIEWPORTS: Readonly<Record<string, Viewport>>;
22
+ /**
23
+ * Resolve the viewports a story should be captured at: the project's global
24
+ * list plus the story's Storybook default viewport (union), deduplicated by
25
+ * name. An unresolvable or duplicate default contributes nothing extra.
26
+ */
27
+ declare function resolveStoryViewports(entry: Pick<StoryEntry, "parameters">, globalViewports: Viewport[]): Viewport[];
5
28
  /** Discovers stories from a built Storybook via its index file. */
6
29
  declare class StorybookAdapter implements StorySourceAdapter {
7
30
  readonly name = "storybook";
8
31
  readonly screenshotSelector = "#storybook-root";
9
32
  discover(source: string): Promise<StoryEntry[]>;
10
33
  buildUrl(baseUrl: string, storyId: string): string;
34
+ /**
35
+ * Load the Vite stats dependency graph of an extracted build for affected
36
+ * capture. Resolves `null` when the build ships no usable stats, in which
37
+ * case the orchestrator renders every story.
38
+ */
39
+ loadDependencyGraph(source: string): Promise<DepGraph | null>;
11
40
  private readIndex;
12
41
  }
13
42
  //#endregion
14
- export { StorybookAdapter };
43
+ export { STORYBOOK_BUILTIN_VIEWPORTS, StorybookAdapter, mergeParameters, resolveStoryViewports };
15
44
  //# sourceMappingURL=storybook.d.mts.map