es-guard 1.6.0 → 1.8.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/dist/cli.js CHANGED
@@ -4,9 +4,9 @@ import * as fs from "fs";
4
4
  import packageJson from "../package.json" with { type: "json" };
5
5
  import { checkCompatibility, formatViolationMessage } from "./lib/checkCompatiblity.js";
6
6
  import { getBrowserTargetsFromString } from "./lib/getBrowserTargets.js";
7
- import { detectTarget, detectOutputDir } from "./lib/detectTarget.js";
7
+ import { detectProjectConfig, getConfigFileNames } from "./lib/detectTarget.js";
8
+ import { getCurrentProjectType } from "./lib/projectType.js";
8
9
  import { setVerboseMode } from "./lib/globalState.js";
9
- import path from "path";
10
10
  const version = packageJson.version;
11
11
  // Create the main program
12
12
  const program = new Command();
@@ -71,22 +71,30 @@ program.action(async (directory, options) => {
71
71
  try {
72
72
  // Set global verbose mode
73
73
  setVerboseMode(options.verbose || false);
74
- // Auto-detect output directory if not specified
74
+ // Auto-detect configuration if not specified
75
75
  let scanDirectory;
76
76
  let outputDirSource;
77
+ let target;
78
+ let targetSource;
79
+ let browserTargets;
80
+ let browserslistSource;
81
+ if (options.verbose) {
82
+ console.log("🔍 Auto-detecting project configuration...");
83
+ console.log(`📂 Searching in: ${process.cwd()}`);
84
+ // Detect and log project type
85
+ const projectType = getCurrentProjectType();
86
+ console.log(`🏗️ Project type: ${projectType}`);
87
+ console.log("");
88
+ }
89
+ // Use unified detection to get all configuration at once
90
+ const detectedConfig = detectProjectConfig(process.cwd());
91
+ // Handle directory detection
77
92
  if (!directory) {
78
- if (options.verbose) {
79
- console.log("🔍 No directory specified, auto-detecting output directory from project configuration files...");
80
- console.log(`📂 Searching in: ${process.cwd()}`);
81
- console.log("");
82
- }
83
- // Try to detect output directory from current working directory
84
- const detectedOutput = detectOutputDir(process.cwd());
85
- if (detectedOutput) {
86
- scanDirectory = detectedOutput.outputDir;
87
- outputDirSource = `auto-detected from ${detectedOutput.source}`;
93
+ if (detectedConfig.outputDir) {
94
+ scanDirectory = detectedConfig.outputDir;
95
+ outputDirSource = `auto-detected from ${detectedConfig.outputSource}`;
88
96
  if (options.verbose) {
89
- console.log(`✅ Found output directory: ${scanDirectory} in ${detectedOutput.source}`);
97
+ console.log(`✅ Found output directory: ${scanDirectory} in ${detectedConfig.outputSource}`);
90
98
  }
91
99
  }
92
100
  else {
@@ -113,80 +121,51 @@ program.action(async (directory, options) => {
113
121
  console.error(`Error: "${scanDirectory}" is not a directory`);
114
122
  process.exit(1);
115
123
  }
116
- // Determine target - use provided target or auto-detect
117
- let target = options.target;
118
- let targetSource = "specified";
119
- if (!target) {
124
+ // Handle target detection
125
+ if (options.target) {
126
+ target = options.target;
127
+ targetSource = "specified";
128
+ }
129
+ else if (detectedConfig.target) {
130
+ target = detectedConfig.target;
131
+ targetSource = `auto-detected from ${detectedConfig.targetSource}`;
132
+ }
133
+ else {
120
134
  if (options.verbose) {
121
- console.log("🔍 Auto-detecting ES target from project configuration files...");
122
- console.log(`📂 Searching in: ${process.cwd()}`);
135
+ console.log(" No valid configuration files found for target detection");
136
+ console.log("📋 Searched for:");
137
+ const configFileNames = getConfigFileNames();
138
+ configFileNames.forEach((filename, index) => {
139
+ console.log(` ${index + 1}. ${filename}`);
140
+ });
123
141
  console.log("");
124
142
  }
125
- // Try to detect target from current working directory
126
- const detectedResult = detectTarget(process.cwd());
127
- if (detectedResult) {
128
- target = detectedResult.target;
129
- targetSource = `auto-detected from ${detectedResult.source}`;
130
- }
131
- else {
132
- if (options.verbose) {
133
- console.log("❌ No valid configuration files found for target detection");
134
- console.log("📋 Searched for:");
135
- console.log(" - package.json (browserslist field)");
136
- console.log(" - .browserslistrc/.browserslist");
137
- console.log(" - tsconfig.json (compilerOptions.target)");
138
- console.log(" - babel.config.js/.babelrc (@babel/preset-env targets)");
139
- console.log(" - vite.config.js/ts (esbuild target)");
140
- console.log(" - webpack.config.js/ts (target)");
141
- console.log(" - next.config.js/ts (output directory)");
142
- console.log("");
143
- }
144
- console.error("Error: No target specified and could not auto-detect from project configuration files.");
145
- console.error("Please specify a target with --target or ensure your project has a valid configuration file.");
146
- process.exit(1);
147
- }
143
+ console.error("Error: No target specified and could not auto-detect from project configuration files.");
144
+ console.error("Please specify a target with --target or ensure your project has a valid configuration file.");
145
+ process.exit(1);
148
146
  }
149
- // Determine browser targets
150
- let browserTargets;
147
+ // Handle browser targets detection
151
148
  if (options.browsers) {
152
149
  browserTargets = options.browsers;
150
+ browserslistSource = "specified";
153
151
  if (options.verbose) {
154
152
  console.log(`🌐 Using specified browser targets: ${browserTargets}`);
155
153
  }
156
154
  }
157
- else {
158
- // If auto-detected from package.json or browserslist file, check for ES version strings
159
- if (targetSource.startsWith("auto-detected from ")) {
160
- const configFile = targetSource.replace("auto-detected from ", "");
161
- let browserslistEntries = [];
162
- if (configFile === "package.json") {
163
- const pkgPath = path.join(process.cwd(), "package.json");
164
- if (fs.existsSync(pkgPath)) {
165
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
166
- if (pkg.browserslist) {
167
- browserslistEntries = Array.isArray(pkg.browserslist) ? pkg.browserslist : [pkg.browserslist];
168
- }
169
- }
170
- }
171
- else if (configFile === ".browserslistrc" || configFile === ".browserslist") {
172
- const blPath = path.join(process.cwd(), configFile);
173
- if (fs.existsSync(blPath)) {
174
- browserslistEntries = fs
175
- .readFileSync(blPath, "utf-8")
176
- .split(/\r?\n/)
177
- .map((l) => l.trim())
178
- .filter((l) => l && !l.startsWith("#"));
179
- }
180
- }
181
- const esVersionRegex = /^es\d{1,4}$/i;
182
- const invalidEntries = browserslistEntries.filter((entry) => esVersionRegex.test(entry));
183
- if (invalidEntries.length > 0) {
184
- 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.`);
185
- }
155
+ else if (detectedConfig.browserslist) {
156
+ // Use the detected browserslist
157
+ browserTargets = detectedConfig.browserslist.join(", ");
158
+ browserslistSource = `auto-detected from ${detectedConfig.browserslistSource}`;
159
+ if (options.verbose) {
160
+ console.log(`🌐 Using detected browserslist: ${browserTargets} (from ${detectedConfig.browserslistSource})`);
186
161
  }
162
+ }
163
+ else {
164
+ // No browserslist detected, auto-determine from target
187
165
  browserTargets = getBrowserTargetsFromString(target);
166
+ browserslistSource = "auto-determined from target";
188
167
  if (options.verbose) {
189
- console.log(`🌐 Auto-determined browser targets: ${browserTargets}`);
168
+ console.log(`🌐 No browserslist detected, auto-determining from target: ${browserTargets}`);
190
169
  }
191
170
  }
192
171
  if (options.verbose) {
@@ -195,6 +174,7 @@ program.action(async (directory, options) => {
195
174
  console.log(` Target ES version: ${target}`);
196
175
  console.log(` Target source: ${targetSource}`);
197
176
  console.log(` Browser targets: ${browserTargets}`);
177
+ console.log(` Browserslist source: ${browserslistSource}`);
198
178
  console.log(` Scan directory: ${scanDirectory}`);
199
179
  console.log(` Output directory source: ${outputDirSource}`);
200
180
  console.log("");
@@ -202,7 +182,7 @@ program.action(async (directory, options) => {
202
182
  console.log(`🔍 ES-Guard v${version}`);
203
183
  console.log(`📁 Scanning directory: ${scanDirectory}`);
204
184
  console.log(`🎯 Target ES version: ${target} (${targetSource})`);
205
- console.log(`🌐 Browser targets: ${browserTargets}${options.browsers ? "" : " (auto-determined)"}`);
185
+ console.log(`🌐 Browser targets: ${browserTargets} (${browserslistSource})`);
206
186
  console.log("");
207
187
  const { errors, warnings } = await checkCompatibility({
208
188
  dir: scanDirectory,
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Default configurations for different project types
3
+ */
4
+ /**
5
+ * Next.js default browserslist configuration
6
+ * Source: https://nextjs.org/docs/basic-features/supported-browsers-features
7
+ */
8
+ export const NEXTJS_DEFAULT_BROWSERSLIST = ["chrome 64", "edge 79", "firefox 67", "opera 51", "safari 12"];
9
+ /**
10
+ * Default output directories for different project types
11
+ */
12
+ export const DEFAULT_OUTPUT_DIRS = {
13
+ nextjs: ".next/static",
14
+ vite: "dist",
15
+ webpack: "dist",
16
+ rollup: "dist",
17
+ parcel: "dist",
18
+ generic: "dist",
19
+ };
20
+ /**
21
+ * Project type detection helpers
22
+ */
23
+ export const PROJECT_TYPES = {
24
+ nextjs: "nextjs",
25
+ vite: "vite",
26
+ webpack: "webpack",
27
+ rollup: "rollup",
28
+ parcel: "parcel",
29
+ generic: "generic",
30
+ };
31
+ export const ProjectTypeKeys = new Set(Object.keys(PROJECT_TYPES));
32
+ export const isProjectType = (projectType) => {
33
+ return ProjectTypeKeys.has(projectType);
34
+ };
35
+ /**
36
+ * Get default browserslist for a project type
37
+ */
38
+ export function getDefaultBrowserslist(projectType) {
39
+ switch (projectType) {
40
+ case PROJECT_TYPES.nextjs:
41
+ return [...NEXTJS_DEFAULT_BROWSERSLIST];
42
+ // Add more project types here as needed
43
+ default:
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Get default output directory for a project type
49
+ */
50
+ export function getDefaultOutputDir(projectType) {
51
+ if (isProjectType(projectType)) {
52
+ return DEFAULT_OUTPUT_DIRS[projectType];
53
+ }
54
+ return DEFAULT_OUTPUT_DIRS.generic;
55
+ }
56
+ /**
57
+ * Detect project type from package.json dependencies
58
+ */
59
+ export function detectProjectType(dependencies = {}, devDependencies = {}) {
60
+ const allDeps = { ...dependencies, ...devDependencies };
61
+ if (allDeps.next) {
62
+ return PROJECT_TYPES.nextjs;
63
+ }
64
+ if (allDeps.vite) {
65
+ return PROJECT_TYPES.vite;
66
+ }
67
+ if (allDeps.webpack) {
68
+ return PROJECT_TYPES.webpack;
69
+ }
70
+ if (allDeps.rollup) {
71
+ return PROJECT_TYPES.rollup;
72
+ }
73
+ if (allDeps.parcel) {
74
+ return PROJECT_TYPES.parcel;
75
+ }
76
+ return PROJECT_TYPES.generic;
77
+ }
@@ -1,6 +1,9 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { verboseMode } from "./globalState.js";
4
+ import { NEXTJS_DEFAULT_BROWSERSLIST } from "./defaults.js";
5
+ import { getCurrentProjectType } from "./projectType.js";
6
+ import { readJsonFile, readTextFile, evaluateJsFile, isPackageJson, isTsConfig, isBabelRc, isViteConfig, isWebpackConfig, isNextConfig, } from "./utils.js";
4
7
  // Shared utilities for ES version parsing and conversion
5
8
  const CONFIG_FILE_NAMES = [
6
9
  "package.json",
@@ -24,23 +27,6 @@ const CONFIG_FILE_NAMES = [
24
27
  "next.config.cjs",
25
28
  "next.config.mjs",
26
29
  ];
27
- /**
28
- * Parse ES version from string (e.g., "es6", "es2020", "ES2015")
29
- * Returns the year as a string, or null if not found
30
- */
31
- const parseESVersion = (str) => {
32
- const esMatch = str.match(/es(\d+)/i);
33
- if (esMatch) {
34
- const esVersion = parseInt(esMatch[1]);
35
- // If it's a 4-digit year (like 2020), use it directly
36
- if (esVersion >= 2000) {
37
- return esVersion.toString();
38
- }
39
- // Otherwise convert ES version to year: ES6=2015, ES7=2016, ES8=2017, etc.
40
- return (2009 + esVersion).toString();
41
- }
42
- return null;
43
- };
44
30
  /**
45
31
  * Common target mapping for TypeScript, Vite, and Webpack configs
46
32
  */
@@ -69,80 +55,6 @@ const TARGET_MAP = {
69
55
  es6: "2015",
70
56
  es5: "2009",
71
57
  };
72
- /**
73
- * Helper to read and parse JSON file safely
74
- */
75
- const readJsonFile = (filePath) => {
76
- const content = fs.readFileSync(filePath, "utf-8");
77
- return JSON.parse(content);
78
- };
79
- /**
80
- * Helper to read text file safely
81
- */
82
- const readTextFile = (filePath) => {
83
- return fs.readFileSync(filePath, "utf-8");
84
- };
85
- /**
86
- * Helper to safely evaluate JavaScript files (for config files)
87
- */
88
- const evaluateJsFile = (filePath) => {
89
- const content = readTextFile(filePath);
90
- // Create a safe evaluation context
91
- const module = { exports: {} };
92
- const require = (id) => {
93
- if (id === "path")
94
- return path;
95
- if (id === "fs")
96
- return fs;
97
- throw new Error(`Cannot require '${id}' in config evaluation`);
98
- };
99
- try {
100
- // Use Function constructor to create a safe evaluation environment
101
- const fn = new Function("module", "exports", "require", "path", "fs", "__dirname", content);
102
- fn(module, module.exports, require, path, fs, path.dirname(filePath));
103
- return module.exports;
104
- }
105
- catch (error) {
106
- console.warn(`Error evaluating ${filePath}:`, error);
107
- return null;
108
- }
109
- };
110
- /**
111
- * Type guard for package.json structure
112
- */
113
- const isPackageJson = (obj) => {
114
- return typeof obj === "object" && obj !== null;
115
- };
116
- /**
117
- * Type guard for tsconfig.json structure
118
- */
119
- const isTsConfig = (obj) => {
120
- return typeof obj === "object" && obj !== null && "compilerOptions" in obj;
121
- };
122
- /**
123
- * Type guard for .babelrc structure
124
- */
125
- const isBabelRc = (obj) => {
126
- return typeof obj === "object" && obj !== null && "presets" in obj;
127
- };
128
- /**
129
- * Type guard for vite config structure
130
- */
131
- const isViteConfig = (obj) => {
132
- return typeof obj === "object" && obj !== null;
133
- };
134
- /**
135
- * Type guard for webpack config structure
136
- */
137
- const isWebpackConfig = (obj) => {
138
- return typeof obj === "object" && obj !== null;
139
- };
140
- /**
141
- * Type guard for next.js config structure
142
- */
143
- const isNextConfig = (obj) => {
144
- return typeof obj === "object" && obj !== null;
145
- };
146
58
  /**
147
59
  * Get parser function for a given config file
148
60
  */
@@ -172,19 +84,18 @@ const getParser = (filename) => {
172
84
  /**
173
85
  * Get all possible config file names for detection
174
86
  */
175
- const getConfigFileNames = () => {
87
+ export const getConfigFileNames = () => {
176
88
  return CONFIG_FILE_NAMES;
177
89
  };
178
90
  /**
179
- * Detects both ES target and output directory from common frontend project configuration files.
180
- * Searches in order of preference: package.json, .browserslistrc/.browserslist, tsconfig.json, babel.config.js, .babelrc, vite.config.js, webpack.config.js
181
- * Returns an object with detected target and output directory information
91
+ * Detection function that extracts all configuration in a single pass
92
+ * This is more efficient than separate detection functions
182
93
  */
183
- export const detectTargetAndOutput = (cwd = process.cwd()) => {
94
+ export const detectProjectConfig = (cwd = process.cwd()) => {
184
95
  const configFileNames = getConfigFileNames();
185
96
  const result = {};
186
97
  if (verboseMode) {
187
- console.log("🔍 Starting configuration file detection...");
98
+ console.log("🔍 Starting configuration detection...");
188
99
  console.log(`📂 Scanning directory: ${cwd}`);
189
100
  console.log("📋 Files to check:");
190
101
  configFileNames.forEach((filename, index) => {
@@ -230,7 +141,7 @@ export const detectTargetAndOutput = (cwd = process.cwd()) => {
230
141
  // Update output directory if found and not already set
231
142
  if (detection.outputDir && !result.outputDir) {
232
143
  result.outputDir = detection.outputDir;
233
- result.outputSource = filename;
144
+ result.outputSource = detection.outputSource || filename;
234
145
  if (verboseMode) {
235
146
  console.log(` 📁 Output directory: ${detection.outputDir}`);
236
147
  }
@@ -241,10 +152,24 @@ export const detectTargetAndOutput = (cwd = process.cwd()) => {
241
152
  else if (verboseMode) {
242
153
  console.log(` 📁 Output directory: not found`);
243
154
  }
244
- // If we found both target and output directory, we can stop searching
245
- if (result.target && result.outputDir) {
155
+ // Update browserslist if found and not already set
156
+ if (detection.browserslist && !result.browserslist) {
157
+ result.browserslist = detection.browserslist;
158
+ result.browserslistSource = detection.browserslistSource || filename;
246
159
  if (verboseMode) {
247
- console.log(` Both target and output directory found, stopping search`);
160
+ console.log(` 🌐 Browserslist: ${detection.browserslist.join(", ")} (from ${result.browserslistSource})`);
161
+ }
162
+ }
163
+ else if (verboseMode && detection.browserslist) {
164
+ console.log(` 🌐 Browserslist: ${detection.browserslist.join(", ")} (already found in ${result.browserslistSource})`);
165
+ }
166
+ else if (verboseMode) {
167
+ console.log(` 🌐 Browserslist: not found`);
168
+ }
169
+ // If we found all three (target, output directory, and browserslist), we can stop searching
170
+ if (result.target && result.outputDir && result.browserslist) {
171
+ if (verboseMode) {
172
+ console.log(` ✅ All configuration found, stopping search`);
248
173
  }
249
174
  break;
250
175
  }
@@ -278,6 +203,12 @@ export const detectTargetAndOutput = (cwd = process.cwd()) => {
278
203
  else {
279
204
  console.log(` 📁 Output directory: not found`);
280
205
  }
206
+ if (result.browserslist) {
207
+ console.log(` 🌐 Browserslist: ${result.browserslist.join(", ")} (from ${result.browserslistSource})`);
208
+ }
209
+ else {
210
+ console.log(` 🌐 Browserslist: not found`);
211
+ }
281
212
  console.log("");
282
213
  }
283
214
  return result;
@@ -286,16 +217,37 @@ export const detectTargetAndOutput = (cwd = process.cwd()) => {
286
217
  * Legacy function for backward compatibility - detects only ES target
287
218
  */
288
219
  export const detectTarget = (cwd = process.cwd()) => {
289
- const result = detectTargetAndOutput(cwd);
220
+ const result = detectProjectConfig(cwd);
290
221
  return result.target ? { target: result.target, source: result.targetSource } : null;
291
222
  };
292
223
  /**
293
224
  * Legacy function for backward compatibility - detects only output directory
294
225
  */
295
226
  export const detectOutputDir = (cwd = process.cwd()) => {
296
- const result = detectTargetAndOutput(cwd);
227
+ const result = detectProjectConfig(cwd);
297
228
  return result.outputDir ? { outputDir: result.outputDir, source: result.outputSource } : null;
298
229
  };
230
+ /**
231
+ * Legacy function for backward compatibility - detects only browserslist
232
+ */
233
+ export const detectBrowserslist = (cwd = process.cwd()) => {
234
+ const result = detectProjectConfig(cwd);
235
+ return result.browserslist ? { browserslist: result.browserslist, source: result.browserslistSource } : null;
236
+ };
237
+ /**
238
+ * Legacy function for backward compatibility - detects target and output directory
239
+ */
240
+ export const detectTargetAndOutput = (cwd = process.cwd()) => {
241
+ const result = detectProjectConfig(cwd);
242
+ return {
243
+ target: result.target,
244
+ targetSource: result.targetSource,
245
+ outputDir: result.outputDir,
246
+ outputSource: result.outputSource,
247
+ browserslist: result.browserslist,
248
+ browserslistSource: result.browserslistSource,
249
+ };
250
+ };
299
251
  /**
300
252
  * Parse package.json for both target and output directory
301
253
  */
@@ -306,20 +258,14 @@ const parsePackageJson = (filePath) => {
306
258
  return {};
307
259
  }
308
260
  const result = {};
309
- // Check for browserslist field for target
261
+ // Check for browserslist field
310
262
  if (pkg.browserslist) {
311
263
  const browserslist = Array.isArray(pkg.browserslist) ? pkg.browserslist : [pkg.browserslist];
312
- // Look for ES target in browserslist
313
- for (const browser of browserslist) {
314
- if (typeof browser === "string") {
315
- const target = parseESVersion(browser);
316
- if (target) {
317
- result.target = target;
318
- break;
319
- }
320
- }
321
- }
264
+ // Store the full browserslist for CLI defaults
265
+ result.browserslist = browserslist.filter((browser) => typeof browser === "string");
322
266
  }
267
+ // Use global project type detection (lazy initialization)
268
+ const projectType = getCurrentProjectType(path.dirname(filePath));
323
269
  // Check for output directory hints
324
270
  if (pkg.dist) {
325
271
  result.outputDir = pkg.dist;
@@ -330,9 +276,15 @@ const parsePackageJson = (filePath) => {
330
276
  else if (pkg.main && pkg.main.startsWith("./dist/")) {
331
277
  result.outputDir = "dist";
332
278
  }
333
- else if (pkg.dependencies?.next || pkg.devDependencies?.next) {
334
- // Next.js apps default to .next directory
279
+ else if (projectType === "nextjs") {
280
+ // Set default output directory for Next.js projects
335
281
  result.outputDir = ".next/static";
282
+ result.outputSource = "package.json (default)";
283
+ }
284
+ // If no browserslist was found, use default for detected project type
285
+ if (!result.browserslist && projectType === "nextjs") {
286
+ result.browserslist = [...NEXTJS_DEFAULT_BROWSERSLIST];
287
+ result.browserslistSource = "Next.js default";
336
288
  }
337
289
  return result;
338
290
  };
@@ -363,6 +315,7 @@ const parseTsConfig = (filePath) => {
363
315
  */
364
316
  const parseBabelConfig = (filePath) => {
365
317
  const content = readTextFile(filePath);
318
+ const result = {};
366
319
  // Look for @babel/preset-env configuration
367
320
  const presetEnvMatch = content.match(/@babel\/preset-env.*?targets.*?(\{[^}]*\})/s);
368
321
  if (presetEnvMatch) {
@@ -371,13 +324,16 @@ const parseBabelConfig = (filePath) => {
371
324
  const browsersMatch = targetsStr.match(/browsers.*?\[(.*?)\]/);
372
325
  if (browsersMatch) {
373
326
  const browsers = browsersMatch[1];
374
- const target = parseESVersion(browsers);
375
- if (target) {
376
- return { target };
377
- }
327
+ // Parse browsers array from string
328
+ const browsersList = browsers
329
+ .split(",")
330
+ .map((b) => b.trim().replace(/['"]/g, ""))
331
+ .filter((b) => b);
332
+ // Store the full browserslist for CLI defaults
333
+ result.browserslist = browsersList;
378
334
  }
379
335
  }
380
- return {};
336
+ return result;
381
337
  };
382
338
  /**
383
339
  * Parse .babelrc for target
@@ -388,23 +344,20 @@ const parseBabelRc = (filePath) => {
388
344
  console.warn(`Warning: ${filePath} does not look like a valid .babelrc (missing or invalid presets field).`);
389
345
  return {};
390
346
  }
347
+ const result = {};
391
348
  if (config.presets) {
392
349
  for (const preset of config.presets) {
393
350
  if (Array.isArray(preset) && preset[0] === "@babel/preset-env") {
394
351
  const options = preset[1];
395
352
  if (options?.targets?.browsers) {
396
353
  const browsers = options.targets.browsers;
397
- for (const browser of browsers) {
398
- const target = parseESVersion(browser);
399
- if (target) {
400
- return { target };
401
- }
402
- }
354
+ // Store the full browserslist for CLI defaults
355
+ result.browserslist = browsers;
403
356
  }
404
357
  }
405
358
  }
406
359
  }
407
- return {};
360
+ return result;
408
361
  };
409
362
  /**
410
363
  * Parse vite.config.js/ts for both target and output directory
@@ -454,7 +407,7 @@ const parseWebpackConfig = (filePath) => {
454
407
  return result;
455
408
  };
456
409
  /**
457
- * Parse .browserslistrc or .browserslist file for ES target
410
+ * Parse .browserslistrc or .browserslist file for browserslist
458
411
  */
459
412
  const parseBrowserslistFile = (filePath) => {
460
413
  const content = readTextFile(filePath);
@@ -462,13 +415,10 @@ const parseBrowserslistFile = (filePath) => {
462
415
  .split(/\r?\n/)
463
416
  .map((line) => line.trim())
464
417
  .filter((line) => line && !line.startsWith("#"));
465
- for (const browser of lines) {
466
- const target = parseESVersion(browser);
467
- if (target) {
468
- return { target };
469
- }
470
- }
471
- return {};
418
+ const result = {};
419
+ // Store the full browserslist for CLI defaults
420
+ result.browserslist = lines;
421
+ return result;
472
422
  };
473
423
  /**
474
424
  * Parse next.config.js/ts/cjs/mjs for output directory
@@ -1,7 +1,9 @@
1
- // Global state management for CLI options
1
+ // Global state management for CLI options and project configuration
2
2
  // Static global state variables
3
3
  export let verboseMode = false;
4
4
  export let debugMode = false;
5
+ export let projectType = undefined;
6
+ export let projectTypeDetected = false;
5
7
  /**
6
8
  * Set the global verbose mode
7
9
  */
@@ -21,6 +23,8 @@ export const getGlobalState = () => {
21
23
  return {
22
24
  verbose: verboseMode,
23
25
  debug: debugMode,
26
+ projectType,
27
+ projectTypeDetected,
24
28
  };
25
29
  };
26
30
  /**
@@ -31,6 +35,10 @@ export const setGlobalState = (options) => {
31
35
  verboseMode = options.verbose;
32
36
  if (options.debug !== undefined)
33
37
  debugMode = options.debug;
38
+ if (options.projectType !== undefined)
39
+ projectType = options.projectType;
40
+ if (options.projectTypeDetected !== undefined)
41
+ projectTypeDetected = options.projectTypeDetected;
34
42
  };
35
43
  /**
36
44
  * Reset global state to defaults
@@ -38,4 +46,25 @@ export const setGlobalState = (options) => {
38
46
  export const resetGlobalState = () => {
39
47
  verboseMode = false;
40
48
  debugMode = false;
49
+ projectType = undefined;
50
+ projectTypeDetected = false;
51
+ };
52
+ /**
53
+ * Set the detected project type globally
54
+ */
55
+ export const setProjectType = (detectedType) => {
56
+ projectType = detectedType;
57
+ projectTypeDetected = true;
58
+ };
59
+ /**
60
+ * Get the detected project type (lazy detection if not already done)
61
+ */
62
+ export const getProjectType = () => {
63
+ return projectType;
64
+ };
65
+ /**
66
+ * Check if project type has been detected
67
+ */
68
+ export const isProjectTypeDetected = () => {
69
+ return projectTypeDetected;
41
70
  };
@@ -0,0 +1,46 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { verboseMode, setProjectType, getProjectType, isProjectTypeDetected } from "./globalState.js";
4
+ import { detectProjectType } from "./defaults.js";
5
+ import { readJsonFile, isPackageJson } from "./utils.js";
6
+ /**
7
+ * Detect and cache project type globally (lazy initialization)
8
+ */
9
+ export const detectAndCacheProjectType = (cwd = process.cwd()) => {
10
+ // Return cached result if already detected
11
+ if (isProjectTypeDetected()) {
12
+ return getProjectType() || null;
13
+ }
14
+ // Detect project type from package.json
15
+ const packageJsonPath = path.join(cwd, "package.json");
16
+ if (fs.existsSync(packageJsonPath)) {
17
+ try {
18
+ const pkg = readJsonFile(packageJsonPath);
19
+ if (isPackageJson(pkg)) {
20
+ const detectedType = detectProjectType(pkg.dependencies, pkg.devDependencies);
21
+ setProjectType(detectedType);
22
+ if (verboseMode) {
23
+ console.log(`🔍 Project type detected: ${detectedType}`);
24
+ }
25
+ return detectedType;
26
+ }
27
+ }
28
+ catch (error) {
29
+ // Ignore errors when detecting project type
30
+ if (verboseMode) {
31
+ console.log(` ⚠️ Error detecting project type: ${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ }
34
+ }
35
+ // No project type detected
36
+ if (verboseMode) {
37
+ console.log(`🔍 No project type detected`);
38
+ }
39
+ return null;
40
+ };
41
+ /**
42
+ * Get the current project type (detects if not already cached)
43
+ */
44
+ export const getCurrentProjectType = (cwd = process.cwd()) => {
45
+ return detectAndCacheProjectType(cwd) || "generic";
46
+ };
@@ -0,0 +1,76 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ /**
4
+ * Helper to read and parse JSON file safely
5
+ */
6
+ export const readJsonFile = (filePath) => {
7
+ const content = fs.readFileSync(filePath, "utf-8");
8
+ return JSON.parse(content);
9
+ };
10
+ /**
11
+ * Helper to read text file safely
12
+ */
13
+ export const readTextFile = (filePath) => {
14
+ return fs.readFileSync(filePath, "utf-8");
15
+ };
16
+ /**
17
+ * Helper to safely evaluate JavaScript files (for config files)
18
+ */
19
+ export const evaluateJsFile = (filePath) => {
20
+ const content = readTextFile(filePath);
21
+ // Create a safe evaluation context
22
+ const module = { exports: {} };
23
+ const require = (id) => {
24
+ if (id === "path")
25
+ return path;
26
+ if (id === "fs")
27
+ return fs;
28
+ throw new Error(`Cannot require '${id}' in config evaluation`);
29
+ };
30
+ try {
31
+ // Use Function constructor to create a safe evaluation environment
32
+ const fn = new Function("module", "exports", "require", "path", "fs", "__dirname", content);
33
+ fn(module, module.exports, require, path, fs, path.dirname(filePath));
34
+ return module.exports;
35
+ }
36
+ catch (error) {
37
+ console.warn(`Error evaluating ${filePath}:`, error);
38
+ return null;
39
+ }
40
+ };
41
+ /**
42
+ * Type guard for package.json structure
43
+ */
44
+ export const isPackageJson = (obj) => {
45
+ return typeof obj === "object" && obj !== null;
46
+ };
47
+ /**
48
+ * Type guard for tsconfig.json structure
49
+ */
50
+ export const isTsConfig = (obj) => {
51
+ return typeof obj === "object" && obj !== null && "compilerOptions" in obj;
52
+ };
53
+ /**
54
+ * Type guard for .babelrc structure
55
+ */
56
+ export const isBabelRc = (obj) => {
57
+ return typeof obj === "object" && obj !== null && "presets" in obj;
58
+ };
59
+ /**
60
+ * Type guard for vite config structure
61
+ */
62
+ export const isViteConfig = (obj) => {
63
+ return typeof obj === "object" && obj !== null;
64
+ };
65
+ /**
66
+ * Type guard for webpack config structure
67
+ */
68
+ export const isWebpackConfig = (obj) => {
69
+ return typeof obj === "object" && obj !== null;
70
+ };
71
+ /**
72
+ * Type guard for next.js config structure
73
+ */
74
+ export const isNextConfig = (obj) => {
75
+ return typeof obj === "object" && obj !== null;
76
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "es-guard",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "A tool to check JavaScript compatibility with target environments",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",