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

untodo

v0.0.1

Published

Type-safe TODO for humans and AI — trackable, structured, and lint-enforceable.

Readme

untodo

Type-safe TODO for humans and AI — trackable, structured, and lint-enforceable.

Type-safe replacement for // TODO: comments. Trackable in your IDE, enforceable by lint, surfaced by the type system.

Status: scaffold — APIs are placeholders. See Project Planning in the repo for the design.

Why

| Problem | Solution | |---|---| | // TODO: comments are easy to ignore | Function calls — lint can fail the build | | Comments don't reach the type system | Returns never so callers detect it | | No structure | Pass an object with reason, issue, etc. | | Hard to track in an IDE | "Find references" works on a function |

Inspired by Kotlin's TODO().

Affinity with AI coding assistants

Now that AI-assisted coding is the norm, // TODO: comments have a new set of problems. AI reads comments as plain text, so it tends to miss the fact that a TODO exists, and the way TODOs are written drifts from author to author.

untodo addresses both issues.

AI recognizes TODOs more reliably

A TODO in untodo is a function call backed by a type definition. AI coding assistants (Claude, GitHub Copilot, etc.) take type information and function signatures into account as context, so they pick up the existence and intent of a TODO far more reliably than from a // TODO: comment.

// AI sees this only as "text"
// TODO: implement later (where? who? why?)

// AI sees this as "a call to an unimplemented function"
return TODO({ reason: "API not implemented yet", issue: 123, assignee: "alice" })

Structured metadata like reason, issue, and assignee lets AI accurately read off who left the TODO, why, and which issue it is tied to.

TODOs become consistent in style

// TODO: comments tend to be written differently by every author.

// TODO: fix this
// TODO(alice): fix later
// TODO #123
// FIXME: has a bug

With untodo, ESLint / oxlint rules can ban comment-style TODOs and enforce the function-call form. Consistent shape also improves AI completion and suggestion quality when it reads the surrounding code.

Install

npm install untodo

Quick start

Run the bundled initializer once at the project root:

npx untodo init

This writes two files (and prints an ESLint snippet you can paste into your existing config). Existing files are skipped unless you pass --force.

wrote: ./untodo.config.ts
wrote: ./global.d.ts

1. untodo.config.ts — runtime configuration

Registers project-wide defaults via defineConfig. Per-call callbacks passed to TODO() / FIXME() / HACK() always win over what's set here, so this file is the right place to put fallback behavior.

The generated template ships with everything commented out:

import { defineConfig } from 'untodo';

export default defineConfig({
  // repo: 'org/repo',
  // onTodo:  (meta) => console.warn(`TODO: ${meta.reason}`),
  // onFixme: (meta) => console.error(`FIXME: ${meta.reason}`),
  // onHack:  (meta) => console.warn(`HACK: ${meta.reason}`),
});

