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

@shadow-js/eslint-plugin

v1.0.4

Published

ESLint rules for reactivity, ownership and security, and for migrating code from React or Solid

Readme

@shadow-js/eslint-plugin

npm CI License: MIT Node.js TypeScript

ESLint rules for ShadowJS 1.0.0: reactivity, ownership, security, and a migration helper for code ported from React or Solid. Works with ESLint 9 and 10 (flat config) and ships eslintrc-shaped configs for ESLint 8.

Install

npm install --save-dev eslint @shadow-js/eslint-plugin

Requires Node 22.12 or later and ESLint 9 or 10 (flat config). The package has no runtime dependency on TypeScript.

Quick start

// eslint.config.js
import shadow from "@shadow-js/eslint-plugin";

export default [
  shadow.configs.recommended, // or .dusk, .strict, .migration
  { rules: { "shadow/no-unkeyed-dynamic-list": "off" } },
];

That is the whole setup: every config carries the plugin's own parser, so TypeScript, TSX and JSX files are linted with no other package. It works on TypeScript 7 and on any older TypeScript, because the parser does not use the TypeScript compiler API (see Parser).

| Config | Use it for | | --- | --- | | recommended | ShadowJS code: reactivity errors are errors, style findings are warnings. | | dusk | A Dusk project: recommended, with shadow/server-only-import-in-client pinned to the Dusk build's client/server boundary, and generated or built files (dist, routeTree.gen.ts) ignored. | | strict | Everything on: unconditional hooks, every timer and frame needs a cleanup, class only, logical direction classes. | | migration | Code being ported from React or Solid: shadow/no-react-patterns with autofixes. | | legacy-recommended, legacy-dusk, legacy-strict, legacy-migration | The same sets for eslintrc (plugins: ["@shadow-js"], parser @shadow-js/eslint-plugin/parser). legacy-dusk ignores the same files as dusk through ignorePatterns. |

Parser

@shadow-js/eslint-plugin/parser (also shadow.parser) is an ESLint parser for JavaScript, JSX, TypeScript and TSX. It parses with oxc-parser, whose syntax tree has the shape of typescript-estree; builds the tokens, comments and locations ESLint wants; and analyzes scopes (value and type scopes) with @typescript-eslint/scope-manager, which needs no TypeScript either. Nothing in the package depends on typescript or typescript-eslint, so npm install eslint @shadow-js/eslint-plugin next to TypeScript 7 resolves without --legacy-peer-deps or overrides. Core ESLint rules that read tokens or scopes work on it too.

typescript-eslint itself cannot run on TypeScript 7: it reads TypeScript through the compiler's JavaScript API, which TypeScript 7 (the native compiler) does not have, and it refuses TypeScript 6.1 and later. If you need its rules, keep TypeScript 6.0 for the linter only.

Type information

The parser builds no TypeScript program, so there is no type checker. One rule can use types when a parser provides them; its docs page marks it "uses types when available". Without types it judges by syntax and reports less, never differently:

  • signal-must-be-called finds accessors from useStore(0) cells (and useSignal), useMemo, useResource, useSelector and children declarations (and aliases) in the file; with types it also suggests calling any value typed as a zero-argument function.

Rules of other plugins that need types (@typescript-eslint/no-floating-promises, ...) stop with typescript-eslint's own error "You have used a rule which requires type information", because parserServices.program is null.

Using typescript-eslint instead

On TypeScript 6.0 or older, put typescript-eslint's parser after the config, so it wins:

import shadow from "@shadow-js/eslint-plugin";
import tseslint from "typescript-eslint";

export default [
  shadow.configs.recommended,
  {
    files: ["**/*.{ts,tsx}"],
    languageOptions: { parser: tseslint.parser, parserOptions: { projectService: true } },
  },
];

Both parsers are tested against every rule (the rule tests run twice), and the typed rule is tested with real type information.

Your own parser setup

import { parser } from "@shadow-js/eslint-plugin/parser";

export default [{ files: ["**/*.{js,jsx,ts,tsx,mjs,cjs}"], languageOptions: { parser } }];

The language comes from the file extension (.ts, .mts, .cts, .d.ts: TypeScript; .tsx: TSX; .jsx or .js with ecmaFeatures.jsx: JSX; anything else, such as stdin: TSX). sourceType is read from the config. The one thing it is not is a validator of TypeScript semantics: it reports syntax errors only.

A source nested more than 1,000 levels deep (brackets, chained operators or member accesses, elements) is a parse error. Brackets are counted before the native parser runs, and the other nesting on the tree it builds. The native parser (oxc-parser) itself can still overflow its stack on very deep JSX or conditional nesting (around 10,000 levels); that limit belongs to the parser and is outside this package. Real code nests a few dozen levels.

Changing a rule in a preset

configure(config, rules) returns a copy of a shipped config (flat or legacy-*) with rule changes applied, so a shared preset can turn a rule off, or on with options, in one place. A severity word keeps the preset's options, { severity, options } changes either or both (options are merged over the preset's), and the ESLint array form replaces the entry. It throws a TypeError for an unknown rule, a bad severity or bad options. The guide has the full rules.

import shadow, { configure } from "@shadow-js/eslint-plugin";

export default [
  configure(shadow.configs.strict, {
    "no-async-effect": "warn",
    "effect-cleanup-registered": { options: { checkTimeouts: false } },
    "no-react-patterns": { severity: "warn", options: { frameworks: ["react"] } },
  }),
];

Dusk

// eslint.config.js
import shadow from "@shadow-js/eslint-plugin";

export default [{ ignores: ["dist"] }, shadow.configs.dusk];

