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

splitcode

v0.3.0

Published

SplitCode — split large JS, TS, HTML or Python files into smaller dependency-ordered files with pure static analysis. No bundler, drop-in loader.

Readme

SplitCode — Split Large JS, TS, HTML or Python Files Into Dependency-Ordered Pieces

npm (coming soon) license: MIT

Split one giant app.js / app.ts / page / app.py into clean, load-ordered modules using pure static analysis — no bundler, no LLM, no config. SplitCode parses your code, finds real dependencies between top-level statements, groups coupled code together, and emits a drop-in bootstrap loader, so existing entry points keep working unchanged.

Keywords: javascript splitter, split js file, typescript splitter, split python file, refactor monolithic script, js code splitting without bundler, legacy code modularization, script dependency ordering.

Why SplitCode?

  • 📦 Break up monolithic scripts — turn a giant app.js into focused, reviewable files grouped by what actually depends on what.
  • 🔗 Correct load order, computed — references that run immediately enforce ordering; deferred callbacks don't (so a click handler won't manufacture false load-order cycles).
  • 🚀 Zero-integration loader — the generated app.js bootstrap pulls in every split file in order. Deploy the folder; change nothing else.
  • 🔍 Honest output — manifest.json reports hubs, duplicates, cycles, and parser mode, so you see exactly how cleanly your file decomposed.

Quick start

npm release pending — npx/-g commands below work once splitcode is published. Until then, use the from-source form (identical behavior).

# once published (no install needed):
npx splitcode app.js ./split-out

# or install globally:
npm install -g splitcode
splitcode app.js ./split-out

From source today (same thing — splitcode is just the bin alias for split-js.js):

npm install   # acorn + node-html-parser (+ optional typescript@5 for .ts)
node split-js.js app.js ./split-out

Deploy ./split-out and keep loading app.js from your pages — the generated loader pulls in the rest in dependency order. That's the whole migration.

For AI agents

Paste this into your project's CLAUDE.md / AGENTS.md so agents split before they read:

For files over ~100KB: run npx splitcode <file> ./split-out first, plan from ./split-out/manifest.json, edit per-file, never reorder order, test after every change.

A ready-made Claude Skill lives at skills/splitcode/SKILL.md — copy it into your project's .claude/skills/ (or global skills dir) and agents will invoke it automatically when files get large. Agent-readable docs: docs/llms.txt (also served at /llms.txt on the site).

How it works

  1. Parse the file with acorn into an AST (classic scripts; ES modules via fallback).
  2. Collect declarations — function/class/var/let/const, import bindings, plus global writes (foo = …, window.foo = …, Object.assign(window, {…})).
  3. Walk each statement with a scope-aware free-variable collector that distinguishes immediate references (run the instant the statement runs) from deferred ones (callbacks that fire later). Immediate includes IIFEs, fn.call/fn.apply, Promise executors, and a spec-backed allowlist (map/forEach/filter/every/some/find*/ reduce*/flatMap/sort, str.replace(re, fn), Array.from(it, fn), JSON.stringify(v, fn)). Block scoping (if/for/switch/bare {}) is respected; var still hoists; with bodies count every ref as live; window.foo reads link back to window.foo = … writes.
  4. Cluster tightly-coupled statements: connected-components first, then Louvain modularity refinement to cut weak bridges in oversized clusters. Over-shared "hub" globals (used everywhere) are excluded from grouping so they don't weld the file into one blob.
  5. Topologically order the files on immediate dependencies only. Function declarations are hoisted, so they move freely; anything that executes immediately (a bare call, const x = f()) keeps its relative position. Grouping ignores edge direction, so a cluster assignment can theoretically demand an impossible order (A before B before A) — the tool condenses such circular groups (Tarjan SCC) into single files first, so the emitted order is always satisfiable. Merges are logged and recorded (sccMerged); files get bigger, never wrong.
  6. Write the outputs (see below): cluster files, manifest.json, script-tags.html, and the app.js bootstrap loader.

Usage

splitcode <input.(js|ts|html|py)> <outDir> [--hub-ratio 0.12] [--min-chars 400] [--loader <name> | --no-loader] [--lang js|ts|html|py]
# from source: node split-js.js <same args>

