Repository: jupyterhub/jupyter-server-proxy (language: python)

Issue:
Do we need .yarnrc, or can we always build the extension with jupyterlab 4 that doesn't need it?
I understand that we have `.yarnrc` to enable jupyterlab <4 to build the jupyterlab extension, and `.yarnrc.yml` to enable jupyterlab>=4 to build the jupyterlab extension.

But since we seem to have a pyproject.toml that declares build env stuff, would it be sufficient for us to update this part to require jupyterlab 4 in there and build the extension with jupyterlab 4 also for jupyterlab 3?

https://github.com/jupyterhub/jupyter-server-proxy/blob/7360d846f1786db4419bac7df3671cb72acb0ba8/pyproject.toml#L1-L12


Discussion / hints:
Alternatively can we drop Jupyterlab 3? It's nice to support old versions, but is it worth the added maintenance burden? The core functions of jupyter-server-proxy have been pretty stable for a while.
>  drop Jupyterlab 3

I can't recommend this route. There will be a lot of deployments that don't upgrade immediately.

For this extension, it _should_ be possible to ship an extension, _built_ only with lab 4, but will _run_  in 3 and 4, and as this repo already has extensive, real browser tests, it would be safe to do so even if a few `as any`s are needed to appease typescript.

> require jupyterlab 4

Yes, having a single build environment (with a stable SHA) is by far the simplest play: the two epochal versions of `yarn` can't share lockfiles or config, so there's really no supporting both without a lot of jumping jacks.

This is also good from a CI perspective, as testing against the as-installed

Even in the `yarn` that ships with lab 4, there _is_ some config worth doing: here's what i've been moving towards:

```yaml
#.yarnc.yml
enableInlineBuilds: false
enableTelemetry: false
httpTimeout: 60000
nodeLinker: node-modules
npmRegistryServer: https://registry.npmjs.org/
installStatePath: ./build/.cache/yarn/install-state.gz
cacheFolder: ./build/.cache/yarn/cache
logFilters:
  - code: YN0006
    level: discard
  - code: YN0002
    level: discard
  - code: YN0007
    level: discard
  - code: YN0013
    level: discard
  - code: YN0019
    level: discard
  - code: YN0008
    level: discard
``` 

This cleans up _a lot_ log of noise, and helps ensure a more reproducible `node_modules` folder.
I see significant value in supporting using the labextension in jupyterlab 3 environments as well as jupyterlab 4 environments a while longer, but I don't overview the complexity of supporting it well.

But when it comes to how we build the extension, which is packaged as part of wheels for use in jlab 3-4, I figure it can make sense to always build the jupyterlab extension in jupyterlab 4 environments - and maybe we already do.

Fix patch:
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index cd0451a9..49407453 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -41,7 +41,7 @@ jobs:
       - uses: actions/setup-node@v3
         with:
           cache: yarn
-          node-version: 18.x
+          node-version: 20.x
           registry-url: https://registry.npmjs.org
           cache-dependency-path: labextension/yarn.lock
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d78be2a9..020fc417 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,6 +7,30 @@ the README.md file.
 
 ## Local development setup
 
