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

@astroscope/eslint-plugin

v2.2.0

Published

Additional ESLint rules for Astro projects (core + i18n rule sets). Plays well with eslint-plugin-astro.

Downloads

1,267

Readme

@astroscope/eslint-plugin

Additional ESLint rules for Astro projects. Plays well with eslint-plugin-astro.

Installation

npm install -D @astroscope/eslint-plugin

Setup

// eslint.config.js
import astroscope from '@astroscope/eslint-plugin';

export default [
  // ... other configs (eslint-plugin-astro, typescript-eslint, etc.)
  ...astroscope.configs.recommended,

  // opt-in for @astroscope/i18n projects
  ...astroscope.configs.i18n,

  // opt-in for @astroscope/wormhole projects
  ...astroscope.configs.wormhole,
];

Rules

| Rule | Severity | Fixable | Type-aware | Description | | ---------------------------------------------------- | -------- | ------- | ---------- | --------------------------------------------------------------------------------- | | @astroscope/no-excess-jsx-props | error | | yes | flag excess properties passed to hydrated React islands (client:* elements) | | @astroscope/island-readonly | error | | yes | require a hydrated island's props to be deeply readonly (no server-data mutation) | | @astroscope/island-not-serializable | error | | yes | require a hydrated island's props to be directly serializable plain data | | @astroscope/no-client-directive-on-astro-component | error | | | disallow client:* directives on Astro components (they only hydrate frameworks) | | @astroscope/no-html-comments | error | yes | | disallow HTML comments in .astro templates — they render into the output HTML | | @astroscope/no-server-action-calls | error | | | disallow calling actions during server rendering of .astro files | | @astroscope/prefer-ssr-guard | error | yes | | prefer import.meta.env.SSR over typeof window !== 'undefined' (and friends) | | @astroscope/no-astro-define-action | error | yes | | require the platform defineAction (with guards) instead of the astro:actions one |

Rule Details

no-excess-jsx-props

Every property passed to a hydrated (client:*) component is serialized into the page HTML. Spreading a server object wider than the declared prop type ships those extras — DB rows, session tokens, internal IDs — to every visitor.

---
const user = { name: 'x', email: 'y', passwordHash: 'secret' };
---

<!-- flagged: 'passwordHash' -->
<UserCard client:load {...user} />

<!-- clean -->
<UserCard client:load name={user.name} email={user.email} />

Also catches excess fields inside nested objects and array elements:

---
// <ArticleList> declares only { title; excerpt } on each element
const articles = [{ id: 'a1', title: 'Hello', excerpt: '…', body: '…', authorEmail: '[email protected]' }];
---

<!-- flagged: 'articles[].authorEmail', 'articles[].body', 'articles[].id' -->
<ArticleList client:load articles={articles} />

island-readonly

A hydrated island is a framework component (React/Vue/Svelte/…) rendered server-side with the original props object — often a server/cache record. If the island's component body mutates a prop, it mutates that shared server object, which can poison a server cache. The fix is structural: declare the island's props deeply readonly so the type system forbids mutation.

---
const article = await getArticle(); // a server/cache record
---

<!-- flagged: <Reader>'s props are mutable -->
<!-- declared: interface Props { article: { title: string } } -->
<Reader client:load article={article} />

<!-- clean -->
<!-- declared: interface Props { readonly article: { readonly title: string } } -->
<Reader client:load article={article} />

Pairs with react/prefer-read-only-props from eslint-plugin-react (shallow, auto-fixable): that enforces the top level on every React component; this enforces deep readonly specifically on island props.

island-not-serializable

Props passed to a hydrated island are serialized into the page HTML and rehydrated in the browser. Only plain data survives that round-trip — primitives, plain objects, and arrays. Functions, symbols, bigints, and class instances (Date, URL, RegExp, Map, …) do not. The children prop is ignored: children arrive as slots, not serialized props.

<!-- flagged: 'onSelect' (function), 'createdAt' (Date) -->
<!-- declared: interface Props { onSelect: () => void; createdAt: Date } -->
<Widget client:load onSelect={fn} createdAt={new Date()} />

<!-- clean -->
<!-- declared: interface Props { selectedId: string; createdAt: string } -->
<Widget client:load selectedId="a1" createdAt={now.toISOString()} />

no-client-directive-on-astro-component

