next-a11y 0.1.3 → 0.1.5
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 +204 -62
- package/dist/cli/index.mjs +199 -57
- package/package.json +2 -1
package/README.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -23,10 +23,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/cli/index.ts
|
|
26
|
-
var
|
|
26
|
+
var import_dotenv2 = require("dotenv");
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
29
|
// src/cli/scan-command.ts
|
|
30
|
+
var fs7 = __toESM(require("fs"));
|
|
31
|
+
var path7 = __toESM(require("path"));
|
|
32
|
+
var import_dotenv = require("dotenv");
|
|
30
33
|
var import_picocolors4 = __toESM(require("picocolors"));
|
|
31
34
|
|
|
32
35
|
// src/config/resolve.ts
|
|
@@ -100,7 +103,7 @@ async function loadConfigFile(cwd) {
|
|
|
100
103
|
}
|
|
101
104
|
function resolveConfig(fileConfig, cliFlags = {}) {
|
|
102
105
|
const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
|
|
103
|
-
const provider = cliFlags.provider ?? merged.provider;
|
|
106
|
+
const provider = cliFlags.provider ?? merged.provider ?? detectProviderFromEnv();
|
|
104
107
|
const model = cliFlags.model ?? merged.model ?? (provider ? PROVIDER_DEFAULTS[provider] : "gpt-4.1-nano");
|
|
105
108
|
return {
|
|
106
109
|
provider,
|
|
@@ -118,6 +121,12 @@ function resolveConfig(fileConfig, cliFlags = {}) {
|
|
|
118
121
|
minScore: cliFlags.minScore
|
|
119
122
|
};
|
|
120
123
|
}
|
|
124
|
+
function detectProviderFromEnv() {
|
|
125
|
+
for (const [name, envVar] of Object.entries(PROVIDER_ENV)) {
|
|
126
|
+
if (envVar && process.env[envVar]) return name;
|
|
127
|
+
}
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
121
130
|
function deepMerge(target, source) {
|
|
122
131
|
const result = { ...target };
|
|
123
132
|
for (const key of Object.keys(source)) {
|
|
@@ -146,13 +155,13 @@ async function discoverFiles(basePath, include, exclude) {
|
|
|
146
155
|
if (typeof fs2.glob === "function") {
|
|
147
156
|
for (const pattern of include) {
|
|
148
157
|
try {
|
|
149
|
-
const matches = await new Promise((
|
|
158
|
+
const matches = await new Promise((resolve5, reject) => {
|
|
150
159
|
fs2.glob(
|
|
151
160
|
pattern,
|
|
152
161
|
{ cwd: absBase },
|
|
153
162
|
(err, files) => {
|
|
154
163
|
if (err) reject(err);
|
|
155
|
-
else
|
|
164
|
+
else resolve5(files);
|
|
156
165
|
}
|
|
157
166
|
);
|
|
158
167
|
});
|
|
@@ -1924,7 +1933,18 @@ var fs5 = __toESM(require("fs"));
|
|
|
1924
1933
|
var path5 = __toESM(require("path"));
|
|
1925
1934
|
var https = __toESM(require("https"));
|
|
1926
1935
|
var http = __toESM(require("http"));
|
|
1936
|
+
var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"];
|
|
1937
|
+
function isImagePath(p) {
|
|
1938
|
+
return IMAGE_EXTENSIONS.some((ext) => p.endsWith(ext));
|
|
1939
|
+
}
|
|
1927
1940
|
async function resolveImageSource(src, file, projectRoot) {
|
|
1941
|
+
if (path5.isAbsolute(src) && isImagePath(src)) {
|
|
1942
|
+
try {
|
|
1943
|
+
const buffer = fs5.readFileSync(src);
|
|
1944
|
+
return { type: "file", buffer, path: src };
|
|
1945
|
+
} catch {
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1928
1948
|
if (src.startsWith("/")) {
|
|
1929
1949
|
const publicPath = path5.join(projectRoot, "public", src);
|
|
1930
1950
|
try {
|
|
@@ -1957,26 +1977,135 @@ async function resolveImageSource(src, file, projectRoot) {
|
|
|
1957
1977
|
}
|
|
1958
1978
|
return { type: "unresolvable", reason: "Dynamic image source" };
|
|
1959
1979
|
}
|
|
1960
|
-
function resolveStaticImportPath(importName, file) {
|
|
1980
|
+
function resolveStaticImportPath(importName, file, projectRoot) {
|
|
1981
|
+
let name = importName;
|
|
1982
|
+
if (name.endsWith(".src")) {
|
|
1983
|
+
name = name.slice(0, -4);
|
|
1984
|
+
}
|
|
1985
|
+
if (name.includes("[") || name.includes("(")) {
|
|
1986
|
+
return void 0;
|
|
1987
|
+
}
|
|
1961
1988
|
const imports = file.getImportDeclarations();
|
|
1989
|
+
const filePath = file.getFilePath();
|
|
1990
|
+
const root = projectRoot ?? findProjectRootFromFile(filePath);
|
|
1991
|
+
const project = file.getProject();
|
|
1962
1992
|
for (const imp of imports) {
|
|
1993
|
+
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
1963
1994
|
const defaultImport = imp.getDefaultImport();
|
|
1964
|
-
if (defaultImport?.getText() ===
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1995
|
+
if (defaultImport?.getText() === name) {
|
|
1996
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, void 0, project);
|
|
1997
|
+
}
|
|
1998
|
+
const namedImports = imp.getNamedImports();
|
|
1999
|
+
for (const named of namedImports) {
|
|
2000
|
+
if (named.getName() === name || named.getAliasNode()?.getText() === name) {
|
|
2001
|
+
const originalName = named.getName();
|
|
2002
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, originalName, project);
|
|
1969
2003
|
}
|
|
1970
2004
|
}
|
|
1971
2005
|
}
|
|
1972
2006
|
return void 0;
|
|
1973
2007
|
}
|
|
2008
|
+
function resolveModuleToImage(moduleSpecifier, fromFile, projectRoot, namedExport, project) {
|
|
2009
|
+
if (isImagePath(moduleSpecifier)) {
|
|
2010
|
+
return resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
2011
|
+
}
|
|
2012
|
+
if (namedExport) {
|
|
2013
|
+
const barrelPath = resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
2014
|
+
if (barrelPath) {
|
|
2015
|
+
return followReExport(barrelPath, namedExport);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return void 0;
|
|
2019
|
+
}
|
|
2020
|
+
function resolveModulePath(moduleSpecifier, fromFile, projectRoot, project) {
|
|
2021
|
+
let resolved;
|
|
2022
|
+
if (moduleSpecifier.startsWith(".")) {
|
|
2023
|
+
resolved = path5.resolve(path5.dirname(fromFile), moduleSpecifier);
|
|
2024
|
+
} else {
|
|
2025
|
+
const aliasResolved = resolvePathAlias(moduleSpecifier, projectRoot, project);
|
|
2026
|
+
if (aliasResolved) {
|
|
2027
|
+
resolved = aliasResolved;
|
|
2028
|
+
} else {
|
|
2029
|
+
return void 0;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (fs5.existsSync(resolved) && fs5.statSync(resolved).isFile()) {
|
|
2033
|
+
return resolved;
|
|
2034
|
+
}
|
|
2035
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ...IMAGE_EXTENSIONS];
|
|
2036
|
+
for (const ext of extensions) {
|
|
2037
|
+
const withExt = resolved + ext;
|
|
2038
|
+
if (fs5.existsSync(withExt)) return withExt;
|
|
2039
|
+
}
|
|
2040
|
+
const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
|
2041
|
+
for (const idx of indexFiles) {
|
|
2042
|
+
const indexPath = path5.join(resolved, idx);
|
|
2043
|
+
if (fs5.existsSync(indexPath)) return indexPath;
|
|
2044
|
+
}
|
|
2045
|
+
return void 0;
|
|
2046
|
+
}
|
|
2047
|
+
function followReExport(barrelPath, exportName) {
|
|
2048
|
+
try {
|
|
2049
|
+
const content = fs5.readFileSync(barrelPath, "utf-8");
|
|
2050
|
+
const reExportPattern = new RegExp(
|
|
2051
|
+
`export\\s*\\{[^}]*\\b(?:default\\s+as\\s+)?${escapeRegex(exportName)}\\b[^}]*\\}\\s*from\\s*["']([^"']+)["']`
|
|
2052
|
+
);
|
|
2053
|
+
const match = content.match(reExportPattern);
|
|
2054
|
+
if (match) {
|
|
2055
|
+
const reExportPath = match[1];
|
|
2056
|
+
if (isImagePath(reExportPath)) {
|
|
2057
|
+
return path5.resolve(path5.dirname(barrelPath), reExportPath);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
} catch {
|
|
2061
|
+
}
|
|
2062
|
+
return void 0;
|
|
2063
|
+
}
|
|
2064
|
+
function resolvePathAlias(moduleSpecifier, projectRoot, project) {
|
|
2065
|
+
if (!project) return void 0;
|
|
2066
|
+
const opts = project.getCompilerOptions();
|
|
2067
|
+
const paths = opts.paths;
|
|
2068
|
+
if (!paths) return void 0;
|
|
2069
|
+
const baseDir = opts.baseUrl ?? projectRoot;
|
|
2070
|
+
for (const [pattern, mappings] of Object.entries(paths)) {
|
|
2071
|
+
if (pattern.endsWith("/*")) {
|
|
2072
|
+
const prefix = pattern.slice(0, -1);
|
|
2073
|
+
if (moduleSpecifier.startsWith(prefix)) {
|
|
2074
|
+
const rest = moduleSpecifier.slice(prefix.length);
|
|
2075
|
+
for (const mapping of mappings) {
|
|
2076
|
+
const mappingBase = mapping.endsWith("/*") ? mapping.slice(0, -1) : mapping;
|
|
2077
|
+
const resolved = path5.resolve(baseDir, mappingBase + rest);
|
|
2078
|
+
return resolved;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
} else if (pattern === moduleSpecifier) {
|
|
2082
|
+
if (mappings.length > 0) {
|
|
2083
|
+
return path5.resolve(baseDir, mappings[0]);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return void 0;
|
|
2088
|
+
}
|
|
2089
|
+
function escapeRegex(str) {
|
|
2090
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2091
|
+
}
|
|
2092
|
+
function findProjectRootFromFile(filePath) {
|
|
2093
|
+
let dir = path5.dirname(filePath);
|
|
2094
|
+
while (dir !== path5.dirname(dir)) {
|
|
2095
|
+
if (fs5.existsSync(path5.join(dir, "package.json"))) return dir;
|
|
2096
|
+
if (fs5.existsSync(path5.join(dir, "next.config.js"))) return dir;
|
|
2097
|
+
if (fs5.existsSync(path5.join(dir, "next.config.mjs"))) return dir;
|
|
2098
|
+
if (fs5.existsSync(path5.join(dir, "next.config.ts"))) return dir;
|
|
2099
|
+
dir = path5.dirname(dir);
|
|
2100
|
+
}
|
|
2101
|
+
return path5.dirname(filePath);
|
|
2102
|
+
}
|
|
1974
2103
|
function fetchImage(url) {
|
|
1975
|
-
return new Promise((
|
|
2104
|
+
return new Promise((resolve5, reject) => {
|
|
1976
2105
|
const client = url.startsWith("https") ? https : http;
|
|
1977
2106
|
const req = client.get(url, { timeout: 1e4 }, (res) => {
|
|
1978
2107
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
1979
|
-
fetchImage(res.headers.location).then(
|
|
2108
|
+
fetchImage(res.headers.location).then(resolve5).catch(reject);
|
|
1980
2109
|
return;
|
|
1981
2110
|
}
|
|
1982
2111
|
const chunks = [];
|
|
@@ -1991,7 +2120,7 @@ function fetchImage(url) {
|
|
|
1991
2120
|
}
|
|
1992
2121
|
chunks.push(chunk);
|
|
1993
2122
|
});
|
|
1994
|
-
res.on("end", () =>
|
|
2123
|
+
res.on("end", () => resolve5(Buffer.concat(chunks)));
|
|
1995
2124
|
res.on("error", reject);
|
|
1996
2125
|
});
|
|
1997
2126
|
req.on("error", reject);
|
|
@@ -2064,6 +2193,8 @@ async function resolveAiFixes(opts) {
|
|
|
2064
2193
|
async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
2065
2194
|
const el = findElement(file, violation.line);
|
|
2066
2195
|
if (!el) return "";
|
|
2196
|
+
const filePath = file.getFilePath();
|
|
2197
|
+
const projectRoot = findProjectRoot(filePath);
|
|
2067
2198
|
const srcAttr = el.getAttribute("src");
|
|
2068
2199
|
let srcValue = "";
|
|
2069
2200
|
if (srcAttr?.getKind() === import_ts_morph18.SyntaxKind.JsxAttribute) {
|
|
@@ -2074,13 +2205,11 @@ async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
|
2074
2205
|
const expr = init.asKind(import_ts_morph18.SyntaxKind.JsxExpression)?.getExpression();
|
|
2075
2206
|
if (expr) {
|
|
2076
2207
|
const importName = expr.getText();
|
|
2077
|
-
const importPath = resolveStaticImportPath(importName, file);
|
|
2208
|
+
const importPath = resolveStaticImportPath(importName, file, projectRoot);
|
|
2078
2209
|
srcValue = importPath ?? importName;
|
|
2079
2210
|
}
|
|
2080
2211
|
}
|
|
2081
2212
|
}
|
|
2082
|
-
const filePath = file.getFilePath();
|
|
2083
|
-
const projectRoot = findProjectRoot(filePath);
|
|
2084
2213
|
const imageSource = await resolveImageSource(srcValue, file, projectRoot);
|
|
2085
2214
|
const context = extractContext(file);
|
|
2086
2215
|
const prompt = buildImgAltPrompt({
|
|
@@ -2167,17 +2296,17 @@ function findElement(file, line) {
|
|
|
2167
2296
|
return elements.find((el) => el.getStartLineNumber() === line);
|
|
2168
2297
|
}
|
|
2169
2298
|
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
|
|
2299
|
+
const path9 = require("path");
|
|
2300
|
+
const fs9 = require("fs");
|
|
2301
|
+
let dir = path9.dirname(filePath);
|
|
2302
|
+
while (dir !== path9.dirname(dir)) {
|
|
2303
|
+
if (fs9.existsSync(path9.join(dir, "package.json"))) return dir;
|
|
2304
|
+
if (fs9.existsSync(path9.join(dir, "next.config.js"))) return dir;
|
|
2305
|
+
if (fs9.existsSync(path9.join(dir, "next.config.mjs"))) return dir;
|
|
2306
|
+
if (fs9.existsSync(path9.join(dir, "next.config.ts"))) return dir;
|
|
2307
|
+
dir = path9.dirname(dir);
|
|
2308
|
+
}
|
|
2309
|
+
return path9.dirname(filePath);
|
|
2181
2310
|
}
|
|
2182
2311
|
|
|
2183
2312
|
// src/scan/scan.ts
|
|
@@ -2199,7 +2328,9 @@ async function detect(targetPath, config2) {
|
|
|
2199
2328
|
config2.scanner.include,
|
|
2200
2329
|
config2.scanner.exclude
|
|
2201
2330
|
);
|
|
2331
|
+
const tsconfigPath = path6.join(absPath, "tsconfig.json");
|
|
2202
2332
|
const project = new import_ts_morph19.Project({
|
|
2333
|
+
tsConfigFilePath: fs6.existsSync(tsconfigPath) ? tsconfigPath : void 0,
|
|
2203
2334
|
skipAddingFilesFromTsConfig: true,
|
|
2204
2335
|
compilerOptions: {
|
|
2205
2336
|
jsx: 4,
|
|
@@ -2315,7 +2446,7 @@ var RULE_ICONS = {
|
|
|
2315
2446
|
function formatReport(result, fix) {
|
|
2316
2447
|
const lines = [];
|
|
2317
2448
|
lines.push("");
|
|
2318
|
-
lines.push(import_picocolors2.default.bold(` next-a11y v0.1.
|
|
2449
|
+
lines.push(import_picocolors2.default.bold(` next-a11y v0.1.4`));
|
|
2319
2450
|
lines.push(
|
|
2320
2451
|
` Scanned ${result.filesScanned} files`
|
|
2321
2452
|
);
|
|
@@ -2505,7 +2636,7 @@ async function interactiveReview(violations, onAccept) {
|
|
|
2505
2636
|
return { applied, skipped };
|
|
2506
2637
|
}
|
|
2507
2638
|
function promptAction() {
|
|
2508
|
-
return new Promise((
|
|
2639
|
+
return new Promise((resolve5) => {
|
|
2509
2640
|
const rl = readline.createInterface({
|
|
2510
2641
|
input: process.stdin,
|
|
2511
2642
|
output: process.stdout
|
|
@@ -2516,10 +2647,10 @@ function promptAction() {
|
|
|
2516
2647
|
(answer) => {
|
|
2517
2648
|
rl.close();
|
|
2518
2649
|
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
|
|
2650
|
+
if (normalized === "n" || normalized === "no") resolve5("no");
|
|
2651
|
+
else if (normalized === "s" || normalized === "skip") resolve5("skip");
|
|
2652
|
+
else if (normalized === "q" || normalized === "quit") resolve5("quit");
|
|
2653
|
+
else resolve5("yes");
|
|
2523
2654
|
}
|
|
2524
2655
|
);
|
|
2525
2656
|
});
|
|
@@ -2528,6 +2659,21 @@ function promptAction() {
|
|
|
2528
2659
|
// src/cli/scan-command.ts
|
|
2529
2660
|
function registerScanCommand(program2) {
|
|
2530
2661
|
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) => {
|
|
2662
|
+
let envDir = path7.resolve(targetPath);
|
|
2663
|
+
if (fs7.existsSync(envDir) && fs7.statSync(envDir).isFile()) {
|
|
2664
|
+
envDir = path7.dirname(envDir);
|
|
2665
|
+
}
|
|
2666
|
+
let searchDir = envDir;
|
|
2667
|
+
while (searchDir !== path7.dirname(searchDir)) {
|
|
2668
|
+
for (const envFile of [".env", ".env.local"]) {
|
|
2669
|
+
const envPath = path7.join(searchDir, envFile);
|
|
2670
|
+
if (fs7.existsSync(envPath)) {
|
|
2671
|
+
(0, import_dotenv.config)({ path: envPath, override: false, quiet: true });
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
if (fs7.existsSync(path7.join(searchDir, "package.json"))) break;
|
|
2675
|
+
searchDir = path7.dirname(searchDir);
|
|
2676
|
+
}
|
|
2531
2677
|
const fileConfig = await loadConfigFile(process.cwd());
|
|
2532
2678
|
const config2 = resolveConfig(fileConfig, {
|
|
2533
2679
|
fix: options.fix,
|
|
@@ -2600,25 +2746,26 @@ function registerScanCommand(program2) {
|
|
|
2600
2746
|
}
|
|
2601
2747
|
|
|
2602
2748
|
// src/cli/init-command.ts
|
|
2603
|
-
var
|
|
2604
|
-
var
|
|
2749
|
+
var fs8 = __toESM(require("fs"));
|
|
2750
|
+
var path8 = __toESM(require("path"));
|
|
2605
2751
|
var readline2 = __toESM(require("readline"));
|
|
2606
2752
|
var import_node_child_process = require("child_process");
|
|
2607
2753
|
var import_picocolors5 = __toESM(require("picocolors"));
|
|
2754
|
+
var import_package_manager_detector = require("package-manager-detector");
|
|
2608
2755
|
function registerInitCommand(program2) {
|
|
2609
2756
|
program2.command("init").description("Initialize next-a11y configuration").action(async () => {
|
|
2610
|
-
console.log(import_picocolors5.default.bold("\n next-a11y v0.1.
|
|
2757
|
+
console.log(import_picocolors5.default.bold("\n next-a11y v0.1.4 \u2014 Setup\n"));
|
|
2611
2758
|
const options = await promptInitOptions();
|
|
2612
2759
|
const cwd = process.cwd();
|
|
2613
|
-
const hasAppDir =
|
|
2614
|
-
const hasSrcDir =
|
|
2760
|
+
const hasAppDir = fs8.existsSync(path8.join(cwd, "app"));
|
|
2761
|
+
const hasSrcDir = fs8.existsSync(path8.join(cwd, "src"));
|
|
2615
2762
|
const include = [];
|
|
2616
2763
|
if (hasSrcDir) include.push("src/**/*.{tsx,jsx}");
|
|
2617
2764
|
if (hasAppDir) include.push("app/**/*.{tsx,jsx}");
|
|
2618
2765
|
if (include.length === 0) include.push("**/*.{tsx,jsx}");
|
|
2619
2766
|
const configContent = generateConfig(options.provider, include);
|
|
2620
|
-
const configPath =
|
|
2621
|
-
|
|
2767
|
+
const configPath = path8.join(cwd, "a11y.config.ts");
|
|
2768
|
+
fs8.writeFileSync(configPath, configContent);
|
|
2622
2769
|
console.log(import_picocolors5.default.green(" Created a11y.config.ts"));
|
|
2623
2770
|
if (options.provider !== "none" && options.installDep) {
|
|
2624
2771
|
const pkgMap = {
|
|
@@ -2629,8 +2776,9 @@ function registerInitCommand(program2) {
|
|
|
2629
2776
|
};
|
|
2630
2777
|
const pkg = pkgMap[options.provider];
|
|
2631
2778
|
if (pkg) {
|
|
2632
|
-
const pm =
|
|
2633
|
-
const
|
|
2779
|
+
const pm = await (0, import_package_manager_detector.detect)({ cwd });
|
|
2780
|
+
const resolved = (0, import_package_manager_detector.resolveCommand)(pm?.agent ?? "npm", "add", [pkg]);
|
|
2781
|
+
const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${pkg}`;
|
|
2634
2782
|
try {
|
|
2635
2783
|
console.log(import_picocolors5.default.dim(` Running: ${installCmd}`));
|
|
2636
2784
|
(0, import_node_child_process.execSync)(installCmd, { cwd, stdio: "pipe" });
|
|
@@ -2641,14 +2789,14 @@ function registerInitCommand(program2) {
|
|
|
2641
2789
|
}
|
|
2642
2790
|
}
|
|
2643
2791
|
if (options.addGitignore) {
|
|
2644
|
-
const gitignorePath =
|
|
2792
|
+
const gitignorePath = path8.join(cwd, ".gitignore");
|
|
2645
2793
|
let content = "";
|
|
2646
|
-
if (
|
|
2647
|
-
content =
|
|
2794
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
2795
|
+
content = fs8.readFileSync(gitignorePath, "utf-8");
|
|
2648
2796
|
}
|
|
2649
2797
|
if (!content.includes(".a11y-cache")) {
|
|
2650
2798
|
const newline = content.endsWith("\n") ? "" : "\n";
|
|
2651
|
-
|
|
2799
|
+
fs8.appendFileSync(gitignorePath, `${newline}.a11y-cache
|
|
2652
2800
|
`);
|
|
2653
2801
|
console.log(import_picocolors5.default.green(" Updated .gitignore"));
|
|
2654
2802
|
}
|
|
@@ -2689,7 +2837,7 @@ async function promptInitOptions() {
|
|
|
2689
2837
|
return { provider, installDep, addGitignore };
|
|
2690
2838
|
}
|
|
2691
2839
|
function promptSelect(question, options) {
|
|
2692
|
-
return new Promise((
|
|
2840
|
+
return new Promise((resolve5) => {
|
|
2693
2841
|
const rl = readline2.createInterface({
|
|
2694
2842
|
input: process.stdin,
|
|
2695
2843
|
output: process.stdout
|
|
@@ -2702,15 +2850,15 @@ function promptSelect(question, options) {
|
|
|
2702
2850
|
rl.close();
|
|
2703
2851
|
const idx = parseInt(answer.trim()) - 1;
|
|
2704
2852
|
if (idx >= 0 && idx < options.length) {
|
|
2705
|
-
|
|
2853
|
+
resolve5(options[idx].value);
|
|
2706
2854
|
} else {
|
|
2707
|
-
|
|
2855
|
+
resolve5(options[0].value);
|
|
2708
2856
|
}
|
|
2709
2857
|
});
|
|
2710
2858
|
});
|
|
2711
2859
|
}
|
|
2712
2860
|
function promptYesNo(question) {
|
|
2713
|
-
return new Promise((
|
|
2861
|
+
return new Promise((resolve5) => {
|
|
2714
2862
|
const rl = readline2.createInterface({
|
|
2715
2863
|
input: process.stdin,
|
|
2716
2864
|
output: process.stdout
|
|
@@ -2718,16 +2866,10 @@ function promptYesNo(question) {
|
|
|
2718
2866
|
rl.question(` ${question} ${import_picocolors5.default.dim("[Y/n]")} `, (answer) => {
|
|
2719
2867
|
rl.close();
|
|
2720
2868
|
const normalized = answer.trim().toLowerCase();
|
|
2721
|
-
|
|
2869
|
+
resolve5(normalized !== "n" && normalized !== "no");
|
|
2722
2870
|
});
|
|
2723
2871
|
});
|
|
2724
2872
|
}
|
|
2725
|
-
function detectPackageManager(cwd) {
|
|
2726
|
-
if (fs7.existsSync(path7.join(cwd, "bun.lock"))) return "bun";
|
|
2727
|
-
if (fs7.existsSync(path7.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2728
|
-
if (fs7.existsSync(path7.join(cwd, "yarn.lock"))) return "yarn";
|
|
2729
|
-
return "npm";
|
|
2730
|
-
}
|
|
2731
2873
|
function generateConfig(provider, include) {
|
|
2732
2874
|
const providerLine = provider === "none" ? " // provider: 'openai', // Uncomment and set when ready" : ` provider: "${provider}",`;
|
|
2733
2875
|
const modelMap = {
|
|
@@ -2802,12 +2944,12 @@ function formatBytes(bytes) {
|
|
|
2802
2944
|
}
|
|
2803
2945
|
|
|
2804
2946
|
// src/cli/index.ts
|
|
2805
|
-
(0,
|
|
2806
|
-
(0,
|
|
2807
|
-
(0,
|
|
2808
|
-
(0,
|
|
2947
|
+
(0, import_dotenv2.config)({ path: ".env", override: false, quiet: true });
|
|
2948
|
+
(0, import_dotenv2.config)({ path: ".env.local", override: true, quiet: true });
|
|
2949
|
+
(0, import_dotenv2.config)({ path: ".env.development", override: true, quiet: true });
|
|
2950
|
+
(0, import_dotenv2.config)({ path: ".env.development.local", override: true, quiet: true });
|
|
2809
2951
|
var program = new import_commander.Command();
|
|
2810
|
-
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version("0.1.
|
|
2952
|
+
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version("0.1.4");
|
|
2811
2953
|
registerScanCommand(program);
|
|
2812
2954
|
registerInitCommand(program);
|
|
2813
2955
|
registerCacheCommand(program);
|
package/dist/cli/index.mjs
CHANGED
|
@@ -11,6 +11,9 @@ import { config } from "dotenv";
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
|
|
13
13
|
// src/cli/scan-command.ts
|
|
14
|
+
import * as fs7 from "fs";
|
|
15
|
+
import * as path7 from "path";
|
|
16
|
+
import { config as dotenvConfig } from "dotenv";
|
|
14
17
|
import pc4 from "picocolors";
|
|
15
18
|
|
|
16
19
|
// src/config/resolve.ts
|
|
@@ -42,7 +45,7 @@ async function loadConfigFile(cwd) {
|
|
|
42
45
|
}
|
|
43
46
|
function resolveConfig(fileConfig, cliFlags = {}) {
|
|
44
47
|
const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
|
|
45
|
-
const provider = cliFlags.provider ?? merged.provider;
|
|
48
|
+
const provider = cliFlags.provider ?? merged.provider ?? detectProviderFromEnv();
|
|
46
49
|
const model = cliFlags.model ?? merged.model ?? (provider ? PROVIDER_DEFAULTS[provider] : "gpt-4.1-nano");
|
|
47
50
|
return {
|
|
48
51
|
provider,
|
|
@@ -60,6 +63,12 @@ function resolveConfig(fileConfig, cliFlags = {}) {
|
|
|
60
63
|
minScore: cliFlags.minScore
|
|
61
64
|
};
|
|
62
65
|
}
|
|
66
|
+
function detectProviderFromEnv() {
|
|
67
|
+
for (const [name, envVar] of Object.entries(PROVIDER_ENV)) {
|
|
68
|
+
if (envVar && process.env[envVar]) return name;
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
63
72
|
function deepMerge(target, source) {
|
|
64
73
|
const result = { ...target };
|
|
65
74
|
for (const key of Object.keys(source)) {
|
|
@@ -88,13 +97,13 @@ async function discoverFiles(basePath, include, exclude) {
|
|
|
88
97
|
if (typeof fs2.glob === "function") {
|
|
89
98
|
for (const pattern of include) {
|
|
90
99
|
try {
|
|
91
|
-
const matches = await new Promise((
|
|
100
|
+
const matches = await new Promise((resolve5, reject) => {
|
|
92
101
|
fs2.glob(
|
|
93
102
|
pattern,
|
|
94
103
|
{ cwd: absBase },
|
|
95
104
|
(err, files) => {
|
|
96
105
|
if (err) reject(err);
|
|
97
|
-
else
|
|
106
|
+
else resolve5(files);
|
|
98
107
|
}
|
|
99
108
|
);
|
|
100
109
|
});
|
|
@@ -1866,7 +1875,18 @@ import * as fs5 from "fs";
|
|
|
1866
1875
|
import * as path5 from "path";
|
|
1867
1876
|
import * as https from "https";
|
|
1868
1877
|
import * as http from "http";
|
|
1878
|
+
var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"];
|
|
1879
|
+
function isImagePath(p) {
|
|
1880
|
+
return IMAGE_EXTENSIONS.some((ext) => p.endsWith(ext));
|
|
1881
|
+
}
|
|
1869
1882
|
async function resolveImageSource(src, file, projectRoot) {
|
|
1883
|
+
if (path5.isAbsolute(src) && isImagePath(src)) {
|
|
1884
|
+
try {
|
|
1885
|
+
const buffer = fs5.readFileSync(src);
|
|
1886
|
+
return { type: "file", buffer, path: src };
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1870
1890
|
if (src.startsWith("/")) {
|
|
1871
1891
|
const publicPath = path5.join(projectRoot, "public", src);
|
|
1872
1892
|
try {
|
|
@@ -1899,26 +1919,135 @@ async function resolveImageSource(src, file, projectRoot) {
|
|
|
1899
1919
|
}
|
|
1900
1920
|
return { type: "unresolvable", reason: "Dynamic image source" };
|
|
1901
1921
|
}
|
|
1902
|
-
function resolveStaticImportPath(importName, file) {
|
|
1922
|
+
function resolveStaticImportPath(importName, file, projectRoot) {
|
|
1923
|
+
let name = importName;
|
|
1924
|
+
if (name.endsWith(".src")) {
|
|
1925
|
+
name = name.slice(0, -4);
|
|
1926
|
+
}
|
|
1927
|
+
if (name.includes("[") || name.includes("(")) {
|
|
1928
|
+
return void 0;
|
|
1929
|
+
}
|
|
1903
1930
|
const imports = file.getImportDeclarations();
|
|
1931
|
+
const filePath = file.getFilePath();
|
|
1932
|
+
const root = projectRoot ?? findProjectRootFromFile(filePath);
|
|
1933
|
+
const project = file.getProject();
|
|
1904
1934
|
for (const imp of imports) {
|
|
1935
|
+
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
1905
1936
|
const defaultImport = imp.getDefaultImport();
|
|
1906
|
-
if (defaultImport?.getText() ===
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1937
|
+
if (defaultImport?.getText() === name) {
|
|
1938
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, void 0, project);
|
|
1939
|
+
}
|
|
1940
|
+
const namedImports = imp.getNamedImports();
|
|
1941
|
+
for (const named of namedImports) {
|
|
1942
|
+
if (named.getName() === name || named.getAliasNode()?.getText() === name) {
|
|
1943
|
+
const originalName = named.getName();
|
|
1944
|
+
return resolveModuleToImage(moduleSpecifier, filePath, root, originalName, project);
|
|
1911
1945
|
}
|
|
1912
1946
|
}
|
|
1913
1947
|
}
|
|
1914
1948
|
return void 0;
|
|
1915
1949
|
}
|
|
1950
|
+
function resolveModuleToImage(moduleSpecifier, fromFile, projectRoot, namedExport, project) {
|
|
1951
|
+
if (isImagePath(moduleSpecifier)) {
|
|
1952
|
+
return resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
1953
|
+
}
|
|
1954
|
+
if (namedExport) {
|
|
1955
|
+
const barrelPath = resolveModulePath(moduleSpecifier, fromFile, projectRoot, project);
|
|
1956
|
+
if (barrelPath) {
|
|
1957
|
+
return followReExport(barrelPath, namedExport);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
return void 0;
|
|
1961
|
+
}
|
|
1962
|
+
function resolveModulePath(moduleSpecifier, fromFile, projectRoot, project) {
|
|
1963
|
+
let resolved;
|
|
1964
|
+
if (moduleSpecifier.startsWith(".")) {
|
|
1965
|
+
resolved = path5.resolve(path5.dirname(fromFile), moduleSpecifier);
|
|
1966
|
+
} else {
|
|
1967
|
+
const aliasResolved = resolvePathAlias(moduleSpecifier, projectRoot, project);
|
|
1968
|
+
if (aliasResolved) {
|
|
1969
|
+
resolved = aliasResolved;
|
|
1970
|
+
} else {
|
|
1971
|
+
return void 0;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
if (fs5.existsSync(resolved) && fs5.statSync(resolved).isFile()) {
|
|
1975
|
+
return resolved;
|
|
1976
|
+
}
|
|
1977
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ...IMAGE_EXTENSIONS];
|
|
1978
|
+
for (const ext of extensions) {
|
|
1979
|
+
const withExt = resolved + ext;
|
|
1980
|
+
if (fs5.existsSync(withExt)) return withExt;
|
|
1981
|
+
}
|
|
1982
|
+
const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
|
1983
|
+
for (const idx of indexFiles) {
|
|
1984
|
+
const indexPath = path5.join(resolved, idx);
|
|
1985
|
+
if (fs5.existsSync(indexPath)) return indexPath;
|
|
1986
|
+
}
|
|
1987
|
+
return void 0;
|
|
1988
|
+
}
|
|
1989
|
+
function followReExport(barrelPath, exportName) {
|
|
1990
|
+
try {
|
|
1991
|
+
const content = fs5.readFileSync(barrelPath, "utf-8");
|
|
1992
|
+
const reExportPattern = new RegExp(
|
|
1993
|
+
`export\\s*\\{[^}]*\\b(?:default\\s+as\\s+)?${escapeRegex(exportName)}\\b[^}]*\\}\\s*from\\s*["']([^"']+)["']`
|
|
1994
|
+
);
|
|
1995
|
+
const match = content.match(reExportPattern);
|
|
1996
|
+
if (match) {
|
|
1997
|
+
const reExportPath = match[1];
|
|
1998
|
+
if (isImagePath(reExportPath)) {
|
|
1999
|
+
return path5.resolve(path5.dirname(barrelPath), reExportPath);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
} catch {
|
|
2003
|
+
}
|
|
2004
|
+
return void 0;
|
|
2005
|
+
}
|
|
2006
|
+
function resolvePathAlias(moduleSpecifier, projectRoot, project) {
|
|
2007
|
+
if (!project) return void 0;
|
|
2008
|
+
const opts = project.getCompilerOptions();
|
|
2009
|
+
const paths = opts.paths;
|
|
2010
|
+
if (!paths) return void 0;
|
|
2011
|
+
const baseDir = opts.baseUrl ?? projectRoot;
|
|
2012
|
+
for (const [pattern, mappings] of Object.entries(paths)) {
|
|
2013
|
+
if (pattern.endsWith("/*")) {
|
|
2014
|
+
const prefix = pattern.slice(0, -1);
|
|
2015
|
+
if (moduleSpecifier.startsWith(prefix)) {
|
|
2016
|
+
const rest = moduleSpecifier.slice(prefix.length);
|
|
2017
|
+
for (const mapping of mappings) {
|
|
2018
|
+
const mappingBase = mapping.endsWith("/*") ? mapping.slice(0, -1) : mapping;
|
|
2019
|
+
const resolved = path5.resolve(baseDir, mappingBase + rest);
|
|
2020
|
+
return resolved;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
} else if (pattern === moduleSpecifier) {
|
|
2024
|
+
if (mappings.length > 0) {
|
|
2025
|
+
return path5.resolve(baseDir, mappings[0]);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return void 0;
|
|
2030
|
+
}
|
|
2031
|
+
function escapeRegex(str) {
|
|
2032
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2033
|
+
}
|
|
2034
|
+
function findProjectRootFromFile(filePath) {
|
|
2035
|
+
let dir = path5.dirname(filePath);
|
|
2036
|
+
while (dir !== path5.dirname(dir)) {
|
|
2037
|
+
if (fs5.existsSync(path5.join(dir, "package.json"))) return dir;
|
|
2038
|
+
if (fs5.existsSync(path5.join(dir, "next.config.js"))) return dir;
|
|
2039
|
+
if (fs5.existsSync(path5.join(dir, "next.config.mjs"))) return dir;
|
|
2040
|
+
if (fs5.existsSync(path5.join(dir, "next.config.ts"))) return dir;
|
|
2041
|
+
dir = path5.dirname(dir);
|
|
2042
|
+
}
|
|
2043
|
+
return path5.dirname(filePath);
|
|
2044
|
+
}
|
|
1916
2045
|
function fetchImage(url) {
|
|
1917
|
-
return new Promise((
|
|
2046
|
+
return new Promise((resolve5, reject) => {
|
|
1918
2047
|
const client = url.startsWith("https") ? https : http;
|
|
1919
2048
|
const req = client.get(url, { timeout: 1e4 }, (res) => {
|
|
1920
2049
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
1921
|
-
fetchImage(res.headers.location).then(
|
|
2050
|
+
fetchImage(res.headers.location).then(resolve5).catch(reject);
|
|
1922
2051
|
return;
|
|
1923
2052
|
}
|
|
1924
2053
|
const chunks = [];
|
|
@@ -1933,7 +2062,7 @@ function fetchImage(url) {
|
|
|
1933
2062
|
}
|
|
1934
2063
|
chunks.push(chunk);
|
|
1935
2064
|
});
|
|
1936
|
-
res.on("end", () =>
|
|
2065
|
+
res.on("end", () => resolve5(Buffer.concat(chunks)));
|
|
1937
2066
|
res.on("error", reject);
|
|
1938
2067
|
});
|
|
1939
2068
|
req.on("error", reject);
|
|
@@ -2006,6 +2135,8 @@ async function resolveAiFixes(opts) {
|
|
|
2006
2135
|
async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
2007
2136
|
const el = findElement(file, violation.line);
|
|
2008
2137
|
if (!el) return "";
|
|
2138
|
+
const filePath = file.getFilePath();
|
|
2139
|
+
const projectRoot = findProjectRoot(filePath);
|
|
2009
2140
|
const srcAttr = el.getAttribute("src");
|
|
2010
2141
|
let srcValue = "";
|
|
2011
2142
|
if (srcAttr?.getKind() === SyntaxKind18.JsxAttribute) {
|
|
@@ -2016,13 +2147,11 @@ async function resolveImgAlt(file, violation, model, config2, cache) {
|
|
|
2016
2147
|
const expr = init.asKind(SyntaxKind18.JsxExpression)?.getExpression();
|
|
2017
2148
|
if (expr) {
|
|
2018
2149
|
const importName = expr.getText();
|
|
2019
|
-
const importPath = resolveStaticImportPath(importName, file);
|
|
2150
|
+
const importPath = resolveStaticImportPath(importName, file, projectRoot);
|
|
2020
2151
|
srcValue = importPath ?? importName;
|
|
2021
2152
|
}
|
|
2022
2153
|
}
|
|
2023
2154
|
}
|
|
2024
|
-
const filePath = file.getFilePath();
|
|
2025
|
-
const projectRoot = findProjectRoot(filePath);
|
|
2026
2155
|
const imageSource = await resolveImageSource(srcValue, file, projectRoot);
|
|
2027
2156
|
const context = extractContext(file);
|
|
2028
2157
|
const prompt = buildImgAltPrompt({
|
|
@@ -2109,17 +2238,17 @@ function findElement(file, line) {
|
|
|
2109
2238
|
return elements.find((el) => el.getStartLineNumber() === line);
|
|
2110
2239
|
}
|
|
2111
2240
|
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
|
|
2241
|
+
const path9 = __require("path");
|
|
2242
|
+
const fs9 = __require("fs");
|
|
2243
|
+
let dir = path9.dirname(filePath);
|
|
2244
|
+
while (dir !== path9.dirname(dir)) {
|
|
2245
|
+
if (fs9.existsSync(path9.join(dir, "package.json"))) return dir;
|
|
2246
|
+
if (fs9.existsSync(path9.join(dir, "next.config.js"))) return dir;
|
|
2247
|
+
if (fs9.existsSync(path9.join(dir, "next.config.mjs"))) return dir;
|
|
2248
|
+
if (fs9.existsSync(path9.join(dir, "next.config.ts"))) return dir;
|
|
2249
|
+
dir = path9.dirname(dir);
|
|
2250
|
+
}
|
|
2251
|
+
return path9.dirname(filePath);
|
|
2123
2252
|
}
|
|
2124
2253
|
|
|
2125
2254
|
// src/scan/scan.ts
|
|
@@ -2141,7 +2270,9 @@ async function detect(targetPath, config2) {
|
|
|
2141
2270
|
config2.scanner.include,
|
|
2142
2271
|
config2.scanner.exclude
|
|
2143
2272
|
);
|
|
2273
|
+
const tsconfigPath = path6.join(absPath, "tsconfig.json");
|
|
2144
2274
|
const project = new Project2({
|
|
2275
|
+
tsConfigFilePath: fs6.existsSync(tsconfigPath) ? tsconfigPath : void 0,
|
|
2145
2276
|
skipAddingFilesFromTsConfig: true,
|
|
2146
2277
|
compilerOptions: {
|
|
2147
2278
|
jsx: 4,
|
|
@@ -2257,7 +2388,7 @@ var RULE_ICONS = {
|
|
|
2257
2388
|
function formatReport(result, fix) {
|
|
2258
2389
|
const lines = [];
|
|
2259
2390
|
lines.push("");
|
|
2260
|
-
lines.push(pc2.bold(` next-a11y v0.1.
|
|
2391
|
+
lines.push(pc2.bold(` next-a11y v0.1.4`));
|
|
2261
2392
|
lines.push(
|
|
2262
2393
|
` Scanned ${result.filesScanned} files`
|
|
2263
2394
|
);
|
|
@@ -2447,7 +2578,7 @@ async function interactiveReview(violations, onAccept) {
|
|
|
2447
2578
|
return { applied, skipped };
|
|
2448
2579
|
}
|
|
2449
2580
|
function promptAction() {
|
|
2450
|
-
return new Promise((
|
|
2581
|
+
return new Promise((resolve5) => {
|
|
2451
2582
|
const rl = readline.createInterface({
|
|
2452
2583
|
input: process.stdin,
|
|
2453
2584
|
output: process.stdout
|
|
@@ -2458,10 +2589,10 @@ function promptAction() {
|
|
|
2458
2589
|
(answer) => {
|
|
2459
2590
|
rl.close();
|
|
2460
2591
|
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
|
|
2592
|
+
if (normalized === "n" || normalized === "no") resolve5("no");
|
|
2593
|
+
else if (normalized === "s" || normalized === "skip") resolve5("skip");
|
|
2594
|
+
else if (normalized === "q" || normalized === "quit") resolve5("quit");
|
|
2595
|
+
else resolve5("yes");
|
|
2465
2596
|
}
|
|
2466
2597
|
);
|
|
2467
2598
|
});
|
|
@@ -2470,6 +2601,21 @@ function promptAction() {
|
|
|
2470
2601
|
// src/cli/scan-command.ts
|
|
2471
2602
|
function registerScanCommand(program2) {
|
|
2472
2603
|
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) => {
|
|
2604
|
+
let envDir = path7.resolve(targetPath);
|
|
2605
|
+
if (fs7.existsSync(envDir) && fs7.statSync(envDir).isFile()) {
|
|
2606
|
+
envDir = path7.dirname(envDir);
|
|
2607
|
+
}
|
|
2608
|
+
let searchDir = envDir;
|
|
2609
|
+
while (searchDir !== path7.dirname(searchDir)) {
|
|
2610
|
+
for (const envFile of [".env", ".env.local"]) {
|
|
2611
|
+
const envPath = path7.join(searchDir, envFile);
|
|
2612
|
+
if (fs7.existsSync(envPath)) {
|
|
2613
|
+
dotenvConfig({ path: envPath, override: false, quiet: true });
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (fs7.existsSync(path7.join(searchDir, "package.json"))) break;
|
|
2617
|
+
searchDir = path7.dirname(searchDir);
|
|
2618
|
+
}
|
|
2473
2619
|
const fileConfig = await loadConfigFile(process.cwd());
|
|
2474
2620
|
const config2 = resolveConfig(fileConfig, {
|
|
2475
2621
|
fix: options.fix,
|
|
@@ -2542,25 +2688,26 @@ function registerScanCommand(program2) {
|
|
|
2542
2688
|
}
|
|
2543
2689
|
|
|
2544
2690
|
// src/cli/init-command.ts
|
|
2545
|
-
import * as
|
|
2546
|
-
import * as
|
|
2691
|
+
import * as fs8 from "fs";
|
|
2692
|
+
import * as path8 from "path";
|
|
2547
2693
|
import * as readline2 from "readline";
|
|
2548
2694
|
import { execSync } from "child_process";
|
|
2549
2695
|
import pc5 from "picocolors";
|
|
2696
|
+
import { detect as detectPM, resolveCommand } from "package-manager-detector";
|
|
2550
2697
|
function registerInitCommand(program2) {
|
|
2551
2698
|
program2.command("init").description("Initialize next-a11y configuration").action(async () => {
|
|
2552
|
-
console.log(pc5.bold("\n next-a11y v0.1.
|
|
2699
|
+
console.log(pc5.bold("\n next-a11y v0.1.4 \u2014 Setup\n"));
|
|
2553
2700
|
const options = await promptInitOptions();
|
|
2554
2701
|
const cwd = process.cwd();
|
|
2555
|
-
const hasAppDir =
|
|
2556
|
-
const hasSrcDir =
|
|
2702
|
+
const hasAppDir = fs8.existsSync(path8.join(cwd, "app"));
|
|
2703
|
+
const hasSrcDir = fs8.existsSync(path8.join(cwd, "src"));
|
|
2557
2704
|
const include = [];
|
|
2558
2705
|
if (hasSrcDir) include.push("src/**/*.{tsx,jsx}");
|
|
2559
2706
|
if (hasAppDir) include.push("app/**/*.{tsx,jsx}");
|
|
2560
2707
|
if (include.length === 0) include.push("**/*.{tsx,jsx}");
|
|
2561
2708
|
const configContent = generateConfig(options.provider, include);
|
|
2562
|
-
const configPath =
|
|
2563
|
-
|
|
2709
|
+
const configPath = path8.join(cwd, "a11y.config.ts");
|
|
2710
|
+
fs8.writeFileSync(configPath, configContent);
|
|
2564
2711
|
console.log(pc5.green(" Created a11y.config.ts"));
|
|
2565
2712
|
if (options.provider !== "none" && options.installDep) {
|
|
2566
2713
|
const pkgMap = {
|
|
@@ -2571,8 +2718,9 @@ function registerInitCommand(program2) {
|
|
|
2571
2718
|
};
|
|
2572
2719
|
const pkg = pkgMap[options.provider];
|
|
2573
2720
|
if (pkg) {
|
|
2574
|
-
const pm =
|
|
2575
|
-
const
|
|
2721
|
+
const pm = await detectPM({ cwd });
|
|
2722
|
+
const resolved = resolveCommand(pm?.agent ?? "npm", "add", [pkg]);
|
|
2723
|
+
const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${pkg}`;
|
|
2576
2724
|
try {
|
|
2577
2725
|
console.log(pc5.dim(` Running: ${installCmd}`));
|
|
2578
2726
|
execSync(installCmd, { cwd, stdio: "pipe" });
|
|
@@ -2583,14 +2731,14 @@ function registerInitCommand(program2) {
|
|
|
2583
2731
|
}
|
|
2584
2732
|
}
|
|
2585
2733
|
if (options.addGitignore) {
|
|
2586
|
-
const gitignorePath =
|
|
2734
|
+
const gitignorePath = path8.join(cwd, ".gitignore");
|
|
2587
2735
|
let content = "";
|
|
2588
|
-
if (
|
|
2589
|
-
content =
|
|
2736
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
2737
|
+
content = fs8.readFileSync(gitignorePath, "utf-8");
|
|
2590
2738
|
}
|
|
2591
2739
|
if (!content.includes(".a11y-cache")) {
|
|
2592
2740
|
const newline = content.endsWith("\n") ? "" : "\n";
|
|
2593
|
-
|
|
2741
|
+
fs8.appendFileSync(gitignorePath, `${newline}.a11y-cache
|
|
2594
2742
|
`);
|
|
2595
2743
|
console.log(pc5.green(" Updated .gitignore"));
|
|
2596
2744
|
}
|
|
@@ -2631,7 +2779,7 @@ async function promptInitOptions() {
|
|
|
2631
2779
|
return { provider, installDep, addGitignore };
|
|
2632
2780
|
}
|
|
2633
2781
|
function promptSelect(question, options) {
|
|
2634
|
-
return new Promise((
|
|
2782
|
+
return new Promise((resolve5) => {
|
|
2635
2783
|
const rl = readline2.createInterface({
|
|
2636
2784
|
input: process.stdin,
|
|
2637
2785
|
output: process.stdout
|
|
@@ -2644,15 +2792,15 @@ function promptSelect(question, options) {
|
|
|
2644
2792
|
rl.close();
|
|
2645
2793
|
const idx = parseInt(answer.trim()) - 1;
|
|
2646
2794
|
if (idx >= 0 && idx < options.length) {
|
|
2647
|
-
|
|
2795
|
+
resolve5(options[idx].value);
|
|
2648
2796
|
} else {
|
|
2649
|
-
|
|
2797
|
+
resolve5(options[0].value);
|
|
2650
2798
|
}
|
|
2651
2799
|
});
|
|
2652
2800
|
});
|
|
2653
2801
|
}
|
|
2654
2802
|
function promptYesNo(question) {
|
|
2655
|
-
return new Promise((
|
|
2803
|
+
return new Promise((resolve5) => {
|
|
2656
2804
|
const rl = readline2.createInterface({
|
|
2657
2805
|
input: process.stdin,
|
|
2658
2806
|
output: process.stdout
|
|
@@ -2660,16 +2808,10 @@ function promptYesNo(question) {
|
|
|
2660
2808
|
rl.question(` ${question} ${pc5.dim("[Y/n]")} `, (answer) => {
|
|
2661
2809
|
rl.close();
|
|
2662
2810
|
const normalized = answer.trim().toLowerCase();
|
|
2663
|
-
|
|
2811
|
+
resolve5(normalized !== "n" && normalized !== "no");
|
|
2664
2812
|
});
|
|
2665
2813
|
});
|
|
2666
2814
|
}
|
|
2667
|
-
function detectPackageManager(cwd) {
|
|
2668
|
-
if (fs7.existsSync(path7.join(cwd, "bun.lock"))) return "bun";
|
|
2669
|
-
if (fs7.existsSync(path7.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2670
|
-
if (fs7.existsSync(path7.join(cwd, "yarn.lock"))) return "yarn";
|
|
2671
|
-
return "npm";
|
|
2672
|
-
}
|
|
2673
2815
|
function generateConfig(provider, include) {
|
|
2674
2816
|
const providerLine = provider === "none" ? " // provider: 'openai', // Uncomment and set when ready" : ` provider: "${provider}",`;
|
|
2675
2817
|
const modelMap = {
|
|
@@ -2749,7 +2891,7 @@ config({ path: ".env.local", override: true, quiet: true });
|
|
|
2749
2891
|
config({ path: ".env.development", override: true, quiet: true });
|
|
2750
2892
|
config({ path: ".env.development.local", override: true, quiet: true });
|
|
2751
2893
|
var program = new Command();
|
|
2752
|
-
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version("0.1.
|
|
2894
|
+
program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version("0.1.4");
|
|
2753
2895
|
registerScanCommand(program);
|
|
2754
2896
|
registerInitCommand(program);
|
|
2755
2897
|
registerCacheCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-a11y",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "AI-powered accessibility codemod for Next.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"ai": "^6.0.97",
|
|
39
39
|
"commander": "^12.0.0",
|
|
40
40
|
"dotenv": "^17.3.1",
|
|
41
|
+
"package-manager-detector": "^1.6.0",
|
|
41
42
|
"picocolors": "^1.1.0",
|
|
42
43
|
"ts-morph": "^24.0.0"
|
|
43
44
|
},
|