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
@@ -1,538 +0,0 @@
1
- /**
2
- * Framework Detector Module
3
- *
4
- * Detects JavaScript frameworks used on a page by checking:
5
- * - Global objects (window.__NEXT_DATA__, etc.)
6
- * - DOM attributes ([data-reactroot], [ng-version], etc.)
7
- * - Script URL patterns (/_next/, /_nuxt/, etc.)
8
- *
9
- * Returns framework info with confidence scoring.
10
- *
11
- * Usage:
12
- * import { detectFramework } from './framework-detector.js';
13
- * const info = await detectFramework(page);
14
- * // { framework: 'next', version: '14.0.0', confidence: 'high', ... }
15
- */
16
-
17
- /**
18
- * @typedef {Object} FrameworkInfo
19
- * @property {string|null} framework - 'next'|'nuxt'|'vue'|'react'|'angular'|'svelte'|'astro'|null
20
- * @property {string|null} version - Framework version if detectable
21
- * @property {'spa'|'ssr'|'ssg'|'unknown'} routingType - Routing/rendering strategy
22
- * @property {'high'|'medium'|'low'} confidence - Detection confidence
23
- * @property {string[]} signals - Matched detection signals
24
- */
25
-
26
- // Confidence thresholds
27
- const CONFIDENCE_HIGH_THRESHOLD = 5;
28
- const CONFIDENCE_MEDIUM_THRESHOLD = 3;
29
-
30
- /**
31
- * Detection signals for each framework
32
- * Each signal has: type, path/selector/pattern, weight (1-3), signal (label)
33
- */
34
- const DETECTION_SIGNALS = {
35
- next: [
36
- { type: 'global', path: ['__NEXT_DATA__'], weight: 3, signal: '__NEXT_DATA__' },
37
- { type: 'global', path: ['__NEXT_LOADED_PAGES__'], weight: 2, signal: '__NEXT_LOADED_PAGES__' },
38
- { type: 'global', path: ['__BUILD_MANIFEST'], weight: 2, signal: '__BUILD_MANIFEST' },
39
- { type: 'dom', selector: '#__next', weight: 2, signal: '#__next' },
40
- { type: 'script', pattern: '/_next/', weight: 1, signal: 'script:/_next/' }
41
- ],
42
- nuxt: [
43
- { type: 'global', path: ['__NUXT__'], weight: 3, signal: '__NUXT__' },
44
- { type: 'global', path: ['$nuxt'], weight: 2, signal: '$nuxt' },
45
- { type: 'global', path: ['__NUXT_PATHS__'], weight: 2, signal: '__NUXT_PATHS__' },
46
- { type: 'dom', selector: '#__nuxt', weight: 2, signal: '#__nuxt' },
47
- { type: 'dom', selector: '#__layout', weight: 1, signal: '#__layout' },
48
- { type: 'script', pattern: '/_nuxt/', weight: 1, signal: 'script:/_nuxt/' }
49
- ],
50
- vue: [
51
- { type: 'global', path: ['__VUE__'], weight: 3, signal: '__VUE__' },
52
- { type: 'global', path: ['Vue'], weight: 2, signal: 'Vue' },
53
- { type: 'global', path: ['__VUE_DEVTOOLS_GLOBAL_HOOK__'], weight: 1, signal: '__VUE_DEVTOOLS_GLOBAL_HOOK__' },
54
- { type: 'dom', selector: '[data-v-]', weight: 2, signal: 'data-v-*' },
55
- { type: 'dom', selector: '[data-server-rendered]', weight: 2, signal: 'data-server-rendered' }
56
- ],
57
- react: [
58
- { type: 'global', path: ['__REACT_DEVTOOLS_GLOBAL_HOOK__'], weight: 1, signal: '__REACT_DEVTOOLS_GLOBAL_HOOK__' },
59
- { type: 'dom', selector: '[data-reactroot]', weight: 3, signal: 'data-reactroot' },
60
- { type: 'dom', selector: '[data-reactid]', weight: 2, signal: 'data-reactid' },
61
- { type: 'dom', selector: '#root[data-reactroot], #root > div', weight: 1, signal: '#root' }
62
- ],
63
- angular: [
64
- { type: 'global', path: ['ng'], weight: 2, signal: 'ng' },
65
- { type: 'global', path: ['getAllAngularRootElements'], weight: 3, signal: 'getAllAngularRootElements' },
66
- { type: 'dom', selector: '[ng-version]', weight: 3, signal: 'ng-version' },
67
- { type: 'dom', selector: 'app-root', weight: 2, signal: 'app-root' },
68
- { type: 'dom', selector: '[_nghost-]', weight: 2, signal: '_nghost-*' },
69
- { type: 'dom', selector: '[ng-app]', weight: 2, signal: 'ng-app' }
70
- ],
71
- svelte: [
72
- { type: 'global', path: ['__svelte__'], weight: 2, signal: '__svelte__' },
73
- { type: 'global', path: ['__sveltekit'], weight: 3, signal: '__sveltekit' },
74
- { type: 'dom', selector: '[data-sveltekit-preload-data]', weight: 3, signal: 'data-sveltekit-preload-data' },
75
- { type: 'dom', selector: '[data-sveltekit-reload]', weight: 2, signal: 'data-sveltekit-reload' },
76
- { type: 'script', pattern: '/@svelte/', weight: 1, signal: 'script:/@svelte/' }
77
- ],
78
- astro: [
79
- { type: 'dom', selector: 'astro-island', weight: 3, signal: 'astro-island' },
80
- { type: 'dom', selector: '[data-astro-cid-]', weight: 2, signal: 'data-astro-cid-*' },
81
- { type: 'dom', selector: '[data-astro-source-file]', weight: 2, signal: 'data-astro-source-file' },
82
- { type: 'meta', name: 'generator', pattern: 'Astro', weight: 3, signal: 'meta:generator:Astro' },
83
- { type: 'script', pattern: '/@astrojs/', weight: 1, signal: 'script:/@astrojs/' }
84
- ]
85
- };
86
-
87
- /**
88
- * Calculate confidence level based on total weight
89
- * @param {number} totalWeight - Sum of matched signal weights
90
- * @returns {'high'|'medium'|'low'} Confidence level
91
- */
92
- function calculateConfidence(totalWeight) {
93
- if (totalWeight >= CONFIDENCE_HIGH_THRESHOLD) return 'high';
94
- if (totalWeight >= CONFIDENCE_MEDIUM_THRESHOLD) return 'medium';
95
- return 'low';
96
- }
97
-
98
- /**
99
- * Safe property access without eval()
100
- * @param {Object} obj - Object to traverse
101
- * @param {string[]} path - Property path array
102
- * @returns {*} Value at path or undefined
103
- */
104
- function safeGet(obj, path) {
105
- let current = obj;
106
- for (const key of path) {
107
- if (current === null || current === undefined) return undefined;
108
- current = current[key];
109
- }
110
- return current;
111
- }
112
-
113
- /**
114
- * Check if element has attribute with prefix
115
- * @param {Element} el - DOM element
116
- * @param {string} prefix - Attribute prefix
117
- * @returns {boolean}
118
- */
119
- function hasAttributeWithPrefix(el, prefix) {
120
- return Array.from(el.attributes).some(attr => attr.name.startsWith(prefix));
121
- }
122
-
123
- /**
124
- * Detection logic that runs in browser context via page.evaluate()
125
- * @param {Object} signals - DETECTION_SIGNALS object
126
- * @returns {Object} Detection results for all frameworks
127
- */
128
- function browserDetectionLogic(signals) {
129
- // Helper: safe property access without eval
130
- function safeGet(obj, path) {
131
- let current = obj;
132
- for (const key of path) {
133
- if (current === null || current === undefined) return undefined;
134
- current = current[key];
135
- }
136
- return current;
137
- }
138
-
139
- // Helper: check if any element has attribute with prefix
140
- function hasAttrPrefix(prefix) {
141
- return Array.from(document.querySelectorAll('*')).some(el =>
142
- Array.from(el.attributes).some(attr => attr.name.startsWith(prefix))
143
- );
144
- }
145
-
146
- const results = {};
147
-
148
- for (const [framework, checks] of Object.entries(signals)) {
149
- let totalWeight = 0;
150
- const matchedSignals = [];
151
- let version = null;
152
-
153
- for (const check of checks) {
154
- let matched = false;
155
-
156
- try {
157
- switch (check.type) {
158
- case 'global':
159
- // Safe property traversal instead of eval()
160
- matched = safeGet(window, check.path) !== undefined;
161
- break;
162
-
163
- case 'dom':
164
- // Handle attribute selectors with partial match
165
- if (check.selector.includes('[data-v-]')) {
166
- matched = hasAttrPrefix('data-v-');
167
- } else if (check.selector.includes('[data-astro-cid-]')) {
168
- matched = hasAttrPrefix('data-astro-cid-');
169
- } else if (check.selector.includes('[_nghost-]')) {
170
- matched = hasAttrPrefix('_nghost-');
171
- } else {
172
- matched = !!document.querySelector(check.selector);
173
- }
174
- break;
175
-
176
- case 'script':
177
- // Check if any script src contains pattern
178
- const scripts = Array.from(document.querySelectorAll('script[src]'));
179
- matched = scripts.some(s => s.src.includes(check.pattern));
180
- break;
181
-
182
- case 'meta':
183
- // Check meta tag content
184
- const meta = document.querySelector(`meta[name="${check.name}"]`);
185
- matched = meta && meta.content && meta.content.includes(check.pattern);
186
- break;
187
- }
188
- } catch (e) {
189
- matched = false;
190
- }
191
-
192
- if (matched) {
193
- totalWeight += check.weight;
194
- matchedSignals.push(check.signal);
195
- }
196
- }
197
-
198
- // Extract version based on framework
199
- if (totalWeight > 0) {
200
- try {
201
- switch (framework) {
202
- case 'next':
203
- const nextData = safeGet(window, ['__NEXT_DATA__']);
204
- if (nextData) {
205
- version = nextData.nextExport ? 'export' : (nextData.buildId || null);
206
- // Try runtime config version
207
- if (nextData.runtimeConfig?.version) {
208
- version = nextData.runtimeConfig.version;
209
- }
210
- }
211
- break;
212
- case 'nuxt':
213
- const nuxtConfig = safeGet(window, ['__NUXT__', 'config', 'app', 'buildId']);
214
- if (nuxtConfig) version = nuxtConfig;
215
- break;
216
- case 'vue':
217
- version = safeGet(window, ['Vue', 'version']) ||
218
- safeGet(window, ['__VUE__', 'version']) || null;
219
- break;
220
- case 'react':
221
- version = safeGet(window, ['React', 'version']) || null;
222
- break;
223
- case 'angular':
224
- const ngVersion = document.querySelector('[ng-version]');
225
- if (ngVersion) version = ngVersion.getAttribute('ng-version');
226
- break;
227
- case 'svelte':
228
- // Svelte doesn't expose version easily
229
- break;
230
- case 'astro':
231
- const astroMeta = document.querySelector('meta[name="generator"]');
232
- if (astroMeta && astroMeta.content.includes('Astro')) {
233
- const match = astroMeta.content.match(/Astro v?([\d.]+)/);
234
- if (match) version = match[1];
235
- }
236
- break;
237
- }
238
- } catch (e) {
239
- // Ignore version extraction errors
240
- }
241
- }
242
-
243
- results[framework] = {
244
- weight: totalWeight,
245
- signals: matchedSignals,
246
- version
247
- };
248
- }
249
-
250
- return results;
251
- }
252
-
253
- /**
254
- * Infer routing type based on framework and detected signals
255
- * @param {import('playwright').Page} page - Playwright page object
256
- * @param {string} framework - Detected framework name
257
- * @returns {Promise<'spa'|'ssr'|'ssg'|'unknown'>} Routing type
258
- */
259
- async function inferRoutingType(page, framework) {
260
- if (!framework) return 'unknown';
261
-
262
- return await page.evaluate((fw) => {
263
- // Helper for safe property access
264
- function safeGet(obj, path) {
265
- let current = obj;
266
- for (const key of path) {
267
- if (current === null || current === undefined) return undefined;
268
- current = current[key];
269
- }
270
- return current;
271
- }
272
-
273
- try {
274
- switch (fw) {
275
- case 'next': {
276
- const nextData = safeGet(window, ['__NEXT_DATA__']);
277
- if (nextData) {
278
- if (nextData.nextExport) return 'ssg';
279
- if (nextData.isFallback === false) return 'ssr';
280
- if (document.querySelector('[data-nscript]')) return 'ssr';
281
- }
282
- return 'ssr';
283
- }
284
-
285
- case 'nuxt': {
286
- const nuxtData = safeGet(window, ['__NUXT__']);
287
- if (nuxtData?.serverRendered === true) return 'ssr';
288
- if (nuxtData?.serverRendered === false) return 'spa';
289
- return 'ssr';
290
- }
291
-
292
- case 'vue':
293
- if (window.$nuxt) return 'ssr'; // Actually Nuxt
294
- if (document.querySelector('[data-server-rendered="true"]')) return 'ssr';
295
- return 'spa';
296
-
297
- case 'react':
298
- if (safeGet(window, ['__NEXT_DATA__'])) return 'ssr';
299
- if (window.___gatsby) return 'ssg';
300
- return 'spa';
301
-
302
- case 'angular':
303
- if (document.querySelector('[ng-server-context]')) return 'ssr';
304
- return 'spa';
305
-
306
- case 'svelte':
307
- if (safeGet(window, ['__sveltekit'])) return 'ssr';
308
- return 'spa';
309
-
310
- case 'astro':
311
- return 'ssg';
312
-
313
- default:
314
- return 'unknown';
315
- }
316
- } catch (e) {
317
- return 'unknown';
318
- }
319
- }, framework);
320
- }
321
-
322
- /**
323
- * Detect framework used on the current page
324
- * @param {import('playwright').Page} page - Playwright page object
325
- * @returns {Promise<FrameworkInfo>} Framework detection result
326
- */
327
- export async function detectFramework(page) {
328
- // Run detection logic in browser context
329
- const results = await page.evaluate((signals) => {
330
- // Helper: safe property access without eval
331
- function safeGet(obj, path) {
332
- let current = obj;
333
- for (const key of path) {
334
- if (current === null || current === undefined) return undefined;
335
- current = current[key];
336
- }
337
- return current;
338
- }
339
-
340
- // Helper: check if any element has attribute with prefix
341
- function hasAttrPrefix(prefix) {
342
- return Array.from(document.querySelectorAll('*')).some(el =>
343
- Array.from(el.attributes).some(attr => attr.name.startsWith(prefix))
344
- );
345
- }
346
-
347
- const results = {};
348
-
349
- for (const [framework, checks] of Object.entries(signals)) {
350
- let totalWeight = 0;
351
- const matchedSignals = [];
352
- let version = null;
353
-
354
- for (const check of checks) {
355
- let matched = false;
356
-
357
- try {
358
- switch (check.type) {
359
- case 'global':
360
- matched = safeGet(window, check.path) !== undefined;
361
- break;
362
-
363
- case 'dom':
364
- if (check.selector.includes('[data-v-]')) {
365
- matched = hasAttrPrefix('data-v-');
366
- } else if (check.selector.includes('[data-astro-cid-]')) {
367
- matched = hasAttrPrefix('data-astro-cid-');
368
- } else if (check.selector.includes('[_nghost-]')) {
369
- matched = hasAttrPrefix('_nghost-');
370
- } else {
371
- matched = !!document.querySelector(check.selector);
372
- }
373
- break;
374
-
375
- case 'script':
376
- const scripts = Array.from(document.querySelectorAll('script[src]'));
377
- matched = scripts.some(s => s.src.includes(check.pattern));
378
- break;
379
-
380
- case 'meta':
381
- const meta = document.querySelector(`meta[name="${check.name}"]`);
382
- matched = meta && meta.content && meta.content.includes(check.pattern);
383
- break;
384
- }
385
- } catch (e) {
386
- matched = false;
387
- }
388
-
389
- if (matched) {
390
- totalWeight += check.weight;
391
- matchedSignals.push(check.signal);
392
- }
393
- }
394
-
395
- // Extract version based on framework
396
- if (totalWeight > 0) {
397
- try {
398
- switch (framework) {
399
- case 'next':
400
- const nextData = safeGet(window, ['__NEXT_DATA__']);
401
- if (nextData) {
402
- version = nextData.nextExport ? 'export' : (nextData.buildId || null);
403
- if (nextData.runtimeConfig?.version) {
404
- version = nextData.runtimeConfig.version;
405
- }
406
- }
407
- break;
408
- case 'nuxt':
409
- const nuxtConfig = safeGet(window, ['__NUXT__', 'config', 'app', 'buildId']);
410
- if (nuxtConfig) version = nuxtConfig;
411
- break;
412
- case 'vue':
413
- version = safeGet(window, ['Vue', 'version']) ||
414
- safeGet(window, ['__VUE__', 'version']) || null;
415
- break;
416
- case 'react':
417
- version = safeGet(window, ['React', 'version']) || null;
418
- break;
419
- case 'angular':
420
- const ngVersion = document.querySelector('[ng-version]');
421
- if (ngVersion) version = ngVersion.getAttribute('ng-version');
422
- break;
423
- case 'svelte':
424
- break;
425
- case 'astro':
426
- const astroMeta = document.querySelector('meta[name="generator"]');
427
- if (astroMeta && astroMeta.content.includes('Astro')) {
428
- const match = astroMeta.content.match(/Astro v?([\d.]+)/);
429
- if (match) version = match[1];
430
- }
431
- break;
432
- }
433
- } catch (e) {
434
- // Ignore version extraction errors
435
- }
436
- }
437
-
438
- results[framework] = {
439
- weight: totalWeight,
440
- signals: matchedSignals,
441
- version
442
- };
443
- }
444
-
445
- return results;
446
- }, DETECTION_SIGNALS);
447
-
448
- // Find framework with highest weight
449
- // Priority order: SSR frameworks first, then base frameworks
450
- const priorityOrder = ['next', 'nuxt', 'astro', 'svelte', 'angular', 'vue', 'react'];
451
-
452
- let bestFramework = null;
453
- let bestWeight = 0;
454
- let bestSignals = [];
455
- let bestVersion = null;
456
-
457
- for (const framework of priorityOrder) {
458
- const result = results[framework];
459
- if (result.weight > bestWeight) {
460
- bestWeight = result.weight;
461
- bestFramework = framework;
462
- bestSignals = result.signals;
463
- bestVersion = result.version;
464
- }
465
- }
466
-
467
- // Calculate confidence
468
- const confidence = bestWeight > 0 ? calculateConfidence(bestWeight) : 'low';
469
-
470
- // Infer routing type
471
- const routingType = await inferRoutingType(page, bestFramework);
472
-
473
- return {
474
- framework: bestFramework,
475
- version: bestVersion,
476
- routingType,
477
- confidence,
478
- signals: bestSignals
479
- };
480
- }
481
-
482
- /**
483
- * Format detection result for CLI output
484
- * @param {FrameworkInfo} info - Detection result
485
- * @returns {string} Human-readable summary
486
- */
487
- export function formatDetectionResult(info) {
488
- if (!info.framework) {
489
- return 'No framework detected (static HTML or unknown framework)';
490
- }
491
-
492
- const parts = [
493
- `Framework: ${info.framework}`,
494
- info.version ? `Version: ${info.version}` : null,
495
- `Routing: ${info.routingType}`,
496
- `Confidence: ${info.confidence}`,
497
- `Signals: ${info.signals.join(', ')}`
498
- ].filter(Boolean);
499
-
500
- return parts.join(' | ');
501
- }
502
-
503
- // CLI support - check if this is the main module being executed directly
504
- // Use import.meta.url to compare with process.argv[1]
505
- import { fileURLToPath } from 'url';
506
- const __filename = fileURLToPath(import.meta.url);
507
- const isMainModule = process.argv[1] === __filename;
508
-
509
- if (isMainModule) {
510
- const { getBrowser, getPage, disconnectBrowser } = await import('../utils/browser.js');
511
-
512
- const url = process.argv[2];
513
- if (!url) {
514
- console.error('Usage: node framework-detector.js <url>');
515
- process.exit(1);
516
- }
517
-
518
- try {
519
- const browser = await getBrowser({ headless: true });
520
- const page = await getPage(browser);
521
-
522
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
523
-
524
- // Wait for hydration
525
- await new Promise(r => setTimeout(r, 2000));
526
-
527
- const result = await detectFramework(page);
528
-
529
- console.log(JSON.stringify(result, null, 2));
530
- console.error('\n' + formatDetectionResult(result));
531
-
532
- await disconnectBrowser();
533
- process.exit(0);
534
- } catch (error) {
535
- console.error(JSON.stringify({ error: error.message }));
536
- process.exit(1);
537
- }
538
- }