@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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
|
+
}
|