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

kensington-eslint-plugin

v0.2.1

Published

ESLint rules for kensington signal correctness

Readme

kensington-eslint-plugin

ESLint rules for kensington signal correctness.

Catches common reactive programming mistakes — read/write loops, writes inside computed derivations, orphaned effects, and async subscription pitfalls — at lint time rather than at runtime.

Installation

npm install --save-dev kensington-eslint-plugin

Requires ESLint 9+ and Node 18+.

Usage

Add the recommended config to your eslint.config.js:

import kensington from 'kensington-eslint-plugin';

export default [
  kensington.configs.recommended,
  // ...your other configs
];

Or enable rules individually:

import kensington from 'kensington-eslint-plugin';

export default [
  {
    plugins: { kensington },
    rules: {
      'kensington/no-set-in-computed': 'error',
      'kensington/no-self-read-write': 'error',
      // ...
    },
  },
];

Editor and tooling support

Because this is a standard ESLint plugin, it works anywhere ESLint runs — no extra configuration needed:

  • Editors — VS Code, JetBrains IDEs (RubyMine, WebStorm, etc.), Neovim, and any editor with an ESLint language server show inline errors automatically once the plugin is configured.
  • CI — run eslint --max-warnings 0 in any pipeline to enforce rules on every push.
  • Pre-commit hooks — works with lint-staged or any hook runner that invokes ESLint.
  • Programmatic use — available via the ESLint Node.js API (new ESLint()) for custom tooling.

Rules

| Rule | Description | Recommended | |------|-------------|-------------| | no-set-in-computed | Disallow .set() inside a computed() body | error | | no-self-read-write | Disallow reading and writing the same signal in the same reactive run | error | | no-set-on-computed | Disallow .set() on a computed signal | error | | no-new-signal-in-effect | Disallow creating a new signal() inside an effect() body | error | | no-effect-in-computed | Disallow calling effect() inside a computed() body | error | | no-signal-async-write | Disallow writing a signal in an async callback when it was read in the enclosing effect() | warn | | no-ignored-effect-return | Require capturing the return value of effect() inside a function | warn | | prefer-value-in-async | Prefer .value over .get() inside async callbacks within an effect() | warn | | no-new-computed-in-effect | Disallow creating a new computed() inside an effect() body | error | | no-new-signal-in-computed | Disallow creating a new signal() inside a computed() body | error | | no-unsafe-literal | Disallow .unsafeLiteral() calls that bypass XSS protection | error | | no-new-computed-in-computed | Disallow creating a new computed() inside a computed() body | error | | no-effect-in-effect | Disallow creating a new effect() inside an effect() body | error | | no-async-effect | Disallow async callbacks passed to effect() | error | | no-async-computed | Disallow async callbacks passed to computed() | error | | no-set-in-transform | Disallow .set() inside a .transform() callback | error | | no-set-on-transform | Disallow .set() on a transform-derived signal | error |


no-set-in-computed

Computed functions must be pure derivations. Calling .set() inside one causes a write during a read pass.

// Bad
const doubled = computed(() => {
  sideEffect.set(true); // error
  return count.get() * 2;
});

// Good — move the write into an effect
effect(() => {
  sideEffect.set(doubled.get() > 10);
});

no-self-read-write

Reading a signal with .get() subscribes to it. Writing it with .set() in the same run re-triggers the run, creating an infinite loop.

// Bad
effect(() => {
  const val = count.get();
  count.set(val + 1); // error — triggers the effect again
});

// Good — use .value to read without subscribing
effect(() => {
  const val = count.value;
  count.set(val + 1);
});

no-set-on-computed

Computed signals are read-only. Kensington throws at runtime if you call .set() on one; this catches it statically.

const doubled = computed(() => count.get() * 2);
doubled.set(10); // error — use signal() for writable state

no-new-signal-in-effect

Each effect run creates a fresh signal with no cleanup path. The signal should be declared outside the effect.

// Bad
effect(() => {
  const local = signal(0); // error — orphaned on every run
});

// Good
const local = signal(0);
effect(() => {
  local.set(local.get() + 1);
});

no-effect-in-computed

Computed functions must be pure. An effect() call inside one runs on every re-evaluation and its handle is dropped, making cleanup impossible.

// Bad
const doubled = computed(() => {
  effect(() => console.log('hi')); // error
  return count.get() * 2;
});

no-signal-async-write

If a signal is read via .get() in an effect and then written in an async callback, the write re-triggers the effect after each async resolution.

// Bad
effect(() => {
  const val = count.get(); // subscribes
  setTimeout(() => {
    count.set(val + 1); // error — re-triggers the effect
  }, 100);
});

// Good — use .value to read without subscribing
effect(() => {
  setTimeout(() => {
    count.set(count.value + 1);
  }, 100);
});

no-ignored-effect-return

effect() returns { pause, resume, stop }. Discarding the return value inside a function makes cleanup impossible, leaking the subscription across calls.

// Bad
function setup() {
  effect(() => console.log(count.get())); // warn — can't stop it
}

// Good
function setup() {
  const fx = effect(() => console.log(count.get()));
  return () => fx.stop();
}

Module-level effects are intentionally long-lived and are not flagged.


prefer-value-in-async

Once an effect's synchronous body completes, async callbacks run outside its reactive context. .get() registers no subscription there — .value makes that explicit.

// Bad
effect(() => {
  fetch('/api').then(() => {
    console.log(count.get()); // warn — no subscription is registered
  });
});

// Good
effect(() => {
  fetch('/api').then(() => {
    console.log(count.value);
  });
});

no-new-computed-in-effect

Creating computed() inside an effect() creates a new orphaned derived signal on every run. The previous one silently loses its subscriber with no cleanup.

// Bad
effect(() => {
  const doubled = computed(() => count.get() * 2); // error — orphaned every run
  console.log(doubled.get());
});

// Good
const doubled = computed(() => count.get() * 2);
effect(() => { console.log(doubled.get()); });

no-new-signal-in-computed

Creating signal() inside computed() creates a new orphaned signal on every recompute.

// Bad
const c = computed(() => {
  const temp = signal(0); // error — orphaned every recompute
  return temp.get() + base.get();
});

// Good
const temp = signal(0);
const c = computed(() => temp.get() + base.get());

no-unsafe-literal

.unsafeLiteral() injects raw HTML with no script-tag validation. Use .literal() instead, which validates the string before injecting it.

// Bad
t.unsafeLiteral(userContent); // error — bypasses XSS protection

// Good
t.literal(userContent);

no-new-computed-in-computed

Creating computed() inside a computed() body creates a new orphaned derived signal on every recompute.

// Bad
const outer = computed(() => {
  const inner = computed(() => count.get() * 2); // error — orphaned every recompute
  return inner.get() + 1;
});

// Good
const inner = computed(() => count.get() * 2);
const outer = computed(() => inner.get() + 1);

no-effect-in-effect

Creating effect() inside an effect() body means every re-run of the outer effect adds a new inner effect without stopping the previous one — subscriptions accumulate indefinitely. Capturing the return handle does not fix this; the previous handle would need to be explicitly stopped at the top of each run.

// Bad
effect(() => {
  const items = list.get();
  effect(() => console.log(items)); // error — previous inner effect never stopped
});

// Good — restructure as a single effect
effect(() => {
  console.log(list.get());
});

no-async-effect

The effect system runs callbacks synchronously and ignores the returned Promise. Any .get() calls after the first await run outside the reactive context and register no subscription. Errors thrown inside the async body are also silently swallowed.

// Bad
effect(async () => { // error
  const data = await fetch(`/api/${id.get()}`).then(r => r.json());
  title.set(data.title); // runs outside reactive context
});

// Good — keep reactive reads synchronous, push async work into .then()
effect(() => {
  fetch(`/api/${id.get()}`).then(r => r.json()).then(data => title.set(data.title));
});

no-async-computed

The reactive system runs computed() callbacks synchronously. An async callback returns a Promise immediately, so the computed value is always a Promise object rather than the intended derived value.

// Bad — computed value is a Promise, not the resolved data
const data = computed(async () => { // error
  return await fetch('/api').then(r => r.json());
});
t.p(data); // renders "[object Promise]"

// Good — signal for the result, effect to populate it
const data = signal(null);
effect(() => {
  fetch('/api').then(r => r.json()).then(v => data.set(v));
});

no-set-in-transform

Transform callbacks must be pure derivations. Calling .set() inside one causes a write during a read pass, the same class of bug as .set() inside computed().

// Bad
const rows = items.transform(list => {
  selectedId.set(null); // error
  return list.map(item => t.li(item.name));
});

// Good — move the write into a separate effect
effect(() => {
  if (!items.get().length) { selectedId.set(null); }
});

no-set-on-transform

Transform-derived signals are read-only. Calling .set() on one throws at runtime; this rule catches it statically.

// Bad
const doubled = count.transform(v => v * 2);
doubled.set(10); // error — transform results are read-only

// Good — write to the source signal instead
count.set(5);