| Option | Default | Meaning | |---|---|---| | --hub-ratio | 0.12 | Names referenced by more than this fraction of statements are treated as shared app state, not a clustering signal. Floor: names used >6 times are always hubs; --no-hubs disables. | | --min-chars | 400 | Clusters smaller than this merge into their neighbour, avoiding a pile of one-line files. Strictly validated (non-negative integer). | | --loader app.js | on | Writes a bootstrap loader named app.js into outDir. Keep loading just that ONE file — it pulls in the split files in order. Load it with a plain <script src>, not async/defer. Rename with --loader bootstrap.js; a cluster that would collide gets suffixed (app-2.js). | | --no-loader | — | Disables the loader; paste script-tags.html into your page instead (JS/TS/HTML; Python has no tags file). | | --loader-mode classic\|inline | classic | classic: loader pulls in parts at runtime via document.write (ESM inputs get type="module" tags). inline: loader is self-contained — all parts concatenated in order, no runtime injection, no extra requests. | | --no-louvain | — | Skip Louvain refinement; group by connected components only (faster, more predictable). | | --no-hubs | — | Disable hub suppression entirely (--hub-ratio 0 can NOT do this — the >6 floor makes 0 the most aggressive setting). | | --lang js\|ts\|html\|py | auto (extension) | Force the frontend for extension-less or oddly-named inputs. | | --check | — | Preflight only: scan for risk patterns, print warnings, write nothing. Exit 0 = clean, 2 = risky. outDir not needed. | | --strict | — | Refuse to write when preflight warns (exit 2 instead of writing risky output). | | --force | — | Allow overwriting colliding files and outputting into the input's own directory. The input file itself is still never deleted. | | --dry-run | — | Plan everything, write nothing. outDir optional. | | --max-bytes | 33554432 | Refuse inputs larger than this many bytes instead of risking OOM (0 = unlimited). | | --timing | — | Print per-phase milliseconds at the end. | | --quiet | — | Suppress info logs (warnings, errors and the final Wrote line still print). | | --help / --version | — | Print help / version, exit 0. |

Exit codes: 0 = success (warnings may be present unless --strict), 1 = error, 2 = risky (warnings under --check or --strict). Flags may appear before or after outDir. The output can replace the original file: keep loading just the loader and the host app behaves identically (verified syntax-only — smoke-test split apps before shipping).

Preflight (automatic, per file type)

Every run scans for constructs the splitter handles poorly and warns before writing anything. --check runs only the scan:

| Lang | ⚠ Warns | • Notes | |---|---|---| | JS | eval, indirect X.eval, new Function, setTimeout("…") strings, dynamic import(), importScripts (bare + X. member form), X.prototype.y =, Object.assign(X.prototype, …), dynamic global keys (window[x] =, non-literal Object.assign(window, …)), Object.defineProperty, unparseable file | getters/setters, top-level await, require(…), bare writes to undeclared names (e.g. loop inits) | | TS | eval, indirect X.eval, new Function, setTimeout("…") strings, dynamic import(), importScripts (bare + member form), X.prototype.y =, Object.assign(X.prototype, …), Object.defineProperty (via compiler API) | getters/setters, top-level await, require(…) | | HTML | <script> inside <template> (would be ACTIVATED), | type="module" left in place | | Python | eval/exec strings, __import__, relative imports (BREAK in parts), from __future__ (must stay first) | import *, __file__ (points at bootstrap) |

Languages

Dispatch is by file extension (override with --lang js|ts|html|py). One shared backend clusters, orders and names — each language gets a frontend plus its own loader.

| Input | Frontend | Output | Loader | |---|---|---|---| | .js / .mjs / .cjs | acorn AST, full scope analysis | .js parts | app.js — synchronous document.write bootstrap, keep loading just it | | .ts / .tsx | typescript@5 API (v6+/native port has no JS AST API — pinned ^5.9) | .ts parts (types kept) | same mechanism, .ts file | | .html | pools every inline classic <script>; external src untouched | rewritten page + .js parts | loader tag inserted at the first inline block's position | | .py | python3 stdlib ast (requires python3 on PATH) | .py parts | app.py bootstrap — execs parts in order in shared globals, so module names behave exactly as one file |

TypeScript specifics: type-position refs (: Foo, implements Bar) cluster but never order (erased at runtime); decorators/enum initializers/namespace bodies/static blocks are immediate; field initializers are deferred (construction time); <Foo /> tags count as refs. HTML specifics: type="module" / non-JS blocks (ld+json) and unparseable blocks are left in place (warned); an external src script between inline blocks can't be ordered against the pool (warned as externalInterleave). Python specifics: def-time evaluations (decorators, defaults, annotations, bases) are immediate; class bodies run at creation; methods can't see class-scope names (real Python scoping). __name__ == "__main__" blocks run exactly as before; caveat: __file__ inside a part points at the bootstrap. Behavioral check: original vs split stdout diffed — identical (modulo independent-print interleaving, see limitations).

