console-sniper 1.0.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/LICENSE +21 -0
- package/README.md +226 -0
- package/dist/cli/cli.js +493 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/index.cjs +250 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/dist/types-J-TZoPXb.d.cts +133 -0
- package/dist/types-J-TZoPXb.d.ts +133 -0
- package/dist/vite/vitePlugin.cjs +289 -0
- package/dist/vite/vitePlugin.cjs.map +1 -0
- package/dist/vite/vitePlugin.d.cts +51 -0
- package/dist/vite/vitePlugin.d.ts +51 -0
- package/dist/vite/vitePlugin.js +252 -0
- package/dist/vite/vitePlugin.js.map +1 -0
- package/package.json +90 -0
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import pc3 from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/core/constants.ts
|
|
8
|
+
var PACKAGE_NAME = "console-sniper";
|
|
9
|
+
var PACKAGE_VERSION = "1.0.0";
|
|
10
|
+
var DEFAULT_METHODS = [
|
|
11
|
+
"log",
|
|
12
|
+
"warn",
|
|
13
|
+
"error",
|
|
14
|
+
"info",
|
|
15
|
+
"debug"
|
|
16
|
+
];
|
|
17
|
+
var DEFAULT_REMOVE_COMMENTS = true;
|
|
18
|
+
var SUPPORTED_EXTENSIONS = [
|
|
19
|
+
".js",
|
|
20
|
+
".mjs",
|
|
21
|
+
".cjs",
|
|
22
|
+
".jsx",
|
|
23
|
+
".ts",
|
|
24
|
+
".tsx",
|
|
25
|
+
".mts",
|
|
26
|
+
".cts"
|
|
27
|
+
];
|
|
28
|
+
var DEFAULT_EXCLUDE_PATTERNS = [
|
|
29
|
+
"**/node_modules/**",
|
|
30
|
+
"**/dist/**",
|
|
31
|
+
"**/build/**",
|
|
32
|
+
"**/.git/**",
|
|
33
|
+
"**/*.min.js",
|
|
34
|
+
"**/*.d.ts"
|
|
35
|
+
];
|
|
36
|
+
var CONSOLE_KEYWORD = "console";
|
|
37
|
+
|
|
38
|
+
// src/core/utils.ts
|
|
39
|
+
import path from "path";
|
|
40
|
+
function resolveOptionsSync(options) {
|
|
41
|
+
return {
|
|
42
|
+
methods: options?.methods?.length ? options.methods : [...DEFAULT_METHODS],
|
|
43
|
+
removeComments: options?.removeComments ?? DEFAULT_REMOVE_COMMENTS,
|
|
44
|
+
include: options?.include ?? [],
|
|
45
|
+
exclude: options?.exclude ?? [],
|
|
46
|
+
silent: options?.silent ?? false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function parseMethodList(input) {
|
|
50
|
+
return input.split(",").map((m) => m.trim()).filter((m) => m.length > 0);
|
|
51
|
+
}
|
|
52
|
+
function formatCount(count, noun) {
|
|
53
|
+
return `${count} ${noun}${count !== 1 ? "s" : ""}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/shared/banner.ts
|
|
57
|
+
import pc from "picocolors";
|
|
58
|
+
var ASCII_BANNER = `
|
|
59
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
60
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
61
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557
|
|
62
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D
|
|
63
|
+
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
64
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
65
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
66
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
67
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
|
|
68
|
+
\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
69
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
70
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
|
|
71
|
+
`.trim();
|
|
72
|
+
function printBanner(silent = false) {
|
|
73
|
+
if (silent) return;
|
|
74
|
+
console.log(pc.cyan(ASCII_BANNER));
|
|
75
|
+
console.log(
|
|
76
|
+
pc.dim(` ${PACKAGE_NAME} v${PACKAGE_VERSION}`) + pc.dim(" \u2014 Remove console.* statements using AST parsing\n")
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
function compactBanner() {
|
|
80
|
+
return pc.cyan(`\u{1F3AF} ${PACKAGE_NAME}`) + pc.dim(` v${PACKAGE_VERSION}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/cli/fileScanner.ts
|
|
84
|
+
import fs from "fs/promises";
|
|
85
|
+
import glob from "fast-glob";
|
|
86
|
+
|
|
87
|
+
// src/core/stripConsole.ts
|
|
88
|
+
import { parse } from "@babel/parser";
|
|
89
|
+
import _traverse from "@babel/traverse";
|
|
90
|
+
import _generate from "@babel/generator";
|
|
91
|
+
import * as t from "@babel/types";
|
|
92
|
+
var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
|
|
93
|
+
var generate = typeof _generate === "function" ? _generate : _generate.default;
|
|
94
|
+
function stripConsoleFromCode(code, options) {
|
|
95
|
+
const opts = resolveOptionsSync(options);
|
|
96
|
+
const methodsToRemove = new Set(opts.methods);
|
|
97
|
+
const removedMethods = [];
|
|
98
|
+
const parserOptions = {
|
|
99
|
+
sourceType: "module",
|
|
100
|
+
// Support import/export statements
|
|
101
|
+
strictMode: false,
|
|
102
|
+
// Don't throw on sloppy-mode code
|
|
103
|
+
allowImportExportEverywhere: true,
|
|
104
|
+
allowReturnOutsideFunction: true,
|
|
105
|
+
allowSuperOutsideMethod: true,
|
|
106
|
+
plugins: [
|
|
107
|
+
"typescript",
|
|
108
|
+
// TypeScript syntax (types, generics, decorators)
|
|
109
|
+
"jsx",
|
|
110
|
+
// JSX/TSX syntax
|
|
111
|
+
"decorators",
|
|
112
|
+
// @decorator syntax
|
|
113
|
+
"classProperties",
|
|
114
|
+
"classPrivateProperties",
|
|
115
|
+
"classPrivateMethods",
|
|
116
|
+
"classStaticBlock",
|
|
117
|
+
"dynamicImport",
|
|
118
|
+
// import(...)
|
|
119
|
+
"exportDefaultFrom",
|
|
120
|
+
"exportNamespaceFrom",
|
|
121
|
+
"nullishCoalescingOperator",
|
|
122
|
+
// ??
|
|
123
|
+
"optionalChaining",
|
|
124
|
+
// ?.
|
|
125
|
+
"optionalCatchBinding",
|
|
126
|
+
"logicalAssignment",
|
|
127
|
+
// &&=, ||=, ??=
|
|
128
|
+
"numericSeparator",
|
|
129
|
+
// 1_000_000
|
|
130
|
+
"bigInt",
|
|
131
|
+
"importMeta",
|
|
132
|
+
// import.meta
|
|
133
|
+
"topLevelAwait"
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
let ast;
|
|
137
|
+
try {
|
|
138
|
+
ast = parse(code, parserOptions);
|
|
139
|
+
} catch (parseError) {
|
|
140
|
+
if (!opts.silent) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`[console-sniper] Failed to parse code, skipping. Error: ${parseError instanceof Error ? parseError.message : String(parseError)}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
code,
|
|
147
|
+
removedCount: 0,
|
|
148
|
+
removedMethods: [],
|
|
149
|
+
changed: false
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
traverse(ast, {
|
|
153
|
+
ExpressionStatement(nodePath) {
|
|
154
|
+
const { expression } = nodePath.node;
|
|
155
|
+
if (!t.isCallExpression(expression)) return;
|
|
156
|
+
const { callee } = expression;
|
|
157
|
+
if (!t.isMemberExpression(callee)) return;
|
|
158
|
+
const { object, property } = callee;
|
|
159
|
+
if (!t.isIdentifier(object, { name: "console" })) return;
|
|
160
|
+
let methodName;
|
|
161
|
+
if (t.isIdentifier(property)) {
|
|
162
|
+
methodName = property.name;
|
|
163
|
+
} else if (t.isStringLiteral(property)) {
|
|
164
|
+
methodName = property.value;
|
|
165
|
+
}
|
|
166
|
+
if (!methodName || !methodsToRemove.has(methodName)) return;
|
|
167
|
+
removedMethods.push(methodName);
|
|
168
|
+
nodePath.remove();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (opts.removeComments) {
|
|
172
|
+
removeConsoleComments(ast);
|
|
173
|
+
}
|
|
174
|
+
const { code: transformedCode } = generate(
|
|
175
|
+
ast,
|
|
176
|
+
{
|
|
177
|
+
retainLines: false,
|
|
178
|
+
// Compact output; set true if you need line parity
|
|
179
|
+
compact: false,
|
|
180
|
+
// Keep human-readable whitespace
|
|
181
|
+
concise: false,
|
|
182
|
+
comments: true,
|
|
183
|
+
// Include non-console comments
|
|
184
|
+
jsescOption: {
|
|
185
|
+
minimal: true
|
|
186
|
+
// Don't over-escape unicode
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
code
|
|
190
|
+
// Original source (used for sourcemap generation)
|
|
191
|
+
);
|
|
192
|
+
const removedCount = removedMethods.length;
|
|
193
|
+
return {
|
|
194
|
+
code: transformedCode,
|
|
195
|
+
removedCount,
|
|
196
|
+
removedMethods,
|
|
197
|
+
changed: removedCount > 0
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function removeConsoleComments(ast) {
|
|
201
|
+
function filterComments(comments) {
|
|
202
|
+
if (!comments || comments.length === 0) return comments ?? null;
|
|
203
|
+
return comments.filter(
|
|
204
|
+
(comment) => !comment.value.includes(CONSOLE_KEYWORD)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
traverse(ast, {
|
|
208
|
+
enter(nodePath) {
|
|
209
|
+
const { node } = nodePath;
|
|
210
|
+
if (node.leadingComments) {
|
|
211
|
+
const filtered = filterComments(node.leadingComments);
|
|
212
|
+
node.leadingComments = filtered;
|
|
213
|
+
}
|
|
214
|
+
if (node.trailingComments) {
|
|
215
|
+
const filtered = filterComments(node.trailingComments);
|
|
216
|
+
node.trailingComments = filtered;
|
|
217
|
+
}
|
|
218
|
+
if (node.innerComments) {
|
|
219
|
+
const filtered = filterComments(node.innerComments);
|
|
220
|
+
node.innerComments = filtered;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/cli/fileScanner.ts
|
|
227
|
+
async function discoverFiles(targets, excludePatterns = []) {
|
|
228
|
+
const patterns = targets.map((target) => {
|
|
229
|
+
const normalized = target.replace(/\\/g, "/");
|
|
230
|
+
if (!normalized.includes("*") && !normalized.includes("?")) {
|
|
231
|
+
return `${normalized}/**/*{${SUPPORTED_EXTENSIONS.join(",")}}`;
|
|
232
|
+
}
|
|
233
|
+
return normalized;
|
|
234
|
+
});
|
|
235
|
+
const ignorePatterns = [
|
|
236
|
+
...DEFAULT_EXCLUDE_PATTERNS,
|
|
237
|
+
...excludePatterns
|
|
238
|
+
];
|
|
239
|
+
const files = await glob(patterns, {
|
|
240
|
+
ignore: ignorePatterns,
|
|
241
|
+
absolute: true,
|
|
242
|
+
// Return absolute paths
|
|
243
|
+
onlyFiles: true,
|
|
244
|
+
// Skip directories
|
|
245
|
+
followSymbolicLinks: false,
|
|
246
|
+
unique: true
|
|
247
|
+
// Deduplicate
|
|
248
|
+
});
|
|
249
|
+
return files;
|
|
250
|
+
}
|
|
251
|
+
async function processFile(filePath, stripOpts = {}, dryRun = false) {
|
|
252
|
+
const baseResult = {
|
|
253
|
+
filePath,
|
|
254
|
+
changed: false,
|
|
255
|
+
removedCount: 0,
|
|
256
|
+
removedMethods: [],
|
|
257
|
+
dryRun,
|
|
258
|
+
error: null
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
const originalCode = await fs.readFile(filePath, "utf-8");
|
|
262
|
+
const { code, removedCount, removedMethods, changed } = stripConsoleFromCode(originalCode, stripOpts);
|
|
263
|
+
if (changed && !dryRun) {
|
|
264
|
+
await fs.writeFile(filePath, code, "utf-8");
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
...baseResult,
|
|
268
|
+
changed,
|
|
269
|
+
removedCount,
|
|
270
|
+
removedMethods
|
|
271
|
+
};
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return {
|
|
274
|
+
...baseResult,
|
|
275
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function processFiles(filePaths, stripOpts = {}, dryRun = false, concurrency = 10) {
|
|
280
|
+
const results = [];
|
|
281
|
+
for (let i = 0; i < filePaths.length; i += concurrency) {
|
|
282
|
+
const chunk = filePaths.slice(i, i + concurrency);
|
|
283
|
+
const chunkResults = await Promise.all(
|
|
284
|
+
chunk.map((filePath) => processFile(filePath, stripOpts, dryRun))
|
|
285
|
+
);
|
|
286
|
+
results.push(...chunkResults);
|
|
287
|
+
}
|
|
288
|
+
return results;
|
|
289
|
+
}
|
|
290
|
+
function computeSummary(results) {
|
|
291
|
+
return {
|
|
292
|
+
totalFiles: results.length,
|
|
293
|
+
modifiedFiles: results.filter((r) => r.changed).length,
|
|
294
|
+
errorFiles: results.filter((r) => r.error !== null).length,
|
|
295
|
+
totalRemoved: results.reduce((acc, r) => acc + r.removedCount, 0)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/cli/logger.ts
|
|
300
|
+
import pc2 from "picocolors";
|
|
301
|
+
var PREFIX = {
|
|
302
|
+
info: pc2.cyan("\u2139"),
|
|
303
|
+
success: pc2.green("\u2713"),
|
|
304
|
+
warn: pc2.yellow("\u26A0"),
|
|
305
|
+
error: pc2.red("\u2717"),
|
|
306
|
+
skip: pc2.dim("\xB7"),
|
|
307
|
+
snipe: pc2.magenta("\u2298")
|
|
308
|
+
// Used when we actually remove something
|
|
309
|
+
};
|
|
310
|
+
function logInfo(message, silent = false) {
|
|
311
|
+
if (silent) return;
|
|
312
|
+
console.log(`${PREFIX.info} ${message}`);
|
|
313
|
+
}
|
|
314
|
+
function logWarn(message, silent = false) {
|
|
315
|
+
if (silent) return;
|
|
316
|
+
console.warn(`${PREFIX.warn} ${pc2.yellow(message)}`);
|
|
317
|
+
}
|
|
318
|
+
function logError(message, silent = false) {
|
|
319
|
+
if (silent) return;
|
|
320
|
+
console.error(`${PREFIX.error} ${pc2.red(message)}`);
|
|
321
|
+
}
|
|
322
|
+
function logFileResult(result, verbose = false, silent = false) {
|
|
323
|
+
if (silent) return;
|
|
324
|
+
const { filePath, changed, removedCount, removedMethods, dryRun, error } = result;
|
|
325
|
+
const shortPath = pc2.dim(shortenPath(filePath));
|
|
326
|
+
if (error) {
|
|
327
|
+
console.log(
|
|
328
|
+
`${PREFIX.error} ${shortPath} ${pc2.red(`Error: ${error.message}`)}`
|
|
329
|
+
);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (!changed) {
|
|
333
|
+
if (verbose) {
|
|
334
|
+
console.log(`${PREFIX.skip} ${shortPath} ${pc2.dim("no changes")}`);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const methodCounts = countBy(removedMethods);
|
|
339
|
+
const methodSummary = Object.entries(methodCounts).map(([method, count]) => `${pc2.cyan("console." + method)} \xD7${count}`).join(", ");
|
|
340
|
+
const dryRunLabel = dryRun ? pc2.yellow(" [dry-run]") : "";
|
|
341
|
+
const removedLabel = pc2.green(
|
|
342
|
+
`removed ${formatCount(removedCount, "call")}`
|
|
343
|
+
);
|
|
344
|
+
console.log(
|
|
345
|
+
`${PREFIX.snipe} ${shortPath} ${removedLabel} ${pc2.dim("\u2192")} ${methodSummary}${dryRunLabel}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
function logSummary(summary, silent = false) {
|
|
349
|
+
if (silent) return;
|
|
350
|
+
const { totalFiles, modifiedFiles, errorFiles, totalRemoved } = summary;
|
|
351
|
+
console.log("");
|
|
352
|
+
console.log(pc2.bold("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
353
|
+
console.log(pc2.bold(" Summary"));
|
|
354
|
+
console.log(pc2.bold("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
355
|
+
console.log(
|
|
356
|
+
` ${pc2.dim("Files scanned:")} ${pc2.white(String(totalFiles))}`
|
|
357
|
+
);
|
|
358
|
+
console.log(
|
|
359
|
+
` ${pc2.dim("Files modified:")} ${modifiedFiles > 0 ? pc2.green(String(modifiedFiles)) : pc2.dim("0")}`
|
|
360
|
+
);
|
|
361
|
+
if (errorFiles > 0) {
|
|
362
|
+
console.log(
|
|
363
|
+
` ${pc2.dim("Files errored:")} ${pc2.red(String(errorFiles))}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
console.log(
|
|
367
|
+
` ${pc2.dim("Calls removed:")} ${totalRemoved > 0 ? pc2.magenta(String(totalRemoved)) : pc2.dim("0")}`
|
|
368
|
+
);
|
|
369
|
+
console.log(pc2.bold("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
370
|
+
if (totalRemoved === 0) {
|
|
371
|
+
console.log(pc2.dim(" Nothing to remove. Your code is already clean!"));
|
|
372
|
+
} else {
|
|
373
|
+
console.log(
|
|
374
|
+
pc2.green(
|
|
375
|
+
` \u2713 Removed ${formatCount(totalRemoved, "console statement")} from ${formatCount(modifiedFiles, "file")}.`
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
console.log("");
|
|
380
|
+
}
|
|
381
|
+
function shortenPath(filePath) {
|
|
382
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
383
|
+
return parts.slice(-3).join("/");
|
|
384
|
+
}
|
|
385
|
+
function countBy(items) {
|
|
386
|
+
const counts = {};
|
|
387
|
+
for (const item of items) {
|
|
388
|
+
counts[item] = (counts[item] ?? 0) + 1;
|
|
389
|
+
}
|
|
390
|
+
return counts;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/cli/cli.ts
|
|
394
|
+
var program = new Command();
|
|
395
|
+
program.name(PACKAGE_NAME).description(
|
|
396
|
+
"Remove console.* statements from JavaScript and TypeScript source files using AST parsing."
|
|
397
|
+
).version(PACKAGE_VERSION, "-v, --version", "Print the current version").argument(
|
|
398
|
+
"[targets...]",
|
|
399
|
+
"Files or directories to process (supports glob patterns)",
|
|
400
|
+
["src"]
|
|
401
|
+
// Default to processing "src/"
|
|
402
|
+
).option(
|
|
403
|
+
"-m, --methods <methods>",
|
|
404
|
+
`Comma-separated list of console methods to remove`,
|
|
405
|
+
DEFAULT_METHODS.join(",")
|
|
406
|
+
).option(
|
|
407
|
+
"--no-comments",
|
|
408
|
+
"Keep comments that reference console (default: remove them)"
|
|
409
|
+
).option(
|
|
410
|
+
"-e, --exclude <patterns...>",
|
|
411
|
+
"Glob patterns to exclude (can be repeated: -e *.test.ts -e dist/**)"
|
|
412
|
+
).option(
|
|
413
|
+
"-d, --dry-run",
|
|
414
|
+
"Preview changes without modifying any files",
|
|
415
|
+
false
|
|
416
|
+
).option(
|
|
417
|
+
"-s, --silent",
|
|
418
|
+
"Suppress all output (exit code still reflects success/failure)",
|
|
419
|
+
false
|
|
420
|
+
).option(
|
|
421
|
+
"--verbose",
|
|
422
|
+
"Show details for every file, including unchanged ones",
|
|
423
|
+
false
|
|
424
|
+
).option(
|
|
425
|
+
"--no-banner",
|
|
426
|
+
"Skip the ASCII art banner"
|
|
427
|
+
).action(async (targets, options) => {
|
|
428
|
+
const {
|
|
429
|
+
methods: methodsRaw,
|
|
430
|
+
comments: removeComments,
|
|
431
|
+
exclude = [],
|
|
432
|
+
dryRun,
|
|
433
|
+
silent,
|
|
434
|
+
verbose,
|
|
435
|
+
banner: showBanner
|
|
436
|
+
} = options;
|
|
437
|
+
if (showBanner && !silent) {
|
|
438
|
+
printBanner();
|
|
439
|
+
} else if (!silent) {
|
|
440
|
+
console.log(compactBanner());
|
|
441
|
+
console.log("");
|
|
442
|
+
}
|
|
443
|
+
const methods = parseMethodList(methodsRaw);
|
|
444
|
+
if (!silent) {
|
|
445
|
+
logInfo(`Targeting methods: ${methods.map((m) => pc3.cyan(`console.${m}`)).join(", ")}`);
|
|
446
|
+
if (dryRun) {
|
|
447
|
+
logInfo(pc3.yellow("Dry-run mode: no files will be modified"));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
let filePaths;
|
|
451
|
+
try {
|
|
452
|
+
filePaths = await discoverFiles(targets, exclude);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
logError(
|
|
455
|
+
`Failed to scan files: ${err instanceof Error ? err.message : String(err)}`,
|
|
456
|
+
silent
|
|
457
|
+
);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
if (filePaths.length === 0) {
|
|
461
|
+
logWarn(`No files found in: ${targets.join(", ")}`, silent);
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
if (!silent) {
|
|
465
|
+
logInfo(`Found ${pc3.white(String(filePaths.length))} files to process...
|
|
466
|
+
`);
|
|
467
|
+
}
|
|
468
|
+
const stripOptions = {
|
|
469
|
+
methods,
|
|
470
|
+
removeComments,
|
|
471
|
+
silent: true
|
|
472
|
+
// Suppress core-level logging; we handle it here
|
|
473
|
+
};
|
|
474
|
+
const results = await processFiles(filePaths, stripOptions, dryRun);
|
|
475
|
+
for (const result of results) {
|
|
476
|
+
logFileResult(result, verbose, silent);
|
|
477
|
+
}
|
|
478
|
+
const summary = computeSummary(results);
|
|
479
|
+
logSummary(summary, silent);
|
|
480
|
+
const hasErrors = results.some((r) => r.error !== null);
|
|
481
|
+
if (hasErrors) {
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
486
|
+
console.error(
|
|
487
|
+
pc3.red(`
|
|
488
|
+
[console-sniper] Fatal error: ${err instanceof Error ? err.message : String(err)}
|
|
489
|
+
`)
|
|
490
|
+
);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
});
|
|
493
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/cli.ts","../../src/core/constants.ts","../../src/core/utils.ts","../../src/shared/banner.ts","../../src/cli/fileScanner.ts","../../src/core/stripConsole.ts","../../src/cli/logger.ts"],"sourcesContent":["#!/usr/bin/env node\r\n\r\n/**\r\n * @file cli.ts\r\n * @description The console-sniper CLI entry point.\r\n *\r\n * Uses `commander` for argument parsing — it handles --help, --version,\r\n * and validation automatically. The CLI orchestrates:\r\n * 1. Parse arguments\r\n * 2. Discover files (fileScanner.ts)\r\n * 3. Process files (core stripConsole engine)\r\n * 4. Print results (logger.ts)\r\n *\r\n * Usage:\r\n * console-sniper src/\r\n * console-sniper src/ --methods log,warn\r\n * console-sniper src/ --dry-run\r\n * console-sniper src/ --exclude \"*.test.ts\" --verbose\r\n */\r\n\r\nimport { Command } from \"commander\";\r\nimport pc from \"picocolors\";\r\n\r\nimport { PACKAGE_NAME, PACKAGE_VERSION, DEFAULT_METHODS } from \"../core/constants.js\";\r\nimport { parseMethodList } from \"../core/utils.js\";\r\nimport { printBanner, compactBanner } from \"../shared/banner.js\";\r\nimport {\r\n discoverFiles,\r\n processFiles,\r\n computeSummary,\r\n} from \"./fileScanner.js\";\r\nimport {\r\n logInfo,\r\n logWarn,\r\n logError,\r\n logFileResult,\r\n logSummary,\r\n} from \"./logger.js\";\r\n\r\n\r\n// CLI Definition\r\n\r\n\r\nconst program = new Command();\r\n\r\nprogram\r\n .name(PACKAGE_NAME)\r\n .description(\r\n \"Remove console.* statements from JavaScript and TypeScript source files using AST parsing.\"\r\n )\r\n .version(PACKAGE_VERSION, \"-v, --version\", \"Print the current version\")\r\n\r\n // ── Positional argument ─────────────────────────────────────\r\n .argument(\r\n \"[targets...]\",\r\n \"Files or directories to process (supports glob patterns)\",\r\n [\"src\"] // Default to processing \"src/\"\r\n )\r\n\r\n // ── Options ─────────────────────────────────────────────────\r\n .option(\r\n \"-m, --methods <methods>\",\r\n `Comma-separated list of console methods to remove`,\r\n DEFAULT_METHODS.join(\",\")\r\n )\r\n .option(\r\n \"--no-comments\",\r\n \"Keep comments that reference console (default: remove them)\"\r\n )\r\n .option(\r\n \"-e, --exclude <patterns...>\",\r\n \"Glob patterns to exclude (can be repeated: -e *.test.ts -e dist/**)\"\r\n )\r\n .option(\r\n \"-d, --dry-run\",\r\n \"Preview changes without modifying any files\",\r\n false\r\n )\r\n .option(\r\n \"-s, --silent\",\r\n \"Suppress all output (exit code still reflects success/failure)\",\r\n false\r\n )\r\n .option(\r\n \"--verbose\",\r\n \"Show details for every file, including unchanged ones\",\r\n false\r\n )\r\n .option(\r\n \"--no-banner\",\r\n \"Skip the ASCII art banner\"\r\n )\r\n\r\n // ── Action ─────────────────────────────────────────────────\r\n .action(async (targets: string[], options) => {\r\n const {\r\n methods: methodsRaw,\r\n comments: removeComments,\r\n exclude = [],\r\n dryRun,\r\n silent,\r\n verbose,\r\n banner: showBanner,\r\n } = options;\r\n\r\n // ── Banner ─────────────────────────────────────────────\r\n if (showBanner && !silent) {\r\n printBanner();\r\n } else if (!silent) {\r\n console.log(compactBanner());\r\n console.log(\"\");\r\n }\r\n\r\n // ── Parse methods ─────────────────────────────────────\r\n const methods = parseMethodList(methodsRaw);\r\n\r\n if (!silent) {\r\n logInfo(`Targeting methods: ${methods.map((m) => pc.cyan(`console.${m}`)).join(\", \")}`);\r\n\r\n if (dryRun) {\r\n logInfo(pc.yellow(\"Dry-run mode: no files will be modified\"));\r\n }\r\n }\r\n\r\n // ── Discover files ────────────────────────────────────\r\n let filePaths: string[];\r\n\r\n try {\r\n filePaths = await discoverFiles(targets, exclude);\r\n } catch (err) {\r\n logError(\r\n `Failed to scan files: ${err instanceof Error ? err.message : String(err)}`,\r\n silent\r\n );\r\n process.exit(1);\r\n }\r\n\r\n if (filePaths.length === 0) {\r\n logWarn(`No files found in: ${targets.join(\", \")}`, silent);\r\n process.exit(0);\r\n }\r\n\r\n if (!silent) {\r\n logInfo(`Found ${pc.white(String(filePaths.length))} files to process...\\n`);\r\n }\r\n\r\n // ── Process files ─────────────────────────────────────\r\n const stripOptions = {\r\n methods,\r\n removeComments,\r\n silent: true, // Suppress core-level logging; we handle it here\r\n };\r\n\r\n const results = await processFiles(filePaths, stripOptions, dryRun);\r\n\r\n // ── Print per-file results ────────────────────────────\r\n for (const result of results) {\r\n logFileResult(result, verbose, silent);\r\n }\r\n\r\n // ── Print summary ─────────────────────────────────────\r\n const summary = computeSummary(results);\r\n logSummary(summary, silent);\r\n\r\n // ── Exit code ─────────────────────────────────────────\r\n // Exit 1 if any files had errors — useful in CI pipelines\r\n const hasErrors = results.some((r) => r.error !== null);\r\n if (hasErrors) {\r\n process.exit(1);\r\n }\r\n });\r\n\r\n\r\n// Parse and Run\r\n\r\n\r\nprogram.parseAsync(process.argv).catch((err: unknown) => {\r\n console.error(\r\n pc.red(`\\n[console-sniper] Fatal error: ${err instanceof Error ? err.message : String(err)}\\n`)\r\n );\r\n process.exit(1);\r\n});\r\n","/**\r\n * @file constants.ts\r\n * @description Global constants used across the project.\r\n *\r\n * Centralizing magic strings and default values here prevents typos\r\n * and makes it trivial to change defaults in one place.\r\n */\r\n\r\n\r\n// Package Identity\r\n\r\n/** The package name — used in logs, banners, and plugin names. */\r\nexport const PACKAGE_NAME = \"console-sniper\";\r\n\r\n/** Current version — ideally sync'd with package.json at build time. */\r\nexport const PACKAGE_VERSION = \"1.0.0\";\r\n\r\n\r\n// Console Stripping Defaults\r\n\r\n/**\r\n * Default console methods that are removed when `methods` is not specified.\r\n *\r\n * Extending this list is the primary way to support new console variants\r\n * like `console.table`, `console.time`, `console.group`, etc.\r\n */\r\nexport const DEFAULT_METHODS: readonly string[] = [\r\n \"log\",\r\n \"warn\",\r\n \"error\",\r\n \"info\",\r\n \"debug\",\r\n] as const;\r\n\r\n/**\r\n * Whether to remove comments containing \"console\" by default.\r\n */\r\nexport const DEFAULT_REMOVE_COMMENTS = true;\r\n\r\n\r\n// File Extension Filters\r\n\r\n/**\r\n * File extensions that the Babel parser can handle.\r\n * These are used by the CLI file scanner to filter which files to process.\r\n *\r\n * Note: The Babel parser handles TypeScript and JSX natively via plugins.\r\n */\r\nexport const SUPPORTED_EXTENSIONS: readonly string[] = [\r\n \".js\",\r\n \".mjs\",\r\n \".cjs\",\r\n \".jsx\",\r\n \".ts\",\r\n \".tsx\",\r\n \".mts\",\r\n \".cts\",\r\n] as const;\r\n\r\n/**\r\n * Default glob patterns for the CLI to EXCLUDE when scanning directories.\r\n */\r\nexport const DEFAULT_EXCLUDE_PATTERNS: readonly string[] = [\r\n \"**/node_modules/**\",\r\n \"**/dist/**\",\r\n \"**/build/**\",\r\n \"**/.git/**\",\r\n \"**/*.min.js\",\r\n \"**/*.d.ts\",\r\n] as const;\r\n\r\n// AST Parser Config\r\n\r\n/**\r\n * The word we look for when scanning comment text.\r\n * Keeping this as a constant prevents subtle bugs from typos.\r\n */\r\nexport const CONSOLE_KEYWORD = \"console\";\r\n","/**\r\n * @file utils.ts\r\n * @description Pure utility functions shared across the core engine.\r\n */\r\n\r\nimport path from \"node:path\";\r\nimport { SUPPORTED_EXTENSIONS, DEFAULT_METHODS, DEFAULT_REMOVE_COMMENTS } from \"./constants.js\";\r\nimport type { StripConsoleOptions } from \"./types.js\";\r\n\r\nexport function resolveOptionsSync(\r\n options?: StripConsoleOptions\r\n): StripConsoleOptions & {\r\n methods: string[];\r\n removeComments: boolean;\r\n silent: boolean;\r\n} {\r\n return {\r\n methods: options?.methods?.length ? options.methods : [...DEFAULT_METHODS],\r\n removeComments: options?.removeComments ?? DEFAULT_REMOVE_COMMENTS,\r\n include: options?.include ?? [],\r\n exclude: options?.exclude ?? [],\r\n silent: options?.silent ?? false,\r\n };\r\n}\r\n\r\nexport function isSupportedFile(filePath: string): boolean {\r\n const ext = path.extname(filePath).toLowerCase();\r\n return (SUPPORTED_EXTENSIONS as readonly string[]).includes(ext);\r\n}\r\n\r\nexport function shouldProcessFile(\r\n filePath: string,\r\n include: RegExp[] = [],\r\n exclude: RegExp[] = []\r\n): boolean {\r\n const normalized = filePath.replace(/\\\\/g, \"/\");\r\n if (include.length > 0 && !include.some((p) => p.test(normalized))) return false;\r\n if (exclude.length > 0 && exclude.some((p) => p.test(normalized))) return false;\r\n return true;\r\n}\r\n\r\nexport function parseMethodList(input: string): string[] {\r\n return input.split(\",\").map((m) => m.trim()).filter((m) => m.length > 0);\r\n}\r\n\r\nexport function formatCount(count: number, noun: string): string {\r\n return `${count} ${noun}${count !== 1 ? \"s\" : \"\"}`;\r\n}\r\n\r\nexport function isNode(): boolean {\r\n return typeof process !== \"undefined\" && process.versions?.node != null;\r\n}\r\n","/**\r\n * @file banner.ts\r\n * @description Renders the console-sniper ASCII banner and version info.\r\n *\r\n * Shared between CLI and any interactive entry points.\r\n * Kept in `shared/` so it never pulls in CLI or Vite dependencies.\r\n */\r\n\r\nimport pc from \"picocolors\";\r\nimport { PACKAGE_NAME, PACKAGE_VERSION } from \"../core/constants.js\";\r\n\r\n\r\n// ASCII Art Banner\r\n\r\n\r\nconst ASCII_BANNER = `\r\n ██████╗ ██████╗ ███╗ ██╗███████╗ ██████╗ ██╗ ███████╗\r\n██╔════╝██╔═══██╗████╗ ██║██╔════╝██╔═══██╗██║ ██╔════╝\r\n██║ ██║ ██║██╔██╗ ██║███████╗██║ ██║██║ █████╗ \r\n██║ ██║ ██║██║╚██╗██║╚════██║██║ ██║██║ ██╔══╝ \r\n╚██████╗╚██████╔╝██║ ╚████║███████║╚██████╔╝███████╗███████╗\r\n ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚══════╝╚══════╝\r\n███████╗███╗ ██╗██╗██████╗ ███████╗██████╗ \r\n██╔════╝████╗ ██║██║██╔══██╗██╔════╝██╔══██╗ \r\n███████╗██╔██╗ ██║██║██████╔╝█████╗ ██████╔╝ \r\n╚════██║██║╚██╗██║██║██╔═══╝ ██╔══╝ ██╔══██╗ \r\n███████║██║ ╚████║██║██║ ███████╗██║ ██║ \r\n╚══════╝╚═╝ ╚═══╝╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ \r\n`.trim();\r\n\r\n// Exports\r\n\r\n/**\r\n * Prints the full ASCII banner with version info to stdout.\r\n *\r\n * @param silent - If `true`, nothing is printed (for quiet/script mode)\r\n */\r\nexport function printBanner(silent = false): void {\r\n if (silent) return;\r\n\r\n console.log(pc.cyan(ASCII_BANNER));\r\n console.log(\r\n pc.dim(` ${PACKAGE_NAME} v${PACKAGE_VERSION}`) +\r\n pc.dim(\" — Remove console.* statements using AST parsing\\n\")\r\n );\r\n}\r\n\r\n/**\r\n * A single-line compact banner for when you want a header without the art.\r\n *\r\n * @example\r\n * \"🎯 console-sniper v1.0.0\"\r\n */\r\nexport function compactBanner(): string {\r\n return pc.cyan(`🎯 ${PACKAGE_NAME}`) + pc.dim(` v${PACKAGE_VERSION}`);\r\n}\r\n","/**\r\n * @file fileScanner.ts\r\n * @description Discovers files to process using fast-glob.\r\n *\r\n * This module handles the \"find which files to process\" step.\r\n * The actual processing is done by the core engine (stripConsole.ts).\r\n *\r\n * Separation of concerns:\r\n * fileScanner → discover files\r\n * stripConsole → transform code\r\n * cli → orchestrate and report\r\n */\r\n\r\nimport fs from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport glob from \"fast-glob\";\r\n\r\nimport { SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDE_PATTERNS } from \"../core/constants.js\";\r\nimport { stripConsoleFromCode } from \"../core/stripConsole.js\";\r\nimport type {\r\n StripConsoleOptions,\r\n FileProcessResult,\r\n BatchSummary,\r\n} from \"../core/types.js\";\r\n\r\n\r\n// File Discovery\r\n\r\n\r\n/**\r\n * Options for scanning a directory.\r\n */\r\nexport interface ScanOptions {\r\n /** Glob patterns or directories to scan. */\r\n targets: string[];\r\n\r\n /** Additional glob patterns to exclude. */\r\n exclude?: string[];\r\n\r\n /** If true, don't write files — just report what would change. */\r\n dryRun?: boolean;\r\n\r\n /** Suppress all output. */\r\n silent?: boolean;\r\n\r\n /** Options passed to the core strip engine. */\r\n stripOptions?: StripConsoleOptions;\r\n}\r\n\r\n/**\r\n * Discover all processable files in the given targets.\r\n *\r\n * Targets can be:\r\n * - A directory (recursively scanned)\r\n * - A glob pattern like \"src/**\\/*.ts\"\r\n * - A specific file path\r\n *\r\n * @returns Array of absolute file paths\r\n */\r\nexport async function discoverFiles(\r\n targets: string[],\r\n excludePatterns: string[] = []\r\n): Promise<string[]> {\r\n // Build glob patterns from targets\r\n // If a target is a directory, expand it to a recursive glob\r\n const patterns = targets.map((target) => {\r\n // Normalize path separators\r\n const normalized = target.replace(/\\\\/g, \"/\");\r\n\r\n // If it looks like a plain directory (no glob characters), make it recursive\r\n if (!normalized.includes(\"*\") && !normalized.includes(\"?\")) {\r\n // Could be a file or directory — use a pattern that handles both\r\n return `${normalized}/**/*{${SUPPORTED_EXTENSIONS.join(\",\")}}`;\r\n }\r\n\r\n return normalized;\r\n });\r\n\r\n // Merge default excludes with user-provided ones\r\n const ignorePatterns = [\r\n ...DEFAULT_EXCLUDE_PATTERNS,\r\n ...excludePatterns,\r\n ];\r\n\r\n const files = await glob(patterns, {\r\n ignore: ignorePatterns,\r\n absolute: true, // Return absolute paths\r\n onlyFiles: true, // Skip directories\r\n followSymbolicLinks: false,\r\n unique: true, // Deduplicate\r\n });\r\n\r\n return files;\r\n}\r\n\r\n\r\n// File Processing\r\n\r\n\r\n/**\r\n * Process a single file: read → strip → write (if changed & not dry-run).\r\n *\r\n * @param filePath - Absolute path to the file\r\n * @param stripOpts - Options for the core strip engine\r\n * @param dryRun - If true, don't write the file back\r\n * @returns - Processing result with metadata\r\n */\r\nexport async function processFile(\r\n filePath: string,\r\n stripOpts: StripConsoleOptions = {},\r\n dryRun = false\r\n): Promise<FileProcessResult> {\r\n // Base result skeleton — we fill this in below\r\n const baseResult: FileProcessResult = {\r\n filePath,\r\n changed: false,\r\n removedCount: 0,\r\n removedMethods: [],\r\n dryRun,\r\n error: null,\r\n };\r\n\r\n try {\r\n // Read the file\r\n const originalCode = await fs.readFile(filePath, \"utf-8\");\r\n\r\n // Run the core AST engine\r\n const { code, removedCount, removedMethods, changed } =\r\n stripConsoleFromCode(originalCode, stripOpts);\r\n\r\n // Write back if something changed and we're not in dry-run mode\r\n if (changed && !dryRun) {\r\n await fs.writeFile(filePath, code, \"utf-8\");\r\n }\r\n\r\n return {\r\n ...baseResult,\r\n changed,\r\n removedCount,\r\n removedMethods,\r\n };\r\n } catch (err) {\r\n return {\r\n ...baseResult,\r\n error: err instanceof Error ? err : new Error(String(err)),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Process a batch of files and return aggregated results.\r\n *\r\n * Files are processed concurrently with a concurrency limit to avoid\r\n * overwhelming the OS with too many open file handles.\r\n *\r\n * @param filePaths - Array of absolute file paths\r\n * @param stripOpts - Options passed to the strip engine\r\n * @param dryRun - If true, don't write files\r\n * @param concurrency - Max parallel file reads (default: 10)\r\n * @returns - Array of per-file results\r\n */\r\nexport async function processFiles(\r\n filePaths: string[],\r\n stripOpts: StripConsoleOptions = {},\r\n dryRun = false,\r\n concurrency = 10\r\n): Promise<FileProcessResult[]> {\r\n const results: FileProcessResult[] = [];\r\n\r\n // Process in chunks to limit concurrency\r\n for (let i = 0; i < filePaths.length; i += concurrency) {\r\n const chunk = filePaths.slice(i, i + concurrency);\r\n\r\n const chunkResults = await Promise.all(\r\n chunk.map((filePath) => processFile(filePath, stripOpts, dryRun))\r\n );\r\n\r\n results.push(...chunkResults);\r\n }\r\n\r\n return results;\r\n}\r\n\r\n/**\r\n * Compute a summary from an array of file results.\r\n *\r\n * @param results - Array of per-file processing results\r\n * @returns - Aggregated BatchSummary\r\n */\r\nexport function computeSummary(results: FileProcessResult[]): BatchSummary {\r\n return {\r\n totalFiles: results.length,\r\n modifiedFiles: results.filter((r) => r.changed).length,\r\n errorFiles: results.filter((r) => r.error !== null).length,\r\n totalRemoved: results.reduce((acc, r) => acc + r.removedCount, 0),\r\n };\r\n}\r\n","/**\r\n * @file stripConsole.ts\r\n * @description The AST-based core engine that removes console statements.\r\n *\r\n * ─── Architecture Note ────────────────────────────────────────────────────────\r\n * This file is the heart of console-sniper. It has ZERO dependencies on Vite,\r\n * the CLI, or Node.js file system APIs. It works purely on source code strings.\r\n *\r\n * This isolation means the same engine can power:\r\n * • The Vite plugin (src/vite/vitePlugin.ts)\r\n * • The CLI (src/cli/cli.ts)\r\n * • Future: Rollup, Webpack, esbuild, Bun plugins\r\n * • Any programmatic usage\r\n * ─────────────────────────────────────────────────────────────────────────────\r\n *\r\n * How it works (at a high level):\r\n * 1. Parse the source code into an AST (Abstract Syntax Tree)\r\n * 2. Walk the AST looking for `ExpressionStatement` nodes where the\r\n * expression is a `CallExpression` like `console.log(...)`\r\n * 3. Remove those nodes from the AST\r\n * 4. Optionally scan and remove comments referencing \"console\"\r\n * 5. Re-generate source code from the modified AST\r\n */\r\n\r\nimport { parse, type ParserOptions } from \"@babel/parser\";\r\nimport _traverse from \"@babel/traverse\";\r\nimport _generate from \"@babel/generator\";\r\nimport * as t from \"@babel/types\";\r\n\r\nimport { CONSOLE_KEYWORD } from \"./constants.js\";\r\nimport { resolveOptionsSync } from \"./utils.js\";\r\nimport type { StripConsoleOptions, StripConsoleResult } from \"./types.js\";\r\n\r\n\r\n// CJS/ESM interop fix\r\n\r\n// @babel/traverse and @babel/generator are CommonJS packages. When this\r\n// module is bundled as ESM (or loaded via ts-node/vitest), the default\r\n// import may resolve to the module object rather than the callable function.\r\n// The actual function is always available on `.default` in that case.\r\n// We normalise once here so every call site just works.\r\nconst traverse = (\r\n typeof _traverse === \"function\" ? _traverse : (_traverse as any).default\r\n) as typeof _traverse;\r\n\r\nconst generate = (\r\n typeof _generate === \"function\" ? _generate : (_generate as any).default\r\n) as typeof _generate;\r\n\r\n\r\n// Public API\r\n\r\n/**\r\n * Strip `console.*` calls from a source code string using AST parsing.\r\n *\r\n * This is the primary public API of console-sniper. It's pure and stateless —\r\n * give it code, get transformed code back. Nothing is written to disk here.\r\n *\r\n * @param code - The source code to transform (JS, TS, JSX, TSX)\r\n * @param options - Optional configuration\r\n * @returns - The transformed code + metadata about what was removed\r\n *\r\n * @example\r\n * ```ts\r\n * const { code, removedCount, changed } = stripConsoleFromCode(`\r\n * console.log(\"hello\");\r\n * const x = 1 + 2;\r\n * console.warn(\"something\");\r\n * `);\r\n * // code → \"\\nconst x = 1 + 2;\\n\"\r\n * // removed → 2\r\n * // changed → true\r\n * ```\r\n */\r\nexport function stripConsoleFromCode(\r\n code: string,\r\n options?: StripConsoleOptions\r\n): StripConsoleResult {\r\n // 1. Resolve options (fills in defaults for anything not specified)\r\n const opts = resolveOptionsSync(options);\r\n const methodsToRemove = new Set(opts.methods);\r\n\r\n // Track what we remove for the metadata result\r\n const removedMethods: string[] = [];\r\n\r\n // ── Step 1: Parse ────────────────────────────────────────────\r\n // We need to tell Babel's parser about all the syntax we might encounter.\r\n // Using all these plugins means a single parser config handles every file\r\n // type (JS, TS, JSX, TSX) without needing per-file configuration.\r\n const parserOptions: ParserOptions = {\r\n sourceType: \"module\", // Support import/export statements\r\n strictMode: false, // Don't throw on sloppy-mode code\r\n allowImportExportEverywhere: true,\r\n allowReturnOutsideFunction: true,\r\n allowSuperOutsideMethod: true,\r\n plugins: [\r\n \"typescript\", // TypeScript syntax (types, generics, decorators)\r\n \"jsx\", // JSX/TSX syntax\r\n \"decorators\", // @decorator syntax\r\n \"classProperties\",\r\n \"classPrivateProperties\",\r\n \"classPrivateMethods\",\r\n \"classStaticBlock\",\r\n \"dynamicImport\", // import(...)\r\n \"exportDefaultFrom\",\r\n \"exportNamespaceFrom\",\r\n \"nullishCoalescingOperator\", // ??\r\n \"optionalChaining\", // ?.\r\n \"optionalCatchBinding\",\r\n \"logicalAssignment\", // &&=, ||=, ??=\r\n \"numericSeparator\", // 1_000_000\r\n \"bigInt\",\r\n \"importMeta\", // import.meta\r\n \"topLevelAwait\",\r\n ],\r\n };\r\n\r\n let ast: ReturnType<typeof parse>;\r\n\r\n try {\r\n ast = parse(code, parserOptions);\r\n } catch (parseError) {\r\n // If parsing fails (e.g. unsupported syntax), return the original code\r\n // unchanged rather than crashing. This is safer for production builds.\r\n if (!opts.silent) {\r\n console.warn(\r\n `[console-sniper] Failed to parse code, skipping. Error: ${\r\n parseError instanceof Error ? parseError.message : String(parseError)\r\n }`\r\n );\r\n }\r\n return {\r\n code,\r\n removedCount: 0,\r\n removedMethods: [],\r\n changed: false,\r\n };\r\n }\r\n\r\n // ── Step 2: Traverse & Remove Console Calls ─────────────────\r\n //\r\n // We look for nodes matching this AST pattern:\r\n //\r\n // ExpressionStatement\r\n // └── CallExpression\r\n // ├── callee: MemberExpression\r\n // │ ├── object: Identifier { name: \"console\" }\r\n // │ └── property: Identifier { name: \"log\" | \"warn\" | ... }\r\n // └── arguments: [...]\r\n //\r\n // Matching the full ExpressionStatement (the statement wrapper) rather than\r\n // just the CallExpression lets us safely remove the entire line — including\r\n // the trailing semicolon — without leaving orphan syntax behind.\r\n\r\n traverse(ast, {\r\n ExpressionStatement(nodePath) {\r\n const { expression } = nodePath.node;\r\n\r\n // Must be a function call\r\n if (!t.isCallExpression(expression)) return;\r\n\r\n const { callee } = expression;\r\n\r\n // The callee must be a member expression (object.method)\r\n if (!t.isMemberExpression(callee)) return;\r\n\r\n const { object, property } = callee;\r\n\r\n // The object must be `console` (an Identifier with name \"console\")\r\n if (!t.isIdentifier(object, { name: \"console\" })) return;\r\n\r\n // The property must be one of our configured methods\r\n // Property can be either an Identifier (console.log) or\r\n // a StringLiteral (console[\"log\"]) — we handle both.\r\n let methodName: string | undefined;\r\n\r\n if (t.isIdentifier(property)) {\r\n methodName = property.name;\r\n } else if (t.isStringLiteral(property)) {\r\n methodName = property.value;\r\n }\r\n\r\n if (!methodName || !methodsToRemove.has(methodName)) return;\r\n\r\n // ✅ This is a console call we should remove!\r\n removedMethods.push(methodName);\r\n\r\n // `nodePath.remove()` is the safe Babel way to delete a node.\r\n // It properly handles parent references, scope bindings, etc.\r\n nodePath.remove();\r\n },\r\n });\r\n\r\n // ── Step 3: Remove Console Comments ─────────────────────────\r\n //\r\n // Babel attaches comments to the nearest AST node as `leadingComments`\r\n // or `trailingComments`. We need to scan ALL nodes and filter out\r\n // any comments that mention \"console\".\r\n //\r\n // We do this AFTER the traverse so we don't interfere with node removal.\r\n\r\n if (opts.removeComments) {\r\n removeConsoleComments(ast);\r\n }\r\n\r\n // ── Step 4: Re-generate Source Code ─────────────────────────\r\n //\r\n // @babel/generator walks the (now modified) AST and prints it back to a\r\n // string. We enable `retainLines` to preserve line numbers as much as\r\n // possible — this keeps source maps accurate and diffs readable.\r\n\r\n const { code: transformedCode } = generate(\r\n ast,\r\n {\r\n retainLines: false, // Compact output; set true if you need line parity\r\n compact: false, // Keep human-readable whitespace\r\n concise: false,\r\n comments: true, // Include non-console comments\r\n jsescOption: {\r\n minimal: true, // Don't over-escape unicode\r\n },\r\n },\r\n code // Original source (used for sourcemap generation)\r\n );\r\n\r\n const removedCount = removedMethods.length;\r\n\r\n return {\r\n code: transformedCode,\r\n removedCount,\r\n removedMethods,\r\n changed: removedCount > 0,\r\n };\r\n}\r\n\r\n// Internal Helpers\r\n\r\n/**\r\n * Walk the AST and strip comments that reference \"console\".\r\n *\r\n * Babel stores comments as arrays on AST nodes:\r\n * node.leadingComments — comments before the node\r\n * node.innerComments — comments inside empty blocks\r\n * node.trailingComments — comments after the node\r\n *\r\n * We filter each array, removing comments whose `value` contains the\r\n * CONSOLE_KEYWORD (\"console\").\r\n *\r\n * @param ast - The parsed AST (mutated in place)\r\n */\r\nfunction removeConsoleComments(ast: t.File): void {\r\n /**\r\n * Filter a comment array, removing any that mention \"console\".\r\n */\r\n function filterComments(\r\n comments: t.Comment[] | null | undefined\r\n ): t.Comment[] | null {\r\n if (!comments || comments.length === 0) return comments ?? null;\r\n\r\n return comments.filter(\r\n (comment) => !comment.value.includes(CONSOLE_KEYWORD)\r\n );\r\n }\r\n\r\n // We need to visit every node in the AST.\r\n // `traverse` with no specific node type visits everything.\r\n traverse(ast, {\r\n enter(nodePath) {\r\n const { node } = nodePath;\r\n\r\n // Filter leading comments (e.g. `// console.log here`)\r\n if (node.leadingComments) {\r\n const filtered = filterComments(node.leadingComments);\r\n // Babel uses `null` when there are no comments\r\n (node as t.Node & { leadingComments: t.Comment[] | null }).leadingComments =\r\n filtered;\r\n }\r\n\r\n // Filter trailing comments (e.g. `} // end console block`)\r\n if (node.trailingComments) {\r\n const filtered = filterComments(node.trailingComments);\r\n (node as t.Node & { trailingComments: t.Comment[] | null }).trailingComments =\r\n filtered;\r\n }\r\n\r\n // Filter inner comments (e.g. comments inside empty blocks)\r\n if (node.innerComments) {\r\n const filtered = filterComments(node.innerComments);\r\n (node as t.Node & { innerComments: t.Comment[] | null }).innerComments =\r\n filtered;\r\n }\r\n },\r\n });\r\n}\r\n","/**\r\n * @file logger.ts\r\n * @description Terminal output utilities for the CLI.\r\n *\r\n * Uses `picocolors` for color support — it's tiny, fast, and has no\r\n * dependencies. All log functions respect a `silent` flag.\r\n *\r\n * Design philosophy: every log line has a consistent structure so output\r\n * is easy to read at a glance during builds.\r\n */\r\n\r\nimport pc from \"picocolors\";\r\nimport { formatCount } from \"../core/utils.js\";\r\nimport type { BatchSummary, FileProcessResult } from \"../core/types.js\";\r\n\r\n\r\n// Log Level Prefixes\r\n\r\n\r\n/** Prefix symbols — visually distinct and keyboard-typeable. */\r\nconst PREFIX = {\r\n info: pc.cyan(\"ℹ\"),\r\n success: pc.green(\"✓\"),\r\n warn: pc.yellow(\"⚠\"),\r\n error: pc.red(\"✗\"),\r\n skip: pc.dim(\"·\"),\r\n snipe: pc.magenta(\"⊘\"), // Used when we actually remove something\r\n} as const;\r\n\r\n\r\n// Core Log Functions\r\n\r\n\r\n/**\r\n * Log an informational message.\r\n */\r\nexport function logInfo(message: string, silent = false): void {\r\n if (silent) return;\r\n console.log(`${PREFIX.info} ${message}`);\r\n}\r\n\r\n/**\r\n * Log a success message.\r\n */\r\nexport function logSuccess(message: string, silent = false): void {\r\n if (silent) return;\r\n console.log(`${PREFIX.success} ${message}`);\r\n}\r\n\r\n/**\r\n * Log a warning (goes to stderr).\r\n */\r\nexport function logWarn(message: string, silent = false): void {\r\n if (silent) return;\r\n console.warn(`${PREFIX.warn} ${pc.yellow(message)}`);\r\n}\r\n\r\n/**\r\n * Log an error (goes to stderr).\r\n */\r\nexport function logError(message: string, silent = false): void {\r\n if (silent) return;\r\n console.error(`${PREFIX.error} ${pc.red(message)}`);\r\n}\r\n\r\n\r\n// File-Level Logging\r\n\r\n\r\n/**\r\n * Log the result of processing a single file.\r\n *\r\n * @param result - The file processing result\r\n * @param verbose - If true, also show unchanged files\r\n * @param silent - If true, nothing is printed\r\n */\r\nexport function logFileResult(\r\n result: FileProcessResult,\r\n verbose = false,\r\n silent = false\r\n): void {\r\n if (silent) return;\r\n\r\n const { filePath, changed, removedCount, removedMethods, dryRun, error } =\r\n result;\r\n\r\n // Shorten the path for readability\r\n const shortPath = pc.dim(shortenPath(filePath));\r\n\r\n if (error) {\r\n console.log(\r\n `${PREFIX.error} ${shortPath} ${pc.red(`Error: ${error.message}`)}`\r\n );\r\n return;\r\n }\r\n\r\n if (!changed) {\r\n // Only show unchanged files in verbose mode — they're visual noise otherwise\r\n if (verbose) {\r\n console.log(`${PREFIX.skip} ${shortPath} ${pc.dim(\"no changes\")}`);\r\n }\r\n return;\r\n }\r\n\r\n // Format the removed methods as a readable list\r\n const methodCounts = countBy(removedMethods);\r\n const methodSummary = Object.entries(methodCounts)\r\n .map(([method, count]) => `${pc.cyan(\"console.\" + method)} ×${count}`)\r\n .join(\", \");\r\n\r\n const dryRunLabel = dryRun ? pc.yellow(\" [dry-run]\") : \"\";\r\n const removedLabel = pc.green(\r\n `removed ${formatCount(removedCount, \"call\")}`\r\n );\r\n\r\n console.log(\r\n `${PREFIX.snipe} ${shortPath} ${removedLabel} ${pc.dim(\"→\")} ${methodSummary}${dryRunLabel}`\r\n );\r\n}\r\n\r\n\r\n// Summary Logging\r\n\r\n\r\n/**\r\n * Print the final batch summary after all files are processed.\r\n */\r\nexport function logSummary(summary: BatchSummary, silent = false): void {\r\n if (silent) return;\r\n\r\n const { totalFiles, modifiedFiles, errorFiles, totalRemoved } = summary;\r\n\r\n console.log(\"\"); // blank line for breathing room\r\n console.log(pc.bold(\"─────────────────────────────────────\"));\r\n console.log(pc.bold(\" Summary\"));\r\n console.log(pc.bold(\"─────────────────────────────────────\"));\r\n\r\n console.log(\r\n ` ${pc.dim(\"Files scanned:\")} ${pc.white(String(totalFiles))}`\r\n );\r\n console.log(\r\n ` ${pc.dim(\"Files modified:\")} ${\r\n modifiedFiles > 0 ? pc.green(String(modifiedFiles)) : pc.dim(\"0\")\r\n }`\r\n );\r\n\r\n if (errorFiles > 0) {\r\n console.log(\r\n ` ${pc.dim(\"Files errored:\")} ${pc.red(String(errorFiles))}`\r\n );\r\n }\r\n\r\n console.log(\r\n ` ${pc.dim(\"Calls removed:\")} ${\r\n totalRemoved > 0 ? pc.magenta(String(totalRemoved)) : pc.dim(\"0\")\r\n }`\r\n );\r\n\r\n console.log(pc.bold(\"─────────────────────────────────────\"));\r\n\r\n if (totalRemoved === 0) {\r\n console.log(pc.dim(\" Nothing to remove. Your code is already clean!\"));\r\n } else {\r\n console.log(\r\n pc.green(\r\n ` ✓ Removed ${formatCount(totalRemoved, \"console statement\")} from ${formatCount(modifiedFiles, \"file\")}.`\r\n )\r\n );\r\n }\r\n\r\n console.log(\"\");\r\n}\r\n\r\n\r\n// Internal Helpers\r\n\r\n\r\n/**\r\n * Shorten an absolute path to just the last 3 segments for readability.\r\n *\r\n * @example\r\n * \"/home/user/project/src/components/App.tsx\"\r\n * → \"src/components/App.tsx\"\r\n */\r\nfunction shortenPath(filePath: string): string {\r\n const parts = filePath.replace(/\\\\/g, \"/\").split(\"/\");\r\n return parts.slice(-3).join(\"/\");\r\n}\r\n\r\n/**\r\n * Count occurrences of each item in an array.\r\n *\r\n * @example\r\n * countBy([\"log\", \"log\", \"warn\"]) // { log: 2, warn: 1 }\r\n */\r\nfunction countBy(items: string[]): Record<string, number> {\r\n const counts: Record<string, number> = {};\r\n for (const item of items) {\r\n counts[item] = (counts[item] ?? 0) + 1;\r\n }\r\n return counts;\r\n}\r\n"],"mappings":"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","names":["pc","pc","pc","pc","pc"]}
|