es-guard 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -162,7 +162,7 @@ npm run dev
162
162
 
163
163
  ```bash
164
164
  npm test
165
- npm run test:run
165
+ npm run test
166
166
  ```
167
167
 
168
168
  ### Linting
package/dist/cli.js CHANGED
@@ -4,6 +4,8 @@ import * as fs from "fs";
4
4
  import packageJson from "../package.json" with { type: "json" };
5
5
  import { checkCompatibility } from "./lib/checkCompatiblity.js";
6
6
  import { getBrowserTargetsFromString } from "./lib/getBrowserTargets.js";
7
+ import { detectTarget } from "./lib/detectTarget.js";
8
+ import path from "path";
7
9
  const version = packageJson.version;
8
10
  // Create the main program
9
11
  const program = new Command();
@@ -13,18 +15,30 @@ program
13
15
  .description("JavaScript Compatibility Checker - Check if your JavaScript code is compatible with target environments")
14
16
  .version(version)
15
17
  .argument("[directory]", "Directory to scan for JavaScript files", "dist")
16
- .option("-t, --target <version>", "Target ES version (2015, 2016, 2017, etc. or 6, 7, 8, etc. or 'latest')", "2015")
18
+ .option("-t, --target <version>", "Target ES version (2015, 2016, 2017, etc. or 6, 7, 8, etc. or 'latest'). If not specified, will auto-detect from project config files.")
17
19
  .option("-b, --browsers <targets>", "Browser targets for compatibility checking (optional: auto-determined from target)")
