<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Darshan Kr. Paudyal · Jupyter Blog</title>
    <link>https://jupyter.org/medium-archive/hugo/authors/darshan-kr-paudyal/</link>
    <description>The Project Jupyter blog: news, releases, and community stories, archived from blog.jupyter.org.</description>
    <generator>Hugo</generator>
    <lastBuildDate>Fri, 19 Jun 2026 13:11:00 &#43;0000</lastBuildDate>
    <atom:link href="https://jupyter.org/medium-archive/hugo/authors/darshan-kr-paudyal/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Announcing jupyter-builder: A Standalone Build System for JupyterLab Extensions</title>
      <link>https://jupyter.org/medium-archive/hugo/posts/2026/announcing-jupyter-builder-a-standalone-build-system-for-jupyterlab-extensions/</link>
      <pubDate>Fri, 19 Jun 2026 13:11:00 &#43;0000</pubDate>
      <dc:creator>Darshan Kr. Paudyal</dc:creator>
      <guid>https://jupyter.org/medium-archive/hugo/posts/2026/announcing-jupyter-builder-a-standalone-build-system-for-jupyterlab-extensions/</guid>
      <description>We’re excited to announce the first stable release of jupyter-builder on PyPI and @jupyter/builder on npm. A standalone, configurable build…</description>
      <content:encoded>&lt;p&gt;&lt;img src=&#34;https://jupyter.org/medium-archive/hugo/medium-archive/hugo/posts/2026/announcing-jupyter-builder-a-standalone-build-system-for-jupyterlab-extensions/images/001-1_3UBtK-sh01N6OXIbGFNrpQ.webp&#34; width=&#34;1400&#34; height=&#34;788&#34; fetchpriority=&#34;high&#34; alt=&#34;Jupyter Builder logo&#34;&gt;&lt;/p&gt;