client:* directives hydrate framework components. On an Astro component (imported from a .astro file) they do nothing — a sign the markup was meant for a framework island or the directive is dead.

---
import Card from './Card.astro';
import Counter from './Counter.tsx';
---

<!-- flagged: client:visible has no effect on an Astro component -->
<Card client:visible />

<!-- clean -->
<Counter client:visible />

no-html-comments

HTML comments (<!-- -->) in .astro templates render into the served HTML and are visible to clients. JSX-style comments ({/* */}) are stripped at compile time and never reach the browser.

<!-- flagged -->
<!-- debug: session={session} -->
<!-- clean -->
{/* debug: session={session} */}

Autofix rewrites <!-- x --> → {/* x */}. Declines to autofix when the comment body contains */ (would terminate the JSX comment early).

no-astro-define-action

defineAction from astro:actions has no place for guards, so an action defined with it is unguarded by construction and nothing in the type system says so. @astroscope/node/guards exports a defineAction that takes guards and narrows locals for the handler; every action goes through it, guarded or not. The other astro:actions exports (ActionError, getActionContext, actions) stay legal. The fix rewrites the import, splitting a mixed one.

// flagged
import { defineAction } from 'astro:actions';

// clean
import { defineAction } from '@astroscope/node/guards';
import { ActionError } from 'astro:actions';

Option source (default @astroscope/node/guards) names the module the fix imports from, for a project wrapping the platform's defineAction once more.

no-server-action-calls

Astro frontmatter and template expressions run during server rendering — a plain GET page render. Calling an action there fires the mutation on every render, outside the action endpoint's POST-only + origin-checked delivery, and couples the page to the controller. A direct actions.<name>() call also throws ActionCalledFromServerError at runtime; Astro.callAction() is Astro's escape hatch around that error and is flagged the same. When an action and astro code need the same logic, put it in its own module and import it from both.

Non-call references stay legal — passing actions.<name> to a form or to Astro.getActionResult() is the intended server-side usage, and client <script> blocks are unaffected.

---
import { actions } from 'astro:actions';

// flagged
const result = await actions.like({ id: postId });
const result2 = await Astro.callAction(actions.like, { id: postId });

// clean — read the submitted result
const submitted = Astro.getActionResult(actions.like);
---

<!-- clean — the intended usage -->
<form method="POST" action={actions.like}></form>

prefer-ssr-guard

Unlike typeof window !== 'undefined', import.meta.env.SSR lets the bundler tree-shake the browser-only code out of the SSR build.

// flagged
if (typeof window !== 'undefined') {
  const { default: HLS } = await import('hls.js'); // shipped to SSR bundle
  player.attach(HLS);
}

// clean — `import('hls.js')` never appears in the SSR bundle
if (!import.meta.env.SSR) {
  const { default: HLS } = await import('hls.js');
  player.attach(HLS);
}

i18n Rules (configs.i18n)

Rules for projects using @astroscope/i18n, opt-in alongside recommended. They keep their own plugin namespace, so rule ids read @astroscope/i18n/....

| Rule | Severity | Fixable | Description | | ---------------------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------- | | @astroscope/i18n/t-import-source | error | | t must be imported from @astroscope/i18n/translate | | @astroscope/i18n/no-module-level-t | error | | t() must not be called at module level (needs request context on server, hydrated translations on client) | | @astroscope/i18n/t-static-key | error | | first argument must be a static string literal (dynamic keys break build-time extraction) | | @astroscope/i18n/t-static-meta | warn | | second argument must be statically analyzable: no concatenation, interpolation or tagged templates (extraction reads it at build time) | | @astroscope/i18n/t-requires-meta | warn | | second argument (fallback/meta) should be provided for development DX | | @astroscope/i18n/no-t-reassign | error | | forbids aliasing or reassigning t (the extractor only recognizes t() calls) | | @astroscope/i18n/no-raw-strings-in-jsx | warn | | warns when raw strings appear in JSX that may need translation |

i18n Rule Details

t-import-source

Ensures t is only imported from the correct @astroscope/i18n entrypoints.

// good
import { t } from '@astroscope/i18n/translate';

// bad
import { t } from 'i18next';
import { t } from './my-translate';

no-module-level-t

Forbids calling t() at module scope. On the server, t() reads from AsyncLocalStorage (request context). On the client, it reads from window.__i18n__. Neither is available during module evaluation.

// good
function render() {
  return t('key', 'fallback');
}

