es-guard 1.2.0 → 1.3.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
@@ -2,16 +2,16 @@
2
2
 
3
3
  [![codecov](https://codecov.io/gh/mkayander/es-guard/branch/main/graph/badge.svg)](https://codecov.io/gh/mkayander/es-guard)
4
4
 
5
- A TypeScript-based tool to check JavaScript compatibility with target environments using ESLint.
5
+ A powerful TypeScript-based tool that ensures your JavaScript code is compatible with target environments using ESLint.
6
6
 
7
7
  ## Features
8
8
 
9
- - 🔍 **ES Version Compatibility**: Check if your JavaScript code is compatible with specific ES versions (ES2015, ES2016, ES2017, etc.)
10
- - 🌐 **Browser Compatibility**: Verify browser support using eslint-plugin-compat
11
- - 🎯 **Auto Browser Detection**: Automatically determine browser targets from ES version (optional)
12
- - 📁 **Directory Scanning**: Automatically scan directories for JavaScript files
13
- - 🎯 **GitHub Actions Ready**: Works seamlessly with GitHub Actions
14
- - 📦 **NPM Package**: Install globally or use as a dependency
9
+ - 🔍 **ES Version Validation**: Verify your JavaScript code compatibility with specific ES versions (ES2015, ES2016, ES2017, and beyond)
10
+ - 🌐 **Browser Support Verification**: Validate browser compatibility using eslint-plugin-compat
11
+ - 🎯 **Smart Browser Detection**: Automatically determine browser targets from ES version (optional)
12
+ - 📁 **Comprehensive Directory Scanning**: Effortlessly scan directories for JavaScript files
13
+ - 🚀 **GitHub Actions Integration**: Seamlessly integrate with GitHub Actions workflows
14
+ - 📦 **Flexible Installation**: Install globally or use as a project dependency
15
15
 
16
16
  ## Installation
17
17
 
@@ -38,13 +38,13 @@ npm run build
38
38
 
39
39
  ## Usage
40
40
 
41
- ### Command Line
41
+ ### Command Line Interface
42
42
 
43
43
  ```bash
44
- # Basic usage with defaults (auto-determined browsers)
44
+ # Basic usage with auto-detected browsers
45
45
  es-guard
46
46
 
47
- # Check specific directory
47
+ # Validate specific directory
48
48
  es-guard build
49
49
 
50
50
  # Specify target ES version (year format)
@@ -59,10 +59,10 @@ es-guard -t latest build
59
59
  # Specify custom browser targets
60
60
  es-guard --browsers "> 0.5%, last 2 versions, Firefox ESR, not dead" dist
61
61
 
62
- # Show help
62
+ # Display help information
63
63
  es-guard --help
64
64
 
65
- # Show version
65
+ # Show version information
66
66
  es-guard --version
67
67
  ```
68
68
 
@@ -71,13 +71,13 @@ es-guard --version
71
71
  ```typescript
72
72
  import { checkCompatibility } from "es-guard";
73
73
 
74
- // With auto-determined browsers
74
+ // With auto-detected browsers
75
75
  const violations = await checkCompatibility({
76
76
  dir: "dist",
77
77
  target: "2015",
78
78
  });
79
79
 
80
- // With custom browsers
80
+ // With custom browser specifications
81
81
  const violations = await checkCompatibility({
82
82
  dir: "dist",
83
83
  target: "2015",
@@ -85,10 +85,10 @@ const violations = await checkCompatibility({
85
85
  });
86
86
  ```
87
87
 
88
- ### GitHub Actions
88
+ ### GitHub Actions Integration
89
89
 
90
90
  ```yaml
91
- name: Check Compatibility
91
+ name: Validate Compatibility
92
92
  on: [push, pull_request]
93
93
 
94
94
  jobs:
@@ -108,15 +108,15 @@ jobs:
108
108
 
109
109
  ### Parameters
110
110
 
111
- | Parameter | Description | Default | Required |
112
- | ---------- | ------------------------------------------ | --------------------------- | -------- |
113
- | `path` | Directory to scan for JavaScript files | `dist` | No |
114
- | `target` | Target ES version | `2015` | Yes |
115
- | `browsers` | Browser targets for compatibility checking | Auto-determined from target | No |
111
+ | Parameter | Description | Default | Required |
112
+ | ---------- | ------------------------------------------ | ------------------------- | -------- |
113
+ | `path` | Directory to scan for JavaScript files | `dist` | No |
114
+ | `target` | Target ES version | `2015` | Yes |
115
+ | `browsers` | Browser targets for compatibility checking | Auto-detected from target | No |
116
116
 
117
117
  ### ES Target Versions
118
118
 
119
- The `target` parameter accepts multiple formats:
119
+ The `target` parameter supports multiple formats:
120
120
 
121
121
  - **Year format**: `2015`, `2016`, `2017`, etc.
122
122
  - **Numeric format**: `6` (ES2015), `7` (ES2016), `11` (ES2020), etc.
@@ -124,17 +124,17 @@ The `target` parameter accepts multiple formats:
124
124
 
125
125
  ### Browser Targets
126
126
 
127
- The `browsers` parameter uses the same format as Browserslist. If not specified, browsers will be automatically determined based on the ES target:
127
+ The `browsers` parameter follows the Browserslist format. When not specified, browsers are automatically determined based on the ES target:
128
128
 
129
129
  - **ES2015/ES6**: `> 1%, last 2 versions, not dead, ie 11`
130
130
  - **ES2016-2017/ES7-8**: `> 1%, last 2 versions, not dead, not ie 11`
131
131
  - **ES2018-2019/ES9-10**: `> 1%, last 2 versions, not dead, not ie 11, not op_mini all`
132
132
  - **ES2020+/ES11+**: `> 1%, last 2 versions, not dead, not ie 11, not op_mini all, not android < 67`
133
133
 
134
- Custom browser targets examples:
134
+ Custom browser target examples:
135
135
 
136
- - `> 1%, last 2 versions, not dead, ie 11` - Modern browsers + IE11
137
- - `> 0.5%, last 2 versions, Firefox ESR, not dead` - Broader support
136
+ - `> 1%, last 2 versions, not dead, ie 11` - Modern browsers with IE11 support
137
+ - `> 0.5%, last 2 versions, Firefox ESR, not dead` - Broader browser support
138
138
  - `defaults` - Default Browserslist targets
139
139
  - `last 1 version` - Latest version of each browser
140
140
 
@@ -186,8 +186,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
186
186
 
187
187
  1. Fork the repository
188
188
  2. Create a feature branch
189
- 3. Make your changes
190
- 4. Add tests if applicable
189
+ 3. Implement your changes
190
+ 4. Add tests where applicable
191
191
  5. Run the linter: `npm run lint`
192
192
  6. Submit a pull request
193
193
 
@@ -1,6 +1,28 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  // Shared utilities for ES version parsing and conversion
4
+ const CONFIG_FILE_NAMES = [
5
+ "package.json",
6
+ ".browserslistrc",
7
+ ".browserslist",
8
+ "tsconfig.json",
9
+ "babel.config.js",
10
+ "babel.config.cjs",
11
+ "babel.config.mjs",
12
+ ".babelrc",
13
+ "vite.config.js",
14
+ "vite.config.ts",
15
+ "vite.config.cjs",
16
+ "vite.config.mjs",
17
+ "webpack.config.js",
18
+ "webpack.config.ts",
19
+ "webpack.config.cjs",
20
+ "webpack.config.mjs",
21
+ "next.config.js",
22
+ "next.config.ts",
23
+ "next.config.cjs",
24
+ "next.config.mjs",
25
+ ];
4
26
  /**
5
27
  * Parse ES version from string (e.g., "es6", "es2020", "ES2015")
6
28
  * Returns the year as a string, or null if not found
@@ -53,11 +75,42 @@ const readJsonFile = (filePath) => {
53
75
  const content = fs.readFileSync(filePath, "utf-8");
54
76
  return JSON.parse(content);
55
77
  };
78
+ /**
79
+ * Helper to read text file safely
80
+ */
81
+ const readTextFile = (filePath) => {
82
+ return fs.readFileSync(filePath, "utf-8");
83
+ };
84
+ /**
85
+ * Helper to safely evaluate JavaScript files (for config files)
86
+ */
87
+ const evaluateJsFile = (filePath) => {
88
+ const content = readTextFile(filePath);
89
+ // Create a safe evaluation context
90
+ const module = { exports: {} };
91
+ const require = (id) => {
92
+ if (id === "path")
93
+ return path;
94
+ if (id === "fs")
95
+ return fs;
96
+ throw new Error(`Cannot require '${id}' in config evaluation`);
97
+ };
98
+ try {
99
+ // Use Function constructor to create a safe evaluation environment
100
+ const fn = new Function("module", "exports", "require", "path", "fs", "__dirname", content);
101
+ fn(module, module.exports, require, path, fs, path.dirname(filePath));
102
+ return module.exports;
103
+ }
104
+ catch (error) {
105
+ console.warn(`Error evaluating ${filePath}:`, error);
106
+ return null;
107
+ }
108
+ };
56
109
  /**
57
110
  * Type guard for package.json structure
58
111
  */
59
112
  const isPackageJson = (obj) => {
60
- return typeof obj === "object" && obj !== null && "browserslist" in obj;
113
+ return typeof obj === "object" && obj !== null;
61
114
  };
62
115
  /**
63
116
  * Type guard for tsconfig.json structure
@@ -72,57 +125,121 @@ const isBabelRc = (obj) => {
72
125
  return typeof obj === "object" && obj !== null && "presets" in obj;
73
126
  };
74
127
  /**
75
- * Helper to read text file safely
128
+ * Type guard for vite config structure
76
129
  */
77
- const readTextFile = (filePath) => {
78
- return fs.readFileSync(filePath, "utf-8");
130
+ const isViteConfig = (obj) => {
131
+ return typeof obj === "object" && obj !== null;
79
132
  };
80
133
  /**
81
- * Detects ES target from common frontend project configuration files.
134
+ * Type guard for webpack config structure
135
+ */
136
+ const isWebpackConfig = (obj) => {
137
+ return typeof obj === "object" && obj !== null;
138
+ };
139
+ /**
140
+ * Type guard for next.js config structure
141
+ */
142
+ const isNextConfig = (obj) => {
143
+ return typeof obj === "object" && obj !== null;
144
+ };
145
+ /**
146
+ * Get parser function for a given config file
147
+ */
148
+ const getParser = (filename) => {
149
+ switch (true) {
150
+ case filename === "package.json":
151
+ return parsePackageJson;
152
+ case filename === ".browserslistrc":
153
+ case filename === ".browserslist":
154
+ return parseBrowserslistFile;
155
+ case filename === "tsconfig.json":
156
+ return parseTsConfig;
157
+ case filename === ".babelrc":
158
+ return parseBabelRc;
159
+ case filename.startsWith("babel.config"):
160
+ return parseBabelConfig;
161
+ case filename.startsWith("vite.config"):
162
+ return parseViteConfig;
163
+ case filename.startsWith("webpack.config"):
164
+ return parseWebpackConfig;
165
+ case filename.startsWith("next.config"):
166
+ return parseNextConfig;
167
+ default:
168
+ return null;
169
+ }
170
+ };
171
+ /**
172
+ * Get all possible config file names for detection
173
+ */
174
+ const getConfigFileNames = () => {
175
+ return CONFIG_FILE_NAMES;
176
+ };
177
+ /**
178
+ * Detects both ES target and output directory from common frontend project configuration files.
82
179
  * 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
180
+ * Returns an object with detected target and output directory information
84
181
  */
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);
182
+ export const detectTargetAndOutput = (cwd = process.cwd()) => {
183
+ const configFileNames = getConfigFileNames();
184
+ const result = {};
185
+ for (const filename of configFileNames) {
186
+ const filePath = path.join(cwd, filename);
100
187
  if (fs.existsSync(filePath)) {
188
+ const parser = getParser(filename);
189
+ if (!parser) {
190
+ console.warn(`No parser found for ${filename}`);
191
+ continue;
192
+ }
101
193
  try {
102
- const target = config.parser(filePath);
103
- if (target) {
104
- return { target, source: config.name };
194
+ const detection = parser(filePath);
195
+ // Update target if found and not already set
196
+ if (detection.target && !result.target) {
197
+ result.target = detection.target;
198
+ result.targetSource = filename;
199
+ }
200
+ // Update output directory if found and not already set
201
+ if (detection.outputDir && !result.outputDir) {
202
+ result.outputDir = detection.outputDir;
203
+ result.outputSource = filename;
204
+ }
205
+ // If we found both target and output directory, we can stop searching
206
+ if (result.target && result.outputDir) {
207
+ break;
105
208
  }
106
209
  }
107
210
  catch (error) {
108
- console.warn(`Error parsing ${config.name}:`, error);
109
- // Continue to next config file if parsing fails
211
+ console.warn(`Error parsing ${filename}:`, error);
110
212
  continue;
111
213
  }
112
214
  }
113
215
  }
114
- return null;
216
+ return result;
217
+ };
218
+ /**
219
+ * Legacy function for backward compatibility - detects only ES target
220
+ */
221
+ export const detectTarget = (cwd = process.cwd()) => {
222
+ const result = detectTargetAndOutput(cwd);
223
+ return result.target ? { target: result.target, source: result.targetSource } : null;
115
224
  };
116
225
  /**
117
- * Parse package.json for ES target in browserslist
226
+ * Legacy function for backward compatibility - detects only output directory
227
+ */
228
+ export const detectOutputDir = (cwd = process.cwd()) => {
229
+ const result = detectTargetAndOutput(cwd);
230
+ return result.outputDir ? { outputDir: result.outputDir, source: result.outputSource } : null;
231
+ };
232
+ /**
233
+ * Parse package.json for both target and output directory
118
234
  */
119
235
  const parsePackageJson = (filePath) => {
120
236
  const pkg = readJsonFile(filePath);
121
237
  if (!isPackageJson(pkg)) {
122
238
  console.warn(`Warning: ${filePath} does not look like a valid package.json (missing or invalid browserslist field).`);
123
- return null;
239
+ return {};
124
240
  }
125
- // Check for browserslist field
241
+ const result = {};
242
+ // Check for browserslist field for target
126
243
  if (pkg.browserslist) {
127
244
  const browserslist = Array.isArray(pkg.browserslist) ? pkg.browserslist : [pkg.browserslist];
128
245
  // Look for ES target in browserslist
@@ -130,32 +247,52 @@ const parsePackageJson = (filePath) => {
130
247
  if (typeof browser === "string") {
131
248
  const target = parseESVersion(browser);
132
249
  if (target) {
133
- return target;
250
+ result.target = target;
251
+ break;
134
252
  }
135
253
  }
136
254
  }
137
255
  }
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;
256
+ // Check for output directory hints
257
+ if (pkg.dist) {
258
+ result.outputDir = pkg.dist;
259
+ }
260
+ else if (pkg.build) {
261
+ result.outputDir = pkg.build;
262
+ }
263
+ else if (pkg.main && pkg.main.startsWith("./dist/")) {
264
+ result.outputDir = "dist";
265
+ }
266
+ else if (pkg.dependencies?.next || pkg.devDependencies?.next) {
267
+ // Next.js apps default to .next directory
268
+ result.outputDir = ".next/static";
269
+ }
270
+ return result;
141
271
  };
142
272
  /**
143
- * Parse tsconfig.json for target
273
+ * Parse tsconfig.json for both target and output directory
144
274
  */
145
275
  const parseTsConfig = (filePath) => {
146
276
  const config = readJsonFile(filePath);
147
277
  if (!isTsConfig(config)) {
148
278
  console.warn(`Warning: ${filePath} does not look like a valid tsconfig.json (missing or invalid compilerOptions field).`);
149
- return null;
279
+ return {};
150
280
  }
281
+ const result = {};
151
282
  if (config.compilerOptions?.target) {
152
283
  const target = config.compilerOptions.target;
153
- return TARGET_MAP[target] || null;
284
+ const mappedTarget = TARGET_MAP[target];
285
+ if (mappedTarget) {
286
+ result.target = mappedTarget;
287
+ }
154
288
  }
155
- return null;
289
+ if (config.compilerOptions?.outDir) {
290
+ result.outputDir = config.compilerOptions.outDir;
291
+ }
292
+ return result;
156
293
  };
157
294
  /**
158
- * Parse babel.config.js for preset-env target
295
+ * Parse babel.config.js for target
159
296
  */
160
297
  const parseBabelConfig = (filePath) => {
161
298
  const content = readTextFile(filePath);
@@ -167,19 +304,22 @@ const parseBabelConfig = (filePath) => {
167
304
  const browsersMatch = targetsStr.match(/browsers.*?\[(.*?)\]/);
168
305
  if (browsersMatch) {
169
306
  const browsers = browsersMatch[1];
170
- return parseESVersion(browsers);
307
+ const target = parseESVersion(browsers);
308
+ if (target) {
309
+ return { target };
310
+ }
171
311
  }
172
312
  }
173
- return null;
313
+ return {};
174
314
  };
175
315
  /**
176
- * Parse .babelrc for preset-env target
316
+ * Parse .babelrc for target
177
317
  */
178
318
  const parseBabelRc = (filePath) => {
179
319
  const config = readJsonFile(filePath);
180
320
  if (!isBabelRc(config)) {
181
321
  console.warn(`Warning: ${filePath} does not look like a valid .babelrc (missing or invalid presets field).`);
182
- return null;
322
+ return {};
183
323
  }
184
324
  if (config.presets) {
185
325
  for (const preset of config.presets) {
@@ -190,40 +330,61 @@ const parseBabelRc = (filePath) => {
190
330
  for (const browser of browsers) {
191
331
  const target = parseESVersion(browser);
192
332
  if (target) {
193
- return target;
333
+ return { target };
194
334
  }
195
335
  }
196
336
  }
197
337
  }
198
338
  }
199
339
  }
200
- return null;
340
+ return {};
201
341
  };
202
342
  /**
203
- * Parse vite.config.js/ts for target
343
+ * Parse vite.config.js/ts for both target and output directory
204
344
  */
205
345
  const parseViteConfig = (filePath) => {
206
346
  const content = readTextFile(filePath);
207
- // Look for esbuild target - more flexible pattern
347
+ const config = evaluateJsFile(filePath);
348
+ const result = {};
349
+ // Look for esbuild target
208
350
  const esbuildMatch = content.match(/esbuild\s*:\s*\{[^}]*target\s*:\s*['"`]([^'"`]+)['"`]/s);
209
351
  if (esbuildMatch) {
210
352
  const target = esbuildMatch[1];
211
- return TARGET_MAP[target] || null;
353
+ const mappedTarget = TARGET_MAP[target];
354
+ if (mappedTarget) {
355
+ result.target = mappedTarget;
356
+ }
212
357
  }
213
- return null;
358
+ // Look for output directory
359
+ if (isViteConfig(config) && config.build?.outDir) {
360
+ result.outputDir = config.build.outDir;
361
+ }
362
+ return result;
214
363
  };
215
364
  /**
216
- * Parse webpack.config.js/ts for target
365
+ * Parse webpack.config.js/ts for both target and output directory
217
366
  */
218
367
  const parseWebpackConfig = (filePath) => {
219
368
  const content = readTextFile(filePath);
369
+ const config = evaluateJsFile(filePath);
370
+ const result = {};
220
371
  // Look for target configuration
221
372
  const targetMatch = content.match(/target.*?['"`]([^'"`]+)['"`]/);
222
373
  if (targetMatch) {
223
374
  const target = targetMatch[1];
224
- return TARGET_MAP[target] || null;
375
+ const mappedTarget = TARGET_MAP[target];
376
+ if (mappedTarget) {
377
+ result.target = mappedTarget;
378
+ }
225
379
  }
226
- return null;
380
+ // Look for output directory
381
+ if (isWebpackConfig(config) && config.output?.path) {
382
+ // Extract just the directory name from the path
383
+ const outputPath = config.output.path;
384
+ const dirName = path.basename(outputPath);
385
+ result.outputDir = dirName;
386
+ }
387
+ return result;
227
388
  };
228
389
  /**
229
390
  * Parse .browserslistrc or .browserslist file for ES target
@@ -237,8 +398,27 @@ const parseBrowserslistFile = (filePath) => {
237
398
  for (const browser of lines) {
238
399
  const target = parseESVersion(browser);
239
400
  if (target) {
240
- return target;
401
+ return { target };
241
402
  }
242
403
  }
243
- return null;
404
+ return {};
405
+ };
406
+ /**
407
+ * Parse next.config.js/ts/cjs/mjs for output directory
408
+ */
409
+ const parseNextConfig = (filePath) => {
410
+ const config = evaluateJsFile(filePath);
411
+ if (!isNextConfig(config)) {
412
+ return {};
413
+ }
414
+ const result = {};
415
+ // Next.js uses .next as default output directory
416
+ if (config.distDir) {
417
+ result.outputDir = config.distDir;
418
+ }
419
+ else {
420
+ // Default Next.js output directory
421
+ result.outputDir = ".next";
422
+ }
423
+ return result;
244
424
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "es-guard",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A tool to check JavaScript compatibility with target environments",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",