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

@tempots/eslint-plugin

v0.14.0

Published

ESLint plugin for TempoTS to catch common signal disposal issues

Readme

@tempots/eslint-plugin

npm version license codecov CI

ESLint plugin for TempoTS to help catch common signal usage issues and prevent memory leaks.

Installation

pnpm add -D @tempots/eslint-plugin

Usage

Recommended Configuration (Easiest)

Use the recommended configuration for automatic signal disposal best practices:

// eslint.config.js
import tempots from '@tempots/eslint-plugin'

export default [
  tempots.configs.recommended,
  // ... your other configs
]

This enables:

  • no-module-level-signals (warn) - Signals at module level
  • no-unnecessary-disposal (warn) - Unnecessary manual disposal
  • require-untracked-disposal (error) - Untracked signals without disposal
  • require-async-signal-disposal (warn) - Signals in async contexts
  • no-signal-reassignment (error) - Signal variable reassignment
  • prefer-const-signals (warn) - Prefer const for signals
  • no-renderable-signal-map (warn) - Mapping signals to renderables
  • no-empty-fragment (warn) - Empty Fragment() usage
  • no-single-child-fragment (warn) - Fragment() with one child
  • no-method-reference (error) - Passing Signal/Prop/Computed methods by reference (requires type-checked linting)

Note: The no-method-reference rule requires type-checked linting (parserOptions.projectService). Without it, the rule will report a warning that type information is missing.

Strict Configuration

For maximum safety, use the strict configuration:

// eslint.config.js
import tempots from '@tempots/eslint-plugin'

export default [
  tempots.configs.strict,
  // ... your other configs
]

All rules are set to error instead of warn (includes type-checked rules).

Custom Configuration

Customize individual rules:

// eslint.config.js
import tempots from '@tempots/eslint-plugin'

export default [
  {
    plugins: {
      tempots,
    },
    rules: {
      'tempots/no-module-level-signals': 'warn',
      'tempots/no-unnecessary-disposal': 'warn',
      'tempots/require-untracked-disposal': 'error',
      'tempots/require-async-signal-disposal': 'warn',
      'tempots/no-signal-reassignment': 'error',
      'tempots/prefer-const-signals': 'warn',
      'tempots/no-renderable-signal-map': 'warn',
      'tempots/no-empty-fragment': 'warn',
      'tempots/no-single-child-fragment': 'warn',
    },
  },
]

Automatic Signal Disposal

Important: As of @tempots/dom >= 1.0.0, signals are automatically disposed when components unmount. You no longer need to manually call OnDispose(signal.dispose) for signals created within renderables!

const MyComponent = ctx => {
  const count = prop(0) // ✨ Auto-disposed
  const doubled = count.map(x => x * 2) // ✨ Auto-disposed

  return html.div('Count: ', count, ' Doubled: ', doubled)
  // No OnDispose needed!
}

Manual Disposal for Special Cases

For signals created in async contexts or that need explicit lifecycle management, use the scope parameter:

const MyComponent = (ctx, scope) => {
  // Option 1: Track a signal manually
  setTimeout(() => {
    const asyncSignal = prop(0)
    scope.track(asyncSignal) // Will be disposed when component unmounts
  }, 1000)

  // Option 2: Register a disposal callback
  setTimeout(() => {
    const asyncSignal = prop(0)
    scope.onDispose(asyncSignal.dispose) // Will be called on unmount
  }, 1000)

  return html.div('Hello')
}

Rules

no-unnecessary-disposal (Recommended)

Warns about unnecessary manual disposal of auto-disposed signals.

Why? Signals created within renderables are automatically disposed. Manually disposing them with OnDispose() is unnecessary and may cause double-disposal issues.

❌ Incorrect

const MyComponent = ctx => {
  const count = prop(0)
  const doubled = count.map(x => x * 2)

  return Fragment(
    OnDispose(count.dispose), // ❌ Unnecessary - auto-disposed
    OnDispose(doubled.dispose), // ❌ Unnecessary - auto-disposed
    html.div(count, doubled)
  )
}

✅ Correct

