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.
@@ -0,0 +1,51 @@
1
+ import { Plugin } from 'vite';
2
+ import { V as VitePluginOptions } from '../types-J-TZoPXb.cjs';
3
+
4
+ /**
5
+ * @file vitePlugin.ts
6
+ * @description Vite plugin that strips console.* statements during builds.
7
+ *
8
+ * ─── How Vite plugins work ────────────────────────────────────────────────────
9
+ * Vite's plugin system is based on Rollup's. Each plugin can hook into
10
+ * different stages of the build lifecycle. We use the `transform` hook,
11
+ * which is called once per module with its source code and file ID.
12
+ *
13
+ * Our plugin:
14
+ * 1. Checks if the file should be processed (by extension + include/exclude)
15
+ * 2. Calls the core `stripConsoleFromCode` engine
16
+ * 3. Returns the transformed code (or null to skip the file)
17
+ *
18
+ * ─── Why Vite is in a separate file ─────────────────────────────────────────
19
+ * The core engine has ZERO Vite dependency. This means:
20
+ * - Users who only use the CLI don't pull in Vite types
21
+ * - The same engine can be used in Rollup, Webpack, or esbuild plugins
22
+ * - Unit tests can test the core without mocking Vite
23
+ * ─────────────────────────────────────────────────────────────────────────────
24
+ */
25
+
26
+ /**
27
+ * A Vite plugin that removes `console.*` statements from source files
28
+ * during the build process using AST-based transformation.
29
+ *
30
+ * @param options - Plugin configuration options
31
+ * @returns - A Vite plugin object
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // vite.config.ts
36
+ * import { defineConfig } from "vite";
37
+ * import consoleSniper from "console-sniper/vite";
38
+ *
39
+ * export default defineConfig({
40
+ * plugins: [
41
+ * consoleSniper({
42
+ * methods: ["log", "warn"],
43
+ * productionOnly: true,
44
+ * }),
45
+ * ],
46
+ * });
47
+ * ```
48
+ */
49
+ declare function consoleSniper(options?: VitePluginOptions): Plugin;
50
+
51
+ export { consoleSniper, consoleSniper as default };
@@ -0,0 +1,51 @@
1
+ import { Plugin } from 'vite';
2
+ import { V as VitePluginOptions } from '../types-J-TZoPXb.js';
3
+
4
+ /**
5
+ * @file vitePlugin.ts
6
+ * @description Vite plugin that strips console.* statements during builds.
7
+ *
8
+ * ─── How Vite plugins work ────────────────────────────────────────────────────
9
+ * Vite's plugin system is based on Rollup's. Each plugin can hook into
10
+ * different stages of the build lifecycle. We use the `transform` hook,
11
+ * which is called once per module with its source code and file ID.
12
+ *
13
+ * Our plugin:
14
+ * 1. Checks if the file should be processed (by extension + include/exclude)
15
+ * 2. Calls the core `stripConsoleFromCode` engine
16
+ * 3. Returns the transformed code (or null to skip the file)
17
+ *
18
+ * ─── Why Vite is in a separate file ─────────────────────────────────────────
19
+ * The core engine has ZERO Vite dependency. This means:
20
+ * - Users who only use the CLI don't pull in Vite types
21
+ * - The same engine can be used in Rollup, Webpack, or esbuild plugins
22
+ * - Unit tests can test the core without mocking Vite
23
+ * ─────────────────────────────────────────────────────────────────────────────
24
+ */
25
+
26
+ /**
27
+ * A Vite plugin that removes `console.*` statements from source files
28
+ * during the build process using AST-based transformation.
29
+ *
30
+ * @param options - Plugin configuration options
31
+ * @returns - A Vite plugin object
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // vite.config.ts
36
+ * import { defineConfig } from "vite";
37
+ * import consoleSniper from "console-sniper/vite";
38
+ *
39
+ * export default defineConfig({
40
+ * plugins: [
41
+ * consoleSniper({
42
+ * methods: ["log", "warn"],
43
+ * productionOnly: true,
44
+ * }),
45
+ * ],
46
+ * });
47
+ * ```
48
+ */
49
+ declare function consoleSniper(options?: VitePluginOptions): Plugin;
50
+
51
+ export { consoleSniper, consoleSniper as default };
@@ -0,0 +1,252 @@
1
+ // src/core/stripConsole.ts
2
+ import { parse } from "@babel/parser";
3
+ import _traverse from "@babel/traverse";
4
+ import _generate from "@babel/generator";
5
+ import * as t from "@babel/types";
6
+
7
+ // src/core/constants.ts
8
+ var PACKAGE_NAME = "console-sniper";
9
+ var DEFAULT_METHODS = [
10
+ "log",
11
+ "warn",
12
+ "error",
13
+ "info",
14
+ "debug"
15
+ ];
16
+ var DEFAULT_REMOVE_COMMENTS = true;
17
+ var SUPPORTED_EXTENSIONS = [
18
+ ".js",
19
+ ".mjs",
20
+ ".cjs",
21
+ ".jsx",
22
+ ".ts",
23
+ ".tsx",
24
+ ".mts",
25
+ ".cts"
26
+ ];
27
+ var CONSOLE_KEYWORD = "console";
28
+
29
+ // src/core/utils.ts
30
+ import path from "path";
31
+ function resolveOptionsSync(options) {
32
+ return {
33
+ methods: options?.methods?.length ? options.methods : [...DEFAULT_METHODS],
34
+ removeComments: options?.removeComments ?? DEFAULT_REMOVE_COMMENTS,
35
+ include: options?.include ?? [],
36
+ exclude: options?.exclude ?? [],
37
+ silent: options?.silent ?? false
38
+ };
39
+ }
40
+ function isSupportedFile(filePath) {
41
+ const ext = path.extname(filePath).toLowerCase();
42
+ return SUPPORTED_EXTENSIONS.includes(ext);
43
+ }
44
+ function shouldProcessFile(filePath, include = [], exclude = []) {
45
+ const normalized = filePath.replace(/\\/g, "/");
46
+ if (include.length > 0 && !include.some((p) => p.test(normalized))) return false;
47
+ if (exclude.length > 0 && exclude.some((p) => p.test(normalized))) return false;
48
+ return true;
49
+ }
50
+
51
+ // src/core/stripConsole.ts
52
+ var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
53
+ var generate = typeof _generate === "function" ? _generate : _generate.default;
54
+ function stripConsoleFromCode(code, options) {
55
+ const opts = resolveOptionsSync(options);
56
+ const methodsToRemove = new Set(opts.methods);
57
+ const removedMethods = [];
58
+ const parserOptions = {
59
+ sourceType: "module",
60
+ // Support import/export statements
61
+ strictMode: false,
62
+ // Don't throw on sloppy-mode code
63
+ allowImportExportEverywhere: true,
64
+ allowReturnOutsideFunction: true,
65
+ allowSuperOutsideMethod: true,
66
+ plugins: [
67
+ "typescript",
68
+ // TypeScript syntax (types, generics, decorators)
69
+ "jsx",
70
+ // JSX/TSX syntax
71
+ "decorators",
72
+ // @decorator syntax
73
+ "classProperties",
74
+ "classPrivateProperties",
75
+ "classPrivateMethods",
76
+ "classStaticBlock",
77
+ "dynamicImport",
78
+ // import(...)
79
+ "exportDefaultFrom",
80
+ "exportNamespaceFrom",
81
+ "nullishCoalescingOperator",
82
+ // ??
83
+ "optionalChaining",
84
+ // ?.
85
+ "optionalCatchBinding",
86
+ "logicalAssignment",
87
+ // &&=, ||=, ??=
88
+ "numericSeparator",
89
+ // 1_000_000
90
+ "bigInt",
91
+ "importMeta",
92
+ // import.meta
93
+ "topLevelAwait"
94
+ ]
95
+ };
96
+ let ast;
97
+ try {
98
+ ast = parse(code, parserOptions);
99
+ } catch (parseError) {
100
+ if (!opts.silent) {
101
+ console.warn(
102
+ `[console-sniper] Failed to parse code, skipping. Error: ${parseError instanceof Error ? parseError.message : String(parseError)}`
103
+ );
104
+ }
105
+ return {
106
+ code,
107
+ removedCount: 0,
108
+ removedMethods: [],
109
+ changed: false
110
+ };
111
+ }
112
+ traverse(ast, {
113
+ ExpressionStatement(nodePath) {
114
+ const { expression } = nodePath.node;
115
+ if (!t.isCallExpression(expression)) return;
116
+ const { callee } = expression;
117
+ if (!t.isMemberExpression(callee)) return;
118
+ const { object, property } = callee;
119
+ if (!t.isIdentifier(object, { name: "console" })) return;
120
+ let methodName;
121
+ if (t.isIdentifier(property)) {
122
+ methodName = property.name;
123
+ } else if (t.isStringLiteral(property)) {
124
+ methodName = property.value;
125
+ }
126
+ if (!methodName || !methodsToRemove.has(methodName)) return;
127
+ removedMethods.push(methodName);
128
+ nodePath.remove();
129
+ }
130
+ });
131
+ if (opts.removeComments) {
132
+ removeConsoleComments(ast);
133
+ }
134
+ const { code: transformedCode } = generate(
135
+ ast,
136
+ {
137
+ retainLines: false,
138
+ // Compact output; set true if you need line parity
139
+ compact: false,
140
+ // Keep human-readable whitespace
141
+ concise: false,
142
+ comments: true,
143
+ // Include non-console comments
144
+ jsescOption: {
145
+ minimal: true
146
+ // Don't over-escape unicode
147
+ }
148
+ },
149
+ code
150
+ // Original source (used for sourcemap generation)
151
+ );
152
+ const removedCount = removedMethods.length;
153
+ return {
154
+ code: transformedCode,
155
+ removedCount,
156
+ removedMethods,
157
+ changed: removedCount > 0
158
+ };
159
+ }
160
+ function removeConsoleComments(ast) {
161
+ function filterComments(comments) {
162
+ if (!comments || comments.length === 0) return comments ?? null;
163
+ return comments.filter(
164
+ (comment) => !comment.value.includes(CONSOLE_KEYWORD)
165
+ );
166
+ }
167
+ traverse(ast, {
168
+ enter(nodePath) {
169
+ const { node } = nodePath;
170
+ if (node.leadingComments) {
171
+ const filtered = filterComments(node.leadingComments);
172
+ node.leadingComments = filtered;
173
+ }
174
+ if (node.trailingComments) {
175
+ const filtered = filterComments(node.trailingComments);
176
+ node.trailingComments = filtered;
177
+ }
178
+ if (node.innerComments) {
179
+ const filtered = filterComments(node.innerComments);
180
+ node.innerComments = filtered;
181
+ }
182
+ }
183
+ });
184
+ }
185
+
186
+ // src/vite/vitePlugin.ts
187
+ function consoleSniper(options = {}) {
188
+ const {
189
+ productionOnly = true,
190
+ // Only strip in production by default
191
+ methods,
192
+ removeComments,
193
+ include = [],
194
+ exclude = [],
195
+ silent = false
196
+ } = options;
197
+ let isProduction = false;
198
+ return {
199
+ // ── Plugin metadata ─────────────────────────────────────
200
+ name: PACKAGE_NAME,
201
+ // `enforce: "pre"` makes our transform run before other plugins.
202
+ // This is important because we want to strip consoles from the
203
+ // original source, not from already-transformed code.
204
+ enforce: "pre",
205
+ // ── configResolved hook ─────────────────────────────────
206
+ // Called once after Vite has merged all config. This is where we
207
+ // learn whether we're in production or development mode.
208
+ configResolved(config) {
209
+ isProduction = config.command === "build" && config.mode === "production";
210
+ if (config.command === "build" && !config.mode) {
211
+ isProduction = true;
212
+ }
213
+ if (!silent && !productionOnly) {
214
+ console.warn(
215
+ `[${PACKAGE_NAME}] productionOnly is false \u2014 console statements will be removed in development mode too.`
216
+ );
217
+ }
218
+ },
219
+ // ── transform hook ─────────────────────────────────────
220
+ // Called for every module that Vite processes.
221
+ // Return `null` to pass the file through unchanged.
222
+ // Return `{ code, map }` to provide a transformation.
223
+ transform(code, id) {
224
+ if (productionOnly && !isProduction) return null;
225
+ if (!isSupportedFile(id)) return null;
226
+ if (id.startsWith("\0")) return null;
227
+ if (!shouldProcessFile(id, include, exclude)) return null;
228
+ const stripOptions = { silent: true };
229
+ if (methods) stripOptions.methods = methods;
230
+ if (removeComments !== void 0) stripOptions.removeComments = removeComments;
231
+ const result = stripConsoleFromCode(code, stripOptions);
232
+ if (!result.changed) return null;
233
+ if (!silent && result.removedCount > 0) {
234
+ const shortId = id.split("/").slice(-2).join("/");
235
+ console.log(
236
+ `[${PACKAGE_NAME}] Removed ${result.removedCount} console call(s) from ${shortId}`
237
+ );
238
+ }
239
+ return {
240
+ code: result.code,
241
+ // map: null tells Vite to skip source map merging for this transform
242
+ map: null
243
+ };
244
+ }
245
+ };
246
+ }
247
+ var vitePlugin_default = consoleSniper;
248
+ export {
249
+ consoleSniper,
250
+ vitePlugin_default as default
251
+ };
252
+ //# sourceMappingURL=vitePlugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/stripConsole.ts","../../src/core/constants.ts","../../src/core/utils.ts","../../src/vite/vitePlugin.ts"],"sourcesContent":["/**\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 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 vitePlugin.ts\r\n * @description Vite plugin that strips console.* statements during builds.\r\n *\r\n * ─── How Vite plugins work ────────────────────────────────────────────────────\r\n * Vite's plugin system is based on Rollup's. Each plugin can hook into\r\n * different stages of the build lifecycle. We use the `transform` hook,\r\n * which is called once per module with its source code and file ID.\r\n *\r\n * Our plugin:\r\n * 1. Checks if the file should be processed (by extension + include/exclude)\r\n * 2. Calls the core `stripConsoleFromCode` engine\r\n * 3. Returns the transformed code (or null to skip the file)\r\n *\r\n * ─── Why Vite is in a separate file ─────────────────────────────────────────\r\n * The core engine has ZERO Vite dependency. This means:\r\n * - Users who only use the CLI don't pull in Vite types\r\n * - The same engine can be used in Rollup, Webpack, or esbuild plugins\r\n * - Unit tests can test the core without mocking Vite\r\n * ─────────────────────────────────────────────────────────────────────────────\r\n */\r\n\r\nimport type { Plugin } from \"vite\";\r\nimport { stripConsoleFromCode } from \"../core/stripConsole.js\";\r\nimport { shouldProcessFile, isSupportedFile } from \"../core/utils.js\";\r\nimport { PACKAGE_NAME } from \"../core/constants.js\";\r\nimport type { VitePluginOptions } from \"../core/types.js\";\r\nimport type { StripConsoleOptions } from \"../core/types.js\";\r\n\r\n\r\n// Plugin Factory\r\n\r\n\r\n/**\r\n * A Vite plugin that removes `console.*` statements from source files\r\n * during the build process using AST-based transformation.\r\n *\r\n * @param options - Plugin configuration options\r\n * @returns - A Vite plugin object\r\n *\r\n * @example\r\n * ```ts\r\n * // vite.config.ts\r\n * import { defineConfig } from \"vite\";\r\n * import consoleSniper from \"console-sniper/vite\";\r\n *\r\n * export default defineConfig({\r\n * plugins: [\r\n * consoleSniper({\r\n * methods: [\"log\", \"warn\"],\r\n * productionOnly: true,\r\n * }),\r\n * ],\r\n * });\r\n * ```\r\n */\r\nexport function consoleSniper(options: VitePluginOptions = {}): Plugin {\r\n const {\r\n productionOnly = true, // Only strip in production by default\r\n methods,\r\n removeComments,\r\n include = [],\r\n exclude = [],\r\n silent = false,\r\n } = options;\r\n\r\n // This flag is set when the plugin initializes — we need the Vite mode\r\n // to decide whether to strip console calls.\r\n let isProduction = false;\r\n\r\n return {\r\n // ── Plugin metadata ─────────────────────────────────────\r\n name: PACKAGE_NAME,\r\n\r\n // `enforce: \"pre\"` makes our transform run before other plugins.\r\n // This is important because we want to strip consoles from the\r\n // original source, not from already-transformed code.\r\n enforce: \"pre\",\r\n\r\n // ── configResolved hook ─────────────────────────────────\r\n // Called once after Vite has merged all config. This is where we\r\n // learn whether we're in production or development mode.\r\n configResolved(config) {\r\n isProduction = config.command === \"build\" && config.mode === \"production\";\r\n\r\n // Also treat \"build\" with mode not set as production\r\n if (config.command === \"build\" && !config.mode) {\r\n isProduction = true;\r\n }\r\n\r\n if (!silent && !productionOnly) {\r\n // Warn the user if they're stripping in dev mode\r\n console.warn(\r\n `[${PACKAGE_NAME}] productionOnly is false — console statements will be removed in development mode too.`\r\n );\r\n }\r\n },\r\n\r\n // ── transform hook ─────────────────────────────────────\r\n // Called for every module that Vite processes.\r\n // Return `null` to pass the file through unchanged.\r\n // Return `{ code, map }` to provide a transformation.\r\n transform(code, id) {\r\n // Skip if productionOnly and we're not in production\r\n if (productionOnly && !isProduction) return null;\r\n\r\n // Skip files with unsupported extensions\r\n if (!isSupportedFile(id)) return null;\r\n\r\n // Skip virtual modules (Vite internal modules start with \\0)\r\n if (id.startsWith(\"\\0\")) return null;\r\n\r\n // Apply include/exclude patterns\r\n if (!shouldProcessFile(id, include, exclude)) return null;\r\n\r\n // ── Run the core AST engine ─────────────────────────\r\n const stripOptions: StripConsoleOptions = { silent: true };\r\n if (methods) stripOptions.methods = methods;\r\n if (removeComments !== undefined) stripOptions.removeComments = removeComments;\r\n const result = stripConsoleFromCode(code, stripOptions);\r\n\r\n // If nothing changed, return null (Vite will use the original)\r\n if (!result.changed) return null;\r\n\r\n // Log what we removed (only if not silent)\r\n if (!silent && result.removedCount > 0) {\r\n const shortId = id.split(\"/\").slice(-2).join(\"/\");\r\n console.log(\r\n `[${PACKAGE_NAME}] Removed ${result.removedCount} console call(s) from ${shortId}`\r\n );\r\n }\r\n\r\n // Return transformed code.\r\n // Note: We don't return a source map here because @babel/generator\r\n // can generate one — this is left as a future enhancement.\r\n return {\r\n code: result.code,\r\n // map: null tells Vite to skip source map merging for this transform\r\n map: null,\r\n };\r\n },\r\n };\r\n}\r\n\r\n\r\n// Default Export\r\n\r\n\r\n// Allow both named and default imports:\r\n// import consoleSniper from \"console-sniper/vite\"\r\n// import { consoleSniper } from \"console-sniper/vite\"\r\nexport default consoleSniper;\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "console-sniper",
3
+ "version": "1.0.0",
4
+ "description": "Remove console.* statements from JS/TS source code using AST parsing. Works as a Vite plugin and standalone CLI.",
5
+ "keywords": [
6
+ "console",
7
+ "remove",
8
+ "strip",
9
+ "ast",
10
+ "babel",
11
+ "vite",
12
+ "plugin",
13
+ "cli",
14
+ "build",
15
+ "typescript"
16
+ ],
17
+ "homepage": "https://github.com/souhailbakioui/console-sniper#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/souhailbakioui/console-sniper/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/souhailbakioui/console-sniper.git"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Bakioui Souhail <bakiouisohail@gmail.com>",
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.cjs"
33
+ },
34
+ "./vite": {
35
+ "types": "./dist/vite/vitePlugin.d.ts",
36
+ "import": "./dist/vite/vitePlugin.js",
37
+ "require": "./dist/vite/vitePlugin.cjs"
38
+ }
39
+ },
40
+ "main": "./dist/index.cjs",
41
+ "module": "./dist/index.js",
42
+ "types": "./dist/index.d.ts",
43
+ "bin": {
44
+ "console-sniper": "./dist/cli/cli.js"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "test:coverage": "vitest run --coverage",
57
+ "lint": "tsc --noEmit",
58
+ "clean": "rimraf dist",
59
+ "prepublishOnly": "npm run clean && npm run build && npm run test"
60
+ },
61
+ "dependencies": {
62
+ "@babel/generator": "^7.23.0",
63
+ "@babel/parser": "^7.23.0",
64
+ "@babel/traverse": "^7.23.0",
65
+ "@babel/types": "^7.23.0",
66
+ "commander": "^11.1.0",
67
+ "fast-glob": "^3.3.2",
68
+ "picocolors": "^1.0.0"
69
+ },
70
+ "devDependencies": {
71
+ "@types/babel__generator": "^7.6.8",
72
+ "@types/babel__traverse": "^7.20.4",
73
+ "@types/node": "^20.10.0",
74
+ "rimraf": "^5.0.5",
75
+ "tsup": "^8.0.1",
76
+ "typescript": "^5.3.2",
77
+ "vitest": "^1.0.4"
78
+ },
79
+ "peerDependencies": {
80
+ "vite": "^4.0.0 || ^5.0.0"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "vite": {
84
+ "optional": true
85
+ }
86
+ },
87
+ "engines": {
88
+ "node": ">=18.0.0"
89
+ }
90
+ }