@fragments-sdk/cli 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,339 @@
1
+ import { chromium, type Browser, type BrowserContext } from "playwright";
2
+ import { BRAND, DEFAULTS, type Viewport } from "../core/index.js";
3
+
4
+ /**
5
+ * Browser pool configuration
6
+ */
7
+ export interface BrowserPoolConfig {
8
+ /** Number of browser contexts to keep warm */
9
+ poolSize?: number;
10
+
11
+ /** Idle timeout before shutdown (ms) */
12
+ idleTimeoutMs?: number;
13
+
14
+ /** Default viewport for contexts */
15
+ viewport?: Viewport;
16
+
17
+ /** Enable headless mode (default: true) */
18
+ headless?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Manages a pool of warm browser contexts for fast screenshot capture.
23
+ *
24
+ * Key features:
25
+ * - Lazy initialization (browser starts on first request)
26
+ * - Pre-warmed contexts for instant capture
27
+ * - Auto-shutdown after idle period
28
+ * - Graceful degradation
29
+ */
30
+ export class BrowserPool {
31
+ private browser: Browser | null = null;
32
+ private contexts: BrowserContext[] = [];
33
+ private available: BrowserContext[] = [];
34
+ private waitingQueue: Array<(ctx: BrowserContext) => void> = [];
35
+ private idleTimeout: NodeJS.Timeout | null = null;
36
+ private initPromise: Promise<void> | null = null;
37
+ private isShuttingDown = false;
38
+
39
+ private readonly poolSize: number;
40
+ private readonly idleTimeoutMs: number;
41
+ private readonly viewport: Viewport;
42
+ private readonly headless: boolean;
43
+
44
+ constructor(config: BrowserPoolConfig = {}) {
45
+ this.poolSize = config.poolSize ?? DEFAULTS.poolSize;
46
+ this.idleTimeoutMs = config.idleTimeoutMs ?? DEFAULTS.idleTimeoutMs;
47
+ this.viewport = config.viewport ?? DEFAULTS.viewport;
48
+ this.headless = config.headless ?? true;
49
+ }
50
+
51
+ /**
52
+ * Check if the pool is initialized and ready
53
+ */
54
+ get isReady(): boolean {
55
+ return this.browser !== null && !this.isShuttingDown;
56
+ }
57
+
58
+ /**
59
+ * Get the current pool size
60
+ */
61
+ get size(): number {
62
+ return this.poolSize;
63
+ }
64
+
65
+ /**
66
+ * Get number of available contexts
67
+ */
68
+ get availableCount(): number {
69
+ return this.available.length;
70
+ }
71
+
72
+ /**
73
+ * Acquire a browser context from the pool.
74
+ * Will initialize the pool on first call.
75
+ * If all contexts are busy, will wait for one to become available.
76
+ */
77
+ async acquire(): Promise<BrowserContext> {
78
+ if (this.isShuttingDown) {
79
+ throw new BrowserPoolError(
80
+ "Cannot acquire context while pool is shutting down",
81
+ "POOL_SHUTTING_DOWN"
82
+ );
83
+ }
84
+
85
+ // Lazy initialization
86
+ if (!this.browser) {
87
+ await this.initialize();
88
+ }
89
+
90
+ // Reset idle timer
91
+ this.resetIdleTimer();
92
+
93
+ // Return available context if one exists
94
+ if (this.available.length > 0) {
95
+ return this.available.pop()!;
96
+ }
97
+
98
+ // All contexts busy - wait for one to free up
99
+ return this.waitForAvailable();
100
+ }
101
+
102
+ /**
103
+ * Release a context back to the pool after use.
104
+ * Clears context state for next user.
105
+ */
106
+ async release(context: BrowserContext): Promise<void> {
107
+ if (this.isShuttingDown) {
108
+ return;
109
+ }
110
+
111
+ // Clear context state (cookies, local storage, etc.)
112
+ await this.clearContext(context);
113
+
114
+ // If someone is waiting, give them this context directly
115
+ if (this.waitingQueue.length > 0) {
116
+ const resolve = this.waitingQueue.shift()!;
117
+ resolve(context);
118
+ return;
119
+ }
120
+
121
+ // Otherwise, return to available pool
122
+ this.available.push(context);
123
+ }
124
+
125
+ /**
126
+ * Pre-warm the browser pool.
127
+ * Call this during dev server startup for instant captures.
128
+ */
129
+ async warmup(): Promise<void> {
130
+ if (this.browser) {
131
+ return; // Already initialized
132
+ }
133
+ await this.initialize();
134
+ }
135
+
136
+ /**
137
+ * Gracefully shut down the pool, closing all contexts and browser.
138
+ */
139
+ async shutdown(): Promise<void> {
140
+ if (this.isShuttingDown || !this.browser) {
141
+ return;
142
+ }
143
+
144
+ this.isShuttingDown = true;
145
+
146
+ // Clear idle timer
147
+ if (this.idleTimeout) {
148
+ clearTimeout(this.idleTimeout);
149
+ this.idleTimeout = null;
150
+ }
151
+
152
+ // Reject any waiting requests
153
+ for (const resolve of this.waitingQueue) {
154
+ // We can't reject directly since we're using resolve, so we'll just let them timeout
155
+ }
156
+ this.waitingQueue = [];
157
+
158
+ // Close all contexts
159
+ for (const context of this.contexts) {
160
+ try {
161
+ await context.close();
162
+ } catch {
163
+ // Ignore errors during shutdown
164
+ }
165
+ }
166
+
167
+ // Close browser
168
+ try {
169
+ await this.browser.close();
170
+ } catch {
171
+ // Ignore errors during shutdown
172
+ }
173
+
174
+ // Reset state
175
+ this.browser = null;
176
+ this.contexts = [];
177
+ this.available = [];
178
+ this.initPromise = null;
179
+ this.isShuttingDown = false;
180
+ }
181
+
182
+ /**
183
+ * Initialize the browser and create the context pool.
184
+ */
185
+ private async initialize(): Promise<void> {
186
+ // Prevent multiple concurrent initializations
187
+ if (this.initPromise) {
188
+ return this.initPromise;
189
+ }
190
+
191
+ this.initPromise = this.doInitialize();
192
+ return this.initPromise;
193
+ }
194
+
195
+ private async doInitialize(): Promise<void> {
196
+ this.browser = await chromium.launch({
197
+ headless: this.headless,
198
+ args: [
199
+ "--disable-gpu",
200
+ "--disable-dev-shm-usage",
201
+ "--disable-extensions",
202
+ "--no-sandbox",
203
+ "--disable-setuid-sandbox",
204
+ ],
205
+ });
206
+
207
+ // Pre-create contexts
208
+ const contextPromises = Array.from({ length: this.poolSize }, () =>
209
+ this.createContext()
210
+ );
211
+
212
+ const contexts = await Promise.all(contextPromises);
213
+ this.contexts = contexts;
214
+ this.available = [...contexts];
215
+
216
+ // Start idle timer
217
+ this.resetIdleTimer();
218
+ }
219
+
220
+ /**
221
+ * Create a new browser context with default settings.
222
+ */
223
+ private async createContext(): Promise<BrowserContext> {
224
+ if (!this.browser) {
225
+ throw new BrowserPoolError(
226
+ "Browser not initialized",
227
+ "BROWSER_NOT_INITIALIZED"
228
+ );
229
+ }
230
+
231
+ return this.browser.newContext({
232
+ viewport: this.viewport,
233
+ deviceScaleFactor: this.viewport.deviceScaleFactor ?? 1,
234
+ // Disable animations for consistent screenshots
235
+ reducedMotion: "reduce",
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Clear context state between uses.
241
+ */
242
+ private async clearContext(context: BrowserContext): Promise<void> {
243
+ try {
244
+ // Clear cookies
245
+ await context.clearCookies();
246
+
247
+ // Close all pages except one (keep one warm)
248
+ const pages = context.pages();
249
+ for (let i = 1; i < pages.length; i++) {
250
+ await pages[i].close();
251
+ }
252
+
253
+ // Navigate first page to blank if it exists
254
+ if (pages.length > 0) {
255
+ await pages[0].goto("about:blank");
256
+ }
257
+ } catch {
258
+ // Ignore errors during cleanup
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Wait for an available context.
264
+ */
265
+ private waitForAvailable(): Promise<BrowserContext> {
266
+ return new Promise((resolve, reject) => {
267
+ // Add timeout to prevent indefinite waiting
268
+ const timeout = setTimeout(() => {
269
+ const index = this.waitingQueue.indexOf(resolve);
270
+ if (index > -1) {
271
+ this.waitingQueue.splice(index, 1);
272
+ }
273
+ reject(
274
+ new BrowserPoolError(
275
+ "Timeout waiting for available browser context",
276
+ "ACQUIRE_TIMEOUT"
277
+ )
278
+ );
279
+ }, 30000); // 30 second timeout
280
+
281
+ // Wrap resolve to clear timeout
282
+ const wrappedResolve = (ctx: BrowserContext) => {
283
+ clearTimeout(timeout);
284
+ resolve(ctx);
285
+ };
286
+
287
+ this.waitingQueue.push(wrappedResolve);
288
+ });
289
+ }
290
+
291
+ /**
292
+ * Reset the idle shutdown timer.
293
+ */
294
+ private resetIdleTimer(): void {
295
+ if (this.idleTimeout) {
296
+ clearTimeout(this.idleTimeout);
297
+ }
298
+
299
+ this.idleTimeout = setTimeout(() => {
300
+ this.shutdown();
301
+ }, this.idleTimeoutMs);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Error class for browser pool errors
307
+ */
308
+ export class BrowserPoolError extends Error {
309
+ constructor(message: string, public readonly code: string) {
310
+ super(message);
311
+ this.name = `${BRAND.name}BrowserPoolError`;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Singleton instance for shared browser pool
317
+ */
318
+ let sharedPool: BrowserPool | null = null;
319
+
320
+ /**
321
+ * Get the shared browser pool instance.
322
+ * Creates one if it doesn't exist.
323
+ */
324
+ export function getSharedPool(config?: BrowserPoolConfig): BrowserPool {
325
+ if (!sharedPool) {
326
+ sharedPool = new BrowserPool(config);
327
+ }
328
+ return sharedPool;
329
+ }
330
+
331
+ /**
332
+ * Shutdown and clear the shared pool instance.
333
+ */
334
+ export async function shutdownSharedPool(): Promise<void> {
335
+ if (sharedPool) {
336
+ await sharedPool.shutdown();
337
+ sharedPool = null;
338
+ }
339
+ }
@@ -0,0 +1,267 @@
1
+ import type { Page } from 'playwright';
2
+ import {
3
+ BRAND,
4
+ DEFAULTS,
5
+ type Screenshot,
6
+ type ScreenshotMetadata,
7
+ type Viewport,
8
+ type Theme,
9
+ } from '../core/index.js';
10
+ import { BrowserPool, BrowserPoolError } from './browser-pool.js';
11
+ import { ServiceError, Timer, computeHash, sleep } from './utils.js';
12
+
13
+ /**
14
+ * Options for capturing a screenshot
15
+ */
16
+ export interface CaptureOptions {
17
+ /** Override default viewport */
18
+ viewport?: Viewport;
19
+
20
+ /** Theme to capture */
21
+ theme?: Theme;
22
+
23
+ /** Wait for specific selector before capture */
24
+ waitForSelector?: string;
25
+
26
+ /** Additional delay after render (ms) */
27
+ delay?: number;
28
+
29
+ /** Timeout for font loading (ms) */
30
+ fontTimeout?: number;
31
+
32
+ /** Whether to disable animations (default: true) */
33
+ disableAnimations?: boolean;
34
+
35
+ /** Clip capture to specific element */
36
+ clipSelector?: string;
37
+ }
38
+
39
+ /**
40
+ * CSS to inject for disabling animations
41
+ */
42
+ const DISABLE_ANIMATIONS_CSS = `
43
+ *, *::before, *::after {
44
+ animation-duration: 0s !important;
45
+ animation-delay: 0s !important;
46
+ transition-duration: 0s !important;
47
+ transition-delay: 0s !important;
48
+ }
49
+ `;
50
+
51
+ /**
52
+ * Capture engine for taking screenshots of component variants.
53
+ */
54
+ export class CaptureEngine {
55
+ constructor(
56
+ private readonly pool: BrowserPool,
57
+ private readonly baseUrl: string
58
+ ) {}
59
+
60
+ /**
61
+ * Capture a single variant screenshot
62
+ */
63
+ async captureVariant(
64
+ component: string,
65
+ variant: string,
66
+ options: CaptureOptions = {}
67
+ ): Promise<Screenshot> {
68
+ const timer = new Timer();
69
+ const context = await this.pool.acquire();
70
+
71
+ try {
72
+ const page = await context.newPage();
73
+
74
+ try {
75
+ // Build the URL for isolated variant rendering
76
+ const url = this.buildVariantUrl(component, variant, options.theme);
77
+
78
+ // Navigate and capture
79
+ const { renderTimeMs, data, captureTimeMs } = await this.navigateAndCapture(
80
+ page,
81
+ url,
82
+ options
83
+ );
84
+
85
+ // Build screenshot object
86
+ const screenshot: Screenshot = {
87
+ data,
88
+ hash: computeHash(data),
89
+ viewport: options.viewport ?? DEFAULTS.viewport,
90
+ capturedAt: new Date(),
91
+ metadata: {
92
+ component,
93
+ variant,
94
+ theme: options.theme ?? DEFAULTS.theme,
95
+ renderTimeMs,
96
+ captureTimeMs,
97
+ },
98
+ };
99
+
100
+ return screenshot;
101
+ } finally {
102
+ await page.close();
103
+ }
104
+ } finally {
105
+ await this.pool.release(context);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Capture all variants for a component
111
+ */
112
+ async captureComponent(
113
+ component: string,
114
+ variants: string[],
115
+ options: CaptureOptions = {}
116
+ ): Promise<Screenshot[]> {
117
+ const results: Screenshot[] = [];
118
+
119
+ // Capture in parallel up to pool size
120
+ const batchSize = this.pool.size;
121
+
122
+ for (let i = 0; i < variants.length; i += batchSize) {
123
+ const batch = variants.slice(i, i + batchSize);
124
+ const batchPromises = batch.map((variant) =>
125
+ this.captureVariant(component, variant, options)
126
+ );
127
+ const batchResults = await Promise.all(batchPromises);
128
+ results.push(...batchResults);
129
+ }
130
+
131
+ return results;
132
+ }
133
+
134
+ /**
135
+ * Build the URL for a variant
136
+ */
137
+ private buildVariantUrl(
138
+ component: string,
139
+ variant: string,
140
+ theme?: Theme
141
+ ): string {
142
+ const params = new URLSearchParams({
143
+ component,
144
+ variant,
145
+ isolated: 'true',
146
+ });
147
+
148
+ if (theme) {
149
+ params.set('theme', theme);
150
+ }
151
+
152
+ return `${this.baseUrl}?${params.toString()}`;
153
+ }
154
+
155
+ /**
156
+ * Navigate to URL and capture screenshot
157
+ */
158
+ private async navigateAndCapture(
159
+ page: Page,
160
+ url: string,
161
+ options: CaptureOptions
162
+ ): Promise<{ renderTimeMs: number; data: Buffer; captureTimeMs: number }> {
163
+ const renderTimer = new Timer();
164
+
165
+ // Inject animation disabling CSS if needed
166
+ if (options.disableAnimations !== false) {
167
+ await page.addStyleTag({ content: DISABLE_ANIMATIONS_CSS });
168
+ }
169
+
170
+ // Navigate to the page
171
+ await page.goto(url, {
172
+ waitUntil: 'domcontentloaded',
173
+ timeout: 30000,
174
+ });
175
+
176
+ // Wait for network to be idle (all resources loaded)
177
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
178
+ // Ignore timeout, continue with capture
179
+ });
180
+
181
+ // Wait for fonts to load
182
+ await this.waitForFonts(page, options.fontTimeout ?? DEFAULTS.fontTimeoutMs);
183
+
184
+ // Wait for specific selector if provided
185
+ if (options.waitForSelector) {
186
+ await page.waitForSelector(options.waitForSelector, { timeout: 5000 });
187
+ }
188
+
189
+ // Additional delay if specified
190
+ if (options.delay ?? DEFAULTS.captureDelayMs) {
191
+ await sleep(options.delay ?? DEFAULTS.captureDelayMs);
192
+ }
193
+
194
+ const renderTimeMs = renderTimer.elapsed();
195
+
196
+ // Capture screenshot
197
+ const captureTimer = new Timer();
198
+ const data = await this.takeScreenshot(page, options);
199
+ const captureTimeMs = captureTimer.elapsed();
200
+
201
+ return { renderTimeMs, data: Buffer.from(data), captureTimeMs };
202
+ }
203
+
204
+ /**
205
+ * Wait for fonts to be loaded
206
+ */
207
+ private async waitForFonts(page: Page, timeout: number): Promise<void> {
208
+ try {
209
+ await page.evaluate(async (timeoutMs) => {
210
+ // Wait for document.fonts.ready
211
+ await Promise.race([
212
+ document.fonts.ready,
213
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
214
+ ]);
215
+ }, timeout);
216
+ } catch {
217
+ // Ignore errors, continue with capture
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Take the actual screenshot
223
+ */
224
+ private async takeScreenshot(
225
+ page: Page,
226
+ options: CaptureOptions
227
+ ): Promise<Uint8Array> {
228
+ // If clip selector is provided, clip to that element
229
+ if (options.clipSelector) {
230
+ const element = page.locator(options.clipSelector);
231
+ const boundingBox = await element.boundingBox();
232
+
233
+ if (boundingBox) {
234
+ return page.screenshot({
235
+ type: 'png',
236
+ clip: boundingBox,
237
+ });
238
+ }
239
+ }
240
+
241
+ // Full page screenshot
242
+ return page.screenshot({
243
+ type: 'png',
244
+ fullPage: false,
245
+ });
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Error class for capture errors
251
+ */
252
+ export class CaptureError extends ServiceError {
253
+ constructor(message: string, code: string, suggestion?: string) {
254
+ super(message, code, suggestion);
255
+ this.name = `${BRAND.name}CaptureError`;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Create a capture engine with the shared browser pool
261
+ */
262
+ export function createCaptureEngine(
263
+ pool: BrowserPool,
264
+ baseUrl: string
265
+ ): CaptureEngine {
266
+ return new CaptureEngine(pool, baseUrl);
267
+ }