const MyComponent = ctx => {
  const count = prop(0) // ✨ Auto-disposed
  const doubled = count.map(x => x * 2) // ✨ Auto-disposed

  return html.div(count, doubled)
  // No OnDispose needed!
}

Auto-fix: This rule can automatically remove unnecessary OnDispose() calls.


require-untracked-disposal (Recommended)

Requires disposal of signals created with untracked().

Why? Signals created with untracked() are explicitly excluded from automatic disposal and must be manually disposed to prevent memory leaks.

❌ Incorrect

// Module level - never disposed!
const globalState = untracked(() => prop(0)) // ❌ Memory leak

const MyComponent = ctx => {
  return html.div(globalState)
}

✅ Correct

const globalState = untracked(() => prop(0))

// Later, when done:
globalState.dispose() // ✅ Manually disposed

// Or in a cleanup function:
function cleanup() {
  globalState.dispose()
}

require-async-signal-disposal (Recommended)

Requires proper disposal for signals created in async contexts.

Why? Signals created in async contexts (setTimeout, Promise callbacks, async functions) execute after the renderable returns, so they're not tracked by the disposal scope and won't be auto-disposed. You must manually manage their disposal.

❌ Incorrect

const MyComponent = ctx => {
  setTimeout(() => {
    const asyncSignal = prop(0) // ❌ Not auto-disposed!
    // This will leak memory
  }, 1000)

  return html.div('Hello')
}

const AsyncComponent = async ctx => {
  const data = await fetchData()
  const signal = prop(data) // ❌ Not auto-disposed!
  return html.div(signal)
}

✅ Correct

// Option 1: Create signals synchronously (preferred)
const MyComponent = ctx => {
  const asyncSignal = prop(0) // ✨ Auto-disposed

  setTimeout(() => {
    asyncSignal.value = 42 // Just update the value
  }, 1000)

  return html.div(asyncSignal)
}

// Option 2: Use scope.track() for manual tracking
const MyComponent = (ctx, scope) => {
  setTimeout(() => {
    const asyncSignal = prop(0)
    scope.track(asyncSignal) // ✅ Manually tracked, will be disposed
  }, 1000)

  return html.div('Hello')
}

// Option 3: Use scope.onDispose() for manual disposal
const MyComponent = (ctx, scope) => {
  setTimeout(() => {
    const asyncSignal = prop(0)
    scope.onDispose(asyncSignal.dispose) // ✅ Will be disposed
  }, 1000)

  return html.div('Hello')
}

// Option 4: Use untracked() if signal should outlive the component
const MyComponent = ctx => {
  setTimeout(() => {
    const asyncSignal = untracked(() => prop(0))
    // Remember to dispose when done:
    // asyncSignal.dispose()
  }, 1000)

  return html.div('Hello')
}

no-module-level-signals (Recommended)

Warns about signals created at module level (outside renderables).

Why? With automatic signal disposal, signals created within renderables are automatically tracked and disposed. However, signals created at module level will be tracked by the global scope and may cause unexpected behavior.

❌ Incorrect

// Module level - will be tracked by global scope!
const globalCount = prop(0)
const doubled = globalCount.map(x => x * 2)

const MyComponent = ctx => {
  return html.div(globalCount)
}

✅ Correct

// Option 1: Move inside renderable (auto-disposed)
const MyComponent = ctx => {
  const count = prop(0) // ✨ Auto-disposed
  return html.div(count)
}

// Option 2: Use untracked() for long-lived signals
const globalCount = untracked(() => prop(0)) // Explicitly long-lived
// Remember to dispose manually when done: globalCount.dispose()

const MyComponent = ctx => {
  return html.div(globalCount)
}

no-signal-reassignment (Recommended)

Prevents reassignment of signal variables.

Why? Reassigning a signal variable creates a memory leak because the original signal is not disposed. Signal values should be updated using .value, not by reassigning the variable.

❌ Incorrect

const MyComponent = ctx => {
  let count = prop(0) // Using let allows reassignment

  // Later...
  count = prop(1) // ❌ Memory leak! Original signal not disposed

  return html.div(count)
}

✅ Correct

