Files
cloudflare__vinext/tests/remove-console.test.ts
T
Divanshu Chauhan (divkix) aae4b50fe8 fix(build): implement compiler.removeConsole option (#1611)
* fix(build): implement compiler.removeConsole option

Implements Next.js parity for compiler.removeConsole, which strips
console.* calls from the client bundle during production builds.

Behavior:
- compiler.removeConsole: true removes all console.<method>() calls
- compiler.removeConsole: { exclude: ['error'] } preserves excluded
  methods (case-insensitive)
- Shadowed console bindings (local vars, params, destructured) are
  preserved per Next.js SWC behavior
- Computed access console[prop]() is preserved
- Removed calls in expression statements become ; for clean output
- Removed calls in other contexts (JSX, arguments, returns) become void 0

Files:
- packages/vinext/src/plugins/remove-console.ts (new transform)
- packages/vinext/src/config/next-config.ts (types + resolution)
- packages/vinext/src/index.ts (Vite plugin registration)
- tests/remove-console.test.ts (24 unit tests)
- tests/next-config.test.ts (update default resolved config)

Fixes #1497

* fix(knip): un-export RemoveConsoleConfig type

Remove the declare -x ABBR_TIPS_PROMPT="\\n💡 \\e[1m{{ .abbr }}\\e[0m => {{ .cmd }}"
declare -x ABBR_TIPS_REGEXES="(^(\\w+\\s+)+(-{1,2})\\w+)(\\s\\S+) (^(\\s?(\\w-?)+){3}).* (^(\\s?(\\w-?)+){2}).* (^(\\s?(\\w-?)+){1}).*"
declare -x BUN_PTY_LIB="/\$bunfs/root/librust_pty_arm64-b7hv765h.dylib"
declare -x COLORTERM="truecolor"
declare -x COMMAND_MODE="unix2003"
declare -x CPPFLAGS="-I/opt/homebrew/opt/openssl@3/include"
declare -x DEBIAN_FRONTEND="noninteractive"
declare -x DIVMEMORY_API_KEY="3b3f7180985460433de23604da244b8e4c1917f81f992199ebeb0ebc75e7a51e"
declare -x DIVMEMORY_WORKER_URL="https://divmemory.divkix.workers.dev"
declare -x DOTNET_CLI_TELEMETRY_OPTOUT="= 1"
declare -x DO_NOT_TRACK="= 1"
declare -x EDITOR="nano"
declare -x FORCE_COLOR="0"
declare -x FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT="= 1"
declare -x GATSBY_TELEMETRY_DISABLED="= 1"
declare -x GCM_INTERACTIVE="never"
declare -x GIT_ASKPASS=""
declare -x GIT_EDITOR="true"
declare -x GIT_SEQUENCE_EDITOR="true"
declare -x GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
declare -x GIT_TERMINAL_PROMPT="0"
declare -x GPG_TTY="/dev/ttys005"
declare -x HOME="/Users/divkix"
declare -x HOMEBREW_CELLAR="/opt/homebrew/Cellar"
declare -x HOMEBREW_NO_ANALYTICS="= 1"
declare -x HOMEBREW_PREFIX="/opt/homebrew"
declare -x HOMEBREW_REPOSITORY="/opt/homebrew"
declare -x INFOPATH="/opt/homebrew/share/info"
declare -x LANG="en_US.UTF-8"
declare -x LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib"
declare -x LOGNAME="divkix"
declare -x LaunchInstanceID="EB1B14BF-88FC-4DF6-A808-CFA3E9E33401"
declare -x MORPH_API_KEY="sk-sKIOR_4BfuniAInrzxgMoovjMKqlBxa2Ae6EC2oVOe44hOl-"
declare -x NEXT_TELEMETRY_DISABLED="= 1"
declare -x NODE_EXTRA_CA_CERTS="/Users/divkix/.factory/cache/certs/factory-cli-certs.pem"
declare -x OLDPWD="/Users/divkix/GitHub/vinext.fix-issue-1497"
declare -x OPENCODE_ENABLE_EXA="true"
declare -x OSLogRateLimit="64"
declare -x PATH="/Users/divkix/.factory/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/Users/divkix/.antigravity-ide/antigravity-ide/bin:/Users/divkix/.antigravity/antigravity/bin:/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/imagemagick-full/bin:/opt/homebrew/opt/ffmpeg-full/bin:/opt/homebrew/opt/ruby/bin:/Users/divkix/.bun/bin:/bin:/Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home/bin:/opt/homebrew/opt/python/libexec/bin:/opt/homebrew/opt/python/bin:/opt/homebrew/opt/curl/bin:/opt/homebrew/opt/libpq/bin:/Users/divkix/go/bin:/Users/divkix/.local/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/MacGPG2/bin:/Users/divkix/.lmstudio/bin:/Users/divkix/.orbstack/bin:/Applications/Warp.app/Contents/Resources/bin"
declare -x PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@3/lib/pkgconfig"
declare -x POWERSHELL_TELEMETRY_OPTOUT="= 1"
declare -x PWD="/Users/divkix/GitHub/vinext.fix-issue-1497"
declare -x PYTHONUNBUFFERED="1"
declare -x SECURITYSESSIONID="186b0"
declare -x SHELL="/opt/homebrew/bin/fish"
declare -x SHLVL="2"
declare -x SSH_ASKPASS=""
declare -x SSH_AUTH_SOCK="/var/run/com.apple.launchd.VER64w7Euk/Listeners"
declare -x SSH_SOCKET_DIR="~/.ssh"
declare -x STARSHIP_CONFIG="/Users/divkix/.config/starship.toml"
declare -x STARSHIP_SESSION_KEY="5594177922001814"
declare -x STARSHIP_SHELL="fish"
declare -x SUDO_ASKPASS=""
declare -x TERM="dumb"
declare -x TERM_PROGRAM="WarpTerminal"
declare -x TERM_PROGRAM_VERSION="v0.2026.05.20.09.21.stable_03"
declare -x TMPDIR="/var/folders/l6/72dpyfhj03x4z9lvndxkdbxr0000gn/T/"
declare -x USER="divkix"
declare -x VSCODE_TELEMETRY_LEVEL="= off"
declare -x WARP_CLIENT_VERSION="v0.2026.05.20.09.21.stable_03"
declare -x WARP_CLI_AGENT_PROTOCOL_VERSION="1"
declare -x WARP_HONOR_PS1="1"
declare -x WARP_IS_LOCAL_SHELL_SESSION="1"
declare -x WARP_USE_SSH_WRAPPER="1"
declare -x XPC_FLAGS="0x0"
declare -x XPC_SERVICE_NAME="0"
declare -x __ABBR_TIPS_KEYS="l la ll lla g gcl gph gphf gpl gplr gst gcm gcam gco gca gcan gitweb gwp gwl reload c df less www untar bu oc fastdl a__cat a__ls a__rmd"
declare -x __ABBR_TIPS_VALUES="ls ls -a ls -l ls -la git git clone git push git push -f git pull git pull --rebase git status git commit -m git commit -am git commit git commit -a --amend git commit --amend -a --no-edit open (git remote get-url origin) git worktree prune -v git worktree list exec fish \"clear && printf '\\\\e[3J'\" df -h more python3 -m http.server 8000 tar -zxvf fisher update && brew update && brew upgrade opencode aria2c -x16 -s16 -k1M bat lsd --icon-theme=unicode trash"
declare -x __CFBundleIdentifier="dev.warp.Warp-Stable"
declare -x __CF_USER_TEXT_ENCODING="0x1F5:0:0" keyword from  — it is only
used internally within  and knip flagged it as an
unused export.

* ci: trigger CI re-run

Previous E2E failures (font-google, error-interactive, hooks) appear
to be flaky/unrelated to the removeConsole changes.

* fix(remove-console): address bonk review feedback

- Document client-only scoping as a known parity gap from Next.js
- Correct the node_modules skip comment (Next.js does apply to deps)
- Hoist SCOPE_ENTERING_TYPES to a module-level Set for O(1) lookup
- Push a scope for CatchClause params so `catch (console)` shadows
- Document the var-hoisting limitation in BlockStatement scope handling
- Add resolution tests covering all removeConsole config shapes

* fix(remove-console): gate to build + cover remaining shadow patterns

- Set `apply: "build"` on the plugin so console stripping no longer fires
  in `next dev`. Matches Next.js, which documents removeConsole as a
  production-only feature.
- Extend `checkBinding` to recognize AssignmentPattern, ArrayPattern, and
  RestElement so destructured defaults, array destructuring, and rest
  elements correctly shadow `console`.
- Mark FunctionDeclaration / ClassDeclaration `id` in the enclosing scope
  before pushing the function's own scope (function and class declarations
  hoist their name into the surrounding scope).
