Skip to content

Add no-top-level-side-effects rule#2912

Open
pannous wants to merge 4 commits intosindresorhus:mainfrom
pannous:no-top-level-side-effects
Open

Add no-top-level-side-effects rule#2912
pannous wants to merge 4 commits intosindresorhus:mainfrom
pannous:no-top-level-side-effects

Conversation

@pannous
Copy link
Copy Markdown

@pannous pannous commented Mar 18, 2026

Summary

Implements the no-top-level-side-effects rule proposed in #1661.

Modules with top-level side effects cannot be safely tree-shaken. Webpack calls these "pure modules" — a module is considered side-effect-free when it doesn't execute observable operations at the top level.

What is flagged

  • Top-level function calls: foo(), console.log(), foo.bar()
  • Top-level optional calls: foo?.()
  • Top-level new expressions: new Foo()
  • Top-level tagged template literals: html`…`
  • Top-level IIFE: (function() {})(), (() => {})()
  • Top-level await wrapping the above: await fetch(url)

What is allowed

  • Variable/const/let declarations (even with initializers): const x = foo()
  • Function/class declarations
  • Import/export statements
  • Assignment expressions: module.exports = …, x = foo()
  • Directives: 'use strict'

Exceptions (entire file is skipped)

  • Files with a hashbang (#!/usr/bin/env node) — treated as scripts
  • Files with no exports — treated as scripts, not modules

Test plan

  • Valid cases: files without exports, hashbang files, declarations, assignments, exports
  • Invalid cases: function calls, new expressions, IIFEs, tagged templates, await
  • package.js tests pass (rule registered, has docs, in configs)

pannous and others added 4 commits March 18, 2026 12:48
Disallows side-effecting expression statements (function calls, `new`
expressions, tagged template literals) at the top level of ES modules,
enabling safe tree-shaking. Files with a hashbang or without any
exports are skipped. Assignment expressions are explicitly allowed.

Closes sindresorhus#1661
… dogfooding

- Commit missing AVA snapshots (were untracked — caused all test CI failures)
- Add blank line before list in docs to fix MD032 markdownlint error
- Refactor GlobalReferenceTracker to use static class fields instead of
  Object.assign() at module top level (fixes run-rules-on-codebase failure)
…Tracker

The @stylistic/lines-between-class-members rule disallows blank lines
between class members. The blank line between static properties and
private fields caused a CI lint error.
@sindresorhus
Copy link
Copy Markdown
Owner

I couple of things:

  • export default foo() and export default await foo() currently pass. Those calls still execute during module evaluation, but the rule only inspects top-level ExpressionStatements, so anything wrapped in export default slips through.
  • The matcher is also very easy to bypass with wrappers like void foo(), condition && foo(), foo(), bar(), or if (condition) { foo(); }. Those are still top-level side effects, but the current implementation doesnt report them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants