design-clone 2.1.0 → 3.0.0

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 (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * DOM Tree Analyzer
3
+ *
4
+ * Traverse DOM tree hierarchically to capture structure,
5
+ * semantic landmarks, and parent-child relationships.
6
+ *
7
+ * Key features:
8
+ * - PreOrder traversal (parent before children)
9
+ * - W3C landmark detection (header, main, footer, nav, aside)
10
+ * - Section context mapping (hero, content, sidebar, footer)
11
+ * - Bidirectional parent-child refs
12
+ * - Configurable max depth (default: 8)
13
+ *
14
+ * Post-processing (landmarks map, heading tree, stats) lives in
15
+ * dom-tree-analyzer-tree-builders.js and runs in Node context.
16
+ */
17
+
18
+ import { buildLandmarksMap, buildHeadingTree, countTreeStats } from './dom-tree-analyzer-tree-builders.js';
19
+
20
+ // Constants
21
+ export const MAX_DEPTH = 8;
22
+ export const LANDMARK_TAGS = ['header', 'main', 'footer', 'nav', 'aside', 'section', 'article'];
23
+ export const HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
24
+
25
+ // Section detection thresholds
26
+ const HERO_THRESHOLD = 0.15;
27
+ const FOOTER_THRESHOLD = 0.85;
28
+ const SIDEBAR_MAX_WIDTH = 400;
29
+ const Y_POSITION_TOLERANCE = 5;
30
+
31
+ /**
32
+ * Extract DOM tree hierarchy from page.
33
+ * @param {import('playwright').Page} page - Playwright page
34
+ * @param {Object} options
35
+ * @param {number} [options.maxDepth=8] - Maximum traversal depth
36
+ * @param {boolean} [options.includeHidden=false] - Include hidden elements
37
+ * @returns {Promise<Object>} DOMHierarchy with root, landmarks, headingTree, stats
38
+ */
39
+ export async function extractDOMHierarchy(page, options = {}) {
40
+ const { maxDepth = MAX_DEPTH, includeHidden = false } = options;
41
+ const startTime = Date.now();
42
+
43
+ // Run DOM traversal inside browser context
44
+ const result = await page.evaluate(
45
+ ({ maxDepth, includeHidden, LANDMARK_TAGS, HEADING_TAGS, HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH }) => {
46
+ const pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
47
+ const pageWidth = document.documentElement.clientWidth;
48
+
49
+ function detectRole(el) {
50
+ const tag = el.tagName.toLowerCase();
51
+ const ariaRole = el.getAttribute('role');
52
+ if (ariaRole) return ariaRole;
53
+
54
+ if (LANDMARK_TAGS.includes(tag)) {
55
+ const isTopLevel = !el.closest('main, section, article, aside');
56
+ if (tag === 'header' || tag === 'footer') {
57
+ return isTopLevel ? `${tag}-landmark` : `${tag}-section`;
58
+ }
59
+ return tag;
60
+ }
61
+
62
+ if (HEADING_TAGS.includes(tag)) return `heading-${tag.slice(1)}`;
63
+
64
+ if (tag === 'div' || tag === 'span') {
65
+ const cls = (el.className || '').toString().toLowerCase();
66
+ if (cls.includes('container')) return 'container';
67
+ if (cls.includes('wrapper')) return 'wrapper';
68
+ if (cls.includes('card')) return 'card';
69
+ if (cls.includes('grid')) return 'grid';
70
+ if (cls.includes('hero')) return 'hero';
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ function detectSectionContext(el, yPos) {
77
+ const tag = el.tagName.toLowerCase();
78
+ if (tag === 'header' || el.closest('header')) return 'header';
79
+ if (tag === 'footer' || el.closest('footer')) return 'footer';
80
+ if (tag === 'aside' || el.closest('aside')) return 'sidebar';
81
+ if (tag === 'nav' || el.closest('nav')) return 'nav';
82
+
83
+ const yRatio = yPos / pageHeight;
84
+ if (yRatio < HERO_THRESHOLD) return 'hero';
85
+ if (yRatio > FOOTER_THRESHOLD) return 'footer';
86
+
87
+ const computed = window.getComputedStyle(el);
88
+ const rect = el.getBoundingClientRect();
89
+ if ((computed.position === 'fixed' || computed.position === 'sticky') && rect.width < SIDEBAR_MAX_WIDTH) {
90
+ return 'sidebar';
91
+ }
92
+
93
+ return 'content';
94
+ }
95
+
96
+ function traverseDOM(el, depth, parentId, path) {
97
+ if (depth > maxDepth) return null;
98
+ const rect = el.getBoundingClientRect();
99
+ if (!includeHidden && rect.width === 0 && rect.height === 0) return null;
100
+
101
+ const id = path.join('-');
102
+ const computed = window.getComputedStyle(el);
103
+ const yPos = rect.y + window.scrollY;
104
+
105
+ const node = {
106
+ id,
107
+ tagName: el.tagName.toLowerCase(),
108
+ depth,
109
+ role: detectRole(el),
110
+ section: detectSectionContext(el, yPos),
111
+ attributes: {
112
+ id: el.id || null,
113
+ className: el.className ? el.className.toString().split(' ').slice(0, 3).join(' ') : null,
114
+ role: el.getAttribute('role')
115
+ },
116
+ dimensions: {
117
+ width: Math.round(rect.width),
118
+ height: Math.round(rect.height),
119
+ x: Math.round(rect.x),
120
+ y: Math.round(yPos)
121
+ },
122
+ layout: {
123
+ display: computed.display,
124
+ position: computed.position !== 'static' ? computed.position : undefined,
125
+ flexDirection: computed.flexDirection !== 'row' ? computed.flexDirection : undefined,
126
+ gridTemplateColumns: computed.gridTemplateColumns !== 'none' ? computed.gridTemplateColumns : undefined
127
+ },
128
+ children: [],
129
+ parentId
130
+ };
131
+
132
+ let childIdx = 0;
133
+ for (const child of el.children) {
134
+ const childNode = traverseDOM(child, depth + 1, id, [...path, childIdx]);
135
+ if (childNode) { node.children.push(childNode); childIdx++; }
136
+ }
137
+
138
+ return node;
139
+ }
140
+
141
+ const root = traverseDOM(document.body, 0, null, [0]);
142
+ return { root, pageHeight, pageWidth };
143
+ },
144
+ { maxDepth, includeHidden, LANDMARK_TAGS, HEADING_TAGS, HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH }
145
+ );
146
+
147
+ // Post-process in Node context using tree-builder helpers
148
+ const landmarks = buildLandmarksMap(result.root);
149
+ const headingTree = buildHeadingTree(result.root);
150
+ const { totalNodes, maxDepth: maxActualDepth } = countTreeStats(result.root);
151
+
152
+ // Enrich headings with text + fontSize (separate evaluate for perf)
153
+ const headingData = await page.evaluate(({ headingTree, yTolerance }) => {
154
+ return headingTree.map(h => {
155
+ const headings = document.querySelectorAll(`h${h.level}`);
156
+ for (const el of headings) {
157
+ const rect = el.getBoundingClientRect();
158
+ const yPos = Math.round(rect.y + window.scrollY);
159
+ if (Math.abs(yPos - h.y) < yTolerance) {
160
+ const computed = window.getComputedStyle(el);
161
+ return {
162
+ ...h,
163
+ text: el.textContent?.trim().slice(0, 60) || null,
164
+ fontSize: parseFloat(computed.fontSize) || null
165
+ };
166
+ }
167
+ }
168
+ return h;
169
+ });
170
+ }, { headingTree, yTolerance: Y_POSITION_TOLERANCE });
171
+
172
+ const duration = Date.now() - startTime;
173
+ if (duration > 500) {
174
+ console.error(`[WARN] DOM hierarchy extraction took ${duration}ms (>500ms target)`);
175
+ }
176
+
177
+ return {
178
+ root: result.root,
179
+ landmarks,
180
+ headingTree: headingData,
181
+ stats: {
182
+ totalNodes,
183
+ maxDepth: maxActualDepth,
184
+ landmarkCount: [landmarks.header, landmarks.main, landmarks.footer].filter(Boolean).length +
185
+ landmarks.nav.length + landmarks.aside.length,
186
+ pageHeight: result.pageHeight,
187
+ pageWidth: result.pageWidth,
188
+ extractionTimeMs: duration
189
+ }
190
+ };
191
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Browser-side state capture for SPA app state snapshots.
3
+ *
4
+ * Runs page.evaluate calls to capture framework-specific data
5
+ * (__NEXT_DATA__, __NUXT__, etc.) and state management store state
6
+ * (Redux, Vuex, Pinia, Zustand, MobX). Used by app-state-snapshot.js.
7
+ */
8
+
9
+ /**
10
+ * Capture framework-specific data from page
11
+ * @param {import('playwright').Page} page - Playwright page
12
+ * @param {string|null} framework - Detected framework name
13
+ * @returns {Promise<Object|null>}
14
+ */
15
+ export async function captureFrameworkData(page, framework) {
16
+ try {
17
+ return await page.evaluate((fw) => {
18
+ switch (fw) {
19
+ case 'next':
20
+ if (!window.__NEXT_DATA__) return null;
21
+ return {
22
+ props: window.__NEXT_DATA__.props,
23
+ page: window.__NEXT_DATA__.page,
24
+ query: window.__NEXT_DATA__.query,
25
+ buildId: window.__NEXT_DATA__.buildId,
26
+ runtimeConfig: window.__NEXT_DATA__.runtimeConfig,
27
+ dynamicIds: window.__NEXT_DATA__.dynamicIds
28
+ };
29
+
30
+ case 'nuxt':
31
+ if (!window.__NUXT__) return null;
32
+ return {
33
+ data: window.__NUXT__.data,
34
+ state: window.__NUXT__.state,
35
+ serverRendered: window.__NUXT__.serverRendered,
36
+ routePath: window.__NUXT__.routePath,
37
+ config: window.__NUXT__.config
38
+ };
39
+
40
+ case 'vue': {
41
+ const vueApp = document.querySelector('[data-v-app]')?.__vue_app__;
42
+ if (vueApp?.config?.globalProperties) {
43
+ return {
44
+ routePath: window.location.pathname,
45
+ hasRouter: !!vueApp.config.globalProperties.$router,
46
+ hasStore: !!vueApp.config.globalProperties.$store ||
47
+ !!vueApp.config.globalProperties.$pinia
48
+ };
49
+ }
50
+ return null;
51
+ }
52
+
53
+ case 'react': {
54
+ const reactRoot = document.getElementById('root') ||
55
+ document.querySelector('[data-reactroot]');
56
+ return reactRoot ? {
57
+ hasReactRoot: true,
58
+ rootId: reactRoot.id || null
59
+ } : null;
60
+ }
61
+
62
+ case 'angular': {
63
+ const appRoot = document.querySelector('app-root');
64
+ if (appRoot && window.ng?.probe) {
65
+ try {
66
+ const component = window.ng.probe(appRoot);
67
+ return {
68
+ componentName: component?.componentInstance?.constructor?.name,
69
+ hasRouter: !!component?.injector?.get?.('Router', null)
70
+ };
71
+ } catch {
72
+ return { hasAppRoot: true };
73
+ }
74
+ }
75
+ return appRoot ? { hasAppRoot: true } : null;
76
+ }
77
+
78
+ case 'svelte':
79
+ if (window.__sveltekit_data__) {
80
+ return window.__sveltekit_data__;
81
+ }
82
+ return null;
83
+
84
+ case 'astro': {
85
+ const islands = document.querySelectorAll('astro-island');
86
+ if (islands.length > 0) {
87
+ return {
88
+ islandCount: islands.length,
89
+ componentNames: Array.from(islands)
90
+ .map(i => i.getAttribute('component-export'))
91
+ .filter(Boolean)
92
+ };
93
+ }
94
+ return null;
95
+ }
96
+
97
+ default:
98
+ return null;
99
+ }
100
+ }, framework);
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Capture state management store state (Redux, Vuex, Pinia, Zustand, MobX)
108
+ * @param {import('playwright').Page} page - Playwright page
109
+ * @returns {Promise<{type: string, state: Object|null}>}
110
+ */
111
+ export async function captureStoreState(page) {
112
+ try {
113
+ return await page.evaluate(() => {
114
+ // Redux - Method 1: Redux DevTools extension
115
+ if (window.__REDUX_DEVTOOLS_EXTENSION__) {
116
+ try {
117
+ const stores = window.__REDUX_DEVTOOLS_EXTENSION__.stores ||
118
+ window.__REDUX_DEVTOOLS_EXTENSION__.open?.() ||
119
+ null;
120
+ if (stores && typeof stores === 'object') {
121
+ const storeKeys = Object.keys(stores);
122
+ if (storeKeys.length > 0) {
123
+ const store = stores[storeKeys[0]];
124
+ if (store?.getState) {
125
+ return { type: 'redux', state: store.getState() };
126
+ }
127
+ }
128
+ }
129
+ } catch {
130
+ // Continue to other methods
131
+ }
132
+ }
133
+
134
+ // Redux - Method 2: Direct store on window
135
+ if (window.store?.getState) {
136
+ return { type: 'redux', state: window.store.getState() };
137
+ }
138
+
139
+ // Redux - Method 3: __REDUX_STATE__ hydration
140
+ if (window.__REDUX_STATE__) {
141
+ return { type: 'redux', state: window.__REDUX_STATE__ };
142
+ }
143
+
144
+ // Vuex - Nuxt 2 / Vue 2/3
145
+ if (window.$nuxt?.$store?.state) {
146
+ return { type: 'vuex', state: window.$nuxt.$store.state };
147
+ }
148
+ if (window.__VUEX__?.state) {
149
+ return { type: 'vuex', state: window.__VUEX__.state };
150
+ }
151
+
152
+ // Vuex via Vue app
153
+ const vueApp = document.querySelector('[data-v-app]')?.__vue_app__;
154
+ if (vueApp?.config?.globalProperties?.$store?.state) {
155
+ return { type: 'vuex', state: vueApp.config.globalProperties.$store.state };
156
+ }
157
+
158
+ // Pinia - Nuxt 3 / Vue 3
159
+ if (window.$nuxt?.$pinia?.state?.value) {
160
+ return { type: 'pinia', state: window.$nuxt.$pinia.state.value };
161
+ }
162
+ if (window.__PINIA__?.state?.value) {
163
+ return { type: 'pinia', state: window.__PINIA__.state.value };
164
+ }
165
+ if (vueApp?.config?.globalProperties?.$pinia?.state?.value) {
166
+ return { type: 'pinia', state: vueApp.config.globalProperties.$pinia.state.value };
167
+ }
168
+
169
+ // Zustand - check common window-exposed store names
170
+ const zustandPatterns = ['useStore', 'useAppStore', 'useBearStore', 'store'];
171
+ for (const pattern of zustandPatterns) {
172
+ const potentialStore = window[pattern];
173
+ if (potentialStore?.getState && typeof potentialStore.getState === 'function') {
174
+ try {
175
+ const state = potentialStore.getState();
176
+ if (state && typeof state === 'object') {
177
+ return { type: 'zustand', state };
178
+ }
179
+ } catch {
180
+ // Not a valid Zustand store
181
+ }
182
+ }
183
+ }
184
+
185
+ // MobX
186
+ if (window.__MOBX_STATE__) {
187
+ return { type: 'mobx', state: window.__MOBX_STATE__ };
188
+ }
189
+
190
+ return { type: 'none', state: null };
191
+ });
192
+ } catch {
193
+ return { type: 'none', state: null };
194
+ }
195
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Serialization and filtering utilities for app state snapshots.
3
+ *
4
+ * Provides safe object serialization (handles circular refs, functions, symbols),
5
+ * sensitive key filtering (tokens, passwords, secrets), and state size enforcement.
6
+ * Used by app-state-snapshot.js (main module).
7
+ */
8
+
9
+ import { SIZE_LIMITS } from '../../shared/config.js';
10
+
11
+ // ============================================================================
12
+ // Constants
13
+ // ============================================================================
14
+
15
+ /** Maximum state size in bytes (1MB) - sourced from centralized config */
16
+ export const MAX_STATE_SIZE = SIZE_LIMITS.MAX_STATE;
17
+
18
+ /** Maximum depth for recursive object traversal */
19
+ export const MAX_TRAVERSAL_DEPTH = 50;
20
+
21
+ /** Patterns to identify sensitive keys */
22
+ export const SENSITIVE_PATTERNS = [
23
+ /token/i,
24
+ /password/i,
25
+ /passwd/i,
26
+ /secret/i,
27
+ /auth/i,
28
+ /api[_-]?key/i,
29
+ /credential/i,
30
+ /private/i,
31
+ /session/i,
32
+ /cookie/i,
33
+ /bearer/i,
34
+ /jwt/i,
35
+ /access[_-]?key/i,
36
+ /refresh[_-]?token/i
37
+ ];
38
+
39
+ /** Marker for filtered sensitive values */
40
+ export const FILTERED_MARKER = '[FILTERED]';
41
+
42
+ /** Marker for circular references */
43
+ export const CIRCULAR_MARKER = '[Circular]';
44
+
45
+ /** Marker for unserializable values */
46
+ export const UNSERIALIZABLE_MARKER = '[Unserializable]';
47
+
48
+ // ============================================================================
49
+ // Utility Functions
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Check if a key matches sensitive patterns
54
+ * @param {string} key - Object key to check
55
+ * @returns {boolean}
56
+ */
57
+ export function isSensitiveKey(key) {
58
+ if (typeof key !== 'string') return false;
59
+ return SENSITIVE_PATTERNS.some(pattern => pattern.test(key));
60
+ }
61
+
62
+ /**
63
+ * Filter sensitive keys from an object recursively
64
+ * @param {*} obj - Object to filter
65
+ * @param {string[]} warnings - Array to collect warnings
66
+ * @param {string} path - Current path for warning messages
67
+ * @param {number} depth - Current recursion depth
68
+ * @returns {*} Filtered object
69
+ */
70
+ export function filterSensitive(obj, warnings = [], path = '', depth = 0) {
71
+ // Prevent infinite recursion
72
+ if (depth > MAX_TRAVERSAL_DEPTH) {
73
+ return '[Max Depth Exceeded]';
74
+ }
75
+
76
+ // Handle primitives
77
+ if (obj === null || obj === undefined) return obj;
78
+ if (typeof obj !== 'object') return obj;
79
+
80
+ // Handle arrays
81
+ if (Array.isArray(obj)) {
82
+ return obj.map((item, i) =>
83
+ filterSensitive(item, warnings, `${path}[${i}]`, depth + 1)
84
+ );
85
+ }
86
+
87
+ // Handle objects
88
+ const filtered = {};
89
+ for (const [key, value] of Object.entries(obj)) {
90
+ const fullPath = path ? `${path}.${key}` : key;
91
+
92
+ if (isSensitiveKey(key)) {
93
+ warnings.push(`Filtered sensitive key: ${fullPath}`);
94
+ filtered[key] = FILTERED_MARKER;
95
+ continue;
96
+ }
97
+
98
+ filtered[key] = filterSensitive(value, warnings, fullPath, depth + 1);
99
+ }
100
+
101
+ return filtered;
102
+ }
103
+
104
+ /**
105
+ * Safely serialize an object handling circular refs, functions, symbols
106
+ * @param {*} obj - Object to serialize
107
+ * @param {WeakSet} seen - Set of seen objects for circular detection
108
+ * @param {number} depth - Current recursion depth
109
+ * @returns {*} Serializable version of object
110
+ */
111
+ export function safeSerialize(obj, seen = new WeakSet(), depth = 0) {
112
+ if (depth > MAX_TRAVERSAL_DEPTH) return '[Max Depth Exceeded]';
113
+
114
+ if (obj === null || obj === undefined) return obj;
115
+ if (typeof obj === 'boolean' || typeof obj === 'number' || typeof obj === 'string') return obj;
116
+
117
+ if (typeof obj === 'function') return '[Function]';
118
+ if (typeof obj === 'symbol') return obj.toString();
119
+ if (typeof obj === 'bigint') return obj.toString();
120
+ if (obj instanceof Date) return obj.toISOString();
121
+ if (obj instanceof RegExp) return obj.toString();
122
+ if (obj instanceof Error) return { message: obj.message, name: obj.name };
123
+ if (obj instanceof Map) return Object.fromEntries(obj);
124
+ if (obj instanceof Set) return Array.from(obj);
125
+
126
+ if (typeof obj === 'object') {
127
+ if (seen.has(obj)) return CIRCULAR_MARKER;
128
+ seen.add(obj);
129
+
130
+ try {
131
+ if (Array.isArray(obj)) {
132
+ return obj.map(item => safeSerialize(item, seen, depth + 1));
133
+ }
134
+
135
+ const result = {};
136
+ for (const [key, value] of Object.entries(obj)) {
137
+ try {
138
+ result[key] = safeSerialize(value, seen, depth + 1);
139
+ } catch {
140
+ result[key] = UNSERIALIZABLE_MARKER;
141
+ }
142
+ }
143
+ return result;
144
+ } catch {
145
+ return UNSERIALIZABLE_MARKER;
146
+ }
147
+ }
148
+
149
+ return obj;
150
+ }
151
+
152
+ /**
153
+ * Enforce state size limit, truncating store state if exceeded
154
+ * @param {import('./app-state-snapshot.js').StateSnapshot} snapshot - State snapshot to check
155
+ * @param {string[]} warnings - Array to collect warnings
156
+ * @returns {import('./app-state-snapshot.js').StateSnapshot} Possibly truncated snapshot
157
+ */
158
+ export function enforceStateLimit(snapshot, warnings) {
159
+ const serialized = JSON.stringify(snapshot);
160
+ const sizeBytes = Buffer.byteLength(serialized, 'utf8');
161
+
162
+ if (sizeBytes > MAX_STATE_SIZE) {
163
+ const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
164
+ warnings.push(`State exceeded 1MB limit (${sizeMB}MB), store state truncated`);
165
+
166
+ return {
167
+ ...snapshot,
168
+ storeState: {
169
+ _truncated: true,
170
+ _reason: `exceeded 1MB limit (${sizeMB}MB)`,
171
+ _originalType: snapshot.storeType
172
+ },
173
+ sizeBytes: MAX_STATE_SIZE
174
+ };
175
+ }
176
+
177
+ return { ...snapshot, sizeBytes };
178
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * App State Snapshot Module
3
+ *
4
+ * Captures application state from SPAs including:
5
+ * - Framework data (__NEXT_DATA__, __NUXT__)
6
+ * - State management stores (Redux, Vuex, Pinia, Zustand)
7
+ *
8
+ * Features:
9
+ * - Sensitive data filtering (tokens, passwords, secrets)
10
+ * - Safe serialization (handles circular refs, functions, symbols)
11
+ * - Size limit enforcement (1MB max)
12
+ *
13
+ * @module app-state-snapshot
14
+ */
15
+
16
+ import {
17
+ MAX_STATE_SIZE,
18
+ SENSITIVE_PATTERNS,
19
+ FILTERED_MARKER,
20
+ CIRCULAR_MARKER,
21
+ isSensitiveKey,
22
+ filterSensitive,
23
+ safeSerialize,
24
+ enforceStateLimit
25
+ } from './app-state-snapshot-utils.js';
26
+
27
+ import {
28
+ captureFrameworkData,
29
+ captureStoreState
30
+ } from './app-state-snapshot-capture.js';
31
+
32
+ // Re-export constants and utilities for backward compatibility
33
+ export {
34
+ MAX_STATE_SIZE,
35
+ SENSITIVE_PATTERNS,
36
+ FILTERED_MARKER,
37
+ CIRCULAR_MARKER,
38
+ isSensitiveKey,
39
+ filterSensitive,
40
+ safeSerialize,
41
+ enforceStateLimit,
42
+ captureFrameworkData,
43
+ captureStoreState
44
+ };
45
+
46
+ // ============================================================================
47
+ // Type Definitions (JSDoc)
48
+ // ============================================================================
49
+
50
+ /**
51
+ * @typedef {Object} StateSnapshot
52
+ * @property {Object|null} frameworkData - __NEXT_DATA__, __NUXT__, etc.
53
+ * @property {Object|null} storeState - Redux/Vuex/Pinia/Zustand state
54
+ * @property {string|null} framework - Detected framework name
55
+ * @property {string} storeType - 'redux'|'vuex'|'pinia'|'zustand'|'none'
56
+ * @property {string[]} warnings - Serialization/filtering warnings
57
+ * @property {number} capturedAt - Unix timestamp
58
+ * @property {number} sizeBytes - Serialized size in bytes
59
+ */
60
+
61
+ // ============================================================================
62
+ // Main Export
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Capture application state from page
67
+ * @param {import('playwright').Page} page - Playwright page instance
68
+ * @param {Object|null} [frameworkInfo] - Framework detection result
69
+ * @returns {Promise<StateSnapshot>}
70
+ */
71
+ export async function captureAppState(page, frameworkInfo = null) {
72
+ const warnings = [];
73
+ const framework = frameworkInfo?.framework || null;
74
+
75
+ let snapshot = {
76
+ frameworkData: null,
77
+ storeState: null,
78
+ framework,
79
+ storeType: 'none',
80
+ warnings,
81
+ capturedAt: Date.now(),
82
+ sizeBytes: 0
83
+ };
84
+
85
+ try {
86
+ const rawFrameworkData = await captureFrameworkData(page, framework);
87
+ if (rawFrameworkData) {
88
+ const serialized = safeSerialize(rawFrameworkData);
89
+ snapshot.frameworkData = filterSensitive(serialized, warnings);
90
+ }
91
+
92
+ const storeResult = await captureStoreState(page);
93
+ if (storeResult.state) {
94
+ const serialized = safeSerialize(storeResult.state);
95
+ snapshot.storeState = filterSensitive(serialized, warnings);
96
+ snapshot.storeType = storeResult.type;
97
+ }
98
+
99
+ snapshot = enforceStateLimit(snapshot, warnings);
100
+ } catch (error) {
101
+ warnings.push(`State capture error: ${error.message}`);
102
+ }
103
+
104
+ return snapshot;
105
+ }
106
+
107
+ /**
108
+ * Format state snapshot for logging
109
+ * @param {StateSnapshot} snapshot - Captured state
110
+ * @returns {string}
111
+ */
112
+ export function formatStateSnapshot(snapshot) {
113
+ const lines = [
114
+ '\n=== App State Snapshot ===',
115
+ `Framework: ${snapshot.framework || 'unknown'}`,
116
+ `Store Type: ${snapshot.storeType}`,
117
+ `Framework Data: ${snapshot.frameworkData ? 'captured' : 'none'}`,
118
+ `Store State: ${snapshot.storeState ? 'captured' : 'none'}`,
119
+ `Size: ${(snapshot.sizeBytes / 1024).toFixed(2)} KB`
120
+ ];
121
+
122
+ if (snapshot.warnings.length > 0) {
123
+ lines.push(`Warnings (${snapshot.warnings.length}):`);
124
+ snapshot.warnings.slice(0, 5).forEach(w => lines.push(` - ${w}`));
125
+ if (snapshot.warnings.length > 5) {
126
+ lines.push(` ... and ${snapshot.warnings.length - 5} more`);
127
+ }
128
+ }
129
+
130
+ return lines.join('\n');
131
+ }