npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eslint-plugin-no-magic

v1.9.1

Published

ESLint rule that flags magic string literals only in suspicious contexts (comparisons, switch cases, action types, analytics/storage/feature-flag/route sinks) and on duplication, instead of reporting every literal.

Readme

eslint-plugin-no-magic

ESLint plugin that flags magic string literals only where they matter: a literal is "magic" when it participates in logic, a protocol, or a business contract whose meaning should be named — not merely because it is a string.

Reporting every string literal turns the linter into noise (JSX props, imports, object keys, type unions, human copy are all strings). This plugin reports a string only when it is one of:

  • an operand of an equality/inequality comparison (===, !==, ==, !=),
  • a switch case test,
  • an argument to a known behavioral sink (analytics, storage, feature flags, routing, etc.),
  • the type of an object passed to an action dispatcher (dispatch({ type: "..." })),
  • or duplicated across the file at or above a configurable threshold.

Everything else is ignored by default: JSX attribute values, visible JSX copy, structural literals inside inline SVG markup, import/export sources, runtime directives ("use client"), typeof comparison vocabulary, object keys, property-name access, TypeScript literal-union types, enum member initializers and extracted const definitions. No imports, packages, or framework names receive special exemptions. JSX and SVG handling is syntax-based.

Install

pnpm add --save-dev eslint-plugin-no-magic

Requires ESLint 10.10+ (flat config) and Node.js 26.x (^26.0.0). Development uses pnpm 12+ (pinned to 12.3.4) and Vitest 5.

Rule reference

Each rule includes examples, defaults, options, and known limitations. ESLint meta.docs.url points to its page, and the package includes all five documents.

Shared configuration

import { createConfig } from "eslint-plugin-no-magic";

export default createConfig({
  files: ["src/**/*.{js,ts,tsx}"],
  contracts: { sinks: [{ callee: "client.send", argumentIndex: 1 }] },
  duplicates: { minDuplicates: 3 },
  contractSeverity: "error",
  duplicateSeverity: "warn",
});

createConfig synchronizes contract classification in both rules and copies options so callers cannot mutate subsequent configurations. Contract allowlists are shared by default; duplicate ignoreStrings can override them. Disabling contracts automatically enables duplicate reporting on contract positions unless duplicates.ignoreContracts explicitly overrides it. Configure the parser in another flat config entry when using TypeScript or non-JavaScript syntax.

Usage

Quick start (recommended flat config)

// eslint.config.mjs
import noMagic from "eslint-plugin-no-magic";

export default [
  ...noMagic.configs.recommended,
];

Manual wiring

import noMagic from "eslint-plugin-no-magic";

export default [
  {
    files: ["app/**/*.{ts,tsx}", "src/**/*.{ts,tsx}"],
    plugins: { "no-magic": noMagic },
    rules: {
      "no-magic/no-magic-contracts": "error",
      "no-magic/no-duplicate-strings": "warn",
    },
  },
];

Independent rules

The recommended preset enables no-magic-contracts as an error and no-duplicate-strings as a warning with ignoreContracts: true. Both can be configured independently:

export default [
  ...noMagic.configs.recommended,
  { rules: {
    "no-magic/no-magic-contracts": ["error", { ignoreStrings: ["legacy"] }],
    "no-magic/no-duplicate-strings": ["warn", { minDuplicates: 4 }],
  } },
];

no-magic-contracts accepts sinks, actionTypeCallees, actionTypeProperty, and ignoreStrings. no-duplicate-strings accepts minDuplicates, ignoreStrings, ignoreContracts, and contractOptions.

ignoreContracts defaults to true in all configurations. It omits duplicate diagnostics on contract positions while retaining those occurrences in the duplicate count. This is a syntactic classification, not coordination between enabled rules: if you disable the contract rule and want duplicate reports everywhere, set ignoreContracts: false.

When customizing contract detection, pass the same options to contractOptions:

const contractOptions = { sinks: [{ callee: "client.send", argumentIndex: 1 }] };
// Rules in your flat configuration:
const rules = {
  "no-magic/no-magic-contracts": ["error", contractOptions],
  "no-magic/no-duplicate-strings": ["warn", {
    ignoreContracts: true, contractOptions, minDuplicates: 3,
  }],
};

A value in contractOptions.ignoreStrings remains eligible for duplication; ignoreStrings on the duplicate rule itself excludes it entirely. Disable the duplicate rule if only contract detection is wanted.

no-magic-strings was deprecated in 0.2.2 and removed in 1.0.0. It is no longer registered or shipped. See migration notes.

Contract and duplicate rule options

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | sinks | (string \| { callee: string, argumentIndex: number })[] | track, trackEvent, sendEvent, logEvent, captureEvent, getItem/setItem/removeItem (argument 0), isFeatureEnabled, isEnabled, getFlag, navigate, { callee: "router.push", argumentIndex: 0 }, { callee: "router.replace", argumentIndex: 0 } | Callee names whose string arguments are treated as contracts. Strings match method names or full static paths; descriptors match a method name or full static path and a zero-based argument index. | | actionTypeCallees | string[] | ["dispatch"] | Callees whose object argument's action-type property is treated as a contract. | | actionTypeProperty | string | "type" | The property name inspected inside actionTypeCallees arguments. | | minDuplicates | integer >= 0 | 3 | Report a value repeated this many times (across non-allowlisted positions). 0 disables duplicate detection. | | ignoreStrings | string[] | [] | Exact string values that are always allowed. |

"no-magic/no-magic-contracts": ["error", {
  sinks: ["track", "getItem", "setItem", "removeItem",
    { callee: "router.push", argumentIndex: 0 },
    { callee: "router.replace", argumentIndex: 0 }],
  ignoreStrings: ["latin"],
}]

JSX and SVG presentation values remain allowed, but comparisons, action types, and configured calls inside their expressions are checked. TypeScript as, satisfies, type assertions, and non-null assertions preserve this detection. Single-character values are checked in these behavioral contexts; only duplicate checking ignores them. ignoreStrings still explicitly allows exact values.

Duplicate counts include occurrences already reported for a behavioral context, without reporting the same node twice. minDuplicates: 0 disables counting.

Routing defaults now match only router.push and router.replace, argument 0. For a differently named router, configure its static path explicitly. Legacy sinks: ["push", "replace"] still opts into matching every method with those names. Descriptors do not resolve aliases or infer receiver types.

Magic numbers

This plugin does not re-implement magic-number detection. Use the upstream @typescript-eslint/no-magic-numbers rule directly. Sensible defaults are exported for convenience:

import tseslint from "typescript-eslint";
import { recommendedMagicNumberOptions } from "eslint-plugin-no-magic";

export default [
  {
    files: ["app/**/*.{ts,tsx}", "src/**/*.{ts,tsx}"],
    plugins: { "@typescript-eslint": tseslint.plugin },
    languageOptions: { parser: tseslint.parser },
    rules: {
      "@typescript-eslint/no-magic-numbers": ["error", recommendedMagicNumberOptions],
    },
  },
];

recommendedMagicNumberOptions ignores -1, 0, 1, array indexes, default values, enums, numeric literal types, and readonly class properties, and enforces const.

Development

pnpm install
pnpm build    # TypeScript implementation -> dist/ JavaScript and declarations
pnpm test     # Vitest 5 + ESLint RuleTester
pnpm lint
pnpm typecheck # Public configuration types
pnpm benchmark # Deterministic end-to-end lint benchmark

Publishing

pnpm create-version publishes automatically. It needs an npm automation token:

cp .env.example .env   # then edit .env and set NPM_TOKEN

The token is read from the environment or the gitignored .env and is never printed. The repository has no .npmrc: beez-rp writes a temporary npm user config outside the repository that only references ${NPM_TOKEN} (npm expands it from the environment, so the token never reaches the disk or a command line), passes it with --userconfig and removes it afterwards. Before publishing, it requires the release commit to stay unmodified, checks the SHA-256 in the releases/<version>-<sha256>/ path, and compares the tarball SHA-512 with the integrity of npm pack --dry-run for that commit; npm pack is reproducible, so a match proves the archive is byte for byte what npm would pack. Only then it publishes that exact tarball. prepublishOnly runs beez-rp guard-publish, which stops a manual pnpm publish (or yarn/bun): publish only through pnpm create-version, which publishes with npm. npm inherits the terminal, so its interactive browser or one-time-password confirmation still works.

License

MIT

Typed configuration

The package includes TypeScript declarations and exports NoMagicContractsOptions and SinkDescriptor for typed consumer configuration:

import type { NoMagicContractsOptions } from "eslint-plugin-no-magic";

const options = {
  sinks: [{ callee: "router.push", argumentIndex: 0 }],
} satisfies NoMagicContractsOptions;