&lt;p&gt;We’re excited to announce the first stable release of &lt;a href=&#34;https://pypi.org/project/jupyter-builder/&#34;&gt;&lt;code&gt;jupyter-builder&lt;/code&gt;&lt;/a&gt; on PyPI and &lt;a href=&#34;https://www.npmjs.com/package/@jupyter/builder&#34;&gt;&lt;code&gt;@jupyter/builder&lt;/code&gt;&lt;/a&gt; on npm. A standalone, configurable build system for JupyterLab and its extensions.&lt;/p&gt;
&lt;p&gt;If you build or maintain a JupyterLab extension, this release is for you.&lt;/p&gt;
&lt;h2 id=&#34;motivation&#34;&gt;Motivation&lt;/h2&gt;
&lt;p&gt;For a long time, building a JupyterLab extension has meant installing all of JupyterLab. Take a look at any extension’s build dependencies and you’ll see &lt;code&gt;jupyterlab&lt;/code&gt; listed there, not because the extension needs JupyterLab at runtime to be built, but because the build tooling lives &lt;em&gt;inside&lt;/em&gt; the JupyterLab repository. The &lt;code&gt;builder/&lt;/code&gt; folder, the &lt;code&gt;jlpm&lt;/code&gt; command, the helper scripts, they&amp;rsquo;re all coupled to the JupyterLab core.&lt;/p&gt;
&lt;p&gt;This setup has caused real friction over the years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux packagers&lt;/strong&gt; have repeatedly raised concerns about the circular dependency where extensions like &lt;code&gt;jupyterlab_pygments&lt;/code&gt; need &lt;code&gt;jupyterlab&lt;/code&gt; to build, but &lt;code&gt;jupyterlab&lt;/code&gt; ships with those extensions. It&amp;rsquo;s a chicken-and-egg problem that makes packaging painful.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extension authors&lt;/strong&gt; have to update their build system every time they update to a new JupyterLab version, which sometimes introduces breakages that have nothing to do with their extension’s actual code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Users&lt;/strong&gt; still occasionally run into installation issues that trace back to the incomplete migration from “source” extensions to prebuilt ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The need to separate the build system was identified back in &lt;a href=&#34;https://github.com/jupyterlab/jupyterlab/issues/13456&#34;&gt;jupyterlab#13456&lt;/a&gt;, and initial work began during GSoC 2024. With the support of the Jupyter Foundation’s first community-funded proposal, we’ve now brought that work to a stable release.&lt;/p&gt;
&lt;h2 id=&#34;what-jupyter-builder-does&#34;&gt;What &lt;code&gt;jupyter-builder&lt;/code&gt; does&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;jupyter-builder&lt;/code&gt; extracts all the Node.js-based build tooling out of JupyterLab into a dedicated package. You install it on its own, and it builds your extension, no full JupyterLab installation required.&lt;/p&gt;
&lt;p&gt;It ships in two parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://pypi.org/project/jupyter-builder/&#34;&gt;&lt;strong&gt;&lt;code&gt;jupyter-builder&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; (PyPI) — the Python package that provides the &lt;code&gt;jupyter-builder&lt;/code&gt; CLI.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.npmjs.com/package/@jupyter/builder&#34;&gt;&lt;strong&gt;&lt;code&gt;@jupyter/builder&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; (npm) — the underlying JavaScript package containing the build scripts. The Python CLI invokes &lt;code&gt;build-labextension.js&lt;/code&gt; from this package, which then runs the &lt;a href=&#34;https://rspack.dev/&#34;&gt;rspack&lt;/a&gt; compiler with all the configuration needed to produce a JupyterLab extension.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of &lt;code&gt;jupyter labextension build .&lt;/code&gt;, you now run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;jupyter-builder build .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;build-compatibility-without-jupyterlab-installed&#34;&gt;Build compatibility without JupyterLab installed&lt;/h2&gt;
&lt;p&gt;One interesting problem we had to solve: if JupyterLab isn’t installed, how does the builder verify that an extension is compatible with a given JupyterLab version?&lt;/p&gt;
&lt;p&gt;The answer is &lt;a href=&#34;https://www.npmjs.com/package/@jupyterlab/core-meta&#34;&gt;&lt;code&gt;@jupyterlab/core-meta&lt;/code&gt;&lt;/a&gt;, a small npm package that publishes JupyterLab&amp;rsquo;s core metadata on its own. &lt;code&gt;@jupyter/builder&lt;/code&gt; declares a dependency on a specific &lt;code&gt;@jupyterlab/core-meta&lt;/code&gt; version, so by default your extension is checked for compatibility against whatever JupyterLab version that pin corresponds to. In general, the latest &lt;code&gt;@jupyter/builder&lt;/code&gt; will track the latest JupyterLab.&lt;/p&gt;
&lt;p&gt;If you want to build against a different JupyterLab version, pass &lt;code&gt;--core-version&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;jupyter-builder build . --core-version 4.4.x
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We’ll download that version’s metadata from &lt;code&gt;@jupyterlab/core-meta&lt;/code&gt; on npm and check compatibility against it.&lt;/p&gt;
&lt;h2 id=&#34;migrating-an-existing-extension&#34;&gt;Migrating an existing extension&lt;/h2&gt;
&lt;p&gt;If you’re starting a new extension with the &lt;a href=&#34;https://github.com/jupyterlab/extension-template&#34;&gt;extension template&lt;/a&gt;, you’ll get all of this set up automatically. For existing extensions, there are two changes to make.&lt;/p&gt;
&lt;h2 id=&#34;1-update-the-python-build-dependencies&#34;&gt;1. Update the Python build dependencies&lt;/h2&gt;
&lt;p&gt;In your &lt;code&gt;pyproject.toml&lt;/code&gt;, swap &lt;code&gt;jupyterlab&lt;/code&gt; for &lt;code&gt;jupyter-builder&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-ini&#34; data-lang=&#34;ini&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Before&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;requires&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;[&amp;#34;hatchling&amp;gt;=1.5.0&amp;#34;, &amp;#34;jupyterlab&amp;gt;=4.0.0,&amp;lt;5&amp;#34;, &amp;#34;hatch-nodejs-version&amp;gt;=0.3.2&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-ini&#34; data-lang=&#34;ini&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# After&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;requires&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;[&amp;#34;hatchling&amp;gt;=1.5.0&amp;#34;, &amp;#34;jupyter-builder&amp;gt;=1.0.0&amp;#34;, &amp;#34;hatch-nodejs-version&amp;gt;=0.3.2&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then update the &lt;code&gt;scripts&lt;/code&gt; section of your &lt;code&gt;package.json&lt;/code&gt; to use the new CLI:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;build:labextension&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;jupyter-builder build .&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;build:labextension:dev&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;jupyter-builder build --development True .&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;watch:labextension&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;jupyter-builder watch .&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your build no longer pulls in all of JupyterLab.&lt;/p&gt;
&lt;h2 id=&#34;2-update-the-npm-builder-dependency&#34;&gt;2. Update the npm builder dependency&lt;/h2&gt;
&lt;p&gt;Replace &lt;a href=&#34;https://www.npmjs.com/package/@jupyterlab/builder&#34;&gt;&lt;code&gt;@jupyterlab/builder&lt;/code&gt;&lt;/a&gt; with &lt;code&gt;@jupyter/builder&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;@jupyter/builder&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;^1.0.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;jupyter-builder&lt;/code&gt; is backwards-compatible with &lt;a href=&#34;https://www.npmjs.com/package/@jupyterlab/builder&#34;&gt;&lt;code&gt;@jupyterlab/builder&lt;/code&gt;&lt;/a&gt;, so things will keep working if you don&amp;rsquo;t migrate immediately. That said, we strongly recommend switching: &lt;a href=&#34;https://www.npmjs.com/package/@jupyterlab/builder&#34;&gt;&lt;code&gt;@jupyterlab/builder&lt;/code&gt;&lt;/a&gt; is no longer being actively developed, and all new work is happening in &lt;a href=&#34;https://www.npmjs.com/package/@jupyter/builder&#34;&gt;&lt;code&gt;@jupyter/builder&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Note that as part of this transition JupyterLab has also moved from Webpack to &lt;a href=&#34;https://rspack.rs/&#34;&gt;Rspack&lt;/a&gt;. Most extensions won’t need any changes, but if yours uses a custom &lt;code&gt;webpackConfig&lt;/code&gt; see the &lt;a href=&#34;https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#building-extensions-with-rspack&#34;&gt;Rspack migration notes&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What’s next&lt;/h2&gt;
&lt;p&gt;This release is the first stable foundation. We’re continuing to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migrate more packages across the &lt;a href=&#34;https://github.com/jupyterlab/&#34;&gt;Jupyter organization&lt;/a&gt; to use &lt;code&gt;jupyter-builder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make the builder more configurable, so it can eventually be used for projects beyond JupyterLab.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you maintain an extension, please try the migration and &lt;a href=&#34;https://github.com/jupyterlab/jupyter-builder/issues&#34;&gt;let us know how it goes&lt;/a&gt;. Feedback from real extensions is the best way to make sure the migration path stays smooth.&lt;/p&gt;
&lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;This work was funded under the &lt;a href=&#34;https://jupyter.org/medium-archive/hugo/posts/2025/announcing-our-first-jupyter-community-funded-proposals/&#34;&gt;first round of Jupyter Community Funded Proposals&lt;/a&gt;. We want to thank the &lt;a href=&#34;https://jupyterfoundation.org/&#34;&gt;Jupyter Foundation&lt;/a&gt; and everyone involved in setting up this funding mechanism.&lt;/p&gt;
&lt;p&gt;A huge thank you to &lt;a href=&#34;https://github.com/bollwyvl&#34;&gt;Nicholas Bollweg&lt;/a&gt; for many thoughtful discussions on the design and packaging of &lt;code&gt;jupyter-builder&lt;/code&gt;, and to &lt;a href=&#34;https://github.com/jtpio&#34;&gt;Jeremy Tuloup&lt;/a&gt; for helping us resolve issues and for guiding the adoption of &lt;code&gt;jupyter-builder&lt;/code&gt; in Jupyter Notebook.&lt;/p&gt;
&lt;p&gt;This package was initially created during &lt;a href=&#34;https://summerofcode.withgoogle.com/archive/2024/projects/YMB3Rc53&#34;&gt;GSoC 2024&lt;/a&gt; by &lt;a href=&#34;https://www.linkedin.com/in/ronan-coutinho/&#34;&gt;Ronan Coutinho&lt;/a&gt;, mentored by &lt;a href=&#34;https://www.linkedin.com/in/fcollonval/&#34;&gt;Frédéric Collonval&lt;/a&gt;. Their work laid the foundation that made this release possible.&lt;/p&gt;
&lt;h2 id=&#34;about-the-developers&#34;&gt;About the developers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://www.linkedin.com/in/darshan-poudel8/&#34;&gt;&lt;strong&gt;Darshan Paudyal&lt;/strong&gt;&lt;/a&gt; is an intern at &lt;a href=&#34;https://openteams.com/&#34;&gt;OpenTeams&lt;/a&gt; and a member of the &lt;a href=&#34;https://jupyterlab-team-compass.readthedocs.io/en/latest/team.html&#34;&gt;Jupyter Frontend Council&lt;/a&gt;. Darshan led the work to bring &lt;code&gt;jupyter-builder&lt;/code&gt; to a stable release, completing the separation from JupyterLab core and shepherding the package through to its first published version.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.linkedin.com/in/michal-krassowski/&#34;&gt;&lt;strong&gt;Michał Krassowski&lt;/strong&gt;&lt;/a&gt; is a Senior Software Engineer at &lt;a href=&#34;https://openteams.com/&#34;&gt;OpenTeams&lt;/a&gt;. Mike provided guidance and technical direction throughout the development of &lt;code&gt;jupyter-builder&lt;/code&gt;, helping shape its architecture and roadmap.&lt;/p&gt;
&lt;h2 id=&#34;useful-links&#34;&gt;Useful links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;📦 PyPI: &lt;a href=&#34;https://pypi.org/project/jupyter-builder/&#34;&gt;&lt;code&gt;jupyter-builder&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📦 npm: &lt;a href=&#34;https://www.npmjs.com/package/@jupyter/builder&#34;&gt;&lt;code&gt;@jupyter/builder&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💻 Source: &lt;a href=&#34;https://github.com/jupyterlab/jupyter-builder&#34;&gt;github.com/jupyterlab/jupyter-builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📖 Migration guide: &lt;a href=&#34;https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#migrating-to-jupyter-builder&#34;&gt;&lt;code&gt;jupyter-builder&lt;/code&gt; migration section&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💬 Issues &amp;amp; feedback: &lt;a href=&#34;https://github.com/jupyterlab/jupyter-builder/issues&#34;&gt;GitHub issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Catching Jupyter-specific bugs before CI does: announcing jupyter eslint plugin</title>
      <link>https://jupyter.org/medium-archive/hugo/posts/2026/catching-jupyter-specific-bugs-before-ci-does-announcing-jupyter-eslint-plugin/</link>
      <pubDate>Thu, 11 Jun 2026 15:13:32 &#43;0000</pubDate>
      <dc:creator>Darshan Kr. Paudyal</dc:creator>
      <guid>https://jupyter.org/medium-archive/hugo/posts/2026/catching-jupyter-specific-bugs-before-ci-does-announcing-jupyter-eslint-plugin/</guid>
      <description>We’re excited to announce the release of the first version of the Jupyter ESLint plugin @jupyter/eslint-plugin, now available on npm. It&#39;s…</description>
      <content:encoded>&lt;p&gt;&lt;img src=&#34;https://jupyter.org/medium-archive/hugo/medium-archive/hugo/posts/2026/catching-jupyter-specific-bugs-before-ci-does-announcing-jupyter-eslint-plugin/images/001-1_CXmGuOa5O6Z60l4KZSEo3g.webp&#34; width=&#34;1500&#34; height=&#34;1000&#34; fetchpriority=&#34;high&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;We’re excited to announce the release of the first version of the Jupyter ESLint plugin &lt;a href=&#34;https://www.npmjs.com/package/@jupyter/eslint-plugin&#34;&gt;&lt;code&gt;@jupyter/eslint-plugin&lt;/code&gt;&lt;/a&gt;, now available on npm. It&amp;rsquo;s a custom ESLint plugin designed specifically for the official Jupyter frontends (JupyterLab, Notebook, JupyterLite) and TypeScript extensions built around them.&lt;/p&gt;
