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.
- package/README.md +13 -34
- package/SKILL.md +69 -45
- package/bin/cli.js +22 -4
- package/bin/commands/clone-site.js +31 -171
- package/bin/commands/help.js +19 -6
- package/bin/commands/init.js +9 -86
- package/bin/commands/uninstall.js +105 -0
- package/bin/commands/update.js +70 -0
- package/bin/commands/verify.js +7 -14
- package/bin/utils/paths.js +28 -0
- package/bin/utils/validate.js +2 -22
- package/bin/utils/version.js +23 -0
- package/docs/code-standards.md +789 -0
- package/docs/codebase-summary.md +533 -286
- package/docs/index.md +74 -0
- package/docs/project-overview-pdr.md +797 -0
- package/docs/system-architecture.md +718 -0
- package/package.json +14 -17
- package/src/ai/prompts/design-tokens/basic.md +80 -0
- package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
- package/src/ai/prompts/design-tokens/section.md +48 -0
- package/src/ai/prompts/design-tokens/with-css.md +87 -0
- package/src/ai/prompts/structure-analysis/basic.md +55 -0
- package/src/ai/prompts/structure-analysis/with-context.md +59 -0
- package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
- package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
- package/src/ai/prompts/ux-audit/aggregation.md +42 -0
- package/src/ai/prompts/ux-audit/desktop.md +92 -0
- package/src/ai/prompts/ux-audit/mobile.md +93 -0
- package/src/ai/prompts/ux-audit/tablet.md +92 -0
- package/src/core/animation/animation-extractor-ast.js +183 -0
- package/src/core/animation/animation-extractor-output.js +152 -0
- package/src/core/animation/animation-extractor.js +178 -0
- package/src/core/animation/state-capture-detection.js +200 -0
- package/src/core/animation/state-capture.js +193 -0
- package/src/core/capture/browser-context-pool.js +96 -0
- package/src/core/capture/multi-page-screenshot-page.js +110 -0
- package/src/core/capture/multi-page-screenshot.js +208 -0
- package/src/core/capture/screenshot-extraction.js +186 -0
- package/src/core/capture/screenshot-helpers.js +175 -0
- package/src/core/capture/screenshot-orchestrator.js +174 -0
- package/src/core/capture/screenshot-viewport.js +93 -0
- package/src/core/capture/screenshot.js +192 -0
- package/src/core/content/content-counter-dom.js +191 -0
- package/src/core/content/content-counter.js +76 -0
- package/src/core/css/breakpoint-detector.js +66 -0
- package/src/core/css/chromium-defaults.json +23 -0
- package/src/core/css/computed-style-extractor.js +102 -0
- package/src/core/css/css-chunker.js +103 -0
- package/src/core/css/filter-css-dead-code.js +120 -0
- package/src/core/css/filter-css-html-analyzer.js +110 -0
- package/src/core/css/filter-css-selector-matcher.js +172 -0
- package/src/core/css/filter-css.js +206 -0
- package/src/core/css/merge-css-atrule-processor.js +158 -0
- package/src/core/css/merge-css-file-io.js +68 -0
- package/src/core/css/merge-css.js +148 -0
- package/src/core/detection/framework-detector-routing.js +68 -0
- package/src/core/detection/framework-detector-signals.js +65 -0
- package/src/core/detection/framework-detector.js +198 -0
- package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
- package/src/core/dimension/dimension-extractor.js +317 -0
- package/src/core/dimension/dimension-output-ai-summary.js +111 -0
- package/src/core/dimension/dimension-output.js +173 -0
- package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
- package/src/core/dimension/dom-tree-analyzer.js +191 -0
- package/src/core/discovery/app-state-snapshot-capture.js +195 -0
- package/src/core/discovery/app-state-snapshot-utils.js +178 -0
- package/src/core/discovery/app-state-snapshot.js +131 -0
- package/src/core/discovery/discover-pages-routes.js +84 -0
- package/src/core/discovery/discover-pages-utils.js +177 -0
- package/src/core/discovery/discover-pages.js +191 -0
- package/src/core/html/html-extractor-inline-styler.js +70 -0
- package/src/core/html/html-extractor.js +147 -0
- package/src/core/html/semantic-enhancer-mappings.js +200 -0
- package/src/core/html/semantic-enhancer-page.js +148 -0
- package/src/core/html/semantic-enhancer.js +135 -0
- package/src/core/links/rewrite-links-css-rewriter.js +53 -0
- package/src/core/links/rewrite-links.js +173 -0
- package/src/core/media/asset-validator.js +118 -0
- package/src/core/media/extract-assets-downloader.js +187 -0
- package/src/core/media/extract-assets-page-scraper.js +115 -0
- package/src/core/media/extract-assets.js +159 -0
- package/src/core/media/video-capture-convert.js +200 -0
- package/src/core/media/video-capture.js +201 -0
- package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
- package/src/core/section/section-cropper-helpers.js +43 -0
- package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
- package/src/core/section/section-detector-strategies.js +139 -0
- package/src/core/section/section-detector-utils.js +100 -0
- package/src/core/section/section-detector.js +88 -0
- package/src/core/tests/test-section-cropper.js +2 -2
- package/src/core/tests/test-section-detector.js +2 -2
- package/src/post-process/enhance-assets.js +29 -4
- package/src/post-process/fetch-images-unsplash-client.js +123 -0
- package/src/post-process/fetch-images.js +60 -263
- package/src/post-process/inject-gosnap.js +88 -0
- package/src/post-process/inject-icons-svg-replacer.js +76 -0
- package/src/post-process/inject-icons.js +47 -200
- package/src/route-discoverers/base-discoverer-utils.js +137 -0
- package/src/route-discoverers/base-discoverer.js +29 -118
- package/src/route-discoverers/index.js +1 -1
- package/src/shared/config.js +38 -0
- package/src/shared/error-codes.js +31 -0
- package/src/shared/viewports.js +46 -0
- package/src/utils/browser.js +0 -7
- package/src/utils/helpers.js +4 -0
- package/src/utils/log.js +12 -0
- package/src/utils/playwright-loader.js +76 -0
- package/src/utils/playwright.js +3 -69
- package/src/utils/progress.js +32 -0
- package/src/verification/generate-audit-report-css-fixes.js +52 -0
- package/src/verification/generate-audit-report-sections.js +158 -0
- package/src/verification/generate-audit-report.js +5 -281
- package/src/verification/quality-scorer.js +92 -0
- package/src/verification/verify-footer-checks.js +103 -0
- package/src/verification/verify-footer-helpers.js +178 -0
- package/src/verification/verify-footer.js +23 -381
- package/src/verification/verify-header-checks.js +104 -0
- package/src/verification/verify-header-helpers.js +156 -0
- package/src/verification/verify-header.js +23 -365
- package/src/verification/verify-layout-report.js +101 -0
- package/src/verification/verify-layout.js +13 -259
- package/src/verification/verify-menu-checks.js +104 -0
- package/src/verification/verify-menu-helpers.js +112 -0
- package/src/verification/verify-menu.js +17 -285
- package/src/verification/verify-slider-checks.js +115 -0
- package/src/verification/verify-slider-constants.js +65 -0
- package/src/verification/verify-slider-helpers.js +164 -0
- package/src/verification/verify-slider.js +23 -414
- package/.env.example +0 -14
- package/docs/basic-clone.md +0 -63
- package/docs/cli-reference.md +0 -316
- package/docs/design-clone-architecture.md +0 -492
- package/docs/pixel-perfect.md +0 -117
- package/docs/project-roadmap.md +0 -382
- package/docs/troubleshooting.md +0 -170
- package/requirements.txt +0 -5
- package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
- package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
- package/src/ai/analyze-structure.py +0 -375
- package/src/ai/extract-design-tokens.py +0 -782
- package/src/ai/prompts/__init__.py +0 -2
- package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
- package/src/ai/prompts/design_tokens.py +0 -316
- package/src/ai/prompts/structure_analysis.py +0 -592
- package/src/ai/prompts/ux_audit.py +0 -198
- package/src/ai/ux-audit.js +0 -596
- package/src/core/animation-extractor.js +0 -526
- package/src/core/app-state-snapshot.js +0 -511
- package/src/core/content-counter.js +0 -342
- package/src/core/design-tokens.js +0 -103
- package/src/core/dimension-extractor.js +0 -438
- package/src/core/dimension-output.js +0 -305
- package/src/core/discover-pages.js +0 -542
- package/src/core/dom-tree-analyzer.js +0 -298
- package/src/core/extract-assets.js +0 -468
- package/src/core/filter-css.js +0 -499
- package/src/core/framework-detector.js +0 -538
- package/src/core/html-extractor.js +0 -212
- package/src/core/merge-css.js +0 -407
- package/src/core/multi-page-screenshot.js +0 -380
- package/src/core/rewrite-links.js +0 -226
- package/src/core/screenshot.js +0 -701
- package/src/core/section-detector.js +0 -386
- package/src/core/semantic-enhancer.js +0 -492
- package/src/core/state-capture.js +0 -598
- package/src/core/video-capture.js +0 -546
- package/src/utils/__init__.py +0 -16
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/env.py +0 -134
- /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
- /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
- /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
|
+
}
|