// bad
const title = t('key', 'fallback');

t-static-key

The first argument must be a string literal. Dynamic keys cannot be extracted at build time by the Babel plugin.

// good
t('checkout.title', 'Checkout');

// bad
t(key, 'fallback');
t('prefix.' + suffix, 'fallback');
t(`prefix.${suffix}`, 'fallback');

t-requires-meta

The second argument (fallback string or meta object) provides the fallback text shown during development when translations are missing.

// good
t('key', 'Hello World');
t('key', { fallback: 'Hello World', description: 'Greeting' });

// bad (no fallback — shows raw key in dev)
t('key');

no-t-reassign

The build-time extractor only recognizes t() calls by name. Aliasing or reassigning breaks extraction.

// good
import { t } from '@astroscope/i18n/translate';

// bad
import { t as translate } from '@astroscope/i18n/translate';
const translate = t;

no-raw-strings-in-jsx

Warns when JSX contains raw string literals that may need translation. Ignores whitespace, numbers, and common non-translatable attributes (className, href, type, etc.).

// warns
<div>Hello World</div>
<button>Submit</button>

<button aria-label="Close dialog" />

// no warning
<div className="container" />
<div aria-modal="true" />
<div>{t('greeting', 'Hello World')}</div>

Options

'@astroscope/i18n/no-raw-strings-in-jsx': ['warn', {
  // additional regex patterns to ignore (applied to text content)
  ignorePatterns: ['^TODO'],
  // additional attribute names to ignore
  ignoreAttributes: ['data-tooltip'],
}]

The default ignore list is exported as DEFAULT_IGNORE_ATTRIBUTES for consumers who want to extend it:

import astroscope, { DEFAULT_IGNORE_ATTRIBUTES } from '@astroscope/eslint-plugin';

// ...
'@astroscope/i18n/no-raw-strings-in-jsx': ['warn', {
  ignoreAttributes: [...DEFAULT_IGNORE_ATTRIBUTES, 'alt', 'data-tooltip'],
}]

Wormhole Rules (configs.wormhole)

Rules for projects using @astroscope/wormhole, opt-in alongside recommended. They keep their own plugin namespace, so rule ids read @astroscope/wormhole/....

| Rule | Severity | Fixable | Description | | ----------------------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | | @astroscope/wormhole/wormholes-static-access | warn | | accesses on the wormholes proxy must be static — dynamic keys and aliasing defeat build-time payload slicing | | @astroscope/wormhole/server-readonly | error | | set() / subscribe() are client-only; astro files are server code where values are request-scoped | | @astroscope/wormhole/no-use-wormhole-in-astro | error | | the useWormhole react hook cannot run in astro server code — use wormholes.<name>.get() | | @astroscope/wormhole/no-registry-import | error | | the wormhole registry (src/wormholes.ts) is server-only — island code reads through the wormholes proxy |

Wormhole Rule Details

wormholes-static-access

The build-time scanner resolves wormholes.<name> accesses to decide which wormholes each chunk needs. Anything it cannot resolve degrades the chunk to "all open wormholes" — sound, but it defeats slicing. Astro files are exempt (server-rendered, never in the client chunk graph).

// good
wormholes.cart.get();
wormholes['session'].subscribe(fn);

// bad
wormholes[name].get();
const w = wormholes;
const { cart } = wormholes;

server-readonly

Astro frontmatter and template expressions run on the server, where wormhole values are request-scoped and read-only: set() throws at runtime and subscribe() never fires. Client <script> blocks are unaffected.

---
const cart = wormholes.cart.get(); // good

wormholes.cart.set({ items: [] }); // bad
---

no-use-wormhole-in-astro

useWormhole is a React hook — there is no React runtime in astro server code.

---
// bad
import { useWormhole } from '@astroscope/wormhole/react';

// good: read directly
import { wormholes } from '@astroscope/wormhole';

const cart = wormholes.cart.get();
---

no-registry-import

The registry carries the handlers, and with them server code — a client build that includes it fails. Island code (.tsx/.jsx) reads through the proxy; type imports of the registry module are fine.

// bad
import { wormholes } from '@/wormholes';

// good
import { wormholes } from '@astroscope/wormhole';
import type { Cart } from '@/wormholes';

Compatibility

  • ESLint 9 and 10
  • Works alongside eslint-plugin-astro (order-independent), or standalone

License

MIT