Contract detection follows ternary value branches and logical fallbacks, including TypeScript wrappers. Computed static dispatcher/property names are equivalent to dot notation. Storage defaults inspect only argument 0 (the key), and preserve method-name matching for custom storage receivers. Real directive prologues and visible JSX copy in conditional branches remain exempt from duplicate reporting.

CI runs frozen installation, lint, type checks, and tests on Windows and Linux with Node 26. The suite packs the package and checks real ESLint behavior and declaration consumption from the extracted artifact.

TypeScript compiler compatibility

Type checking uses TypeScript 7 (tsc), installed as @typescript/native: npm:typescript@^7.0.2. The ESLint parser still requires the TypeScript 6 API, so typescript aliases @typescript/typescript6 for parsing. Both are real implementations; the compatibility package exposes tsc6 and does not replace the TypeScript 7 compiler used by pnpm typecheck or the packaged consumer test. This follows Microsoft's side-by-side installation guidance.

Performance baseline

pnpm benchmark compares parsing alone and the recommended independent rules on 300, 3,000, and 15,000 literals in JavaScript, JSX, TypeScript, and expressions with 40 nested conditional branches. Every run asserts diagnostic counts. It records the median of five measured runs after two warmups, along with Node, ESLint, OS, and CPU metadata. See the baseline. The benchmark includes parsing and diagnostic construction; it is not an isolated rule timing or a CI performance threshold.

Advisory constant rules

These optional rules implement statically verifiable parts of constant reuse:

rules: {
  "no-magic/prefer-existing-constant": "warn",
  "no-magic/no-duplicate-constants": ["warn", { ignoreValues: [100] }],
}

prefer-existing-constant accepts the contract rule options. It suggests a preceding, visible primitive string constant when the same value occurs in a comparison, switch case, action type, or configured call. It respects lexical shadowing and does not evaluate imports or expressions.

no-duplicate-constants reports repeated primitive string/number definitions in the same lexical scope, excluding trivial values and ignoreValues. Separate scopes are independent. Neither rule applies automatic fixes: equal values can represent different business concepts. Neither infers cross-file ownership, folder placement, deployment configuration, or whether a shared constant is safe for client/server boundaries. Both are opt-in and absent from the recommended preset.

Visual formatter

Consumer CLI usage:

pnpm exec eslint . --format ./node_modules/eslint-plugin-no-magic/dist/formatter.js

The formatter shows severity colors, Unicode frames, numbered source excerpts, underlined ranges, rule IDs, and a summary. Duplicate messages include the first eligible occurrence's line and column. It uses source supplied by ESLint and does not read other files. Long lines are clipped around the diagnostic.

For programmatic control:

import { createFormatter } from "eslint-plugin-no-magic/formatter";
const format = createFormatter({ color: false, unicode: false });
console.log(format(results));

Color is detected from the terminal, honors NO_COLOR and FORCE_COLOR, and can be overridden explicitly. Unicode decoration is enabled by default; the ASCII mode disables it. Control characters are escaped to prevent terminal injection. Terminal-specific ambiguous Unicode widths may differ. Editors and JSON formatters still receive ordinary text messages with standard ESLint source locations.

Create a version

Changes are recorded under ## [Unreleased] in CHANGELOG.md, grouped by Keep a Changelog sections (### Added, ### Changed, ### Deprecated, ### Removed, ### Fixed, ### Security), in English ASCII. Never edit the version or the release date by hand.

pnpm create-version            # alias: pnpm cv
pnpm create-version --dry-run  # diagnosis and plan only
pnpm create-version --bump minor

The command comes from beez-rp, shared by the Beez projects, and is configured in beez-rp.config.js. From a clean, synced main it asks Codex to fill an empty [Unreleased] block in English ASCII, lets you choose the next patch, minor or major version, releases [Unreleased] as ## [X.Y.Z] - YYYY-MM-DD, commits package.json and CHANGELOG.md as X.Y.Z with the annotated vX.Y.Z tag, runs pnpm release:prepare on that commit, pushes main plus the tag atomically, and publishes that exact verified tarball to npm with NPM_TOKEN (see Publishing). Feature branches, uncommitted files or foreign commits on main stop it with the next action to take. Running it again resumes a release left unpushed or a version npm does not have yet, preparing and publishing only what is missing.

Prepare a local release

pnpm release:prepare