shadow/server-only-import-in-client follows the build's rule. Dusk replaces every server function (serverFn(...), "use server") by a stub in the browser bundle, drops the imports only that code used, and fails the build with [IMPORT_PROTECTED] for a server-only import that is left. So the rule reports exactly that and nothing else:

  • a route may import a module of server functions from src/server, and a .server import used inside a serverFn handler, its input schema or its middleware is fine;
  • a .server import used in a component, at the top level of a module, or in a loader, beforeLoad or head is reported: those run in the browser too (a loader is isomorphic);
  • Node built-ins and @shadow-js/dusk/server|node|static|testing are reported in code that reaches the browser;
  • server code is not checked: *.server.*, entry-server.*, API routes, tests, *.config.*, scripts/, and Node entry files (*.mjs/*.cjs outside src/, such as serve.mjs). Add more with the serverFiles option (regular expressions on the path inside the project).

The rule works on one file at a time. It cannot see who imports a module: a plain src/server/db.ts (no server function in it) is assumed to be server code, and a component that imports it is not reported by this rule, although the build would fail with the import chain.

Rules

| Rule | What it checks | | --- | --- | | shadow/no-destructure-props-outside-forgiving | Disallow destructuring props where the compiler does not keep them reactive (autoReactive off, or an unsupported form). | | shadow/signal-must-be-called | Require signal and memo accessors to be called when a value is needed. | | shadow/no-write-in-memo | Disallow writing signals or stores inside a useMemo computation. | | shadow/no-signal-write-in-render-body | Disallow writing external signals or stores while a component renders. | | shadow/effect-cleanup-registered | Require event listeners, intervals and subscriptions made in an effect to be cleaned up. | | shadow/no-async-effect | Disallow async functions as effect or memo callbacks. | | shadow/hooks-in-owner | Require use* hooks to be created in a component or hook, not in loops, after early returns, or later. | | shadow/prefer-show-for-early-return | Prefer <Show> over an early return guarded by a reactive condition. | | shadow/no-react-patterns | Flag React (and optionally Solid) APIs and JSX habits that ShadowJS does differently. | | shadow/jsx-no-script-url | Disallow javascript: (and similar) URLs in URL attributes. | | shadow/jsx-no-unsafe-inner-html | Disallow innerHTML (and similar HTML sinks) with values that are not literals. | | shadow/no-unsafe-spread-user-input | Disallow spreading untrusted data onto DOM elements. | | shadow/for-key-required-for-reorder | Require a key on <For> when items have an identity or the list is rebuilt. | | shadow/no-unkeyed-dynamic-list | Prefer <For> over .map() for lists rendered in JSX. | | shadow/server-only-import-in-client | Disallow importing server-only modules from code that reaches the browser. | | shadow/valid-event-names | Require JSX event attributes to name real DOM events and take functions. | | shadow/no-lowercase-dom-attrs-casing | Keep DOM attribute spelling consistent: do not mix class / className (or for / htmlFor). | | shadow/no-physical-direction-classes | Prefer logical direction classes so application layouts work in both LTR and RTL. |

Each page in docs/rules is generated from the rule's meta (npm run docs -w @shadow-js/eslint-plugin), and a test fails when a page is stale.

Porting React or Solid code

npx eslint --fix --config eslint.migrate.config.js src

with export default [shadow.configs.migration]. ESLint applies the fixes over several passes: useState becomes useSignal with reads turned into calls, dependency arrays disappear ([] becomes onMount; if the effect also writes a signal it reads, only a suggestion offers the removal, because the effect would loop), a lazy useState(() => value) initializer is unwrapped, useCallback/memo are unwrapped, className becomes class, imports move to @shadow-js/core / @shadow-js/web and the old import is removed once unused. What cannot be translated mechanically (useRef, forwardRef, useCallback without a dependency array, ...) stays reported with the alternative. Renamed and same-name hooks (useSyncExternalStore, useEffectEvent, useActionState, useLayoutEffect, useReducer, createReaction) are rewritten.

Solid's onChange on a text input fires when the value is committed; ShadowJS's onChange on a text input fires on every edit (as in React). Solid code that uses onChange on a text input or a textarea is reported by shadow/no-react-patterns (with frameworks including "solid") and renamed to onchange, the native change event. Use onInput when you want per-edit updates.

Dependencies

  • Runtime oxc-parser 0.153.0: parses files for the rules (src/parser/deps.ts).
  • Runtime @typescript-eslint/scope-manager 8.71.1: scope analysis for the rules (src/parser/deps.ts).
  • Runtime @typescript-eslint/visitor-keys 8.71.1: child-node traversal (src/parser/deps.ts).
  • Peers: eslint ^9.0.0 || ^10.0.0: the host that runs the rules (src/rule.ts).

Types

  • Names from the root: ShadowParser, ParseOptions, ParseResult, RuleSpec, ShadowRule, ShadowRuleDocs, ConfigName, LegacyConfig, RuleSetting, RuleChange, RuleOverride, RuleOverrides, Severity.

Support

  • Node.js >=22.12.0. ESM only: use import; require is not supported.
  • Browsers: not applicable; the package has no browser target.
  • Versioning: every @shadow-js/* package releases together at one version. Upgrade them as one set.
  • Documentation: ShadowJS documentation.

Using, changing and contributing

  • Use the package (a person or a coding agent that builds with it): this README, and SKILL.md, the short page an agent reads first.
  • Change the package (fix a bug, add a feature): AGENTS.md says how the repository is laid out, how to implement and test a change, and how a pull request is reviewed. It is written for people and coding agents alike.
  • Take part: CONTRIBUTING.md is the short guide; SECURITY.md is for vulnerabilities.