next-a11y 0.1.4 → 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 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,13 @@ 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 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((resolve4, reject) => {
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 resolve4(files);
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() === 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);
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);
2003
+ }
2004
+ }
2005
+ }
2006
+ return void 0;
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]);
1969
2084
  }
1970
2085
  }
1971
2086
  }
1972
2087
  return void 0;
1973
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((resolve4, reject) => {
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(resolve4).catch(reject);
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", () => resolve4(Buffer.concat(chunks)));
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 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);
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,
@@ -2505,7 +2636,7 @@ async function interactiveReview(violations, onAccept) {
2505
2636
  return { applied, skipped };
2506
2637
  }
2507
2638
  function promptAction() {
2508
- return new Promise((resolve4) => {
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") resolve4("no");
2520
- else if (normalized === "s" || normalized === "skip") resolve4("skip");
2521
- else if (normalized === "q" || normalized === "quit") resolve4("quit");
2522
- else resolve4("yes");
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,8 +2746,8 @@ function registerScanCommand(program2) {
2600
2746
  }
2601
2747
 
2602
2748
  // src/cli/init-command.ts
2603
- var fs7 = __toESM(require("fs"));
2604
- var path7 = __toESM(require("path"));
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"));
@@ -2611,15 +2757,15 @@ function registerInitCommand(program2) {
2611
2757
  console.log(import_picocolors5.default.bold("\n next-a11y v0.1.4 \u2014 Setup\n"));
2612
2758
  const options = await promptInitOptions();
2613
2759
  const cwd = process.cwd();
2614
- const hasAppDir = fs7.existsSync(path7.join(cwd, "app"));
2615
- const hasSrcDir = fs7.existsSync(path7.join(cwd, "src"));
2760
+ const hasAppDir = fs8.existsSync(path8.join(cwd, "app"));
2761
+ const hasSrcDir = fs8.existsSync(path8.join(cwd, "src"));
2616
2762
  const include = [];
2617
2763
  if (hasSrcDir) include.push("src/**/*.{tsx,jsx}");
2618
2764
  if (hasAppDir) include.push("app/**/*.{tsx,jsx}");
2619
2765
  if (include.length === 0) include.push("**/*.{tsx,jsx}");
2620
2766
  const configContent = generateConfig(options.provider, include);
2621
- const configPath = path7.join(cwd, "a11y.config.ts");
2622
- fs7.writeFileSync(configPath, configContent);
2767
+ const configPath = path8.join(cwd, "a11y.config.ts");
2768
+ fs8.writeFileSync(configPath, configContent);
2623
2769
  console.log(import_picocolors5.default.green(" Created a11y.config.ts"));
2624
2770
  if (options.provider !== "none" && options.installDep) {
2625
2771
  const pkgMap = {
@@ -2643,14 +2789,14 @@ function registerInitCommand(program2) {
2643
2789
  }
2644
2790
  }
2645
2791
  if (options.addGitignore) {
2646
- const gitignorePath = path7.join(cwd, ".gitignore");
2792
+ const gitignorePath = path8.join(cwd, ".gitignore");
2647
2793
  let content = "";
2648
- if (fs7.existsSync(gitignorePath)) {
2649
- content = fs7.readFileSync(gitignorePath, "utf-8");
2794
+ if (fs8.existsSync(gitignorePath)) {
2795
+ content = fs8.readFileSync(gitignorePath, "utf-8");
2650
2796
  }
2651
2797
  if (!content.includes(".a11y-cache")) {
2652
2798
  const newline = content.endsWith("\n") ? "" : "\n";
2653
- fs7.appendFileSync(gitignorePath, `${newline}.a11y-cache
2799
+ fs8.appendFileSync(gitignorePath, `${newline}.a11y-cache
2654
2800
  `);
2655
2801
  console.log(import_picocolors5.default.green(" Updated .gitignore"));
2656
2802
  }
@@ -2691,7 +2837,7 @@ async function promptInitOptions() {
2691
2837
  return { provider, installDep, addGitignore };
2692
2838
  }
2693
2839
  function promptSelect(question, options) {
2694
- return new Promise((resolve4) => {
2840
+ return new Promise((resolve5) => {
2695
2841
  const rl = readline2.createInterface({
2696
2842
  input: process.stdin,
2697
2843
  output: process.stdout
@@ -2704,15 +2850,15 @@ function promptSelect(question, options) {
2704
2850
  rl.close();
2705
2851
  const idx = parseInt(answer.trim()) - 1;
2706
2852
  if (idx >= 0 && idx < options.length) {
2707
- resolve4(options[idx].value);
2853
+ resolve5(options[idx].value);
2708
2854
  } else {
2709
- resolve4(options[0].value);
2855
+ resolve5(options[0].value);
2710
2856
  }
2711
2857
  });
2712
2858
  });
2713
2859
  }
2714
2860
  function promptYesNo(question) {
2715
- return new Promise((resolve4) => {
2861
+ return new Promise((resolve5) => {
2716
2862
  const rl = readline2.createInterface({
2717
2863
  input: process.stdin,
2718
2864
  output: process.stdout
@@ -2720,7 +2866,7 @@ function promptYesNo(question) {
2720
2866
  rl.question(` ${question} ${import_picocolors5.default.dim("[Y/n]")} `, (answer) => {
2721
2867
  rl.close();
2722
2868
  const normalized = answer.trim().toLowerCase();
2723
- resolve4(normalized !== "n" && normalized !== "no");
2869
+ resolve5(normalized !== "n" && normalized !== "no");
2724
2870
  });
2725
2871
  });
2726
2872
  }
@@ -2798,10 +2944,10 @@ function formatBytes(bytes) {
2798
2944
  }
2799
2945
 
2800
2946
  // 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 });
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 });
2805
2951
  var program = new import_commander.Command();
2806
2952
  program.name("next-a11y").description("AI-powered accessibility codemod for Next.js").version("0.1.4");
2807
2953
  registerScanCommand(program);
@@ -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((resolve4, reject) => {
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 resolve4(files);
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() === 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);
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);
1945
+ }
1946
+ }
1947
+ }
1948
+ return void 0;
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);
1911
2000
  }
1912
2001
  }
2002
+ } catch {
1913
2003
  }
1914
2004
  return void 0;
1915
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((resolve4, reject) => {
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(resolve4).catch(reject);
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", () => resolve4(Buffer.concat(chunks)));
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 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);
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,
@@ -2447,7 +2578,7 @@ async function interactiveReview(violations, onAccept) {
2447
2578
  return { applied, skipped };
2448
2579
  }
2449
2580
  function promptAction() {
2450
- return new Promise((resolve4) => {
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") resolve4("no");
2462
- else if (normalized === "s" || normalized === "skip") resolve4("skip");
2463
- else if (normalized === "q" || normalized === "quit") resolve4("quit");
2464
- else resolve4("yes");
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,8 +2688,8 @@ function registerScanCommand(program2) {
2542
2688
  }
2543
2689
 
2544
2690
  // src/cli/init-command.ts
2545
- import * as fs7 from "fs";
2546
- import * as path7 from "path";
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";
@@ -2553,15 +2699,15 @@ function registerInitCommand(program2) {
2553
2699
  console.log(pc5.bold("\n next-a11y v0.1.4 \u2014 Setup\n"));
2554
2700
  const options = await promptInitOptions();
2555
2701
  const cwd = process.cwd();
2556
- const hasAppDir = fs7.existsSync(path7.join(cwd, "app"));
2557
- const hasSrcDir = fs7.existsSync(path7.join(cwd, "src"));
2702
+ const hasAppDir = fs8.existsSync(path8.join(cwd, "app"));
2703
+ const hasSrcDir = fs8.existsSync(path8.join(cwd, "src"));
2558
2704
  const include = [];
2559
2705
  if (hasSrcDir) include.push("src/**/*.{tsx,jsx}");
2560
2706
  if (hasAppDir) include.push("app/**/*.{tsx,jsx}");
2561
2707
  if (include.length === 0) include.push("**/*.{tsx,jsx}");
2562
2708
  const configContent = generateConfig(options.provider, include);
2563
- const configPath = path7.join(cwd, "a11y.config.ts");
2564
- fs7.writeFileSync(configPath, configContent);
2709
+ const configPath = path8.join(cwd, "a11y.config.ts");
2710
+ fs8.writeFileSync(configPath, configContent);
2565
2711
  console.log(pc5.green(" Created a11y.config.ts"));
2566
2712
  if (options.provider !== "none" && options.installDep) {
2567
2713
  const pkgMap = {
@@ -2585,14 +2731,14 @@ function registerInitCommand(program2) {
2585
2731
  }
2586
2732
  }
2587
2733
  if (options.addGitignore) {
2588
- const gitignorePath = path7.join(cwd, ".gitignore");
2734
+ const gitignorePath = path8.join(cwd, ".gitignore");
2589
2735
  let content = "";
2590
- if (fs7.existsSync(gitignorePath)) {
2591
- content = fs7.readFileSync(gitignorePath, "utf-8");
2736
+ if (fs8.existsSync(gitignorePath)) {
2737
+ content = fs8.readFileSync(gitignorePath, "utf-8");
2592
2738
  }
2593
2739
  if (!content.includes(".a11y-cache")) {
2594
2740
  const newline = content.endsWith("\n") ? "" : "\n";
2595
- fs7.appendFileSync(gitignorePath, `${newline}.a11y-cache
2741
+ fs8.appendFileSync(gitignorePath, `${newline}.a11y-cache
2596
2742
  `);
2597
2743
  console.log(pc5.green(" Updated .gitignore"));
2598
2744
  }
@@ -2633,7 +2779,7 @@ async function promptInitOptions() {
2633
2779
  return { provider, installDep, addGitignore };
2634
2780
  }
2635
2781
  function promptSelect(question, options) {
2636
- return new Promise((resolve4) => {
2782
+ return new Promise((resolve5) => {
2637
2783
  const rl = readline2.createInterface({
2638
2784
  input: process.stdin,
2639
2785
  output: process.stdout
@@ -2646,15 +2792,15 @@ function promptSelect(question, options) {
2646
2792
  rl.close();
2647
2793
  const idx = parseInt(answer.trim()) - 1;
2648
2794
  if (idx >= 0 && idx < options.length) {
2649
- resolve4(options[idx].value);
2795
+ resolve5(options[idx].value);
2650
2796
  } else {
2651
- resolve4(options[0].value);
2797
+ resolve5(options[0].value);
2652
2798
  }
2653
2799
  });
2654
2800
  });
2655
2801
  }
2656
2802
  function promptYesNo(question) {
2657
- return new Promise((resolve4) => {
2803
+ return new Promise((resolve5) => {
2658
2804
  const rl = readline2.createInterface({
2659
2805
  input: process.stdin,
2660
2806
  output: process.stdout
@@ -2662,7 +2808,7 @@ function promptYesNo(question) {
2662
2808
  rl.question(` ${question} ${pc5.dim("[Y/n]")} `, (answer) => {
2663
2809
  rl.close();
2664
2810
  const normalized = answer.trim().toLowerCase();
2665
- resolve4(normalized !== "n" && normalized !== "no");
2811
+ resolve5(normalized !== "n" && normalized !== "no");
2666
2812
  });
2667
2813
  });
2668
2814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-a11y",
3
- "version": "0.1.4",
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",