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

dawg-text

v0.5.0

Published

Frequency-ranked word autocomplete + fuzzy spell-check, backed by a compact trie/DAWG. Works in plain JS, React, and Node.

Readme

dawg-text

Frequency-ranked word autocomplete (Tab-complete) + fuzzy spell-check, backed by a single trie/DAWG-style structure. One dictionary, two traversals — exact-prefix walk for autocomplete, bounded edit-distance walk for spell-check. No separate SymSpell index needed (see reasoning below).

Ships with a production-ready ~108k-word English dictionary built in — install and use it, no separate dictionary file to fetch, build, or serve yourself.

Install

npm install dawg-text

Quick start (plain JS/TS)

import { DawgText } from 'dawg-text';

const spell = new DawgText({ editDistance: 2, maxSuggestions: 3, learningBoost: 25 });
await spell.whenReady(); // the bundled English dictionary auto-loads on construction — see "The bundled dictionary" below

spell.suggestNextWord('hel');        // ["hello", "help", "held"] — frequency ranked
spell.checkText('I recieve you mesage zzqx');
// [{ word: 'recieve', start: 2,  end: 9,  suggestions: ['receive'] },
//  { word: 'mesage',  start: 15, end: 21, suggestions: ['message', 'massage'] },
//  { word: 'zzqx',    start: 22, end: 26, suggestions: [], message: 'No edits available' }]
// suggestions is capped at `maxSuggestions` (default 3, ranked best-first); when a
// misspelled word has no close-enough match, suggestions is [] and `message` explains why.

spell.checkText('I recieve you mesag'); // 'mesag' still mid-word — flagged too, by default
spell.checkText('I recieve you mesag', true);
// [{ word: 'recieve', ... }] — last word skipped since nothing follows it yet (no
// trailing space/./,/etc). Pass `ignoreIncompleteLastWord: true` to the constructor
// instead to make this the default for every checkText() call.

spell.disable(); // turns both features off; methods return [] while disabled
spell.enable();

spell.addWord('DoubleTick', 50);   // "Add to dictionary" — word won't be flagged by checkText again
spell.clearCustomWords();          // undo every addWord() acceptance, back to just the loaded dictionary
spell.ignoreWord('lorem');         // don't flag, don't suggest
spell.learn('doubletick');         // Tab-accepted (or sent) — auto-prioritize it next time, weighted by `learningBoost`

const backup = spell.exportUserDictionary(); // persist custom words + ignore list
spell.importUserDictionary(backup);

React (recommended for web apps — runs off the main thread)

The dictionary parse + fuzzy search runs in a Web Worker via useDawgText, so it never blocks typing or rendering.

import { useDawgText } from 'dawg-text/react';

function MessageInput() {
  const spell = useDawgText({ editDistance: 2 }); // bundled English dictionary auto-loads in the worker
  const [text, setText] = useState('');
  const [suggestions, setSuggestions] = useState<string[]>([]);

  const onChange = async (value: string) => {
    setText(value);
    setSuggestions(await spell.suggestNextWord(value));
  };

  const onTab = async (e: React.KeyboardEvent) => {
    if (e.key === 'Tab' && suggestions[0]) {
      e.preventDefault();
      // replace the in-progress word with suggestions[0]
      spell.learn(suggestions[0]); // auto-prioritize it for next time (see `learningBoost`)
    }
  };

  if (!spell.isReady) return <input disabled placeholder="Loading dictionary…" />;
  return <input value={text} onChange={(e) => onChange(e.target.value)} onKeyDown={onTab} />;
}

Bundler setup for the Worker

DawgTextWorkerClient (used internally by useDawgText) constructs the worker via new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }), which Vite, webpack 5+, and modern Rollup all support natively — no extra config needed in most setups. If your bundler needs an explicit worker chunk path, pass one:

useDawgText({
  workerUrl: new URL('dawg-text/dist/worker.js', import.meta.url),
});

If you're in a non-Worker environment (SSR, Node scripts), import DawgText directly from dawg-text instead of the React hook / worker client.

The bundled dictionary

DawgText and useDawgText auto-load a dictionary on construction — controlled by the dictionary config option:

| dictionary | Behavior | |---|---| | 'full' (default) | The bundled ~108k-word English dictionary (SCOWL-derived) | | 'sample' | A tiny ~250-word demo list — fast to load, handy for tests | | false | Skip auto-load; call loadDictionary()/loadDictionaryFromUrl() yourself | | a TrieNodeJSON object | Load that dictionary directly, equivalent to calling loadDictionary() up front |

