@decantr/verifier 3.5.4 → 3.6.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 +1 -1
- package/dist/index.js +96 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npm install @decantr/verifier
|
|
|
37
37
|
- Next.js static/document outputs are treated as framework-rendered documents instead of requiring a Vite-style `id="root"` mount element
|
|
38
38
|
- project source audits ignore test, spec, story, fixture, and mock files for production drift warnings such as localhost endpoints and unsafe rendering patterns
|
|
39
39
|
- project audits emit `COMP001` / `import-existing-component` findings when a production source file locally redeclares a primitive such as `Button`, `Card`, or `Dialog` while an exported reusable primitive already exists under common component paths
|
|
40
|
-
- project audits emit `COMP010` / `replace-raw-control-with-local-component` findings when production JSX renders raw controls such as `<button>` or `<input>` while a project-owned primitive already exists
|
|
40
|
+
- project audits emit `COMP010` / `replace-raw-control-with-local-component` findings when production JSX renders generic raw controls such as `<button>` or text-like `<input>` while a project-owned primitive already exists; specialized inputs such as file, hidden, checkbox, radio, color, range, and Dropzone `getInputProps()` controls are not treated as generic `Input` drift
|
|
41
41
|
- project audits emit behavior-obligation findings when accepted `.decantr/local-patterns.json` patterns declare `behavior_obligations` and production source strongly violates statically checkable dialog/form obligations:
|
|
42
42
|
- `A11Y010` / `restore-dialog-accessible-name`
|
|
43
43
|
- `A11Y011` / `restore-label-association`
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,18 @@ var RAW_CONTROL_COMPONENT_BY_ELEMENT = {
|
|
|
69
69
|
table: "Table",
|
|
70
70
|
textarea: "Textarea"
|
|
71
71
|
};
|
|
72
|
+
var NON_GENERIC_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
73
|
+
"button",
|
|
74
|
+
"checkbox",
|
|
75
|
+
"color",
|
|
76
|
+
"file",
|
|
77
|
+
"hidden",
|
|
78
|
+
"image",
|
|
79
|
+
"radio",
|
|
80
|
+
"range",
|
|
81
|
+
"reset",
|
|
82
|
+
"submit"
|
|
83
|
+
]);
|
|
72
84
|
function normalizePath(path) {
|
|
73
85
|
return path.replace(/\\/g, "/");
|
|
74
86
|
}
|
|
@@ -120,10 +132,14 @@ function jsxAttributeStringValue(node, attributeName) {
|
|
|
120
132
|
}
|
|
121
133
|
return null;
|
|
122
134
|
}
|
|
123
|
-
function
|
|
135
|
+
function isSpecializedInput(node) {
|
|
124
136
|
const tagName = jsxIdentifierName(node.tagName);
|
|
125
137
|
if (tagName !== "input") return false;
|
|
126
|
-
|
|
138
|
+
const type = jsxAttributeStringValue(node, "type")?.toLowerCase();
|
|
139
|
+
if (type && NON_GENERIC_INPUT_TYPES.has(type)) return true;
|
|
140
|
+
return node.attributes.properties.some(
|
|
141
|
+
(property) => ts.isJsxSpreadAttribute(property) && /\bgetInputProps\s*\(/.test(property.expression.getText())
|
|
142
|
+
);
|
|
127
143
|
}
|
|
128
144
|
function isReusableComponentPath(file, name) {
|
|
129
145
|
const normalized = normalizePath(file);
|
|
@@ -222,7 +238,7 @@ function collectRawControls(sourceFile, file) {
|
|
|
222
238
|
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
223
239
|
const tagName = jsxIdentifierName(node.tagName);
|
|
224
240
|
const component = tagName && Object.hasOwn(RAW_CONTROL_COMPONENT_BY_ELEMENT, tagName) ? RAW_CONTROL_COMPONENT_BY_ELEMENT[tagName] : null;
|
|
225
|
-
if (component && !
|
|
241
|
+
if (component && !isSpecializedInput(node)) {
|
|
226
242
|
rawControls.push({
|
|
227
243
|
element: tagName,
|
|
228
244
|
component,
|
|
@@ -2365,6 +2381,15 @@ var ROUTE_VARIABLE_NAMES = "(?:path|pathname|route|currentPath|currentRoute|loca
|
|
|
2365
2381
|
var ROUTE_ASSET_EXTENSION_RE = /\.(?:avif|bmp|css|gif|ico|jpeg|jpg|js|json|map|mp4|pdf|png|svg|webp|woff2?)$/i;
|
|
2366
2382
|
var JSX_ROUTE_PATH_RE = /<(?:Route|[A-Z][\w.]*Route)\b[^>]*\bpath\s*=\s*(?:"([^"]+)"|'([^']+)'|{\s*"([^"]+)"\s*}|{\s*'([^']+)'\s*}|{\s*`([^`]+)`\s*})/g;
|
|
2367
2383
|
var OBJECT_ROUTE_PATH_RE = /\bpath\s*:\s*(?:"([^"]+)"|'([^']+)'|`([^`]+)`)/g;
|
|
2384
|
+
var STATIC_HTML_ENTRY_FILES = [
|
|
2385
|
+
"index.html",
|
|
2386
|
+
"docs/index.html",
|
|
2387
|
+
"src/index.html",
|
|
2388
|
+
"public/index.html",
|
|
2389
|
+
"dist/index.html"
|
|
2390
|
+
];
|
|
2391
|
+
var STATIC_HTML_ROUTE_DIRS = ["demos", "examples"];
|
|
2392
|
+
var MAX_STATIC_HTML_CANDIDATES = 1e3;
|
|
2368
2393
|
var MAX_FILE_READ_BYTES = 512 * 1024;
|
|
2369
2394
|
var MAX_WALK_FILES = 5e3;
|
|
2370
2395
|
var MAX_REPORT_ROUTES = 80;
|
|
@@ -2462,6 +2487,10 @@ function dependencyVersion(dependencies, names) {
|
|
|
2462
2487
|
function hasAnyFile(projectRoot, paths) {
|
|
2463
2488
|
return paths.some((path) => existsSync3(join3(projectRoot, path)));
|
|
2464
2489
|
}
|
|
2490
|
+
function hasStaticHtmlSurface(projectRoot) {
|
|
2491
|
+
if (hasAnyFile(projectRoot, STATIC_HTML_ENTRY_FILES)) return true;
|
|
2492
|
+
return STATIC_HTML_ROUTE_DIRS.some((dir) => existsSync3(join3(projectRoot, dir, "index.html")));
|
|
2493
|
+
}
|
|
2465
2494
|
function detectPackageManager(projectRoot, pkg) {
|
|
2466
2495
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
2467
2496
|
if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
|
|
@@ -2474,12 +2503,12 @@ function detectPackageManager(projectRoot, pkg) {
|
|
|
2474
2503
|
return "unknown";
|
|
2475
2504
|
}
|
|
2476
2505
|
function detectPrimaryLanguage(projectRoot, packageJsonPresent) {
|
|
2477
|
-
if (packageJsonPresent) return "javascript";
|
|
2506
|
+
if (packageJsonPresent) return hasStaticHtmlSurface(projectRoot) ? "html" : "javascript";
|
|
2478
2507
|
if (hasAnyFile(projectRoot, ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"]))
|
|
2479
2508
|
return "python";
|
|
2480
2509
|
if (hasAnyFile(projectRoot, ["go.mod"])) return "go";
|
|
2481
2510
|
if (hasAnyFile(projectRoot, ["Cargo.toml"])) return "rust";
|
|
2482
|
-
if (
|
|
2511
|
+
if (hasStaticHtmlSurface(projectRoot)) return "html";
|
|
2483
2512
|
return "unknown";
|
|
2484
2513
|
}
|
|
2485
2514
|
function detectProject(projectRoot) {
|
|
@@ -2512,7 +2541,7 @@ function detectProject(projectRoot) {
|
|
|
2512
2541
|
} else if (dependencies.react) {
|
|
2513
2542
|
framework = "react";
|
|
2514
2543
|
frameworkVersion = dependencyVersion(dependencies, ["react"]);
|
|
2515
|
-
} else if (
|
|
2544
|
+
} else if (hasStaticHtmlSurface(projectRoot)) {
|
|
2516
2545
|
framework = "html";
|
|
2517
2546
|
}
|
|
2518
2547
|
return {
|
|
@@ -2701,6 +2730,65 @@ function collectRouteLiterals(pattern, content, routes) {
|
|
|
2701
2730
|
}
|
|
2702
2731
|
return count;
|
|
2703
2732
|
}
|
|
2733
|
+
function htmlRouteFromFile(file) {
|
|
2734
|
+
let withoutExt = file.slice(0, -extname3(file).length).replace(/\\/g, "/");
|
|
2735
|
+
if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
|
|
2736
|
+
if (withoutExt === "index" || withoutExt === "docs" || withoutExt === "src" || withoutExt === "public" || withoutExt === "dist") {
|
|
2737
|
+
return "/";
|
|
2738
|
+
}
|
|
2739
|
+
return `/${withoutExt.split("/").filter(Boolean).join("/")}` || "/";
|
|
2740
|
+
}
|
|
2741
|
+
function collectStaticHtmlFiles(dir, projectRoot, files, depth = 0) {
|
|
2742
|
+
if (depth > 5 || files.length >= MAX_STATIC_HTML_CANDIDATES) return;
|
|
2743
|
+
let entries;
|
|
2744
|
+
try {
|
|
2745
|
+
entries = readdirSync3(dir).sort();
|
|
2746
|
+
} catch {
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
for (const entry of entries) {
|
|
2750
|
+
if (files.length >= MAX_STATIC_HTML_CANDIDATES) return;
|
|
2751
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
2752
|
+
const fullPath = join3(dir, entry);
|
|
2753
|
+
try {
|
|
2754
|
+
const stat = statSync2(fullPath);
|
|
2755
|
+
if (stat.isDirectory()) {
|
|
2756
|
+
collectStaticHtmlFiles(fullPath, projectRoot, files, depth + 1);
|
|
2757
|
+
} else if (stat.isFile() && /\.(?:html|htm)$/i.test(entry)) {
|
|
2758
|
+
files.push(relative3(projectRoot, fullPath).replace(/\\/g, "/"));
|
|
2759
|
+
}
|
|
2760
|
+
} catch {
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
function staticHtmlFilePriority(file) {
|
|
2765
|
+
const base = file.split("/").pop()?.toLowerCase();
|
|
2766
|
+
if (base === "index.html" || base === "index.htm") return 0;
|
|
2767
|
+
if (base === "default.html" || base === "default.htm") return 1;
|
|
2768
|
+
return 2;
|
|
2769
|
+
}
|
|
2770
|
+
function scanStaticHtmlRoutes(projectRoot) {
|
|
2771
|
+
const routes = /* @__PURE__ */ new Map();
|
|
2772
|
+
for (const file of STATIC_HTML_ENTRY_FILES) {
|
|
2773
|
+
if (!existsSync3(join3(projectRoot, file))) continue;
|
|
2774
|
+
routes.set("/", { path: "/", file, hasLayout: false });
|
|
2775
|
+
break;
|
|
2776
|
+
}
|
|
2777
|
+
for (const dir of STATIC_HTML_ROUTE_DIRS) {
|
|
2778
|
+
const fullDir = join3(projectRoot, dir);
|
|
2779
|
+
if (!existsSync3(fullDir)) continue;
|
|
2780
|
+
const files = [];
|
|
2781
|
+
collectStaticHtmlFiles(fullDir, projectRoot, files);
|
|
2782
|
+
for (const file of files.sort(
|
|
2783
|
+
(a, b) => staticHtmlFilePriority(a) - staticHtmlFilePriority(b) || a.localeCompare(b)
|
|
2784
|
+
)) {
|
|
2785
|
+
const routePath = htmlRouteFromFile(file);
|
|
2786
|
+
if (routes.has(routePath)) continue;
|
|
2787
|
+
routes.set(routePath, { path: routePath, file, hasLayout: false });
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
return [...routes.values()].slice(0, MAX_REPORT_ROUTES);
|
|
2791
|
+
}
|
|
2704
2792
|
function detectPathnameBranchRoutes(content) {
|
|
2705
2793
|
const routes = /* @__PURE__ */ new Set();
|
|
2706
2794
|
const comparison = new RegExp(
|
|
@@ -2760,18 +2848,8 @@ function scanRoutes(projectRoot, detection) {
|
|
|
2760
2848
|
if (reactRouter.routes.length > 0)
|
|
2761
2849
|
return { strategy: "react-router", routes: reactRouter.routes };
|
|
2762
2850
|
if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
strategy: "static-html",
|
|
2766
|
-
routes: [{ path: "/", file: "index.html", hasLayout: false }]
|
|
2767
|
-
};
|
|
2768
|
-
}
|
|
2769
|
-
if (existsSync3(join3(projectRoot, "docs", "index.html"))) {
|
|
2770
|
-
return {
|
|
2771
|
-
strategy: "static-html",
|
|
2772
|
-
routes: [{ path: "/", file: "docs/index.html", hasLayout: false }]
|
|
2773
|
-
};
|
|
2774
|
-
}
|
|
2851
|
+
const staticHtmlRoutes = scanStaticHtmlRoutes(projectRoot);
|
|
2852
|
+
if (staticHtmlRoutes.length > 0) return { strategy: "static-html", routes: staticHtmlRoutes };
|
|
2775
2853
|
return { strategy: "none", routes: [] };
|
|
2776
2854
|
}
|
|
2777
2855
|
function countComponents(projectRoot, routes) {
|