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 CHANGED
@@ -1,3 +1,6 @@
1
+ > [!IMPORTANT]
2
+ > THIS PACKAGE IS IN ACTIVE DEVELOPMENT
3
+
1
4
  # next-a11y
2
5
 
3
6
  **Finds accessibility violations in your Next.js source code. Writes the fix.**
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 import_dotenv = require("dotenv");
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((resolve4, reject) => {
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 resolve4(files);
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 pkg = PROVIDER_PACKAGES[provider];
1710
+ const pkg2 = PROVIDER_PACKAGES[provider];
1696
1711
  let mod;
1697
1712
  try {
1698
- mod = require(pkg);
1713
+ mod = require(pkg2);
1699
1714
  } catch {
1700
1715
  throw new Error(
1701
- `AI provider "${provider}" requires the "${pkg}" package.
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() === importName) {
1965
- const moduleSpecifier = imp.getModuleSpecifierValue();
1966
- if (moduleSpecifier.endsWith(".png") || moduleSpecifier.endsWith(".jpg") || moduleSpecifier.endsWith(".jpeg") || moduleSpecifier.endsWith(".webp") || moduleSpecifier.endsWith(".gif") || moduleSpecifier.endsWith(".svg") || moduleSpecifier.endsWith(".avif")) {
1967
- const filePath = file.getFilePath();
1968
- return path5.resolve(path5.dirname(filePath), moduleSpecifier);
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((resolve4, reject) => {
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(resolve4).catch(reject);
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", () => resolve4(Buffer.concat(chunks)));
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 path8 = require("path");
2171
- const fs8 = require("fs");
2172
- let dir = path8.dirname(filePath);
2173
- while (dir !== path8.dirname(dir)) {
2174
- if (fs8.existsSync(path8.join(dir, "package.json"))) return dir;
2175
- if (fs8.existsSync(path8.join(dir, "next.config.js"))) return dir;
2176
- if (fs8.existsSync(path8.join(dir, "next.config.mjs"))) return dir;
2177
- if (fs8.existsSync(path8.join(dir, "next.config.ts"))) return dir;
2178
- dir = path8.dirname(dir);
2179
- }
2180
- return path8.dirname(filePath);
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 v0.1.4`));
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((resolve4) => {
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") resolve4("no");
2520
- else if (normalized === "s" || normalized === "skip") resolve4("skip");
2521
- else if (normalized === "q" || normalized === "quit") resolve4("quit");
2522
- else resolve4("yes");
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 fs7 = __toESM(require("fs"));
2604
- var path7 = __toESM(require("path"));
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
- console.log(import_picocolors5.default.bold("\n next-a11y v0.1.4 \u2014 Setup\n"));
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 = fs7.existsSync(path7.join(cwd, "app"));
2615
- const hasSrcDir = fs7.existsSync(path7.join(cwd, "src"));
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 = path7.join(cwd, "a11y.config.ts");
2622
- fs7.writeFileSync(configPath, configContent);
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 pkg = pkgMap[options.provider];
2632
- if (pkg) {
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", [pkg]);
2635
- const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${pkg}`;
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 ${pkg}`));
2795
+ console.log(import_picocolors5.default.green(` Installed ${pkg2}`));
2640
2796
  } catch {
2641
- console.log(import_picocolors5.default.yellow(` Failed to install ${pkg}. Run manually: ${installCmd}`));
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 = path7.join(cwd, ".gitignore");
2802
+ const gitignorePath = path8.join(cwd, ".gitignore");
2647
2803
  let content = "";
2648
- if (fs7.existsSync(gitignorePath)) {
2649
- content = fs7.readFileSync(gitignorePath, "utf-8");
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
- fs7.appendFileSync(gitignorePath, `${newline}.a11y-cache
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((resolve4) => {
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
- resolve4(options[idx].value);
2863
+ resolve6(options[idx].value);
2708
2864
  } else {
2709
- resolve4(options[0].value);
2865
+ resolve6(options[0].value);
2710
2866
  }
2711
2867
  });
2712
2868
  });
2713
2869
  }
2714
2870
  function promptYesNo(question) {
2715
- return new Promise((resolve4) => {
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
- resolve4(normalized !== "n" && normalized !== "no");
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
- (0, import_dotenv.config)({ path: ".env", override: false, quiet: true });
2802
- (0, import_dotenv.config)({ path: ".env.local", override: true, quiet: true });
2803
- (0, import_dotenv.config)({ path: ".env.development", override: true, quiet: true });
2804
- (0, import_dotenv.config)({ path: ".env.development.local", override: true, quiet: true });
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("0.1.4");
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);
@@ -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((resolve4, reject) => {
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 resolve4(files);
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 pkg = PROVIDER_PACKAGES[provider];
1652
+ const pkg2 = PROVIDER_PACKAGES[provider];
1638
1653
  let mod;
1639
1654
  try {
1640
- mod = __require(pkg);
1655
+ mod = __require(pkg2);
1641
1656
  } catch {
1642
1657
  throw new Error(
1643
- `AI provider "${provider}" requires the "${pkg}" package.
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() === importName) {
1907
- const moduleSpecifier = imp.getModuleSpecifierValue();
1908
- if (moduleSpecifier.endsWith(".png") || moduleSpecifier.endsWith(".jpg") || moduleSpecifier.endsWith(".jpeg") || moduleSpecifier.endsWith(".webp") || moduleSpecifier.endsWith(".gif") || moduleSpecifier.endsWith(".svg") || moduleSpecifier.endsWith(".avif")) {
1909
- const filePath = file.getFilePath();
1910
- return path5.resolve(path5.dirname(filePath), moduleSpecifier);
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((resolve4, reject) => {
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(resolve4).catch(reject);
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", () => resolve4(Buffer.concat(chunks)));
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 path8 = __require("path");
2113
- const fs8 = __require("fs");
2114
- let dir = path8.dirname(filePath);
2115
- while (dir !== path8.dirname(dir)) {
2116
- if (fs8.existsSync(path8.join(dir, "package.json"))) return dir;
2117
- if (fs8.existsSync(path8.join(dir, "next.config.js"))) return dir;
2118
- if (fs8.existsSync(path8.join(dir, "next.config.mjs"))) return dir;
2119
- if (fs8.existsSync(path8.join(dir, "next.config.ts"))) return dir;
2120
- dir = path8.dirname(dir);
2121
- }
2122
- return path8.dirname(filePath);
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 v0.1.4`));
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((resolve4) => {
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") resolve4("no");
2462
- else if (normalized === "s" || normalized === "skip") resolve4("skip");
2463
- else if (normalized === "q" || normalized === "quit") resolve4("quit");
2464
- else resolve4("yes");
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 fs7 from "fs";
2546
- import * as path7 from "path";
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
- console.log(pc5.bold("\n next-a11y v0.1.4 \u2014 Setup\n"));
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 = fs7.existsSync(path7.join(cwd, "app"));
2557
- const hasSrcDir = fs7.existsSync(path7.join(cwd, "src"));
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 = path7.join(cwd, "a11y.config.ts");
2564
- fs7.writeFileSync(configPath, configContent);
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 pkg = pkgMap[options.provider];
2574
- if (pkg) {
2729
+ const pkg2 = pkgMap[options.provider];
2730
+ if (pkg2) {
2575
2731
  const pm = await detectPM({ cwd });
2576
- const resolved = resolveCommand(pm?.agent ?? "npm", "add", [pkg]);
2577
- const installCmd = resolved ? `${resolved.command} ${resolved.args.join(" ")}` : `npm install ${pkg}`;
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 ${pkg}`));
2737
+ console.log(pc5.green(` Installed ${pkg2}`));
2582
2738
  } catch {
2583
- console.log(pc5.yellow(` Failed to install ${pkg}. Run manually: ${installCmd}`));
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 = path7.join(cwd, ".gitignore");
2744
+ const gitignorePath = path8.join(cwd, ".gitignore");
2589
2745
  let content = "";
2590
- if (fs7.existsSync(gitignorePath)) {
2591
- content = fs7.readFileSync(gitignorePath, "utf-8");
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
- fs7.appendFileSync(gitignorePath, `${newline}.a11y-cache
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((resolve4) => {
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
- resolve4(options[idx].value);
2805
+ resolve6(options[idx].value);
2650
2806
  } else {
2651
- resolve4(options[0].value);
2807
+ resolve6(options[0].value);
2652
2808
  }
2653
2809
  });
2654
2810
  });
2655
2811
  }
2656
2812
  function promptYesNo(question) {
2657
- return new Promise((resolve4) => {
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
- resolve4(normalized !== "n" && normalized !== "no");
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("0.1.4");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-a11y",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "AI-powered accessibility codemod for Next.js",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",