| Field | Type | Purpose | |---|---|---| | repo | string | Repository in org/repo form. Tooling uses it to expand the issue meta field into a full URL (e.g. https://github.com/org/repo/issues/123). | | onTodo | (meta: TodoMeta) => void | Default handler for every TODO() call that doesn't pass its own callback. | | onFixme | (meta: FixmeMeta) => void | Default handler for every FIXME() call that doesn't pass its own callback. | | onHack | (meta: HackMeta) => void | Default handler for every HACK() call that doesn't pass its own callback. |

A typical setup that warns in development and integrates with your logger:

import { defineConfig } from 'untodo';

export default defineConfig({
  repo: 'ysknsid25/untodo',
  onTodo: (meta) => {
    const issue = meta.issue ? ` (#${meta.issue})` : '';
    console.warn(`TODO${issue}: ${meta.reason}`);
  },
  onFixme: (meta) => console.error(`FIXME: ${meta.reason}`),
  onHack:  (meta) => console.warn(`HACK: ${meta.reason}`),
});

Import this file once from your application entry point (or test setup) so defineConfig runs before any TODO() is evaluated:

import './untodo.config';

2. global.d.ts — extending the meta types

TODO(), FIXME(), and HACK() accept structured metadata. By default only reason: string is required. Add project-specific fields to the TodoMeta / FixmeMeta / HackMeta interfaces via TypeScript Declaration Merging.

The generated template:

declare module 'untodo' {
  interface TodoMeta {
    // issue?: number | string;
    // assignee?: string;
  }
  interface FixmeMeta {
    // issue?: number | string;
  }
  interface HackMeta {
    // issue?: number | string;
  }
}

export {};

Uncomment or add fields to fit your team's workflow. For example:

declare module 'untodo' {
  interface TodoMeta {
    issue?: number | string;
    assignee?: string;
    severity?: 'low' | 'medium' | 'high';
  }
  interface FixmeMeta {
    issue?: number | string;
  }
  interface HackMeta {
    issue?: number | string;
  }
}

export {};

After this, callers get full type checking and IDE autocomplete for the extra fields:

return TODO({
  reason: 'wire up the user repository',
  issue: 123,           // ← typed
  assignee: 'alice',    // ← typed
  severity: 'high',     // ← typed, only 'low' | 'medium' | 'high' allowed
});

Make sure global.d.ts is included by your tsconfig.json (most default "include" patterns already pick up **/*.d.ts).

The trailing export {}; keeps the file as a module, which is required for declare module augmentation to apply.

3. Add the ESLint plugin

init finishes by printing a flat-config snippet. Paste it into your existing eslint.config.ts (or .mjs):

import untodoPlugin from 'untodo/eslint';

export default [
  {
    plugins: { untodo: untodoPlugin },
    rules: {
      'untodo/no-todo':  'error',
      'untodo/no-fixme': 'error',
      'untodo/no-hack':  'warn',
    },
  },
];

The same plugin works with oxlint's JS plugin support.

Usage

import { TODO, FIXME, HACK } from 'untodo';

function fetchUser(): User {
  return TODO({ reason: 'wire up the user repository' });
}

function parseDate(input: string): Date {
  return FIXME({ reason: 'rejects valid ISO strings with offsets' });
}

function legacyAdapter(): Adapter {
  return HACK({ reason: 'old API ships in the next major; remove then' });
}

Because each function returns never, the surrounding code keeps type-checking as if a real value flowed through — there's no need to add as User casts or fake return values.

Per-call handlers

Pass a callback as the second argument to override the global handler for one call:

return TODO(
  { reason: 'wire up the user repository' },
  (meta) => myLogger.warn('todo.hit', meta),
);

Opting out of the global handler

Once defineConfig({ onTodo }) is set, every TODO() call without a per-call callback fires the global handler. That's usually what you want — but on a hot path, or for a deliberately quiet placeholder, you may want one specific call to skip the handler.

untodo does not bake an opt-out flag into the meta types — naming and semantics are policy, and policy belongs to your team. The recommended pattern is to add a boolean field via Declaration Merging and short-circuit inside your handler:

// global.d.ts — pick whatever name your team prefers (silent / muted / skip ...)
declare module 'untodo' {
  interface TodoMeta {
    silent?: boolean;
  }
}

// untodo.config.ts
import { defineConfig } from 'untodo';

export default defineConfig({
  onTodo: (meta) => {
    if (meta.silent) return;
    sendToSentry(meta);
  },
});
// usage
TODO({ reason: 'normal placeholder' });                  // global fires
TODO({ reason: 'tight loop', silent: true });            // global skipped
TODO({ reason: 'custom path' }, (m) => myLogger.warn(m)); // per-call wins, global skipped

The same pattern applies to FixmeMeta / HackMeta. Keeping the gate inside the handler — rather than as a library-level switch — means the predicate can be as nuanced as you need (if (meta.severity === 'low' && process.env.CI) return; etc.) without untodo having to anticipate every shape.

API

| Export | Description | |---|---| | TODO(meta, cb?) | Marks a code path as not yet implemented. Returns never. | | FIXME(meta, cb?) | Marks broken code that must be repaired. Returns never. | | HACK(meta, cb?) | Marks a deliberate workaround to revisit. Returns never. | | defineConfig(config) | Registers project-wide defaults (used in untodo.config.ts). | | untodo/eslint | ESLint / oxlint plugin exposing no-todo, no-fixme, no-hack rules. |

None of TODO / FIXME / HACK throw at runtime — they only invoke the configured handler. Lint enforcement is what makes them block the build.

Scripts

npm run build          # unbuild
npm test               # vitest run
npm run test:coverage  # vitest run --coverage
npm run lint           # eslint .
npm run lint:fix       # eslint . --fix

License

MIT