&lt;h2 id=&#34;motivation&#34;&gt;Motivation&lt;/h2&gt;
&lt;p&gt;If you’ve ever written a JupyterLab extension, you’ve probably hit something like this:&lt;/p&gt;
&lt;p&gt;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 &lt;code&gt;activate&lt;/code&gt; function. TypeScript was happy. The editor showed no red squiggles. Nothing in your local toolchain said a word.&lt;/p&gt;
&lt;p&gt;And the slow CI feedback loop is only half the story. Some of these mistakes don’t fail CI at all, a hardcoded English &lt;code&gt;label&lt;/code&gt;, an &lt;code&gt;enum&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;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 &lt;em&gt;how&lt;/em&gt; the framework expects code to be written, not about whether the code type-checks.&lt;/p&gt;
&lt;p&gt;A few examples of what generic linters and TypeScript can’t catch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;JupyterFrontEndPlugin&lt;/code&gt; that requires &lt;code&gt;[TokenA, TokenB]&lt;/code&gt; but whose &lt;code&gt;activate&lt;/code&gt; function takes those arguments in the wrong order. Both are tokens, both type-check but the plugin fails at activation.&lt;/li&gt;
&lt;li&gt;A command added with &lt;code&gt;commands.addCommand(...)&lt;/code&gt; whose &lt;code&gt;label&lt;/code&gt; is a hardcoded English string. The extension ships, gets translated by the community, and that one button silently stays in English.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id=&#34;what-v1-catches&#34;&gt;What v1 catches&lt;/h2&gt;
&lt;p&gt;The plugin currently ships eight rules, grouped by what they protect:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plugin structure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/plugin-activation-args/&#34;&gt;&lt;code&gt;plugin-activation-args&lt;/code&gt;&lt;/a&gt; — Keeps &lt;code&gt;activate&lt;/code&gt; function arguments aligned with &lt;code&gt;requires&lt;/code&gt; and &lt;code&gt;optional&lt;/code&gt; tokens.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/plugin-description/&#34;&gt;&lt;code&gt;plugin-description&lt;/code&gt;&lt;/a&gt; — Requires a non-empty &lt;code&gt;description&lt;/code&gt; on every &lt;code&gt;JupyterFrontEndPlugin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/token-format/&#34;&gt;&lt;code&gt;token-format&lt;/code&gt;&lt;/a&gt; — Enforces the &lt;code&gt;&amp;lt;package&amp;gt;:&amp;lt;TokenSymbol&amp;gt;&lt;/code&gt; convention for &lt;code&gt;new Token(...)&lt;/code&gt; ids.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Commands and settings&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/command-described-by/&#34;&gt;&lt;code&gt;command-described-by&lt;/code&gt;&lt;/a&gt; — Requires a &lt;code&gt;describedBy&lt;/code&gt; schema on commands that accept &lt;code&gt;args&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/no-schema-enum/&#34;&gt;&lt;code&gt;no-schema-enum&lt;/code&gt;&lt;/a&gt; — Disallows &lt;code&gt;enum&lt;/code&gt; in settings JSON schemas in favor of translatable &lt;code&gt;oneOf&lt;/code&gt; with &lt;code&gt;const&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Internationalization&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/no-untranslated-string/&#34;&gt;&lt;code&gt;no-untranslated-string&lt;/code&gt;&lt;/a&gt; — Flags hardcoded user-facing strings in command labels, dialogs, accessibility attributes, JSX, and more.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/no-translation-concatenation/&#34;&gt;&lt;code&gt;no-translation-concatenation&lt;/code&gt;&lt;/a&gt; — Forbids dynamic concatenation inside &lt;code&gt;trans.__()&lt;/code&gt; calls that translation extractors can&amp;rsquo;t pick up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/rules/require-soft-assertions-before-snapshots/&#34;&gt;&lt;code&gt;require-soft-assertions-before-snapshots&lt;/code&gt;&lt;/a&gt; — Requires &lt;code&gt;expect.soft(...)&lt;/code&gt; for all but the last snapshot in a Playwright test, so a single failure doesn&amp;rsquo;t block the rest from updating.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each rule has incorrect/correct examples, options, and rationale in the &lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/category/rules/&#34;&gt;rules reference&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;getting-started-in-60-seconds&#34;&gt;Getting started in 60 seconds&lt;/h2&gt;
&lt;p&gt;Install the plugin:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;npm install --save-dev @jupyter/eslint-plugin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add it to your ESLint fconfig and turn on the recommended ruleset:&lt;/p&gt;
&lt;p&gt;javascript&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;jupyterPlugin&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;@jupyter/eslint-plugin&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;default&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;plugins&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nx&#34;&gt;jupyter&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;jupyterPlugin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;jupyterPlugin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;configs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;recommended&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That’s it. Run &lt;code&gt;jlpm eslint&lt;/code&gt; and you&amp;rsquo;ll start seeing Jupyter-specific issues alongside your existing lint output. If you&amp;rsquo;re still on legacy &lt;code&gt;.eslintrc&lt;/code&gt; configs, there&amp;rsquo;s a &lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/user-guide/#for-legacy-eslintrc-configs&#34;&gt;&lt;code&gt;recommended-legacy&lt;/code&gt;&lt;/a&gt; config too. However, we strongly recommend upgrading to &lt;a href=&#34;https://eslint.org/docs/latest/use/migrate-to-9.0.0&#34;&gt;ESLint v9&lt;/a&gt; and using the new &lt;a href=&#34;https://eslint.org/blog/2022/08/new-config-system-part-2/&#34;&gt;flat config&lt;/a&gt; format.&lt;/p&gt;
&lt;p&gt;Full setup details, including type-aware configuration for &lt;code&gt;plugin-activation-args&lt;/code&gt; and the JSON parser setup for &lt;code&gt;no-schema-enum&lt;/code&gt;, are in the &lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/user-guide/&#34;&gt;user guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you maintain an extension, &lt;strong&gt;the most useful thing you can do today is install it and run it once on your codebase.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;h2 id=&#34;help-us-encode-more-knowledge&#34;&gt;Help us encode more knowledge&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;If you’ve ever been bitten by a Jupyter-specific issue that &lt;em&gt;could&lt;/em&gt; have been a lint rule, something where you found yourself thinking “I wish my editor had warned me about this” please &lt;a href=&#34;https://github.com/jupyterlab/eslint-plugin/issues&#34;&gt;open an issue on the repository&lt;/a&gt;. 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.&lt;/p&gt;
&lt;h2 id=&#34;acknowledgement&#34;&gt;Acknowledgement&lt;/h2&gt;
&lt;p&gt;This work was funded under the &lt;a href=&#34;https://jupyter.org/medium-archive/hugo/posts/2025/announcing-our-first-jupyter-community-funded-proposals/&#34;&gt;first round of Jupyter Community Funded Proposals&lt;/a&gt;. We want to thank the &lt;a href=&#34;https://jupyterfoundation.org/&#34;&gt;Jupyter Foundation&lt;/a&gt; and everyone involved in setting up this funding mechanism.&lt;/p&gt;
&lt;p&gt;We’re also grateful to &lt;a href=&#34;https://github.com/jtpio&#34;&gt;Jeremy Tuloup&lt;/a&gt; and &lt;a href=&#34;https://github.com/brichet&#34;&gt;Nicolas Brichet&lt;/a&gt; for their help in adopting the plugin.&lt;/p&gt;
&lt;h2 id=&#34;about-the-developers&#34;&gt;About the developers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://www.linkedin.com/in/darshan-poudel8/&#34;&gt;&lt;strong&gt;Darshan Paudyal&lt;/strong&gt;&lt;/a&gt; is an intern at &lt;a href=&#34;https://openteams.com/&#34;&gt;OpenTeams&lt;/a&gt; and a member of the &lt;a href=&#34;https://jupyterlab-team-compass.readthedocs.io/en/latest/team.html&#34;&gt;Jupyter Frontend Council&lt;/a&gt;. Darshan led the development of v1 of &lt;code&gt;@jupyter/eslint-plugin&lt;/code&gt;, designing and implementing the rule set, the documentation, and the release.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.linkedin.com/in/michal-krassowski/&#34;&gt;&lt;strong&gt;Michał Krassowski&lt;/strong&gt;&lt;/a&gt; is a Senior Software Engineer at &lt;a href=&#34;https://openteams.com/&#34;&gt;OpenTeams&lt;/a&gt;. Mike provided guidance and direction for the development of the plugin.&lt;/p&gt;
&lt;h2 id=&#34;useful-links&#34;&gt;Useful links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;📦 &lt;a href=&#34;https://www.npmjs.com/package/@jupyter/eslint-plugin&#34;&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📖 &lt;a href=&#34;https://eslint-plugin.readthedocs.io/en/latest/&#34;&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href=&#34;https://github.com/jupyterlab/eslint-plugin&#34;&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💬 &lt;a href=&#34;https://github.com/jupyterlab/eslint-plugin/issues&#34;&gt;Open an issue&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    </item>
    
  </channel>
</rss>