This checks the package version against the newest released changelog entry (skipping [Unreleased]), enforces ASCII release notes, runs frozen installation/tests/types/lint, packs with npm pack --ignore-scripts (npm, not pnpm, so the archive is reproducible), then verifies the tarball contains its declared exports and no unexpected private files. Output goes to releases/<version>-<sha256>/ so previous artifacts are preserved. The directory is ignored by Git. It does not bump versions, create commits or tags, or publish: pnpm create-version runs it on the release commit and publishes that tarball.

Configurable duplicate syntax exemptions

no-duplicate-strings accepts ignoreSyntax, with all categories enabled by default:

createConfig({ duplicates: {
  ignoreSyntax: { jsx: false, svg: true, constDefinitions: false },
} });

jsx controls attribute values and rendered expressions outside SVG; svg controls SVG markup independently; constDefinitions controls extracted constant values. Setting a category to false includes it in duplicate counts and reports. Imports, type declarations, directives, object keys, and property-name access stay exempt. Behavioral contracts are still checked regardless of presentation exemptions. These switches affect string duplication, not no-duplicate-constants.

Exclude constant names

Both advisory constant rules accept ignoreConstantNames: ["HTTP_TIMEOUT_MS"]. Names are exact and case-sensitive, not patterns. The duplicate-definition rule excludes those names as both candidates and reported declarations. The reuse rule never suggests excluded names and still respects lexical shadowing; it may suggest another visible, non-excluded constant with the same value. These options do not affect the independent string contract and duplicate-string rules.

Use PreferExistingConstantOptions or NoDuplicateConstantsOptions for typed configuration. Value-level and name-level exclusions can be combined.

Profile optional constant rules

Run pnpm benchmark:constants to compare parser-only, reuse-only, duplicate-definition-only, and both optional rules on 100, 500, and 1,000 candidates. The fixtures cover hits, misses, and 20 nested scopes. Five measured runs follow two warmups, with exact diagnostic assertions and ESLint per-rule statistics. Results are written to benchmarks/constants-results.json. See the measured baseline. This command measures existing behavior; it neither optimizes the rules nor sets a timing gate.

Reuse regression profiles

The constant benchmark also covers many constants with the same value, matching candidates shadowed by parameters, and fully shadowed misses. pnpm benchmark:constants --baseline records constants-before.json; pnpm benchmark:constants --compare records current results and requires identical complete diagnostics (including messages, locations, severity, and order) across all recorded scenarios. Capture a new baseline only when the reference behavior is intentionally changed. Profiling still uses five samples after two warmups.

prefer-existing-constant uses file-local scope/value indexes and cached visible candidates. Temporal availability, declaration preference, name exclusions, and shadowing remain unchanged. See before/after results for measurements and complete diagnostic-equivalence checks.

constants-refactor preset

export default createConfig({
  files: ["src/**/*.{js,ts,tsx}"],
  preset: "constants-refactor",
});