+### `conda`
+
+<details>
+
+<summary>
+<i>
+Optional, but especially recommended on non-Linux platforms...
+</i>
+</summary>
+
+Using the `conda` (or `mamba` or `micromamba`) package manager with packages from
+[`conda-forge`](https://conda-forge.org/feedstock-outputs) can help isolate development
+environments on nearly any operating system and architecture.
+
+For example, after installing [`mambaforge`](https://conda-forge.org/miniforge),
+create a new environment with all heavy development and test dependencies:
+
+```yaml
+mamba create --name=jupyter-server-proxy --channel=conda-forge "python=3.11" "nodejs=20" pip git geckodriver firefox
+mamba activate jupyter-server-proxy
+```
+
+</details>
+
 ### Python package
 
 ```bash
@@ -14,14 +38,16 @@ the README.md file.
 git clone https://github.com/jupyterhub/jupyter-server-proxy.git
 # Change directory to the jupyter-server-proxy directory
 cd jupyter-server-proxy
-# Install package in development mode
-pip install -e ".[test]"
-# Link your development version of the extension with JupyterLab
-jupyter labextension develop --overwrite .
+# Install package in development mode, with the latest Jupyter clients
+pip install -e ".[test,lab]"
+# Link your development version of the extension with JupyterLab and Notebook
+jlpm labextension develop --overwrite .
 # Server extension must be manually installed in develop mode
 jupyter server extension enable jupyter_server_proxy
 ```
 
+## Testing
+
 Run the tests:
 
 ```bash
@@ -32,21 +58,32 @@ These generate test and coverage reports in `build/pytest` and `build/coverage`.
 
 ### Acceptance tests
 
-If you have `robotframework-jupyterlibary` installed, the acceptance tests will run.
+In `tests/acceptance`, a number of
+[`.robot` files](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html)
+emulate a user starting a Jupyter server, opening a real browser, clicking on
+screen elements, and seeing several working proxy servers in supported Jupyter clients.
 
-To install these in addition to the [Python package](#python-package) test
-dependencies, run:
+These tests are slower and more resource intensive than the unit and integration
+tests, and generate additional screenshots, browser logs, server logs, and report
+HTML in `build/robot`.
 
-```bash
-pip install -e ".[acceptance]"
-```
+#### Extra browser dependencies
 
-In addition, compatible versions of:
+Compatible versions of [`geckodriver`](https://github.com/mozilla/geckodriver)
+and [`firefox`](https://www.mozilla.org/en-US/firefox) need to be on `$PATH`.
 
-- `geckodriver`
-- `firefox`
+These can be provisioned by [a `conda`-compatible installer](#conda), a system
+package manager, or as a last resort, direct binary downloads.
 
-Need to be on your `$PATH` and compatible with each other.
+#### Acceptance test dependencies
+
+To install the additional dependencies beyond the [Python package](#python-package)
+test dependencies, and run the tests against the latest Jupyter clients:
+
+```bash
+pip install -e ".[test,acceptance,lab]"
+pytest
+```
 
 To run _only_ the acceptance tests, use the `-k` switch:
 
@@ -54,53 +91,138 @@ To run _only_ the acceptance tests, use the `-k` switch:
 pytest -k acceptance
 ```
 
-These are slower than the rest of the `pytest` tests, and generate screenshots,
-browser logs, server logs, and report HTML in `build/robot`.
+#### Older Jupyter Clients
+
+To run the acceptance tests against the previous major versions of Notebook
+and JupyterLab, it is advisable to use a separate, isolated environment, testing the
+as-built assets from `pyproject-build`.
+
+After creating and activating such an environment with `virtualenv` or [`conda`](#conda):
+
+```bash
+pip install --find-links ./dist/ --no-index-url jupyter-server-proxy[test,acceptance,classic]
+```
+
+## Frontend Development
+
+To support a wide range of clients, both JupyterLab and Notebook Classic extensions
+are built and distributed, each with their own quirks.
+
+### JupyterLab/Notebook extension
+
+The `./labextension/` directory contains the extension for the
+[`lumino`](https://github.com/jupyterlab/lumino/)-based JupyterLab and Notebook
+clients.
+
+#### `nodejs`
+
+Building this extension requires a compatible version of
+[`nodejs`](https://nodejs.org/en/download/package-manager), with a supported, long
+term support (LTS) release recommended.
 
-### JupyterLab extension
+#### `jlpm`
 
-The `jlpm` command is JupyterLab's pinned version of `yarn` that is
-installed with JupyterLab.
+The `jlpm` command is a vendored, pinned version of the [`yarn`](https://yarnpkg.com)
+package manager. Installed with JupyterLab, it performs commands such
+as installing `npm` dependencies listed in `labextension/package.json`, building
+and watching the extension from source, and formatting web-related source code files.
 
-> You may use `yarn` or `npm run` instead of `jlpm` below.
+#### The built Lab extension
+
+During a [`pyproject-build`](https://pypi.org/project/build/)
+of the python package, a temporary JupyterLab and `jlpm` will be installed as part
+of the `build-system`, executing roughly the commands:
 
 ```bash
 cd labextension            # Change to the root of the labextension
-jlpm                       # Install dependencies (or `npm i`)
+jlpm                       # Install dependencies
 jlpm build:prod            # Build:
-                           # - `labextension/lib`
-                           # - `jupyter_server_proxy/labextension`
+                           # - `labextension/lib` with type checking
+                           # - `jupyter_server_proxy/labextension` with minimization
+```
+
+During `pip install`, the built assets are copied to the user's
+`{sys.prefix}/share/jupyter/labextensions/@jupyterhub/jupyter-server-proxy` to be
+found by the application at startup.
+
+#### Developing the Lab extension
+
+For fine-grained access to the `jlpm` command and various build steps:
+
+```bash
+pip install -e .[lab]      # Ensure a compatible jlpm
+cd labextension            # Change to the root of the labextension
+jlpm
 jlpm install:extension     # Symlink into `{sys.prefix}/share/jupyter/labextensions`
 ```
 
-You can watch the source directory and automatically rebuild the `labextension/lib`
+Watch the source directory and automatically rebuild the `labextension/lib`
 and `jupyter_server_proxy/labextension` folders:
 
 ```bash
 cd labextension
 # Watch the source directory in one terminal, automatically rebuilding when needed
-jlpm run watch
+jlpm watch
 # Run JupyterLab in another terminal
 jupyter lab
 ```
 
-With the watch command running, every saved change will immediately be built locally
-and available in your running JupyterLab. Refresh JupyterLab to load the change in
-your browser (you may need to wait several seconds for the extension to be rebuilt).
+While running `jlpm watch`, every saved change to a `.ts` file will immediately be
+built locally and available in your running Jupyter client. "Hard" refresh JupyterLab or Notebook
+with <kbd>CTRL-F5</kbd> or <kbd>⌘-F5</kbd> to load the change in your browser
+(you may need to wait several seconds for the extension to be fully rebuilt).
+
+#### Source Maps
+
+By default, the `jlpm build` and `jlpm watch` commands generate
+[source maps](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/)
+for this extension to improve debugging using the browser development tools,
+often revealed by pressing <kbd>F12</kbd>.
 
-By default, the `jlpm run build` command generates the source maps for this
-extension to make it easier to debug using the browser dev tools. To also generate
-source maps for the JupyterLab core extensions, you can run the following command:
+To also generate source maps for the JupyterLab core application, run the following command:
 
 ```bash
 jupyter lab build --minimize=False
 ```
 
-### Documentation
+### Notebook Classic extension
+
+The files in `jupyter_server_proxy/static` extend the Notebook Classic application's
+_Tree_ page.
+
+#### RequireJS
+
+The Notebook Classic extension uses the [`require.js`](https://requirejs.org)
+dependency injection system, and presently uses no dependencies beyond what is
+provided by Notebook Classic.
+
+#### The built Classic extension
+
+During a user's `pip install`, the static assets are copied to
+`{sys.prefix}/share/jupyter/nbextensions/jupyter_server_proxy`, to be
+found by the application at startup.
+
+#### Developing the Classic extension
+
+While this extension is served as-is once installed, for live development the
+extension assets must be linked:
+
+```bash
+pip install -e ".[classic]"
+jupyter nbextension install --symlink --sys-prefix --py jupyter_server_proxy
+```
+
+After making changes, "hard" refresh the browser application, usually with
+<kbd>CTRL-F5</kbd> or <kbd>⌘-F5</kbd>.
+
+## Documentation
 
 The documentation uses a fairly standard [Sphinx](https://www.sphinx-doc.org)
 build chain, and requires `make` on Linux/MacOS, which cannot be installed with
-`pip`.
+`pip`
+
+> `make` is available from [`conda-forge`](#conda) as `make` for Linux/OSX, and `m2-make`
+> on Windows
 
 In addition to any system packages, building the documentation requires
 additional packages. To install the needed packages:
@@ -132,3 +254,12 @@ make linkcheck
 ```bash
 make devenv
 ```
+
+## Linting
+
+During continuous integration (CI) the `pre-commit` package is used to run a
+number of checks, with each tool in a private virtual environment. If it is able to,
+the CI bot will push to a PR with fixes.
+
+By installing `pre-commit` with `pip` or `conda`, you can have this same experience,
+or inspect the configuration to try to recreate it yourself locally.
diff --git a/README.md b/README.md
index af6252e8..bfdebc8e 100644
--- a/README.md
+++ b/README.md
@@ -52,10 +52,6 @@ Server via the container.
 
 ## Install
 
-### Requirements
-
-Either `jupyterlab>=3` or `notebook` is required.
-
 ### Python package
 
 #### `pip`
@@ -67,17 +63,27 @@ pip install jupyter-server-proxy
 #### `conda`
 
 ```bash
-conda install jupyter-server-proxy -c conda-forge
+conda install -c conda-forge jupyter-server-proxy
 ```
 
-### JupyterLab extension
+### Jupyter Client Extensions
+
+A JupyterLab and Notebook extension is bundled with the Python package to
+provide:
+
+- servers in the _New_ dropwdown of the Notebook Tree view
+- launch buttons in JupyterLab's Launcher panel for registered server processes.
+  - ![a screenshot of the JupyterLab Launcher](docs/source/_static/images/labextension-launcher.png "launch proxied servers as JupyterLab panels or new browser tabs")
 
-A JupyterLab extension is bundled with the Python package to provide launch
-buttons in JupyterLab's Launcher panel for registered server processes.
+#### Client compatibility
 
-![](docs/source/_static/images/labextension-launcher.png)
+For historical compatibility ranges, see the table below:
 
-Clicking on them opens the proxied application in a new browser window.
+| `jupyter-server-proxy` | `notebook` | `jupyterlab` |
+| :--------------------: | :--------: | :----------: |
+|        `4.1.x`         |  `>=6,<8`  |   `>=3,<5`   |
+|        `4.0.x`         |  `>=6,<7`  |   `>=3,<4`   |
+|         `3.x`          |  `>=6,<7`  |   `>=2,<4`   |
 
 ## Disable
 
@@ -88,7 +94,7 @@ jupyter serverextension disable --sys-prefix jupyter_server_proxy
 jupyter server extension disable --sys-prefix jupyter_server_proxy
 ```
 
-### Notebook classic extension
+### Notebook Classic extension
 
 ```bash
 jupyter nbextension disable --sys-prefix --py jupyter_server_proxy
diff --git a/labextension/.yarnrc b/labextension/.yarnrc
deleted file mode 100644
index 55794136..00000000
--- a/labextension/.yarnrc
+++ /dev/null
@@ -1,11 +0,0 @@
-# jlpm in JupyterLab 2 and 3 is yarn 1, and this is a yarn 1 compatible
-# configuration file.
-#
-# Config reference: https://classic.yarnpkg.com/lang/en/docs/yarnrc/
-#
-disable-self-update-check true
-ignore-optional true
-network-timeout "300000"
-registry "https://registry.npmjs.org/"
-yarn-offline-mirror "../build/.yarn-packages"
-yarn-offline-mirror-pruning true
diff --git a/labextension/.yarnrc.yml b/labextension/.yarnrc.yml
index e73c960d..cadab868 100644
--- a/labextension/.yarnrc.yml
+++ b/labextension/.yarnrc.yml
@@ -1,8 +1,27 @@
-# jlpm in JupyterLab 4 is yarn 3, and this is a yarn 2+ compatible configuration
-# file.
-#
-# Config reference: https://yarnpkg.com/configuration/yarnrc
+# jlpm in JupyterLab 4 is yarn 3
 #
+# JupyterLab 4 is used when building the extension as specified via
+# ../pyproject.toml's build-system config, but the built extension can be used by
+# JupyterLab 3 as well.
+
+enableInlineBuilds: false
+enableTelemetry: false
+httpTimeout: 60000
 nodeLinker: node-modules
-httpTimeout: 300000
-npmRegistryServer: "https://registry.npmjs.org/"
+npmRegistryServer: https://registry.npmjs.org/
+installStatePath: ./build/.cache/yarn/install-state.gz
+cacheFolder: ./build/.cache/yarn/cache
+# logFilters codes described: https://yarnpkg.com/advanced/error-codes
+logFilters:
+  - code: YN0002 # MISSING_PEER_DEPENDENCY
+    level: discard
+  - code: YN0006 # SOFT_LINK_BUILD
+    level: discard
+  - code: YN0007 # MUST_BUILD
+    level: discard
+  - code: YN0008 # MUST_REBUILD
+    level: discard
+  - code: YN0013 # FETCH_NOT_CACHED
+    level: discard
+  - code: YN0019 # UNUSED_CACHE_ENTRY
+    level: discard
diff --git a/labextension/README.md b/labextension/README.md
index 3cc85ca8..6f002f9b 100644
--- a/labextension/README.md
+++ b/labextension/README.md
@@ -1,17 +1,18 @@
 # `@jupyterhub/jupyter-server-proxy`
 
-A JupyterLab extension that adds items to the JupyterLab [Launcher] representing the
-configured server processes managed by the python package `jupyter-server-proxy` (required).
+A pre-built JupyterLab extension that adds items to the JupyterLab [Launcher]
+to open server processes managed by the python package
+[`jupyter-server-proxy`](https://pypi.org/project/jupyter-server-proxy).
 
 [launcher]: https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#launcher
 
 ## Prerequisites
 
-- JupyterLab >=2,<5
+- JupyterLab >=3,<5
 
 ## Installation
 
-For JupyterLab 3+, use your preferred Python package manager to install `jupyter-server-proxy`:
+Use your preferred Python package manager to install `jupyter-server-proxy`:
 
 ```bash
 pip install jupyter-server-proxy
@@ -20,24 +21,12 @@ pip install jupyter-server-proxy
 or
 
 ```bash
-conda install jupyter-server-proxy
+conda install -c conda-forge jupyter-server-proxy
 ```
 
 > As a _prebuilt_ extension, it will "just work," only a simple page reload should be required
 > to see launcher items. However, a full restart of `jupyter_server` or `notebook` is required
 > to reload the `jupyter_server_proxy` serverextension which provides most of the functionality.
 
-### JupyterLab 2
-
-For JupyterLab 2, ensure `nodejs` is installed and available on `$PATH` and run:
-
-```bash
-jupyter labextension install @jupyterhub/jupyter-server-proxy
-```
-
-> This will cause:
->
-> - a download of a great deal of build assets from `npmjs.org`
-> - a large `webpack` build that may exhaust memory and open files handles
->
-> **Please** consider upgrading to JupyterLab 3+ before reporting issues.
+For a full development and testing installation, see the
+[contributing guide](https://github.com/jupyterhub/jupyter-server-proxy/blob/main/CONTRIBUTING.md).
diff --git a/labextension/package.json b/labextension/package.json
index 5a90350d..3c7aead4 100644
--- a/labextension/package.json
+++ b/labextension/package.json
@@ -40,18 +40,21 @@
     "watch": "run-p watch:src watch:labextension",
     "watch:src": "jlpm build:lib -w --preserveWatchOutput",
     "watch:labextension": "jupyter labextension watch .",
-    "deduplicate": "yarn-deduplicate -s fewer --fail"
+    "deduplicate": "yarn-deduplicate -s fewer --fail",
+    "lint:prettier": "prettier --write \"../.github/**/*.yaml\" \"../*.{yaml,yml,md}\" \"../docs/**/*.md\" \"src/**/*.{tsx,ts}\" \"./*.{yml,json,md}\""
   },
   "dependencies": {
-    "@jupyterlab/application": "^2.0 || ^3.0 || ^4.0",
-    "@jupyterlab/launcher": "^2.0 || ^3.0 || ^4.0"
+    "@jupyterlab/application": "^3.0 || ^4.0",
+    "@jupyterlab/filebrowser": "^3.0 || ^4.0",
+    "@jupyterlab/launcher": "^3.0 || ^4.0"
   },
   "devDependencies": {
-    "@jupyterlab/builder": "^3.2.4 || ^4.0.0",
+    "@jupyterlab/builder": "^4.0.6",
     "npm-run-all": "^4.1.5",
-    "rimraf": "^3.0.2",
-    "typescript": "~4.8.4",
-    "yarn-deduplicate": "^6.0.0"
+    "prettier": "^3.0.3",
+    "rimraf": "^5.0.1",
+    "typescript": "~5.2.2",
+    "yarn-berry-deduplicate": "^6.0.0"
   },
   "jupyterlab": {
     "extension": true,
diff --git a/labextension/src/index.ts b/labextension/src/index.ts
index d90cce9a..e706d5a8 100644
--- a/labextension/src/index.ts
+++ b/labextension/src/index.ts
@@ -1,70 +1,151 @@
+/**
+ * The main extension file, which provides plugins that can be loaded into
+ * a Lumino application such as JupyterLab or Notebook 7.
+ *
+ * Outside of this file, of note is `./tokens.ts`, which provides a number of
+ * run-time constants and compile-type interfaces for type-checking.
+ *
+ * Imports (mostly) adhere to the ES `import` semantics, exceptions noted below
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
+ */
+
+// this is a type-only import for low-level components of the UI
+// @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html
+import type {
+  // the base class for all on-screen components
+  Widget,
+  // the Lumino component for menu bars
+  Menu,
+} from "@lumino/widgets";
+
 import {
+  // the dependency-injection (DI) Token required to request a dependency for...
+  ILayoutRestorer,
+  // any lumino-based Jupyter application...
   JupyterFrontEnd,
+  // to be extended with...
   JupyterFrontEndPlugin,
-  ILayoutRestorer,
 } from "@jupyterlab/application";
+import {
+  // a DI token for the toolbar, used to extend Notebook's Tree
+  IToolbarWidgetRegistry,
+  // the concrete class used to create iframes in the JupyterLab main area
+  IFrame,
+  // a wrapper for the iframe that handles house-keeping boilerplate
+  MainAreaWidget,
+  // a tracker which handles iframe placement housekeeping when JupyterLab is reloaded
+  WidgetTracker,
+} from "@jupyterlab/apputils";
+// utilities for working with URLs and the `jupyter-config-data` script
+import { PageConfig, URLExt } from "@jupyterlab/coreutils";
+// a DI token for the file browser, present in JupyterLab and Notebook 7
+import { IDefaultFileBrowser } from "@jupyterlab/filebrowser";
+// a DI token for the card-based Launcher, present in JupyterLab
 import { ILauncher } from "@jupyterlab/launcher";
-import { PageConfig } from "@jupyterlab/coreutils";
-import { IFrame, MainAreaWidget, WidgetTracker } from "@jupyterlab/apputils";
+// the application-wide configuration for making HTTP requests with headers, etc.
+import { ServerConnection } from "@jupyterlab/services";
 
-function newServerProxyWidget(
-  id: string,
-  url: string,
-  text: string,
-): MainAreaWidget<IFrame> {
-  const content = new IFrame({
-    sandbox: [
-      "allow-same-origin",
-      "allow-scripts",
-      "allow-popups",
-      "allow-forms",
-      "allow-downloads",
-      "allow-modals",
-    ],
-  });
-  content.title.label = text;
-  content.title.closable = true;
-  content.url = url;
-  content.addClass("jp-ServerProxy");
-  content.id = id;
-  const widget = new MainAreaWidget({ content });
-  widget.addClass("jp-ServerProxy");
-  return widget;
-}
+// local imports from `tokens.ts` for immutable constants
+import {
+  CommandIDs,
+  IOpenArgs,
+  IServerProcess,
+  IServersInfo,
+  NAME,
+  NS,
+  argSchema,
+  sandbox,
+} from "./tokens";
+
+/*
+ * top level constants that won't change during the application lifecycle
+ */
+const baseUrl = PageConfig.getBaseUrl();
+/**
+ * The Notebook 7 sub-application: `tree`, `notebook`, `editor`, `terminal`, etc.
+ */
+const notebookPage = PageConfig.getOption("notebookPage");
+/**
+ * Whether the current application is `/tree`: otherwise we don't do anything
+ */
+const isTree = notebookPage === "tree";
+/**
+ * Whether this is a notebook app at all
+ */
+const isNotebook7 = !!notebookPage;
+
+/**
+ * Data to register the extension with jupyterlab which also clarifies the shape of
+ * what is required by the extension and passed to our provided activate function.
+ *
+ * This plugin is `export`ed at the end of the file.
+ *
+ * @see https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#application-plugins
+ */
+const plugin: JupyterFrontEndPlugin<void> = {
+  id: `${NAME}:add-launcher-entries`,
+  autoStart: true,
+  // to support JupyterLab and Notebook, we don't _require_ any other DI tokens...
+  requires: [],
+  // ... but some decisions will be made on the presence of these optional DI tokens,
+  //     which will be given to `activate` in the order listed here
+  optional: [
+    ILauncher,
+    ILayoutRestorer,
+    IToolbarWidgetRegistry,
+    IDefaultFileBrowser,
+  ],
+  activate,
+};
 
 /**
  * The activate function is registered to be called on activation of the
- * jupyterlab extension.
+ * JupyterLab/Notebook 7 plugin.
  *
- * ref: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
+ * @see https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
  */
 async function activate(
   app: JupyterFrontEnd,
-  launcher: ILauncher,
-  restorer: ILayoutRestorer,
+  // `requires` would have show up here with concrete types ...
+  // ... but all `optional` DI tokens _might_ be `null`
+  launcher: ILauncher | null,
+  restorer: ILayoutRestorer | null,
+  toolbarRegistry: IToolbarWidgetRegistry | null,
+  fileBrowser: IDefaultFileBrowser | null,
 ): Promise<void> {
+  // when viewing `/notebook`, `/terminal` or `/editor`, bail as early as possible
+  if (isNotebook7 && !isTree) {
+    return;
+  }
+
+  // server connection settings (such as headers) _can't_ be a global, as they
+  // can potentially be configured by other extensions
+  const serverSettings = ServerConnection.makeSettings();
+
   // Fetch configured server processes from {base_url}/server-proxy/servers-info
-  const response = await fetch(
-    PageConfig.getBaseUrl() + "server-proxy/servers-info",
-  );
+  // TODO: consider moving this to a separate plugin
+  // TODO: consider not blocking the application load
+  const url = URLExt.join(baseUrl, `${NS}/servers-info`);
+  const response = await ServerConnection.makeRequest(url, {}, serverSettings);
+
   if (!response.ok) {
-    console.log(
+    console.warn(
       "Could not fetch metadata about registered servers. Make sure jupyter-server-proxy is installed.",
     );
-    console.log(response);
+    console.warn(response);
     return;
   }
-  const data = await response.json();
 
-  const namespace = "server-proxy";
-  const tracker = new WidgetTracker<MainAreaWidget<IFrame>>({
-    namespace,
-  });
-  const command = namespace + ":" + "open";
+  // load and trust the JSON as a type of data described by the `IServersInfo` interface
+  // TODO: consider adding JSON schema-derived types
+  const data = (await response.json()) as IServersInfo;
 
+  // handle restoring persistent JupyterLab workspace widgets on page reload
+  // this is created even in the Notebook `tree` page to reduce complexity below
+  const tracker = new WidgetTracker<MainAreaWidget<IFrame>>({ namespace: NS });
   if (restorer) {
     void restorer.restore(tracker, {
-      command: command,
+      command: CommandIDs.open,
       args: (widget) => ({
         url: widget.content.url,
         title: widget.content.title.label,
@@ -75,21 +156,24 @@ async function activate(
     });
   }
 
+  // register commands
+  // commands provide "loose" coupling, based on well-known strings and JSON-like
+  // structures instead of heavy DI tokens
   const { commands, shell } = app;
-  commands.addCommand(command, {
-    label: (args) => args["title"] as string,
+  commands.addCommand(CommandIDs.open, {
+    label: (args) => (args as IOpenArgs).title,
+    describedBy: async () => {
+      return { args: argSchema };
+    },
     execute: (args) => {
-      const id = args["id"] as string;
-      const title = args["title"] as string;
-      const url = args["url"] as string;
-      const newBrowserTab = args["newBrowserTab"] as boolean;
+      // the syntax below is an example of "destructuring"
+      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
+      const { id, title, url, newBrowserTab } = args as IOpenArgs;
       if (newBrowserTab) {
         window.open(url, "_blank");
         return;
       }
-      let widget = tracker.find((widget) => {
-        return widget.content.id == id;
-      });
+      let widget = tracker.find((widget) => widget.content.id === id);
       if (!widget) {
         widget = newServerProxyWidget(id, url, title);
       }
@@ -98,52 +182,103 @@ async function activate(
       }
       if (!widget.isAttached) {
         shell.add(widget);
-        return widget;
       } else {
         shell.activateById(widget.id);
       }
+      return widget;
     },
   });
 
-  for (let server_process of data.server_processes) {
-    if (!server_process.launcher_entry.enabled) {
-      continue;
+  // handle adding JupyterLab launcher cards
+  // TODO: consider moving this to a separate plugin (keeping this ID)
+  if (launcher) {
+    for (let server_process of data.server_processes) {
+      const { launcher_entry } = server_process;
+
+      if (!launcher_entry.enabled) {
+        continue;
+      }
+
+      launcher.add({
+        command: CommandIDs.open,
+        args: argsForServer(server_process),
+        category: "Notebook",
+        kernelIconUrl: launcher_entry.icon_url || void 0,
+      });
     }
+  }
 
-    const url =
-      PageConfig.getBaseUrl() + server_process.launcher_entry.path_info;
-    const title = server_process.launcher_entry.title;
-    const newBrowserTab = server_process.new_browser_tab;
-    const id = namespace + ":" + server_process.name;
-    const launcher_item: ILauncher.IItemOptions = {
-      command: command,
-      args: {
-        url: url,
-        title: title + (newBrowserTab ? " [↗]" : ""),
-        newBrowserTab: newBrowserTab,
-        id: id,
-      },
-      category: "Notebook",
-    };
-
-    if (server_process.launcher_entry.icon_url) {
-      launcher_item.kernelIconUrl = server_process.launcher_entry.icon_url;
+  // handle adding servers menu items to the Notebook 7 _Tree_ toolbar
+  // TODO: consider moving this to a separate plugin
+  if (isTree && toolbarRegistry && fileBrowser) {
+    const { toolbar } = fileBrowser;
+    const widgets = ((toolbar.layout || {}) as any).widgets as Widget[];
+    if (widgets && widgets.length) {
+      for (const widget of widgets) {
+        if (widget && (widget as any).menus) {
+          // simple DOM queries can't be used, as there is no guarantee it is
+          // attached yet
+          const menu: Menu = (widget as any).menus[0];
+          menu.addItem({ type: "separator" });
+          for (const server_process of data.server_processes) {
+            // create args, overriding all to launch in new heavyweight browser tabs
+            let args = {
+              ...argsForServer(server_process),
+              newBrowserTab: true,
+            };
+            menu.addItem({ command: CommandIDs.open, args });
+          }
+        }
+      }
     }
-    launcher.add(launcher_item);
   }
 }
 
 /**
- * Data to register the extension with jupyterlab which also clarifies whats
- * required by the extension and passed to our provided activate function.
+ * Create a new `iframe`, with a wrapper for including in the main area.
+ */
+function newServerProxyWidget(
+  id: string,
+  url: string,
+  text: string,
+): MainAreaWidget<IFrame> {
+  const content = new IFrame({
+    // provide relatively permissing sandbox exceptions
+    sandbox,
+  });
+  content.title.label = text;
+  content.title.closable = true;
+  content.url = url;
+  content.addClass("jp-ServerProxy");
+  content.id = id;
+  const widget = new MainAreaWidget({ content });
+  widget.addClass("jp-ServerProxy");
+  return widget;
+}
+
+/**
+ * Generate command arguments for a given server.
+ *
+ * @see https://lumino.readthedocs.io/en/latest/api/interfaces/commands.CommandRegistry.ICommandOptions.html
  *
- * ref: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#application-plugins
+ * For parity with Notebook Classic, any server opened from the Tree will be loaded
+ * in a new browser tab.
  */
-const extension: JupyterFrontEndPlugin<void> = {
-  id: "@jupyterhub/jupyter-server-proxy:add-launcher-entries",
-  autoStart: true,
-  requires: [ILauncher, ILayoutRestorer],
-  activate: activate,
-};
+function argsForServer(server_process: IServerProcess): IOpenArgs {
+  const { new_browser_tab, launcher_entry, name } = server_process;
+
+  const suffix = new_browser_tab && !isNotebook7 ? " [↗]" : "";
+
+  return {
+    // do not use URLext here due to trailing slash opinions
+    url: `${baseUrl}${launcher_entry.path_info}`,
+    title: `${launcher_entry.title}${suffix}`,
+    newBrowserTab: new_browser_tab,
+    id: `${NS}:${name}`,
+  };
+}
 
-export default extension;
+// finally, export the plugin...
+export default plugin;
+// in the future, the above could be an array of plugins, to provide finer-grained
+// or replaceable implementations
diff --git a/labextension/src/tokens.ts b/labextension/src/tokens.ts
new file mode 100644
index 00000000..f8344374
--- /dev/null
+++ b/labextension/src/tokens.ts
@@ -0,0 +1,105 @@
+/**
+ * A collection of tokens and constants used in `index.ts`.
+ *
+ * `import`ing this file should have basically no side-effects.
+ */
+
+import type { IFrame } from "@jupyterlab/apputils";
+
+import type { ReadonlyJSONObject } from "@lumino/coreutils";
+
+/**
+ * The canonical name of the extension, also used in CLI commands such as
+ * `jupyter labextension disable`.
+ */
+export const NAME = "@jupyterhub/jupyter-server-proxy";
+
+/**
+ * The short namespace for commands, etc.
+ */
+export const NS = "server-proxy";
+
+/**
+ * The values for the `iframe` `sandbox` attribute.
+ *
+ * These are generally required for most non-trivial client applications, but
+ * do not affect full browser tabs.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
+ */
+export const sandbox = Object.freeze([
+  "allow-same-origin",
+  "allow-scripts",
+  "allow-popups",
+  "allow-forms",
+  "allow-downloads",
+  "allow-modals",
+]) as IFrame.SandboxExceptions[];
+
+/**
+ * The JSON schema for the open command arguments.
+ *
+ * @see https://lumino.readthedocs.io/en/latest/api/interfaces/commands.CommandRegistry.ICommandOptions.html
+ */
+export const argSchema = Object.freeze({
+  type: "object",
+  properties: {
+    id: { type: "string" },
+    title: { type: "string" },
+    url: { type: "string", format: "uri" },
+    newBrowserTab: { type: "boolean" },
+  },
+});
+
+/**
+ * The identifying string names for server proxy commands.
+ *
+ * Namespaces are real objects that exist at runtime in the browser
+ *
+ * @see https://www.typescriptlang.org/docs/handbook/namespaces.html
+ */
+export namespace CommandIDs {
+  /* Opens a new server proxy tab */
+  export const open = `${NS}:open`;
+}
+
+// Below here are TypeScript interfaces. These do _not_ exist at runtime
+// but are useful when working with untyped data such as provided by the server
+// @see https://www.typescriptlang.org/docs/handbook/interfaces.html
+
+/**
+ * An interface for the arguments to the open command.
+ */
+export interface IOpenArgs extends ReadonlyJSONObject {
+  id: string;
+  title: string;
+  url: string;
+  newBrowserTab: boolean;
+}
+
+/**
+ * An interface for the server response from `/server-proxy/servers-info`
+ */
+export interface IServersInfo {
+  server_processes: IServerProcess[];
+}
+
+/**
+ * An interface for the public description of a single server proxy.
+ */
+export interface IServerProcess {
+  new_browser_tab: boolean;
+  launcher_entry: ILauncherEntry;
+  name: string;
+}
+
+/**
+ * An interface for launcher-card specific information.
+ */
+export interface ILauncherEntry {
+  enabled: boolean;
+  title: string;
+  path_info: string;
+  // the `?` means this argument may not exist, but if it does, it must be a string
+  icon_url?: string;
+}
diff --git a/labextension/yarn.lock b/labextension/yarn.lock
index a70a94f3..7fb40f15 100644
--- a/labextension/yarn.lock
+++ b/labextension/yarn.lock
@@ -5,6 +5,15 @@ __metadata:
   version: 6
   cacheKey: 8
 
+"@arcanis/slice-ansi@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "@arcanis/slice-ansi@npm:1.1.1"
+  dependencies:
+    grapheme-splitter: ^1.0.4
+  checksum: 14ed60cb45750d386c64229ac7bab20e10eedc193503fa4decff764162d329d6d3363ed2cd3debec833186ee54affe4f824f6e8eff531295117fd1ebda200270
+  languageName: node
+  linkType: hard
+
 "@codemirror/state@npm:^6.2.0":
   version: 6.2.1
   resolution: "@codemirror/state@npm:6.2.1"
@@ -26,6 +35,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@isaacs/cliui@npm:^8.0.2":
+  version: 8.0.2
+  resolution: "@isaacs/cliui@npm:8.0.2"
+  dependencies:
+    string-width: ^5.1.2
+    string-width-cjs: "npm:string-width@^4.2.0"
+    strip-ansi: ^7.0.1
+    strip-ansi-cjs: "npm:strip-ansi@^6.0.1"
+    wrap-ansi: ^8.1.0
+    wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0"
+  checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb
+  languageName: node
+  linkType: hard
+
 "@jridgewell/gen-mapping@npm:^0.3.0":
   version: 0.3.3
   resolution: "@jridgewell/gen-mapping@npm:0.3.3"
@@ -96,17 +119,19 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@jupyterhub/jupyter-server-proxy@workspace:."
   dependencies:
-    "@jupyterlab/application": ^2.0 || ^3.0 || ^4.0
-    "@jupyterlab/builder": ^3.2.4 || ^4.0.0
-    "@jupyterlab/launcher": ^2.0 || ^3.0 || ^4.0
+    "@jupyterlab/application": ^3.0 || ^4.0
+    "@jupyterlab/builder": ^4.0.6
+    "@jupyterlab/filebrowser": ^3.0 || ^4.0
+    "@jupyterlab/launcher": ^3.0 || ^4.0
     npm-run-all: ^4.1.5
-    rimraf: ^3.0.2
-    typescript: ~4.8.4
-    yarn-deduplicate: ^6.0.0
+    prettier: ^3.0.3
+    rimraf: ^5.0.1
+    typescript: ~5.2.2
+    yarn-berry-deduplicate: ^6.0.0
   languageName: unknown
   linkType: soft
 
-"@jupyterlab/application@npm:^2.0 || ^3.0 || ^4.0":
+"@jupyterlab/application@npm:^3.0 || ^4.0":
   version: 4.0.6
   resolution: "@jupyterlab/application@npm:4.0.6"
   dependencies:
@@ -163,7 +188,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jupyterlab/builder@npm:^3.2.4 || ^4.0.0":
+"@jupyterlab/builder@npm:^4.0.6":
   version: 4.0.6
   resolution: "@jupyterlab/builder@npm:4.0.6"
   dependencies:
@@ -241,6 +266,29 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@jupyterlab/docmanager@npm:^4.0.6":
+  version: 4.0.6
+  resolution: "@jupyterlab/docmanager@npm:4.0.6"
+  dependencies:
+    "@jupyterlab/apputils": ^4.1.6
+    "@jupyterlab/coreutils": ^6.0.6
+    "@jupyterlab/docregistry": ^4.0.6
+    "@jupyterlab/services": ^7.0.6
+    "@jupyterlab/statusbar": ^4.0.6
+    "@jupyterlab/translation": ^4.0.6
+    "@jupyterlab/ui-components": ^4.0.6
+    "@lumino/algorithm": ^2.0.1
+    "@lumino/coreutils": ^2.1.2
+    "@lumino/disposable": ^2.1.2
+    "@lumino/messaging": ^2.0.1
+    "@lumino/properties": ^2.0.1
+    "@lumino/signaling": ^2.1.2
+    "@lumino/widgets": ^2.3.0
+    react: ^18.2.0
+  checksum: 25d3f694ae8664ca6c54bfcd36d8913caba9455fea68ed3df23963ce9723254c1f2c38fb6a93e267187f095392507d40cd2a4181c30173306c1c0b962e001b93
+  languageName: node
+  linkType: hard
+
 "@jupyterlab/docregistry@npm:^4.0.6":
   version: 4.0.6
   resolution: "@jupyterlab/docregistry@npm:4.0.6"
@@ -266,7 +314,35 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jupyterlab/launcher@npm:^2.0 || ^3.0 || ^4.0":
+"@jupyterlab/filebrowser@npm:^3.0 || ^4.0":
+  version: 4.0.6
+  resolution: "@jupyterlab/filebrowser@npm:4.0.6"
+  dependencies:
+    "@jupyterlab/apputils": ^4.1.6
+    "@jupyterlab/coreutils": ^6.0.6
+    "@jupyterlab/docmanager": ^4.0.6
+    "@jupyterlab/docregistry": ^4.0.6
+    "@jupyterlab/services": ^7.0.6
+    "@jupyterlab/statedb": ^4.0.6
+    "@jupyterlab/statusbar": ^4.0.6
+    "@jupyterlab/translation": ^4.0.6
+    "@jupyterlab/ui-components": ^4.0.6
+    "@lumino/algorithm": ^2.0.1
+    "@lumino/coreutils": ^2.1.2
+    "@lumino/disposable": ^2.1.2
+    "@lumino/domutils": ^2.0.1
+    "@lumino/dragdrop": ^2.1.3
+    "@lumino/messaging": ^2.0.1
+    "@lumino/polling": ^2.1.2
+    "@lumino/signaling": ^2.1.2
+    "@lumino/virtualdom": ^2.0.1
+    "@lumino/widgets": ^2.3.0
+    react: ^18.2.0
+  checksum: abe7eca4072a9c3d1f7a756840d0ad209403928b958fe09dfd81fbb693cb18c91c64027157babe1e7353a556b11c070716326b16ee2eb629089399906a3467be
+  languageName: node
+  linkType: hard
+
+"@jupyterlab/launcher@npm:^3.0 || ^4.0":
   version: 4.0.6
   resolution: "@jupyterlab/launcher@npm:4.0.6"
   dependencies:
@@ -593,6 +669,40 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@nodelib/fs.scandir@npm:2.1.5":
+  version: 2.1.5
+  resolution: "@nodelib/fs.scandir@npm:2.1.5"
+  dependencies:
+    "@nodelib/fs.stat": 2.0.5
+    run-parallel: ^1.1.9
+  checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59
+  languageName: node
+  linkType: hard
+
+"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2":
+  version: 2.0.5
+  resolution: "@nodelib/fs.stat@npm:2.0.5"
+  checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0
+  languageName: node
+  linkType: hard
+
+"@nodelib/fs.walk@npm:^1.2.3":
+  version: 1.2.8
+  resolution: "@nodelib/fs.walk@npm:1.2.8"
+  dependencies:
+    "@nodelib/fs.scandir": 2.1.5
+    fastq: ^1.6.0
+  checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53
+  languageName: node
+  linkType: hard
+
+"@pkgjs/parseargs@npm:^0.11.0":
+  version: 0.11.0
+  resolution: "@pkgjs/parseargs@npm:0.11.0"
+  checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f
+  languageName: node
+  linkType: hard
+
 "@rjsf/core@npm:^5.1.0":
   version: 5.13.0
   resolution: "@rjsf/core@npm:5.13.0"
@@ -624,6 +734,41 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@sindresorhus/is@npm:^4.0.0":
+  version: 4.6.0
+  resolution: "@sindresorhus/is@npm:4.6.0"
+  checksum: 83839f13da2c29d55c97abc3bc2c55b250d33a0447554997a85c539e058e57b8da092da396e252b11ec24a0279a0bed1f537fa26302209327060643e327f81d2
+  languageName: node
+  linkType: hard
+
+"@szmarczak/http-timer@npm:^4.0.5":
+  version: 4.0.6
+  resolution: "@szmarczak/http-timer@npm:4.0.6"
+  dependencies:
+    defer-to-connect: ^2.0.0
+  checksum: c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95
+  languageName: node
+  linkType: hard
+
+"@types/cacheable-request@npm:^6.0.1":
+  version: 6.0.3
+  resolution: "@types/cacheable-request@npm:6.0.3"
+  dependencies:
+    "@types/http-cache-semantics": "*"
+    "@types/keyv": ^3.1.4
+    "@types/node": "*"
+    "@types/responselike": ^1.0.0
+  checksum: d9b26403fe65ce6b0cb3720b7030104c352bcb37e4fac2a7089a25a97de59c355fa08940658751f2f347a8512aa9d18fdb66ab3ade835975b2f454f2d5befbd9
+  languageName: node
+  linkType: hard
+
+"@types/emscripten@npm:^1.39.6":
+  version: 1.39.7
+  resolution: "@types/emscripten@npm:1.39.7"
+  checksum: 9871e4495358cc06cc45b2798022cd097d8ac2eb5b2fae7c276c6c5cadea05507150fad053c73ed346d4cbd844c50a3438604e5d7c3c2a7446b703cacb1ce172
+  languageName: node
+  linkType: hard
+
 "@types/eslint-scope@npm:^3.7.3":
   version: 3.7.4
   resolution: "@types/eslint-scope@npm:3.7.4"
@@ -651,6 +796,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/http-cache-semantics@npm:*":
+  version: 4.0.2
+  resolution: "@types/http-cache-semantics@npm:4.0.2"
+  checksum: 513429786a45d8124f93cc7ea1454b692008190ef743e9fec75a6a3c998309782d216f1e67d7d497ffece9c9212310ae05a8c56e8955492ee400eacdd7620e61
+  languageName: node
+  linkType: hard
+
 "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
   version: 7.0.13
   resolution: "@types/json-schema@npm:7.0.13"
@@ -658,6 +810,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/keyv@npm:^3.1.4":
+  version: 3.1.4
+  resolution: "@types/keyv@npm:3.1.4"
+  dependencies:
+    "@types/node": "*"
+  checksum: e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d
+  languageName: node
+  linkType: hard
+
 "@types/node@npm:*":
   version: 20.6.2
   resolution: "@types/node@npm:20.6.2"
@@ -683,6 +844,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/responselike@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "@types/responselike@npm:1.0.0"
+  dependencies:
+    "@types/node": "*"
+  checksum: e99fc7cc6265407987b30deda54c1c24bb1478803faf6037557a774b2f034c5b097ffd65847daa87e82a61a250d919f35c3588654b0fdaa816906650f596d1b0
+  languageName: node
+  linkType: hard
+
 "@types/scheduler@npm:*":
   version: 0.16.3
   resolution: "@types/scheduler@npm:0.16.3"
@@ -690,6 +860,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/semver@npm:^7.1.0":
+  version: 7.5.2
+  resolution: "@types/semver@npm:7.5.2"
+  checksum: 743aa8a2b58e20b329c19bd2459152cb049d12fafab7279b90ac11e0f268c97efbcb606ea0c681cca03f79015381b40d9b1244349b354270bec3f939ed49f6e9
+  languageName: node
+  linkType: hard
+
 "@types/source-list-map@npm:*":
   version: 0.1.2
   resolution: "@types/source-list-map@npm:0.1.2"
@@ -697,6 +874,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/treeify@npm:^1.0.0":
+  version: 1.0.1
+  resolution: "@types/treeify@npm:1.0.1"
+  checksum: 972022f753b49b16a17cbfc0c6051c64983d333f27dbf456353c8168446fa8948ac11072687be8ba5b2144122bae461481ce7814c7a855ab477fffe45ed7a793
+  languageName: node
+  linkType: hard
+
 "@types/webpack-sources@npm:^0.1.5":
   version: 0.1.9
   resolution: "@types/webpack-sources@npm:0.1.9"
@@ -906,10 +1090,87 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@yarnpkg/lockfile@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "@yarnpkg/lockfile@npm:1.1.0"
-  checksum: 05b881b4866a3546861fee756e6d3812776ea47fa6eb7098f983d6d0eefa02e12b66c3fff931574120f196286a7ad4879ce02743c8bb2be36c6a576c7852083a
+"@yarnpkg/core@npm:^4.0.0-rc.37":
+  version: 4.0.0-rc.51
+  resolution: "@yarnpkg/core@npm:4.0.0-rc.51"
+  dependencies:
+    "@arcanis/slice-ansi": ^1.1.1
+    "@types/semver": ^7.1.0
+    "@types/treeify": ^1.0.0
+    "@yarnpkg/fslib": ^3.0.0-rc.51
+    "@yarnpkg/libzip": ^3.0.0-rc.51
+    "@yarnpkg/parsers": ^3.0.0-rc.51
+    "@yarnpkg/shell": ^4.0.0-rc.51
+    camelcase: ^5.3.1
+    chalk: ^3.0.0
+    ci-info: ^3.2.0
+    clipanion: ^4.0.0-rc.2
+    cross-spawn: 7.0.3
+    diff: ^5.1.0
+    dotenv: ^16.3.1
+    fast-glob: ^3.2.2
+    got: ^11.7.0
+    lodash: ^4.17.15
+    micromatch: ^4.0.2
+    p-limit: ^2.2.0
+    semver: ^7.1.2
+    strip-ansi: ^6.0.0
+    tar: ^6.0.5
+    tinylogic: ^2.0.0
+    treeify: ^1.1.0
+    tslib: ^2.4.0
+    tunnel: ^0.0.6
+  checksum: 8dcb6c764f0f6ef0c8bdeec9063557d927ce8e8c697ebe0d78475945bd613d6759943909ab16a5b97ab7660258a41de5a49beb6d557d9ff554ee8899b7ae5594
+  languageName: node
+  linkType: hard
+
+"@yarnpkg/fslib@npm:^3.0.0-rc.51":
+  version: 3.0.0-rc.51
+  resolution: "@yarnpkg/fslib@npm:3.0.0-rc.51"
+  dependencies:
+    tslib: ^2.4.0
+  checksum: e3fddbd810dfb402381ef5b8cfa7833a382cee191961ead3b0ff780cbac726d3d7f7d0238672db26315aeca2df2782f7a2059ff15e161826fadbbba77d2f2fe7
+  languageName: node
+  linkType: hard
+
+"@yarnpkg/libzip@npm:^3.0.0-rc.51":
+  version: 3.0.0-rc.51
+  resolution: "@yarnpkg/libzip@npm:3.0.0-rc.51"
+  dependencies:
+    "@types/emscripten": ^1.39.6
+    "@yarnpkg/fslib": ^3.0.0-rc.51
+    tslib: ^2.4.0
+  peerDependencies:
+    "@yarnpkg/fslib": ^3.0.0-rc.51
+  checksum: 481a4809c2af6a056d6bcd872af81010c04727d6baef8f360cc0e7a63d4b787a40743b770669f2f07a6f8c599f81c42a2c301f44a05401a5f25e304af78cd3ba
+  languageName: node
+  linkType: hard
+
+"@yarnpkg/parsers@npm:^3.0.0-rc.37, @yarnpkg/parsers@npm:^3.0.0-rc.51":
+  version: 3.0.0-rc.51
+  resolution: "@yarnpkg/parsers@npm:3.0.0-rc.51"
+  dependencies:
+    js-yaml: ^3.10.0
+    tslib: ^2.4.0
+  checksum: 35748fada41e45ada477e9e9da67e5e79acf895d4170d500966855a147912103e5bf33f3a658ad08a60ed22f21be05c5cfe13e342145c074b8ef6dd34b14572e
+  languageName: node
+  linkType: hard
+
+"@yarnpkg/shell@npm:^4.0.0-rc.51":
+  version: 4.0.0-rc.51
+  resolution: "@yarnpkg/shell@npm:4.0.0-rc.51"
+  dependencies:
+    "@yarnpkg/fslib": ^3.0.0-rc.51
+    "@yarnpkg/parsers": ^3.0.0-rc.51
+    chalk: ^3.0.0
+    clipanion: ^4.0.0-rc.2
+    cross-spawn: 7.0.3
+    fast-glob: ^3.2.2
+    micromatch: ^4.0.2
+    tslib: ^2.4.0
+  bin:
+    shell: ./lib/cli.js
+  checksum: 2703a6fbfe9a8df44907028b0af9e5e8571759e8917c50f67fb49f47538fb741b7be94a0d323f67df1e11d0dabde4ec4fd58c2b01927a192fdf0978832c8bfa4
   languageName: node
   linkType: hard
 
@@ -996,6 +1257,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"ansi-regex@npm:^5.0.1":
+  version: 5.0.1
+  resolution: "ansi-regex@npm:5.0.1"
+  checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b
+  languageName: node
+  linkType: hard
+
+"ansi-regex@npm:^6.0.1":
+  version: 6.0.1
+  resolution: "ansi-regex@npm:6.0.1"
+  checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169
+  languageName: node
+  linkType: hard
+
 "ansi-styles@npm:^3.2.1":
   version: 3.2.1
   resolution: "ansi-styles@npm:3.2.1"
@@ -1005,6 +1280,31 @@ __metadata:
   languageName: node
   linkType: hard
 
+"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0":
+  version: 4.3.0
+  resolution: "ansi-styles@npm:4.3.0"
+  dependencies:
+    color-convert: ^2.0.1
+  checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4
+  languageName: node
+  linkType: hard
+
+"ansi-styles@npm:^6.1.0":
+  version: 6.2.1
+  resolution: "ansi-styles@npm:6.2.1"
+  checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9
+  languageName: node
+  linkType: hard
+
+"argparse@npm:^1.0.7":
+  version: 1.0.10
+  resolution: "argparse@npm:1.0.10"
+  dependencies:
+    sprintf-js: ~1.0.2
+  checksum: 7ca6e45583a28de7258e39e13d81e925cfa25d7d4aacbf806a382d3c02fcb13403a07fb8aeef949f10a7cfe4a62da0e2e807b348a5980554cc28ee573ef95945
+  languageName: node
+  linkType: hard
+
 "array-buffer-byte-length@npm:^1.0.0":
   version: 1.0.0
   resolution: "array-buffer-byte-length@npm:1.0.0"
@@ -1061,6 +1361,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"brace-expansion@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "brace-expansion@npm:2.0.1"
+  dependencies:
+    balanced-match: ^1.0.0
+  checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1
+  languageName: node
+  linkType: hard
+
+"braces@npm:^3.0.2":
+  version: 3.0.2
+  resolution: "braces@npm:3.0.2"
+  dependencies:
+    fill-range: ^7.0.1
+  checksum: e2a8e769a863f3d4ee887b5fe21f63193a891c68b612ddb4b68d82d1b5f3ff9073af066c343e9867a393fe4c2555dcb33e89b937195feb9c1613d259edfcd459
+  languageName: node
+  linkType: hard
+
 "browserslist@npm:^4.14.5":
   version: 4.21.10
   resolution: "browserslist@npm:4.21.10"
@@ -1082,6 +1400,28 @@ __metadata:
   languageName: node
   linkType: hard
 
+"cacheable-lookup@npm:^5.0.3":
+  version: 5.0.4
+  resolution: "cacheable-lookup@npm:5.0.4"
+  checksum: 763e02cf9196bc9afccacd8c418d942fc2677f22261969a4c2c2e760fa44a2351a81557bd908291c3921fe9beb10b976ba8fa50c5ca837c5a0dd945f16468f2d
+  languageName: node
+  linkType: hard
+
+"cacheable-request@npm:^7.0.2":
+  version: 7.0.4
+  resolution: "cacheable-request@npm:7.0.4"
+  dependencies:
+    clone-response: ^1.0.2
+    get-stream: ^5.1.0
+    http-cache-semantics: ^4.0.0
+    keyv: ^4.0.0
+    lowercase-keys: ^2.0.0
+    normalize-url: ^6.0.1
+    responselike: ^2.0.0
+  checksum: 0de9df773fd4e7dd9bd118959878f8f2163867e2e1ab3575ffbecbe6e75e80513dd0c68ba30005e5e5a7b377cc6162bbc00ab1db019bb4e9cb3c2f3f7a6f1ee4
+  languageName: node
+  linkType: hard
+
 "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2":
   version: 1.0.2
   resolution: "call-bind@npm:1.0.2"
@@ -1092,6 +1432,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"camelcase@npm:^5.3.1":
+  version: 5.3.1
+  resolution: "camelcase@npm:5.3.1"
+  checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b
+  languageName: node
+  linkType: hard
+
 "caniuse-lite@npm:^1.0.30001517":
   version: 1.0.30001538
   resolution: "caniuse-lite@npm:1.0.30001538"
@@ -1110,6 +1457,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"chalk@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "chalk@npm:3.0.0"
+  dependencies:
+    ansi-styles: ^4.1.0
+    supports-color: ^7.1.0
+  checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505
+  languageName: node
+  linkType: hard
+
+"chownr@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "chownr@npm:2.0.0"
+  checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f
+  languageName: node
+  linkType: hard
+
 "chrome-trace-event@npm:^1.0.2":
   version: 1.0.3
   resolution: "chrome-trace-event@npm:1.0.3"
@@ -1117,6 +1481,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"ci-info@npm:^3.2.0":
+  version: 3.8.0
+  resolution: "ci-info@npm:3.8.0"
+  checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098
+  languageName: node
+  linkType: hard
+
+"clipanion@npm:^4.0.0-rc.2":
+  version: 4.0.0-rc.2
+  resolution: "clipanion@npm:4.0.0-rc.2"
+  dependencies:
+    typanion: ^3.8.0
+  peerDependencies:
+    typanion: "*"
+  checksum: 533a2a0fd2d18b0599afc8bb01452f87bdc3a6b5481c8c02d35821a01fba0a407a2057eb52168cb57c867a025adebe976d19327ce361390886d9850075805bb4
+  languageName: node
+  linkType: hard
+
 "clone-deep@npm:^4.0.1":
   version: 4.0.1
   resolution: "clone-deep@npm:4.0.1"
@@ -1128,6 +1510,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"clone-response@npm:^1.0.2":
+  version: 1.0.3
+  resolution: "clone-response@npm:1.0.3"
+  dependencies:
+    mimic-response: ^1.0.0
+  checksum: 4e671cac39b11c60aa8ba0a450657194a5d6504df51bca3fac5b3bd0145c4f8e8464898f87c8406b83232e3bc5cca555f51c1f9c8ac023969ebfbf7f6bdabb2e
+  languageName: node
+  linkType: hard
+
 "color-convert@npm:^1.9.0":
   version: 1.9.3
   resolution: "color-convert@npm:1.9.3"
@@ -1137,6 +1528,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"color-convert@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "color-convert@npm:2.0.1"
+  dependencies:
+    color-name: ~1.1.4
+  checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336
+  languageName: node
+  linkType: hard
+
 "color-name@npm:1.1.3":
   version: 1.1.3
   resolution: "color-name@npm:1.1.3"
@@ -1144,6 +1544,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"color-name@npm:~1.1.4":
+  version: 1.1.4
+  resolution: "color-name@npm:1.1.4"
+  checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610
+  languageName: node
+  linkType: hard
+
 "colorette@npm:^2.0.14":
   version: 2.0.20
   resolution: "colorette@npm:2.0.20"
@@ -1202,6 +1609,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3":
+  version: 7.0.3
+  resolution: "cross-spawn@npm:7.0.3"
+  dependencies:
+    path-key: ^3.1.0
+    shebang-command: ^2.0.0
+    which: ^2.0.1
+  checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52
+  languageName: node
+  linkType: hard
+
 "cross-spawn@npm:^6.0.5":
   version: 6.0.5
   resolution: "cross-spawn@npm:6.0.5"
@@ -1215,17 +1633,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cross-spawn@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "cross-spawn@npm:7.0.3"
-  dependencies:
-    path-key: ^3.1.0
-    shebang-command: ^2.0.0
-    which: ^2.0.1
-  checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52
-  languageName: node
-  linkType: hard
-
 "css-loader@npm:^6.7.1":
   version: 6.8.1
   resolution: "css-loader@npm:6.8.1"
@@ -1253,20 +1660,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"csstype@npm:3.0.10":
+"csstype@npm:3.0.10, csstype@npm:^3.0.2":
   version: 3.0.10
   resolution: "csstype@npm:3.0.10"
   checksum: 20a8fa324f2b33ddf94aa7507d1b6ab3daa6f3cc308888dc50126585d7952f2471de69b2dbe0635d1fdc31223fef8e070842691877e725caf456e2378685a631
   languageName: node
   linkType: hard
 
-"csstype@npm:^3.0.2":
-  version: 3.1.2
-  resolution: "csstype@npm:3.1.2"
-  checksum: e1a52e6c25c1314d6beef5168da704ab29c5186b877c07d822bd0806717d9a265e8493a2e35ca7e68d0f5d472d43fac1cdce70fd79fd0853dff81f3028d857b5
-  languageName: node
-  linkType: hard
-
 "data-urls@npm:^2.0.0":
   version: 2.0.0
   resolution: "data-urls@npm:2.0.0"
@@ -1278,6 +1678,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"decompress-response@npm:^6.0.0":
+  version: 6.0.0
+  resolution: "decompress-response@npm:6.0.0"
+  dependencies:
+    mimic-response: ^3.1.0
+  checksum: d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812
+  languageName: node
+  linkType: hard
+
 "deepmerge@npm:^4.2.2":
   version: 4.3.1
   resolution: "deepmerge@npm:4.3.1"
@@ -1285,6 +1694,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"defer-to-connect@npm:^2.0.0":
+  version: 2.0.1
+  resolution: "defer-to-connect@npm:2.0.1"
+  checksum: 8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b
+  languageName: node
+  linkType: hard
+
 "define-data-property@npm:^1.0.1":
   version: 1.1.0
   resolution: "define-data-property@npm:1.1.0"
@@ -1307,6 +1723,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"diff@npm:^5.1.0":
+  version: 5.1.0
+  resolution: "diff@npm:5.1.0"
+  checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90
+  languageName: node
+  linkType: hard
+
 "dom-serializer@npm:^1.0.1":
   version: 1.4.1
   resolution: "dom-serializer@npm:1.4.1"
@@ -1345,6 +1768,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"dotenv@npm:^16.3.1":
+  version: 16.3.1
+  resolution: "dotenv@npm:16.3.1"
+  checksum: 15d75e7279018f4bafd0ee9706593dd14455ddb71b3bcba9c52574460b7ccaf67d5cf8b2c08a5af1a9da6db36c956a04a1192b101ee102a3e0cf8817bbcf3dfd
+  languageName: node
+  linkType: hard
+
 "duplicate-package-checker-webpack-plugin@npm:^3.0.0":
   version: 3.0.0
   resolution: "duplicate-package-checker-webpack-plugin@npm:3.0.0"
@@ -1357,6 +1787,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"eastasianwidth@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "eastasianwidth@npm:0.2.0"
+  checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed
+  languageName: node
+  linkType: hard
+
 "electron-to-chromium@npm:^1.4.477":
   version: 1.4.523
   resolution: "electron-to-chromium@npm:1.4.523"
@@ -1364,6 +1801,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"emoji-regex@npm:^8.0.0":
+  version: 8.0.0
+  resolution: "emoji-regex@npm:8.0.0"
+  checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192
+  languageName: node
+  linkType: hard
+
+"emoji-regex@npm:^9.2.2":
+  version: 9.2.2
+  resolution: "emoji-regex@npm:9.2.2"
+  checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601
+  languageName: node
+  linkType: hard
+
 "emojis-list@npm:^3.0.0":
   version: 3.0.0
   resolution: "emojis-list@npm:3.0.0"
@@ -1371,6 +1822,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"end-of-stream@npm:^1.1.0":
+  version: 1.4.4
+  resolution: "end-of-stream@npm:1.4.4"
+  dependencies:
+    once: ^1.4.0
+  checksum: 530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b
+  languageName: node
+  linkType: hard
+
 "enhanced-resolve@npm:^5.15.0":
   version: 5.15.0
   resolution: "enhanced-resolve@npm:5.15.0"
@@ -1513,6 +1973,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"esprima@npm:^4.0.0":
+  version: 4.0.1
+  resolution: "esprima@npm:4.0.1"
+  bin:
+    esparse: ./bin/esparse.js
+    esvalidate: ./bin/esvalidate.js
+  checksum: b45bc805a613dbea2835278c306b91aff6173c8d034223fa81498c77dcbce3b2931bf6006db816f62eacd9fd4ea975dfd85a5b7f3c6402cfd050d4ca3c13a628
+  languageName: node
+  linkType: hard
+
 "esrecurse@npm:^4.3.0":
   version: 4.3.0
   resolution: "esrecurse@npm:4.3.0"
@@ -1550,6 +2020,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fast-glob@npm:^3.2.2":
+  version: 3.3.1
+  resolution: "fast-glob@npm:3.3.1"
+  dependencies:
+    "@nodelib/fs.stat": ^2.0.2
+    "@nodelib/fs.walk": ^1.2.3
+    glob-parent: ^5.1.2
+    merge2: ^1.3.0
+    micromatch: ^4.0.4
+  checksum: b6f3add6403e02cf3a798bfbb1183d0f6da2afd368f27456010c0bc1f9640aea308243d4cb2c0ab142f618276e65ecb8be1661d7c62a7b4e5ba774b9ce5432e5
+  languageName: node
+  linkType: hard
+
 "fast-json-stable-stringify@npm:^2.0.0":
   version: 2.1.0
   resolution: "fast-json-stable-stringify@npm:2.1.0"
@@ -1564,6 +2047,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fastq@npm:^1.6.0":
+  version: 1.15.0
+  resolution: "fastq@npm:1.15.0"
+  dependencies:
+    reusify: ^1.0.4
+  checksum: 0170e6bfcd5d57a70412440b8ef600da6de3b2a6c5966aeaf0a852d542daff506a0ee92d6de7679d1de82e644bce69d7a574a6c93f0b03964b5337eed75ada1a
+  languageName: node
+  linkType: hard
+
+"fill-range@npm:^7.0.1":
+  version: 7.0.1
+  resolution: "fill-range@npm:7.0.1"
+  dependencies:
+    to-regex-range: ^5.0.1
+  checksum: cc283f4e65b504259e64fd969bcf4def4eb08d85565e906b7d36516e87819db52029a76b6363d0f02d0d532f0033c9603b9e2d943d56ee3b0d4f7ad3328ff917
+  languageName: node
+  linkType: hard
+
 "find-root@npm:^1.0.0":
   version: 1.1.0
   resolution: "find-root@npm:1.1.0"
@@ -1590,6 +2091,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"foreground-child@npm:^3.1.0":
+  version: 3.1.1
+  resolution: "foreground-child@npm:3.1.1"
+  dependencies:
+    cross-spawn: ^7.0.0
+    signal-exit: ^4.0.1
+  checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5
+  languageName: node
+  linkType: hard
+
 "free-style@npm:3.1.0":
   version: 3.1.0
   resolution: "free-style@npm:3.1.0"
@@ -1608,6 +2119,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fs-minipass@npm:^2.0.0":
+  version: 2.1.0
+  resolution: "fs-minipass@npm:2.1.0"
+  dependencies:
+    minipass: ^3.0.0
+  checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1
+  languageName: node
+  linkType: hard
+
 "fs.realpath@npm:^1.0.0":
   version: 1.0.0
   resolution: "fs.realpath@npm:1.0.0"
@@ -1653,6 +2173,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"get-stream@npm:^5.1.0":
+  version: 5.2.0
+  resolution: "get-stream@npm:5.2.0"
+  dependencies:
+    pump: ^3.0.0
+  checksum: 8bc1a23174a06b2b4ce600df38d6c98d2ef6d84e020c1ddad632ad75bac4e092eeb40e4c09e0761c35fc2dbc5e7fff5dab5e763a383582c4a167dd69a905bd12
+  languageName: node
+  linkType: hard
+
 "get-symbol-description@npm:^1.0.0":
   version: 1.0.0
   resolution: "get-symbol-description@npm:1.0.0"
@@ -1663,6 +2192,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"glob-parent@npm:^5.1.2":
+  version: 5.1.2
+  resolution: "glob-parent@npm:5.1.2"
+  dependencies:
+    is-glob: ^4.0.1
+  checksum: f4f2bfe2425296e8a47e36864e4f42be38a996db40420fe434565e4480e3322f18eb37589617a98640c5dc8fdec1a387007ee18dbb1f3f5553409c34d17f425e
+  languageName: node
+  linkType: hard
+
 "glob-to-regexp@npm:^0.4.1":
   version: 0.4.1
   resolution: "glob-to-regexp@npm:0.4.1"
@@ -1670,17 +2208,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"glob@npm:^7.1.3":
-  version: 7.2.3
-  resolution: "glob@npm:7.2.3"
+"glob@npm:^10.2.5":
+  version: 10.3.4
+  resolution: "glob@npm:10.3.4"
   dependencies:
-    fs.realpath: ^1.0.0
-    inflight: ^1.0.4
-    inherits: 2
-    minimatch: ^3.1.1
-    once: ^1.3.0
-    path-is-absolute: ^1.0.0
-  checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133
+    foreground-child: ^3.1.0
+    jackspeak: ^2.0.3
+    minimatch: ^9.0.1
+    minipass: ^5.0.0 || ^6.0.2 || ^7.0.0
+    path-scurry: ^1.10.1
+  bin:
+    glob: dist/cjs/src/bin.js
+  checksum: 176b97c124414401cb51329a93d2ba112cef8814adbed10348481916b9521b677773eee2691cb6b24d66632d8c8bb8913533f5ac4bfb2d0ef5454a1856082361
   languageName: node
   linkType: hard
 
@@ -1716,6 +2255,25 @@ __metadata:
   languageName: node
   linkType: hard
 
+"got@npm:^11.7.0":
+  version: 11.8.6
+  resolution: "got@npm:11.8.6"
+  dependencies:
+    "@sindresorhus/is": ^4.0.0
+    "@szmarczak/http-timer": ^4.0.5
+    "@types/cacheable-request": ^6.0.1
+    "@types/responselike": ^1.0.0
+    cacheable-lookup: ^5.0.3
+    cacheable-request: ^7.0.2
+    decompress-response: ^6.0.0
+    http2-wrapper: ^1.0.0-beta.5.2
+    lowercase-keys: ^2.0.0
+    p-cancelable: ^2.0.0
+    responselike: ^2.0.0
+  checksum: bbc783578a8d5030c8164ef7f57ce41b5ad7db2ed13371e1944bef157eeca5a7475530e07c0aaa71610d7085474d0d96222c9f4268d41db333a17e39b463f45d
+  languageName: node
+  linkType: hard
+
 "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.9":
   version: 4.2.11
   resolution: "graceful-fs@npm:4.2.11"
@@ -1723,6 +2281,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"grapheme-splitter@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "grapheme-splitter@npm:1.0.4"
+  checksum: 0c22ec54dee1b05cd480f78cf14f732cb5b108edc073572c4ec205df4cd63f30f8db8025afc5debc8835a8ddeacf648a1c7992fe3dcd6ad38f9a476d84906620
+  languageName: node
+  linkType: hard
+
 "has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2":
   version: 1.0.2
   resolution: "has-bigints@npm:1.0.2"
@@ -1804,6 +2369,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"http-cache-semantics@npm:^4.0.0":
+  version: 4.1.1
+  resolution: "http-cache-semantics@npm:4.1.1"
+  checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236
+  languageName: node
+  linkType: hard
+
+"http2-wrapper@npm:^1.0.0-beta.5.2":
+  version: 1.0.3
+  resolution: "http2-wrapper@npm:1.0.3"
+  dependencies:
+    quick-lru: ^5.1.1
+    resolve-alpn: ^1.0.0
+  checksum: 74160b862ec699e3f859739101ff592d52ce1cb207b7950295bf7962e4aa1597ef709b4292c673bece9c9b300efad0559fc86c71b1409c7a1e02b7229456003e
+  languageName: node
+  linkType: hard
+
 "iconv-lite@npm:^0.6.2":
   version: 0.6.3
   resolution: "iconv-lite@npm:0.6.3"
@@ -1931,6 +2513,29 @@ __metadata:
   languageName: node
   linkType: hard
 
+"is-extglob@npm:^2.1.1":
+  version: 2.1.1
+  resolution: "is-extglob@npm:2.1.1"
+  checksum: df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85
+  languageName: node
+  linkType: hard
+
+"is-fullwidth-code-point@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "is-fullwidth-code-point@npm:3.0.0"
+  checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348
+  languageName: node
+  linkType: hard
+
+"is-glob@npm:^4.0.1":
+  version: 4.0.3
+  resolution: "is-glob@npm:4.0.3"
+  dependencies:
+    is-extglob: ^2.1.1
+  checksum: d381c1319fcb69d341cc6e6c7cd588e17cd94722d9a32dbd60660b993c4fb7d0f19438674e68dfec686d09b7c73139c9166b47597f846af387450224a8101ab4
+  languageName: node
+  linkType: hard
+
 "is-negative-zero@npm:^2.0.2":
   version: 2.0.2
   resolution: "is-negative-zero@npm:2.0.2"
@@ -1947,6 +2552,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"is-number@npm:^7.0.0":
+  version: 7.0.0
+  resolution: "is-number@npm:7.0.0"
+  checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a
+  languageName: node
+  linkType: hard
+
 "is-plain-object@npm:^2.0.4":
   version: 2.0.4
   resolution: "is-plain-object@npm:2.0.4"
@@ -2046,6 +2658,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"jackspeak@npm:^2.0.3":
+  version: 2.3.3
+  resolution: "jackspeak@npm:2.3.3"
+  dependencies:
+    "@isaacs/cliui": ^8.0.2
+    "@pkgjs/parseargs": ^0.11.0
+  dependenciesMeta:
+    "@pkgjs/parseargs":
+      optional: true
+  checksum: 4313a7c0cc44c7753c4cb9869935f0b06f4cf96827515f63f58ff46b3d2f6e29aba6b3b5151778397c3f5ae67ef8bfc48871967bd10343c27e90cff198ec7808
+  languageName: node
+  linkType: hard
+
 "jest-worker@npm:^27.4.5":
   version: 27.5.1
   resolution: "jest-worker@npm:27.5.1"
@@ -2064,6 +2689,25 @@ __metadata:
   languageName: node
   linkType: hard
 
+"js-yaml@npm:^3.10.0":
+  version: 3.14.1
+  resolution: "js-yaml@npm:3.14.1"
+  dependencies:
+    argparse: ^1.0.7
+    esprima: ^4.0.0
+  bin:
+    js-yaml: bin/js-yaml.js
+  checksum: bef146085f472d44dee30ec34e5cf36bf89164f5d585435a3d3da89e52622dff0b188a580e4ad091c3341889e14cb88cac6e4deb16dc5b1e9623bb0601fc255c
+  languageName: node
+  linkType: hard
+
+"json-buffer@npm:3.0.1":
+  version: 3.0.1
+  resolution: "json-buffer@npm:3.0.1"
+  checksum: 9026b03edc2847eefa2e37646c579300a1f3a4586cfb62bf857832b60c852042d0d6ae55d1afb8926163fa54c2b01d83ae24705f34990348bdac6273a29d4581
+  languageName: node
+  linkType: hard
+
 "json-parse-better-errors@npm:^1.0.1":
   version: 1.0.2
   resolution: "json-parse-better-errors@npm:1.0.2"
@@ -2141,6 +2785,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"keyv@npm:^4.0.0":
+  version: 4.5.3
+  resolution: "keyv@npm:4.5.3"
+  dependencies:
+    json-buffer: 3.0.1
+  checksum: 3ffb4d5b72b6b4b4af443bbb75ca2526b23c750fccb5ac4c267c6116888b4b65681015c2833cb20d26cf3e6e32dac6b988c77f7f022e1a571b7d90f1442257da
+  languageName: node
+  linkType: hard
+
 "kind-of@npm:^6.0.2":
   version: 6.0.3
   resolution: "kind-of@npm:6.0.3"
@@ -2226,7 +2879,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0":
+"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0":
   version: 4.17.21
   resolution: "lodash@npm:4.17.21"
   checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
@@ -2244,6 +2897,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lowercase-keys@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "lowercase-keys@npm:2.0.0"
+  checksum: 24d7ebd56ccdf15ff529ca9e08863f3c54b0b9d1edb97a3ae1af34940ae666c01a1e6d200707bce730a8ef76cb57cc10e65f245ecaaf7e6bc8639f2fb460ac23
+  languageName: node
+  linkType: hard
+
 "lru-cache@npm:^6.0.0":
   version: 6.0.0
   resolution: "lru-cache@npm:6.0.0"
@@ -2253,6 +2913,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lru-cache@npm:^9.1.1 || ^10.0.0":
+  version: 10.0.1
+  resolution: "lru-cache@npm:10.0.1"
+  checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181
+  languageName: node
+  linkType: hard
+
 "markdown-to-jsx@npm:^7.3.2":
   version: 7.3.2
   resolution: "markdown-to-jsx@npm:7.3.2"
@@ -2276,6 +2943,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"merge2@npm:^1.3.0":
+  version: 1.4.1
+  resolution: "merge2@npm:1.4.1"
+  checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2
+  languageName: node
+  linkType: hard
+
+"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4":
+  version: 4.0.5
+  resolution: "micromatch@npm:4.0.5"
+  dependencies:
+    braces: ^3.0.2
+    picomatch: ^2.3.1
+  checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc
+  languageName: node
+  linkType: hard
+
 "mime-db@npm:1.52.0":
   version: 1.52.0
   resolution: "mime-db@npm:1.52.0"
@@ -2292,6 +2976,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"mimic-response@npm:^1.0.0":
+  version: 1.0.1
+  resolution: "mimic-response@npm:1.0.1"
+  checksum: 034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823
+  languageName: node
+  linkType: hard
+
+"mimic-response@npm:^3.1.0":
+  version: 3.1.0
+  resolution: "mimic-response@npm:3.1.0"
+  checksum: 25739fee32c17f433626bf19f016df9036b75b3d84a3046c7d156e72ec963dd29d7fc8a302f55a3d6c5a4ff24259676b15d915aad6480815a969ff2ec0836867
+  languageName: node
+  linkType: hard
+
 "mini-css-extract-plugin@npm:^2.7.0":
   version: 2.7.6
   resolution: "mini-css-extract-plugin@npm:2.7.6"
@@ -2312,7 +3010,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1":
+"minimatch@npm:^3.0.4":
   version: 3.1.2
   resolution: "minimatch@npm:3.1.2"
   dependencies:
@@ -2321,6 +3019,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"minimatch@npm:^9.0.1":
+  version: 9.0.3
+  resolution: "minimatch@npm:9.0.3"
+  dependencies:
+    brace-expansion: ^2.0.1
+  checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5
+  languageName: node
+  linkType: hard
+
 "minimist@npm:~1.2.0":
   version: 1.2.8
   resolution: "minimist@npm:1.2.8"
@@ -2328,6 +3035,41 @@ __metadata:
   languageName: node
   linkType: hard
 
+"minipass@npm:^3.0.0":
+  version: 3.3.6
+  resolution: "minipass@npm:3.3.6"
+  dependencies:
+    yallist: ^4.0.0
+  checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48
+  languageName: node
+  linkType: hard
+
+"minipass@npm:^5.0.0, minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0":
+  version: 5.0.0
+  resolution: "minipass@npm:5.0.0"
+  checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea
+  languageName: node
+  linkType: hard
+
+"minizlib@npm:^2.1.1":
+  version: 2.1.2
+  resolution: "minizlib@npm:2.1.2"
+  dependencies:
+    minipass: ^3.0.0
+    yallist: ^4.0.0
+  checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3
+  languageName: node
+  linkType: hard
+
+"mkdirp@npm:^1.0.3":
+  version: 1.0.4
+  resolution: "mkdirp@npm:1.0.4"
+  bin:
+    mkdirp: bin/cmd.js
+  checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f
+  languageName: node
+  linkType: hard
+
 "nanoid@npm:^3.3.6":
   version: 3.3.6
   resolution: "nanoid@npm:3.3.6"
@@ -2370,6 +3112,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"normalize-url@npm:^6.0.1":
+  version: 6.1.0
+  resolution: "normalize-url@npm:6.1.0"
+  checksum: 4a4944631173e7d521d6b80e4c85ccaeceb2870f315584fa30121f505a6dfd86439c5e3fdd8cd9e0e291290c41d0c3599f0cb12ab356722ed242584c30348e50
+  languageName: node
+  linkType: hard
+
 "npm-run-all@npm:^4.1.5":
   version: 4.1.5
   resolution: "npm-run-all@npm:4.1.5"
@@ -2424,7 +3173,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"once@npm:^1.3.0":
+"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0":
   version: 1.4.0
   resolution: "once@npm:1.4.0"
   dependencies:
@@ -2433,6 +3182,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"p-cancelable@npm:^2.0.0":
+  version: 2.1.1
+  resolution: "p-cancelable@npm:2.1.1"
+  checksum: 3dba12b4fb4a1e3e34524535c7858fc82381bbbd0f247cc32dedc4018592a3950ce66b106d0880b4ec4c2d8d6576f98ca885dc1d7d0f274d1370be20e9523ddf
+  languageName: node
+  linkType: hard
+
 "p-limit@npm:^2.2.0":
   version: 2.3.0
   resolution: "p-limit@npm:2.3.0"
@@ -2517,6 +3273,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"path-scurry@npm:^1.10.1":
+  version: 1.10.1
+  resolution: "path-scurry@npm:1.10.1"
+  dependencies:
+    lru-cache: ^9.1.1 || ^10.0.0
+    minipass: ^5.0.0 || ^6.0.2 || ^7.0.0
+  checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90
+  languageName: node
+  linkType: hard
+
 "path-type@npm:^3.0.0":
   version: 3.0.0
   resolution: "path-type@npm:3.0.0"
@@ -2533,6 +3299,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"picomatch@npm:^2.3.1":
+  version: 2.3.1
+  resolution: "picomatch@npm:2.3.1"
+  checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf
+  languageName: node
+  linkType: hard
+
 "pidtree@npm:^0.3.0":
   version: 0.3.1
   resolution: "pidtree@npm:0.3.1"
@@ -2630,6 +3403,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"prettier@npm:^3.0.3":
+  version: 3.0.3
+  resolution: "prettier@npm:3.0.3"
+  bin:
+    prettier: bin/prettier.cjs
+  checksum: e10b9af02b281f6c617362ebd2571b1d7fc9fb8a3bd17e371754428cda992e5e8d8b7a046e8f7d3e2da1dcd21aa001e2e3c797402ebb6111b5cd19609dd228e0
+  languageName: node
+  linkType: hard
+
 "process@npm:^0.11.10":
   version: 0.11.10
   resolution: "process@npm:0.11.10"
@@ -2648,6 +3430,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"pump@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "pump@npm:3.0.0"
+  dependencies:
+    end-of-stream: ^1.1.0
+    once: ^1.3.1
+  checksum: e42e9229fba14732593a718b04cb5e1cfef8254544870997e0ecd9732b189a48e1256e4e5478148ecb47c8511dca2b09eae56b4d0aad8009e6fac8072923cfc9
+  languageName: node
+  linkType: hard
+
 "punycode@npm:^2.1.0, punycode@npm:^2.1.1":
   version: 2.3.0
   resolution: "punycode@npm:2.3.0"
@@ -2662,6 +3454,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"queue-microtask@npm:^1.2.2":
+  version: 1.2.3
+  resolution: "queue-microtask@npm:1.2.3"
+  checksum: b676f8c040cdc5b12723ad2f91414d267605b26419d5c821ff03befa817ddd10e238d22b25d604920340fd73efd8ba795465a0377c4adf45a4a41e4234e42dc4
+  languageName: node
+  linkType: hard
+
+"quick-lru@npm:^5.1.1":
+  version: 5.1.1
+  resolution: "quick-lru@npm:5.1.1"
+  checksum: a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed
+  languageName: node
+  linkType: hard
+
 "randombytes@npm:^2.1.0":
   version: 2.1.0
   resolution: "randombytes@npm:2.1.0"
@@ -2751,6 +3557,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"resolve-alpn@npm:^1.0.0":
+  version: 1.2.1
+  resolution: "resolve-alpn@npm:1.2.1"
+  checksum: f558071fcb2c60b04054c99aebd572a2af97ef64128d59bef7ab73bd50d896a222a056de40ffc545b633d99b304c259ea9d0c06830d5c867c34f0bfa60b8eae0
+  languageName: node
+  linkType: hard
+
 "resolve-cwd@npm:^3.0.0":
   version: 3.0.0
   resolution: "resolve-cwd@npm:3.0.0"
@@ -2793,14 +3606,39 @@ __metadata:
   languageName: node
   linkType: hard
 
-"rimraf@npm:^3.0.2":
-  version: 3.0.2
-  resolution: "rimraf@npm:3.0.2"
+"responselike@npm:^2.0.0":
+  version: 2.0.1
+  resolution: "responselike@npm:2.0.1"
+  dependencies:
+    lowercase-keys: ^2.0.0
+  checksum: b122535466e9c97b55e69c7f18e2be0ce3823c5d47ee8de0d9c0b114aa55741c6db8bfbfce3766a94d1272e61bfb1ebf0a15e9310ac5629fbb7446a861b4fd3a
+  languageName: node
+  linkType: hard
+
+"reusify@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "reusify@npm:1.0.4"
+  checksum: c3076ebcc22a6bc252cb0b9c77561795256c22b757f40c0d8110b1300723f15ec0fc8685e8d4ea6d7666f36c79ccc793b1939c748bf36f18f542744a4e379fcc
+  languageName: node
+  linkType: hard
+
+"rimraf@npm:^5.0.1":
+  version: 5.0.1
+  resolution: "rimraf@npm:5.0.1"
   dependencies:
-    glob: ^7.1.3
+    glob: ^10.2.5
   bin:
-    rimraf: bin.js
-  checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0
+    rimraf: dist/cjs/src/bin.js
+  checksum: bafce85391349a2d960847980bf9b5caa2a8887f481af630f1ea27e08288217293cec72d75e9a2ba35495c212789f66a7f3d23366ba6197026ab71c535126857
+  languageName: node
+  linkType: hard
+
+"run-parallel@npm:^1.1.9":
+  version: 1.2.0
+  resolution: "run-parallel@npm:1.2.0"
+  dependencies:
+    queue-microtask: ^1.2.2
+  checksum: cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d
   languageName: node
   linkType: hard
 
@@ -2907,7 +3745,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"semver@npm:^7.3.8, semver@npm:^7.5.0":
+"semver@npm:^7.1.2, semver@npm:^7.3.8":
   version: 7.5.4
   resolution: "semver@npm:7.5.4"
   dependencies:
@@ -2997,6 +3835,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"signal-exit@npm:^4.0.1":
+  version: 4.1.0
+  resolution: "signal-exit@npm:4.1.0"
+  checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549
+  languageName: node
+  linkType: hard
+
 "source-list-map@npm:^2.0.0":
   version: 2.0.1
   resolution: "source-list-map@npm:2.0.1"
@@ -3077,6 +3922,35 @@ __metadata:
   languageName: node
   linkType: hard
 
+"sprintf-js@npm:~1.0.2":
+  version: 1.0.3
+  resolution: "sprintf-js@npm:1.0.3"
+  checksum: 19d79aec211f09b99ec3099b5b2ae2f6e9cdefe50bc91ac4c69144b6d3928a640bb6ae5b3def70c2e85a2c3d9f5ec2719921e3a59d3ca3ef4b2fd1a4656a0df3
+  languageName: node
+  linkType: hard
+
+"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
+  version: 4.2.3
+  resolution: "string-width@npm:4.2.3"
+  dependencies:
+    emoji-regex: ^8.0.0
+    is-fullwidth-code-point: ^3.0.0
+    strip-ansi: ^6.0.1
+  checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb
+  languageName: node
+  linkType: hard
+
+"string-width@npm:^5.0.1, string-width@npm:^5.1.2":
+  version: 5.1.2
+  resolution: "string-width@npm:5.1.2"
+  dependencies:
+    eastasianwidth: ^0.2.0
+    emoji-regex: ^9.2.2
+    strip-ansi: ^7.0.1
+  checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193
+  languageName: node
+  linkType: hard
+
 "string.prototype.padend@npm:^3.0.0":
   version: 3.1.5
   resolution: "string.prototype.padend@npm:3.1.5"
@@ -3121,6 +3995,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
+  version: 6.0.1
+  resolution: "strip-ansi@npm:6.0.1"
+  dependencies:
+    ansi-regex: ^5.0.1
+  checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c
+  languageName: node
+  linkType: hard
+
+"strip-ansi@npm:^7.0.1":
+  version: 7.1.0
+  resolution: "strip-ansi@npm:7.1.0"
+  dependencies:
+    ansi-regex: ^6.0.1
+  checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d
+  languageName: node
+  linkType: hard
+
 "strip-bom@npm:^3.0.0":
   version: 3.0.0
   resolution: "strip-bom@npm:3.0.0"
@@ -3146,7 +4038,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"supports-color@npm:^7.2.0":
+"supports-color@npm:^7.1.0, supports-color@npm:^7.2.0":
   version: 7.2.0
   resolution: "supports-color@npm:7.2.0"
   dependencies:
@@ -3178,6 +4070,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tar@npm:^6.0.5":
+  version: 6.2.0
+  resolution: "tar@npm:6.2.0"
+  dependencies:
+    chownr: ^2.0.0
+    fs-minipass: ^2.0.0
+    minipass: ^5.0.0
+    minizlib: ^2.1.1
+    mkdirp: ^1.0.3
+    yallist: ^4.0.0
+  checksum: db4d9fe74a2082c3a5016630092c54c8375ff3b280186938cfd104f2e089c4fd9bad58688ef6be9cf186a889671bf355c7cda38f09bbf60604b281715ca57f5c
+  languageName: node
+  linkType: hard
+
 "terser-webpack-plugin@npm:^5.3.7":
   version: 5.3.9
   resolution: "terser-webpack-plugin@npm:5.3.9"
@@ -3214,6 +4120,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tinylogic@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "tinylogic@npm:2.0.0"
+  checksum: b966cbb41241a048095fb9e685d5e2020475fdea2c65b4ae51e5dee48964860a4505d987503c004b8a76e96b64c7da2f49954dd36c691d559c315d878ce7da29
+  languageName: node
+  linkType: hard
+
+"to-regex-range@npm:^5.0.1":
+  version: 5.0.1
+  resolution: "to-regex-range@npm:5.0.1"
+  dependencies:
+    is-number: ^7.0.0
+  checksum: f76fa01b3d5be85db6a2a143e24df9f60dd047d151062d0ba3df62953f2f697b16fe5dad9b0ac6191c7efc7b1d9dcaa4b768174b7b29da89d4428e64bc0a20ed
+  languageName: node
+  linkType: hard
+
 "tr46@npm:^2.1.0":
   version: 2.1.0
   resolution: "tr46@npm:2.1.0"
@@ -3223,13 +4145,34 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tslib@npm:^2.5.0":
+"treeify@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "treeify@npm:1.1.0"
+  checksum: aa00dded220c1dd052573bd6fc2c52862f09870851a284f0d3650d72bf913ba9b4f6b824f4f1ab81899bae29375f4266b07fe47cbf82343a1efa13cc09ce87af
+  languageName: node
+  linkType: hard
+
+"tslib@npm:^2.4.0, tslib@npm:^2.4.1":
   version: 2.6.2
   resolution: "tslib@npm:2.6.2"
   checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
   languageName: node
   linkType: hard
 
+"tunnel@npm:^0.0.6":
+  version: 0.0.6
+  resolution: "tunnel@npm:0.0.6"
+  checksum: c362948df9ad34b649b5585e54ce2838fa583aa3037091aaed66793c65b423a264e5229f0d7e9a95513a795ac2bd4cb72cda7e89a74313f182c1e9ae0b0994fa
+  languageName: node
+  linkType: hard
+
+"typanion@npm:^3.8.0":
+  version: 3.14.0
+  resolution: "typanion@npm:3.14.0"
+  checksum: fc0590d02c13c659eb1689e8adf7777e6c00dc911377e44cd36fe1b1271cfaca71547149f12cdc275058c0de5562a14e5273adbae66d47e6e0320e36007f5912
+  languageName: node
+  linkType: hard
+
 "typed-array-buffer@npm:^1.0.0":
   version: 1.0.0
   resolution: "typed-array-buffer@npm:1.0.0"
@@ -3277,23 +4220,23 @@ __metadata:
   languageName: node
   linkType: hard
 
-"typescript@npm:~4.8.4":
-  version: 4.8.4
-  resolution: "typescript@npm:4.8.4"
+"typescript@npm:~5.2.2":
+  version: 5.2.2
+  resolution: "typescript@npm:5.2.2"
   bin:
     tsc: bin/tsc
     tsserver: bin/tsserver
-  checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0
+  checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c
   languageName: node
   linkType: hard
 
-"typescript@patch:typescript@~4.8.4#~builtin<compat/typescript>":
-  version: 4.8.4
-  resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin<compat/typescript>::version=4.8.4&hash=1a91c8"
+"typescript@patch:typescript@~5.2.2#~builtin<compat/typescript>":
+  version: 5.2.2
+  resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin<compat/typescript>::version=5.2.2&hash=85af82"
   bin:
     tsc: bin/tsc
     tsserver: bin/tsserver
-  checksum: c981e82b77a5acdcc4e69af9c56cdecf5b934a87a08e7b52120596701e389a878b8e3f860e73ffb287bf649cc47a8c741262ce058148f71de4cdd88bb9c75153
+  checksum: 07106822b4305de3f22835cbba949a2b35451cad50888759b6818421290ff95d522b38ef7919e70fb381c5fe9c1c643d7dea22c8b31652a717ddbd57b7f4d554
   languageName: node
   linkType: hard
 
@@ -3614,6 +4557,28 @@ __metadata:
   languageName: node
   linkType: hard
 
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+  version: 7.0.0
+  resolution: "wrap-ansi@npm:7.0.0"
+  dependencies:
+    ansi-styles: ^4.0.0
+    string-width: ^4.1.0
+    strip-ansi: ^6.0.0
+  checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b
+  languageName: node
+  linkType: hard
+
+"wrap-ansi@npm:^8.1.0":
+  version: 8.1.0
+  resolution: "wrap-ansi@npm:8.1.0"
+  dependencies:
+    ansi-styles: ^6.1.0
+    string-width: ^5.0.1
+    strip-ansi: ^7.0.1
+  checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238
+  languageName: node
+  linkType: hard
+
 "wrappy@npm:1":
   version: 1.0.2
   resolution: "wrappy@npm:1.0.2"
@@ -3654,17 +4619,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"yarn-deduplicate@npm:^6.0.0":
-  version: 6.0.2
-  resolution: "yarn-deduplicate@npm:6.0.2"
+"yarn-berry-deduplicate@npm:^6.0.0":
+  version: 6.1.1
+  resolution: "yarn-berry-deduplicate@npm:6.1.1"
   dependencies:
-    "@yarnpkg/lockfile": ^1.1.0
-    commander: ^10.0.1
-    semver: ^7.5.0
-    tslib: ^2.5.0
+    "@yarnpkg/core": ^4.0.0-rc.37
+    "@yarnpkg/parsers": ^3.0.0-rc.37
+    commander: ^9.4.1
+    semver: ^7.3.8
+    tslib: ^2.4.1
   bin:
-    yarn-deduplicate: dist/cli.js
-  checksum: 2f6c38deaa1139f3a099069dc946a3800e5ba64410d1c45f516dc381e4b1619f0d4f7ad3b38a617e3a85d629ce42e5592105de7089a0da4d0198881ee5390947
+    yarn-berry-deduplicate: dist/cli.js
+  checksum: ca119b25df07d03ef7003a13de4d69dfc3e86662af61d24ec244ad28f1d137711678a0007630854c7d9c1ae78859d243ca5a9c9e88938ae67cda0235199436f9
   languageName: node
   linkType: hard
 
diff --git a/pyproject.toml b/pyproject.toml
index 3915e790..0b118b63 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ requires = [
     "hatch-jupyter-builder >=0.5",
     "hatch-nodejs-version",
     "hatchling >=1.4.0",
-    "jupyterlab >=3.4.7,<5.0.0",
+    "jupyterlab >=4.0.6,<5.0.0a0",
 ]
 
 
@@ -61,12 +61,19 @@ test = [
 # acceptance tests additionally require firefox and geckodriver
 acceptance = [
     "jupyter-server-proxy[test]",
-    "notebook <7",
     "robotframework-jupyterlibrary >=0.4.2",
-    # Work around https://github.com/robotframework/SeleniumLibrary/issues/1836
-    "selenium <4.10",
 ]
-
+classic = [
+    "jupyter-server <2",
+    "jupyterlab >=3.0.0,<4.0.0a0",
+    "notebook <7.0.0a0",
+]
+lab = [
+    "jupyter-server >=2",
+    "jupyterlab >=4.0.5,<5.0.0a0",
+    "nbclassic",
+    "notebook >=7",
+]
 
 [tool.hatch.version]
 source = "nodejs"
@@ -185,6 +192,7 @@ src = "labextension/package.json"
 
 [tool.pytest.ini_options]
 cache_dir = "build/.cache/pytest"
+testpaths = ["tests"]
 addopts = [
     "-vv",
     "--cov=jupyter_server_proxy",
@@ -192,6 +200,7 @@ addopts = [
     "--cov-context=test",
     "--cov-report=term-missing:skip-covered",
     "--cov-report=html:build/coverage",
+    "--no-cov-on-fail",
     "--html=build/pytest/index.html",
     "--color=yes",
 ]


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index b4a62fc6..b59c9a6b 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,5 +1,5 @@
 # This is a GitHub workflow defining a set of jobs with a set of steps.
-# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
+# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
 #
 name: Test
 
@@ -23,55 +23,81 @@ on:
       - "pre-commit-ci-update-config"
   workflow_dispatch:
 
+env:
+  # avoid warnings about config paths
+  JUPYTER_PLATFORM_DIRS: "1"
+  # avoid looking at every version of pip ever released
+  PIP_DISABLE_PIP_VERSION_CHECK: "1"
+
 jobs:
+  build:
+    runs-on: ubuntu-22.04
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "3.11"
+
+      - uses: actions/setup-node@v3
+        with:
+          cache: yarn
+          node-version: 20.x
+          registry-url: https://registry.npmjs.org
+          cache-dependency-path: labextension/yarn.lock
+
+      - name: Update root build packages
+        run: pip install --upgrade build
+
+      - name: Build Python package
+        run: pyproject-build
+
+      - name: Upload built artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: dist-${{ github.run_number }}
+          path: ./dist
+
   test:
+    name: ${{ matrix.os }} ${{ matrix.python-version }} ${{ matrix.pip-extras }}
+    needs: [build]
     timeout-minutes: 30
+    runs-on: ${{ matrix.os }}
+    defaults:
+      run:
+        shell: bash # windows default isn't bash
+
     strategy:
       fail-fast: false
       matrix:
+        os: [ubuntu-22.04, windows-2022]
         python-version: ["3.8", "3.11"]
-        jupyter_server-version: ["1", "2"]
-        jupyterlab-version: ["3", "4"]
-        os: [ubuntu-22.04]
-        include:
-          - python-version: "3.11"
-            jupyter_server-version: "2"
-            jupyterlab-version: "3"
-            os: windows-2022
-          - python-version: "3.11"
-            jupyter_server-version: "2"
-            jupyterlab-version: "4"
-            os: windows-2022
+        pip-extras: ["lab", "classic"]
         exclude:
-          - jupyter_server-version: "1"
-            jupyterlab-version: "4"
-
-    runs-on: ${{ matrix.os }}
-    defaults:
-      run:
-        shell: bash
+          # windows should work for all test variations, but a limited selection
+          # is run to avoid doubling the amount of test runs
+          - os: windows-2022
+            python-version: "3.11"
+            pip-extras: classic
+          - os: windows-2022
+            python-version: "3.8"
+            pip-extras: lab
 
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - uses: actions/setup-python@v4
         with:
           python-version: "${{ matrix.python-version }}"
 
-      - uses: actions/setup-node@v3
-        with:
-          cache: yarn
-          node-version: 18.x
-          registry-url: https://registry.npmjs.org
-          cache-dependency-path: labextension/yarn.lock
-
       - name: Update root build packages
-        run: |
-          pip install --upgrade build pip
+        run: pip install --upgrade pip
 
-      - name: Build Python package
-        run: |
-          pyproject-build
+      - name: Download built artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: dist-${{ github.run_number }}
+          path: ./dist
 
       - name: Install Python package
         # NOTE: See CONTRIBUTING.md for a local development setup that differs
@@ -79,53 +105,63 @@ jobs:
         #
         #       Pytest options are set in `pyproject.toml`.
         run: |
-          pip install -vv $(ls ./dist/jupyter_server_proxy-*.whl)\[acceptance\] 'jupyterlab~=${{ matrix.jupyterlab-version }}.0' 'jupyter_server~=${{ matrix.jupyter_server-version }}.0'
+          pip install -vv $(ls ./dist/*.whl)\[acceptance,${{ matrix.pip-extras }}\]
 
       - name: List Python packages
         run: |
           pip freeze
           pip check
 
-      - name: Run tests
+      - name: Check server extension for jupyter_server
         run: |
-          pytest -k "not acceptance" -vv
-
-      - name: Upload pytest and coverage reports
-        if: always()
-        uses: actions/upload-artifact@v3
-        with:
-          name: |-
-            unit-tests-${{ matrix.python-version }}-${{ matrix.jupyterlab-version }}-${{ github.run_number }}
-          path: |
-            ./build/pytest
-            ./build/coverage
+          jupyter server extension list
+          jupyter server extension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" -
 
-      - name: Check the Notebook Server extension is installed
+      - name: Check server extension for notebook v6
+        if: contains(matrix.pip-extras, 'classic')
         run: |
           jupyter serverextension list
           jupyter serverextension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" -
 
-      - name: Check the Jupyter Server extension is installed
+      - name: Check frontend extension for notebook v6
+        if: contains(matrix.pip-extras, 'classic')
         run: |
-          pip install jupyter-server
-          jupyter server extension list
-          jupyter server extension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" -
+          jupyter nbextension list
+          PYTHONUNBUFFERED=1 jupyter nbextension list 2>&1 | grep -A1 -iE '.*jupyter_server_proxy.*enabled' | grep -B1 -iE "Validating.*OK"
 
-      - name: Check the lab extension
+      - name: Check frontend extension for notebook v7+
+        if: ${{ !contains(matrix.pip-extras, 'classic') }}
         run: |
-          jupyter labextension list
-          jupyter labextension list 2>&1 | grep -iE '@jupyterhub/jupyter-server-proxy.*OK.*'
-          python -m jupyterlab.browser_check
+          jupyter notebook extension list
+          jupyter notebook extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*'
 
-      - name: Run acceptance tests
+      - name: Check frontend extension for jupyterlab
         run: |
-          pytest -s -k "acceptance"
+          jupyter lab extension list
+          jupyter lab extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*'
 
-      - name: Upload acceptance test reports
+      # we have installed a pre-built wheel and configured code coverage to
+      # inspect "jupyter_server_proxy", by re-locating to another directory,
+      # there is no confusion about "jupyter_server_proxy" referring to our
+      # installed package rather than the local directory
+      - name: Run tests
+        run: |
+          mkdir build
+          cd build
+          pytest -c ../pyproject.toml ../tests
+
+      - name: Upload test reports
         if: always()
         uses: actions/upload-artifact@v3
         with:
           name: |-
-            acceptance-tests-${{ matrix.python-version }}-${{ matrix.jupyterlab-version }}-${{ github.run_number }}
+            tests-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.pip-extras }}-${{ github.run_number }}
           path: |
+            ./build/pytest
+            ./build/coverage
             ./build/robot
+
+      # GitHub action reference: https://github.com/codecov/codecov-action
+      - uses: codecov/codecov-action@v3
+        with:
+          directory: build/.coverage
diff --git a/tests/acceptance/Classic.robot b/tests/acceptance/Classic.robot
index 7544014d..960886df 100644
--- a/tests/acceptance/Classic.robot
+++ b/tests/acceptance/Classic.robot
@@ -7,6 +7,7 @@ Documentation     Server Proxies in Notebook Classic
 Library           JupyterLibrary
 Suite Setup       Start Notebook Classic Tests
 Test Setup        Switch Window    MAIN
+Test Tags         app:classic
 
 *** Keywords ***
 Start Notebook Classic Tests
@@ -26,12 +27,12 @@ Launch Browser Tab
     Click Launcher    foo
     Wait Until Keyword Succeeds    3x    0.5s    Switch Window    title:Hello World
     Location Should Contain    foo
-    Page Should Contain    Hello World
+    Wait Until Page Contains    Hello World    timeout=10s
     Close Window
 
 Launch Another Browser Tab
     Click Launcher    bar
     Wait Until Keyword Succeeds    3x    0.5s    Switch Window    title:Hello World
     Location Should Contain    bar
-    Page Should Contain    Hello World
+    Wait Until Page Contains    Hello World    timeout=10s
     Close Window
diff --git a/tests/acceptance/Lab.robot b/tests/acceptance/Lab.robot
index fdc01464..b0341d58 100644
--- a/tests/acceptance/Lab.robot
+++ b/tests/acceptance/Lab.robot
@@ -6,6 +6,11 @@ https://robotframework-jupyterlibrary.readthedocs.io/en/stable/.
 Documentation     Server Proxies in Lab
 Library           JupyterLibrary
 Suite Setup       Start Lab Tests
+Test Tags         app:lab
+
+
+*** Variables ***
+${CSS_LAUNCHER_CARD}   css:.jp-LauncherCard-label
 
 *** Test Cases ***
 Lab Loads
@@ -15,7 +20,7 @@ Launch Browser Tab
     Click Launcher    foo
     Wait Until Keyword Succeeds    3x    0.5s    Switch Window    title:Hello World
     Location Should Contain    foo
-    Page Should Contain    Hello World
+    Wait Until Page Contains    Hello World    timeout=10s
     Close Window
     [Teardown]    Switch Window    title:JupyterLab
 
@@ -23,7 +28,7 @@ Launch Lab Tab
     Click Launcher    bar
     Wait Until Page Contains Element    css:iframe
     Select Frame    css:iframe
-    Page Should Contain    Hello World
+    Wait Until Page Contains    Hello World    timeout=10s
 
 *** Keywords ***
 Start Lab Tests
@@ -33,4 +38,6 @@ Start Lab Tests
 
 Click Launcher
     [Arguments]    ${title}
-    Click Element    css:.jp-LauncherCard-label[title^\="${title}"]
+    ${item} =   Set Variable   ${CSS_LAUNCHER_CARD}\[title^\="${title}"]
+    Wait Until Element Is Visible  ${item}  timeout=10s
+    Click Element    ${item}
diff --git a/tests/acceptance/Notebook.robot b/tests/acceptance/Notebook.robot
new file mode 100644
index 00000000..82fe831a
--- /dev/null
+++ b/tests/acceptance/Notebook.robot
@@ -0,0 +1,48 @@
+*** Comments ***
+To learn more about these .robot files, see
+https://robotframework-jupyterlibrary.readthedocs.io/en/stable/.
+
+*** Settings ***
+Documentation     Server Proxies in Notebook
+Library           JupyterLibrary
+Suite Setup       Start Notebook Tests
+Test Tags         app:notebook
+
+*** Variables ***
+${XP_NEW_MENU}   xpath://div[contains(@class, "jp-FileBrowser-toolbar")]//*[contains(text(), "New")]
+${XP_OPEN_COMMAND}   xpath://li[@data-command = "server-proxy:open"]
+
+*** Test Cases ***
+Notebook Loads
+    Capture Page Screenshot    00-smoke.png
+
+Launch Browser Tab
+    Launch With Toolbar Menu    foo
+    Wait Until Keyword Succeeds    3x    0.5s    Switch Window    title:Hello World
+    Location Should Contain    foo
+    Wait Until Page Contains    Hello World    timeout=10s
+    Close Window
+    [Teardown]    Switch Window    title:Home
+
+Launch Another Browser Tab
+    Launch With Toolbar Menu    bar
+    Wait Until Keyword Succeeds    3x    0.5s    Switch Window    title:Hello World
+    Location Should Contain    bar
+    Wait Until Page Contains    Hello World    timeout=10s
+    Close Window
+    [Teardown]    Switch Window    title:Home
+
+*** Keywords ***
+Start Notebook Tests
+    Open Notebook
+    Tag With JupyterLab Metadata
+    Set Screenshot Directory    ${OUTPUT DIR}${/}notebook
+
+Launch With Toolbar Menu
+    [Arguments]    ${title}
+    Mouse Over   ${XP_NEW_MENU}
+    Click Element    ${XP_NEW_MENU}
+    ${item} =   Set Variable   ${XP_OPEN_COMMAND}//div[text() = '${title}']
+    Wait Until Element Is Visible   ${item}
+    Mouse Over   ${item}
+    Click Element   ${item}
diff --git a/tests/acceptance/__init__.robot b/tests/acceptance/__init__.robot
index f774959e..fb3e3805 100644
--- a/tests/acceptance/__init__.robot
+++ b/tests/acceptance/__init__.robot
@@ -6,6 +6,7 @@ https://robotframework-jupyterlibrary.readthedocs.io/en/stable/.
 Documentation     Acceptance tests for jupyter-server-proxy
 Library           JupyterLibrary
 Library           OperatingSystem
+Library           Process
 Suite Setup       Set Up
 Suite Teardown    Clean Up
 
@@ -26,3 +27,4 @@ Set Up
 Clean Up
     Close all Browsers
     Terminate All Jupyter Servers
+    Terminate All Processes
diff --git a/tests/acceptance/resources/jupyter_config.json b/tests/acceptance/resources/jupyter_config.json
index 8af6f482..a3fd20bf 100644
--- a/tests/acceptance/resources/jupyter_config.json
+++ b/tests/acceptance/resources/jupyter_config.json
@@ -15,5 +15,8 @@
         "new_browser_tab": false
       }
     }
+  },
+  "LanguageServerManager": {
+    "autodetect": false
   }
 }
diff --git a/tests/acceptance/test_acceptance.py b/tests/acceptance/test_acceptance.py
index 1c856eca..d7da11c8 100644
--- a/tests/acceptance/test_acceptance.py
+++ b/tests/acceptance/test_acceptance.py
@@ -9,6 +9,14 @@
 OUTPUT = HERE.parent.parent / "build/robot"
 JUPYTER_SERVER_INFO = None
 
+
+try:
+    import notebook
+
+    NOTEBOOK_VERSION = int(notebook.__version__.split(".")[0])
+except ImportError:
+    NOTEBOOK_VERSION = None
+
 try:
     import jupyter_server
 
@@ -22,6 +30,12 @@ def test_robot():
     pytest.importorskip("JupyterLibrary")
 
     env = dict(**os.environ)
+    robot_args = ["robot", "--consolecolors=on", f"--outputdir={OUTPUT}"]
+
+    if NOTEBOOK_VERSION and NOTEBOOK_VERSION >= 7:
+        robot_args += ["--exclude", "app:classic"]
+    else:
+        robot_args += ["--exclude", "app:notebook"]
 
     # JUPYTER_LIBRARY_* env vars documentation:
     # https://robotframework-jupyterlibrary.readthedocs.io/en/stable/LIMITS.html#notebookapp-vs-serverapp
@@ -40,7 +54,7 @@ def test_robot():
         shutil.rmtree(OUTPUT)
 
     return_code = subprocess.call(
-        ["robot", "--consolecolors=on", f"--outputdir={OUTPUT}", str(HERE)],
+        [*robot_args, str(HERE)],
         cwd=str(HERE),
         env=env,
     )
diff --git a/tests/conftest.py b/tests/conftest.py
index 148cdf6c..d81e21fc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,19 +11,19 @@
 from urllib.request import urlopen
 from uuid import uuid4
 
-from pytest import fixture
+from pytest import TempPathFactory, fixture
 
 HERE = Path(__file__).parent
 RESOURCES = HERE / "resources"
 
 
-@fixture
+@fixture(scope="session")
 def a_token() -> str:
     """Get a random UUID to use for a token."""
     return str(uuid4())
 
 
-@fixture
+@fixture(scope="session")
 def an_unused_port() -> int:
     """Get a random unused port."""
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -34,20 +34,23 @@ def an_unused_port() -> int:
     return port
 
 
-@fixture(params=["notebook", "lab"])
+@fixture(params=["notebook", "lab"], scope="session")
 def a_server_cmd(request: Any) -> str:
     """Get a viable name for a command."""
     return request.param
 
 
-@fixture
+@fixture(scope="session")
 def a_server(
     a_server_cmd: str,
-    tmp_path: Path,
+    tmp_path_factory: TempPathFactory,
     an_unused_port: int,
     a_token: str,
 ) -> Generator[str, None, None]:
     """Get a running server."""
+
+    tmp_path = tmp_path_factory.mktemp(a_server_cmd)
+
     # get a copy of the resources
     tests = tmp_path / "tests"
     tests.mkdir()
@@ -65,7 +68,7 @@ def a_server(
 
     # prepare an env
     env = dict(os.environ)
-    env.update(JUPYTER_TOKEN=a_token)
+    env.update(JUPYTER_TOKEN=a_token, JUPYTER_PLATFORM_DIRS="1")
 
     # start the process
     server_proc = Popen(args, cwd=str(tmp_path), env=env)
@@ -75,35 +78,39 @@ def a_server(
     canary_url = f"{url}favicon.ico"
     shutdown_url = f"{url}api/shutdown?token={a_token}"
 
-    retries = 10
+    wait_until_urlopen(canary_url)
+
+    print(f"{a_server_cmd} is ready...", flush=True)
+
+    yield url
+
+    # clean up after server is no longer needed
+    print(f"{a_server_cmd} shutting down...", flush=True)
+    wait_until_urlopen(shutdown_url, data=[])
+    server_proc.wait()
+    print(f"{a_server_cmd} is stopped", flush=True)
+
+
+def wait_until_urlopen(url, **kwargs):
+    retries = 20
 
     while retries:
         try:
-            urlopen(canary_url)
+            urlopen(url, **kwargs)
             break
         except URLError:
             if not retries:
                 print(
-                    f"{a_server_cmd} not ready, aborting",
+                    f"{url} not ready, aborting",
                     flush=True,
                 )
                 raise
             print(
-                f"{a_server_cmd} not ready, will try again in 0.5s [{retries} retries]",
+                f"{url} not ready, will try again in 0.5s [{retries} retries]",
                 flush=True,
             )
-            time.sleep(0.5)
             retries -= 1
-
-    print(f"{a_server_cmd} is ready...", flush=True)
-
-    yield url
-
-    # clean up after server is no longer needed
-    print(f"{a_server_cmd} shutting down...", flush=True)
-    urlopen(shutdown_url, data=[])
-    server_proc.wait()
-    print(f"{a_server_cmd} is stopped", flush=True)
+        time.sleep(0.5)
 
 
 @fixture
diff --git a/tests/resources/jupyter_server_config.py b/tests/resources/jupyter_server_config.py
index e7b8a3c2..2fd2dd99 100644
--- a/tests/resources/jupyter_server_config.py
+++ b/tests/resources/jupyter_server_config.py
@@ -124,4 +124,11 @@ def cats_only(response, path):
 c.ServerProxy.non_service_rewrite_response = hello_to_foo
 
 c.ServerApp.jpserver_extensions = {"proxyextension": True}
+
 c.NotebookApp.nbserver_extensions = {"proxyextension": True}
+
+# disable notebook 7 banner
+c.ServerApp.show_banner = False
+
+# disable slow/noisy discovery of language servers
+c.LanguageServerManager.autodetect = False