@castlenine/vite-plugin-console-stripper
| Install | |
|---|---|
npm install @castlenine/vite-plugin-console-stripper |
|
| Latest Version: | 0.1.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Sep 27, 2026 |
| Links: | GitHub · npm |
@castlenine/vite-plugin-console-stripper
A Vite plugin that removes console calls from your production build and keeps the ones you still need (by default console.error, console.warn, console.info and console.clear). It works on your own code and on dependencies, and supports ignore comments, path ignore rules, composable extension presets and sourcemaps.
Table of Contents
- Disclaimer
- Features
- Requirements
- Installation
- Usage
- Options
- Ignore comments
- What gets removed
- Framework notes
- Limitations
- Examples
- How it works
- Changelog
- License
Disclaimer
Tested against real vite build runs with Svelte (with and without vitePreprocess()), Vue.js, React, Preact, Solid, Astro, Angular through Analog (AOT, JIT and fastCompile), .svx pages and entry HTML, including multi-environment and concurrent builds. Qwik is verified on the transform only. Please open an issue if you run into problems with these or other frameworks.
Features
- Removes a standalone console statement however it is written: multiline, with nested parentheses or string and template-literal arguments, with or without a semicolon. No blank line is left behind.
- Matches qualified and less common call forms:
window.console.log(),console?.log(),console['log'](),console.log<T>()and tagged templates. - Turns a call used inside a larger expression into
void 0, so the code still parses. - Keeps
console.error,console.warn,console.infoandconsole.clearby default;methodssets the list. - Processes script, markup and HTML files out of the box (
DEFAULT_EXTENSIONS), and ships frozen extension presets (SCRIPT_EXTENSIONS,SVELTE_EXTENSIONS,VUE_EXTENSIONS, …) that you compose instead of listing extensions by hand. - Never edits text: markup text, plain quoted attributes,
<style>blocks and JSX text stay byte-identical. - Strips dependencies under
node_modulestoo, with per-package and global opt-outs. - Keeps specific calls through ignore comments, and skips the folders and files you configure on top of a built-in ignore list.
- Leaves a call it cannot delimit in place and emits a build warning.
- Emits a sourcemap for each changed file when sourcemaps are enabled. No runtime dependencies.
Requirements
| Requirement | Version |
|---|---|
| Node.js | >=18.0.0 |
| Vite | >=2.0.0 (peer dependency) |
Installation
Use your package manager to install it as a development dependency:
pnpm add -D @castlenine/vite-plugin-console-stripper
# or
npm i -D @castlenine/vite-plugin-console-stripper
# or
yarn add -D @castlenine/vite-plugin-console-stripper
Usage
Quick start
List consoleStripper() first in plugins:
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import consoleStripper from '@castlenine/vite-plugin-console-stripper';
export default defineConfig({
plugins: [consoleStripper(), sveltekit()],
});
Run vite build. console.log, console.debug and the other default methods disappear from your code and your dependencies, while console.error, console.warn, console.info and console.clear stay.
The plugin only runs during vite build (apply: 'build'), so the dev server and Vitest keep every console call without a mode check. To keep them in one build mode as well, a staging build for example, register the plugin conditionally:
export default defineConfig(({ mode }) => ({
plugins: [mode === 'staging' ? null : consoleStripper(), sveltekit()],
}));
Vite ignores falsy entries in plugins, so mode !== 'staging' && consoleStripper() is equivalent to the ternary.
Plugin order
consoleStripper() returns two plugins, and Vite flattens the array, so plugins: [consoleStripper()] is all you write:
console-stripperruns withenforce: 'pre'so it sees raw source, before a later transform such as esbuild drops the comments that carry ignore directives.console-stripper:angularruns withenforce: 'post'and only does something when Analog'sangular()plugin is registered (see Angular through Analog).
pre plugins run in the order they appear in plugins, so order can matter when a framework plugin has its own pre pass.
| Framework | Where to list consoleStripper() |
Why |
|---|---|---|
| Svelte / SvelteKit | Before svelte() / sveltekit() |
@sveltejs/vite-plugin-svelte runs your preprocessors in a pre pass. With vitePreprocess({ script: true }), TypeScript is transpiled there, and a file-level console-stripper-ignore written above an import type is erased with it. Next-line and start/end directives survive in either order. |
| Markdown via mdsvex | Before svelte() / sveltekit() |
mdsvex is a Svelte preprocessor, so the stripper should read the source before it does. |
| Vue | Anywhere | @vitejs/plugin-vue is not pre-enforced. |
| React, Preact, Solid | Anywhere | Their plugins have pre passes, but builds in both orders keep ignore directives working. |
| Astro | In astro.config.* under vite.plugins |
Astro's own plugins run first, so .astro modules arrive compiled (see Astro). |
| Angular (Analog) | Anywhere for AOT and jit: true; before angular() with fastCompile: true |
See Angular through Analog. |
| HTML entries | Anywhere | There is no framework plugin to order against. |
When in doubt, list consoleStripper() first.
CommonJS
The package also ships a CommonJS entry. require() returns the plugin function directly (.default works too), and DEFAULT_METHODS and every extension preset are properties of the same module:
// vite.config.cjs
const { defineConfig } = require('vite');
const consoleStripper = require('@castlenine/vite-plugin-console-stripper');
module.exports = defineConfig({
plugins: [
consoleStripper({
methods: [...consoleStripper.DEFAULT_METHODS, 'info'],
extensions: [...consoleStripper.SCRIPT_EXTENSIONS, ...consoleStripper.SVELTE_EXTENSIONS],
}),
],
});
Options
Everything is configured through the options object; the plugin reads no environment variables. Array options are typed readonly, so you can pass the frozen presets, DEFAULT_METHODS and as const lists as they are.
| Option | Type | Default | Purpose |
|---|---|---|---|
methods |
readonly ConsoleMethod[] |
DEFAULT_METHODS |
Console methods to strip |
extensions |
readonly string[] |
DEFAULT_EXTENSIONS |
File extensions to process (leading dot optional, case-insensitive) |
ignoreComments |
boolean |
true |
Honor the console-stripper-ignore* comments |
ignoreFolders |
readonly string[] |
[] |
Folders to skip, relative to the Vite root |
ignoreFiles |
readonly string[] |
[] |
Files to skip, relative to the Vite root |
ignoreDefaults |
boolean |
true |
Also apply the built-in ignore list to project files |
stripDependencies |
boolean |
true |
Strip files under node_modules too |
ignoreDependencies |
readonly string[] |
[] |
Packages to leave untouched, by exact name |
verbose |
boolean |
false |
Print a summary line when the build ends |
methods
The console methods to strip. Default: DEFAULT_METHODS:
['assert', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'group',
'groupCollapsed', 'groupEnd', 'log', 'profile', 'profileEnd', 'table',
'time', 'timeEnd', 'timeLog', 'timeStamp', 'trace']
error, warn, info, clear (and the non-standard exception) are kept by default because production code usually still needs them.
methodsreplaces the default list. SpreadDEFAULT_METHODSto extend it.- Names that are not
consolemembers are ignored. methods: []turns stripping off. Whenevermethodsorextensionsresolves to an empty list, the plugin logs one[console-stripper]warning naming the rejected entries. It never fails the build.
import consoleStripper, { DEFAULT_METHODS } from '@castlenine/vite-plugin-console-stripper';
// Strip the default list plus console.info
consoleStripper({ methods: [...DEFAULT_METHODS, 'info'] });
extensions
The file extensions to process. Default: DEFAULT_EXTENSIONS. Entries are trimmed.
The package exports nine frozen presets. Pass one directly (extensions: SCRIPT_EXTENSIONS) or spread several into a new array.
| Constant | Extensions | Typical use |
|---|---|---|
JAVASCRIPT_EXTENSIONS |
js, mjs, cjs |
Plain JavaScript; also Lit and Angular |
TYPESCRIPT_EXTENSIONS |
ts, mts, cts |
Plain TypeScript; also Lit and Angular |
JSX_EXTENSIONS |
jsx, tsx |
React, Preact, Solid, Qwik, Vue JSX |
SCRIPT_EXTENSIONS |
The three above | Any plain script file |
SVELTE_EXTENSIONS |
svelte |
Svelte components (.svelte.js / .svelte.ts rune modules are covered by the script presets) |
VUE_EXTENSIONS |
vue |
Vue single-file components |
ASTRO_EXTENSIONS |
astro |
Astro components |
HTML_EXTENSIONS |
html, htm |
Inline <script> blocks in entry HTML |
DEFAULT_EXTENSIONS |
Every preset above | The default |
A preset decides which files get scanned, not how they are scanned (see File kinds). Only add extensions whose files are JavaScript or contain JavaScript <script> blocks (see Limitations).
import consoleStripper, { DEFAULT_EXTENSIONS, SCRIPT_EXTENSIONS, SVELTE_EXTENSIONS } from '@castlenine/vite-plugin-console-stripper';
// Only the file kinds this project uses
consoleStripper({ extensions: [...SCRIPT_EXTENSIONS, ...SVELTE_EXTENSIONS] });
// The defaults plus a custom extension
consoleStripper({ extensions: [...DEFAULT_EXTENSIONS, 'svx'] });
ignoreComments
Whether ignore comments are honored. Default: true. Set it to false to strip every matching call, marked or not.
consoleStripper({ ignoreComments: false });
ignoreFolders and ignoreFiles
Folders and files to skip, relative to the Vite root. Default: [] for both. They apply to dependency files too.
- Tokens match on whole path segments, at any depth:
buildmatchesbuild/app.jsbut notbuildhome/app.js, andHeader.sveltematchessrc/components/Header.svelte. *matches within one segment:*.storiesmatches aButton.stories/folder; use*.stories.*forButton.stories.ts.- Entries are trimmed,
\becomes/, and a leading/,./or../and a trailing/are dropped. - A module id's
?queryand#hashare ignored, but a#followed by/is part of a folder name.
consoleStripper({
ignoreFolders: ['src/tests', 'fixtures'],
ignoreFiles: ['Header.svelte', 'src/components/Modal.svelte'],
});
ignoreDefaults
Whether the built-in ignore list applies on top of ignoreFolders / ignoreFiles. Default: true. The list:
node_modules, .git, .idea, .vscode, .DS_Store, Thumbs.db, .env, .env.*, logs, *.log, public, build,
.svelte-kit, dist, .nuxt, .output, .nitro, .data, .next, out, .react-router, .astro, .solid, .vinxi,
.tanstack, .angular, e2e, angular.json, browserslist, .vercel, .netlify, .wrangler, .cache
It only applies to your project's files, never to dependencies. Apart from node_modules and .git, each token matches only as the first segment under the Vite root: public/ is skipped, src/routes/public/ is not. Add a nested folder to ignoreFolders if you want it skipped.
// Strip everything in the project, build and public folders included
consoleStripper({ ignoreDefaults: false });
stripDependencies
Whether files under node_modules are stripped. Default: true. Dependencies get the same methods, extensions and ignore-comment handling as your files. Turn it off to keep a library's own diagnostics, or to check whether an issue comes from a dependency. SSR builds only reach bundled dependencies (see Limitations).
consoleStripper({ stripDependencies: false });
ignoreDependencies
Packages whose files are left alone while other dependencies are still stripped. Default: [].
A name matches the package that owns the file, by exact name including its scope (@scope/pkg). There are no wildcards or version ranges. A nested install belongs to its innermost package, so node_modules/a/node_modules/b belongs to b, and listing a does not cover it. pnpm-store layouts and dependencies hoisted above the Vite root resolve the same way. A path token works too: ignoreFolders: ['node_modules/some-logger'].
consoleStripper({ ignoreDependencies: ['some-logger', '@scope/pkg'] });
verbose
Prints one summary line through Vite's logger after a successful build, with project and dependency counts. A failed build prints nothing. Default: false.
consoleStripper({ verbose: true });
[console-stripper] [client] stripped 12 console calls in 4 project files and 3 console calls in 2 dependency files
On Vite 6+, the line is prefixed with the environment name, and a multi-environment build prints one line per environment. Before Vite 6 there is no prefix. When some calls had to be left in place (see Build warnings), the line ends with ; left N undelimitable call(s) in place (X project, Y dependency).
Types
Options and ConsoleMethod are exported for typing a shared options object:
import type { ConsoleMethod, Options } from '@castlenine/vite-plugin-console-stripper';
const sharedOptions: Options = { methods: ['error', 'warn'] as ConsoleMethod[] };
Ignore comments
Directives work in any comment: //, /* */, and <!-- --> in markup files. Each takes an optional : reason. ignoreComments: false disables them all.
console-stripper-ignore-next-line keeps every console call on the next line, even one written across several lines:
// console-stripper-ignore-next-line: needed to trace this in production
console.log('trace', payload);
console-stripper-ignore-start / console-stripper-ignore-end keep every call between the two markers. A start with no matching end protects the rest of the file:
// console-stripper-ignore-start: temporary diagnostics
console.log('a');
console.log('b');
// console-stripper-ignore-end
console-stripper-ignore within the first 10 lines of a file leaves the whole file untouched:
// console-stripper-ignore: this file is excluded on purpose
console.log('kept');
What gets removed
A console call that stands alone as a statement is removed with its trailing semicolon, if any. If it had the line to itself, the whole line goes, so no blank line is left. When removal would glue two lines into one expression, the plugin inserts a ; guard.
// Before
before();
console.log('hello');
after();
// After
before();
after();
A call used inside a larger expression becomes void 0 (parenthesized when something follows it, as in console.log(1).x → (void 0).x). Minifiers drop it.
// Before
if (debug) console.log(x);
ready && console.log('up');
const log = () => console.log(x);
// After
if (debug) void 0;
ready && void 0;
const log = () => void 0;
File kinds
How much of a file counts as code depends on its extension, not on which preset matched it. Compound names use the last extension, so Counter.svelte.ts is a ts file.
| Kind | Extensions | What is edited |
|---|---|---|
script |
.js, .mjs, .cjs, .jsx, .tsx |
The whole file (JSX text is left alone) |
ts |
.ts, .mts, .cts |
Like script, but JSX is never detected |
html |
.html, .htm |
JavaScript <script> blocks only |
astro |
.astro |
Removal in <script> and the --- frontmatter; void 0 in {…} expressions |
vue |
.vue |
Removal in <script>; void 0 in {{ }} and directive values |
markdown |
.md, .mdx, .svx |
Like markup, but fenced code and front matter are left alone |
markup |
.svelte and any other extension |
Removal in <script>; void 0 in {…} slots |
A Vue or Astro component's extracted <script> sub-request (App.vue?vue&type=script…) is scanned as script.
Matched call forms
The plugin matches console.<method>(, optionally qualified by window, globalThis, self or global, plus:
- optional chaining:
console?.log(),console.log?.() - bracket access:
console['log']() - TypeScript generics and non-null assertions:
console.log<T>(),console.log!() - tagged templates:
console.log`x`
Left alone:
- other qualifiers:
foo.console.log(…),window?.console.log(…) - identifiers that just end in
console:$console.log(…),this.#console.log(…) - a comment between
consoleand the method:console/* c */.log(1) - anything that is not a direct call:
const log = console.log,console.log.bind(…),new console.log(…),console[level](https://github.com/Castlenine/vite-plugin-console-stripper/blob/HEAD/…),.apply,.call
Skipped <script> blocks
A <script> block is never edited, in any file kind, when:
- its
typeis not JavaScript (application/json,importmap,text/template,speculationrules, …). Parameters are ignored, sotext/javascript; charset=utf-8still counts. - the tag has a
srcattribute. - the tag is self-closing (
<script … />). Everything up to the next</script>is skipped too, so write<script src="…"></script>. - the block contains another
<scriptopener, which makes its end impossible to know.
Sourcemaps
A changed file gets a sourcemap only when the resolved build.sourcemap is true, 'inline' or 'hidden'. On Vite 6+, the per-environment setting wins over the top-level one. Maps keep original line and column positions and include sourcesContent.
Build warnings
When the plugin cannot tell where a call ends, it leaves the call untouched rather than risk a wrong edit. For an unclosed parenthesis or template literal, or a call running past </script>, it also raises a warning through Rollup's this.warn, with a code frame, filterable in onwarn:
[plugin console-stripper] …
The Astro and Angular warnings described under Framework notes use the same channel. Warnings only come from project files, never dependencies, and each fires once per module per plugin instance, across client and SSR builds and vite build --watch rebuilds.
Framework notes
For plugin ordering, see Plugin order.
Svelte and SvelteKit
.sveltefiles aremarkup: statements are removed inside<script>, and a call in a{…}template slot becomesvoid 0. Text, plain quoted attributes (eventitle="{console.log()}") and<style>are left alone..svelte.js/.svelte.tsrune modules are plain script files.
Vue
- In
.vuefiles, statements are removed inside<script>. Outside it, only{{ }}interpolations and directive values (@click,:prop,#slot,v-*) are code, and a call there becomesvoid 0. A single{is text.
React, Preact and Solid
.jsx/.tsxfiles arescript: the whole file is code, but JSX text (<p>Use console.log(x) to debug</p>) is left alone. JSX detection is a heuristic, not a full parser..tsfiles never detect JSX, since TypeScript forbids it there.
Astro
- In
.astrosource, statements are removed inside<script>and the---frontmatter, and a call in a{…}expression becomesvoid 0. - In a real Astro build, Astro's plugins run first, so
.astromodules arrive compiled and are scanned asscript. Ignore directives are recovered from Astro's inline sourcemap, andis:inlinescripts are stripped too. define:varsscripts are left untouched. An ignore-start/end range reaches into anis:inlinescript body only when it spans the whole{…}expression containing it.- If Astro's inline sourcemap cannot be read, the plugin warns once for that file (project files only) and ignores its directives.
Angular through Analog
With @analogjs/vite-plugin-angular, the console-stripper:angular pass (enforce: 'post') strips Analog's compiled JavaScript.
- AOT (the default) and
jit: truework in either plugin order. The file-levelconsole-stripper-ignoredirective is read from your authored source. - With
fastCompile: true, listconsoleStripper()beforeangular(). Listed after it, the plugin only sees fastCompile output, which drops a file-level directive written above animport type, and it logs one[console-stripper] @analogjs/vite-plugin-angular-fast-compile runs before console-stripper, …warning. - A directive attached only to type-only code (
import type,interface,type) is erased along with it by TypeScript. - If the Angular compiler drops comments from its output (for example
"removeComments": true), a module whose source had next-line or start/end directives is left unstripped, with one[plugin console-stripper:angular] the Angular compiler removed the ignore comments of <file> …warning per file. The file-level directive still works. - Template text (inline
template:andtemplateUrl.htmlfiles) is never edited. Angular templates cannot callconsoleanyway.
HTML entries
- In
.html/.htmfiles only JavaScript<script>blocks are edited. Page text,<code>samples and inline event handlers stay byte-identical. - HTML-comment ignore directives are honored for inline module scripts. Inline scripts injected by other plugins (
transformIndexHtml) are still stripped, even on a page marked with<!-- console-stripper-ignore -->.
Markdown, MDX and svx
.md,.mdxand.svxhave no preset. Add them toextensionsyourself, for example[...DEFAULT_EXTENSIONS, 'svx'].- They are scanned as
markdown: likemarkup, but fenced code blocks (```or~~~) and a leading---front-matter block stay byte-identical.
Limitations
- Side effects in arguments are dropped.
console.log(index++)loses the increment. Use an ignore comment if the side effect must run. The same goes for third-party code, where you cannot add one; useignoreDependenciesfor packages that rely on it, or whose job is printing throughconsole.log/console.debug. - Some removable calls become
void 0instead. After a label orcaseclause (case 1: void 0;), inside a TypeScriptnamespacebody, and right above a line starting with!=or!==. The output is still valid. - Undelimitable calls stay. See Build warnings.
- It works on text, not a syntax tree. A locally shadowed
consoleis treated like the global one, and only direct calls are matched. Most logger libraries, which alias or wrapconsole, keep working. - SSR builds only reach bundled dependencies. Vite externalizes dependencies in SSR by default, so they never go through
transform. Only the client build and packages inssr.noExternalare stripped. - Only add JavaScript-syntax files to
extensions. A language like Imba, Civet, CoffeeScript, ReScript, Elm or Gleam would be scanned as JavaScript and corrupted. - Marko, Riot and Ripple are unverified. They have no preset; adding their extension uses markup mode at your own risk.
Examples
SvelteKit project
Strip the defaults plus console.info, skip the test folder and keep one package's logging:
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import consoleStripper, { DEFAULT_METHODS, SCRIPT_EXTENSIONS, SVELTE_EXTENSIONS } from '@castlenine/vite-plugin-console-stripper';
export default defineConfig({
plugins: [
// List it before sveltekit() so it reads the source before any preprocessor (see Plugin order)
consoleStripper({
methods: [...DEFAULT_METHODS, 'info'],
extensions: [...SCRIPT_EXTENSIONS, ...SVELTE_EXTENSIONS, 'svx'],
ignoreFolders: ['src/tests'],
ignoreDependencies: ['some-logger'],
}),
sveltekit(),
],
});
Vue project
Only process Vue and script files, and print a summary at the end of the build:
// vite.config.ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import consoleStripper, { SCRIPT_EXTENSIONS, VUE_EXTENSIONS } from '@castlenine/vite-plugin-console-stripper';
export default defineConfig({
plugins: [
vue(), // Order doesn't matter with vue()
consoleStripper({
extensions: [...SCRIPT_EXTENSIONS, ...VUE_EXTENSIONS],
ignoreFiles: ['src/components/DebugPanel.vue'],
verbose: true,
}),
],
});
Astro project
Register it under Astro's vite key:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import consoleStripper from '@castlenine/vite-plugin-console-stripper';
export default defineConfig({
vite: {
plugins: [consoleStripper()],
},
});
Angular project
With Analog, list consoleStripper() first so the order still holds if you turn on fastCompile:
// vite.config.ts
import angular from '@analogjs/vite-plugin-angular';
import { defineConfig } from 'vite';
import consoleStripper from '@castlenine/vite-plugin-console-stripper';
export default defineConfig({
plugins: [consoleStripper(), angular()],
});
How it works
For the mechanics behind file kinds, void 0 replacement, compiled Astro and Angular modules, dependency ownership, sourcemaps and concurrent builds, see documentation/how-it-works.md.
Changelog
For more information, refer to the CHANGELOG.md.
License
Related Packages
Vite plugin that removes specified attributes, such as 'data-testid' used in tes...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.0 | ^3.5.43 | MIT |