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,148 @@
1
+ /**
2
+ * CSS Merge & Deduplication
3
+ *
4
+ * Combines multiple CSS strings into a single stylesheet with deduplication.
5
+ * Preserves cascade order (first occurrence wins).
6
+ * File I/O (reading/writing CSS files) lives in merge-css-file-io.js.
7
+ *
8
+ * Usage:
9
+ * import { mergeCssFiles } from './merge-css-file-io.js';
10
+ * const result = await mergeCssFiles(['a.css', 'b.css'], 'merged.css');
11
+ */
12
+
13
+ import { sanitizeCss } from './filter-css.js';
14
+ import { getRuleHash, processAtrule } from './merge-css-atrule-processor.js';
15
+
16
+ // Import css-tree (already in package.json)
17
+ let csstree;
18
+ try {
19
+ csstree = await import('css-tree');
20
+ } catch {
21
+ console.error('css-tree not installed. Run: npm install css-tree');
22
+ process.exit(1);
23
+ }
24
+
25
+ const DEFAULT_OPTIONS = {
26
+ combineMediaQueries: true,
27
+ deduplicateFontFaces: true,
28
+ deduplicateKeyframes: true,
29
+ removeEmptyRules: true
30
+ };
31
+
32
+ /**
33
+ * Merge multiple CSS strings with deduplication.
34
+ * @param {string[]} cssContents - Array of CSS strings
35
+ * @param {Object} options - Merge options
36
+ * @returns {{ css: string, stats: Object }}
37
+ */
38
+ export function mergeStylesheets(cssContents, options = {}) {
39
+ const opts = { ...DEFAULT_OPTIONS, ...options };
40
+ const stats = {
41
+ inputRules: 0,
42
+ outputRules: 0,
43
+ duplicateRulesRemoved: 0,
44
+ fontFacesDeduped: 0,
45
+ keyframesDeduped: 0,
46
+ mediaQueriesCombined: 0
47
+ };
48
+
49
+ const seenRules = new Map();
50
+ const seenFontFaces = new Map();
51
+ const seenKeyframes = new Map();
52
+ const seenCharset = { found: false, node: null };
53
+ const imports = [];
54
+ const mediaGroups = new Map();
55
+ const outputNodes = [];
56
+ const collections = { seenFontFaces, seenKeyframes, seenCharset, imports, mediaGroups, outputNodes, stats };
57
+
58
+ for (const css of cssContents) {
59
+ if (!css || typeof css !== 'string') continue;
60
+
61
+ let ast;
62
+ try {
63
+ ast = csstree.parse(css, { parseRulePrelude: true, parseValue: false });
64
+ } catch {
65
+ continue;
66
+ }
67
+
68
+ csstree.walk(ast, {
69
+ visit: 'Atrule',
70
+ enter(node) { processAtrule(node, csstree, opts, collections); }
71
+ });
72
+
73
+ csstree.walk(ast, {
74
+ visit: 'Rule',
75
+ enter(node, item, list) {
76
+ // Skip rules nested inside @media (handled by processAtrule)
77
+ let parent = list;
78
+ while (parent && parent.data) {
79
+ if (parent.data.type === 'Atrule') return;
80
+ parent = parent.parent;
81
+ }
82
+
83
+ stats.inputRules++;
84
+ const hash = getRuleHash(node, csstree);
85
+ if (!seenRules.has(hash)) {
86
+ seenRules.set(hash, node);
87
+ outputNodes.push({ type: 'rule', node });
88
+ } else {
89
+ stats.duplicateRulesRemoved++;
90
+ }
91
+ }
92
+ });
93
+ }
94
+
95
+ // Build output AST
96
+ const outputAst = { type: 'StyleSheet', children: new csstree.List() };
97
+ if (seenCharset.node) outputAst.children.push(seenCharset.node);
98
+ for (const imp of imports) outputAst.children.push(imp);
99
+
100
+ for (const item of outputNodes) {
101
+ outputAst.children.push(item.node);
102
+ if (item.type === 'rule' || item.type === 'fontface' || item.type === 'keyframes') {
103
+ stats.outputRules++;
104
+ }
105
+ }
106
+
107
+ if (opts.combineMediaQueries) {
108
+ for (const [condition, rules] of mediaGroups) {
109
+ if (rules.length === 0) continue;
110
+ stats.mediaQueriesCombined++;
111
+
112
+ const mediaBlock = { type: 'Block', children: new csstree.List() };
113
+ for (const r of rules) { mediaBlock.children.push(r.node); stats.outputRules++; }
114
+
115
+ outputAst.children.push({
116
+ type: 'Atrule',
117
+ name: 'media',
118
+ prelude: csstree.parse(condition, { context: 'mediaQueryList' }),
119
+ block: mediaBlock
120
+ });
121
+ }
122
+ }
123
+
124
+ let outputCss = csstree.generate(outputAst);
125
+ outputCss = sanitizeCss(outputCss);
126
+ return { css: outputCss, stats };
127
+ }
128
+
129
+ // CLI support
130
+ const isMainModule = process.argv[1] && (
131
+ process.argv[1].endsWith('merge-css.js') ||
132
+ process.argv[1].includes('merge-css')
133
+ );
134
+
135
+ if (isMainModule) {
136
+ const { mergeCssFiles } = await import('./merge-css-file-io.js');
137
+ const args = process.argv.slice(2);
138
+
139
+ if (args.length < 2) {
140
+ console.error('Usage: node merge-css.js <output.css> <input1.css> [input2.css] ...');
141
+ process.exit(1);
142
+ }
143
+
144
+ const [outputPath, ...inputFiles] = args;
145
+ mergeCssFiles(inputFiles, outputPath)
146
+ .then(result => { console.log(JSON.stringify(result, null, 2)); process.exit(result.success ? 0 : 1); })
147
+ .catch(err => { console.error(JSON.stringify({ success: false, error: err.message })); process.exit(1); });
148
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Routing type inference for detected JavaScript frameworks.
3
+ *
4
+ * Runs page.evaluate to determine whether a framework is using
5
+ * SPA, SSR, or SSG rendering by inspecting framework-specific
6
+ * global objects and DOM attributes. Used by framework-detector.js.
7
+ */
8
+
9
+ /**
10
+ * Infer routing type based on framework and detected signals.
11
+ * @param {import('playwright').Page} page - Playwright page object
12
+ * @param {string|null} framework - Detected framework name
13
+ * @returns {Promise<'spa'|'ssr'|'ssg'|'unknown'>}
14
+ */
15
+ export async function inferRoutingType(page, framework) {
16
+ if (!framework) return 'unknown';
17
+
18
+ return await page.evaluate((fw) => {
19
+ function safeGet(obj, path) {
20
+ let current = obj;
21
+ for (const key of path) {
22
+ if (current === null || current === undefined) return undefined;
23
+ current = current[key];
24
+ }
25
+ return current;
26
+ }
27
+
28
+ try {
29
+ switch (fw) {
30
+ case 'next': {
31
+ const nextData = safeGet(window, ['__NEXT_DATA__']);
32
+ if (nextData) {
33
+ if (nextData.nextExport) return 'ssg';
34
+ if (nextData.isFallback === false) return 'ssr';
35
+ if (document.querySelector('[data-nscript]')) return 'ssr';
36
+ }
37
+ return 'ssr';
38
+ }
39
+ case 'nuxt': {
40
+ const nuxtData = safeGet(window, ['__NUXT__']);
41
+ if (nuxtData?.serverRendered === true) return 'ssr';
42
+ if (nuxtData?.serverRendered === false) return 'spa';
43
+ return 'ssr';
44
+ }
45
+ case 'vue':
46
+ if (window.$nuxt) return 'ssr';
47
+ if (document.querySelector('[data-server-rendered="true"]')) return 'ssr';
48
+ return 'spa';
49
+ case 'react':
50
+ if (safeGet(window, ['__NEXT_DATA__'])) return 'ssr';
51
+ if (window.___gatsby) return 'ssg';
52
+ return 'spa';
53
+ case 'angular':
54
+ if (document.querySelector('[ng-server-context]')) return 'ssr';
55
+ return 'spa';
56
+ case 'svelte':
57
+ if (safeGet(window, ['__sveltekit'])) return 'ssr';
58
+ return 'spa';
59
+ case 'astro':
60
+ return 'ssg';
61
+ default:
62
+ return 'unknown';
63
+ }
64
+ } catch (e) {
65
+ return 'unknown';
66
+ }
67
+ }, framework);
68
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Framework detection signals configuration.
3
+ *
4
+ * Contains the DETECTION_SIGNALS object that maps each supported framework
5
+ * to its detection rules (global objects, DOM selectors, script patterns, meta tags).
6
+ * Each signal has a weight (1-3) used for confidence scoring.
7
+ * Used exclusively by framework-detector.js.
8
+ */
9
+
10
+ /**
11
+ * Detection signals for each framework.
12
+ * Each signal: { type, path|selector|pattern|name, weight (1-3), signal (label) }
13
+ */
14
+ export const DETECTION_SIGNALS = {
15
+ next: [
16
+ { type: 'global', path: ['__NEXT_DATA__'], weight: 3, signal: '__NEXT_DATA__' },
17
+ { type: 'global', path: ['__NEXT_LOADED_PAGES__'], weight: 2, signal: '__NEXT_LOADED_PAGES__' },
18
+ { type: 'global', path: ['__BUILD_MANIFEST'], weight: 2, signal: '__BUILD_MANIFEST' },
19
+ { type: 'dom', selector: '#__next', weight: 2, signal: '#__next' },
20
+ { type: 'script', pattern: '/_next/', weight: 1, signal: 'script:/_next/' }
21
+ ],
22
+ nuxt: [
23
+ { type: 'global', path: ['__NUXT__'], weight: 3, signal: '__NUXT__' },
24
+ { type: 'global', path: ['$nuxt'], weight: 2, signal: '$nuxt' },
25
+ { type: 'global', path: ['__NUXT_PATHS__'], weight: 2, signal: '__NUXT_PATHS__' },
26
+ { type: 'dom', selector: '#__nuxt', weight: 2, signal: '#__nuxt' },
27
+ { type: 'dom', selector: '#__layout', weight: 1, signal: '#__layout' },
28
+ { type: 'script', pattern: '/_nuxt/', weight: 1, signal: 'script:/_nuxt/' }
29
+ ],
30
+ vue: [
31
+ { type: 'global', path: ['__VUE__'], weight: 3, signal: '__VUE__' },
32
+ { type: 'global', path: ['Vue'], weight: 2, signal: 'Vue' },
33
+ { type: 'global', path: ['__VUE_DEVTOOLS_GLOBAL_HOOK__'], weight: 1, signal: '__VUE_DEVTOOLS_GLOBAL_HOOK__' },
34
+ { type: 'dom', selector: '[data-v-]', weight: 2, signal: 'data-v-*' },
35
+ { type: 'dom', selector: '[data-server-rendered]', weight: 2, signal: 'data-server-rendered' }
36
+ ],
37
+ react: [
38
+ { type: 'global', path: ['__REACT_DEVTOOLS_GLOBAL_HOOK__'], weight: 1, signal: '__REACT_DEVTOOLS_GLOBAL_HOOK__' },
39
+ { type: 'dom', selector: '[data-reactroot]', weight: 3, signal: 'data-reactroot' },
40
+ { type: 'dom', selector: '[data-reactid]', weight: 2, signal: 'data-reactid' },
41
+ { type: 'dom', selector: '#root[data-reactroot], #root > div', weight: 1, signal: '#root' }
42
+ ],
43
+ angular: [
44
+ { type: 'global', path: ['ng'], weight: 2, signal: 'ng' },
45
+ { type: 'global', path: ['getAllAngularRootElements'], weight: 3, signal: 'getAllAngularRootElements' },
46
+ { type: 'dom', selector: '[ng-version]', weight: 3, signal: 'ng-version' },
47
+ { type: 'dom', selector: 'app-root', weight: 2, signal: 'app-root' },
48
+ { type: 'dom', selector: '[_nghost-]', weight: 2, signal: '_nghost-*' },
49
+ { type: 'dom', selector: '[ng-app]', weight: 2, signal: 'ng-app' }
50
+ ],
51
+ svelte: [
52
+ { type: 'global', path: ['__svelte__'], weight: 2, signal: '__svelte__' },
53
+ { type: 'global', path: ['__sveltekit'], weight: 3, signal: '__sveltekit' },
54
+ { type: 'dom', selector: '[data-sveltekit-preload-data]', weight: 3, signal: 'data-sveltekit-preload-data' },
55
+ { type: 'dom', selector: '[data-sveltekit-reload]', weight: 2, signal: 'data-sveltekit-reload' },
56
+ { type: 'script', pattern: '/@svelte/', weight: 1, signal: 'script:/@svelte/' }
57
+ ],
58
+ astro: [
59
+ { type: 'dom', selector: 'astro-island', weight: 3, signal: 'astro-island' },
60
+ { type: 'dom', selector: '[data-astro-cid-]', weight: 2, signal: 'data-astro-cid-*' },
61
+ { type: 'dom', selector: '[data-astro-source-file]', weight: 2, signal: 'data-astro-source-file' },
62
+ { type: 'meta', name: 'generator', pattern: 'Astro', weight: 3, signal: 'meta:generator:Astro' },
63
+ { type: 'script', pattern: '/@astrojs/', weight: 1, signal: 'script:/@astrojs/' }
64
+ ]
65
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Framework Detector Module
3
+ *
4
+ * Detects JS frameworks via global objects, DOM attributes, script patterns.
5
+ * Returns framework info with confidence scoring.
6
+ *
7
+ * Usage:
8
+ * import { detectFramework } from './framework-detector.js';
9
+ * const info = await detectFramework(page);
10
+ */
11
+
12
+ import { DETECTION_SIGNALS } from './framework-detector-signals.js';
13
+ import { inferRoutingType } from './framework-detector-routing.js';
14
+
15
+ export { DETECTION_SIGNALS };
16
+
17
+ /**
18
+ * @typedef {Object} FrameworkInfo
19
+ * @property {string|null} framework
20
+ * @property {string|null} version
21
+ * @property {'spa'|'ssr'|'ssg'|'unknown'} routingType
22
+ * @property {'high'|'medium'|'low'} confidence
23
+ * @property {string[]} signals
24
+ */
25
+
26
+ function calculateConfidence(w) {
27
+ return w >= 5 ? 'high' : w >= 3 ? 'medium' : 'low';
28
+ }
29
+
30
+ /**
31
+ * Detect framework used on the current page.
32
+ * @param {import('playwright').Page} page
33
+ * @returns {Promise<FrameworkInfo>}
34
+ */
35
+ export async function detectFramework(page) {
36
+ const results = await page.evaluate((signals) => {
37
+ function safeGet(obj, path) {
38
+ let current = obj;
39
+ for (const key of path) {
40
+ if (current === null || current === undefined) return undefined;
41
+ current = current[key];
42
+ }
43
+ return current;
44
+ }
45
+
46
+ function hasAttrPrefix(prefix) {
47
+ return Array.from(document.querySelectorAll('*')).some(el =>
48
+ Array.from(el.attributes).some(attr => attr.name.startsWith(prefix))
49
+ );
50
+ }
51
+
52
+ const results = {};
53
+
54
+ for (const [framework, checks] of Object.entries(signals)) {
55
+ let totalWeight = 0;
56
+ const matchedSignals = [];
57
+ let version = null;
58
+
59
+ for (const check of checks) {
60
+ let matched = false;
61
+ try {
62
+ switch (check.type) {
63
+ case 'global':
64
+ matched = safeGet(window, check.path) !== undefined;
65
+ break;
66
+ case 'dom':
67
+ if (check.selector.includes('[data-v-]')) {
68
+ matched = hasAttrPrefix('data-v-');
69
+ } else if (check.selector.includes('[data-astro-cid-]')) {
70
+ matched = hasAttrPrefix('data-astro-cid-');
71
+ } else if (check.selector.includes('[_nghost-]')) {
72
+ matched = hasAttrPrefix('_nghost-');
73
+ } else {
74
+ matched = !!document.querySelector(check.selector);
75
+ }
76
+ break;
77
+ case 'script': {
78
+ const scripts = Array.from(document.querySelectorAll('script[src]'));
79
+ matched = scripts.some(s => s.src.includes(check.pattern));
80
+ break;
81
+ }
82
+ case 'meta': {
83
+ const meta = document.querySelector(`meta[name="${check.name}"]`);
84
+ matched = !!(meta?.content?.includes(check.pattern));
85
+ break;
86
+ }
87
+ }
88
+ } catch (e) {
89
+ matched = false;
90
+ }
91
+
92
+ if (matched) {
93
+ totalWeight += check.weight;
94
+ matchedSignals.push(check.signal);
95
+ }
96
+ }
97
+
98
+ // Version extraction
99
+ if (totalWeight > 0) {
100
+ try {
101
+ switch (framework) {
102
+ case 'next': {
103
+ const d = safeGet(window, ['__NEXT_DATA__']);
104
+ if (d) {
105
+ version = d.nextExport ? 'export' : (d.buildId || null);
106
+ if (d.runtimeConfig?.version) version = d.runtimeConfig.version;
107
+ }
108
+ break;
109
+ }
110
+ case 'nuxt': {
111
+ const v = safeGet(window, ['__NUXT__', 'config', 'app', 'buildId']);
112
+ if (v) version = v;
113
+ break;
114
+ }
115
+ case 'vue':
116
+ version = safeGet(window, ['Vue', 'version']) ||
117
+ safeGet(window, ['__VUE__', 'version']) || null;
118
+ break;
119
+ case 'react':
120
+ version = safeGet(window, ['React', 'version']) || null;
121
+ break;
122
+ case 'angular': {
123
+ const el = document.querySelector('[ng-version]');
124
+ if (el) version = el.getAttribute('ng-version');
125
+ break;
126
+ }
127
+ case 'astro': {
128
+ const m = document.querySelector('meta[name="generator"]');
129
+ if (m?.content?.includes('Astro')) {
130
+ const match = m.content.match(/Astro v?([\d.]+)/);
131
+ if (match) version = match[1];
132
+ }
133
+ break;
134
+ }
135
+ }
136
+ } catch (e) { /* ignore version errors */ }
137
+ }
138
+
139
+ results[framework] = { weight: totalWeight, signals: matchedSignals, version };
140
+ }
141
+
142
+ return results;
143
+ }, DETECTION_SIGNALS);
144
+
145
+ // SSR frameworks take priority over base frameworks
146
+ let bestFramework = null, bestWeight = 0, bestSignals = [], bestVersion = null;
147
+ for (const fw of ['next', 'nuxt', 'astro', 'svelte', 'angular', 'vue', 'react']) {
148
+ if (results[fw].weight > bestWeight) {
149
+ bestWeight = results[fw].weight; bestFramework = fw;
150
+ bestSignals = results[fw].signals; bestVersion = results[fw].version;
151
+ }
152
+ }
153
+ const confidence = bestWeight > 0 ? calculateConfidence(bestWeight) : 'low';
154
+ const routingType = await inferRoutingType(page, bestFramework);
155
+
156
+ return {
157
+ framework: bestFramework,
158
+ version: bestVersion,
159
+ routingType,
160
+ confidence,
161
+ signals: bestSignals
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Format detection result for CLI output.
167
+ * @param {FrameworkInfo} info
168
+ * @returns {string}
169
+ */
170
+ export function formatDetectionResult(info) {
171
+ if (!info.framework) return 'No framework detected (static HTML or unknown framework)';
172
+ return [`Framework: ${info.framework}`, info.version ? `Version: ${info.version}` : null,
173
+ `Routing: ${info.routingType}`, `Confidence: ${info.confidence}`,
174
+ `Signals: ${info.signals.join(', ')}`].filter(Boolean).join(' | ');
175
+ }
176
+
177
+ // CLI support
178
+ import { fileURLToPath } from 'url';
179
+ const __filename = fileURLToPath(import.meta.url);
180
+ if (process.argv[1] === __filename) {
181
+ const { getBrowser, getPage, disconnectBrowser } = await import('../../utils/browser.js');
182
+ const url = process.argv[2];
183
+ if (!url) { console.error('Usage: node framework-detector.js <url>'); process.exit(1); }
184
+ try {
185
+ const browser = await getBrowser({ headless: true });
186
+ const page = await getPage(browser);
187
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
188
+ await new Promise(r => setTimeout(r, 2000));
189
+ const result = await detectFramework(page);
190
+ console.log(JSON.stringify(result, null, 2));
191
+ console.error('\n' + formatDetectionResult(result));
192
+ await disconnectBrowser();
193
+ process.exit(0);
194
+ } catch (error) {
195
+ console.error(JSON.stringify({ error: error.message }));
196
+ process.exit(1);
197
+ }
198
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Card Pattern and Grid Layout Detector
3
+ *
4
+ * Browser-side functions (run inside page.evaluate) that detect repeating
5
+ * card groups and grid/flex layout patterns from extracted container data.
6
+ * These are injected into the page context via dimension-extractor.js.
7
+ */
8
+
9
+ /**
10
+ * Calculate visual similarity score between two card elements.
11
+ * Weights: width 40%, height 30%, margin 15%, border-radius 15%.
12
+ * @param {{ width, height, marginTop, marginBottom, borderRadius }} a
13
+ * @param {{ width, height, marginTop, marginBottom, borderRadius }} b
14
+ * @returns {number} 0–1 similarity score
15
+ */
16
+ export function calculateSimilarity(a, b) {
17
+ const widthSim = 1 - Math.abs(a.width - b.width) / Math.max(a.width, b.width, 1);
18
+ const heightSim = 1 - Math.abs(a.height - b.height) / Math.max(a.height, b.height, 1);
19
+ const marginA = a.marginTop + a.marginBottom;
20
+ const marginB = b.marginTop + b.marginBottom;
21
+ const marginSim = 1 - Math.abs(marginA - marginB) / Math.max(marginA, marginB, 1);
22
+ const radiusSim = a.borderRadius === b.borderRadius ? 1 : 0.5;
23
+ return (widthSim * 0.4) + (heightSim * 0.3) + (marginSim * 0.15) + (radiusSim * 0.15);
24
+ }
25
+
26
+ /**
27
+ * Detect layout type from a group of elements.
28
+ * @param {Array<{ x, y, width, height }>} elements
29
+ * @returns {'row'|'column'|'grid'|'single'}
30
+ */
31
+ export function detectLayoutType(elements) {
32
+ if (elements.length < 2) return 'single';
33
+ const yPositions = elements.map(el => el.y);
34
+ const xPositions = elements.map(el => el.x);
35
+ const yVariance = Math.max(...yPositions) - Math.min(...yPositions);
36
+ const xVariance = Math.max(...xPositions) - Math.min(...xPositions);
37
+ const avgHeight = elements.reduce((s, el) => s + el.height, 0) / elements.length;
38
+ const avgWidth = elements.reduce((s, el) => s + el.width, 0) / elements.length;
39
+
40
+ if (yVariance < avgHeight * 0.3 && xVariance > avgWidth) return 'row';
41
+ if (xVariance < avgWidth * 0.3 && yVariance > avgHeight) return 'column';
42
+ return 'grid';
43
+ }
44
+
45
+ /**
46
+ * Calculate average gap between elements based on layout direction.
47
+ * @param {Array<{ x, y, width, height }>} elements
48
+ * @param {'row'|'column'|'grid'} layout
49
+ * @returns {number} Average gap in px
50
+ */
51
+ export function calculateGap(elements, layout) {
52
+ if (elements.length < 2) return 0;
53
+ const sorted = layout === 'column'
54
+ ? [...elements].sort((a, b) => a.y - b.y)
55
+ : [...elements].sort((a, b) => a.x - b.x);
56
+
57
+ let totalGap = 0, gapCount = 0;
58
+ for (let i = 1; i < sorted.length; i++) {
59
+ const gap = layout === 'column'
60
+ ? sorted[i].y - (sorted[i - 1].y + sorted[i - 1].height)
61
+ : sorted[i].x - (sorted[i - 1].x + sorted[i - 1].width);
62
+ if (gap > 0 && gap < 200) { totalGap += gap; gapCount++; }
63
+ }
64
+ return gapCount > 0 ? Math.round(totalGap / gapCount) : 0;
65
+ }
66
+
67
+ /**
68
+ * Return serializable card/grid detector functions as source strings.
69
+ * These are injected into page.evaluate so they run in browser context.
70
+ *
71
+ * Usage in page.evaluate:
72
+ * const { calculateSimilarity, detectLayoutType, calculateGap } = injected;
73
+ *
74
+ * @returns {{ calculateSimilarity: string, detectLayoutType: string, calculateGap: string }}
75
+ */
76
+ export function getCardDetectorSources() {
77
+ return {
78
+ calculateSimilarity: calculateSimilarity.toString(),
79
+ detectLayoutType: detectLayoutType.toString(),
80
+ calculateGap: calculateGap.toString()
81
+ };
82
+ }