The default ('full') is what makes install-and-use work with no dictionary file to fetch, build, or serve — new DawgText() / useDawgText() starts loading it immediately; await whenReady() (plain JS/TS) or check isReady (React) before using it.

Under the hood, the full and sample dictionaries are bundled into the package as separate, dynamically-imported chunks — in ESM builds (what Vite, webpack 5+, and modern Rollup all use) bundlers code-split them out, so you only pay for the one you actually load. CJS consumers (plain require()) don't get that split — dist/index.cjs inlines both dictionaries unconditionally (~3.9MB) regardless of the dictionary option, since CommonJS doesn't support dynamic chunk loading the same way. Stick to import (the useDawgText/ESM path) if that matters for your bundle size.

Pass dictionary: false to opt out entirely — e.g. you only need a different language, or want to keep your bundle free of the English word list. You're then responsible for calling loadDictionary()/loadDictionaryFromUrl() with your own dictionary before use; isReady() stays false until you do.

Building your own dictionary

For other languages, custom vocab sizes, or domain-specific word lists, build your own dictionary.json and load it (with dictionary: false so the bundled one doesn't load too):

npx tsx node_modules/dawg-text/scripts/build-dictionary.ts words.txt public/dictionary.json

Input format: one word per line, optionally word<TAB>frequency. Without a frequency column, line order is used as rank (earlier = more frequent).

Recommended source: SCOWL (also on GitHub as kevina/wordlist) — it's the word list Hunspell/aspell are built from, purpose-built for spell-checkers, and lets you pick a vocabulary size (10/20/35/.../95) so you control size vs. coverage yourself. Pair it with a real frequency source (e.g. a Google Books ngram unigram export, or SUBTLEX) for ranking that reflects actual usage rather than dictionary order. This is in fact how the bundled dictionary was generated.

The raw JSON also ships at node_modules/dawg-text/data/dictionary.full.json (and data/dictionary.sample.json) if you want it outside the auto-load path — e.g. to serve your own copy via dictionaryUrl/loadDictionaryFromUrl, or to read it directly in Node:

import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const dictionary = JSON.parse(
  readFileSync(require.resolve('dawg-text/data/dictionary.full.json'), 'utf-8')
);
await spell.loadDictionary(dictionary);

Why one structure instead of DAWG + SymSpell

Both structures answer the same underlying question — "what word is this close to, and how frequent is it" — and at chat/CRM scale (checking one word at a time as someone types, not bulk-processing documents), the speed difference between a live bounded edit-distance walk and SymSpell's precomputed O(1) hash lookup is imperceptible to a human, while SymSpell's index costs roughly 3-15x more memory for the same coverage. If you ever profile this in production and find the live walk is genuinely a bottleneck, that's the signal to add SymSpell — not before.

API reference

DawgText

| Method | Description | |---|---| | enable() / disable() / isEnabled() | Toggle both features globally | | loadDictionary(json) / loadDictionaryFromUrl(url) | Load a built dictionary | | suggestNextWord(text, limit?) | Frequency-ranked completions for the word being typed | | checkText(text, ignoreIncompleteLastWord?) | Returns { word, start, end, suggestions, message? }[] for misspelled words — up to maxSuggestions candidates (default 3); message is 'No edits available' when suggestions is empty. Pass ignoreIncompleteLastWord (or set it in the constructor config, default false) to skip the last word while it's still being typed — i.e. nothing follows it yet | | suggestForWord(word, limit?) | "Did you mean" for a single known-bad word | | addWord(word, frequency?) / removeWord(word) | Custom vocabulary ("Add to dictionary") | | clearCustomWords() | Undo every addWord() acceptance, back to just the loaded dictionary | | ignoreWord(word) / unignoreWord(word) | Skip flagging without adding to suggestions | | learn(word) | Auto-prioritize a word after Tab-accept/real usage, weighted by the learningBoost config option (default 1) | | exportUserDictionary() / importUserDictionary(data) | Persist custom words + ignore list + learned (Tab-accepted) boosts |

useDawgText(options) (React)

Same surface as above, worker-backed and Promise-returning, plus isReady / isEnabled state for your UI.

Note on "DAWG" naming

This ships as a plain trie (one node per character prefix-sharing only). A true DAWG additionally merges identical suffixes via a minimization pass (Daciuk et al.), shrinking the structure further — useful if you're memory-constrained with a very large dictionary, but not required for correctness. Autocomplete, spell-check, and serialization all work identically either way; only in-memory/on-disk size changes. A minimization pass over Trie.toJSON()'s node array can be added later without touching the public API.

dawg-text