Catching Jupyter-specific bugs before CI does: announcing jupyter eslint plugin
We’re excited to announce the release of the first version of the Jupyter ESLint plugin @jupyter/eslint-plugin, now available on npm. It's…
Catching Jupyter-specific bugs before CI does: announcing jupyter eslint plugin

We’re excited to announce the release of the first version of the Jupyter ESLint plugin [@jupyter/eslint-plugin](https://www.npmjs.com/package/@jupyter/eslint-plugin), now available on npm. It's a custom ESLint plugin designed specifically for the official Jupyter frontends (JupyterLab, Notebook, JupyterLite) and TypeScript extensions built around them.
Motivation
If you’ve ever written a JupyterLab extension, you’ve probably hit something like this:
You wire up a new plugin, push a PR, walk away to make coffee, and 30 minutes later CI fails on a test you didn’t expect to be related. You scroll through logs, eventually trace it back, and realize the problem was a single misaligned argument in your activate function. TypeScript was happy. The editor showed no red squiggles. Nothing in your local toolchain said a word.
And the slow CI feedback loop is only half the story. Some of these mistakes don’t fail CI at all, a hardcoded English label, an enum in a settings schema, a malformed token id. They pass tests, ship to users, and only surface at runtime. They’re the kind of bugs you hear about from users months later, if at all.
JupyterLab, Jupyter Notebook, and the wider extension ecosystem follow a lot of project-specific patterns around plugins, commands, tokens, settings schemas, translations, and visual tests. These patterns aren’t enforced by TypeScript, because they live one level above the type system. They’re contracts about how the framework expects code to be written, not about whether the code type-checks.
A few examples of what generic linters and TypeScript can’t catch:
- A
JupyterFrontEndPluginthat requires[TokenA, TokenB]but whoseactivatefunction takes those arguments in the wrong order. Both are tokens, both type-check but the plugin fails at activation. - A command added with
commands.addCommand(...)whoselabelis a hardcoded English string. The extension ships, gets translated by the community, and that one button silently stays in English.
These are real, recurring issues. They cost contributor time, they frustrate first-time contributors, and they pile onto reviewer load. They’re exactly the kind of knowledge that lives in the heads of long-time maintainers and exactly the kind of knowledge that a linter is great at encoding. So we built one.
What v1 catches
The plugin currently ships eight rules, grouped by what they protect:
Plugin structure
[plugin-activation-args](https://eslint-plugin.readthedocs.io/en/latest/rules/plugin-activation-args/) — Keepsactivatefunction arguments aligned withrequiresandoptionaltokens.[plugin-description](https://eslint-plugin.readthedocs.io/en/latest/rules/plugin-description/) — Requires a non-emptydescriptionon everyJupyterFrontEndPlugin.[token-format](https://eslint-plugin.readthedocs.io/en/latest/rules/token-format/) — Enforces the<package>:<TokenSymbol>convention fornew Token(...)ids.
Commands and settings
[command-described-by](https://eslint-plugin.readthedocs.io/en/latest/rules/command-described-by/) — Requires adescribedByschema on commands that acceptargs.[no-schema-enum](https://eslint-plugin.readthedocs.io/en/latest/rules/no-schema-enum/) — Disallowsenumin settings JSON schemas in favor of translatableoneOfwithconstandtitle.
Internationalization
[no-untranslated-string](https://eslint-plugin.readthedocs.io/en/latest/rules/no-untranslated-string/) — Flags hardcoded user-facing strings in command labels, dialogs, accessibility attributes, JSX, and more.[no-translation-concatenation](https://eslint-plugin.readthedocs.io/en/latest/rules/no-translation-concatenation/) — Forbids dynamic concatenation insidetrans.__()calls that translation extractors can't pick up.
Testing
[require-soft-assertions-before-snapshots](https://eslint-plugin.readthedocs.io/en/latest/rules/require-soft-assertions-before-snapshots/) — Requiresexpect.soft(...)for all but the last snapshot in a Playwright test, so a single failure doesn't block the rest from updating.
Each rule has incorrect/correct examples, options, and rationale in the rules reference.
Getting started in 60 seconds
Install the plugin:
npm install --save-dev @jupyter/eslint-plugin
Add it to your ESLint fconfig and turn on the recommended ruleset:
javascript
import jupyterPlugin from '@jupyter/eslint-plugin';
export default [
{
plugins: {
jupyter: jupyterPlugin
}
},
jupyterPlugin.configs.recommended
];
That’s it. Run jlpm eslint and you'll start seeing Jupyter-specific issues alongside your existing lint output. If you're still on legacy .eslintrc configs, there's a [recommended-legacy ](https://eslint-plugin.readthedocs.io/en/latest/user-guide/#for-legacy-eslintrc-configs)config too. However, we strongly recommend upgrading to ESLint v9 and using the new flat config format.
Full setup details, including type-aware configuration for plugin-activation-args and the JSON parser setup for no-schema-enum, are in the user guide.
If you maintain an extension, the most useful thing you can do today is install it and run it once on your codebase. Even if you don’t adopt it permanently, the first run is often informative most extensions surface a handful of issues they didn’t know they had.
Help us encode more knowledge
Eight rules is a starting point. The plugin is most valuable when it captures the recurring mistakes that maintainers across the ecosystem keep running into and a lot of that knowledge isn’t written down anywhere.
If you’ve ever been bitten by a Jupyter-specific issue that could have been a lint rule, something where you found yourself thinking “I wish my editor had warned me about this” please open an issue on the repository. Even a short description of the bug and a minimal example helps. We’d much rather encode that knowledge into an automated check once than have every new contributor learn it the hard way.
Acknowledgement
This work was funded under the first round of Jupyter Community Funded Proposals. We want to thank the Jupyter Foundation and everyone involved in setting up this funding mechanism.
We’re also grateful to Jeremy Tuloup and Nicolas Brichet for their help in adopting the plugin.
About the developers
**Darshan Paudyal** is an intern at OpenTeams and a member of the Jupyter Frontend Council. Darshan led the development of v1 of @jupyter/eslint-plugin, designing and implementing the rule set, the documentation, and the release.
**Michał Krassowski** is a Senior Software Engineer at OpenTeams. Mike provided guidance and direction for the development of the plugin.
Useful links
메타데이터
- post_id
- fc65ae414630
- slug
- catching-jupyter-specific-bugs-before-ci-does-announcing-jupyter-eslint-plugin-fc65ae414630
- url
- https://blog.jupyter.org/catching-jupyter-specific-bugs-before-ci-does-announcing-jupyter-eslint-plugin-fc65ae414630
- canonical_url
- https://blog.jupyter.org/catching-jupyter-specific-bugs-before-ci-does-announcing-jupyter-eslint-plugin-fc65ae414630
- author_url
- https://medium.com/@pranishpoudel10
- status
- ok
- fetched_at
- 2026-06-17 16:37:43