- Mark named FunctionExpression `id` in the function's own scope (named
  function expressions bind their name only inside their body).
- Add 10 tests covering all newly-supported shadow patterns plus a
  CatchClause regression test.

---------

Co-authored-by: James <james@eli.cx>
2026-05-27 19:16:34 +01:00

360 lines
10 KiB
TypeScript

/**
* Remove console tests — verifies `compiler.removeConsole` behavior.
*
* Mirrors Next.js SWC remove_console transform:
* - `removeConsole: true` strips all `console.*` calls
* - `removeConsole: { exclude: ["error"] }` strips all except excluded methods
* - Preserves calls when `console` is shadowed by a local variable/param
* - Preserves computed property access `console[prop]()`
* - Only strips the global `console` object
*/
import { describe, it, expect } from "vite-plus/test";
import { removeConsoleCalls } from "../packages/vinext/src/plugins/remove-console.js";
describe("removeConsoleCalls", () => {
it("returns null when code has no console calls", () => {
const code = `
export default function Page() {
return <div>Hello</div>;
}
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("returns null when removeConsole is disabled", () => {
const code = `
console.log("hello");
export default function Page() { return null; }
`;
expect(removeConsoleCalls(code, false)).toBeNull();
});
it("strips console.log from top level", () => {
const code = `
console.log("hello world");
export default function Page() { return null; }
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.log");
expect(result).toContain("export default function Page");
});
it("strips console.warn, console.info, console.debug", () => {
const code = `
console.warn("warn");
console.info("info");
console.debug("debug");
export default function Page() { return null; }
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.warn");
expect(result).not.toContain("console.info");
expect(result).not.toContain("console.debug");
expect(result).toContain("export default");
});
it("strips console.error when not excluded", () => {
const code = `
console.error("error");
export default function Page() { return null; }
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.error");
});
it("preserves console.error when excluded", () => {
const code = `
console.error("keep me");
console.log("remove me");
export default function Page() { return null; }
`;
const result = removeConsoleCalls(code, { exclude: ["error"] });
expect(result).not.toBeNull();
expect(result).toContain('console.error("keep me")');
expect(result).not.toContain("console.log");
});
it("preserves console.table and console.assert when excluded (case-insensitive)", () => {
const code = `
console.table([1, 2]);
console.assert(false, "msg");
console.log("remove me");
`;
const result = removeConsoleCalls(code, { exclude: ["TABLE", "assert"] });
expect(result).not.toBeNull();
expect(result).toContain("console.table");
expect(result).toContain("console.assert");
expect(result).not.toContain("console.log");
});
it("strips console calls inside function bodies", () => {
const code = `
function greet(name) {
console.log("Hello, " + name);
return "Hello, " + name;
}
export default function Page() { return greet("world"); }
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.log");
expect(result).toContain("function greet");
expect(result).toContain('return "Hello, " + name');
});
it("strips console calls in if/else blocks", () => {
const code = `
if (process.env.NODE_ENV === "development") {
console.log("dev mode");
} else {
console.warn("prod mode");
}
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.log");
expect(result).not.toContain("console.warn");
});
it("strips chained console calls on same line", () => {
const code = `
console.log("a"); console.log("b");
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.log");
});
it("preserves console calls when console is shadowed (local variable)", () => {
const code = `
const console = { log: () => {} };
console.log("custom");
`;
const result = removeConsoleCalls(code, true);
expect(result).toBeNull();
});
it("preserves console calls when console is a function parameter", () => {
const code = `
function foo(console) {
console.log("param");
}
`;
const result = removeConsoleCalls(code, true);
expect(result).toBeNull();
});
it("preserves console calls when console is destructured", () => {
const code = `
function foo({ console }) {
console.log("destructured");
}
`;
const result = removeConsoleCalls(code, true);
expect(result).toBeNull();
});
it("preserves console with default in destructured param: ({ console = {} })", () => {
const code = `
function foo({ console = {} }) {
console.log("destructured-default");
}
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console as default-valued positional param: (console = {})", () => {
const code = `
function foo(console = { log: () => {} }) {
console.log("default-param");
}
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console bound as a rest param: (...console)", () => {
const code = `
function foo(...console) {
console.log("rest");
}
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console bound via array destructuring", () => {
const code = `
const [console] = [{ log: () => {} }];
console.log("array-destructured");
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console bound via nested array destructuring", () => {
const code = `
const [[console]] = [[{ log: () => {} }]];
console.log("nested-array-destructured");
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console bound via rest in array destructuring", () => {
const code = `
const [, ...console] = [1, 2, 3];
console.log("rest-destructured");
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console when shadowed by a function declaration name", () => {
const code = `
function console(...args) {}
console.log("function-decl-name");
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console when shadowed by a class declaration name", () => {
const code = `
class console {}
console.log("class-decl-name");
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console inside a named FunctionExpression named console", () => {
const code = `
const x = function console() {
console.log("named-fn-expr");
};
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves console when shadowed by a CatchClause param", () => {
const code = `
try { throw 0; } catch (console) {
console.log("catch-param");
}
`;
expect(removeConsoleCalls(code, true)).toBeNull();
});
it("preserves computed property access console[prop]()", () => {
const code = `
const method = "log";
console[method]("computed");
`;
const result = removeConsoleCalls(code, true);
expect(result).toBeNull();
});
it("preserves calls on non-console objects", () => {
const code = `
const logger = { log: (x) => x };
logger.log("not console");
`;
const result = removeConsoleCalls(code, true);
expect(result).toBeNull();
});
it("preserves console as an expression value (not a call)", () => {
const code = `
const c = console;
`;
const result = removeConsoleCalls(code, true);
expect(result).toBeNull();
});
it("strips console.group and console.groupEnd", () => {
const code = `
console.group("section");
console.log("inside");
console.groupEnd();
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.group");
expect(result).not.toContain("console.groupEnd");
expect(result).not.toContain("console.log");
});
it("handles console calls without arguments", () => {
const code = `
console.log();
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.log");
});
it("handles console calls with complex arguments", () => {
const code = `
console.log({ a: 1 }, [2, 3], () => 4);
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.log");
});
it("strips console inside nested callbacks", () => {
const code = `
setTimeout(() => {
console.log("delayed");
}, 100);
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).not.toContain("console.log");
});
it("preserves console.time/Console.timeEnd when excluded", () => {
const code = `
console.time("timer");
doWork();
console.timeEnd("timer");
console.log("done");
`;
const result = removeConsoleCalls(code, { exclude: ["time", "timeEnd"] });
expect(result).not.toBeNull();
expect(result).toContain('console.time("timer")');
expect(result).toContain('console.timeEnd("timer")');
expect(result).not.toContain("console.log");
});
it("strips console as function argument (replaces with void 0)", () => {
const code = `
foo(console.warn("arg"));
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).toContain("foo(void 0)");
expect(result).not.toContain("console.warn");
});
it("strips console in return statement (replaces with void 0)", () => {
const code = `
function run() {
return console.info("result");
}
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).toContain("return void 0");
expect(result).not.toContain("console.info");
});
it("strips console in ternary branches (replaces with void 0)", () => {
const code = `
const action = isDev ? console.log("dev") : console.warn("prod");
`;
const result = removeConsoleCalls(code, true);
expect(result).not.toBeNull();
expect(result).toContain("isDev ? void 0 : void 0");
expect(result).not.toContain("console.log");
expect(result).not.toContain("console.warn");
});
});