Outputs (outDir)

  • Cluster files — named after their most-used declaration in kebab-case (auth-token.js), section-N.js fallback. Each carries a // Declares: … header comment.

  • Loader (app.js) — resolves its own directory and injects the split files synchronously via document.write during parsing (the only single-file mechanism with <script>-tag semantics). Caveat: Chrome may block document.write-injected scripts on very slow (2G) connections — use script-tags.html then.

  • manifest.json — machine-readable result:

    | Field | Meaning | |---|---| | order | Files with declares + statementCount, in load order | | loader | Entry-point file name (null with --no-loader) | | loaderMode | classic (runtime injection) or inline (self-contained) | | strict | Whether --strict was on for this run | | tool / toolVersion / schemaVersion | Producer identity (splitcode, semver, manifest schema 1) | | input | {file, bytes, statements, sha256} of the split source | | output | {statements, bytes} across parts (statements must equal input) | | hubNamesSuppressed | Shared-everywhere globals excluded from grouping | | hubThreshold | Effective use-count above which a name is a hub | | cycleFallback | Safety net only (condensation makes it unreachable); whether original-order fallback was used | | sccMerged | Circular-dependency groups merged into single files to guarantee order | | parserMode | script, or module if the ESM fallback parsed it | | duplicateDeclarations | Repeated top-level names (callers link to every same-name declaration) | | verified | Always "syntax-only" — what was (and wasn't) proven |

  • script-tags.html — paste-in alternative to the loader.

Hygiene (automatic)

  • Stale cleanup — reruns delete previous tool output (files listed in the prior manifest.json, plus the exact names about to be written — never extension globs) from outDir first, so orphaned files from a different cluster count can't linger. Unchanged files are hash-skipped, not rewritten. Foreign files are never touched; collisions refuse unless --force. The input file itself is never deleted or overwritten (same-dir output with a colliding loader name refuses — use a separate outDir).
  • CLI validation — bad --hub-ratio/--min-chars/--max-bytes values and unknown flags exit with an error instead of silently becoming NaN. Safety refusals (input overwrite, foreign-file collision) exit 1; --strict / --check report risk with exit 2.
  • Duplicate declarations — legal var/function redeclarations warn on console and in the manifest; callers link to every same-name declaration so load order stays safe (files get more coupled, never misordered).

Proven on a real 460KB app (671 top-level statements)

  • 671 statements in → 671 out. Nothing lost or duplicated (input checksum verified unchanged after the run).
  • Reassembled output passes node --check (syntax only — smoke-test split pages for behavior).
  • 18 files, largest 95 statements; no load-order cycles; zero ordering violations; 1 hub suppressed (toast); 1 duplicate declaration reported (showTabEditor — callers link to every same-name declaration).

Honest limitations

  • Side-effect order across files. Order is enforced only along dependency edges. Two top-level statements with side effects (prints, DOM writes) but no shared names may run in a different relative order after splitting — demonstrated by test: independent print lines swapped files. If exact interleaving matters, keep those statements coupled (shared name) or in one file.
  • An unknown receiver's callback defaults to deferred (arr.map(fn) is covered; a custom runNow(fn) is not) — the general case is undecidable by syntax analysis. Keep synchronously-coupled code together or verify order.
  • Dynamic global keys (window[x] = …) can't be tracked statically — detected and warned as dynamic-global-key, but readers stay unlinked.
  • Heavy shared mutable state genuinely merges files — the tool can't invent boundaries that don't exist. Check hubNamesSuppressed and cluster sizes.
  • manifest.json carries "verified": "syntax-only" as a reminder of what was (and wasn't) proven.

Requirements

  • Node.js ≥ 16.
  • JS/HTML: acorn + node-html-parser (npm install).
  • TS: typescript@5 (optional dependency — installed by default, skippable with npm install --omit=optional; missing install fails with guidance).
  • Python: python3 on PATH (stdlib ast only — no pip packages).

License

MIT — do what you want, no warranty. Static analysis can miss dynamic edges; smoke-test split pages before shipping.

Author

unn-Known1 — [email protected] (github.com/unn-Known1)