const MyComponent = ctx => {
  const count = prop(0) // Using const prevents reassignment

  // Update the value, not the variable
  count.value = 1 // ✅ Correct way to update

  return html.div(count)
}

prefer-const-signals (Recommended)

Prefers const for signal declarations instead of let or var.

Why? Using const prevents accidental reassignment of signal variables, which would cause memory leaks. This rule is auto-fixable.

❌ Incorrect

const MyComponent = ctx => {
  let count = prop(0) // ❌ Should use const
  var doubled = count.map(x => x * 2) // ❌ Should use const

  return html.div(count, doubled)
}

✅ Correct

const MyComponent = ctx => {
  const count = prop(0) // ✅ Using const
  const doubled = count.map(x => x * 2) // ✅ Using const

  return html.div(count, doubled)
}

no-renderable-signal-map (Recommended)

Warns when signals/computeds/props produce renderables (e.g., signal.map(v => html.div(v)) or computedOf(signal)(v => html.div(v))).

Why? Signals can be passed directly into renderables. Producing a renderable from a signal creates a signal of renderables and is usually unnecessary.

❌ Incorrect

const MyComponent = ctx => {
  const count = prop(0)
  const view = count.map(v => html.div(v)) // ❌ Avoid this pattern

  return view
}

✅ Correct

const MyComponent = ctx => {
  const count = prop(0)

  return html.div(count) // ✅ Pass signal directly
}

no-empty-fragment (Recommended)

Warns about Fragment() with no children.

Why? An empty fragment does nothing. Use Empty instead.

❌ Incorrect

const MyComponent = ctx => {
  return Fragment()
}

✅ Correct

const MyComponent = ctx => {
  return Empty
}

no-single-child-fragment (Recommended)

Warns about Fragment() with a single child.

Why? A fragment is only needed to group multiple children.

❌ Incorrect

const MyComponent = ctx => {
  return Fragment(html.div('hello'))
}

✅ Correct

const MyComponent = ctx => {
  return html.div('hello')
}

no-method-reference (Type-Checked)

Disallows passing Signal/Prop/Computed methods by reference (loses this binding).

Why? Tempo signal classes use prototype methods, not arrow function fields. Extracting a method (e.g., signal.dispose instead of () => signal.dispose()) loses the this binding and will fail at runtime.

Requires type-checked linting. This rule uses the TypeScript type checker to determine whether an object is a Signal type. Without parserOptions.projectService, the rule will report a warning that type information is missing.

❌ Incorrect

const s = signal(0)
const other = signal(1)

s.onDispose(other.dispose)        // ❌ Loses `this`
s.on(p.set)                       // ❌ Loses `this`
const setter = p.set              // ❌ Loses `this`
const fns = [s.dispose]           // ❌ Loses `this`
const obj = { cleanup: s.dispose } // ❌ Loses `this`

✅ Correct

const s = signal(0)
const other = signal(1)

s.onDispose(() => other.dispose())        // ✅ Wrapped in lambda
s.on(v => p.set(v))                       // ✅ Wrapped in lambda
const setter = (v: number) => p.set(v)    // ✅ Wrapped in lambda
const fns = [() => s.dispose()]           // ✅ Wrapped in lambda
const obj = { cleanup: () => s.dispose() } // ✅ Wrapped in lambda

require-signal-disposal (Deprecated)

⚠️ DEPRECATED: This rule is deprecated as of @tempots/dom >= 1.0.0 because signals are now automatically disposed. It is kept for backward compatibility with older versions but will be removed in a future release.

For projects using @tempots/dom >= 1.0.0, use no-module-level-signals instead.

Options

{
  'tempots/require-signal-disposal': ['warn', {
    checkTransforms: true,      // Check signal transformations (.map, .filter, etc.)
    checkCreations: true,       // Check signal creations (prop, signal, computed)
    useTypeInformation: 'auto', // Use TypeScript type checking when available
                                // Options: 'auto' | 'always' | 'never'
  }]
}

See the legacy documentation for details on this deprecated rule.

Contributing

See the main CONTRIBUTING.md for guidelines.

License

Apache-2.0