next-a11y 0.1.4 → 0.1.6
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 +3 -0
- package/dist/cli/index.js +225 -67
- package/dist/cli/index.mjs +220 -62
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -23,10 +23,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/cli/index.ts
|
|
26
|
-
var
|
|
26
|
+
var fs9 = __toESM(require("fs"));
|
|
27
|
+
var path9 = __toESM(require("path"));
|
|
28
|
+
var import_dotenv2 = require("dotenv");
|
|
27
29
|
var import_commander = require("commander");
|
|
28
30
|
|
|
29
31
|
// src/cli/scan-command.ts
|
|
32
|
+
var fs7 = __toESM(require("fs"));
|
|
33
|
+
var path7 = __toESM(require("path"));
|
|
34
|
+
var import_dotenv = require("dotenv");
|
|
30
35
|
var import_picocolors4 = __toESM(require("picocolors"));
|
|
31
36
|
|
|
32
37
|
// src/config/resolve.ts
|
|
@@ -100,7 +105,7 @@ async function loadConfigFile(cwd) {
|
|
|
100
105
|
}
|
|
101
106
|
function resolveConfig(fileConfig, cliFlags = {}) {
|
|
102
107
|
const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
|
|
103
|
-
const provider = cliFlags.provider ?? merged.provider;
|
|
108
|
+
const provider = cliFlags.provider ?? merged.provider ?? detectProviderFromEnv();
|
|
104
109
|
const model = cliFlags.model ?? merged.model ?? (provider ? PROVIDER_DEFAULTS[provider] : "gpt-4.1-nano");
|
|
105
110
|
return {
|
|
106
111
|
provider,
|
|
@@ -118,6 +123,12 @@ function resolveConfig(fileConfig, cliFlags = {}) {
|
|
|
118
123
|
minScore: cliFlags.minScore
|
|
119
124
|
};
|
|
120
125
|
}
|
|
126
|
+
function detectProviderFromEnv() {
|
|
127
|
+
for (const [name, envVar] of Object.entries(PROVIDER_ENV)) {
|
|
128
|
+
if (envVar && process.env[envVar]) return name;
|
|
129
|
+
}
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
121
132
|
function deepMerge(target, source) {
|
|
122
133
|
const result = { ...target };
|
|
123
134
|
for (const key of Object.keys(source)) {
|
|
@@ -146,13 +157,13 @@ async function discoverFiles(basePath, include, exclude) {
|
|
|
146
157
|
if (typeof fs2.glob === "function") {
|
|
147
158
|
for (const pattern of include) {
|
|
148
159
|
try {
|
|
149
|
-
const matches = await new Promise((
|
|
160
|
+
const matches = await new Promise((resolve6, reject) => {
|
|
150
161
|
fs2.glob(
|
|
151
162
|
pattern,
|
|
152
163
|
{ cwd: absBase },
|
|
153
164
|
(err, files) => {
|
|
154
165
|
if (err) reject(err);
|
|
155
|
-
else
|
|
166
|
+
else resolve6(files);
|
|
156
167
|
}
|
|
157
168
|
);
|
|
158
169
|
});
|
|
@@ -358,6 +369,8 @@ var buttonLabelRule = {
|
|
|
358
369
|
if (parent) {
|
|
359
370
|
const hasTextContent = parent.getDescendantsOfKind(import_ts_morph2.SyntaxKind.JsxText).some((t) => t.getText().trim().length > 0);
|
|
360
371
|
if (hasTextContent) continue;
|
|
372
|
+
const hasExpressionContent = parent.getDescendantsOfKind(import_ts_morph2.SyntaxKind.JsxExpression).some((expr) => expr.getExpression() != null);
|
|
373
|
+
if (hasExpressionContent) continue;
|
|
361
374
|
const nestedElements = parent.getDescendantsOfKind(
|
|
362
375
|
import_ts_morph2.SyntaxKind.JsxOpeningElement
|
|
363
376
|
);
|
|
@@ -512,6 +525,8 @@ var linkLabelRule = {
|
|
|
512
525
|
if (parent) {
|
|
513
526
|
const hasTextContent = parent.getDescendantsOfKind(import_ts_morph3.SyntaxKind.JsxText).some((t) => t.getText().trim().length > 0);
|
|
514
527
|
if (hasTextContent) continue;
|
|
528
|
+
const hasExpressionContent = parent.getDescendantsOfKind(import_ts_morph3.SyntaxKind.JsxExpression).some((expr) => expr.getExpression() != null);
|
|
529
|
+
if (hasExpressionContent) continue;
|
|
515
530
|
const images = [
|
|
516
531
|
...parent.getDescendantsOfKind(import_ts_morph3.SyntaxKind.JsxSelfClosingElement),
|
|
517
532
|
...parent.getDescendantsOfKind(import_ts_morph3.SyntaxKind.JsxOpeningElement)
|
|
@@ -1692,13 +1707,13 @@ var PROVIDER_INSTALL = {
|
|
|
1692
1707
|
ollama: "npm install ollama-ai-provider"
|
|
1693
1708
|
};
|
|
1694
1709
|
function createProvider(provider, model) {
|
|
1695
|
-
const
|
|
1710
|
+
const pkg2 = PROVIDER_PACKAGES[provider];
|
|
1696
1711
|
let mod;
|
|
1697
1712
|
try {
|
|
1698
|
-
mod = require(
|
|
1713
|
+
mod = require(pkg2);
|
|
1699
1714
|
} catch {
|
|
1700
1715
|
throw new Error(
|
|
1701
|
-
`AI provider "${provider}" requires the "${
|
|
1716
|
+
`AI provider "${provider}" requires the "${pkg2}" package.
|
|
1702
1717
|
Install it with: ${PROVIDER_INSTALL[provider]}
|
|
1703
1718
|
|
|
1704
1719
|
Or run: npx next-a11y init`
|
|
@@ -1924,7 +1939,18 @@ var fs5 = __toESM(require("fs"));
|
|
|
1924
1939
|
var path5 = __toESM(require("path"));
|
|
1925
1940
|
var https = __toESM(require("https"));
|
|
1926
1941
|
var http = __toESM(require("http"));
|
|
1942
|
+
var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"];
|
|
1943
|
+
function isImagePath(p) {
|
|
1944
|
+
return IMAGE_EXTENSIONS.some((ext) => p.endsWith(ext));
|
|
1945
|
+
}
|
|
1927
1946
|
async function resolveImageSource(src, file, projectRoot) {
|
|
1947
|
+
if (path5.isAbsolute(src) && isImagePath(src)) {
|
|
1948
|
+
try {
|
|
1949
|
+
const buffer = fs5.readFileSync(src);
|
|
1950
|
+
return { type: "file", buffer, path: src };
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1928
1954
|
if (src.startsWith("/")) {
|
|
1929
1955
|
const publicPath = path5.join(projectRoot, "public", src);
|
|
1930
1956
|
try {
|
|
@@ -1957,26 +1983,135 @@ async function resolveImageSource(src, file, projectRoot) {
|
|
|
1957
1983
|
}
|
|
1958
1984
|
return { type: "unresolvable", reason: "Dynamic image source" };
|
|
1959
1985
|
}
|
|
1960
|
-
function resolveStaticImportPath(importName, file) {
|
|
1986
|
+
function resolveStaticImportPath(importName, file, projectRoot) {
|
|
1987
|
+
let name = importName;
|
|
1988
|
+
if (name.endsWith(".src")) {
|
|
1989
|
+
name = name.slice(0, -4);
|
|
1990
|
+
}
|
|
1991
|
+
if (name.includes("[") || name.includes("(")) {
|
|
1992
|
+
return void 0;
|
|
1993
|
+
}
|
|
1961
1994
|
const imports = file.getImportDeclarations();
|
|
1995
|
+
const filePath = file.getFilePath();
|
|
1996
|
+
const root = projectRoot ?? findProjectRootFromFile(filePath);
|
|
1997
|
+
const project = file.getProject();
|
|
1962
1998
|
for (const imp of imports) {
|
|
1999
|
+
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
1963
2000
|
const defaultImport = imp.getDefaultImport();
|
|
1964
|
-
if (defaultImport?.getText() ===
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2001
|
+
if (defaultImport?.getText() === name) {
|
|
2002
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, void 0, project);
|
|
2003
|
+
}
|
|
2004
|
+
const namedImports = imp.getNamedImports();
|
|
2005
|
+
for (const named of namedImports) {
|
|
2006
|
+
if (named.getName() === name || named.getAliasNode()?.getText() === name) {
|
|
2007
|
+
const originalName = named.getName();
|
|
2008
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, originalName, project);
|
|
1969
2009
|
}
|
|
1970
2010
|
}
|
|
1971
2011
|
}
|
|
1972
2012
|
return void 0;
|
|
1973
2013
|
}
|
|
2014
|
+
function resolveModuleToImage(moduleSpecifier, fromFile, projectRoot, namedExport, project) {
|
|
2015
|
+
if (isImagePath(moduleSpecifier)) {
|
|
2016
|
+
return resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
2017
|
+
}
|
|
2018
|
+
if (namedExport) {
|
|
2019
|
+
const barrelPath = resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
2020
|
+
if (barrelPath) {
|
|
2021
|
+
return followReExport(barrelPath, namedExport);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return void 0;
|
|
2025
|
+
}
|
|
2026
|
+
function resolveModulePath(moduleSpecifier, fromFile, projectRoot, project) {
|
|
2027
|
+
let resolved;
|
|
2028
|
+
if (moduleSpecifier.startsWith(".")) {
|
|
2029
|
+
resolved = path5.resolve(path5.dirname(fromFile), moduleSpecifier);
|
|
2030
|
+
} else {
|
|
2031
|
+
const aliasResolved = resolvePathAlias(moduleSpecifier, projectRoot, project);
|
|
2032
|
+
if (aliasResolved) {
|
|
2033
|
+
resolved = aliasResolved;
|
|
2034
|
+
} else {
|
|
2035
|
+
return void 0;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
if (fs5.existsSync(resolved) && fs5.statSync(resolved).isFile()) {
|
|
2039
|
+
return resolved;
|
|
2040
|
+
}
|
|
2041
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ...IMAGE_EXTENSIONS];
|
|
2042
|
+
for (const ext of extensions) {
|
|
2043
|
+
const withExt = resolved + ext;
|
|
2044
|
+
if (fs5.existsSync(withExt)) return withExt;
|
|
2045
|
+
}
|
|
2046
|
+
const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
|
2047
|
+
for (const idx of indexFiles) {
|
|
2048
|
+
const indexPath = path5.join(resolved, idx);
|
|
2049
|
+
if (fs5.existsSync(indexPath)) return indexPath;
|
|
2050
|
+
}
|
|
2051
|
+
return void 0;
|
|
2052
|
+
}
|
|
2053
|
+
function followReExport(barrelPath, exportName) {
|
|
2054
|
+
try {
|
|
2055
|
+
const content = fs5.readFileSync(barrelPath, "utf-8");
|
|
2056
|
+
const reExportPattern = new RegExp(
|
|
2057
|
+
`export\\s*\\{[^}]*\\b(?:default\\s+as\\s+)?${escapeRegex(exportName)}\\b[^}]*\\}\\s*from\\s*["']([^"']+)["']`
|
|
2058
|
+
);
|
|
2059
|
+
const match = content.match(reExportPattern);
|
|
2060
|
+
if (match) {
|
|
2061
|
+
const reExportPath = match[1];
|
|
2062
|
+
if (isImagePath(reExportPath)) {
|
|
2063
|
+
return path5.resolve(path5.dirname(barrelPath), reExportPath);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
return void 0;
|
|
2069
|
+
}
|
|
2070
|
+
function resolvePathAlias(moduleSpecifier, projectRoot, project) {
|
|
2071
|
+
if (!project) return void 0;
|
|
2072
|
+
const opts = project.getCompilerOptions();
|
|
2073
|
+
const paths = opts.paths;
|
|
2074
|
+
if (!paths) return void 0;
|
|
2075
|
+
const baseDir = opts.baseUrl ?? projectRoot;
|
|
2076
|
+
for (const [pattern, mappings] of Object.entries(paths)) {
|
|
2077
|
+
if (pattern.endsWith("/*")) {
|
|
2078
|
+
const prefix = pattern.slice(0, -1);
|
|
2079
|
+
if (moduleSpecifier.startsWith(prefix)) {
|
|
2080
|
+
const rest = moduleSpecifier.slice(prefix.length);
|
|
2081
|
+
for (const mapping of mappings) {
|
|
2082
|
+
const mappingBase = mapping.endsWith("/*") ? mapping.slice(0, -1) : mapping;
|
|
2083
|
+
const resolved = path5.resolve(baseDir, mappingBase + rest);
|
|
2084
|
+
return resolved;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
} else if (pattern === moduleSpecifier) {
|
|
2088
|
+
if (mappings.length > 0) {
|
|
2089
|
+
return path5.resolve(baseDir, mappings[0]);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return void 0;
|
|
2094
|
+
}
|
|
2095
|
+
function escapeRegex(str) {
|
|
2096
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2097
|
+
}
|
|
2098
|
+
function findProjectRootFromFile(filePath) {
|
|
2099
|
+
let dir = path5.dirname(filePath);
|
|
2100
|
+
while (dir !== path5.dirname(dir)) {
|
|
2101
|
+
if (fs5.existsSync(path5.join(dir, "package.json"))) return dir;
|
|
2102
|
+
if (fs5.existsSync(path5.join(dir, "next.config.js"))) return dir;
|
|
2103
|
+
if (fs5.existsSync(path5.join(dir, "next.config.mjs"))) return dir;
|
|
2104
|
+
if (fs5.existsSync(path5.join(dir, "next.config.ts"))) return dir;
|
|
2105
|
+
dir = path5.dirname(dir);
|
|
2106
|
+
}
|
|
2107
|
+
return path5.dirname(filePath);
|
|
2108
|
+
}
|
|
1974
2109
|
function fetchImage(url) {
|
|
1975
|
-
return new Promise((
|
|
2110
|
+
return new Promise((resolve6, reject) => {
|
|
1976
2111
|
const client = url.startsWith("https") ? https : http;
|
|
1977
2112
|
const req = client.get(url, { timeout: 1e4 }, (res) => {
|
|
1978
2113
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
1979
|
-
fetchImage(res.headers.location).then(
|
|
2114
|
+
fetchImage(res.headers.location).then(resolve6).catch(reject);
|
|
1980
2115
|
return;
|
|
1981
2116
|
}
|
|
1982
2117
|
const chunks = [];
|
|
@@ -1991,7 +2126,7 @@ function fetchImage(url) {
|
|
|
1991
2126
|
}
|
|
1992
2127
|
chunks.push(chunk);
|
|
1993
2128
|
});
|
|
1994
|
-
res.on("end", () =>
|
|
2129
|
+
res.on("end", () => resolve6(Buffer.concat(chunks)));
|
|
1995
2130
|
res.on("error", reject);
|
|
1996
2131
|
});
|
|
1997
2132
|
req.on("error", reject);
|
|
@@ -2064,6 +2199,8 @@ async function resolveAiFixes(opts) {
|
|
|
2064
2199
|
async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
2065
2200
|
const el = findElement(file, violation.line);
|
|
2066
2201
|
if (!el) return "";
|
|
2202
|
+
const filePath = file.getFilePath();
|
|
2203
|
+
const projectRoot = findProjectRoot(filePath);
|
|
2067
2204
|
const srcAttr = el.getAttribute("src");
|
|
2068
2205
|
let srcValue = "";
|
|
2069
2206
|
if (srcAttr?.getKind() === import_ts_morph18.SyntaxKind.JsxAttribute) {
|
|
@@ -2074,13 +2211,11 @@ async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
|
2074
2211
|
const expr = init.asKind(import_ts_morph18.SyntaxKind.JsxExpression)?.getExpression();
|
|
2075
2212
|
if (expr) {
|
|
2076
2213
|
const importName = expr.getText();
|
|
2077
|
-
const importPath = resolveStaticImportPath(importName, file);
|
|
2214
|
+
const importPath = resolveStaticImportPath(importName, file, projectRoot);
|
|
2078
2215
|
srcValue = importPath ?? importName;
|
|
2079
2216
|
}
|
|
2080
2217
|
}
|
|
2081
2218
|
}
|
|
2082
|
-
const filePath = file.getFilePath();
|
|
2083
|
-
const projectRoot = findProjectRoot(filePath);
|
|
2084
2219
|
const imageSource = await resolveImageSource(srcValue, file, projectRoot);
|
|
2085
2220
|
const context = extractContext(file);
|
|
2086
2221
|
const prompt = buildImgAltPrompt({
|
|
@@ -2167,17 +2302,17 @@ function findElement(file, line) {
|
|
|
2167
2302
|
return elements.find((el) => el.getStartLineNumber() === line);
|
|
2168
2303
|
}
|
|
2169
2304
|
function findProjectRoot(filePath) {
|
|
2170
|
-
const
|
|
2171
|
-
const
|
|
2172
|
-
let dir =
|
|
2173
|
-
while (dir !==
|
|
2174
|
-
if (
|
|
2175
|
-
if (
|
|
2176
|
-
if (
|
|
2177
|
-
if (
|
|
2178
|
-
dir =
|
|
2179
|
-
}
|
|
2180
|
-
return
|
|
2305
|
+
const path10 = require("path");
|
|
2306
|
+
const fs10 = require("fs");
|
|
2307
|
+
let dir = path10.dirname(filePath);
|
|
2308
|
+
while (dir !== path10.dirname(dir)) {
|
|
2309
|
+
if (fs10.existsSync(path10.join(dir, "package.json"))) return dir;
|
|
2310
|
+
if (fs10.existsSync(path10.join(dir, "next.config.js"))) return dir;
|
|
2311
|
+
if (fs10.existsSync(path10.join(dir, "next.config.mjs"))) return dir;
|
|
2312
|
+
if (fs10.existsSync(path10.join(dir, "next.config.ts"))) return dir;
|
|
2313
|
+
dir = path10.dirname(dir);
|
|
2314
|
+
}
|
|
2315
|
+
return path10.dirname(filePath);
|
|
2181
2316
|
}
|
|
2182
2317
|
|
|
2183
2318
|
// src/scan/scan.ts
|
|
@@ -2199,7 +2334,9 @@ async function detect(targetPath, config2) {
|
|
|
2199
2334
|
config2.scanner.include,
|
|
2200
2335
|
config2.scanner.exclude
|
|
2201
2336
|
);
|
|
2337
|
+
const tsconfigPath = path6.join(absPath, "tsconfig.json");
|
|
2202
2338
|
const project = new import_ts_morph19.Project({
|
|
2339
|
+
tsConfigFilePath: fs6.existsSync(tsconfigPath) ? tsconfigPath : void 0,
|
|
2203
2340
|
skipAddingFilesFromTsConfig: true,
|
|
2204
2341
|
compilerOptions: {
|
|
2205
2342
|
jsx: 4,
|
|
@@ -2312,10 +2449,10 @@ var RULE_ICONS = {
|
|
|
2312
2449
|
"heading-order": "hdg",
|
|
2313
2450
|
"no-div-interactive": "div"
|
|
2314
2451
|
};
|
|
2315
|
-
function formatReport(result, fix) {
|
|
2452
|
+
function formatReport(result, fix, version) {
|
|
2316
2453
|
const lines = [];
|
|
2317
2454
|
lines.push("");
|
|
2318
|
-
lines.push(import_picocolors2.default.bold(` next-a11y
|
|
2455
|
+
lines.push(import_picocolors2.default.bold(` next-a11y${version ? ` v${version}` : ""}`));
|
|
2319
2456
|
lines.push(
|
|
2320
2457
|
` Scanned ${result.filesScanned} files`
|
|
2321
2458
|
);
|
|
@@ -2505,7 +2642,7 @@ async function interactiveReview(violations, onAccept) {
|
|
|
2505
2642
|
return { applied, skipped };
|
|
2506
2643
|
}
|
|
2507
2644
|
function promptAction() {
|
|
2508
|
-
return new Promise((
|
|
2645
|
+
return new Promise((resolve6) => {
|
|
2509
2646
|
const rl = readline.createInterface({
|
|
2510
2647
|
input: process.stdin,
|
|
2511
2648
|
output: process.stdout
|
|
@@ -2516,10 +2653,10 @@ function promptAction() {
|
|
|
2516
2653
|
(answer) => {
|
|
2517
2654
|
rl.close();
|
|
2518
2655
|
const normalized = answer.trim().toLowerCase();
|
|
2519
|
-
if (normalized === "n" || normalized === "no")
|
|
2520
|
-
else if (normalized === "s" || normalized === "skip")
|
|
2521
|
-
else if (normalized === "q" || normalized === "quit")
|
|
2522
|
-
else
|
|
2656
|
+
if (normalized === "n" || normalized === "no") resolve6("no");
|
|
2657
|
+
else if (normalized === "s" || normalized === "skip") resolve6("skip");
|
|
2658
|
+
else if (normalized === "q" || normalized === "quit") resolve6("quit");
|
|
2659
|
+
else resolve6("yes");
|
|
2523
2660
|
}
|
|
2524
2661
|
);
|
|
2525
2662
|
});
|
|
@@ -2527,7 +2664,23 @@ function promptAction() {
|
|
|
2527
2664
|
|
|
2528
2665
|
// src/cli/scan-command.ts
|
|
2529
2666
|
function registerScanCommand(program2) {
|
|
2667
|
+
const version = program2.version();
|
|
2530
2668
|
program2.command("scan").description("Scan files for accessibility issues").argument("<path>", "Path to scan").option("--fix", "Auto-fix issues").option("-i, --interactive", "Review each fix interactively").option("--no-ai", "Skip AI-powered fixes").option("--provider <provider>", "Override AI provider").option("--model <model>", "Override AI model").option("--min-score <score>", "Minimum score threshold (exit code 1 if below)", parseInt).action(async (targetPath, options) => {
|
|
2669
|
+
let envDir = path7.resolve(targetPath);
|
|
2670
|
+
if (fs7.existsSync(envDir) && fs7.statSync(envDir).isFile()) {
|
|
2671
|
+
envDir = path7.dirname(envDir);
|
|
2672
|
+
}
|
|
2673
|
+
let searchDir = envDir;
|
|
2674
|
+
while (searchDir !== path7.dirname(searchDir)) {
|
|
2675
|
+
for (const envFile of [".env", ".env.local"]) {
|
|
2676
|
+
const envPath = path7.join(searchDir, envFile);
|
|
2677
|
+
if (fs7.existsSync(envPath)) {
|
|
2678
|
+
(0, import_dotenv.config)({ path: envPath, override: false, quiet: true });
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (fs7.existsSync(path7.join(searchDir, "package.json"))) break;
|
|
2682
|
+
searchDir = path7.dirname(searchDir);
|
|
2683
|
+
}
|
|
2531
2684
|
const fileConfig = await loadConfigFile(process.cwd());
|
|
2532
2685
|
const config2 = resolveConfig(fileConfig, {
|
|
2533
2686
|
fix: options.fix,
|
|
@@ -2552,7 +2705,7 @@ function registerScanCommand(program2) {
|
|
|
2552
2705
|
}
|
|
2553
2706
|
);
|
|
2554
2707
|
result = await finalize(ctx, applied);
|
|
2555
|
-
console.log(formatReport(result, true));
|
|
2708
|
+
console.log(formatReport(result, true, version));
|
|
2556
2709
|
} else if (config2.fix) {
|
|
2557
2710
|
const ctx = await detect(targetPath, config2);
|
|
2558
2711
|
await resolveAi(ctx, (resolved, total, _violation, aiResult) => {
|
|
@@ -2579,10 +2732,10 @@ function registerScanCommand(program2) {
|
|
|
2579
2732
|
console.log(formatFixApplied(f.filePath, f.line, f.rule, f.message));
|
|
2580
2733
|
}
|
|
2581
2734
|
}
|
|
2582
|
-
console.log(formatReport(result, true));
|
|
2735
|
+
console.log(formatReport(result, true, version));
|
|
2583
2736
|
} else {
|
|
2584
2737
|
result = await scan(targetPath, config2);
|
|
2585
|
-
console.log(formatReport(result, false));
|
|
2738
|
+
console.log(formatReport(result, false, version));
|
|
2586
2739
|
}
|
|
2587
2740
|
if (config2.minScore !== void 0 && result.score < config2.minScore) {
|
|
2588
2741
|
console.error(
|
|
@@ -2600,26 +2753,29 @@ function registerScanCommand(program2) {
|
|
|
2600
2753
|
}
|
|
2601
2754
|
|
|
2602
2755
|
// src/cli/init-command.ts
|
|
2603
|
-
var
|
|
2604
|
-
var
|
|
2756
|
+
var fs8 = __toESM(require("fs"));
|
|
2757
|
+
var path8 = __toESM(require("path"));
|
|
2605
2758
|
var readline2 = __toESM(require("readline"));
|
|
2606
2759
|
var import_node_child_process = require("child_process");
|
|
2607
2760
|
var import_picocolors5 = __toESM(require("picocolors"));
|
|
2608
2761
|
var import_package_manager_detector = require("package-manager-detector");
|
|
2609
2762
|
function registerInitCommand(program2) {
|
|
2610
2763
|
program2.command("init").description("Initialize next-a11y configuration").action(async () => {
|
|
2611
|
-
|
|
2764
|
+
const version = program2.version();
|
|
2765
|
+
console.log(import_picocolors5.default.bold(`
|
|
2766
|
+
next-a11y v${version} \u2014 Setup
|
|
2767
|
+
`));
|
|
2612
2768
|
const options = await promptInitOptions();
|
|
2613
2769
|
const cwd = process.cwd();
|
|
2614
|
-
const hasAppDir =
|
|
2615
|
-
const hasSrcDir =
|
|
2770
|
+
const hasAppDir = fs8.existsSync(path8.join(cwd, "app"));
|
|
2771
|
+
const hasSrcDir = fs8.existsSync(path8.join(cwd, "src"));
|
|
2616
2772
|
const include = [];
|
|
2617
2773
|
if (hasSrcDir) include.push("src/**/*.{tsx,jsx}");
|
|
2618
2774
|
if (hasAppDir) include.push("app/**/*.{tsx,jsx}");
|
|
2619
2775
|
if (include.length === 0) include.push("**/*.{tsx,jsx}");
|
|
2620
2776
|
const configContent = generateConfig(options.provider, include);
|
|
2621
|
-
const configPath =
|
|
2622
|
-
|
|
2777
|
+
const configPath = path8.join(cwd, "a11y.config.ts");
|
|
2778
|
+
fs8.writeFileSync(configPath, configContent);
|
|
2623
2779
|
console.log(import_picocolors5.default.green(" Created a11y.config.ts"));
|
|
2624
2780
|
if (options.provider !== "none" && options.installDep) {
|
|
2625
2781
|
const pkgMap = {
|
|
@@ -2628,29 +2784,29 @@ function registerInitCommand(program2) {
|
|
|
2628
2784
|
google: "@ai-sdk/google",
|
|
2629
2785
|
ollama: "ollama-ai-provider"
|
|
2630
2786
|
};
|
|
2631
|
-
const
|
|
2632
|
-
if (
|
|
2787
|
+
const pkg2 = pkgMap[options.provider];
|
|
2788
|
+
if (pkg2) {
|
|
2633
2789
|
const pm = await (0, import_package_manager_detector.detect)({ cwd });
|
|
2634
|
-
const resolved = (0, import_package_manager_detector.resolveCommand)(pm?.agent ?? "npm", "add", [
|
|
2635
|
-
const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${
|
|
2790
|
+
const resolved = (0, import_package_manager_detector.resolveCommand)(pm?.agent ?? "npm", "add", [pkg2]);
|
|
2791
|
+
const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${pkg2}`;
|
|
2636
2792
|
try {
|
|
2637
2793
|
console.log(import_picocolors5.default.dim(` Running: ${installCmd}`));
|
|
2638
2794
|
(0, import_node_child_process.execSync)(installCmd, { cwd, stdio: "pipe" });
|
|
2639
|
-
console.log(import_picocolors5.default.green(` Installed ${
|
|
2795
|
+
console.log(import_picocolors5.default.green(` Installed ${pkg2}`));
|
|
2640
2796
|
} catch {
|
|
2641
|
-
console.log(import_picocolors5.default.yellow(` Failed to install ${
|
|
2797
|
+
console.log(import_picocolors5.default.yellow(` Failed to install ${pkg2}. Run manually: ${installCmd}`));
|
|
2642
2798
|
}
|
|
2643
2799
|
}
|
|
2644
2800
|
}
|
|
2645
2801
|
if (options.addGitignore) {
|
|
2646
|
-
const gitignorePath =
|
|
2802
|
+
const gitignorePath = path8.join(cwd, ".gitignore");
|
|
2647
2803
|
let content = "";
|
|
2648
|
-
if (
|
|
2649
|
-
content =
|
|
2804
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
2805
|
+
content = fs8.readFileSync(gitignorePath, "utf-8");
|
|
2650
2806
|
}
|
|
2651
2807
|
if (!content.includes(".a11y-cache")) {
|
|
2652
2808
|
const newline = content.endsWith("\n") ? "" : "\n";
|
|
2653
|
-
|
|
2809
|
+
fs8.appendFileSync(gitignorePath, `${newline}.a11y-cache
|
|
2654
2810
|
`);
|
|
2655
2811
|
console.log(import_picocolors5.default.green(" Updated .gitignore"));
|
|
2656
2812
|
}
|
|
@@ -2691,7 +2847,7 @@ async function promptInitOptions() {
|
|
|
2691
2847
|
return { provider, installDep, addGitignore };
|
|
2692
2848
|
}
|
|
2693
2849
|
function promptSelect(question, options) {
|
|
2694
|
-
return new Promise((
|
|
2850
|
+
return new Promise((resolve6) => {
|
|
2695
2851
|
const rl = readline2.createInterface({
|
|
2696
2852
|
input: process.stdin,
|
|
2697
2853
|
output: process.stdout
|
|
@@ -2704,15 +2860,15 @@ function promptSelect(question, options) {
|
|
|
2704
2860
|
rl.close();
|
|
2705
2861
|
const idx = parseInt(answer.trim()) - 1;
|
|
2706
2862
|
if (idx >= 0 && idx < options.length) {
|
|
2707
|
-
|
|
2863
|
+
resolve6(options[idx].value);
|
|
2708
2864
|
} else {
|
|
2709
|
-
|
|
2865
|
+
resolve6(options[0].value);
|
|
2710
2866
|
}
|
|
2711
2867
|
});
|
|
2712
2868
|
});
|
|
2713
2869
|
}
|
|
2714
2870
|
function promptYesNo(question) {
|
|
2715
|
-
return new Promise((
|
|
2871
|
+
return new Promise((resolve6) => {
|
|
2716
2872
|
const rl = readline2.createInterface({
|
|
2717
2873
|
input: process.stdin,
|
|
2718
2874
|
output: process.stdout
|
|
@@ -2720,7 +2876,7 @@ function promptYesNo(question) {
|
|
|
2720
2876
|
rl.question(` ${question} ${import_picocolors5.default.dim("[Y/n]")} `, (answer) => {
|
|
2721
2877
|
rl.close();
|
|
2722
2878
|
const normalized = answer.trim().toLowerCase();
|
|
2723
|
-
|
|
2879
|
+
resolve6(normalized !== "n" && normalized !== "no");
|
|
2724
2880
|
});
|
|
2725
2881
|
});
|
|
2726
2882
|
}
|
|
@@ -2798,12 +2954,14 @@ function formatBytes(bytes) {
|
|
|
2798
2954
|
}
|
|
2799
2955
|
|
|
2800
2956
|
// src/cli/index.ts
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
(0,
|
|
2804
|
-
(0,
|
|
2957
|
+
var pkgPath = path9.resolve(__dirname, "../../package.json");
|
|
2958
|
+
var pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
2959
|
+
(0, import_dotenv2.config)({ path: ".env", override: false, quiet: true });
|
|
2960
|
+
(0, import_dotenv2.config)({ path: ".env.local", override: true, quiet: true });
|
|
2961
|
+
(0, import_dotenv2.config)({ path: ".env.development", override: true, quiet: true });
|
|
2962
|
+
(0, import_dotenv2.config)({ path: ".env.development.local", override: true, quiet: true });
|
|
2805
2963
|
var program = new import_commander.Command();
|
|
2806
|
-
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version(
|
|
2964
|
+
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version(pkg.version);
|
|
2807
2965
|
registerScanCommand(program);
|
|
2808
2966
|
registerInitCommand(program);
|
|
2809
2967
|
registerCacheCommand(program);
|
package/dist/cli/index.mjs
CHANGED
|
@@ -7,10 +7,15 @@ import {
|
|
|
7
7
|
} from "../chunk-A4KDGFRG.mjs";
|
|
8
8
|
|
|
9
9
|
// src/cli/index.ts
|
|
10
|
+
import * as fs9 from "fs";
|
|
11
|
+
import * as path9 from "path";
|
|
10
12
|
import { config } from "dotenv";
|
|
11
13
|
import { Command } from "commander";
|
|
12
14
|
|
|
13
15
|
// src/cli/scan-command.ts
|
|
16
|
+
import * as fs7 from "fs";
|
|
17
|
+
import * as path7 from "path";
|
|
18
|
+
import { config as dotenvConfig } from "dotenv";
|
|
14
19
|
import pc4 from "picocolors";
|
|
15
20
|
|
|
16
21
|
// src/config/resolve.ts
|
|
@@ -42,7 +47,7 @@ async function loadConfigFile(cwd) {
|
|
|
42
47
|
}
|
|
43
48
|
function resolveConfig(fileConfig, cliFlags = {}) {
|
|
44
49
|
const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
|
|
45
|
-
const provider = cliFlags.provider ?? merged.provider;
|
|
50
|
+
const provider = cliFlags.provider ?? merged.provider ?? detectProviderFromEnv();
|
|
46
51
|
const model = cliFlags.model ?? merged.model ?? (provider ? PROVIDER_DEFAULTS[provider] : "gpt-4.1-nano");
|
|
47
52
|
return {
|
|
48
53
|
provider,
|
|
@@ -60,6 +65,12 @@ function resolveConfig(fileConfig, cliFlags = {}) {
|
|
|
60
65
|
minScore: cliFlags.minScore
|
|
61
66
|
};
|
|
62
67
|
}
|
|
68
|
+
function detectProviderFromEnv() {
|
|
69
|
+
for (const [name, envVar] of Object.entries(PROVIDER_ENV)) {
|
|
70
|
+
if (envVar && process.env[envVar]) return name;
|
|
71
|
+
}
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
63
74
|
function deepMerge(target, source) {
|
|
64
75
|
const result = { ...target };
|
|
65
76
|
for (const key of Object.keys(source)) {
|
|
@@ -88,13 +99,13 @@ async function discoverFiles(basePath, include, exclude) {
|
|
|
88
99
|
if (typeof fs2.glob === "function") {
|
|
89
100
|
for (const pattern of include) {
|
|
90
101
|
try {
|
|
91
|
-
const matches = await new Promise((
|
|
102
|
+
const matches = await new Promise((resolve6, reject) => {
|
|
92
103
|
fs2.glob(
|
|
93
104
|
pattern,
|
|
94
105
|
{ cwd: absBase },
|
|
95
106
|
(err, files) => {
|
|
96
107
|
if (err) reject(err);
|
|
97
|
-
else
|
|
108
|
+
else resolve6(files);
|
|
98
109
|
}
|
|
99
110
|
);
|
|
100
111
|
});
|
|
@@ -300,6 +311,8 @@ var buttonLabelRule = {
|
|
|
300
311
|
if (parent) {
|
|
301
312
|
const hasTextContent = parent.getDescendantsOfKind(SyntaxKind2.JsxText).some((t) => t.getText().trim().length > 0);
|
|
302
313
|
if (hasTextContent) continue;
|
|
314
|
+
const hasExpressionContent = parent.getDescendantsOfKind(SyntaxKind2.JsxExpression).some((expr) => expr.getExpression() != null);
|
|
315
|
+
if (hasExpressionContent) continue;
|
|
303
316
|
const nestedElements = parent.getDescendantsOfKind(
|
|
304
317
|
SyntaxKind2.JsxOpeningElement
|
|
305
318
|
);
|
|
@@ -454,6 +467,8 @@ var linkLabelRule = {
|
|
|
454
467
|
if (parent) {
|
|
455
468
|
const hasTextContent = parent.getDescendantsOfKind(SyntaxKind3.JsxText).some((t) => t.getText().trim().length > 0);
|
|
456
469
|
if (hasTextContent) continue;
|
|
470
|
+
const hasExpressionContent = parent.getDescendantsOfKind(SyntaxKind3.JsxExpression).some((expr) => expr.getExpression() != null);
|
|
471
|
+
if (hasExpressionContent) continue;
|
|
457
472
|
const images = [
|
|
458
473
|
...parent.getDescendantsOfKind(SyntaxKind3.JsxSelfClosingElement),
|
|
459
474
|
...parent.getDescendantsOfKind(SyntaxKind3.JsxOpeningElement)
|
|
@@ -1634,13 +1649,13 @@ var PROVIDER_INSTALL = {
|
|
|
1634
1649
|
ollama: "npm install ollama-ai-provider"
|
|
1635
1650
|
};
|
|
1636
1651
|
function createProvider(provider, model) {
|
|
1637
|
-
const
|
|
1652
|
+
const pkg2 = PROVIDER_PACKAGES[provider];
|
|
1638
1653
|
let mod;
|
|
1639
1654
|
try {
|
|
1640
|
-
mod = __require(
|
|
1655
|
+
mod = __require(pkg2);
|
|
1641
1656
|
} catch {
|
|
1642
1657
|
throw new Error(
|
|
1643
|
-
`AI provider "${provider}" requires the "${
|
|
1658
|
+
`AI provider "${provider}" requires the "${pkg2}" package.
|
|
1644
1659
|
Install it with: ${PROVIDER_INSTALL[provider]}
|
|
1645
1660
|
|
|
1646
1661
|
Or run: npx next-a11y init`
|
|
@@ -1866,7 +1881,18 @@ import * as fs5 from "fs";
|
|
|
1866
1881
|
import * as path5 from "path";
|
|
1867
1882
|
import * as https from "https";
|
|
1868
1883
|
import * as http from "http";
|
|
1884
|
+
var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"];
|
|
1885
|
+
function isImagePath(p) {
|
|
1886
|
+
return IMAGE_EXTENSIONS.some((ext) => p.endsWith(ext));
|
|
1887
|
+
}
|
|
1869
1888
|
async function resolveImageSource(src, file, projectRoot) {
|
|
1889
|
+
if (path5.isAbsolute(src) && isImagePath(src)) {
|
|
1890
|
+
try {
|
|
1891
|
+
const buffer = fs5.readFileSync(src);
|
|
1892
|
+
return { type: "file", buffer, path: src };
|
|
1893
|
+
} catch {
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1870
1896
|
if (src.startsWith("/")) {
|
|
1871
1897
|
const publicPath = path5.join(projectRoot, "public", src);
|
|
1872
1898
|
try {
|
|
@@ -1899,26 +1925,135 @@ async function resolveImageSource(src, file, projectRoot) {
|
|
|
1899
1925
|
}
|
|
1900
1926
|
return { type: "unresolvable", reason: "Dynamic image source" };
|
|
1901
1927
|
}
|
|
1902
|
-
function resolveStaticImportPath(importName, file) {
|
|
1928
|
+
function resolveStaticImportPath(importName, file, projectRoot) {
|
|
1929
|
+
let name = importName;
|
|
1930
|
+
if (name.endsWith(".src")) {
|
|
1931
|
+
name = name.slice(0, -4);
|
|
1932
|
+
}
|
|
1933
|
+
if (name.includes("[") || name.includes("(")) {
|
|
1934
|
+
return void 0;
|
|
1935
|
+
}
|
|
1903
1936
|
const imports = file.getImportDeclarations();
|
|
1937
|
+
const filePath = file.getFilePath();
|
|
1938
|
+
const root = projectRoot ?? findProjectRootFromFile(filePath);
|
|
1939
|
+
const project = file.getProject();
|
|
1904
1940
|
for (const imp of imports) {
|
|
1941
|
+
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
1905
1942
|
const defaultImport = imp.getDefaultImport();
|
|
1906
|
-
if (defaultImport?.getText() ===
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1943
|
+
if (defaultImport?.getText() === name) {
|
|
1944
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, void 0, project);
|
|
1945
|
+
}
|
|
1946
|
+
const namedImports = imp.getNamedImports();
|
|
1947
|
+
for (const named of namedImports) {
|
|
1948
|
+
if (named.getName() === name || named.getAliasNode()?.getText() === name) {
|
|
1949
|
+
const originalName = named.getName();
|
|
1950
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, originalName, project);
|
|
1911
1951
|
}
|
|
1912
1952
|
}
|
|
1913
1953
|
}
|
|
1914
1954
|
return void 0;
|
|
1915
1955
|
}
|
|
1956
|
+
function resolveModuleToImage(moduleSpecifier, fromFile, projectRoot, namedExport, project) {
|
|
1957
|
+
if (isImagePath(moduleSpecifier)) {
|
|
1958
|
+
return resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
1959
|
+
}
|
|
1960
|
+
if (namedExport) {
|
|
1961
|
+
const barrelPath = resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
1962
|
+
if (barrelPath) {
|
|
1963
|
+
return followReExport(barrelPath, namedExport);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return void 0;
|
|
1967
|
+
}
|
|
1968
|
+
function resolveModulePath(moduleSpecifier, fromFile, projectRoot, project) {
|
|
1969
|
+
let resolved;
|
|
1970
|
+
if (moduleSpecifier.startsWith(".")) {
|
|
1971
|
+
resolved = path5.resolve(path5.dirname(fromFile), moduleSpecifier);
|
|
1972
|
+
} else {
|
|
1973
|
+
const aliasResolved = resolvePathAlias(moduleSpecifier, projectRoot, project);
|
|
1974
|
+
if (aliasResolved) {
|
|
1975
|
+
resolved = aliasResolved;
|
|
1976
|
+
} else {
|
|
1977
|
+
return void 0;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (fs5.existsSync(resolved) && fs5.statSync(resolved).isFile()) {
|
|
1981
|
+
return resolved;
|
|
1982
|
+
}
|
|
1983
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ...IMAGE_EXTENSIONS];
|
|
1984
|
+
for (const ext of extensions) {
|
|
1985
|
+
const withExt = resolved + ext;
|
|
1986
|
+
if (fs5.existsSync(withExt)) return withExt;
|
|
1987
|
+
}
|
|
1988
|
+
const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
|
1989
|
+
for (const idx of indexFiles) {
|
|
1990
|
+
const indexPath = path5.join(resolved, idx);
|
|
1991
|
+
if (fs5.existsSync(indexPath)) return indexPath;
|
|
1992
|
+
}
|
|
1993
|
+
return void 0;
|
|
1994
|
+
}
|
|
1995
|
+
function followReExport(barrelPath, exportName) {
|
|
1996
|
+
try {
|
|
1997
|
+
const content = fs5.readFileSync(barrelPath, "utf-8");
|
|
1998
|
+
const reExportPattern = new RegExp(
|
|
1999
|
+
`export\\s*\\{[^}]*\\b(?:default\\s+as\\s+)?${escapeRegex(exportName)}\\b[^}]*\\}\\s*from\\s*["']([^"']+)["']`
|
|
2000
|
+
);
|
|
2001
|
+
const match = content.match(reExportPattern);
|
|
2002
|
+
if (match) {
|
|
2003
|
+
const reExportPath = match[1];
|
|
2004
|
+
if (isImagePath(reExportPath)) {
|
|
2005
|
+
return path5.resolve(path5.dirname(barrelPath), reExportPath);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
} catch {
|
|
2009
|
+
}
|
|
2010
|
+
return void 0;
|
|
2011
|
+
}
|
|
2012
|
+
function resolvePathAlias(moduleSpecifier, projectRoot, project) {
|
|
2013
|
+
if (!project) return void 0;
|
|
2014
|
+
const opts = project.getCompilerOptions();
|
|
2015
|
+
const paths = opts.paths;
|
|
2016
|
+
if (!paths) return void 0;
|
|
2017
|
+
const baseDir = opts.baseUrl ?? projectRoot;
|
|
2018
|
+
for (const [pattern, mappings] of Object.entries(paths)) {
|
|
2019
|
+
if (pattern.endsWith("/*")) {
|
|
2020
|
+
const prefix = pattern.slice(0, -1);
|
|
2021
|
+
if (moduleSpecifier.startsWith(prefix)) {
|
|
2022
|
+
const rest = moduleSpecifier.slice(prefix.length);
|
|
2023
|
+
for (const mapping of mappings) {
|
|
2024
|
+
const mappingBase = mapping.endsWith("/*") ? mapping.slice(0, -1) : mapping;
|
|
2025
|
+
const resolved = path5.resolve(baseDir, mappingBase + rest);
|
|
2026
|
+
return resolved;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
} else if (pattern === moduleSpecifier) {
|
|
2030
|
+
if (mappings.length > 0) {
|
|
2031
|
+
return path5.resolve(baseDir, mappings[0]);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return void 0;
|
|
2036
|
+
}
|
|
2037
|
+
function escapeRegex(str) {
|
|
2038
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2039
|
+
}
|
|
2040
|
+
function findProjectRootFromFile(filePath) {
|
|
2041
|
+
let dir = path5.dirname(filePath);
|
|
2042
|
+
while (dir !== path5.dirname(dir)) {
|
|
2043
|
+
if (fs5.existsSync(path5.join(dir, "package.json"))) return dir;
|
|
2044
|
+
if (fs5.existsSync(path5.join(dir, "next.config.js"))) return dir;
|
|
2045
|
+
if (fs5.existsSync(path5.join(dir, "next.config.mjs"))) return dir;
|
|
2046
|
+
if (fs5.existsSync(path5.join(dir, "next.config.ts"))) return dir;
|
|
2047
|
+
dir = path5.dirname(dir);
|
|
2048
|
+
}
|
|
2049
|
+
return path5.dirname(filePath);
|
|
2050
|
+
}
|
|
1916
2051
|
function fetchImage(url) {
|
|
1917
|
-
return new Promise((
|
|
2052
|
+
return new Promise((resolve6, reject) => {
|
|
1918
2053
|
const client = url.startsWith("https") ? https : http;
|
|
1919
2054
|
const req = client.get(url, { timeout: 1e4 }, (res) => {
|
|
1920
2055
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
1921
|
-
fetchImage(res.headers.location).then(
|
|
2056
|
+
fetchImage(res.headers.location).then(resolve6).catch(reject);
|
|
1922
2057
|
return;
|
|
1923
2058
|
}
|
|
1924
2059
|
const chunks = [];
|
|
@@ -1933,7 +2068,7 @@ function fetchImage(url) {
|
|
|
1933
2068
|
}
|
|
1934
2069
|
chunks.push(chunk);
|
|
1935
2070
|
});
|
|
1936
|
-
res.on("end", () =>
|
|
2071
|
+
res.on("end", () => resolve6(Buffer.concat(chunks)));
|
|
1937
2072
|
res.on("error", reject);
|
|
1938
2073
|
});
|
|
1939
2074
|
req.on("error", reject);
|
|
@@ -2006,6 +2141,8 @@ async function resolveAiFixes(opts) {
|
|
|
2006
2141
|
async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
2007
2142
|
const el = findElement(file, violation.line);
|
|
2008
2143
|
if (!el) return "";
|
|
2144
|
+
const filePath = file.getFilePath();
|
|
2145
|
+
const projectRoot = findProjectRoot(filePath);
|
|
2009
2146
|
const srcAttr = el.getAttribute("src");
|
|
2010
2147
|
let srcValue = "";
|
|
2011
2148
|
if (srcAttr?.getKind() === SyntaxKind18.JsxAttribute) {
|
|
@@ -2016,13 +2153,11 @@ async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
|
2016
2153
|
const expr = init.asKind(SyntaxKind18.JsxExpression)?.getExpression();
|
|
2017
2154
|
if (expr) {
|
|
2018
2155
|
const importName = expr.getText();
|
|
2019
|
-
const importPath = resolveStaticImportPath(importName, file);
|
|
2156
|
+
const importPath = resolveStaticImportPath(importName, file, projectRoot);
|
|
2020
2157
|
srcValue = importPath ?? importName;
|
|
2021
2158
|
}
|
|
2022
2159
|
}
|
|
2023
2160
|
}
|
|
2024
|
-
const filePath = file.getFilePath();
|
|
2025
|
-
const projectRoot = findProjectRoot(filePath);
|
|
2026
2161
|
const imageSource = await resolveImageSource(srcValue, file, projectRoot);
|
|
2027
2162
|
const context = extractContext(file);
|
|
2028
2163
|
const prompt = buildImgAltPrompt({
|
|
@@ -2109,17 +2244,17 @@ function findElement(file, line) {
|
|
|
2109
2244
|
return elements.find((el) => el.getStartLineNumber() === line);
|
|
2110
2245
|
}
|
|
2111
2246
|
function findProjectRoot(filePath) {
|
|
2112
|
-
const
|
|
2113
|
-
const
|
|
2114
|
-
let dir =
|
|
2115
|
-
while (dir !==
|
|
2116
|
-
if (
|
|
2117
|
-
if (
|
|
2118
|
-
if (
|
|
2119
|
-
if (
|
|
2120
|
-
dir =
|
|
2121
|
-
}
|
|
2122
|
-
return
|
|
2247
|
+
const path10 = __require("path");
|
|
2248
|
+
const fs10 = __require("fs");
|
|
2249
|
+
let dir = path10.dirname(filePath);
|
|
2250
|
+
while (dir !== path10.dirname(dir)) {
|
|
2251
|
+
if (fs10.existsSync(path10.join(dir, "package.json"))) return dir;
|
|
2252
|
+
if (fs10.existsSync(path10.join(dir, "next.config.js"))) return dir;
|
|
2253
|
+
if (fs10.existsSync(path10.join(dir, "next.config.mjs"))) return dir;
|
|
2254
|
+
if (fs10.existsSync(path10.join(dir, "next.config.ts"))) return dir;
|
|
2255
|
+
dir = path10.dirname(dir);
|
|
2256
|
+
}
|
|
2257
|
+
return path10.dirname(filePath);
|
|
2123
2258
|
}
|
|
2124
2259
|
|
|
2125
2260
|
// src/scan/scan.ts
|
|
@@ -2141,7 +2276,9 @@ async function detect(targetPath, config2) {
|
|
|
2141
2276
|
config2.scanner.include,
|
|
2142
2277
|
config2.scanner.exclude
|
|
2143
2278
|
);
|
|
2279
|
+
const tsconfigPath = path6.join(absPath, "tsconfig.json");
|
|
2144
2280
|
const project = new Project2({
|
|
2281
|
+
tsConfigFilePath: fs6.existsSync(tsconfigPath) ? tsconfigPath : void 0,
|
|
2145
2282
|
skipAddingFilesFromTsConfig: true,
|
|
2146
2283
|
compilerOptions: {
|
|
2147
2284
|
jsx: 4,
|
|
@@ -2254,10 +2391,10 @@ var RULE_ICONS = {
|
|
|
2254
2391
|
"heading-order": "hdg",
|
|
2255
2392
|
"no-div-interactive": "div"
|
|
2256
2393
|
};
|
|
2257
|
-
function formatReport(result, fix) {
|
|
2394
|
+
function formatReport(result, fix, version) {
|
|
2258
2395
|
const lines = [];
|
|
2259
2396
|
lines.push("");
|
|
2260
|
-
lines.push(pc2.bold(` next-a11y
|
|
2397
|
+
lines.push(pc2.bold(` next-a11y${version ? ` v${version}` : ""}`));
|
|
2261
2398
|
lines.push(
|
|
2262
2399
|
` Scanned ${result.filesScanned} files`
|
|
2263
2400
|
);
|
|
@@ -2447,7 +2584,7 @@ async function interactiveReview(violations, onAccept) {
|
|
|
2447
2584
|
return { applied, skipped };
|
|
2448
2585
|
}
|
|
2449
2586
|
function promptAction() {
|
|
2450
|
-
return new Promise((
|
|
2587
|
+
return new Promise((resolve6) => {
|
|
2451
2588
|
const rl = readline.createInterface({
|
|
2452
2589
|
input: process.stdin,
|
|
2453
2590
|
output: process.stdout
|
|
@@ -2458,10 +2595,10 @@ function promptAction() {
|
|
|
2458
2595
|
(answer) => {
|
|
2459
2596
|
rl.close();
|
|
2460
2597
|
const normalized = answer.trim().toLowerCase();
|
|
2461
|
-
if (normalized === "n" || normalized === "no")
|
|
2462
|
-
else if (normalized === "s" || normalized === "skip")
|
|
2463
|
-
else if (normalized === "q" || normalized === "quit")
|
|
2464
|
-
else
|
|
2598
|
+
if (normalized === "n" || normalized === "no") resolve6("no");
|
|
2599
|
+
else if (normalized === "s" || normalized === "skip") resolve6("skip");
|
|
2600
|
+
else if (normalized === "q" || normalized === "quit") resolve6("quit");
|
|
2601
|
+
else resolve6("yes");
|
|
2465
2602
|
}
|
|
2466
2603
|
);
|
|
2467
2604
|
});
|
|
@@ -2469,7 +2606,23 @@ function promptAction() {
|
|
|
2469
2606
|
|
|
2470
2607
|
// src/cli/scan-command.ts
|
|
2471
2608
|
function registerScanCommand(program2) {
|
|
2609
|
+
const version = program2.version();
|
|
2472
2610
|
program2.command("scan").description("Scan files for accessibility issues").argument("<path>", "Path to scan").option("--fix", "Auto-fix issues").option("-i, --interactive", "Review each fix interactively").option("--no-ai", "Skip AI-powered fixes").option("--provider <provider>", "Override AI provider").option("--model <model>", "Override AI model").option("--min-score <score>", "Minimum score threshold (exit code 1 if below)", parseInt).action(async (targetPath, options) => {
|
|
2611
|
+
let envDir = path7.resolve(targetPath);
|
|
2612
|
+
if (fs7.existsSync(envDir) && fs7.statSync(envDir).isFile()) {
|
|
2613
|
+
envDir = path7.dirname(envDir);
|
|
2614
|
+
}
|
|
2615
|
+
let searchDir = envDir;
|
|
2616
|
+
while (searchDir !== path7.dirname(searchDir)) {
|
|
2617
|
+
for (const envFile of [".env", ".env.local"]) {
|
|
2618
|
+
const envPath = path7.join(searchDir, envFile);
|
|
2619
|
+
if (fs7.existsSync(envPath)) {
|
|
2620
|
+
dotenvConfig({ path: envPath, override: false, quiet: true });
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
if (fs7.existsSync(path7.join(searchDir, "package.json"))) break;
|
|
2624
|
+
searchDir = path7.dirname(searchDir);
|
|
2625
|
+
}
|
|
2473
2626
|
const fileConfig = await loadConfigFile(process.cwd());
|
|
2474
2627
|
const config2 = resolveConfig(fileConfig, {
|
|
2475
2628
|
fix: options.fix,
|
|
@@ -2494,7 +2647,7 @@ function registerScanCommand(program2) {
|
|
|
2494
2647
|
}
|
|
2495
2648
|
);
|
|
2496
2649
|
result = await finalize(ctx, applied);
|
|
2497
|
-
console.log(formatReport(result, true));
|
|
2650
|
+
console.log(formatReport(result, true, version));
|
|
2498
2651
|
} else if (config2.fix) {
|
|
2499
2652
|
const ctx = await detect(targetPath, config2);
|
|
2500
2653
|
await resolveAi(ctx, (resolved, total, _violation, aiResult) => {
|
|
@@ -2521,10 +2674,10 @@ function registerScanCommand(program2) {
|
|
|
2521
2674
|
console.log(formatFixApplied(f.filePath, f.line, f.rule, f.message));
|
|
2522
2675
|
}
|
|
2523
2676
|
}
|
|
2524
|
-
console.log(formatReport(result, true));
|
|
2677
|
+
console.log(formatReport(result, true, version));
|
|
2525
2678
|
} else {
|
|
2526
2679
|
result = await scan(targetPath, config2);
|
|
2527
|
-
console.log(formatReport(result, false));
|
|
2680
|
+
console.log(formatReport(result, false, version));
|
|
2528
2681
|
}
|
|
2529
2682
|
if (config2.minScore !== void 0 && result.score < config2.minScore) {
|
|
2530
2683
|
console.error(
|
|
@@ -2542,26 +2695,29 @@ function registerScanCommand(program2) {
|
|
|
2542
2695
|
}
|
|
2543
2696
|
|
|
2544
2697
|
// src/cli/init-command.ts
|
|
2545
|
-
import * as
|
|
2546
|
-
import * as
|
|
2698
|
+
import * as fs8 from "fs";
|
|
2699
|
+
import * as path8 from "path";
|
|
2547
2700
|
import * as readline2 from "readline";
|
|
2548
2701
|
import { execSync } from "child_process";
|
|
2549
2702
|
import pc5 from "picocolors";
|
|
2550
2703
|
import { detect as detectPM, resolveCommand } from "package-manager-detector";
|
|
2551
2704
|
function registerInitCommand(program2) {
|
|
2552
2705
|
program2.command("init").description("Initialize next-a11y configuration").action(async () => {
|
|
2553
|
-
|
|
2706
|
+
const version = program2.version();
|
|
2707
|
+
console.log(pc5.bold(`
|
|
2708
|
+
next-a11y v${version} \u2014 Setup
|
|
2709
|
+
`));
|
|
2554
2710
|
const options = await promptInitOptions();
|
|
2555
2711
|
const cwd = process.cwd();
|
|
2556
|
-
const hasAppDir =
|
|
2557
|
-
const hasSrcDir =
|
|
2712
|
+
const hasAppDir = fs8.existsSync(path8.join(cwd, "app"));
|
|
2713
|
+
const hasSrcDir = fs8.existsSync(path8.join(cwd, "src"));
|
|
2558
2714
|
const include = [];
|
|
2559
2715
|
if (hasSrcDir) include.push("src/**/*.{tsx,jsx}");
|
|
2560
2716
|
if (hasAppDir) include.push("app/**/*.{tsx,jsx}");
|
|
2561
2717
|
if (include.length === 0) include.push("**/*.{tsx,jsx}");
|
|
2562
2718
|
const configContent = generateConfig(options.provider, include);
|
|
2563
|
-
const configPath =
|
|
2564
|
-
|
|
2719
|
+
const configPath = path8.join(cwd, "a11y.config.ts");
|
|
2720
|
+
fs8.writeFileSync(configPath, configContent);
|
|
2565
2721
|
console.log(pc5.green(" Created a11y.config.ts"));
|
|
2566
2722
|
if (options.provider !== "none" && options.installDep) {
|
|
2567
2723
|
const pkgMap = {
|
|
@@ -2570,29 +2726,29 @@ function registerInitCommand(program2) {
|
|
|
2570
2726
|
google: "@ai-sdk/google",
|
|
2571
2727
|
ollama: "ollama-ai-provider"
|
|
2572
2728
|
};
|
|
2573
|
-
const
|
|
2574
|
-
if (
|
|
2729
|
+
const pkg2 = pkgMap[options.provider];
|
|
2730
|
+
if (pkg2) {
|
|
2575
2731
|
const pm = await detectPM({ cwd });
|
|
2576
|
-
const resolved = resolveCommand(pm?.agent ?? "npm", "add", [
|
|
2577
|
-
const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${
|
|
2732
|
+
const resolved = resolveCommand(pm?.agent ?? "npm", "add", [pkg2]);
|
|
2733
|
+
const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${pkg2}`;
|
|
2578
2734
|
try {
|
|
2579
2735
|
console.log(pc5.dim(` Running: ${installCmd}`));
|
|
2580
2736
|
execSync(installCmd, { cwd, stdio: "pipe" });
|
|
2581
|
-
console.log(pc5.green(` Installed ${
|
|
2737
|
+
console.log(pc5.green(` Installed ${pkg2}`));
|
|
2582
2738
|
} catch {
|
|
2583
|
-
console.log(pc5.yellow(` Failed to install ${
|
|
2739
|
+
console.log(pc5.yellow(` Failed to install ${pkg2}. Run manually: ${installCmd}`));
|
|
2584
2740
|
}
|
|
2585
2741
|
}
|
|
2586
2742
|
}
|
|
2587
2743
|
if (options.addGitignore) {
|
|
2588
|
-
const gitignorePath =
|
|
2744
|
+
const gitignorePath = path8.join(cwd, ".gitignore");
|
|
2589
2745
|
let content = "";
|
|
2590
|
-
if (
|
|
2591
|
-
content =
|
|
2746
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
2747
|
+
content = fs8.readFileSync(gitignorePath, "utf-8");
|
|
2592
2748
|
}
|
|
2593
2749
|
if (!content.includes(".a11y-cache")) {
|
|
2594
2750
|
const newline = content.endsWith("\n") ? "" : "\n";
|
|
2595
|
-
|
|
2751
|
+
fs8.appendFileSync(gitignorePath, `${newline}.a11y-cache
|
|
2596
2752
|
`);
|
|
2597
2753
|
console.log(pc5.green(" Updated .gitignore"));
|
|
2598
2754
|
}
|
|
@@ -2633,7 +2789,7 @@ async function promptInitOptions() {
|
|
|
2633
2789
|
return { provider, installDep, addGitignore };
|
|
2634
2790
|
}
|
|
2635
2791
|
function promptSelect(question, options) {
|
|
2636
|
-
return new Promise((
|
|
2792
|
+
return new Promise((resolve6) => {
|
|
2637
2793
|
const rl = readline2.createInterface({
|
|
2638
2794
|
input: process.stdin,
|
|
2639
2795
|
output: process.stdout
|
|
@@ -2646,15 +2802,15 @@ function promptSelect(question, options) {
|
|
|
2646
2802
|
rl.close();
|
|
2647
2803
|
const idx = parseInt(answer.trim()) - 1;
|
|
2648
2804
|
if (idx >= 0 && idx < options.length) {
|
|
2649
|
-
|
|
2805
|
+
resolve6(options[idx].value);
|
|
2650
2806
|
} else {
|
|
2651
|
-
|
|
2807
|
+
resolve6(options[0].value);
|
|
2652
2808
|
}
|
|
2653
2809
|
});
|
|
2654
2810
|
});
|
|
2655
2811
|
}
|
|
2656
2812
|
function promptYesNo(question) {
|
|
2657
|
-
return new Promise((
|
|
2813
|
+
return new Promise((resolve6) => {
|
|
2658
2814
|
const rl = readline2.createInterface({
|
|
2659
2815
|
input: process.stdin,
|
|
2660
2816
|
output: process.stdout
|
|
@@ -2662,7 +2818,7 @@ function promptYesNo(question) {
|
|
|
2662
2818
|
rl.question(` ${question} ${pc5.dim("[Y/n]")} `, (answer) => {
|
|
2663
2819
|
rl.close();
|
|
2664
2820
|
const normalized = answer.trim().toLowerCase();
|
|
2665
|
-
|
|
2821
|
+
resolve6(normalized !== "n" && normalized !== "no");
|
|
2666
2822
|
});
|
|
2667
2823
|
});
|
|
2668
2824
|
}
|
|
@@ -2740,12 +2896,14 @@ function formatBytes(bytes) {
|
|
|
2740
2896
|
}
|
|
2741
2897
|
|
|
2742
2898
|
// src/cli/index.ts
|
|
2899
|
+
var pkgPath = path9.resolve(__dirname, "../../package.json");
|
|
2900
|
+
var pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
2743
2901
|
config({ path: ".env", override: false, quiet: true });
|
|
2744
2902
|
config({ path: ".env.local", override: true, quiet: true });
|
|
2745
2903
|
config({ path: ".env.development", override: true, quiet: true });
|
|
2746
2904
|
config({ path: ".env.development.local", override: true, quiet: true });
|
|
2747
2905
|
var program = new Command();
|
|
2748
|
-
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version(
|
|
2906
|
+
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version(pkg.version);
|
|
2749
2907
|
registerScanCommand(program);
|
|
2750
2908
|
registerInitCommand(program);
|
|
2751
2909
|
registerCacheCommand(program);
|