crawlemon 0.3.0 → 0.3.2
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 +15 -3
- package/dist/index.js +200 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,18 @@ Crawlemon is the SEO safety layer between code and production. It works like an
|
|
|
6
6
|
|
|
7
7
|
No account, cloud connection, browser, AI provider, or API key is required.
|
|
8
8
|
|
|
9
|
+
## What is new in 0.3.2
|
|
10
|
+
|
|
11
|
+
- Accurate heading analysis across early `return` and ternary/`&&` branches, plus lazy `next/dynamic` imports — far fewer false "missing/duplicate `<h1>`" findings.
|
|
12
|
+
- Thin-content detection counts component text and stops stripping JSX text inside `return` bodies.
|
|
13
|
+
- Fixes metadata inheritance for multi-line site config (`export const x =` followed by the value), so pages no longer show phantom "missing meta description".
|
|
14
|
+
|
|
15
|
+
## What is new in 0.3.1
|
|
16
|
+
|
|
17
|
+
- Canonical URLs are no longer required on `robots: noindex` pages.
|
|
18
|
+
- `<h1>` duplicates in mutually exclusive early-return branches (auth gates, loading/success/error states) are no longer reported.
|
|
19
|
+
- Empty `alt` on dynamic-src decorative images (e.g. favicons) is no longer flagged.
|
|
20
|
+
|
|
9
21
|
## What is new in 0.3.0
|
|
10
22
|
|
|
11
23
|
- Production-grade static resolution for Next.js: site config, `metadataBase`, title objects (`default`/`template`/`absolute`), and layout inheritance.
|
|
@@ -217,11 +229,11 @@ jobs:
|
|
|
217
229
|
- name: Audit SEO
|
|
218
230
|
env:
|
|
219
231
|
CI: true
|
|
220
|
-
run: npx --yes crawlemon@0.3.
|
|
232
|
+
run: npx --yes crawlemon@0.3.2 audit
|
|
221
233
|
```
|
|
222
234
|
|
|
223
235
|
The exact package version is pinned so an upstream release cannot unexpectedly
|
|
224
|
-
change your CI result. Update `crawlemon@0.3.
|
|
236
|
+
change your CI result. Update `crawlemon@0.3.2` deliberately when you are ready
|
|
225
237
|
to adopt a newer version.
|
|
226
238
|
|
|
227
239
|
With `CI=true`, the workflow fails when Crawlemon finds at least one error-level
|
|
@@ -236,7 +248,7 @@ working directory:
|
|
|
236
248
|
working-directory: apps/web
|
|
237
249
|
env:
|
|
238
250
|
CI: true
|
|
239
|
-
run: npx --yes crawlemon@0.3.
|
|
251
|
+
run: npx --yes crawlemon@0.3.2 audit
|
|
240
252
|
```
|
|
241
253
|
|
|
242
254
|
## Zero-configuration detection
|
package/dist/index.js
CHANGED
|
@@ -183,7 +183,7 @@ function parsePageSource(content, framework = "nextjs") {
|
|
|
183
183
|
isInternal
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
-
const cleanBodyText = content.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\{\{[\s\S]*?\}\}/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/\s+/g, " ").trim();
|
|
186
|
+
const cleanBodyText = content.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\{\{[\s\S]*?\}\}/g, " ").replace(/\{[^{}]*\}/g, (block) => /\b(return|function)\b|=>/.test(block) ? block : " ").replace(/\s+/g, " ").trim();
|
|
187
187
|
const words = cleanBodyText.split(/\s+/).filter((word) => word.length > 2);
|
|
188
188
|
const hasLittleContent = words.length < 20;
|
|
189
189
|
return {
|
|
@@ -814,7 +814,13 @@ function absolutize(value, base) {
|
|
|
814
814
|
function resolvePageMetadata(content, file, inherited, ctx) {
|
|
815
815
|
const extract = extractNextMetadata(content, file, ctx.resolver);
|
|
816
816
|
if (!extract) {
|
|
817
|
-
|
|
817
|
+
const clean = { ...inherited };
|
|
818
|
+
delete clean.titleDeclared;
|
|
819
|
+
delete clean.descriptionDeclared;
|
|
820
|
+
delete clean.canonicalDeclared;
|
|
821
|
+
delete clean.dynamicMetadata;
|
|
822
|
+
delete clean.metadataSource;
|
|
823
|
+
return clean;
|
|
818
824
|
}
|
|
819
825
|
return buildEffectiveMetadata(extract, inherited, { ...ctx, isLayout: false });
|
|
820
826
|
}
|
|
@@ -1016,6 +1022,14 @@ function readStringEnd(text, index) {
|
|
|
1016
1022
|
}
|
|
1017
1023
|
return text.length;
|
|
1018
1024
|
}
|
|
1025
|
+
function statementContinues(code) {
|
|
1026
|
+
const trimmed = code.trimEnd();
|
|
1027
|
+
if (!trimmed) return false;
|
|
1028
|
+
if (trimmed.endsWith(";")) return false;
|
|
1029
|
+
if (/=>$/.test(trimmed)) return true;
|
|
1030
|
+
const last = trimmed[trimmed.length - 1];
|
|
1031
|
+
return /[=+\-*/%&|?:,.<>]/.test(last);
|
|
1032
|
+
}
|
|
1019
1033
|
function splitTopLevelStatements(code) {
|
|
1020
1034
|
const out = [];
|
|
1021
1035
|
let depth = 0;
|
|
@@ -1037,7 +1051,7 @@ function splitTopLevelStatements(code) {
|
|
|
1037
1051
|
continue;
|
|
1038
1052
|
}
|
|
1039
1053
|
if (ch === "\n") {
|
|
1040
|
-
if (depth === 0) flush(i);
|
|
1054
|
+
if (depth === 0 && !(statementStart !== -1 && statementContinues(code.slice(statementStart, i)))) flush(i);
|
|
1041
1055
|
i += 1;
|
|
1042
1056
|
continue;
|
|
1043
1057
|
}
|
|
@@ -1461,14 +1475,52 @@ function collectInlineHeadings(content) {
|
|
|
1461
1475
|
}
|
|
1462
1476
|
return headings.sort((a, b) => (a.line || 0) - (b.line || 0));
|
|
1463
1477
|
}
|
|
1464
|
-
function
|
|
1465
|
-
const
|
|
1478
|
+
function textWordCount(content) {
|
|
1479
|
+
const cleaned = content.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\{\{[\s\S]*?\}\}/g, " ").replace(/\{[^{}]*\}/g, (block) => /\b(return|function)\b|=>/.test(block) ? block : " ").replace(/\s+/g, " ").trim();
|
|
1480
|
+
return cleaned.split(/\s+/).filter((word) => word.length > 2).length;
|
|
1481
|
+
}
|
|
1482
|
+
var CONTENT_WORD_THRESHOLD = 20;
|
|
1483
|
+
function pageHasMeaningfulContent(filePath, resolver) {
|
|
1484
|
+
const visited = /* @__PURE__ */ new Set([filePath]);
|
|
1485
|
+
let total = 0;
|
|
1486
|
+
const enqueue = (file, queue2, depth) => {
|
|
1487
|
+
if (!visited.has(file) && depth <= 3) queue2.push({ file, depth });
|
|
1488
|
+
};
|
|
1489
|
+
const queue = [];
|
|
1490
|
+
let content = resolver.readModuleFile(filePath);
|
|
1491
|
+
if (content === void 0) return false;
|
|
1492
|
+
total += textWordCount(content);
|
|
1493
|
+
if (total >= CONTENT_WORD_THRESHOLD) return true;
|
|
1466
1494
|
for (const m of content.matchAll(/import[\s\S]*?from\s+["']([^"']+)["']/g)) {
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
if (resolved) files.push(resolved);
|
|
1495
|
+
const resolved = resolver.resolveModule(m[1], filePath);
|
|
1496
|
+
if (resolved) enqueue(resolved, queue, 1);
|
|
1470
1497
|
}
|
|
1471
|
-
|
|
1498
|
+
while (queue.length > 0) {
|
|
1499
|
+
const { file, depth } = queue.shift();
|
|
1500
|
+
if (visited.has(file)) continue;
|
|
1501
|
+
visited.add(file);
|
|
1502
|
+
const nextContent = resolver.readModuleFile(file);
|
|
1503
|
+
if (nextContent === void 0) continue;
|
|
1504
|
+
total += textWordCount(nextContent);
|
|
1505
|
+
if (total >= CONTENT_WORD_THRESHOLD) return true;
|
|
1506
|
+
if (depth < 3) {
|
|
1507
|
+
for (const m of nextContent.matchAll(/import[\s\S]*?from\s+["']([^"']+)["']/g)) {
|
|
1508
|
+
const resolved = resolver.resolveModule(m[1], file);
|
|
1509
|
+
if (resolved) enqueue(resolved, queue, depth + 1);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return total >= CONTENT_WORD_THRESHOLD;
|
|
1514
|
+
}
|
|
1515
|
+
function localComponentImports(content, fromFile, resolver) {
|
|
1516
|
+
const files = /* @__PURE__ */ new Set();
|
|
1517
|
+
const add = (spec) => {
|
|
1518
|
+
const resolved = resolver.resolveModule(spec, fromFile);
|
|
1519
|
+
if (resolved) files.add(resolved);
|
|
1520
|
+
};
|
|
1521
|
+
for (const m of content.matchAll(/import[\s\S]*?from\s+["']([^"']+)["']/g)) add(m[1]);
|
|
1522
|
+
for (const m of content.matchAll(/(?:\bimport|require)\s*\(\s*["']([^"']+)["']/g)) add(m[1]);
|
|
1523
|
+
return Array.from(files);
|
|
1472
1524
|
}
|
|
1473
1525
|
function fileHasFlag(content, flag) {
|
|
1474
1526
|
const re = new RegExp(`\\b${flag}\\s*=\\s*false\\b`);
|
|
@@ -1547,7 +1599,7 @@ function discoverImportedH1(filePath, resolver, maxDepth = 3) {
|
|
|
1547
1599
|
if (nextContent === void 0) continue;
|
|
1548
1600
|
const h1s = collectInlineHeadings(nextContent).filter((h) => h.level === 1);
|
|
1549
1601
|
if (h1s.length > 0) {
|
|
1550
|
-
result.push(
|
|
1602
|
+
result.push(h1s[0]);
|
|
1551
1603
|
continue;
|
|
1552
1604
|
}
|
|
1553
1605
|
for (const next of localComponentImports(nextContent, file, resolver)) {
|
|
@@ -1556,6 +1608,105 @@ function discoverImportedH1(filePath, resolver, maxDepth = 3) {
|
|
|
1556
1608
|
}
|
|
1557
1609
|
return result;
|
|
1558
1610
|
}
|
|
1611
|
+
function countH1(text) {
|
|
1612
|
+
const re = /<h1\b/g;
|
|
1613
|
+
let count = 0;
|
|
1614
|
+
while (re.exec(text)) count += 1;
|
|
1615
|
+
return count;
|
|
1616
|
+
}
|
|
1617
|
+
function interpolationRegions(text) {
|
|
1618
|
+
const regions = [];
|
|
1619
|
+
let i = 0;
|
|
1620
|
+
while (i < text.length) {
|
|
1621
|
+
const ch = text[i];
|
|
1622
|
+
if (ch === "'" || ch === '"') {
|
|
1623
|
+
const q = text[i];
|
|
1624
|
+
i += 1;
|
|
1625
|
+
while (i < text.length && text[i] !== q) i += 1;
|
|
1626
|
+
i += 1;
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (ch === "`") {
|
|
1630
|
+
let t = i + 1;
|
|
1631
|
+
while (t < text.length && text[t] !== "`") {
|
|
1632
|
+
if (text[t] === "\\") t += 1;
|
|
1633
|
+
t += 1;
|
|
1634
|
+
}
|
|
1635
|
+
i = t + 1;
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
if (ch === "{") {
|
|
1639
|
+
const end = readBalanced(text, i);
|
|
1640
|
+
regions.push({ start: i, end });
|
|
1641
|
+
i = end + 1;
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
i += 1;
|
|
1645
|
+
}
|
|
1646
|
+
return regions;
|
|
1647
|
+
}
|
|
1648
|
+
function findTopLevelTernary(text) {
|
|
1649
|
+
let depth = 0;
|
|
1650
|
+
let question = -1;
|
|
1651
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1652
|
+
const ch = text[i];
|
|
1653
|
+
if (ch === "'" || ch === '"') {
|
|
1654
|
+
const q = ch;
|
|
1655
|
+
i += 1;
|
|
1656
|
+
while (i < text.length && text[i] !== q) i += 1;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (ch === "`") {
|
|
1660
|
+
while (i < text.length && text[i] !== "`") i += 1;
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
if (ch === "(" || ch === "[" || ch === "{") depth += 1;
|
|
1664
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth = Math.max(0, depth - 1);
|
|
1665
|
+
else if (ch === "?" && depth === 0) {
|
|
1666
|
+
question = i;
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (question === -1) return null;
|
|
1671
|
+
let depth2 = 0;
|
|
1672
|
+
for (let i = question + 1; i < text.length; i += 1) {
|
|
1673
|
+
const ch = text[i];
|
|
1674
|
+
if (ch === "(" || ch === "[" || ch === "{") depth2 += 1;
|
|
1675
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth2 = Math.max(0, depth2 - 1);
|
|
1676
|
+
else if (ch === ":" && depth2 === 0) return { question, colon: i };
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
function computeBlockMaxH1(text) {
|
|
1681
|
+
const regions = interpolationRegions(text);
|
|
1682
|
+
let jsxLevelH1 = countH1(text);
|
|
1683
|
+
let add = 0;
|
|
1684
|
+
for (const region of regions) {
|
|
1685
|
+
const inner = text.slice(region.start + 1, region.end);
|
|
1686
|
+
jsxLevelH1 -= countH1(inner);
|
|
1687
|
+
const ternary = findTopLevelTernary(inner);
|
|
1688
|
+
if (ternary) {
|
|
1689
|
+
const truePart = inner.slice(0, ternary.colon);
|
|
1690
|
+
const falsePart = inner.slice(ternary.colon + 1);
|
|
1691
|
+
add += Math.max(countH1(truePart), countH1(falsePart));
|
|
1692
|
+
} else {
|
|
1693
|
+
add += countH1(inner);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return jsxLevelH1 + add;
|
|
1697
|
+
}
|
|
1698
|
+
function computeH1Concurrency(content, h1Lines) {
|
|
1699
|
+
const returnRe = /(?:^|[^A-Za-z0-9_$])return\s*(?:\(\s*)?</g;
|
|
1700
|
+
const starts = [];
|
|
1701
|
+
for (const match of content.matchAll(returnRe)) starts.push(match.index || 0);
|
|
1702
|
+
if (starts.length === 0) return null;
|
|
1703
|
+
let max = 0;
|
|
1704
|
+
for (let index = 0; index < starts.length; index += 1) {
|
|
1705
|
+
const blockText = content.slice(starts[index], index + 1 < starts.length ? starts[index + 1] : content.length);
|
|
1706
|
+
max = Math.max(max, computeBlockMaxH1(blockText));
|
|
1707
|
+
}
|
|
1708
|
+
return max;
|
|
1709
|
+
}
|
|
1559
1710
|
|
|
1560
1711
|
// ../next-adapter/src/scanner.ts
|
|
1561
1712
|
function normalizeRouteGroup(segment) {
|
|
@@ -1698,6 +1849,15 @@ var NextJsAdapter = class {
|
|
|
1698
1849
|
headings.push(...discoverImportedH1(fullPath, resolver));
|
|
1699
1850
|
}
|
|
1700
1851
|
const dynamic = resolver ? dynamicParamsForFile(fullPath, resolver) : void 0;
|
|
1852
|
+
const inlineH1 = parsed.headings.filter((h) => h.level === 1);
|
|
1853
|
+
let maxConcurrentH1;
|
|
1854
|
+
if (inlineH1.length > 0) {
|
|
1855
|
+
const concurrency = computeH1Concurrency(content, inlineH1.map((h) => h.line || 0));
|
|
1856
|
+
maxConcurrentH1 = concurrency === null ? inlineH1.length : concurrency;
|
|
1857
|
+
} else {
|
|
1858
|
+
maxConcurrentH1 = headings.some((h) => h.level === 1) ? 1 : 0;
|
|
1859
|
+
}
|
|
1860
|
+
const hasLittleContent = parsed.hasLittleContent && (!resolver || !pageHasMeaningfulContent(fullPath, resolver));
|
|
1701
1861
|
routes.push({
|
|
1702
1862
|
route: routePath,
|
|
1703
1863
|
filePath: fullPath,
|
|
@@ -1706,11 +1866,12 @@ var NextJsAdapter = class {
|
|
|
1706
1866
|
images: parsed.images,
|
|
1707
1867
|
links: parsed.links,
|
|
1708
1868
|
textContent: parsed.textContent,
|
|
1709
|
-
hasLittleContent
|
|
1869
|
+
hasLittleContent,
|
|
1710
1870
|
hasDynamicSegments: routePath.includes("["),
|
|
1711
1871
|
dynamicParams: dynamic?.dynamicParams,
|
|
1712
1872
|
generatedParams: dynamic?.generatedParams,
|
|
1713
|
-
isRedirect: isRedirectOnly(content)
|
|
1873
|
+
isRedirect: isRedirectOnly(content),
|
|
1874
|
+
maxConcurrentH1
|
|
1714
1875
|
});
|
|
1715
1876
|
}
|
|
1716
1877
|
}
|
|
@@ -1738,6 +1899,9 @@ var NextJsAdapter = class {
|
|
|
1738
1899
|
if (routePath === "//" || routePath === "") routePath = "/";
|
|
1739
1900
|
const content = fs3.readFileSync(fullPath, "utf8");
|
|
1740
1901
|
const parsed = parsePageSource(content);
|
|
1902
|
+
const inlineH1 = parsed.headings.filter((h) => h.level === 1);
|
|
1903
|
+
const concurrency = computeH1Concurrency(content, inlineH1.map((h) => h.line || 0));
|
|
1904
|
+
const maxConcurrentH1 = concurrency === null ? inlineH1.length : concurrency;
|
|
1741
1905
|
routes.push({
|
|
1742
1906
|
route: routePath,
|
|
1743
1907
|
filePath: fullPath,
|
|
@@ -1747,7 +1911,8 @@ var NextJsAdapter = class {
|
|
|
1747
1911
|
links: parsed.links,
|
|
1748
1912
|
textContent: parsed.textContent,
|
|
1749
1913
|
hasLittleContent: parsed.hasLittleContent,
|
|
1750
|
-
hasDynamicSegments: routePath.includes("[")
|
|
1914
|
+
hasDynamicSegments: routePath.includes("["),
|
|
1915
|
+
maxConcurrentH1
|
|
1751
1916
|
});
|
|
1752
1917
|
}
|
|
1753
1918
|
}
|
|
@@ -2220,6 +2385,7 @@ var canonicalRule = {
|
|
|
2220
2385
|
}
|
|
2221
2386
|
for (const route of context.routes) {
|
|
2222
2387
|
if (route.isRedirect) continue;
|
|
2388
|
+
if ((route.metadata.robots || "").toLowerCase().includes("noindex")) continue;
|
|
2223
2389
|
if (route.metadata.dynamicMetadata) continue;
|
|
2224
2390
|
const canonical = route.metadata.canonical;
|
|
2225
2391
|
if (!canonical || canonical.trim() === "") {
|
|
@@ -2364,19 +2530,22 @@ var headingsRule = {
|
|
|
2364
2530
|
fixable: false,
|
|
2365
2531
|
explanation: "Every page should have exactly one <h1> element defining its primary topic for search engines and accessibility."
|
|
2366
2532
|
});
|
|
2367
|
-
} else
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2533
|
+
} else {
|
|
2534
|
+
const effectiveCount = route.maxConcurrentH1 !== void 0 ? Math.max(route.maxConcurrentH1, h1s.length > 0 ? 1 : 0) : h1s.length;
|
|
2535
|
+
if (effectiveCount > 1) {
|
|
2536
|
+
findings.push({
|
|
2537
|
+
id: `heading-multiple-h1-${route.route}`,
|
|
2538
|
+
rule: "headings",
|
|
2539
|
+
severity: "warning",
|
|
2540
|
+
category: "content",
|
|
2541
|
+
message: `Found ${effectiveCount} <h1> tags on route "${route.route}". Best practice is a single prominent <h1>.`,
|
|
2542
|
+
file: route.filePath,
|
|
2543
|
+
line: h1s[1].line,
|
|
2544
|
+
route: route.route,
|
|
2545
|
+
fixable: false,
|
|
2546
|
+
explanation: "Multiple <h1> tags can dilute topical hierarchy and confuse screen readers."
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2380
2549
|
}
|
|
2381
2550
|
let prevLevel = 1;
|
|
2382
2551
|
for (const h of headings) {
|
|
@@ -2438,7 +2607,9 @@ var imagesRule = {
|
|
|
2438
2607
|
// Never invent alt descriptions without AI
|
|
2439
2608
|
explanation: "Missing alt attributes harm accessibility and prevent images from ranking in Google Image Search."
|
|
2440
2609
|
});
|
|
2441
|
-
} else if (img.alt.trim() === "" && !img.src.includes("icon") && !img.src.includes("decorative"))
|
|
2610
|
+
} else if (img.alt.trim() === "" && !img.src.includes("icon") && !img.src.includes("decorative") && // A runtime/dynamic src (e.g. favicon from data) cannot be judged;
|
|
2611
|
+
// empty alt for it is commonly the intended decorative usage.
|
|
2612
|
+
!img.src.includes("unknown-image")) {
|
|
2442
2613
|
findings.push({
|
|
2443
2614
|
id: `img-empty-alt-${route.route}-${img.src}`,
|
|
2444
2615
|
rule: "images",
|
|
@@ -3803,7 +3974,7 @@ function runSEOAudit(options) {
|
|
|
3803
3974
|
recommendations,
|
|
3804
3975
|
opportunities,
|
|
3805
3976
|
timestamp,
|
|
3806
|
-
engineVersion: "0.3.
|
|
3977
|
+
engineVersion: "0.3.2"
|
|
3807
3978
|
};
|
|
3808
3979
|
}
|
|
3809
3980
|
|
|
@@ -5721,7 +5892,7 @@ async function runCli(args) {
|
|
|
5721
5892
|
return;
|
|
5722
5893
|
}
|
|
5723
5894
|
if (command === "--version" || command === "-v") {
|
|
5724
|
-
console.log("0.3.
|
|
5895
|
+
console.log("0.3.2");
|
|
5725
5896
|
return;
|
|
5726
5897
|
}
|
|
5727
5898
|
checkSecretRisk(projectRoot);
|