@saeroon/cli 0.2.8 → 0.2.10
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/dist/index.js +248 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2547,9 +2547,8 @@ var VALID_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
|
2547
2547
|
"main-block",
|
|
2548
2548
|
"aside-block",
|
|
2549
2549
|
"article-block",
|
|
2550
|
-
// Specialized (
|
|
2550
|
+
// Specialized (4)
|
|
2551
2551
|
"site-menu",
|
|
2552
|
-
"floating-action-widget",
|
|
2553
2552
|
"sticky-cta-bar",
|
|
2554
2553
|
"cookie-consent-bar",
|
|
2555
2554
|
"announcement-bar"
|
|
@@ -3926,9 +3925,18 @@ import { resolve as resolve13, join as join2 } from "path";
|
|
|
3926
3925
|
import { mkdir as mkdir4, writeFile as writeFile8 } from "fs/promises";
|
|
3927
3926
|
|
|
3928
3927
|
// src/scripts/analyze-reference.ts
|
|
3929
|
-
import { spawnSync } from "child_process";
|
|
3928
|
+
import { spawnSync, execSync } from "child_process";
|
|
3930
3929
|
import { writeFile as writeFile7, mkdir as mkdir3 } from "fs/promises";
|
|
3931
3930
|
import { join } from "path";
|
|
3931
|
+
function getNodeEnvWithGlobalModules() {
|
|
3932
|
+
try {
|
|
3933
|
+
const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
|
|
3934
|
+
const existing = process.env.NODE_PATH || "";
|
|
3935
|
+
return { ...process.env, NODE_PATH: existing ? `${existing}:${globalRoot}` : globalRoot };
|
|
3936
|
+
} catch {
|
|
3937
|
+
return { ...process.env };
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3932
3940
|
var VIEWPORTS = [
|
|
3933
3941
|
{ name: "mobile", width: 375, height: 812 },
|
|
3934
3942
|
{ name: "tablet", width: 768, height: 1024 },
|
|
@@ -4026,6 +4034,25 @@ var EXTRACTION_SCRIPT = `
|
|
|
4026
4034
|
return 'card-thumbnail';
|
|
4027
4035
|
}
|
|
4028
4036
|
|
|
4037
|
+
// \u2500\u2500 0. \uD31D\uC5C5/\uC624\uBC84\uB808\uC774 DOM \uC81C\uAC70 (\uC139\uC158 \uAC10\uC9C0 \uC804) \u2500\u2500
|
|
4038
|
+
document.querySelectorAll(
|
|
4039
|
+
'dialog[open], [class*=popup], [class*=layerpopup], [class*=modal]:not(body), [class*=cookie], [class*=consent], [class*=overlay]:not(body), [class*=dimmed]'
|
|
4040
|
+
).forEach(el => {
|
|
4041
|
+
const rect = el.getBoundingClientRect();
|
|
4042
|
+
const style = window.getComputedStyle(el);
|
|
4043
|
+
const pos = style.position;
|
|
4044
|
+
// fixed/absolute \uC704\uCE58 + \uBDF0\uD3EC\uD2B8 \uB300\uBD80\uBD84 \uCC28\uC9C0 \u2192 \uD31D\uC5C5/\uC624\uBC84\uB808\uC774
|
|
4045
|
+
if ((pos === 'fixed' || pos === 'absolute') && rect.width >= window.innerWidth * 0.5) {
|
|
4046
|
+
el.remove();
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
// leeko-layerpopup \uAC19\uC740 \uD31D\uC5C5: class\uC5D0 popup/modal/overlay \uD3EC\uD568 + \uB2EB\uAE30 \uBC84\uD2BC \uC874\uC7AC
|
|
4050
|
+
if (el.querySelector('[class*=close], [aria-label*=close], button')) {
|
|
4051
|
+
const isSmall = rect.width < 100 && rect.height < 100;
|
|
4052
|
+
if (!isSmall) el.remove();
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
|
|
4029
4056
|
// \u2500\u2500 1. Structure \u2500\u2500
|
|
4030
4057
|
const sectionSelectors = 'body > header, body > footer, body > nav, body > main, body > section, body > div, body > article, body > aside';
|
|
4031
4058
|
const topLevelEls = document.querySelectorAll(sectionSelectors);
|
|
@@ -4098,6 +4125,39 @@ var EXTRACTION_SCRIPT = `
|
|
|
4098
4125
|
return !leafTags.has(tag);
|
|
4099
4126
|
});
|
|
4100
4127
|
|
|
4128
|
+
// \u2500\u2500 Fallback: \uC139\uC158 3\uAC1C \uBBF8\uB9CC\uC774\uBA74 content-based \uD0D0\uC0C9 \u2500\u2500
|
|
4129
|
+
if (sectionEls.length < 3) {
|
|
4130
|
+
const vw = window.innerWidth;
|
|
4131
|
+
const candidates = [];
|
|
4132
|
+
// \uBAA8\uB4E0 \uC694\uC18C\uB97C \uC21C\uD68C\uD558\uBA70 \uC139\uC158 \uD6C4\uBCF4 \uD0D0\uC0C9
|
|
4133
|
+
document.querySelectorAll('header, footer, nav, section, article, aside, div, main').forEach(el => {
|
|
4134
|
+
if (leafTags.has(el.tagName.toLowerCase())) return;
|
|
4135
|
+
const rect = el.getBoundingClientRect();
|
|
4136
|
+
// \uC804\uCCB4 \uD3ED\uC758 80% \uC774\uC0C1 + \uB192\uC774 100px \uC774\uC0C1
|
|
4137
|
+
if (rect.width < vw * 0.8 || rect.height < 100) return;
|
|
4138
|
+
// \uD615\uC81C\uAC00 \uC788\uB294 \uC694\uC18C\uB9CC (\uC139\uC158 = \uAC19\uC740 \uB808\uBCA8 \uD615\uC81C\uB4E4)
|
|
4139
|
+
const parent = el.parentElement;
|
|
4140
|
+
if (!parent) return;
|
|
4141
|
+
const siblings = Array.from(parent.children).filter(s => {
|
|
4142
|
+
const sr = s.getBoundingClientRect();
|
|
4143
|
+
return sr.width >= vw * 0.8 && sr.height >= 100 && !leafTags.has(s.tagName.toLowerCase());
|
|
4144
|
+
});
|
|
4145
|
+
if (siblings.length >= 2) {
|
|
4146
|
+
candidates.push({ el, siblings, parentEl: parent, sibCount: siblings.length });
|
|
4147
|
+
}
|
|
4148
|
+
});
|
|
4149
|
+
|
|
4150
|
+
// \uAC00\uC7A5 \uB9CE\uC740 \uD615\uC81C\uB97C \uAC00\uC9C4 \uADF8\uB8F9 \uC120\uD0DD
|
|
4151
|
+
if (candidates.length > 0) {
|
|
4152
|
+
candidates.sort((a, b) => b.sibCount - a.sibCount);
|
|
4153
|
+
const best = candidates[0];
|
|
4154
|
+
// \uAE30\uC874 \uACB0\uACFC\uBCF4\uB2E4 \uB098\uC740 \uACBD\uC6B0\uB9CC \uAD50\uCCB4
|
|
4155
|
+
if (best.sibCount > sectionEls.length) {
|
|
4156
|
+
sectionEls = best.siblings;
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4101
4161
|
const sections = [];
|
|
4102
4162
|
let maxDepth = 0;
|
|
4103
4163
|
|
|
@@ -4256,7 +4316,22 @@ var EXTRACTION_SCRIPT = `
|
|
|
4256
4316
|
if (ff) fontFamilySet.add(ff.split(',')[0].trim().replace(/['"]/g, ''));
|
|
4257
4317
|
});
|
|
4258
4318
|
|
|
4259
|
-
// 2c. Spacing
|
|
4319
|
+
// 2c. Spacing (B-2: \uC815\uBC00 \uCD94\uCD9C)
|
|
4320
|
+
function median(arr) {
|
|
4321
|
+
if (arr.length === 0) return 0;
|
|
4322
|
+
const s = [...arr].sort((a, b) => a - b);
|
|
4323
|
+
const mid = Math.floor(s.length / 2);
|
|
4324
|
+
return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
|
|
4325
|
+
}
|
|
4326
|
+
function mode(arr) {
|
|
4327
|
+
if (arr.length === 0) return 0;
|
|
4328
|
+
const freq = {};
|
|
4329
|
+
arr.forEach(v => { const k = Math.round(v); freq[k] = (freq[k] || 0) + 1; });
|
|
4330
|
+
return Number(Object.entries(freq).sort((a, b) => b[1] - a[1])[0][0]);
|
|
4331
|
+
}
|
|
4332
|
+
function roundTo4(v) { return Math.round(v / 4) * 4; } // 4px \uB2E8\uC704 \uC815\uADDC\uD654
|
|
4333
|
+
|
|
4334
|
+
// \uC139\uC158 \uAC04 \uAC04\uACA9
|
|
4260
4335
|
const sectionGaps = [];
|
|
4261
4336
|
for (let i = 1; i < sections.length; i++) {
|
|
4262
4337
|
const prev = topLevelEls[i - 1];
|
|
@@ -4265,25 +4340,107 @@ var EXTRACTION_SCRIPT = `
|
|
|
4265
4340
|
const prevRect = prev.getBoundingClientRect();
|
|
4266
4341
|
const currRect = curr.getBoundingClientRect();
|
|
4267
4342
|
const gap = currRect.top - prevRect.bottom;
|
|
4268
|
-
if (gap > 0 && gap < 500) sectionGaps.push(gap);
|
|
4343
|
+
if (gap > 0 && gap < 500) sectionGaps.push(Math.round(gap));
|
|
4269
4344
|
}
|
|
4270
4345
|
}
|
|
4271
4346
|
|
|
4272
|
-
|
|
4273
|
-
const
|
|
4347
|
+
// \uC139\uC158 padding-top / padding-bottom \uC218\uC9D1
|
|
4348
|
+
const sectionPaddingTops = [];
|
|
4349
|
+
const sectionPaddingBottoms = [];
|
|
4350
|
+
topLevelEls.forEach(el => {
|
|
4351
|
+
if (!el) return;
|
|
4352
|
+
const pt = parsePixel(getComputedProp(el, 'padding-top'));
|
|
4353
|
+
const pb = parsePixel(getComputedProp(el, 'padding-bottom'));
|
|
4354
|
+
if (pt > 0) sectionPaddingTops.push(Math.round(pt));
|
|
4355
|
+
if (pb > 0) sectionPaddingBottoms.push(Math.round(pb));
|
|
4356
|
+
});
|
|
4274
4357
|
|
|
4275
|
-
//
|
|
4276
|
-
|
|
4277
|
-
const
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4358
|
+
// content padding: \uB2E4\uC218 \uCEE8\uD14C\uC774\uB108\uC5D0\uC11C \uC218\uC9D1
|
|
4359
|
+
const contentPaddingValues = [];
|
|
4360
|
+
const contentPaddingDetails = [];
|
|
4361
|
+
const containerSelectors = 'main, [class*=container], [class*=wrapper], [class*=content], [class*=inner]';
|
|
4362
|
+
document.querySelectorAll(containerSelectors).forEach(el => {
|
|
4363
|
+
const pl = parsePixel(getComputedProp(el, 'padding-left'));
|
|
4364
|
+
const pr = parsePixel(getComputedProp(el, 'padding-right'));
|
|
4365
|
+
if (pl > 0 || pr > 0) {
|
|
4366
|
+
contentPaddingValues.push(pl, pr);
|
|
4367
|
+
contentPaddingDetails.push({ left: Math.round(pl), right: Math.round(pr), count: 1 });
|
|
4368
|
+
}
|
|
4369
|
+
});
|
|
4370
|
+
// \uC911\uBCF5 \uD569\uC0B0
|
|
4371
|
+
const paddingMap = {};
|
|
4372
|
+
contentPaddingDetails.forEach(d => {
|
|
4373
|
+
const k = d.left + ',' + d.right;
|
|
4374
|
+
if (paddingMap[k]) paddingMap[k].count++;
|
|
4375
|
+
else paddingMap[k] = { ...d };
|
|
4376
|
+
});
|
|
4377
|
+
const uniquePaddings = Object.values(paddingMap).sort((a, b) => b.count - a.count);
|
|
4378
|
+
const contentPadding = contentPaddingValues.length > 0 ? mode(contentPaddingValues.filter(v => v > 0)) : 16;
|
|
4379
|
+
|
|
4380
|
+
// card gap: \uBAA8\uB4E0 grid/flex \uCEE8\uD14C\uC774\uB108\uC5D0\uC11C \uC218\uC9D1
|
|
4381
|
+
const cardGapValues = [];
|
|
4382
|
+
const allLayoutContainers = document.querySelectorAll('*');
|
|
4383
|
+
allLayoutContainers.forEach(el => {
|
|
4384
|
+
const display = getComputedProp(el, 'display');
|
|
4385
|
+
if (!display || (!display.includes('grid') && !display.includes('flex'))) return;
|
|
4386
|
+
const gap = parsePixel(getComputedProp(el, 'gap'));
|
|
4387
|
+
const colGap = parsePixel(getComputedProp(el, 'column-gap'));
|
|
4388
|
+
const rowGap = parsePixel(getComputedProp(el, 'row-gap'));
|
|
4389
|
+
const v = gap || colGap || rowGap;
|
|
4390
|
+
if (v > 0 && v < 200) cardGapValues.push(Math.round(v));
|
|
4391
|
+
});
|
|
4392
|
+
// margin \uAE30\uBC18 \uAC04\uACA9\uB3C4 \uAC10\uC9C0 (flex/grid \uC544\uB2CC \uB9AC\uC2A4\uD2B8)
|
|
4393
|
+
document.querySelectorAll('ul, ol, [class*=list], [class*=cards]').forEach(parent => {
|
|
4394
|
+
const children = Array.from(parent.children);
|
|
4395
|
+
for (let i = 1; i < Math.min(children.length, 5); i++) {
|
|
4396
|
+
const prevRect = children[i - 1].getBoundingClientRect();
|
|
4397
|
+
const currRect = children[i].getBoundingClientRect();
|
|
4398
|
+
const vGap = currRect.top - prevRect.bottom;
|
|
4399
|
+
const hGap = currRect.left - prevRect.right;
|
|
4400
|
+
const g = Math.abs(vGap) < 2 ? hGap : vGap; // \uC218\uD3C9 \uBC30\uCE58\uBA74 hGap
|
|
4401
|
+
if (g > 0 && g < 200) cardGapValues.push(Math.round(g));
|
|
4402
|
+
}
|
|
4403
|
+
});
|
|
4404
|
+
const cardGap = cardGapValues.length > 0 ? mode(cardGapValues) : 16;
|
|
4405
|
+
|
|
4406
|
+
// \uC139\uC158 \uB0B4 \uD615\uC81C \uC694\uC18C \uAC04 \uC218\uC9C1 \uAC04\uACA9 (elementGap)
|
|
4407
|
+
const elementGapValues = [];
|
|
4408
|
+
topLevelEls.forEach(sectionEl => {
|
|
4409
|
+
if (!sectionEl) return;
|
|
4410
|
+
const children = Array.from(sectionEl.children);
|
|
4411
|
+
for (let i = 1; i < Math.min(children.length, 8); i++) {
|
|
4412
|
+
const prevRect = children[i - 1].getBoundingClientRect();
|
|
4413
|
+
const currRect = children[i].getBoundingClientRect();
|
|
4414
|
+
const gap = currRect.top - prevRect.bottom;
|
|
4415
|
+
if (gap > 0 && gap < 200) elementGapValues.push(Math.round(gap));
|
|
4416
|
+
}
|
|
4417
|
+
});
|
|
4418
|
+
const elementGap = elementGapValues.length > 0 ? median(elementGapValues) : 16;
|
|
4282
4419
|
|
|
4283
|
-
//
|
|
4284
|
-
const
|
|
4420
|
+
// \uBE48\uB3C4 \uBD84\uD3EC (4px \uB2E8\uC704\uB85C \uC815\uADDC\uD654)
|
|
4421
|
+
const spacingFrequency = {};
|
|
4422
|
+
[...sectionGaps, ...sectionPaddingTops, ...sectionPaddingBottoms, ...contentPaddingValues, ...cardGapValues, ...elementGapValues]
|
|
4423
|
+
.filter(v => v > 0)
|
|
4424
|
+
.forEach(v => { const k = roundTo4(v); spacingFrequency[k] = (spacingFrequency[k] || 0) + 1; });
|
|
4425
|
+
|
|
4426
|
+
// base unit: \uBE48\uB3C4 \uCD5C\uB2E4 \uAC12 \uAE30\uBC18, 4/8 \uCCB4\uACC4 \uC790\uB3D9 \uAC10\uC9C0
|
|
4427
|
+
const freqEntries = Object.entries(spacingFrequency).sort((a, b) => b[1] - a[1]);
|
|
4428
|
+
const topValues = freqEntries.slice(0, 6).map(e => Number(e[0]));
|
|
4285
4429
|
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
|
|
4286
|
-
const baseUnit =
|
|
4430
|
+
const baseUnit = topValues.length > 1 ? topValues.reduce((a, b) => gcd(a, b)) : (topValues[0] || 8);
|
|
4431
|
+
|
|
4432
|
+
// spacing details \uAC1D\uCCB4
|
|
4433
|
+
const spacingDetails = {
|
|
4434
|
+
sectionGaps,
|
|
4435
|
+
sectionPadding: {
|
|
4436
|
+
top: sectionPaddingTops.length > 0 ? median(sectionPaddingTops) : 0,
|
|
4437
|
+
bottom: sectionPaddingBottoms.length > 0 ? median(sectionPaddingBottoms) : 0,
|
|
4438
|
+
},
|
|
4439
|
+
contentPaddings: uniquePaddings.slice(0, 5),
|
|
4440
|
+
cardGaps: [...new Set(cardGapValues)].sort((a, b) => a - b).slice(0, 10),
|
|
4441
|
+
elementGap,
|
|
4442
|
+
spacingFrequency,
|
|
4443
|
+
};
|
|
4287
4444
|
|
|
4288
4445
|
// 2d. BorderRadius
|
|
4289
4446
|
const radiusValues = [];
|
|
@@ -4596,10 +4753,11 @@ var EXTRACTION_SCRIPT = `
|
|
|
4596
4753
|
scale: typographyScale,
|
|
4597
4754
|
},
|
|
4598
4755
|
spacing: {
|
|
4599
|
-
sectionGap: sectionGaps.length > 0 ?
|
|
4756
|
+
sectionGap: sectionGaps.length > 0 ? median(sectionGaps) : 80,
|
|
4600
4757
|
contentPadding,
|
|
4601
4758
|
cardGap: cardGap || 16,
|
|
4602
4759
|
baseUnit: Math.max(baseUnit, 4),
|
|
4760
|
+
details: spacingDetails,
|
|
4603
4761
|
},
|
|
4604
4762
|
borderRadius: {
|
|
4605
4763
|
small: uniqueRadii[0] || 0,
|
|
@@ -4720,7 +4878,7 @@ function captureScreenshot(url, outputPath, width, height, timeout) {
|
|
|
4720
4878
|
const result = spawnSync("node", ["-e", scriptContent], {
|
|
4721
4879
|
stdio: "pipe",
|
|
4722
4880
|
timeout: timeout + 3e4,
|
|
4723
|
-
env:
|
|
4881
|
+
env: getNodeEnvWithGlobalModules()
|
|
4724
4882
|
});
|
|
4725
4883
|
if (result.status !== 0) {
|
|
4726
4884
|
const stderr = result.stderr?.toString() ?? "";
|
|
@@ -4789,7 +4947,7 @@ async function extractPageData(url, timeout) {
|
|
|
4789
4947
|
const result = spawnSync("node", ["-e", scriptContent], {
|
|
4790
4948
|
stdio: "pipe",
|
|
4791
4949
|
timeout: timeout + 3e4,
|
|
4792
|
-
env:
|
|
4950
|
+
env: getNodeEnvWithGlobalModules()
|
|
4793
4951
|
});
|
|
4794
4952
|
if (result.status !== 0) {
|
|
4795
4953
|
const stderr = result.stderr?.toString() ?? "";
|
|
@@ -5033,7 +5191,17 @@ async function commandAnalyze(url, options) {
|
|
|
5033
5191
|
console.log(chalk15.dim(` Accent: ${colors.accent}`));
|
|
5034
5192
|
console.log(chalk15.dim(` \uD3F0\uD2B8: ${typography.fontFamilies.join(", ") || "(\uCD94\uCD9C \uC2E4\uD328)"}`));
|
|
5035
5193
|
console.log(chalk15.dim(` \uC139\uC158 \uAC04\uACA9: ${spacing.sectionGap}px`));
|
|
5194
|
+
console.log(chalk15.dim(` \uCF58\uD150\uCE20 \uD328\uB529: ${spacing.contentPadding}px`));
|
|
5195
|
+
console.log(chalk15.dim(` \uCE74\uB4DC \uAC04\uACA9: ${spacing.cardGap}px`));
|
|
5036
5196
|
console.log(chalk15.dim(` \uAE30\uBCF8 \uB2E8\uC704: ${spacing.baseUnit}px`));
|
|
5197
|
+
if (spacing.details) {
|
|
5198
|
+
const d = spacing.details;
|
|
5199
|
+
console.log(chalk15.dim(` \uC139\uC158 \uD328\uB529: top ${d.sectionPadding.top}px / bottom ${d.sectionPadding.bottom}px`));
|
|
5200
|
+
console.log(chalk15.dim(` \uC694\uC18C \uAC04\uACA9: ${d.elementGap}px`));
|
|
5201
|
+
if (d.cardGaps.length > 1) {
|
|
5202
|
+
console.log(chalk15.dim(` \uCE74\uB4DC \uAC04\uACA9 \uBD84\uD3EC: [${d.cardGaps.join(", ")}]px`));
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5037
5205
|
console.log("");
|
|
5038
5206
|
console.log(chalk15.bold("\uC778\uD130\uB799\uC158"));
|
|
5039
5207
|
const { interactions } = analysis;
|
|
@@ -5177,6 +5345,15 @@ import chalk16 from "chalk";
|
|
|
5177
5345
|
import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
|
|
5178
5346
|
import { resolve as resolve14, join as join3 } from "path";
|
|
5179
5347
|
import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
|
|
5348
|
+
function getNodeEnvWithGlobalModules2() {
|
|
5349
|
+
try {
|
|
5350
|
+
const globalRoot = execSync2("npm root -g", { encoding: "utf-8" }).trim();
|
|
5351
|
+
const existing = process.env.NODE_PATH || "";
|
|
5352
|
+
return { ...process.env, NODE_PATH: existing ? `${existing}:${globalRoot}` : globalRoot };
|
|
5353
|
+
} catch {
|
|
5354
|
+
return { ...process.env };
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5180
5357
|
async function commandCompare(options) {
|
|
5181
5358
|
console.log(chalk16.bold("\n\uC2DC\uAC01 \uBE44\uAD50 (Visual Diff)\n"));
|
|
5182
5359
|
if (!options.ref || !options.preview) {
|
|
@@ -5214,6 +5391,27 @@ async function runMultiViewportCompare(options) {
|
|
|
5214
5391
|
console.log("");
|
|
5215
5392
|
const hasMagick = checkCommand("magick");
|
|
5216
5393
|
const results = [];
|
|
5394
|
+
const baselineMap = /* @__PURE__ */ new Map();
|
|
5395
|
+
if (hasMagick) {
|
|
5396
|
+
const blSpinner = spinner("\uAE30\uC900\uC120 \uCE21\uC815 \uC911 (\uB808\uD37C\uB7F0\uC2A4 2\uD68C \uCEA1\uCC98)...");
|
|
5397
|
+
for (const vp of viewports) {
|
|
5398
|
+
const blRef1 = join3(outputDir, `baseline-1-${vp.name}.png`);
|
|
5399
|
+
const blRef2 = join3(outputDir, `baseline-2-${vp.name}.png`);
|
|
5400
|
+
const blDiff = join3(outputDir, `baseline-diff-${vp.name}.png`);
|
|
5401
|
+
try {
|
|
5402
|
+
captureScreenshot2(options.ref, blRef1, vp.width, vp.height);
|
|
5403
|
+
captureScreenshot2(options.ref, blRef2, vp.width, vp.height);
|
|
5404
|
+
const blPct = generateDiffWithMagick(blRef1, blRef2, blDiff);
|
|
5405
|
+
baselineMap.set(vp.name, blPct >= 0 ? blPct : 0);
|
|
5406
|
+
} catch {
|
|
5407
|
+
baselineMap.set(vp.name, 0);
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
const blValues = [...baselineMap.values()];
|
|
5411
|
+
const blAvg = blValues.reduce((a, b) => a + b, 0) / blValues.length;
|
|
5412
|
+
blSpinner.stop(chalk16.dim(` \uAE30\uC900\uC120 \uB178\uC774\uC988: \uD3C9\uADE0 ${blAvg.toFixed(1)}% (${blValues.map((v, i) => `${viewports[i].name} ${v.toFixed(1)}%`).join(", ")})`));
|
|
5413
|
+
console.log("");
|
|
5414
|
+
}
|
|
5217
5415
|
for (const vp of viewports) {
|
|
5218
5416
|
const vpSpinner = spinner(`${vp.name} (${vp.width}px) \uBE44\uAD50 \uC911...`);
|
|
5219
5417
|
const refPath = join3(outputDir, `ref-${vp.name}.png`);
|
|
@@ -5229,17 +5427,23 @@ async function runMultiViewportCompare(options) {
|
|
|
5229
5427
|
generateSideBySide(refPath, previewPath, diffPath);
|
|
5230
5428
|
diffPercentage = -1;
|
|
5231
5429
|
}
|
|
5430
|
+
const baselinePct = baselineMap.get(vp.name) ?? 0;
|
|
5431
|
+
const adjustedPct = diffPercentage >= 0 ? Math.round(Math.max(0, diffPercentage - baselinePct) * 10) / 10 : -1;
|
|
5232
5432
|
results.push({
|
|
5233
5433
|
name: vp.name,
|
|
5234
5434
|
width: vp.width,
|
|
5235
5435
|
referenceScreenshot: refPath,
|
|
5236
5436
|
previewScreenshot: previewPath,
|
|
5237
5437
|
diffScreenshot: diffPath,
|
|
5238
|
-
diffPercentage
|
|
5438
|
+
diffPercentage,
|
|
5439
|
+
baselineDiffPercentage: baselinePct,
|
|
5440
|
+
adjustedDiffPercentage: adjustedPct
|
|
5239
5441
|
});
|
|
5240
|
-
const
|
|
5241
|
-
const
|
|
5242
|
-
|
|
5442
|
+
const rawText = diffPercentage >= 0 ? `${diffPercentage.toFixed(1)}%` : "(\uACC4\uC0B0 \uBD88\uAC00)";
|
|
5443
|
+
const adjText = adjustedPct >= 0 ? `${adjustedPct.toFixed(1)}%` : "";
|
|
5444
|
+
const adjColor = adjustedPct <= 10 ? chalk16.green : adjustedPct <= 25 ? chalk16.yellow : chalk16.red;
|
|
5445
|
+
const label = baselinePct > 0 ? ` ${vp.name}: raw ${rawText} \u2192 ${chalk16.bold("adjusted")} ${adjColor(adjText)} ${chalk16.dim(`(baseline ${baselinePct.toFixed(1)}%)`)}` : ` ${vp.name}: diff ${adjColor(rawText)}`;
|
|
5446
|
+
vpSpinner.stop(label);
|
|
5243
5447
|
} catch (error) {
|
|
5244
5448
|
vpSpinner.stop(chalk16.red(` ${vp.name}: \uC2E4\uD328 \u2014 ${error instanceof Error ? error.message : String(error)}`));
|
|
5245
5449
|
results.push({
|
|
@@ -5254,27 +5458,38 @@ async function runMultiViewportCompare(options) {
|
|
|
5254
5458
|
}
|
|
5255
5459
|
const validResults = results.filter((r) => r.diffPercentage >= 0);
|
|
5256
5460
|
const overallDiff = validResults.length > 0 ? validResults.reduce((sum, r) => sum + r.diffPercentage, 0) / validResults.length : -1;
|
|
5461
|
+
const adjustedResults = results.filter((r) => (r.adjustedDiffPercentage ?? -1) >= 0);
|
|
5462
|
+
const overallAdjusted = adjustedResults.length > 0 ? adjustedResults.reduce((sum, r) => sum + r.adjustedDiffPercentage, 0) / adjustedResults.length : void 0;
|
|
5257
5463
|
const report = {
|
|
5258
5464
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5259
5465
|
referenceUrl: options.ref,
|
|
5260
5466
|
previewUrl: options.preview,
|
|
5261
5467
|
viewports: results,
|
|
5262
|
-
overallDiffPercentage: Math.round(overallDiff * 10) / 10
|
|
5468
|
+
overallDiffPercentage: Math.round(overallDiff * 10) / 10,
|
|
5469
|
+
overallAdjustedDiffPercentage: overallAdjusted != null ? Math.round(overallAdjusted * 10) / 10 : void 0
|
|
5263
5470
|
};
|
|
5264
5471
|
const reportPath = join3(outputDir, "comparison-report.json");
|
|
5265
5472
|
await writeFile9(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
5266
5473
|
console.log("");
|
|
5267
5474
|
console.log(chalk16.bold("\uBE44\uAD50 \uC694\uC57D"));
|
|
5475
|
+
const hasBaseline = results.some((r) => (r.baselineDiffPercentage ?? 0) > 0);
|
|
5268
5476
|
for (const r of results) {
|
|
5269
|
-
const
|
|
5270
|
-
const
|
|
5271
|
-
|
|
5477
|
+
const adjPct = r.adjustedDiffPercentage ?? r.diffPercentage;
|
|
5478
|
+
const displayPct = adjPct >= 0 ? `${adjPct.toFixed(1)}%` : "N/A";
|
|
5479
|
+
const icon = adjPct <= 10 ? chalk16.green("\u25CF") : adjPct <= 25 ? chalk16.yellow("\u25CF") : chalk16.red("\u25CF");
|
|
5480
|
+
const blNote = hasBaseline && (r.baselineDiffPercentage ?? 0) > 0 ? chalk16.dim(` (raw ${r.diffPercentage.toFixed(1)}%, baseline \u2212${r.baselineDiffPercentage.toFixed(1)}%)`) : "";
|
|
5481
|
+
console.log(` ${icon} ${r.name.padEnd(8)} ${displayPct.padStart(6)}${blNote}`);
|
|
5272
5482
|
}
|
|
5483
|
+
const judgeDiff = overallAdjusted ?? overallDiff;
|
|
5273
5484
|
console.log("");
|
|
5274
|
-
if (
|
|
5275
|
-
const overallColor =
|
|
5276
|
-
|
|
5277
|
-
|
|
5485
|
+
if (judgeDiff >= 0) {
|
|
5486
|
+
const overallColor = judgeDiff <= 10 ? chalk16.green : judgeDiff <= 25 ? chalk16.yellow : chalk16.red;
|
|
5487
|
+
const label = overallAdjusted != null ? "\uC804\uCCB4 \uD3C9\uADE0 diff (adjusted)" : "\uC804\uCCB4 \uD3C9\uADE0 diff";
|
|
5488
|
+
console.log(chalk16.bold(`${label}: ${overallColor(`${judgeDiff.toFixed(1)}%`)}`));
|
|
5489
|
+
if (overallAdjusted != null && overallAdjusted !== overallDiff) {
|
|
5490
|
+
console.log(chalk16.dim(` (raw \uD3C9\uADE0: ${overallDiff.toFixed(1)}%, \uAE30\uC900\uC120 \uCC28\uAC10: \u2212${(overallDiff - overallAdjusted).toFixed(1)}%)`));
|
|
5491
|
+
}
|
|
5492
|
+
if (judgeDiff <= 10) {
|
|
5278
5493
|
console.log(chalk16.green(" \u2192 \uBE44\uAD50 \uB8E8\uD504 \uC644\uB8CC! CEO \uCD5C\uC885 \uD655\uC778 \uC694\uCCAD \uAC00\uB2A5"));
|
|
5279
5494
|
} else {
|
|
5280
5495
|
console.log(chalk16.yellow(" \u2192 diff > 10%: Vision \uBE44\uAD50 \u2192 \uC2A4\uD0A4\uB9C8 \uC218\uC815 \uD544\uC694"));
|
|
@@ -5461,7 +5676,7 @@ function captureScreenshot2(url, outputPath, width, height) {
|
|
|
5461
5676
|
const result = spawnSync2("node", ["-e", scriptContent], {
|
|
5462
5677
|
stdio: "pipe",
|
|
5463
5678
|
timeout: 9e4,
|
|
5464
|
-
env:
|
|
5679
|
+
env: getNodeEnvWithGlobalModules2()
|
|
5465
5680
|
});
|
|
5466
5681
|
if (result.status !== 0) {
|
|
5467
5682
|
const stderr = result.stderr?.toString() ?? "";
|