18
20
  .addHelpText("after", `
19
21
 
20
22
  Examples:
21
- es-guard # Check 'dist' directory with ES2015 (auto-determined browsers)
22
- es-guard build # Check 'build' directory with ES2015 (auto-determined browsers)
23
+ es-guard # Check 'dist' directory with auto-detected target
24
+ es-guard build # Check 'build' directory with auto-detected target
23
25
  es-guard -t 2020 build # Check 'build' directory with ES2020 (auto-determined browsers)
24
26
  es-guard -t 6 build # Check 'build' directory with ES6 (auto-determined browsers)
25
27
  es-guard -t latest build # Check 'build' directory with latest ES (auto-determined browsers)
26
28
  es-guard --target 2017 --browsers "> 0.5%, last 2 versions" dist
27
29
 
30
+ Auto-detection searches for ES target in:
31
+ - package.json (browserslist field)
32
+ - tsconfig.json (compilerOptions.target)
33
+ - babel.config.js (.babelrc) (@babel/preset-env targets)
34
+ - vite.config.js/ts (esbuild target)
35
+ - webpack.config.js/ts (target)
36
+
37
+ Auto-detection behavior:
38
+ - Searches in the directory being scanned first
39
+ - Falls back to current working directory if no config found
40
+ - Uses the first valid target found (package.json has highest priority)
41
+
28
42
  Browser targets use Browserslist format:
29
43
  - If not specified, browsers will be auto-determined from the ES target version
30
44
  - "> 1%, last 2 versions, not dead, ie 11" (for ES2015/ES6)
@@ -40,10 +54,12 @@ Exit codes:
40
54
  program.hook("preAction", (thisCommand) => {
41
55
  const options = thisCommand.opts();
42
56
  const target = options.target;
43
- // Validate ES target format - accept year format (YYYY), numeric format (N), or "latest"
44
- if (!/^\d{4}$/.test(target) && !/^\d+$/.test(target) && target !== "latest") {
45
- console.error(`Error: Invalid ES target: "${target}". Expected format: YYYY (e.g., 2015, 2020), numeric (e.g., 6, 11), or "latest"`);
46
- process.exit(1);
57
+ if (target) {
58
+ // Validate ES target format - accept year format (YYYY), numeric format (N), or "latest"
59
+ if (!/^\d{4}$/.test(target) && !/^\d+$/.test(target) && target !== "latest") {
60
+ console.error(`Error: Invalid ES target: "${target}". Expected format: YYYY (e.g., 2015, 2020), numeric (e.g., 6, 11), or "latest"`);
61
+ process.exit(1);
62
+ }
47
63
  }
48
64
  });
49
65
  // Main action
@@ -59,16 +75,67 @@ program.action(async (directory, options) => {
59
75
  console.error(`Error: "${directory}" is not a directory`);
60
76
  process.exit(1);
61
77
  }
78
+ // Determine target - use provided target or auto-detect
79
+ let target = options.target;
80
+ let targetSource = "specified";
81
+ if (!target) {
82
+ // Try to detect target from current working directory
83
+ const detectedResult = detectTarget(process.cwd());
84
+ if (detectedResult) {
85
+ target = detectedResult.target;
86
+ targetSource = `auto-detected from ${detectedResult.source}`;
87
+ }
88
+ else {
89
+ console.error("Error: No target specified and could not auto-detect from project configuration files.");
90
+ console.error("Please specify a target with --target or ensure your project has a valid configuration file.");
91
+ process.exit(1);
92
+ }
93
+ }
62
94
  // Determine browser targets
63
- const browserTargets = options.browsers || getBrowserTargetsFromString(options.target);
95
+ let browserTargets;
96
+ if (options.browsers) {
97
+ browserTargets = options.browsers;
98
+ }
99
+ else {
100
+ // If auto-detected from package.json or browserslist file, check for ES version strings
101
+ if (targetSource.startsWith("auto-detected from ")) {
102
+ const configFile = targetSource.replace("auto-detected from ", "");
103
+ let browserslistEntries = [];
104
+ if (configFile === "package.json") {
105
+ const pkgPath = path.join(process.cwd(), "package.json");
106
+ if (fs.existsSync(pkgPath)) {
107
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
108
+ if (pkg.browserslist) {
109
+ browserslistEntries = Array.isArray(pkg.browserslist) ? pkg.browserslist : [pkg.browserslist];
110
+ }
111
+ }
112
+ }
113
+ else if (configFile === ".browserslistrc" || configFile === ".browserslist") {
114
+ const blPath = path.join(process.cwd(), configFile);
115
+ if (fs.existsSync(blPath)) {
116
+ browserslistEntries = fs
117
+ .readFileSync(blPath, "utf-8")
118
+ .split(/\r?\n/)
119
+ .map((l) => l.trim())
120
+ .filter((l) => l && !l.startsWith("#"));
121
+ }
122
+ }
123
+ const esVersionRegex = /^es\d{1,4}$/i;
124
+ const invalidEntries = browserslistEntries.filter((entry) => esVersionRegex.test(entry));
125
+ if (invalidEntries.length > 0) {
126
+ console.warn(`Warning: Detected ES version string(s) in browserslist (${invalidEntries.join(", ")}). These are not valid Browserslist queries and will be ignored for browser compatibility checks.`);
127
+ }
128
+ }
129
+ browserTargets = getBrowserTargetsFromString(target);
130
+ }
64
131
  console.log(`🔍 ES-Guard v${version}`);
65
132
  console.log(`📁 Scanning directory: ${directory}`);
66
- console.log(`🎯 Target ES version: ${options.target}`);
133
+ console.log(`🎯 Target ES version: ${target} (${targetSource})`);
67
134
  console.log(`🌐 Browser targets: ${browserTargets}${options.browsers ? "" : " (auto-determined)"}`);
68
135
  console.log("");
69
136
  const violations = await checkCompatibility({
70
137
  dir: directory,
71
- target: options.target,
138
+ target: target,
72
139
  browsers: browserTargets,
73
140
  });
74
141
  if (violations.length > 0) {
@@ -7,6 +7,11 @@ export const checkCompatibility = async (config) => {
7
7
  console.log(`No JavaScript files found in directory: ${config.dir}`);
8
8
  return [];
9
9
  }
10
+ // Set BROWSERSLIST env variable to override Browserslist file detection
11
+ const originalBrowserslistEnv = process.env.BROWSERSLIST;
12
+ if (config.browsers) {
13
+ process.env.BROWSERSLIST = config.browsers;
14
+ }
10
15
  const eslint = new ESLint(createESLintConfig(config.target, config.browsers));
11
16
  const violations = [];
12
17
  for (const file of jsFiles) {
@@ -30,5 +35,12 @@ export const checkCompatibility = async (config) => {
30
35
  console.warn(`Warning: Could not lint file ${file}:`, error);
31
36
  }
32
37
  }
38
+ // Restore original BROWSERSLIST env variable
39
+ if (originalBrowserslistEnv !== undefined) {
40
+ process.env.BROWSERSLIST = originalBrowserslistEnv;
41
+ }
42
+ else {
43
+ delete process.env.BROWSERSLIST;
44
+ }
33
45
  return violations;
34
46
  };
@@ -0,0 +1,244 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ // Shared utilities for ES version parsing and conversion
4
+ /**
5
+ * Parse ES version from string (e.g., "es6", "es2020", "ES2015")
6
+ * Returns the year as a string, or null if not found
7
+ */
8
+ const parseESVersion = (str) => {
9
+ const esMatch = str.match(/es(\d+)/i);
10
+ if (esMatch) {
11
+ const esVersion = parseInt(esMatch[1]);
12
+ // If it's a 4-digit year (like 2020), use it directly
13
+ if (esVersion >= 2000) {
14
+ return esVersion.toString();
15
+ }
16
+ // Otherwise convert ES version to year: ES6=2015, ES7=2016, ES8=2017, etc.
17
+ return (2009 + esVersion).toString();
18
+ }
19
+ return null;
20
+ };
21
+ /**
22
+ * Common target mapping for TypeScript, Vite, and Webpack configs
23
+ */
24
+ const TARGET_MAP = {
25
+ // TypeScript targets
26
+ ES2022: "2022",
27
+ ES2021: "2021",
28
+ ES2020: "2020",
29
+ ES2019: "2019",
30
+ ES2018: "2018",
31
+ ES2017: "2017",
32
+ ES2016: "2016",
33
+ ES2015: "2015",
34
+ ES6: "2015",
35
+ ES5: "2009",
36
+ ES3: "1999",
37
+ // Vite/Webpack targets (lowercase)
38
+ es2022: "2022",
39
+ es2021: "2021",
40
+ es2020: "2020",
41
+ es2019: "2019",
42
+ es2018: "2018",
43
+ es2017: "2017",
44
+ es2016: "2016",
45
+ es2015: "2015",
46
+ es6: "2015",
47
+ es5: "2009",
48
+ };
49
+ /**
50
+ * Helper to read and parse JSON file safely
51
+ */
52
+ const readJsonFile = (filePath) => {
53
+ const content = fs.readFileSync(filePath, "utf-8");
54
+ return JSON.parse(content);
55
+ };
56
+ /**
57
+ * Type guard for package.json structure
58
+ */
59
+ const isPackageJson = (obj) => {
60
+ return typeof obj === "object" && obj !== null && "browserslist" in obj;
61
+ };
62
+ /**
63
+ * Type guard for tsconfig.json structure
64
+ */
65
+ const isTsConfig = (obj) => {
66
+ return typeof obj === "object" && obj !== null && "compilerOptions" in obj;
67
+ };
68
+ /**
69
+ * Type guard for .babelrc structure
70
+ */
71
+ const isBabelRc = (obj) => {
72
+ return typeof obj === "object" && obj !== null && "presets" in obj;
73
+ };
74
+ /**
75
+ * Helper to read text file safely
76
+ */
77
+ const readTextFile = (filePath) => {
78
+ return fs.readFileSync(filePath, "utf-8");
79
+ };
80
+ /**
81
+ * Detects ES target from common frontend project configuration files.
82
+ * Searches in order of preference: package.json, .browserslistrc/.browserslist, tsconfig.json, babel.config.js, .babelrc, vite.config.js, webpack.config.js
83
+ * Returns an object with the detected target and the source file name, or null if not found
84
+ */
85
+ export const detectTarget = (cwd = process.cwd()) => {
86
+ const configFiles = [
87
+ { name: "package.json", parser: parsePackageJson },
88
+ { name: ".browserslistrc", parser: parseBrowserslistFile },
89
+ { name: ".browserslist", parser: parseBrowserslistFile },
90
+ { name: "tsconfig.json", parser: parseTsConfig },
91
+ { name: "babel.config.js", parser: parseBabelConfig },
92
+ { name: ".babelrc", parser: parseBabelRc },
93
+ { name: "vite.config.js", parser: parseViteConfig },
94
+ { name: "vite.config.ts", parser: parseViteConfig },
95
+ { name: "webpack.config.js", parser: parseWebpackConfig },
96
+ { name: "webpack.config.ts", parser: parseWebpackConfig },
97
+ ];
98
+ for (const config of configFiles) {
99
+ const filePath = path.join(cwd, config.name);
100
+ if (fs.existsSync(filePath)) {
101
+ try {
102
+ const target = config.parser(filePath);
103
+ if (target) {
104
+ return { target, source: config.name };
105
+ }
106
+ }
107
+ catch (error) {
108
+ console.warn(`Error parsing ${config.name}:`, error);
109
+ // Continue to next config file if parsing fails
110
+ continue;
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ };
116
+ /**
117
+ * Parse package.json for ES target in browserslist
118
+ */
119
+ const parsePackageJson = (filePath) => {
120
+ const pkg = readJsonFile(filePath);
121
+ if (!isPackageJson(pkg)) {
122
+ console.warn(`Warning: ${filePath} does not look like a valid package.json (missing or invalid browserslist field).`);
123
+ return null;
124
+ }
125
+ // Check for browserslist field
126
+ if (pkg.browserslist) {
127
+ const browserslist = Array.isArray(pkg.browserslist) ? pkg.browserslist : [pkg.browserslist];
128
+ // Look for ES target in browserslist
129
+ for (const browser of browserslist) {
130
+ if (typeof browser === "string") {
131
+ const target = parseESVersion(browser);
132
+ if (target) {
133
+ return target;
134
+ }
135
+ }
136
+ }
137
+ }
138
+ // Note: engines.node is for development/build tools, not client-side targets
139
+ // So we don't use it for auto-detection
140
+ return null;
141
+ };
142
+ /**
143
+ * Parse tsconfig.json for target
144
+ */
145
+ const parseTsConfig = (filePath) => {
146
+ const config = readJsonFile(filePath);
147
+ if (!isTsConfig(config)) {
148
+ console.warn(`Warning: ${filePath} does not look like a valid tsconfig.json (missing or invalid compilerOptions field).`);
149
+ return null;
150
+ }
151
+ if (config.compilerOptions?.target) {
152
+ const target = config.compilerOptions.target;
153
+ return TARGET_MAP[target] || null;
154
+ }
155
+ return null;
156
+ };
157
+ /**
158
+ * Parse babel.config.js for preset-env target
159
+ */
160
+ const parseBabelConfig = (filePath) => {
161
+ const content = readTextFile(filePath);
162
+ // Look for @babel/preset-env configuration
163
+ const presetEnvMatch = content.match(/@babel\/preset-env.*?targets.*?(\{[^}]*\})/s);
164
+ if (presetEnvMatch) {
165
+ const targetsStr = presetEnvMatch[1];
166
+ // Look for browsers or esmodules target
167
+ const browsersMatch = targetsStr.match(/browsers.*?\[(.*?)\]/);
168
+ if (browsersMatch) {
169
+ const browsers = browsersMatch[1];
170
+ return parseESVersion(browsers);
171
+ }
172
+ }
173
+ return null;
174
+ };
175
+ /**
176
+ * Parse .babelrc for preset-env target
177
+ */
178
+ const parseBabelRc = (filePath) => {
179
+ const config = readJsonFile(filePath);
180
+ if (!isBabelRc(config)) {
181
+ console.warn(`Warning: ${filePath} does not look like a valid .babelrc (missing or invalid presets field).`);
182
+ return null;
183
+ }
184
+ if (config.presets) {
185
+ for (const preset of config.presets) {
186
+ if (Array.isArray(preset) && preset[0] === "@babel/preset-env") {
187
+ const options = preset[1];
188
+ if (options?.targets?.browsers) {
189
+ const browsers = options.targets.browsers;
190
+ for (const browser of browsers) {
191
+ const target = parseESVersion(browser);
192
+ if (target) {
193
+ return target;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ return null;
201
+ };
202
+ /**
203
+ * Parse vite.config.js/ts for target
204
+ */
205
+ const parseViteConfig = (filePath) => {
206
+ const content = readTextFile(filePath);
207
+ // Look for esbuild target - more flexible pattern
208
+ const esbuildMatch = content.match(/esbuild\s*:\s*\{[^}]*target\s*:\s*['"`]([^'"`]+)['"`]/s);
209
+ if (esbuildMatch) {
210
+ const target = esbuildMatch[1];
211
+ return TARGET_MAP[target] || null;
212
+ }
213
+ return null;
214
+ };
215
+ /**
216
+ * Parse webpack.config.js/ts for target
217
+ */
218
+ const parseWebpackConfig = (filePath) => {
219
+ const content = readTextFile(filePath);
220
+ // Look for target configuration
221
+ const targetMatch = content.match(/target.*?['"`]([^'"`]+)['"`]/);
222
+ if (targetMatch) {
223
+ const target = targetMatch[1];
224
+ return TARGET_MAP[target] || null;
225
+ }
226
+ return null;
227
+ };
228
+ /**
229
+ * Parse .browserslistrc or .browserslist file for ES target
230
+ */
231
+ const parseBrowserslistFile = (filePath) => {
232
+ const content = readTextFile(filePath);
233
+ const lines = content
234
+ .split(/\r?\n/)
235
+ .map((line) => line.trim())
236
+ .filter((line) => line && !line.startsWith("#"));
237
+ for (const browser of lines) {
238
+ const target = parseESVersion(browser);
239
+ if (target) {
240
+ return target;
241
+ }
242
+ }
243
+ return null;
244
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "es-guard",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A tool to check JavaScript compatibility with target environments",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -14,8 +14,10 @@
14
14
  "build": "tsc",
15
15
  "dev": "tsc --watch",
16
16
  "start": "node dist/cli.js",
17
- "test": "npm run build && vitest",
18
- "test:run": "npm run build && vitest run",
17
+ "test:dev": "npm run build && vitest",
18
+ "test": "npm run build && vitest run",
19
+ "test:junit": "npm run build && vitest run --reporter=junit --outputFile=test-report.junit.xml",
20
+ "test:junit:coverage": "npm run build && vitest run --reporter=junit --outputFile=test-report.junit.xml --coverage",
19
21
  "coverage": "vitest run --coverage",
20
22
  "coverage:check": "vitest run --coverage --reporter=verbose",
21
23
  "lint": "eslint src/**/*.ts",
@@ -72,5 +74,10 @@
72
74
  "url": "https://github.com/mkayander/es-guard/issues"
73
75
  },
74
76
  "homepage": "https://github.com/mkayander/es-guard#readme",
75
- "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
77
+ "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184",
78
+ "browserslist": [
79
+ "es2020",
80
+ "> 1%",
81
+ "last 2 versions"
82
+ ]
76
83
  }