The preset applies one policy across rules, matching a "every static constant lives in constants/" refactor workflow:

  • contracts.structuralDiscriminants defaults to ["type", "kind", "operator"], so comparisons such as node.type === "Identifier" are treated as language vocabulary by contract, duplicate, and reuse rules. This also exempts action.type === "ADD_TODO"; override the list where type carries contracts.
  • constantsLocation is enabled with exportedOnly: false, requiring every static module-level constant to live under **/constants/**.

Explicit settings always win: contracts.structuralDiscriminants, a constantsLocation object (merged over the preset defaults), or constantsLocation: false. Without preset, output is unchanged.

Advisory rules through createConfig

createConfig({
  contracts: { sinks: ["send"] },
  reuse: { severity: "warn", ignoreConstantNames: ["LOCAL_VALUE"] },
  constantDuplicates: { severity: "warn", ignoreValues: [100] },
});

constantsLocation: { severity, patterns, exportedOnly, ignoreConstantNames } enables require-constants-location the same way.

Both advisory options default to false. true enables a rule with warning severity. An object enables it with options and an optional severity. Reuse inherits contract settings, including sinks and ignored strings, without duplicate configuration. Its own options are severity and excluded names. Constant-definition duplication has independent value and name exclusions. Input options are copied.

Terminal navigation

createFormatter({ hyperlinks: true, linkTarget: "vscode" }) emits OSC 8 links that open an exact editor position. The default linkTarget: "file" uses file URLs with location fragments; whether the opener honors the line depends on the application. Labels include file:line:column. URLs escape spaces and controls.

Without an explicit option, links are enabled only for interactive terminals identified as Windows Terminal, VS Code, iTerm, WezTerm or Ghostty, and disabled in CI. Unknown terminals and non-file results keep plain locations. Use hyperlinks: false for logs regardless of terminal capabilities. Color and links are independent settings.

Memory profiling

pnpm benchmark:memory runs parser-only, reuse, and both advisory rules in separate Node processes. It verifies diagnostics on 100 files with multiple scopes and records heap after explicit GC, retained delta, sampled peak heap, and RSS. The memory report explains the methodology and limits. This is a diagnostic benchmark, not a leak assertion or CI memory threshold.

Minimum runtime compatibility

CI runs both Node 26.0.0 (the minimum allowed by engines.node) and the current 26.x version selected by .nvmrc, on Linux and Windows. Each combination installs from the frozen lockfile and runs lint, public type checks and the full test suite, including packaged-consumer integration. Locally, the same checks passed on Windows with Node 26.0.0 and 26.8.1. Remote CI execution is separate from local validation.

Reuse boundary guarantees and limits

Reuse suggestions accept direct primitive string initializers (including static templates and TypeScript assertions), not destructuring, imports, concatenations, or calls. Parameters and destructured bindings still shadow outer names.

Hoisted function declarations can run before outer constants initialize, so reuse searches stop after checking their local scope. Function expressions and arrows created after a constant remain eligible. Searches also stop at dynamic with environments. These conservative exclusions may omit safe suggestions; the rule does not execute code, resolve imports, or prove arbitrary call ordering.

The benchmark's shadowing fixtures use arrows created after their declarations. Historical diagnostic baselines predate this boundary audit; capture a new --baseline before using --compare with the revised fixtures.

Overlapping configurations

ESLint applies matching flat-config entries in order. Each createConfig entry supplies complete core string-rule options, so a later matching helper replaces those earlier options with its supplied values and defaults. A manual severity-only override retains existing rule options, following ESLint behavior.

Advisory options that are omitted inherit earlier matching entries. Explicit reuse: false or constantDuplicates: false emits off, disabling earlier rules. When changing shared contracts in a narrower helper, repeat reuse: true if you want the inherited reuse rule to receive the new contracts.

export default [
  ...createConfig({ reuse: true, constantDuplicates: true }),
  ...createConfig({ files: ["tests/**/*.js"], reuse: false, constantDuplicates: false }),
];

TypeScript implementation and distribution

Implementation lives in src/ and compiles with TypeScript 7 in strict mode. pnpm build recreates dist/ with ESM JavaScript and generated declarations. TypeScript checks unused bindings; ESLint checks the typed implementation using the real parser. AST and scope types are development-only dependencies.

Public imports remain eslint-plugin-no-magic and eslint-plugin-no-magic/formatter. Consumers execute JavaScript and do not need TypeScript or a runtime loader. The formatter CLI path is now ./node_modules/eslint-plugin-no-magic/dist/formatter.js.

Tests and benchmarks import the compiled implementation; their pnpm commands build first. typecheck also validates a consumer against the generated declarations. The package test loads the compiled tarball and its formatter, then compiles a consumer using the package exports. prepack builds automatically. dist/ and releases/ are generated and ignored by Git. Tooling scripts and test fixtures remain JavaScript; the published plugin implementation is TypeScript-authored.

One-build validation flows

pnpm check creates one fresh build, then runs the suite, consumer type checks, and lint against that output. CI and release:prepare use this flow. Release packing skips lifecycle scripts only after these gates succeed, so prepack does not compile the same output again.

Standalone pnpm test, pnpm typecheck, benchmarks, and ordinary pnpm pack remain self-contained and build first. No timestamps or persistent skip flags are used to decide whether output is current. If build or validation fails, check stops and release preparation does not pack an artifact.

Public type isolation checks

Consumer fixtures cover supported configurations and expected type errors for unknown rule names, invalid severities, misplaced options, malformed descriptors, syntax toggles, advisory settings, removed exports, and formatter options. Numeric ranges such as nonnegative thresholds remain runtime schema checks.

The package test copies the tarball's package into a temporary consumer outside this repository. Only its ESLint peer is linked there. A plain Node process, without inherited NODE_PATH or module loader options, verifies the plugin works and that parser, utils, compiler, Vitest and Node type packages are not resolvable from the consumer. TypeScript 7 is invoked externally as the test compiler; it checks every consumer fixture against the physically copied generated declarations with strict checking and no implicit ambient types. This checks dependency isolation; it is not an npm-registry installation test.