Repository: microsoft/qsharp
Pull request #625 (2023-08-24 23:27:20)

VS Code extension tests
Minimal setup for integration tests for the VS Code extension. 

Runs the Chrome browser in "headless" mode so it can (in theory) be run in CI.

minestarks: Putting back into draft as I work to address some of the feedback. 

Will also add a folder structure for the test suite to separate debugger/language service/etc tests from each other.
minestarks: @billti this is ready for review again.
minestarks: Looks like this _still_ needs approval from @billti (since @cesarzc is out).

Patch:
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index fdc01533..eaaac718 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -13,6 +13,7 @@ module.exports = {
     "/jupyterlab/lib",
     "/jupyterlab/qsharp-jupyterlab/labextension",
     "/vscode/out/",
+    "/vscode/test/out/",
   ],
   env: {
     browser: true,
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 519c46f2..ddf7efa4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,6 +17,8 @@ concurrency:
 
 env:
   CARGO_TERM_COLOR: always
+  NODE_VERSION: "18.17.1"
+  PYTHON_VERSION: "3.11"
   RUST_TOOLCHAIN_VERSION: "1.72"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
@@ -77,14 +79,14 @@ jobs:
           submodules: "true"
       - uses: actions/setup-node@v3
         with:
-          node-version: '18.17.1'
+          node-version: ${{ env.NODE_VERSION }}
       - name: npm install
         run: npm install
       - name: npm check
         run: npm run check
 
   build:
-    name: CI Build and Test
+    name: Build and test
     strategy:
       matrix:
         os: [windows-latest, ubuntu-latest, macos-latest]
@@ -106,12 +108,45 @@ jobs:
         if: matrix.os == 'macos-latest'
       - uses: actions/setup-python@v4
         with:
-          python-version: '3.11'
+          python-version: ${{ env.PYTHON_VERSION }}
       - uses: actions/setup-node@v3
         with:
-          node-version: '18.17.1'
+          node-version: ${{ env.NODE_VERSION }}
       - uses: Swatinem/rust-cache@v2
       - name: Prereqs
         run: python ./prereqs.py --install
       - name: Build and Test
         run: python ./build.py --no-check
+
+  integration-tests:
+    name: Integration tests
+    strategy:
+      matrix:
+        os: [windows-latest, ubuntu-latest, macos-latest]
+
+    runs-on: ${{matrix.os}}
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: "true"
+      - name: Setup rust toolchain
+        uses: ./.github/actions/toolchains/rust
+        with:
+          toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }}
+          components: ${{ env.RUST_TOOLCHAIN_COMPONENTS }}
+      - name: Add additional Rust targets
+        run: |
+          rustup target add aarch64-apple-darwin
+        if: matrix.os == 'macos-latest'
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ env.PYTHON_VERSION }}
+      - uses: actions/setup-node@v3
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+      - uses: Swatinem/rust-cache@v2
+      - name: Prereqs
+        run: python ./prereqs.py --install
+      - name: Run integration tests
+        run: python ./build.py --no-check --no-test --wasm --npm --vscode --integration-tests
diff --git a/.prettierignore b/.prettierignore
index 53c93c42..ba7ddf6e 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -21,4 +21,5 @@ __pycache__/
 /samples/
 /target/
 /vscode/out/
+/vscode/test/out/
 /wasm/
diff --git a/build.py b/build.py
index 909ac0ef..58a55f5e 100755
--- a/build.py
+++ b/build.py
@@ -7,6 +7,7 @@ import argparse
 import os
 import platform
 import sys
+import time
 import venv
 import shutil
 import subprocess
@@ -60,6 +61,13 @@ parser.add_argument(
     help="Run the prerequisites check (default is --check-prereqs)",
 )
 
+parser.add_argument(
+    "--integration-tests",
+    action=argparse.BooleanOptionalAction,
+    default=False,
+    help="Build and run the integration tests (default is --no-integration-tests)",
+)
+
 args = parser.parse_args()
 
 if args.check_prereqs:
@@ -95,9 +103,6 @@ npm_cmd = "npm.cmd" if platform.system() == "Windows" else "npm"
 build_type = "debug" if args.debug else "release"
 run_tests = args.test
 
-# TODO: This requires that both targets are installed on macOS to build Python packages. Add to prereqs checks.
-pip_archflags = "-arch x86_64 -arch arm64" if platform.system() == "Darwin" else None
-
 root_dir = os.path.dirname(os.path.abspath(__file__))
 wasm_src = os.path.join(root_dir, "wasm")
 wasm_bld = os.path.join(root_dir, "target", "wasm32", build_type)
@@ -109,11 +114,26 @@ wheels_dir = os.path.join(root_dir, "target", "wheels")
 vscode_src = os.path.join(root_dir, "vscode")
 jupyterlab_src = os.path.join(root_dir, "jupyterlab")
 
+
+def step_start(description):
+    global start_time
+    print(f"build.py step: {description}")
+    start_time = time.time()
+
+
+def step_end():
+    global start_time
+    duration = time.time() - start_time
+    print(f"build.py step: Finished in {duration:.3f}s.")
+
+
 if npm_install_needed:
+    step_start("Running npm install")
     subprocess.run([npm_cmd, "install"], check=True, text=True, cwd=root_dir)
+    step_end()
 
 if args.check:
-    print("Running eslint and prettier checks")
+    step_start("Running eslint and prettier checks")
     subprocess.run([npm_cmd, "run", "check"], check=True, text=True, cwd=root_dir)
 
     if build_wasm or build_cli:
@@ -139,9 +159,10 @@ if args.check:
             text=True,
             cwd=root_dir,
         )
+    step_end()
 
 if build_cli:
-    print("Building the command line compiler")
+    step_start("Building the command line compiler")
     cargo_build_args = ["cargo", "build"]
     if build_type == "release":
         cargo_build_args.append("--release")
@@ -153,9 +174,10 @@ if build_cli:
         if build_type == "release":
             cargo_test_args.append("--release")
         subprocess.run(cargo_test_args, check=True, text=True, cwd=root_dir)
+    step_end()
 
 if build_pip:
-    print("Building the pip package")
+    step_start("Building the pip package")
     # Check if in a virtual environment
     if (
         os.environ.get("VIRTUAL_ENV") is None
@@ -181,9 +203,9 @@ if build_pip:
 
     # copy the process env vars
     pip_env: dict[str, str] = os.environ.copy()
-    if pip_archflags is not None:
+    if platform.system() == "Darwin":
         # if on mac, add the arch flags for universal binary
-        pip_env["ARCHFLAGS"] = pip_archflags
+        pip_env["ARCHFLAGS"] = "-arch x86_64 -arch arm64"
 
     pip_build_args = [
         python_bin,
@@ -247,9 +269,10 @@ if build_pip:
             subprocess.run(pytest_args, check=True, text=True, cwd=qir_test_dir)
         else:
             print("Could not import PyQIR, skipping tests")
+    step_end()
 
 if build_wasm:
-    print("Building the wasm crate")
+    step_start("Building the wasm crate")
     # wasm-pack can't build for web and node in the same build, so need to run twice.
     # Hopefully not needed if https://github.com/rustwasm/wasm-pack/issues/313 lands.
     build_flag = "--release" if build_type == "release" else "--dev"
@@ -266,23 +289,25 @@ if build_wasm:
     subprocess.run(
         wasm_pack_args + node_build_args, check=True, text=True, cwd=wasm_src
     )
+    step_end()
 
 if build_samples:
-    print("Building qsharp samples")
+    step_start("Building qsharp samples")
     files = [
         os.path.join(dp, f)
         for dp, _, filenames in os.walk(samples_src)
         for f in filenames
         if os.path.splitext(f)[1] == ".qs"
     ]
-    args = ["cargo", "run", "--bin", "qsc"]
+    cargo_args = ["cargo", "run", "--bin", "qsc"]
     if build_type == "release":
-        args.append("--release")
+        cargo_args.append("--release")
     for file in files:
-        subprocess.run((args + ["--", file]), check=True, text=True, cwd=root_dir)
+        subprocess.run((cargo_args + ["--", file]), check=True, text=True, cwd=root_dir)
+    step_end()
 
 if build_npm:
-    print("Building the npm package")
+    step_start("Building the npm package")
     # Copy the wasm build files over for web and node targets
     for target in ["web", "node"]:
         lib_dir = os.path.join(npm_src, "lib", target)
@@ -307,19 +332,22 @@ if build_npm:
         print("Running tests for the npm package")
         npm_test_args = ["node", "--test"]
         subprocess.run(npm_test_args, check=True, text=True, cwd=npm_src)
+    step_end()
 
 if build_play:
-    print("Building the playground")
+    step_start("Building the playground")
     play_args = [npm_cmd, "run", "build"]
     subprocess.run(play_args, check=True, text=True, cwd=play_src)
+    step_end()
 
 if build_vscode:
-    print("Building the VS Code extension")
+    step_start("Building the VS Code extension")
     vscode_args = [npm_cmd, "run", "build"]
     subprocess.run(vscode_args, check=True, text=True, cwd=vscode_src)
+    step_end()
 
 if build_jupyterlab:
-    print("Building the JupyterLab extension")
+    step_start("Building the JupyterLab extension")
 
     # Check if in a virtual environment
     if (
@@ -354,3 +382,10 @@ if build_jupyterlab:
         jupyterlab_src,
     ]
     subprocess.run(pip_build_args, check=True, text=True, cwd=jupyterlab_src)
+    step_end()
+
+if args.integration_tests:
+    step_start("Running the VS Code integration tests")
+    vscode_args = [npm_cmd, "test"]
+    subprocess.run(vscode_args, check=True, text=True, cwd=vscode_src)
+    step_end()
diff --git a/compiler/qsc/src/incremental.rs b/compiler/qsc/src/incremental.rs
new file mode 100644
index 00000000..eee49cd1
--- /dev/null
+++ b/compiler/qsc/src/incremental.rs
@@ -0,0 +1,191 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::compile::{self, compile, core, std};
+use miette::Diagnostic;
+use qsc_frontend::{
+    compile::{OpenPackageStore, PackageStore, SourceMap, TargetProfile},
+    error::WithSource,
+    incremental::Increment,
+};
+use qsc_hir::hir::PackageId;
+use qsc_passes::{PackageType, PassContext};
+
+/// An incremental Q# compiler.
+pub struct Compiler {
+    /// A package store that contains the current, mutable, `CompileUnit`
+    /// as well as all its immutable dependencies.
+    store: OpenPackageStore,
+    /// The ID of the source package. The source package
+    /// is made up of the initial sources passed in when creating the compiler.
+    source_package_id: PackageId,
+    /// Context for passes that is reused across incremental compilations.
+    passes: PassContext,
+    /// The frontend incremental compiler.
+    frontend: qsc_frontend::incremental::Compiler,
+}
+
+/// An incremental compiler error.
+pub type Errors = Vec<WithSource<compile::Error>>;
+
+impl Compiler {
+    /// Creates a new incremental compiler, compiling the passed in sources.
+    /// # Errors
+    /// If compiling the sources fails, compiler errors are returned.
+    pub fn new(
+        include_std: bool,
+        sources: SourceMap,
+        package_type: PackageType,
+        target: TargetProfile,
+    ) -> Result<Self, Errors> {
+        let core = core();
+        let mut store = PackageStore::new(core);
+        let mut dependencies = Vec::new();
+        if include_std {
+            let std = std(&store, target);
+            let id = store.insert(std);
+            dependencies.push(id);
+        }
+
+        let (unit, errors) = compile(&store, &dependencies, sources, package_type, target);
+        if !errors.is_empty() {
+            return Err(into_errors_with_source(errors, &unit.sources));
+        }
+
+        let source_package_id = store.insert(unit);
+        dependencies.push(source_package_id);
+
+        let frontend = qsc_frontend::incremental::Compiler::new(&store, dependencies, target);
+        let store = store.open();
+
+        Ok(Self {
+            store,
+            source_package_id,
+            frontend,
+            passes: PassContext::new(target),
+        })
+    }
+
+    /// Compiles Q# fragments. Fragments are Q# code that can contain
+    /// top-level statements as well as namespaces. A notebook cell
+    /// or an interpreter entry is an example of fragments.
+    ///
+    /// This method returns the AST and HIR packages that were created as a result of
+    /// the compilation, however it does *not* update the current compilation.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit` using the `update()` method.
+    pub fn compile_fragments(
+        &mut self,
+        source_name: &str,
+        source_contents: &str,
+    ) -> Result<Increment, Errors> {
+        let (core, unit) = self.store.get_open_mut();
+
+        let mut increment = self
+            .frontend
+            .compile_fragments(unit, source_name, source_contents)
+            .map_err(into_errors)?;
+
+        let pass_errors = self.passes.run_default_passes(
+            &mut increment.hir,
+            &mut unit.assigner,
+            core,
+            PackageType::Lib,
+        );
+
+        if !pass_errors.is_empty() {
+            return Err(into_errors_with_source(pass_errors, &unit.sources));
+        }
+
+        Ok(increment)
+    }
+
+    /// Compiles an entry expression.
+    ///
+    /// This method returns the AST and HIR packages that were created as a result of
+    /// the compilation, however it does *not* update the current compilation.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit` using the `update()` method.
+    pub fn compile_expr(&mut self, expr: &str) -> Result<Increment, Errors> {
+        let (core, unit) = self.store.get_open_mut();
+
+        let mut increment = self
+            .frontend
+            .compile_expr(unit, "<entry>", expr)
+            .map_err(into_errors)?;
+
+        let pass_errors = self.passes.run_default_passes(
+            &mut increment.hir,
+            &mut unit.assigner,
+            core,
+            PackageType::Lib,
+        );
+
+        if !pass_errors.is_empty() {
+            return Err(into_errors_with_source(pass_errors, &unit.sources));
+        }
+
+        Ok(increment)
+    }
+
+    /// Updates the current compilation with the AST and HIR packages,
+    /// and any associated context, returned from a previous incremental compilation.
+    pub fn update(&mut self, new: Increment) {
+        let (_, unit) = self.store.get_open_mut();
+
+        self.frontend.update(unit, new);
+    }
+
+    /// Returns a reference to the underlying package store.
+    #[must_use]
+    pub fn package_store(&self) -> &PackageStore {
+        self.store.package_store()
+    }
+
+    /// Returns ID of the current `CompileUnit`.
+    #[must_use]
+    pub fn package_id(&self) -> PackageId {
+        self.store.open_package_id()
+    }
+
+    /// Returns the ID of the source package created from the sources
+    /// passed in during inital creation.
+    #[must_use]
+    pub fn source_package_id(&self) -> PackageId {
+        self.source_package_id
+    }
+
+    /// Consumes the incremental compiler and returns an immutable package store.
+    /// This method can be used to finalize the compilation.
+    #[must_use]
+    pub fn into_package_store(self) -> (PackageStore, PackageId) {
+        self.store.into_package_store()
+    }
+}
+
+fn into_errors_with_source<T>(errors: Vec<T>, sources: &SourceMap) -> Errors
+where
+    compile::Error: From<T>,
+{
+    errors
+        .into_iter()
+        .map(|e| WithSource::from_map(sources, e.into()))
+        .collect()
+}
+
+fn into_errors<T>(errors: Vec<WithSource<T>>) -> Errors
+where
+    compile::Error: From<T>,
+    T: Diagnostic,
+{
+    errors
+        .into_iter()
+        .map(qsc_frontend::error::WithSource::into_with_source)
+        .collect()
+}
diff --git a/compiler/qsc/src/interpret/stateful.rs b/compiler/qsc/src/interpret/stateful.rs
index ff193bb6..62b0066a 100644
--- a/compiler/qsc/src/interpret/stateful.rs
+++ b/compiler/qsc/src/interpret/stateful.rs
@@ -9,9 +9,8 @@ mod stepping_tests;
 
 use super::debug::format_call_stack;
 use crate::{
-    compile::{self, compile},
     error::{self, WithStack},
-    hir,
+    incremental::Compiler,
 };
 use miette::Diagnostic;
 use num_bigint::BigUint;
@@ -27,44 +26,32 @@ use qsc_eval::{
     Env, Global, NodeLookup, State, StepAction, StepResult, VariableInfo,
 };
 use qsc_fir::{
-    fir::{
-        Block, BlockId, CallableDecl, Expr, ExprId, ItemKind, LocalItemId, Package, PackageId, Pat,
-        PatId, Stmt, StmtId,
-    },
+    fir::{Block, BlockId, Expr, ExprId, ItemKind, Package, PackageId, Pat, PatId, Stmt, StmtId},
     visit::{self, Visitor},
 };
 use qsc_frontend::{
     compile::{CompileUnit, PackageStore, Source, SourceMap, TargetProfile},
     error::WithSource,
-    incremental::{self, Compiler, Fragment},
 };
-use qsc_passes::{PackageType, PassContext};
+use qsc_passes::PackageType;
 use std::collections::HashSet;
 use thiserror::Error;
 
-#[derive(Clone, Debug, Diagnostic, Error)]
-#[diagnostic(transparent)]
-#[error(transparent)]
-pub struct Error(#[from] ErrorKind);
-
 impl Error {
     #[must_use]
     pub fn stack_trace(&self) -> &Option<String> {
-        match &self.0 {
-            ErrorKind::Eval(err) => err.stack_trace(),
+        match &self {
+            Error::Eval(err) => err.stack_trace(),
             _ => &None,
         }
     }
 }
 
 #[derive(Clone, Debug, Diagnostic, Error)]
-enum ErrorKind {
+pub enum Error {
     #[error(transparent)]
     #[diagnostic(transparent)]
-    Compile(#[from] WithSource<compile::Error>),
-    #[error(transparent)]
-    #[diagnostic(transparent)]
-    Incremental(#[from] WithSource<incremental::Error>),
+    Compile(#[from] WithSource<crate::compile::Error>),
     #[error(transparent)]
     #[diagnostic(transparent)]
     Pass(#[from] WithSource<qsc_passes::Error>),
@@ -81,9 +68,6 @@ enum ErrorKind {
 
 struct Lookup<'a> {
     fir_store: &'a IndexMap<PackageId, qsc_fir::fir::Package>,
-    package: PackageId,
-    udts: &'a HashSet<LocalItemId>,
-    callables: &'a IndexMap<LocalItemId, CallableDecl>,
 }
 
 impl<'a> Lookup<'a> {
@@ -96,7 +80,7 @@ impl<'a> Lookup<'a> {
 
 impl<'a> NodeLookup for Lookup<'a> {
     fn get(&self, id: GlobalId) -> Option<Global<'a>> {
-        get_global(self.fir_store, self.udts, self.callables, self.package, id)
+        get_global(self.fir_store, id)
     }
     fn get_block(&self, package: PackageId, id: BlockId) -> &qsc_fir::fir::Block {
         self.get_package(package)
@@ -124,33 +108,41 @@ impl<'a> NodeLookup for Lookup<'a> {
     }
 }
 
+/// A Q# interpreter.
 pub struct Interpreter {
-    // compilation state (ast, hir)
-    store: PackageStore,
+    /// The incremental Q# compiler.
     compiler: Compiler,
-    passes: PassContext,
-    lines: u32,
+    /// The `TargetProfile` used for compilation.
     target: TargetProfile,
-    // fir state
+    /// The number of lines that have so far been compiled.
+    /// This field is used to generate a unique label
+    /// for each line evaluated with `eval_fragments`.
+    lines: u32,
+    // The FIR store
     fir_store: IndexMap<PackageId, qsc_fir::fir::Package>,
-    udts: HashSet<LocalItemId>,
-    callables: IndexMap<LocalItemId, CallableDecl>,
+    /// FIR lowerer
     lowerer: qsc_eval::lower::Lowerer,
-    // package IDs (used to index into both ast/hir and fir package stores)
+    /// The ID of the current package.
+    /// This ID is valid both for the FIR store and the `PackageStore`.
     package: PackageId,
+    /// The ID of the source package. The source package
+    /// is made up of the initial sources passed in when creating the interpreter.
+    /// This ID is valid both for the FIR store and the `PackageStore`.
     source_package: PackageId,
-    // evaluator state
-    env: Env,
+    /// The default simulator backend.
     sim: SparseSim,
+    /// The evaluator environment.
+    env: Env,
+    /// The current state of the evaluator.
     state: State,
 }
 
 pub type InterpretResult = Result<Value, Vec<Error>>;
 
 impl Interpreter {
+    /// Creates a new incremental compiler, compiling the passed in sources.
     /// # Errors
-    /// If the compilation of the standard library fails, an error is returned.
-    /// If the compilation of the sources fails, an error is returned.
+    /// If compiling the sources fails, compiler errors are returned.
     pub fn new(
         std: bool,
         sources: SourceMap,
@@ -158,56 +150,31 @@ impl Interpreter {
         target: TargetProfile,
     ) -> Result<Self, Vec<Error>> {
         let mut lowerer = qsc_eval::lower::Lowerer::new();
-        let core = compile::core();
-        let core_fir = lowerer.lower_package(&core.package);
         let mut fir_store = IndexMap::new();
-        fir_store.insert(
-            map_hir_package_to_fir(qsc_hir::hir::PackageId::CORE),
-            core_fir,
-        );
-        let mut store = PackageStore::new(core);
-        let mut dependencies = Vec::new();
-        if std {
-            let std = compile::std(&store, target);
-            let std_fir = lowerer.lower_package(&std.package);
-            let id = store.insert(std);
-            fir_store.insert(map_hir_package_to_fir(id), std_fir);
-            dependencies.push(id);
-        }
 
-        let (unit, errors) = compile(&store, &dependencies, sources, package_type, target);
-        if !errors.is_empty() {
-            return Err(errors
-                .into_iter()
-                .map(|error| Error(WithSource::from_map(&unit.sources, error).into()))
-                .collect());
+        let compiler = Compiler::new(std, sources, package_type, target).map_err(into_errors)?;
+
+        for (id, unit) in compiler.package_store().iter() {
+            fir_store.insert(
+                map_hir_package_to_fir(id),
+                lowerer.lower_package(&unit.package),
+            );
         }
 
-        let user_fir = lowerer.lower_package(&unit.package);
-        let package = store.insert(unit);
-        fir_store.insert(map_hir_package_to_fir(package), user_fir);
+        let source_package_id = compiler.source_package_id();
+        let package_id = compiler.package_id();
 
-        dependencies.push(package);
-        let unit = CompileUnit::default();
-        let user_fir = lowerer.lower_package(&unit.package);
-        let user_package = store.insert(unit);
-        fir_store.insert(map_hir_package_to_fir(user_package), user_fir);
-        let compiler = Compiler::new(&store, dependencies, target);
         Ok(Self {
-            store,
             compiler,
-            passes: PassContext::new(target),
             lines: 0,
             target,
             fir_store,
-            udts: HashSet::new(),
-            callables: IndexMap::new(),
             lowerer,
             env: Env::with_empty_scope(),
             sim: SparseSim::new(),
-            state: State::new(map_hir_package_to_fir(package)),
-            package: map_hir_package_to_fir(user_package),
-            source_package: map_hir_package_to_fir(package),
+            state: State::new(map_hir_package_to_fir(source_package_id)),
+            package: map_hir_package_to_fir(package_id),
+            source_package: map_hir_package_to_fir(source_package_id),
         })
     }
 
@@ -225,9 +192,6 @@ impl Interpreter {
     /// Resumes execution with specified `StepAction`.
     /// # Errors
     /// Returns a vector of errors if evaluating the entry point fails.
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     pub fn eval_step(
         &mut self,
         receiver: &mut impl Receiver,
@@ -236,9 +200,6 @@ impl Interpreter {
     ) -> Result<StepResult, Vec<Error>> {
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
 
         self.state
@@ -251,31 +212,22 @@ impl Interpreter {
                 step,
             )
             .map_err(|(error, call_stack)| {
-                let stack_trace = if call_stack.is_empty() {
-                    None
-                } else {
-                    Some(self.render_call_stack(call_stack, &error))
-                };
-
-                vec![Error(
-                    error::from_eval(error, &self.store, stack_trace).into(),
-                )]
+                eval_error(
+                    self.compiler.package_store(),
+                    &self.fir_store,
+                    call_stack,
+                    error,
+                )
             })
     }
 
     /// Executes the entry expression until the end of execution.
     /// # Errors
     /// Returns a vector of errors if evaluating the entry point fails.
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     pub fn eval_entry(&mut self, receiver: &mut impl Receiver) -> Result<Value, Vec<Error>> {
         let expr = self.get_entry_expr()?;
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
 
         eval_expr(
@@ -287,15 +239,12 @@ impl Interpreter {
             receiver,
         )
         .map_err(|(error, call_stack)| {
-            let stack_trace = if call_stack.is_empty() {
-                None
-            } else {
-                Some(self.render_call_stack(call_stack, &error))
-            };
-
-            vec![Error(
-                error::from_eval(error, &self.store, stack_trace).into(),
-            )]
+            eval_error(
+                self.compiler.package_store(),
+                &self.fir_store,
+                call_stack,
+                error,
+            )
         })
     }
 
@@ -307,7 +256,7 @@ impl Interpreter {
         if let Some(entry) = unit.entry {
             return Ok(entry);
         };
-        Err(vec![Error(ErrorKind::NoEntryPoint)])
+        Err(vec![Error::NoEntryPoint])
     }
 
     /// # Errors
@@ -319,36 +268,28 @@ impl Interpreter {
         receiver: &mut impl Receiver,
         fragments: &str,
     ) -> InterpretResult {
-        let mut result = Value::unit();
+        let label = self.next_line_label();
 
-        let label = &self.next_line_label();
+        let increment = self
+            .compiler
+            .compile_fragments(&label, fragments)
+            .map_err(into_errors)?;
 
-        let (items, stmts) =
-            self.compile_incremental(|c, p| c.compile_fragments(p, label, fragments))?;
+        let stmts = self.lower(&increment);
 
-        for item in items {
-            match item.kind {
-                qsc_hir::hir::ItemKind::Callable(callable) => {
-                    let callable = self.lower_callable_decl(&callable);
+        // Updating the compiler state with the new AST/HIR nodes
+        // is not necessary for the interpreter to function, as all
+        // the state required for evaluation already exists in the
+        // FIR store. It could potentially save some memory
+        // *not* to do hold on to the AST/HIR, but it is done
+        // here to keep the package stores consistent.
+        self.compiler.update(increment);
 
-                    self.callables
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id), callable);
-                }
-                qsc_hir::hir::ItemKind::Namespace(..) => {}
-                qsc_hir::hir::ItemKind::Ty(..) => {
-                    self.udts
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id));
-                }
-            }
-        }
+        let mut result = Value::unit();
 
-        for stmt in stmts {
-            let stmt_id = self.lower_stmt(&stmt);
+        for stmt_id in stmts {
             let globals = Lookup {
                 fir_store: &self.fir_store,
-                package: self.package,
-                udts: &self.udts,
-                callables: &self.callables,
             };
 
             match eval_stmt(
@@ -361,15 +302,12 @@ impl Interpreter {
             ) {
                 Ok(value) => result = value,
                 Err((error, call_stack)) => {
-                    let stack_trace = if call_stack.is_empty() {
-                        None
-                    } else {
-                        Some(self.render_call_stack(call_stack, &error))
-                    };
-
-                    return Err(vec![Error(
-                        error::from_eval(error, &self.store, stack_trace).into(),
-                    )]);
+                    return Err(eval_error(
+                        self.compiler.package_store(),
+                        &self.fir_store,
+                        call_stack,
+                        error,
+                    ))
                 }
             }
         }
@@ -377,50 +315,6 @@ impl Interpreter {
         Ok(result)
     }
 
-    fn compile_incremental(
-        &mut self,
-        mut compile: impl FnMut(
-            &mut Compiler,
-            &mut CompileUnit,
-        ) -> Result<Vec<Fragment>, Vec<incremental::Error>>,
-    ) -> Result<(Vec<hir::Item>, Vec<hir::Stmt>), Vec<Error>> {
-        let (core, package) = self.store.get_mut(map_fir_package_to_hir(self.package));
-
-        let package = package.expect("expected to find package");
-
-        let mut fragments = compile(&mut self.compiler, package).map_err(|errors| {
-            errors
-                .into_iter()
-                .map(|error| Error(WithSource::from_map(&package.sources, error).into()))
-                .collect::<Vec<_>>()
-        })?;
-
-        let pass_errors = fragments
-            .iter_mut()
-            .flat_map(|fragment| self.passes.run(core, &mut package.assigner, fragment))
-            .collect::<Vec<_>>();
-        if !pass_errors.is_empty() {
-            return Err(pass_errors
-                .into_iter()
-                .map(|error| Error(WithSource::from_map(&package.sources, error).into()))
-                .collect());
-        }
-
-        // Partition list into items and statements since items are always
-        // meant to be processed before statements.
-        let mut items = Vec::new();
-        let mut stmts = Vec::new();
-
-        for f in fragments {
-            match f {
-                Fragment::Item(item) => items.push(item),
-                Fragment::Stmt(stmt) => stmts.push(stmt),
-            }
-        }
-
-        Ok((items, stmts))
-    }
-
     /// Runs the given entry expression on a new instance of the environment and simulator,
     /// but using the current compilation.
     pub fn run(
@@ -436,7 +330,7 @@ impl Interpreter {
     /// and simulator but using the current compilation.
     pub fn qirgen(&mut self, expr: &str) -> Result<String, Vec<Error>> {
         if self.target != TargetProfile::Base {
-            return Err(vec![Error(ErrorKind::TargetMismatch)]);
+            return Err(vec![Error::TargetMismatch]);
         }
 
         let mut sim = BaseProfSim::new();
@@ -461,74 +355,42 @@ impl Interpreter {
         expr: &str,
         shots: u32,
     ) -> Result<Vec<InterpretResult>, Vec<Error>> {
-        let (items, stmts) = self.compile_incremental(|c, p| c.compile_expr(p, "<entry>", expr))?;
+        let stmt_id = self.compile_expr_to_stmt(expr)?;
 
-        Ok(self.run_internal(sim, receiver, items, stmts, shots))
+        Ok(self.run_internal(sim, receiver, stmt_id, shots))
     }
 
     fn run_internal(
         &mut self,
         sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
         receiver: &mut impl Receiver,
-        items: Vec<hir::Item>,
-        stmts: Vec<hir::Stmt>,
+        stmt_id: StmtId,
         shots: u32,
     ) -> Vec<InterpretResult> {
-        for item in items {
-            match item.kind {
-                qsc_hir::hir::ItemKind::Callable(callable) => {
-                    let callable = self.lower_callable_decl(&callable);
-
-                    self.callables
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id), callable);
-                }
-                qsc_hir::hir::ItemKind::Namespace(..) => {}
-                qsc_hir::hir::ItemKind::Ty(..) => {
-                    self.udts
-                        .insert(qsc_eval::lower::lower_local_item_id(item.id));
-                }
-            }
-        }
-
-        let mut stmt_ids = Vec::new();
-        for stmt in stmts {
-            stmt_ids.push(self.lower_stmt(&stmt));
-        }
-
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
 
         let mut results: Vec<InterpretResult> = Vec::new();
         for i in 0..shots {
-            for stmt_id in &stmt_ids {
-                results.push(
-                    match eval_stmt(
-                        *stmt_id,
-                        &globals,
-                        &mut Env::with_empty_scope(),
-                        sim,
-                        self.package,
-                        receiver,
-                    ) {
-                        Ok(value) => Ok(value),
-                        Err((error, call_stack)) => {
-                            let stack_trace = if call_stack.is_empty() {
-                                None
-                            } else {
-                                Some(self.render_call_stack(call_stack, &error))
-                            };
-
-                            Err(vec![Error(
-                                error::from_eval(error, &self.store, stack_trace).into(),
-                            )])
-                        }
-                    },
-                );
-            }
+            results.push(
+                match eval_stmt(
+                    stmt_id,
+                    &globals,
+                    &mut Env::with_empty_scope(),
+                    sim,
+                    self.package,
+                    receiver,
+                ) {
+                    Ok(value) => Ok(value),
+                    Err((error, call_stack)) => Err(eval_error(
+                        self.compiler.package_store(),
+                        &self.fir_store,
+                        call_stack,
+                        error,
+                    )),
+                },
+            );
 
             if i != 0 {
                 // If running more than one shot, re-initialize the simulator to start the next shot
@@ -540,59 +402,52 @@ impl Interpreter {
         results
     }
 
-    fn package(&self) -> &CompileUnit {
-        self.store
-            .get(map_fir_package_to_hir(self.source_package))
-            .expect("Could not load package")
-    }
+    fn compile_expr_to_stmt(&mut self, expr: &str) -> Result<StmtId, Vec<Error>> {
+        let increment = self.compiler.compile_expr(expr).map_err(into_errors)?;
 
-    fn next_line_label(&mut self) -> String {
-        let label = format!("line_{}", self.lines);
-        self.lines += 1;
-        label
-    }
+        let stmts = self.lower(&increment);
 
-    fn lower_callable_decl(&mut self, callable: &qsc_hir::hir::CallableDecl) -> CallableDecl {
-        let callable = self.lowerer.lower_callable_decl(callable);
-        self.update_fir_package();
-        callable
-    }
+        // Updating the compiler state with the new AST/HIR nodes
+        // is not necessary for the interpreter to function, as all
+        // the state required for evaluation already exists in the
+        // FIR store. It could potentially save some memory
+        // *not* to do hold on to the AST/HIR, but it is done
+        // here to keep the package stores consistent.
+        self.compiler.update(increment);
+
+        assert!(stmts.len() == 1, "expected exactly one statement");
+        let stmt_id = stmts.get(0).expect("expected exactly one statement");
 
-    fn lower_stmt(&mut self, stmt: &qsc_hir::hir::Stmt) -> StmtId {
-        let stmt_id = self.lowerer.lower_stmt(stmt);
-        self.update_fir_package();
-        stmt_id
+        Ok(*stmt_id)
     }
 
-    fn update_fir_package(&mut self) {
-        let package = self
+    fn lower(&mut self, unit_addition: &qsc_frontend::incremental::Increment) -> Vec<StmtId> {
+        let fir_package = self
             .fir_store
             .get_mut(self.package)
             .expect("package should be in store");
 
-        self.lowerer.update_package(package);
+        self.lowerer
+            .lower_and_update_package(fir_package, &unit_addition.hir)
     }
 
-    fn render_call_stack(&self, call_stack: Vec<Frame>, error: &dyn std::error::Error) -> String {
-        let globals = Lookup {
-            fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
-        };
-        format_call_stack(&self.store, &globals, call_stack, error)
+    fn source_package(&self) -> &CompileUnit {
+        self.compiler
+            .package_store()
+            .get(map_fir_package_to_hir(self.source_package))
+            .expect("Could not load package")
+    }
+
+    fn next_line_label(&mut self) -> String {
+        let label = format!("line_{}", self.lines);
+        self.lines += 1;
+        label
     }
 
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     #[must_use]
     pub fn get_stack_frames(&self) -> Vec<StackFrame> {
         let globals = Lookup {
             fir_store: &self.fir_store,
-            package: self.package,
-            udts: &self.udts,
-            callables: &self.callables,
         };
         let frames = self.state.get_stack_frames();
         let stack_frames = frames
@@ -606,7 +461,8 @@ impl Interpreter {
                 };
 
                 let hir_package = self
-                    .store
+                    .compiler
+                    .package_store()
                     .get(map_fir_package_to_hir(frame.id.package))
                     .expect("package should exist");
                 let source = hir_package
@@ -630,12 +486,9 @@ impl Interpreter {
         self.sim.capture_quantum_state()
     }
 
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     #[must_use]
     pub fn get_breakpoints(&self, path: &str) -> Vec<BreakpointSpan> {
-        let unit = self.package();
+        let unit = self.source_package();
 
         if let Some(source) = unit.sources.find_by_name(path) {
             let package = self
@@ -684,26 +537,17 @@ pub struct StackFrame {
     pub hi: u32,
 }
 
-fn get_global<'a>(
-    fir_store: &'a IndexMap<PackageId, qsc_fir::fir::Package>,
-    udts: &'a HashSet<LocalItemId>,
-    callables: &'a IndexMap<LocalItemId, CallableDecl>,
-    package: PackageId,
+fn get_global(
+    fir_store: &IndexMap<PackageId, qsc_fir::fir::Package>,
     id: GlobalId,
-) -> Option<Global<'a>> {
-    if id.package == package {
-        udts.contains(&id.item)
-            .then_some(Global::Udt)
-            .or_else(|| callables.get(id.item).map(Global::Callable))
-    } else {
-        fir_store
-            .get(id.package)
-            .and_then(|package| match &package.items.get(id.item)?.kind {
-                ItemKind::Callable(callable) => Some(Global::Callable(callable)),
-                ItemKind::Namespace(..) => None,
-                ItemKind::Ty(..) => Some(Global::Udt),
-            })
-    }
+) -> Option<Global<'_>> {
+    fir_store
+        .get(id.package)
+        .and_then(|package| match &package.items.get(id.item)?.kind {
+            ItemKind::Callable(callable) => Some(Global::Callable(callable)),
+            ItemKind::Namespace(..) => None,
+            ItemKind::Ty(..) => Some(Global::Udt),
+        })
 }
 
 #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
@@ -798,3 +642,30 @@ impl<'a> Visitor<'a> for BreakpointCollector<'a> {
             .expect("couldn't find stmt in FIR")
     }
 }
+
+fn eval_error(
+    package_store: &PackageStore,
+    fir_store: &IndexMap<PackageId, qsc_fir::fir::Package>,
+    call_stack: Vec<Frame>,
+    error: qsc_eval::Error,
+) -> Vec<Error> {
+    let stack_trace = if call_stack.is_empty() {
+        None
+    } else {
+        Some(format_call_stack(
+            package_store,
+            &Lookup { fir_store },
+            call_stack,
+            &error,
+        ))
+    };
+
+    vec![error::from_eval(error, package_store, stack_trace).into()]
+}
+
+fn into_errors(errors: Vec<WithSource<crate::compile::Error>>) -> Vec<Error> {
+    errors
+        .into_iter()
+        .map(|error| Error::Compile(error.into_with_source()))
+        .collect::<Vec<_>>()
+}
diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs
index c558e35c..543fd9bd 100644
--- a/compiler/qsc/src/lib.rs
+++ b/compiler/qsc/src/lib.rs
@@ -6,9 +6,12 @@
 
 pub mod compile;
 mod error;
+pub mod incremental;
 pub mod interpret;
 
-pub use qsc_frontend::compile::{CompileUnit, PackageStore, SourceContents, SourceMap, SourceName};
+pub use qsc_frontend::compile::{
+    CompileUnit, PackageStore, SourceContents, SourceMap, SourceName, TargetProfile,
+};
 
 pub mod resolve {
     pub use qsc_frontend::resolve::Res;
@@ -28,9 +31,7 @@ pub mod ast {
 
 pub use qsc_data_structures::span::Span;
 
-pub use qsc_frontend::compile::TargetProfile;
-
-pub use qsc_passes::PackageType;
+pub use qsc_passes::{PackageType, PassContext};
 
 pub use qsc_eval::{
     backend::{Backend, SparseSim},
diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs
index 5e6b0033..6222012d 100644
--- a/compiler/qsc_ast/src/ast.rs
+++ b/compiler/qsc_ast/src/ast.rs
@@ -68,6 +68,12 @@ impl Display for NodeId {
     }
 }
 
+impl From<usize> for NodeId {
+    fn from(value: usize) -> Self {
+        Self(u32::try_from(value).expect("node ID should fit in u32"))
+    }
+}
+
 impl From<NodeId> for usize {
     fn from(value: NodeId) -> Self {
         assert!(!value.is_default(), "default node ID should be replaced");
@@ -109,8 +115,8 @@ impl Hash for NodeId {
 pub struct Package {
     /// The node ID.
     pub id: NodeId,
-    /// The namespaces in the package.
-    pub namespaces: Box<[Namespace]>,
+    /// The top-level syntax nodes in the package.
+    pub nodes: Box<[TopLevelNode]>,
     /// The entry expression for an executable package.
     pub entry: Option<Box<Expr>>,
 }
@@ -123,13 +129,31 @@ impl Display for Package {
         if let Some(e) = &self.entry {
             write!(indent, "\nentry expression: {e}")?;
         }
-        for ns in &*self.namespaces {
-            write!(indent, "\n{ns}")?;
+        for node in &*self.nodes {
+            write!(indent, "\n{node}")?;
         }
         Ok(())
     }
 }
 
+/// A node that can exist at the top level of a package.
+#[derive(Clone, Debug, PartialEq)]
+pub enum TopLevelNode {
+    /// A namespace
+    Namespace(Namespace),
+    /// A statement
+    Stmt(Box<Stmt>),
+}
+
+impl Display for TopLevelNode {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Namespace(n) => n.fmt(f),
+            Self::Stmt(s) => s.fmt(f),
+        }
+    }
+}
+
 /// A namespace.
 #[derive(Clone, Debug, PartialEq)]
 pub struct Namespace {
diff --git a/compiler/qsc_ast/src/mut_visit.rs b/compiler/qsc_ast/src/mut_visit.rs
index 8e1a4963..b1a445ae 100644
--- a/compiler/qsc_ast/src/mut_visit.rs
+++ b/compiler/qsc_ast/src/mut_visit.rs
@@ -4,7 +4,8 @@
 use crate::ast::{
     Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FunctorExpr, FunctorExprKind, Ident,
     Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, QubitInitKind, SpecBody,
-    SpecDecl, Stmt, StmtKind, StringComponent, Ty, TyDef, TyDefKind, TyKind, Visibility,
+    SpecDecl, Stmt, StmtKind, StringComponent, TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
+    Visibility,
 };
 use qsc_data_structures::span::Span;
 
@@ -79,10 +80,10 @@ pub trait MutVisitor: Sized {
 }
 
 pub fn walk_package(vis: &mut impl MutVisitor, package: &mut Package) {
-    package
-        .namespaces
-        .iter_mut()
-        .for_each(|n| vis.visit_namespace(n));
+    package.nodes.iter_mut().for_each(|n| match n {
+        TopLevelNode::Namespace(ns) => vis.visit_namespace(ns),
+        TopLevelNode::Stmt(stmt) => vis.visit_stmt(stmt),
+    });
     package.entry.iter_mut().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_ast/src/visit.rs b/compiler/qsc_ast/src/visit.rs
index be85e12c..d80b09ca 100644
--- a/compiler/qsc_ast/src/visit.rs
+++ b/compiler/qsc_ast/src/visit.rs
@@ -4,7 +4,8 @@
 use crate::ast::{
     Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FunctorExpr, FunctorExprKind, Ident,
     Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, QubitInitKind, SpecBody,
-    SpecDecl, Stmt, StmtKind, StringComponent, Ty, TyDef, TyDefKind, TyKind, Visibility,
+    SpecDecl, Stmt, StmtKind, StringComponent, TopLevelNode, Ty, TyDef, TyDefKind, TyKind,
+    Visibility,
 };
 
 pub trait Visitor<'a>: Sized {
@@ -74,10 +75,10 @@ pub trait Visitor<'a>: Sized {
 }
 
 pub fn walk_package<'a>(vis: &mut impl Visitor<'a>, package: &'a Package) {
-    package
-        .namespaces
-        .iter()
-        .for_each(|n| vis.visit_namespace(n));
+    package.nodes.iter().for_each(|n| match n {
+        TopLevelNode::Namespace(ns) => vis.visit_namespace(ns),
+        TopLevelNode::Stmt(stmt) => vis.visit_stmt(stmt),
+    });
     package.entry.iter().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_eval/src/lower.rs b/compiler/qsc_eval/src/lower.rs
index 687a0c41..d15e906d 100644
--- a/compiler/qsc_eval/src/lower.rs
+++ b/compiler/qsc_eval/src/lower.rs
@@ -39,27 +39,6 @@ impl Lowerer {
         }
     }
 
-    /// Used to update the package with the lowered items.
-    /// Incremental compilation requires that we update the package
-    /// instead of returning a new one.
-    pub fn update_package(&mut self, package: &mut fir::Package) {
-        for (id, value) in self.blocks.drain() {
-            package.blocks.insert(id, value);
-        }
-
-        for (id, value) in self.exprs.drain() {
-            package.exprs.insert(id, value);
-        }
-
-        for (id, value) in self.pats.drain() {
-            package.pats.insert(id, value);
-        }
-
-        for (id, value) in self.stmts.drain() {
-            package.stmts.insert(id, value);
-        }
-    }
-
     pub fn lower_package(&mut self, package: &hir::Package) -> fir::Package {
         let entry = package.entry.as_ref().map(|e| self.lower_expr(e));
         let items: IndexMap<LocalItemId, fir::Item> = package
@@ -69,6 +48,11 @@ impl Lowerer {
             .map(|i| (i.id, i))
             .collect();
 
+        // Lower top-level statements
+        for stmt in &package.stmts {
+            self.lower_stmt(stmt);
+        }
+
         let blocks: IndexMap<_, _> = self.blocks.drain().collect();
         let exprs: IndexMap<_, _> = self.exprs.drain().collect();
         let pats: IndexMap<_, _> = self.pats.drain().collect();
@@ -86,6 +70,57 @@ impl Lowerer {
         package
     }
 
+    /// Used to update the package with the lowered items.
+    /// Incremental compilation requires that we update the package
+    /// instead of returning a new one.
+    #[allow(clippy::similar_names)]
+    pub fn lower_and_update_package(
+        &mut self,
+        fir_package: &mut fir::Package,
+        hir_package: &hir::Package,
+    ) -> Vec<StmtId> {
+        let items: IndexMap<LocalItemId, fir::Item> = hir_package
+            .items
+            .values()
+            .map(|i| self.lower_item(i))
+            .map(|i| (i.id, i))
+            .collect();
+
+        let new_stmts = hir_package
+            .stmts
+            .iter()
+            .map(|s| self.lower_stmt(s))
+            .collect();
+
+        self.update_package(fir_package);
+
+        for (k, v) in items {
+            fir_package.items.insert(k, v);
+        }
+
+        qsc_fir::validate::validate(fir_package);
+
+        new_stmts
+    }
+
+    fn update_package(&mut self, package: &mut fir::Package) {
+        for (id, value) in self.blocks.drain() {
+            package.blocks.insert(id, value);
+        }
+
+        for (id, value) in self.exprs.drain() {
+            package.exprs.insert(id, value);
+        }
+
+        for (id, value) in self.pats.drain() {
+            package.pats.insert(id, value);
+        }
+
+        for (id, value) in self.stmts.drain() {
+            package.stmts.insert(id, value);
+        }
+    }
+
     fn lower_item(&mut self, item: &hir::Item) -> fir::Item {
         let kind = match &item.kind {
             hir::ItemKind::Namespace(name, items) => {
@@ -117,7 +152,7 @@ impl Lowerer {
         }
     }
 
-    pub fn lower_callable_decl(&mut self, decl: &hir::CallableDecl) -> fir::CallableDecl {
+    fn lower_callable_decl(&mut self, decl: &hir::CallableDecl) -> fir::CallableDecl {
         let id = self.lower_id(decl.id);
         let kind = lower_callable_kind(decl.kind);
         let name = self.lower_ident(&decl.name);
@@ -197,7 +232,7 @@ impl Lowerer {
         id
     }
 
-    pub fn lower_stmt(&mut self, stmt: &hir::Stmt) -> fir::StmtId {
+    fn lower_stmt(&mut self, stmt: &hir::Stmt) -> fir::StmtId {
         let id = self.assigner.next_stmt();
         let kind = match &stmt.kind {
             hir::StmtKind::Expr(expr) => fir::StmtKind::Expr(self.lower_expr(expr)),
@@ -658,6 +693,6 @@ fn lower_functor_set_value(value: qsc_hir::ty::FunctorSetValue) -> qsc_fir::ty::
 
 #[must_use]
 #[allow(clippy::module_name_repetitions)]
-pub fn lower_local_item_id(id: qsc_hir::hir::LocalItemId) -> LocalItemId {
+fn lower_local_item_id(id: qsc_hir::hir::LocalItemId) -> LocalItemId {
     LocalItemId::from(usize::from(id))
 }
diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs
index 238b7e65..2e4b177a 100644
--- a/compiler/qsc_frontend/src/compile.rs
+++ b/compiler/qsc_frontend/src/compile.rs
@@ -15,8 +15,11 @@ use crate::{
 use miette::{Diagnostic, Report};
 use preprocess::TrackedName;
 use qsc_ast::{
-    assigner::Assigner as AstAssigner, ast, mut_visit::MutVisitor,
-    validate::Validator as AstValidator, visit::Visitor as _,
+    assigner::Assigner as AstAssigner,
+    ast::{self, TopLevelNode},
+    mut_visit::MutVisitor,
+    validate::Validator as AstValidator,
+    visit::Visitor as _,
 };
 use qsc_data_structures::{
     index_map::{self, IndexMap},
@@ -212,13 +215,67 @@ impl PackageStore {
     }
 
     #[must_use]
-    pub fn get_mut(&mut self, id: PackageId) -> (&global::Table, Option<&mut CompileUnit>) {
-        (&self.core, self.units.get_mut(id))
+    pub fn iter(&self) -> Iter {
+        Iter(self.units.iter())
     }
 
+    /// "Opens" the package store. This inserts an empty
+    /// package into the store, which will be considered
+    /// the open package and which can be incrementally updated.
     #[must_use]
-    pub fn iter(&self) -> Iter {
-        Iter(self.units.iter())
+    pub fn open(mut self) -> OpenPackageStore {
+        let id = self.next_id;
+        self.next_id = id.successor();
+        self.units.insert(id, CompileUnit::default());
+
+        OpenPackageStore {
+            store: self,
+            open: id,
+        }
+    }
+}
+
+/// A package store that contains one mutable `CompileUnit`.
+pub struct OpenPackageStore {
+    store: PackageStore,
+    open: PackageId,
+}
+
+impl OpenPackageStore {
+    /// Returns a reference to the underlying, immutable,
+    /// package store.
+    #[must_use]
+    pub fn package_store(&self) -> &PackageStore {
+        &self.store
+    }
+
+    /// Returns the ID of the open package.
+    #[must_use]
+    pub fn open_package_id(&self) -> PackageId {
+        self.open
+    }
+
+    /// Returns a mutable reference to the open package,
+    /// along with a reference to the core library that can be used
+    /// to perform passes.
+    #[must_use]
+    pub fn get_open_mut(&mut self) -> (&global::Table, &mut CompileUnit) {
+        let id = self.open;
+
+        (
+            &self.store.core,
+            self.store
+                .units
+                .get_mut(id)
+                .expect("open package id should exist in store"),
+        )
+    }
+
+    /// Consumes the `OpenPackageStore` and returns a `PackageStore`
+    /// along with the id of the formerly open package.
+    #[must_use]
+    pub fn into_package_store(self) -> (PackageStore, PackageId) {
+        (self.store, self.open)
     }
 }
 
@@ -400,7 +457,7 @@ fn parse_all(sources: &SourceMap) -> (ast::Package, Vec<qsc_parse::Error>) {
         let (source_namespaces, source_errors) = qsc_parse::namespaces(&source.contents);
         for mut namespace in source_namespaces {
             Offsetter(source.offset).visit_namespace(&mut namespace);
-            namespaces.push(namespace);
+            namespaces.push(TopLevelNode::Namespace(namespace));
         }
 
         append_parse_errors(&mut errors, source.offset, source_errors);
@@ -419,7 +476,7 @@ fn parse_all(sources: &SourceMap) -> (ast::Package, Vec<qsc_parse::Error>) {
 
     let package = ast::Package {
         id: ast::NodeId::default(),
-        namespaces: namespaces.into_boxed_slice(),
+        nodes: namespaces.into_boxed_slice(),
         entry,
     };
 
diff --git a/compiler/qsc_frontend/src/error.rs b/compiler/qsc_frontend/src/error.rs
index 601a3283..5e7a47a2 100644
--- a/compiler/qsc_frontend/src/error.rs
+++ b/compiler/qsc_frontend/src/error.rs
@@ -12,10 +12,7 @@ use std::{
 };
 
 #[derive(Clone, Debug)]
-pub struct WithSource<E>
-where
-    E: Diagnostic,
-{
+pub struct WithSource<E> {
     sources: Vec<Source>,
     error: E,
 }
@@ -24,15 +21,16 @@ impl<E: Diagnostic> WithSource<E> {
     pub fn error(&self) -> &E {
         &self.error
     }
+
+    pub fn into_error(self) -> E {
+        self.error
+    }
 }
 
 impl<E: Diagnostic> WithSource<E> {
     /// Construct a diagnostic with source information from a source map.
     /// Since errors may contain labeled spans from any source file in the
     /// compilation, the entire source map is needed to resolve offsets.
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
     pub fn from_map(sources: &SourceMap, error: E) -> Self {
         // Filter the source map to the relevant sources
         // to avoid cloning all of them.
@@ -60,6 +58,16 @@ impl<E: Diagnostic> WithSource<E> {
             error,
         }
     }
+
+    pub fn into_with_source<T>(self) -> WithSource<T>
+    where
+        T: From<E>,
+    {
+        WithSource {
+            sources: self.sources,
+            error: self.error.into(),
+        }
+    }
 }
 
 impl<E: Diagnostic> Error for WithSource<E> {
diff --git a/compiler/qsc_frontend/src/incremental.rs b/compiler/qsc_frontend/src/incremental.rs
index 1c42e275..bda1b0b6 100644
--- a/compiler/qsc_frontend/src/incremental.rs
+++ b/compiler/qsc_frontend/src/incremental.rs
@@ -5,43 +5,33 @@
 mod tests;
 
 use crate::{
-    compile::{preprocess, CompileUnit, Offsetter, PackageStore, TargetProfile},
-    lower::{self, Lowerer},
+    compile::{
+        self, preprocess, AstPackage, CompileUnit, Offsetter, PackageStore, SourceMap,
+        TargetProfile,
+    },
+    error::WithSource,
+    lower::Lowerer,
     resolve::{self, Resolver},
     typeck::{self, Checker},
 };
-use miette::Diagnostic;
-use qsc_ast::{assigner::Assigner as AstAssigner, ast, mut_visit::MutVisitor, visit::Visitor};
+use qsc_ast::{
+    assigner::Assigner as AstAssigner,
+    ast::{self, Stmt, TopLevelNode},
+    mut_visit::MutVisitor,
+    validate::Validator as AstValidator,
+    visit::Visitor as AstVisitor,
+};
 use qsc_hir::{
     assigner::Assigner as HirAssigner,
     hir::{self, PackageId},
+    validate::Validator as HirValidator,
+    visit::Visitor as HirVisitor,
 };
-use thiserror::Error;
-
-#[derive(Clone, Debug, Diagnostic, Error)]
-#[diagnostic(transparent)]
-#[error(transparent)]
-pub struct Error(ErrorKind);
-
-#[derive(Clone, Debug, Diagnostic, Error)]
-#[diagnostic(transparent)]
-enum ErrorKind {
-    #[error("syntax error")]
-    Parse(#[from] qsc_parse::Error),
-    #[error("name error")]
-    Resolve(#[from] resolve::Error),
-    #[error("type error")]
-    Type(#[from] typeck::Error),
-    #[error(transparent)]
-    Lower(#[from] lower::Error),
-}
-
-#[derive(Debug)]
-pub enum Fragment {
-    Stmt(hir::Stmt),
-    Item(hir::Item),
-}
+use std::mem::take;
 
+/// The frontend for an incremental compiler.
+/// It is used to update a single `CompileUnit`
+/// with additional sources.
 pub struct Compiler {
     ast_assigner: AstAssigner,
     resolver: Resolver,
@@ -50,10 +40,19 @@ pub struct Compiler {
     target: TargetProfile,
 }
 
+pub type Error = WithSource<compile::Error>;
+
+/// The result of an incremental compilation.
+/// These packages can be merged into the original
+/// `CompileUnit` that was used for the incremental compilation.
+#[derive(Debug)]
+pub struct Increment {
+    pub ast: AstPackage,
+    pub hir: hir::Package,
+}
+
 impl Compiler {
-    /// # Panics
-    ///
-    /// This function will panic if compiler state is invalid or in out-of-memory conditions.
+    /// Creates a new compiler.
     pub fn new(
         store: &PackageStore,
         dependencies: impl IntoIterator<Item = PackageId>,
@@ -86,227 +85,250 @@ impl Compiler {
         }
     }
 
-    /// Compile a string with a single fragment of Q# code that is an expression.
-    /// # Errors
-    /// Returns a vector of errors if the input fails compilation.
-    pub fn compile_expr(
+    /// Compiles Q# fragments.
+    ///
+    /// Uses the assigners and other mutable state from the passed in
+    /// `CompileUnit` to guarantee uniqueness, however does not
+    /// update the `CompileUnit` with the resulting AST and HIR packages.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit`.
+    pub fn compile_fragments(
         &mut self,
         unit: &mut CompileUnit,
         source_name: &str,
         source_contents: &str,
-    ) -> Result<Vec<Fragment>, Vec<Error>> {
-        let fragments = self.compile(unit, source_name, source_contents, |s| {
-            let (expr, errors) = qsc_parse::expr(s);
-            if !errors.is_empty() {
-                return (Vec::new(), errors);
-            }
+    ) -> Result<Increment, Vec<Error>> {
+        let (mut ast, parse_errors) =
+            Self::parse_fragments(&mut unit.sources, source_name, source_contents);
 
-            let fragment = qsc_parse::Fragment::Stmt(Box::new(ast::Stmt {
-                id: ast::NodeId::default(),
-                span: expr.span,
-                kind: Box::new(ast::StmtKind::Expr(expr)),
-            }));
-
-            (vec![fragment], errors)
-        })?;
+        if !parse_errors.is_empty() {
+            return Err(parse_errors);
+        }
 
-        Ok(fragments)
+        let (hir, lower_errors) = self.resolve_check_lower(unit, &mut ast);
+
+        if lower_errors.is_empty() {
+            Ok(Increment {
+                ast: AstPackage {
+                    package: ast,
+                    names: self.resolver.names().clone(),
+                    tys: self.checker.table().clone(),
+                },
+                hir,
+            })
+        } else {
+            self.lowerer.clear_items();
+            Err(lower_errors)
+        }
     }
 
-    /// Compile a string with one or more fragments of Q# code.
-    /// # Errors
-    /// Returns a vector of errors if any of the input fails compilation.
-    pub fn compile_fragments(
+    /// Compiles an entry expression.
+    ///
+    /// Uses the assigners and other mutable state from the passed in
+    /// `CompileUnit` to guarantee uniqueness, however does not
+    /// update the `CompileUnit` with the resulting AST and HIR packages.
+    ///
+    /// The caller can use the returned packages to perform passes,
+    /// get information about the newly added items, or do other modifications.
+    /// It is then the caller's responsibility to merge
+    /// these packages into the current `CompileUnit`.
+    pub fn compile_expr(
         &mut self,
         unit: &mut CompileUnit,
         source_name: &str,
         source_contents: &str,
-    ) -> Result<Vec<Fragment>, Vec<Error>> {
-        self.compile(unit, source_name, source_contents, qsc_parse::fragments)
-    }
+    ) -> Result<Increment, Vec<Error>> {
+        let (mut ast, parse_errors) =
+            Self::parse_expr(&mut unit.sources, source_name, source_contents);
 
-    fn compile<F>(
-        &mut self,
-        unit: &mut CompileUnit,
-        source_name: &str,
-        source_contents: &str,
-        parse: F,
-    ) -> Result<Vec<Fragment>, Vec<Error>>
-    where
-        F: Fn(&str) -> (Vec<qsc_parse::Fragment>, Vec<qsc_parse::Error>),
-    {
-        // Append the line to the source map with the appropriate offset
-        let offset = unit
-            .sources
-            .push(source_name.into(), source_contents.into());
-
-        let (mut fragments, errors) = parse(source_contents);
-        if !errors.is_empty() {
-            return Err(errors
-                .into_iter()
-                .map(|e| Error(ErrorKind::Parse(e.with_offset(offset))))
-                .collect());
+        if !parse_errors.is_empty() {
+            return Err(parse_errors);
         }
 
-        let mut offsetter = Offsetter(offset);
-        for fragment in &mut fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => offsetter.visit_namespace(namespace),
-                qsc_parse::Fragment::Stmt(stmt) => offsetter.visit_stmt(stmt),
-            }
-        }
+        let (package, errors) = self.resolve_check_lower(unit, &mut ast);
 
-        let mut cond_compile = preprocess::Conditional::new(self.target);
-        for fragment in &mut fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    cond_compile.visit_namespace(namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    cond_compile.visit_stmt(stmt);
-                }
-            }
+        if errors.is_empty() {
+            Ok(Increment {
+                ast: AstPackage {
+                    package: ast,
+                    names: self.resolver.names().clone(),
+                    tys: self.checker.table().clone(),
+                },
+                hir: package,
+            })
+        } else {
+            self.lowerer.clear_items();
+            Err(errors)
         }
-        self.resolver
-            .extend_dropped_names(cond_compile.into_names());
+    }
 
-        // Namespaces must be processed before top-level statements, so sort the fragments.
-        // Note that stable sorting is used here to preserve the order of top-level statements.
-        fragments.sort_by_key(|f| match f {
-            qsc_parse::Fragment::Namespace(_) => 0,
-            qsc_parse::Fragment::Stmt(_) => 1,
-        });
+    pub fn update(&mut self, unit: &mut CompileUnit, new: Increment) {
+        // Update the AST
+        unit.ast.package = self.concat_ast(take(&mut unit.ast.package), new.ast.package);
 
-        self.assign_ast_ids(&mut fragments);
+        // The new `Increment` will contain the names and tys
+        // from the original package as well, so just
+        // replace the current tables instead of extending.
+        unit.ast.names = new.ast.names;
+        unit.ast.tys = new.ast.tys;
+
+        // Update the HIR
+        extend_hir(&mut unit.package, new.hir);
+    }
+
+    fn resolve_check_lower(
+        &mut self,
+        unit: &mut CompileUnit,
+        ast: &mut ast::Package,
+    ) -> (hir::Package, Vec<Error>) {
+        let mut cond_compile = preprocess::Conditional::new(self.target);
+        cond_compile.visit_package(ast);
 
-        self.bind_items(&mut unit.assigner, &fragments);
+        self.ast_assigner.visit_package(ast);
 
-        self.resolve(&mut unit.assigner, &fragments);
+        self.resolver
+            .extend_dropped_names(cond_compile.into_names());
+        self.resolver.bind_fragments(ast, &mut unit.assigner);
+        self.resolver.with(&mut unit.assigner).visit_package(ast);
 
-        self.collect_items(&fragments);
+        self.checker.check_package(self.resolver.names(), ast);
+        self.checker.solve(self.resolver.names());
 
-        self.type_check(&fragments);
+        let package = self.lower(&mut unit.assigner, &*ast);
 
-        let fragments = fragments
+        let errors: Vec<Error> = self
+            .drain_errors()
             .into_iter()
-            .flat_map(|f| self.lower_fragment(&mut unit.assigner, f))
+            .map(|e| WithSource::from_map(&unit.sources, e))
             .collect();
 
-        let errors = self.drain_errors();
-        if errors.is_empty() {
-            Ok(fragments)
-        } else {
-            self.lowerer.clear_items();
-            Err(errors)
-        }
+        (package, errors)
     }
 
-    fn type_check(&mut self, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => self
-                    .checker
-                    .check_namespace_fragment(self.resolver.names(), namespace),
-                qsc_parse::Fragment::Stmt(stmt) => self
-                    .checker
-                    .check_stmt_fragment(self.resolver.names(), stmt),
-            }
-        }
-
-        self.checker.solve(self.resolver.names());
+    /// Creates a new `Package` by combining two packages.
+    /// The two packages should not contain any conflicting `NodeId`s.
+    /// Entry expressions are ignored.
+    #[must_use]
+    fn concat_ast(&mut self, mut left: ast::Package, right: ast::Package) -> ast::Package {
+        assert!(right.entry.is_none(), "package should not have entry expr");
+        assert!(left.entry.is_none(), "package should not have entry expr");
+
+        let mut nodes = Vec::with_capacity(left.nodes.len() + right.nodes.len());
+        nodes.extend(left.nodes.into_vec());
+        nodes.extend(right.nodes.into_vec());
+        left.id = self.ast_assigner.next_id();
+        left.nodes = nodes.into_boxed_slice();
+
+        AstValidator::default().visit_package(&left);
+        left
     }
 
-    fn collect_items(&mut self, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => self
-                    .checker
-                    .collect_namespace_items(self.resolver.names(), namespace),
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    self.checker.collect_stmt_items(self.resolver.names(), stmt);
-                }
-            }
-        }
-    }
+    fn parse_expr(
+        sources: &mut SourceMap,
+        source_name: &str,
+        source_contents: &str,
+    ) -> (ast::Package, Vec<Error>) {
+        let offset = sources.push(source_name.into(), source_contents.into());
+
+        let (expr, errors) = qsc_parse::expr(source_contents);
+        let mut stmt = Box::new(Stmt {
+            id: ast::NodeId::default(),
+            span: expr.span,
+            kind: Box::new(ast::StmtKind::Expr(expr)),
+        });
 
-    fn resolve(&mut self, hir_assigner: &mut HirAssigner, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    self.resolver.with(hir_assigner).visit_namespace(namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    self.resolver.with(hir_assigner).visit_stmt(stmt);
-                }
-            }
-        }
-    }
+        let mut offsetter = Offsetter(offset);
+        offsetter.visit_stmt(&mut stmt);
 
-    fn bind_items(&mut self, hir_assigner: &mut HirAssigner, fragments: &Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    self.resolver.bind_namespace_items(hir_assigner, namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => {
-                    if let ast::StmtKind::Item(item) = stmt.kind.as_ref() {
-                        self.resolver.bind_local_item(hir_assigner, item);
-                    }
-                }
-            }
-        }
+        let top_level_nodes = Box::new([TopLevelNode::Stmt(stmt)]);
+
+        let package = ast::Package {
+            id: ast::NodeId::default(),
+            nodes: top_level_nodes,
+            entry: None,
+        };
+
+        (package, with_source(errors, sources, offset))
     }
 
-    fn assign_ast_ids(&mut self, fragments: &mut Vec<qsc_parse::Fragment>) {
-        for fragment in fragments {
-            match fragment {
-                qsc_parse::Fragment::Namespace(namespace) => {
-                    self.ast_assigner.visit_namespace(namespace);
-                }
-                qsc_parse::Fragment::Stmt(stmt) => self.ast_assigner.visit_stmt(stmt),
+    fn parse_fragments(
+        sources: &mut SourceMap,
+        source_name: &str,
+        source_contents: &str,
+    ) -> (ast::Package, Vec<Error>) {
+        let offset = sources.push(source_name.into(), source_contents.into());
+
+        let (mut top_level_nodes, errors) = qsc_parse::top_level_nodes(source_contents);
+        let mut offsetter = Offsetter(offset);
+        for node in &mut top_level_nodes {
+            match node {
+                ast::TopLevelNode::Namespace(ns) => offsetter.visit_namespace(ns),
+                ast::TopLevelNode::Stmt(stmt) => offsetter.visit_stmt(stmt),
             }
         }
-    }
 
-    fn lower_fragment(
-        &mut self,
-        hir_assigner: &mut HirAssigner,
-        fragment: qsc_parse::Fragment,
-    ) -> Vec<Fragment> {
-        let fragment = match fragment {
-            qsc_parse::Fragment::Namespace(namespace) => {
-                self.lower_namespace(hir_assigner, &namespace);
-                None
-            }
-            qsc_parse::Fragment::Stmt(stmt) => self.lower_stmt(hir_assigner, &stmt),
+        let package = ast::Package {
+            id: ast::NodeId::default(),
+            nodes: top_level_nodes.into_boxed_slice(),
+            entry: None,
         };
 
-        self.lowerer
-            .drain_items()
-            .map(Fragment::Item)
-            .chain(fragment)
-            .collect()
-    }
-
-    fn lower_namespace(&mut self, hir_assigner: &mut HirAssigner, namespace: &ast::Namespace) {
-        self.lowerer
-            .with(hir_assigner, self.resolver.names(), self.checker.table())
-            .lower_namespace(namespace);
+        (package, with_source(errors, sources, offset))
     }
 
-    fn lower_stmt(&mut self, hir_assigner: &mut HirAssigner, stmt: &ast::Stmt) -> Option<Fragment> {
+    fn lower(&mut self, hir_assigner: &mut HirAssigner, package: &ast::Package) -> hir::Package {
         self.lowerer
             .with(hir_assigner, self.resolver.names(), self.checker.table())
-            .lower_stmt(stmt)
-            .map(Fragment::Stmt)
+            .lower_package(package)
     }
 
-    fn drain_errors(&mut self) -> Vec<Error> {
+    fn drain_errors(&mut self) -> Vec<compile::Error> {
         self.resolver
             .drain_errors()
-            .map(|e| Error(e.into()))
-            .chain(self.checker.drain_errors().map(|e| Error(e.into())))
-            .chain(self.lowerer.drain_errors().map(|e| Error(e.into())))
+            .map(|e| compile::Error(e.into()))
+            .chain(
+                self.checker
+                    .drain_errors()
+                    .map(|e| compile::Error(e.into())),
+            )
+            .chain(
+                self.lowerer
+                    .drain_errors()
+                    .map(|e| compile::Error(e.into())),
+            )
             .collect()
     }
 }
+
+/// Extends the `Package` with the contents of another `Package`.
+/// `other` should not contain any `LocalItemId`s
+/// that conflict with the current `Package`.
+/// The entry expression from `other` will be ignored.
+fn extend_hir(this: &mut hir::Package, mut other: hir::Package) {
+    for (k, v) in other.items.drain() {
+        this.items.insert(k, v);
+    }
+
+    this.stmts.extend(other.stmts);
+
+    HirValidator::default().visit_package(this);
+}
+
+fn with_source(
+    errors: Vec<qsc_parse::Error>,
+    sources: &SourceMap,
+    offset: u32,
+) -> Vec<WithSource<compile::Error>> {
+    errors
+        .into_iter()
+        .map(|e| {
+            WithSource::from_map(
+                sources,
+                compile::Error(compile::ErrorKind::Parse(e.with_offset(offset))),
+            )
+        })
+        .collect()
+}
diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs
index 1a626922..bdb312d2 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -74,10 +74,6 @@ impl Lowerer {
         self.items.clear();
     }
 
-    pub(super) fn drain_items(&mut self) -> vec::Drain<hir::Item> {
-        self.items.drain(..)
-    }
-
     pub(super) fn drain_errors(&mut self) -> vec::Drain<Error> {
         self.errors.drain(..)
     }
@@ -106,13 +102,25 @@ pub(super) struct With<'a> {
 
 impl With<'_> {
     pub(super) fn lower_package(&mut self, package: &ast::Package) -> hir::Package {
-        for namespace in &*package.namespaces {
-            self.lower_namespace(namespace);
+        let mut stmts = Vec::new();
+        for node in &*package.nodes {
+            match node {
+                ast::TopLevelNode::Namespace(namespace) => self.lower_namespace(namespace),
+                ast::TopLevelNode::Stmt(stmt) => {
+                    if let Some(stmt) = self.lower_stmt(stmt) {
+                        stmts.push(stmt);
+                    }
+                }
+            }
         }
 
         let entry = package.entry.as_ref().map(|e| self.lower_expr(e));
         let items = self.lowerer.items.drain(..).map(|i| (i.id, i)).collect();
-        hir::Package { items, entry }
+        hir::Package {
+            items,
+            stmts,
+            entry,
+        }
     }
 
     pub(super) fn lower_namespace(&mut self, namespace: &ast::Namespace) {
diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs
index bc722dea..4695d874 100644
--- a/compiler/qsc_frontend/src/resolve.rs
+++ b/compiler/qsc_frontend/src/resolve.rs
@@ -6,7 +6,7 @@ mod tests;
 
 use miette::Diagnostic;
 use qsc_ast::{
-    ast::{self, CallableDecl, Ident, NodeId},
+    ast::{self, CallableDecl, Ident, NodeId, TopLevelNode},
     visit::{self as ast_visit, Visitor as AstVisitor},
 };
 use qsc_data_structures::{index_map::IndexMap, span::Span};
@@ -220,6 +220,27 @@ impl Resolver {
         self.dropped_names.extend(dropped_names);
     }
 
+    pub(super) fn bind_fragments(&mut self, ast: &mut ast::Package, assigner: &mut Assigner) {
+        for node in &mut ast.nodes.iter() {
+            match node {
+                ast::TopLevelNode::Namespace(namespace) => {
+                    bind_global_items(
+                        &mut self.names,
+                        &mut self.globals,
+                        namespace,
+                        assigner,
+                        &mut self.errors,
+                    );
+                }
+                ast::TopLevelNode::Stmt(stmt) => {
+                    if let ast::StmtKind::Item(item) = stmt.kind.as_ref() {
+                        self.bind_local_item(assigner, item);
+                    }
+                }
+            }
+        }
+    }
+
     fn resolve_ident(&mut self, kind: NameKind, name: &Ident) {
         let namespace = None;
         match resolve(kind, &self.globals, &self.scopes, name, &namespace) {
@@ -311,34 +332,6 @@ impl Resolver {
         }
     }
 
-    pub(super) fn bind_namespace_items(
-        &mut self,
-        assigner: &mut Assigner,
-        namespace: &ast::Namespace,
-    ) {
-        if !self.names.contains_key(namespace.name.id) {
-            let id = assigner.next_item();
-            self.names
-                .insert(namespace.name.id, Res::Item(intrapackage(id)));
-            self.globals
-                .namespaces
-                .insert(Rc::clone(&namespace.name.name));
-
-            for item in &*namespace.items {
-                match bind_global_item(
-                    &mut self.names,
-                    &mut self.globals,
-                    &namespace.name.name,
-                    || intrapackage(assigner.next_item()),
-                    item,
-                ) {
-                    Ok(()) => {}
-                    Err(error) => self.errors.push(error),
-                }
-            }
-        }
-    }
-
     fn bind_type_parameters(&mut self, decl: &CallableDecl) {
         decl.generics.iter().enumerate().for_each(|(ix, ident)| {
             let scope = self
@@ -388,8 +381,6 @@ impl With<'_> {
 
 impl AstVisitor<'_> for With<'_> {
     fn visit_namespace(&mut self, namespace: &ast::Namespace) {
-        self.resolver.bind_namespace_items(self.assigner, namespace);
-
         let kind = ScopeKind::Namespace(Rc::clone(&namespace.name.name));
         self.with_scope(kind, |visitor| {
             for item in &*namespace.items {
@@ -550,25 +541,19 @@ impl GlobalTable {
         package: &ast::Package,
     ) -> Vec<Error> {
         let mut errors = Vec::new();
-        for namespace in &*package.namespaces {
-            self.names.insert(
-                namespace.name.id,
-                Res::Item(intrapackage(assigner.next_item())),
-            );
-            self.scope
-                .namespaces
-                .insert(Rc::clone(&namespace.name.name));
-
-            for item in &*namespace.items {
-                match bind_global_item(
-                    &mut self.names,
-                    &mut self.scope,
-                    &namespace.name.name,
-                    || intrapackage(assigner.next_item()),
-                    item,
-                ) {
-                    Ok(()) => {}
-                    Err(error) => errors.push(error),
+        for node in &*package.nodes {
+            match node {
+                TopLevelNode::Namespace(namespace) => {
+                    bind_global_items(
+                        &mut self.names,
+                        &mut self.scope,
+                        namespace,
+                        assigner,
+                        &mut errors,
+                    );
+                }
+                TopLevelNode::Stmt(_) => {
+                    unimplemented!("did not expect top-level statements in the ast")
                 }
             }
         }
@@ -602,6 +587,33 @@ impl GlobalTable {
     }
 }
 
+fn bind_global_items(
+    names: &mut IndexMap<NodeId, Res>,
+    scope: &mut GlobalScope,
+    namespace: &ast::Namespace,
+    assigner: &mut Assigner,
+    errors: &mut Vec<Error>,
+) {
+    names.insert(
+        namespace.name.id,
+        Res::Item(intrapackage(assigner.next_item())),
+    );
+    scope.namespaces.insert(Rc::clone(&namespace.name.name));
+
+    for item in &*namespace.items {
+        match bind_global_item(
+            names,
+            scope,
+            &namespace.name.name,
+            || intrapackage(assigner.next_item()),
+            item,
+        ) {
+            Ok(()) => {}
+            Err(error) => errors.push(error),
+        }
+    }
+}
+
 /// Tries to extract a field name from an expression in cases where it is syntactically ambiguous
 /// whether the expression is a field name or a variable name. This applies to the index operand in
 /// a ternary update operator.
diff --git a/compiler/qsc_frontend/src/typeck.rs b/compiler/qsc_frontend/src/typeck.rs
index 8ecf6a12..eca3314f 100644
--- a/compiler/qsc_frontend/src/typeck.rs
+++ b/compiler/qsc_frontend/src/typeck.rs
@@ -20,7 +20,7 @@ use thiserror::Error;
 
 pub(super) use check::{Checker, GlobalTable};
 
-#[derive(Debug, Default)]
+#[derive(Debug, Default, Clone)]
 pub struct Table {
     pub udts: HashMap<ItemId, Udt>,
 
diff --git a/compiler/qsc_frontend/src/typeck/check.rs b/compiler/qsc_frontend/src/typeck/check.rs
index 6bd81121..bb08b979 100644
--- a/compiler/qsc_frontend/src/typeck/check.rs
+++ b/compiler/qsc_frontend/src/typeck/check.rs
@@ -11,7 +11,7 @@ use crate::{
     typeck::convert::{self, MissingTyError},
 };
 use qsc_ast::{
-    ast::{self, NodeId},
+    ast::{self, NodeId, TopLevelNode},
     visit::{self, Visitor},
 };
 use qsc_data_structures::index_map::IndexMap;
@@ -105,6 +105,18 @@ impl Checker {
                 entry,
             ));
         }
+
+        for top_level_node in &*package.nodes {
+            if let TopLevelNode::Stmt(stmt) = top_level_node {
+                self.new.append(&mut rules::stmt(
+                    names,
+                    &self.globals,
+                    &mut self.table,
+                    &mut self.inferrer,
+                    stmt,
+                ));
+            }
+        }
     }
 
     fn check_callable_decl(&mut self, names: &Names, decl: &ast::CallableDecl) {
@@ -163,30 +175,6 @@ impl Checker {
         ));
     }
 
-    pub(crate) fn collect_namespace_items(&mut self, names: &Names, namespace: &ast::Namespace) {
-        ItemCollector::new(self, names).visit_namespace(namespace);
-    }
-
-    pub(crate) fn collect_stmt_items(&mut self, names: &Names, stmt: &ast::Stmt) {
-        ItemCollector::new(self, names).visit_stmt(stmt);
-    }
-
-    pub(crate) fn check_namespace_fragment(&mut self, names: &Names, namespace: &ast::Namespace) {
-        ItemChecker::new(self, names).visit_namespace(namespace);
-    }
-
-    pub(crate) fn check_stmt_fragment(&mut self, names: &Names, stmt: &ast::Stmt) {
-        ItemChecker::new(self, names).visit_stmt(stmt);
-
-        self.new.append(&mut rules::stmt_fragment(
-            names,
-            &self.globals,
-            &mut self.table,
-            &mut self.inferrer,
-            stmt,
-        ));
-    }
-
     pub(crate) fn solve(&mut self, names: &Names) {
         self.errors.append(&mut rules::solve(
             names,
diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs
index 8d0d5c6a..b83ba304 100644
--- a/compiler/qsc_frontend/src/typeck/rules.rs
+++ b/compiler/qsc_frontend/src/typeck/rules.rs
@@ -790,7 +790,7 @@ pub(super) fn expr(
     context.solve()
 }
 
-pub(super) fn stmt_fragment(
+pub(super) fn stmt(
     names: &Names,
     globals: &HashMap<ItemId, Scheme>,
     table: &mut Table,
diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs
index 23a82190..3edf6dee 100644
--- a/compiler/qsc_hir/src/hir.rs
+++ b/compiler/qsc_hir/src/hir.rs
@@ -232,6 +232,8 @@ impl Display for Res {
 pub struct Package {
     /// The items in the package.
     pub items: IndexMap<LocalItemId, Item>,
+    /// The top-level statements in the package.
+    pub stmts: Vec<Stmt>,
     /// The entry expression for an executable package.
     pub entry: Option<Expr>,
 }
@@ -247,6 +249,9 @@ impl Display for Package {
         for item in self.items.values() {
             write!(indent, "\n{item}")?;
         }
+        for stmt in &self.stmts {
+            write!(indent, "\n{stmt}")?;
+        }
         Ok(())
     }
 }
diff --git a/compiler/qsc_hir/src/mut_visit.rs b/compiler/qsc_hir/src/mut_visit.rs
index 0186cba3..48421410 100644
--- a/compiler/qsc_hir/src/mut_visit.rs
+++ b/compiler/qsc_hir/src/mut_visit.rs
@@ -53,6 +53,7 @@ pub trait MutVisitor: Sized {
 
 pub fn walk_package(vis: &mut impl MutVisitor, package: &mut Package) {
     package.items.values_mut().for_each(|i| vis.visit_item(i));
+    package.stmts.iter_mut().for_each(|s| vis.visit_stmt(s));
     package.entry.iter_mut().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_hir/src/visit.rs b/compiler/qsc_hir/src/visit.rs
index 8348dd8a..f979e381 100644
--- a/compiler/qsc_hir/src/visit.rs
+++ b/compiler/qsc_hir/src/visit.rs
@@ -48,6 +48,7 @@ pub trait Visitor<'a>: Sized {
 
 pub fn walk_package<'a>(vis: &mut impl Visitor<'a>, package: &'a Package) {
     package.items.values().for_each(|i| vis.visit_item(i));
+    package.stmts.iter().for_each(|s| vis.visit_stmt(s));
     package.entry.iter().for_each(|e| vis.visit_expr(e));
 }
 
diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs
index 76937ab8..66f2795d 100644
--- a/compiler/qsc_parse/src/item.rs
+++ b/compiler/qsc_parse/src/item.rs
@@ -22,16 +22,11 @@ use crate::{
 };
 use qsc_ast::ast::{
     Attr, Block, CallableBody, CallableDecl, CallableKind, Ident, Item, ItemKind, Namespace,
-    NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, Stmt, StmtKind, Ty, TyDef,
-    TyDefKind, TyKind, Visibility, VisibilityKind,
+    NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, TopLevelNode, Ty,
+    TyDef, TyDefKind, TyKind, Visibility, VisibilityKind,
 };
 use qsc_data_structures::span::Span;
 
-pub enum Fragment {
-    Namespace(Namespace),
-    Stmt(Box<Stmt>),
-}
-
 pub(super) fn parse(s: &mut Scanner) -> Result<Box<Item>> {
     let lo = s.peek().span.lo;
     let doc = parse_doc(s);
@@ -97,13 +92,13 @@ pub(super) fn parse_namespaces(s: &mut Scanner) -> Result<Vec<Namespace>> {
     Ok(namespaces)
 }
 
-pub(super) fn parse_fragments(s: &mut Scanner) -> Result<Vec<Fragment>> {
-    let fragments = many(s, parse_fragment)?;
+pub(super) fn parse_top_level_nodes(s: &mut Scanner) -> Result<Vec<TopLevelNode>> {
+    let nodes = many(s, parse_top_level_node)?;
     recovering_token(s, TokenKind::Eof)?;
-    Ok(fragments)
+    Ok(nodes)
 }
 
-fn parse_fragment(s: &mut Scanner) -> Result<Fragment> {
+fn parse_top_level_node(s: &mut Scanner) -> Result<TopLevelNode> {
     // Here we parse any doc comments ahead of calling `parse_namespace` or `stmt::parse` in order
     // to avoid problems with error reporting. Specifically, if `parse_namespace` consumes the
     // doc comment and then fails to find a namespace, that becomes an unrecoverable error even with
@@ -111,7 +106,7 @@ fn parse_fragment(s: &mut Scanner) -> Result<Fragment> {
     let doc = parse_doc(s).unwrap_or_default();
     if let Some(mut namespace) = opt(s, parse_namespace)? {
         namespace.doc = doc.into();
-        Ok(Fragment::Namespace(namespace))
+        Ok(TopLevelNode::Namespace(namespace))
     } else {
         let kind = s.peek().kind;
         let span = s.peek().span;
@@ -121,7 +116,7 @@ fn parse_fragment(s: &mut Scanner) -> Result<Fragment> {
         } else if !doc.is_empty() {
             return Err(Error(ErrorKind::Rule("item", kind, span)));
         }
-        Ok(Fragment::Stmt(stmt))
+        Ok(TopLevelNode::Stmt(stmt))
     }
 }
 
diff --git a/compiler/qsc_parse/src/lib.rs b/compiler/qsc_parse/src/lib.rs
index aaf3adef..9d4e674f 100644
--- a/compiler/qsc_parse/src/lib.rs
+++ b/compiler/qsc_parse/src/lib.rs
@@ -18,14 +18,12 @@ mod ty;
 
 use lex::TokenKind;
 use miette::Diagnostic;
-use qsc_ast::ast::{Expr, Namespace};
+use qsc_ast::ast::{Expr, Namespace, TopLevelNode};
 use qsc_data_structures::span::Span;
 use scan::Scanner;
 use std::result;
 use thiserror::Error;
 
-pub use item::Fragment;
-
 #[derive(Clone, Copy, Debug, Diagnostic, Eq, Error, PartialEq)]
 #[error(transparent)]
 #[diagnostic(transparent)]
@@ -102,10 +100,10 @@ pub fn namespaces(input: &str) -> (Vec<Namespace>, Vec<Error>) {
     }
 }
 
-pub fn fragments(input: &str) -> (Vec<Fragment>, Vec<Error>) {
+pub fn top_level_nodes(input: &str) -> (Vec<TopLevelNode>, Vec<Error>) {
     let mut scanner = Scanner::new(input);
-    match item::parse_fragments(&mut scanner) {
-        Ok(fragments) => (fragments, scanner.into_errors()),
+    match item::parse_top_level_nodes(&mut scanner) {
+        Ok(nodes) => (nodes, scanner.into_errors()),
         Err(error) => {
             let mut errors = scanner.into_errors();
             errors.push(error);
diff --git a/compiler/qsc_passes/src/baseprofck.rs b/compiler/qsc_passes/src/baseprofck.rs
index 0229bdae..b9284b3f 100644
--- a/compiler/qsc_passes/src/baseprofck.rs
+++ b/compiler/qsc_passes/src/baseprofck.rs
@@ -8,11 +8,11 @@ use miette::Diagnostic;
 use qsc_data_structures::span::Span;
 use qsc_hir::{
     hir::{
-        BinOp, CallableDecl, CallableKind, Expr, ExprKind, Item, ItemKind, Lit, Package, SpecBody,
-        SpecGen, Stmt, StmtKind,
+        BinOp, CallableKind, Expr, ExprKind, Item, ItemKind, Lit, Package, SpecBody, SpecGen,
+        StmtKind,
     },
     ty::{Prim, Ty},
-    visit::{walk_expr, walk_item, Visitor},
+    visit::{walk_expr, walk_item, walk_package, Visitor},
 };
 use thiserror::Error;
 
@@ -60,32 +60,21 @@ pub fn check_base_profile_compliance(package: &Package) -> Vec<Error> {
     checker.errors
 }
 
-#[must_use]
-pub fn check_base_profile_compliance_for_stmt(stmt: &Stmt) -> Vec<Error> {
-    let mut checker = Checker { errors: Vec::new() };
-    checker.visit_stmt(stmt);
-    if let StmtKind::Expr(expr) = &stmt.kind {
-        if any_non_result_ty(&expr.ty) {
-            checker.errors.push(Error::ReturnNonResult(stmt.span));
-        }
-    }
-
-    checker.errors
-}
-
-#[must_use]
-pub fn check_base_profile_compliance_for_callable(decl: &CallableDecl) -> Vec<Error> {
-    let mut checker = Checker { errors: Vec::new() };
-    checker.visit_callable_decl(decl);
-
-    checker.errors
-}
-
 struct Checker {
     errors: Vec<Error>,
 }
 
 impl<'a> Visitor<'a> for Checker {
+    fn visit_package(&mut self, package: &'a Package) {
+        if let Some(StmtKind::Expr(expr)) = &package.stmts.last().map(|stmt| &stmt.kind) {
+            if any_non_result_ty(&expr.ty) {
+                self.errors.push(Error::ReturnNonResult(expr.span));
+            }
+        }
+
+        walk_package(self, package);
+    }
+
     fn visit_item(&mut self, item: &'a Item) {
         match &item.kind {
             ItemKind::Callable(callable)
diff --git a/compiler/qsc_passes/src/conjugate_invert.rs b/compiler/qsc_passes/src/conjugate_invert.rs
index d9442a05..f07838ee 100644
--- a/compiler/qsc_passes/src/conjugate_invert.rs
+++ b/compiler/qsc_passes/src/conjugate_invert.rs
@@ -8,12 +8,11 @@ use std::{collections::HashSet, mem::take};
 
 use miette::Diagnostic;
 use qsc_data_structures::span::Span;
-use qsc_frontend::compile::CompileUnit;
 use qsc_hir::{
     assigner::Assigner,
     global::Table,
     hir::{
-        Block, CallableDecl, Expr, ExprKind, Ident, Mutability, NodeId, Pat, PatKind, Res, Stmt,
+        Block, Expr, ExprKind, Ident, Mutability, NodeId, Package, Pat, PatKind, Res, Stmt,
         StmtKind,
     },
     mut_visit::{self, MutVisitor},
@@ -47,41 +46,17 @@ pub enum Error {
 
 /// Generates adjoint inverted blocks for within-blocks across all conjugate expressions,
 /// eliminating the conjugate expression from the compilation unit.
-pub(super) fn invert_conjugate_exprs(core: &Table, unit: &mut CompileUnit) -> Vec<Error> {
-    let mut pass = ConjugateElim {
-        core,
-        assigner: &mut unit.assigner,
-        errors: Vec::new(),
-    };
-    pass.visit_package(&mut unit.package);
-    pass.errors
-}
-
-pub(super) fn invert_conjugate_exprs_for_callable(
-    core: &Table,
-    assigner: &mut Assigner,
-    decl: &mut CallableDecl,
-) -> Vec<Error> {
-    let mut pass = ConjugateElim {
-        core,
-        assigner,
-        errors: Vec::new(),
-    };
-    pass.visit_callable_decl(decl);
-    pass.errors
-}
-
-pub(super) fn invert_conjugate_exprs_for_stmt(
+pub(super) fn invert_conjugate_exprs(
     core: &Table,
+    package: &mut Package,
     assigner: &mut Assigner,
-    stmt: &mut Stmt,
 ) -> Vec<Error> {
     let mut pass = ConjugateElim {
         core,
         assigner,
         errors: Vec::new(),
     };
-    pass.visit_stmt(stmt);
+    pass.visit_package(package);
     pass.errors
 }
 
diff --git a/compiler/qsc_passes/src/entry_point.rs b/compiler/qsc_passes/src/entry_point.rs
index 28bc8c26..784964a5 100644
--- a/compiler/qsc_passes/src/entry_point.rs
+++ b/compiler/qsc_passes/src/entry_point.rs
@@ -7,7 +7,6 @@ mod tests;
 use super::Error as PassErr;
 use miette::Diagnostic;
 use qsc_data_structures::span::Span;
-use qsc_frontend::compile::CompileUnit;
 use qsc_hir::{
     assigner::Assigner,
     hir::{
@@ -42,15 +41,18 @@ pub enum Error {
 
 // If no entry expression is provided, generate one from the entry point callable.
 // Only one callable should be annotated with the entry point attribute.
-pub(super) fn generate_entry_expr(unit: &mut CompileUnit) -> Vec<super::Error> {
-    if unit.package.entry.is_some() {
+pub(super) fn generate_entry_expr(
+    package: &mut Package,
+    assigner: &mut Assigner,
+) -> Vec<super::Error> {
+    if package.entry.is_some() {
         return vec![];
     }
-    let callables = get_callables(&unit.package);
+    let callables = get_callables(package);
 
-    match create_entry_from_callables(&mut unit.assigner, callables) {
+    match create_entry_from_callables(assigner, callables) {
         Ok(expr) => {
-            unit.package.entry = Some(expr);
+            package.entry = Some(expr);
             vec![]
         }
         Err(errs) => errs,
diff --git a/compiler/qsc_passes/src/lib.rs b/compiler/qsc_passes/src/lib.rs
index b2030662..d79b8bd3 100644
--- a/compiler/qsc_passes/src/lib.rs
+++ b/compiler/qsc_passes/src/lib.rs
@@ -21,14 +21,11 @@ use callable_limits::CallableLimits;
 use entry_point::generate_entry_expr;
 use loop_unification::LoopUni;
 use miette::Diagnostic;
-use qsc_frontend::{
-    compile::{CompileUnit, TargetProfile},
-    incremental::Fragment,
-};
+use qsc_frontend::compile::{CompileUnit, TargetProfile};
 use qsc_hir::{
     assigner::Assigner,
     global::{self, Table},
-    hir::{Item, ItemKind},
+    hir::Package,
     mut_visit::MutVisitor,
     validate::Validator,
     visit::Visitor,
@@ -59,6 +56,68 @@ pub struct PassContext {
     borrow_check: borrowck::Checker,
 }
 
+impl PassContext {
+    #[must_use]
+    pub fn new(target: TargetProfile) -> Self {
+        Self {
+            target,
+            borrow_check: borrowck::Checker::default(),
+        }
+    }
+
+    /// Run the default set of passes required for evaluation.
+    pub fn run_default_passes(
+        &mut self,
+        package: &mut Package,
+        assigner: &mut Assigner,
+        core: &Table,
+        package_type: PackageType,
+    ) -> Vec<Error> {
+        let mut call_limits = CallableLimits::default();
+        call_limits.visit_package(package);
+        let callable_errors = call_limits.errors;
+
+        self.borrow_check.visit_package(package);
+        let borrow_errors = &mut self.borrow_check.errors;
+
+        let spec_errors = spec_gen::generate_specs(core, package, assigner);
+        Validator::default().visit_package(package);
+
+        let conjugate_errors = conjugate_invert::invert_conjugate_exprs(core, package, assigner);
+        Validator::default().visit_package(package);
+
+        let entry_point_errors = if package_type == PackageType::Exe {
+            let entry_point_errors = generate_entry_expr(package, assigner);
+            Validator::default().visit_package(package);
+            entry_point_errors
+        } else {
+            Vec::new()
+        };
+
+        LoopUni { core, assigner }.visit_package(package);
+        Validator::default().visit_package(package);
+
+        ReplaceQubitAllocation::new(core, assigner).visit_package(package);
+        Validator::default().visit_package(package);
+
+        let base_prof_errors = if self.target == TargetProfile::Base {
+            baseprofck::check_base_profile_compliance(package)
+        } else {
+            Vec::new()
+        };
+
+        callable_errors
+            .into_iter()
+            .map(Error::CallableLimits)
+            .chain(borrow_errors.drain(..).map(Error::BorrowCk))
+            .chain(spec_errors.into_iter().map(Error::SpecGen))
+            .chain(conjugate_errors.into_iter().map(Error::ConjInvert))
+            .chain(entry_point_errors)
+            .chain(base_prof_errors.into_iter().map(Error::BaseProfCk))
+            .collect()
+    }
+}
+
 /// Run the default set of passes required for evaluation.
 pub fn run_default_passes(
     core: &Table,
@@ -66,53 +125,12 @@ pub fn run_default_passes(
     package_type: PackageType,
     target: TargetProfile,
 ) -> Vec<Error> {
-    let mut call_limits = CallableLimits::default();
-    call_limits.visit_package(&unit.package);
-    let callable_errors = call_limits.errors;
-
-    let mut borrow_check = borrowck::Checker::default();
-    borrow_check.visit_package(&unit.package);
-    let borrow_errors = borrow_check.errors;
-
-    let spec_errors = spec_gen::generate_specs(core, unit);
-    Validator::default().visit_package(&unit.package);
-
-    let conjugate_errors = conjugate_invert::invert_conjugate_exprs(core, unit);
-    Validator::default().visit_package(&unit.package);
-
-    let entry_point_errors = if package_type == PackageType::Exe {
-        let entry_point_errors = generate_entry_expr(unit);
-        Validator::default().visit_package(&unit.package);
-        entry_point_errors
-    } else {
-        Vec::new()
-    };
-
-    LoopUni {
+    PassContext::new(target).run_default_passes(
+        &mut unit.package,
+        &mut unit.assigner,
         core,
-        assigner: &mut unit.assigner,
-    }
-    .visit_package(&mut unit.package);
-    Validator::default().visit_package(&unit.package);
-
-    ReplaceQubitAllocation::new(core, &mut unit.assigner).visit_package(&mut unit.package);
-    Validator::default().visit_package(&unit.package);
-
-    let base_prof_errors = if target == TargetProfile::Base {
-        baseprofck::check_base_profile_compliance(&unit.package)
-    } else {
-        Vec::new()
-    };
-
-    callable_errors
-        .into_iter()
-        .map(Error::CallableLimits)
-        .chain(borrow_errors.into_iter().map(Error::BorrowCk))
-        .chain(spec_errors.into_iter().map(Error::SpecGen))
-        .chain(conjugate_errors.into_iter().map(Error::ConjInvert))
-        .chain(entry_point_errors)
-        .chain(base_prof_errors.into_iter().map(Error::BaseProfCk))
-        .collect()
+        package_type,
+    )
 }
 
 pub fn run_core_passes(core: &mut CompileUnit) -> Vec<Error> {
@@ -139,81 +157,3 @@ pub fn run_core_passes(core: &mut CompileUnit) -> Vec<Error> {
         .chain(base_prof_errors.into_iter().map(Error::BaseProfCk))
         .collect()
 }
-
-impl PassContext {
-    #[must_use]
-    pub fn new(target: TargetProfile) -> Self {
-        Self {
-            target,
-            borrow_check: borrowck::Checker::default(),
-        }
-    }
-
-    pub fn run(
-        &mut self,
-        core: &Table,
-        assigner: &mut Assigner,
-        fragment: &mut Fragment,
-    ) -> Vec<Error> {
-        let mut errors = Vec::new();
-
-        match fragment {
-            Fragment::Stmt(stmt) => {
-                self.borrow_check.visit_stmt(stmt);
-                errors.extend(self.borrow_check.errors.drain(..).map(Error::BorrowCk));
-
-                errors.extend(
-                    conjugate_invert::invert_conjugate_exprs_for_stmt(core, assigner, stmt)
-                        .into_iter()
-                        .map(Error::ConjInvert),
-                );
-                LoopUni { core, assigner }.visit_stmt(stmt);
-                ReplaceQubitAllocation::new(core, assigner).visit_stmt(stmt);
-
-                if self.target == TargetProfile::Base {
-                    errors.extend(
-                        baseprofck::check_base_profile_compliance_for_stmt(stmt)
-                            .into_iter()
-                            .map(Error::BaseProfCk),
-                    );
-                }
-            }
-            Fragment::Item(Item {
-                kind: ItemKind::Callable(decl),
-                ..
-            }) => {
-                let mut call_limits = CallableLimits::default();
-                call_limits.visit_callable_decl(decl);
-                errors.extend(call_limits.errors.into_iter().map(Error::CallableLimits));
-
-                let mut borrow_check = borrowck::Checker::default();
-                borrow_check.visit_callable_decl(decl);
-                errors.extend(borrow_check.errors.into_iter().map(Error::BorrowCk));
-
-                errors.extend(
-                    spec_gen::generate_specs_for_callable(core, assigner, decl)
-                        .into_iter()
-                        .map(Error::SpecGen),
-                );
-                errors.extend(
-                    conjugate_invert::invert_conjugate_exprs_for_callable(core, assigner, decl)
-                        .into_iter()
-                        .map(Error::ConjInvert),
-                );
-                LoopUni { core, assigner }.visit_callable_decl(decl);
-                ReplaceQubitAllocation::new(core, assigner).visit_callable_decl(decl);
-
-                if self.target == TargetProfile::Base {
-                    errors.extend(
-                        baseprofck::check_base_profile_compliance_for_callable(decl)
-                            .into_iter()
-                            .map(Error::BaseProfCk),
-                    );
-                }
-            }
-            Fragment::Item(_) => {}
-        }
-
-        errors
-    }
-}
diff --git a/compiler/qsc_passes/src/replace_qubit_allocation.rs b/compiler/qsc_passes/src/replace_qubit_allocation.rs
index d4d9e5bc..59ec07d3 100644
--- a/compiler/qsc_passes/src/replace_qubit_allocation.rs
+++ b/compiler/qsc_passes/src/replace_qubit_allocation.rs
@@ -384,7 +384,7 @@ impl MutVisitor for ReplaceQubitAllocation<'_> {
 
     fn visit_stmt(&mut self, stmt: &mut Stmt) {
         // This function is not called by visit_block above, so the only time it will be used is for
-        // top-level statement fragments. Given that, the qubits allocated will always be live for
+        // top-level statements. Given that, the qubits allocated will always be live for
         // the entirety of a global scope, so only qubit allocations need to be generated.
         match stmt.kind.clone() {
             StmtKind::Qubit(_, pat, qubit_init, None) => {
diff --git a/compiler/qsc_passes/src/spec_gen.rs b/compiler/qsc_passes/src/spec_gen.rs
index d6c58914..7f35854b 100644
--- a/compiler/qsc_passes/src/spec_gen.rs
+++ b/compiler/qsc_passes/src/spec_gen.rs
@@ -12,7 +12,6 @@ use crate::{id_update::NodeIdRefresher, invert_block::adj_invert_block};
 use self::{adj_gen::AdjDistrib, ctl_gen::CtlDistrib};
 use miette::Diagnostic;
 use qsc_data_structures::span::Span;
-use qsc_frontend::compile::CompileUnit;
 use qsc_hir::{
     assigner::Assigner,
     global::Table,
@@ -42,28 +41,19 @@ pub enum Error {
 }
 
 /// Generates specializations for the given compile unit, updating it in-place.
-pub(super) fn generate_specs(core: &Table, unit: &mut CompileUnit) -> Vec<Error> {
-    generate_placeholders(&mut unit.package, &mut unit.assigner);
-    generate_spec_impls(core, unit)
-}
-
-pub(super) fn generate_specs_for_callable(
+pub(super) fn generate_specs(
     core: &Table,
+    package: &mut Package,
     assigner: &mut Assigner,
-    decl: &mut CallableDecl,
 ) -> Vec<Error> {
-    generate_placeholders_for_callable(decl, assigner);
-    generate_spec_impls_for_decl(core, assigner, decl)
+    generate_placeholders(package, assigner);
+    generate_spec_impls(core, package, assigner)
 }
 
 fn generate_placeholders(package: &mut Package, assigner: &mut Assigner) {
     SpecPlacePass { assigner }.visit_package(package);
 }
 
-fn generate_placeholders_for_callable(decl: &mut CallableDecl, assigner: &mut Assigner) {
-    SpecPlacePass { assigner }.visit_callable_decl(decl);
-}
-
 struct SpecPlacePass<'a> {
     assigner: &'a mut Assigner,
 }
@@ -125,27 +115,13 @@ fn is_self_adjoint(decl: &CallableDecl) -> bool {
     matches!(&decl.adj, Some(s) if matches!(&s.body, SpecBody::Gen(SpecGen::Slf)))
 }
 
-fn generate_spec_impls(core: &Table, unit: &mut CompileUnit) -> Vec<Error> {
-    let mut pass = SpecImplPass {
-        core,
-        assigner: &mut unit.assigner,
-        errors: Vec::new(),
-    };
-    pass.visit_package(&mut unit.package);
-    pass.errors
-}
-
-fn generate_spec_impls_for_decl(
-    core: &Table,
-    assigner: &mut Assigner,
-    decl: &mut CallableDecl,
-) -> Vec<Error> {
+fn generate_spec_impls(core: &Table, package: &mut Package, assigner: &mut Assigner) -> Vec<Error> {
     let mut pass = SpecImplPass {
         core,
         assigner,
         errors: Vec::new(),
     };
-    pass.visit_callable_decl(decl);
+    pass.visit_package(package);
     pass.errors
 }
 
diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs
index b2ee24a0..e4f5bee4 100644
--- a/language_service/src/completion.rs
+++ b/language_service/src/completion.rs
@@ -29,7 +29,7 @@ pub(crate) fn get_completions(
     // Determine context for the offset
     let mut context_finder = ContextFinder {
         offset,
-        context: if compilation.unit.ast.package.namespaces.as_ref().is_empty() {
+        context: if compilation.unit.ast.package.nodes.is_empty() {
             // The parser failed entirely, no context to go on
             Context::NoCompilation
         } else {
diff --git a/package-lock.json b/package-lock.json
index 6c8929d5..c9cff044 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,8 @@
         "./vscode"
       ],
       "devDependencies": {
+        "@types/chai": "^4.3.5",
+        "@types/mocha": "^10.0.1",
         "@types/node": "^18.15.3",
         "@types/vscode": "^1.77.0",
         "@typescript-eslint/eslint-plugin": "^5.59.2",
@@ -19,12 +21,15 @@
         "@vscode/debugadapter": "^1.61.0",
         "@vscode/debugprotocol": "1.61.0",
         "@vscode/extension-telemetry": "0.8.4",
+        "@vscode/test-web": "^0.0.45",
+        "chai": "^4.3.8",
         "esbuild": "^0.17.12",
         "eslint": "^8.39.0",
         "events": "^3.3.0",
         "github-markdown-css": "^5.2.0",
         "marked": "^4.3.0",
         "mathjax": "^3.2.2",
+        "mocha": "^10.2.0",
         "monaco-editor": "^0.36.1",
         "preact": "^10.13.1",
         "prettier": "2.8.8",
@@ -628,6 +633,33 @@
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
       "dev": true
     },
+    "node_modules/@koa/cors": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-4.0.0.tgz",
+      "integrity": "sha512-Y4RrbvGTlAaa04DBoPBWJqDR5gPj32OOz827ULXfgB1F7piD1MB/zwn8JR2LAnvdILhxUbXbkXGWuNVsFuVFCQ==",
+      "dev": true,
+      "dependencies": {
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@koa/router": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.0.tgz",
+      "integrity": "sha512-cnnxeKHXlt7XARJptflGURdJaO+ITpNkOHmQu7NHmCoRinPbyvFzce/EG/E8Zy81yQ1W9MoSdtklc3nyaDReUw==",
+      "dev": true,
+      "dependencies": {
+        "http-errors": "^2.0.0",
+        "koa-compose": "^4.1.0",
+        "methods": "^1.1.2",
+        "path-to-regexp": "^6.2.1"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/@microsoft/1ds-core-js": {
       "version": "3.2.13",
       "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz",
@@ -974,12 +1006,24 @@
         "node": ">= 10"
       }
     },
+    "node_modules/@types/chai": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz",
+      "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==",
+      "dev": true
+    },
     "node_modules/@types/json-schema": {
       "version": "7.0.12",
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
       "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
       "dev": true
     },
+    "node_modules/@types/mocha": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz",
+      "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==",
+      "dev": true
+    },
     "node_modules/@types/node": {
       "version": "18.16.19",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz",
@@ -1225,6 +1269,84 @@
         "vscode": "^1.75.0"
       }
     },
+    "node_modules/@vscode/test-web": {
+      "version": "0.0.45",
+      "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.45.tgz",
+      "integrity": "sha512-yPJgxEbbxDlKGGpLzNQ8PDPOOUKUVo7Epg/3o+y7mKtjbg3nktIReJ/pPtOHrZxsNC8FkzTNBoTh5DMBHX/LGg==",
+      "dev": true,
+      "dependencies": {
+        "@koa/cors": "^4.0.0",
+        "@koa/router": "^12.0.0",
+        "gunzip-maybe": "^1.4.2",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.1",
+        "koa": "^2.14.2",
+        "koa-morgan": "^1.0.1",
+        "koa-mount": "^4.0.0",
+        "koa-static": "^5.0.0",
+        "minimist": "^1.2.8",
+        "playwright": "^1.37.1",
+        "tar-fs": "^3.0.4",
+        "vscode-uri": "^3.0.7"
+      },
+      "bin": {
+        "vscode-test-web": "out/index.js"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@vscode/test-web/node_modules/agent-base": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
+      "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@vscode/test-web/node_modules/http-proxy-agent": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
+      "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
+      "dev": true,
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@vscode/test-web/node_modules/https-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+      "dev": true,
+      "dependencies": {
+        "agent-base": "^7.0.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dev": true,
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.10.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
@@ -1283,6 +1405,15 @@
         "url": "https://github.com/sponsors/epoberezkin"
       }
     },
+    "node_modules/ansi-colors": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+      "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1307,6 +1438,19 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/applicationinsights": {
       "version": "2.7.3",
       "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz",
@@ -1354,6 +1498,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/async-hook-jl": {
       "version": "1.7.6",
       "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz",
@@ -1394,12 +1547,45 @@
       "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
       "dev": true
     },
+    "node_modules/b4a": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
+      "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==",
+      "dev": true
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "dev": true
     },
+    "node_modules/basic-auth": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+      "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "5.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/basic-auth/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1422,6 +1608,40 @@
         "node": ">=8"
       }
     },
+    "node_modules/browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "node_modules/browserify-zlib": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
+      "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==",
+      "dev": true,
+      "dependencies": {
+        "pako": "~0.2.0"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "dev": true
+    },
+    "node_modules/cache-content-type": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
+      "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==",
+      "dev": true,
+      "dependencies": {
+        "mime-types": "^2.1.18",
+        "ylru": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
     "node_modules/call-bind": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -1444,6 +1664,36 @@
         "node": ">=6"
       }
     },
+    "node_modules/camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/chai": {
+      "version": "4.3.8",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz",
+      "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==",
+      "dev": true,
+      "dependencies": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^4.1.2",
+        "get-func-name": "^2.0.0",
+        "loupe": "^2.3.1",
+        "pathval": "^1.1.1",
+        "type-detect": "^4.0.5"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -1460,12 +1710,71 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/cjs-module-lexer": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
       "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==",
       "dev": true
     },
+    "node_modules/cliui": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
     "node_modules/cls-hooked": {
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz",
@@ -1489,6 +1798,16 @@
         "semver": "bin/semver"
       }
     },
+    "node_modules/co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+      "dev": true,
+      "engines": {
+        "iojs": ">= 1.0.0",
+        "node": ">= 0.12.0"
+      }
+    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1525,6 +1844,27 @@
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "dev": true
     },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/continuation-local-storage": {
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz",
@@ -1535,6 +1875,25 @@
         "emitter-listener": "^1.1.1"
       }
     },
+    "node_modules/cookies": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz",
+      "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==",
+      "dev": true,
+      "dependencies": {
+        "depd": "~2.0.0",
+        "keygrip": "~1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1566,6 +1925,36 @@
         }
       }
     },
+    "node_modules/decamelize": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+      "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/deep-eql": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
+      "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
+      "dev": true,
+      "dependencies": {
+        "type-detect": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/deep-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+      "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==",
+      "dev": true
+    },
     "node_modules/deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -1581,6 +1970,31 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+      "dev": true
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
     "node_modules/diagnostic-channel": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz",
@@ -1599,6 +2013,15 @@
         "diagnostic-channel": "*"
       }
     },
+    "node_modules/diff": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
     "node_modules/dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1623,6 +2046,24 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/duplexify": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+      "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+      "dev": true,
+      "dependencies": {
+        "end-of-stream": "^1.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "dev": true
+    },
     "node_modules/emitter-listener": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
@@ -1632,6 +2073,30 @@
         "shimmer": "^1.2.0"
       }
     },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
     "node_modules/esbuild": {
       "version": "0.17.19",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
@@ -1669,6 +2134,21 @@
         "@esbuild/win32-x64": "0.17.19"
       }
     },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "dev": true
+    },
     "node_modules/escape-string-regexp": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -1877,6 +2357,12 @@
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
+    "node_modules/fast-fifo": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+      "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+      "dev": true
+    },
     "node_modules/fast-glob": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
@@ -1966,6 +2452,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/flat": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+      "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+      "dev": true,
+      "bin": {
+        "flat": "cli.js"
+      }
+    },
     "node_modules/flat-cache": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -1999,18 +2494,59 @@
         "node": ">= 6"
       }
     },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
       "dev": true
     },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
     "node_modules/function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/get-intrinsic": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
@@ -2108,6 +2644,23 @@
       "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
       "dev": true
     },
+    "node_modules/gunzip-maybe": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz",
+      "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==",
+      "dev": true,
+      "dependencies": {
+        "browserify-zlib": "^0.1.4",
+        "is-deflate": "^1.0.0",
+        "is-gzip": "^1.0.0",
+        "peek-stream": "^1.1.0",
+        "pumpify": "^1.3.3",
+        "through2": "^2.0.3"
+      },
+      "bin": {
+        "gunzip-maybe": "bin.js"
+      }
+    },
     "node_modules/has": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -2153,6 +2706,93 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "dev": true,
+      "dependencies": {
+        "has-symbols": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/http-assert": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
+      "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==",
+      "dev": true,
+      "dependencies": {
+        "deep-equal": "~1.0.1",
+        "http-errors": "~1.8.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/http-assert/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-assert/node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "dev": true,
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-assert/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "dev": true,
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/http-proxy-agent": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
@@ -2242,6 +2882,18 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
       "dev": true
     },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/is-core-module": {
       "version": "2.13.0",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
@@ -2254,6 +2906,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-deflate": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz",
+      "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==",
+      "dev": true
+    },
     "node_modules/is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2263,6 +2921,30 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-generator-function": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+      "dev": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-glob": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -2275,6 +2957,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-gzip": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz",
+      "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/is-number": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -2293,6 +2984,33 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-plain-obj": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+      "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true
+    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2323,6 +3041,197 @@
       "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
       "dev": true
     },
+    "node_modules/keygrip": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
+      "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
+      "dev": true,
+      "dependencies": {
+        "tsscmp": "1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa": {
+      "version": "2.14.2",
+      "resolved": "https://registry.npmjs.org/koa/-/koa-2.14.2.tgz",
+      "integrity": "sha512-VFI2bpJaodz6P7x2uyLiX6RLYpZmOJqNmoCst/Yyd7hQlszyPwG/I9CQJ63nOtKSxpt5M7NH67V6nJL2BwCl7g==",
+      "dev": true,
+      "dependencies": {
+        "accepts": "^1.3.5",
+        "cache-content-type": "^1.0.0",
+        "content-disposition": "~0.5.2",
+        "content-type": "^1.0.4",
+        "cookies": "~0.8.0",
+        "debug": "^4.3.2",
+        "delegates": "^1.0.0",
+        "depd": "^2.0.0",
+        "destroy": "^1.0.4",
+        "encodeurl": "^1.0.2",
+        "escape-html": "^1.0.3",
+        "fresh": "~0.5.2",
+        "http-assert": "^1.3.0",
+        "http-errors": "^1.6.3",
+        "is-generator-function": "^1.0.7",
+        "koa-compose": "^4.1.0",
+        "koa-convert": "^2.0.0",
+        "on-finished": "^2.3.0",
+        "only": "~0.0.2",
+        "parseurl": "^1.3.2",
+        "statuses": "^1.5.0",
+        "type-is": "^1.6.16",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
+      }
+    },
+    "node_modules/koa-compose": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz",
+      "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==",
+      "dev": true
+    },
+    "node_modules/koa-convert": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz",
+      "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==",
+      "dev": true,
+      "dependencies": {
+        "co": "^4.6.0",
+        "koa-compose": "^4.1.0"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/koa-morgan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz",
+      "integrity": "sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==",
+      "dev": true,
+      "dependencies": {
+        "morgan": "^1.6.1"
+      }
+    },
+    "node_modules/koa-mount": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz",
+      "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^4.0.1",
+        "koa-compose": "^4.1.0"
+      },
+      "engines": {
+        "node": ">= 7.6.0"
+      }
+    },
+    "node_modules/koa-send": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz",
+      "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^4.1.1",
+        "http-errors": "^1.7.3",
+        "resolve-path": "^1.4.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/koa-send/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa-send/node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "dev": true,
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa-send/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa-static": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz",
+      "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^3.1.0",
+        "koa-send": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 7.6.0"
+      }
+    },
+    "node_modules/koa-static/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/koa/node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "dev": true,
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa/node_modules/http-errors/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/levn": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2357,6 +3266,31 @@
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
       "dev": true
     },
+    "node_modules/log-symbols": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/loupe": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
+      "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
+      "dev": true,
+      "dependencies": {
+        "get-func-name": "^2.0.0"
+      }
+    },
     "node_modules/lru-cache": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -2387,6 +3321,15 @@
       "integrity": "sha512-Bt+SSVU8eBG27zChVewOicYs7Xsdt40qm4+UpHyX7k0/O9NliPc+x77k1/FEsPsjKPZGJvtRZM1vO+geW0OhGw==",
       "dev": true
     },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2396,6 +3339,15 @@
         "node": ">= 8"
       }
     },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/micromatch": {
       "version": "4.0.5",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
@@ -2442,6 +3394,135 @@
         "node": "*"
       }
     },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+      "dev": true
+    },
+    "node_modules/mocha": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+      "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+      "dev": true,
+      "dependencies": {
+        "ansi-colors": "4.1.1",
+        "browser-stdout": "1.3.1",
+        "chokidar": "3.5.3",
+        "debug": "4.3.4",
+        "diff": "5.0.0",
+        "escape-string-regexp": "4.0.0",
+        "find-up": "5.0.0",
+        "glob": "7.2.0",
+        "he": "1.2.0",
+        "js-yaml": "4.1.0",
+        "log-symbols": "4.1.0",
+        "minimatch": "5.0.1",
+        "ms": "2.1.3",
+        "nanoid": "3.3.3",
+        "serialize-javascript": "6.0.0",
+        "strip-json-comments": "3.1.1",
+        "supports-color": "8.1.1",
+        "workerpool": "6.2.1",
+        "yargs": "16.2.0",
+        "yargs-parser": "20.2.4",
+        "yargs-unparser": "2.0.0"
+      },
+      "bin": {
+        "_mocha": "bin/_mocha",
+        "mocha": "bin/mocha.js"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mochajs"
+      }
+    },
+    "node_modules/mocha/node_modules/glob": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/mocha/node_modules/glob/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/mocha/node_modules/minimatch": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+      "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/mocha/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true
+    },
+    "node_modules/mocha/node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
     "node_modules/module-details-from-path": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
@@ -2454,12 +3535,67 @@
       "integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg==",
       "dev": true
     },
+    "node_modules/morgan": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+      "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+      "dev": true,
+      "dependencies": {
+        "basic-auth": "~2.0.1",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-finished": "~2.3.0",
+        "on-headers": "~1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/morgan/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/morgan/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true
+    },
+    "node_modules/morgan/node_modules/on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+      "dev": true,
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
       "dev": true
     },
+    "node_modules/nanoid": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+      "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+      "dev": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
     "node_modules/natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -2472,6 +3608,24 @@
       "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
       "dev": true
     },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/object-inspect": {
       "version": "1.12.3",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
@@ -2481,6 +3635,27 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "dev": true,
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -2490,6 +3665,12 @@
         "wrappy": "1"
       }
     },
+    "node_modules/only": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
+      "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==",
+      "dev": true
+    },
     "node_modules/optionator": {
       "version": "0.9.3",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -2537,6 +3718,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/pako": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+      "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+      "dev": true
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -2549,6 +3736,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2582,13 +3778,39 @@
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
       "dev": true
     },
+    "node_modules/path-to-regexp": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+      "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
+      "dev": true
+    },
     "node_modules/path-type": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
       "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
       "dev": true,
       "engines": {
-        "node": ">=8"
+        "node": ">=8"
+      }
+    },
+    "node_modules/pathval": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+      "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/peek-stream": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
+      "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==",
+      "dev": true,
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "duplexify": "^3.5.0",
+        "through2": "^2.0.3"
       }
     },
     "node_modules/picomatch": {
@@ -2603,6 +3825,50 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/playwright": {
+      "version": "1.38.0",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz",
+      "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==",
+      "dev": true,
+      "dependencies": {
+        "playwright-core": "1.38.0"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.2"
+      }
+    },
+    "node_modules/playwright-core": {
+      "version": "1.38.0",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz",
+      "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==",
+      "dev": true,
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/playwright/node_modules/fsevents": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
     "node_modules/preact": {
       "version": "10.16.0",
       "resolved": "https://registry.npmjs.org/preact/-/preact-10.16.0.tgz",
@@ -2637,6 +3903,33 @@
         "url": "https://github.com/prettier/prettier?sponsor=1"
       }
     },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true
+    },
+    "node_modules/pump": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+      "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+      "dev": true,
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/pumpify": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+      "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+      "dev": true,
+      "dependencies": {
+        "duplexify": "^3.6.0",
+        "inherits": "^2.0.3",
+        "pump": "^2.0.0"
+      }
+    },
     "node_modules/punycode": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -2693,6 +3986,63 @@
         }
       ]
     },
+    "node_modules/queue-tick": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+      "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
+      "dev": true
+    },
+    "node_modules/randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dev": true,
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/readable-stream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/require-in-the-middle": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz",
@@ -2733,6 +4083,64 @@
         "node": ">=4"
       }
     },
+    "node_modules/resolve-path": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz",
+      "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==",
+      "dev": true,
+      "dependencies": {
+        "http-errors": "~1.6.2",
+        "path-is-absolute": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/resolve-path/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/resolve-path/node_modules/http-errors": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+      "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
+      "dev": true,
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.0",
+        "statuses": ">= 1.4.0 < 2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/resolve-path/node_modules/inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
+      "dev": true
+    },
+    "node_modules/resolve-path/node_modules/setprototypeof": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+      "dev": true
+    },
+    "node_modules/resolve-path/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -2781,6 +4189,26 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
     "node_modules/semver": {
       "version": "7.5.4",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@@ -2796,6 +4224,21 @@
         "node": ">=10"
       }
     },
+    "node_modules/serialize-javascript": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+      "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+      "dev": true,
+      "dependencies": {
+        "randombytes": "^2.1.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "dev": true
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2852,6 +4295,60 @@
       "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==",
       "dev": true
     },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/stream-shift": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
+      "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
+      "dev": true
+    },
+    "node_modules/streamx": {
+      "version": "2.15.1",
+      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz",
+      "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==",
+      "dev": true,
+      "dependencies": {
+        "fast-fifo": "^1.1.0",
+        "queue-tick": "^1.0.1"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/string_decoder/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -2900,12 +4397,54 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/tar-fs": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
+      "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
+      "dev": true,
+      "dependencies": {
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^3.1.5"
+      }
+    },
+    "node_modules/tar-fs/node_modules/pump": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+      "dev": true,
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/tar-stream": {
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
+      "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
+      "dev": true,
+      "dependencies": {
+        "b4a": "^1.6.4",
+        "fast-fifo": "^1.2.0",
+        "streamx": "^2.15.0"
+      }
+    },
     "node_modules/text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
       "dev": true
     },
+    "node_modules/through2": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+      "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+      "dev": true,
+      "dependencies": {
+        "readable-stream": "~2.3.6",
+        "xtend": "~4.0.1"
+      }
+    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2918,12 +4457,30 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
     "node_modules/tslib": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
       "dev": true
     },
+    "node_modules/tsscmp": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+      "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.6.x"
+      }
+    },
     "node_modules/tsutils": {
       "version": "3.21.0",
       "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
@@ -2951,6 +4508,15 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/type-fest": {
       "version": "0.20.2",
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -2963,6 +4529,19 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dev": true,
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/typescript": {
       "version": "5.1.6",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
@@ -3001,6 +4580,12 @@
       "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
       "dev": true
     },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true
+    },
     "node_modules/uuid": {
       "version": "8.3.2",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -3010,6 +4595,21 @@
         "uuid": "dist/bin/uuid"
       }
     },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz",
+      "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==",
+      "dev": true
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3025,18 +4625,110 @@
         "node": ">= 8"
       }
     },
+    "node_modules/workerpool": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+      "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+      "dev": true
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
       "dev": true
     },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
+    "node_modules/yargs": {
+      "version": "16.2.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+      "dev": true,
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "20.2.4",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+      "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs-unparser": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+      "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+      "dev": true,
+      "dependencies": {
+        "camelcase": "^6.0.0",
+        "decamelize": "^4.0.0",
+        "flat": "^5.0.2",
+        "is-plain-obj": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ylru": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.3.2.tgz",
+      "integrity": "sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 9f979748..34775fb2 100644
--- a/package.json
+++ b/package.json
@@ -17,19 +17,24 @@
   },
   "private": true,
   "devDependencies": {
+    "@types/chai": "^4.3.5",
+    "@types/mocha": "^10.0.1",
     "@types/node": "^18.15.3",
     "@types/vscode": "^1.77.0",
+    "@typescript-eslint/eslint-plugin": "^5.59.2",
+    "@typescript-eslint/parser": "^5.59.2",
     "@vscode/debugadapter": "^1.61.0",
     "@vscode/debugprotocol": "1.61.0",
     "@vscode/extension-telemetry": "0.8.4",
-    "@typescript-eslint/eslint-plugin": "^5.59.2",
-    "@typescript-eslint/parser": "^5.59.2",
+    "@vscode/test-web": "^0.0.45",
+    "chai": "^4.3.8",
     "esbuild": "^0.17.12",
     "eslint": "^8.39.0",
     "events": "^3.3.0",
     "github-markdown-css": "^5.2.0",
     "marked": "^4.3.0",
     "mathjax": "^3.2.2",
+    "mocha": "^10.2.0",
     "monaco-editor": "^0.36.1",
     "preact": "^10.13.1",
     "prettier": "2.8.8",
diff --git a/vscode/.gitignore b/vscode/.gitignore
index 277eedce..2e84c1bd 100644
--- a/vscode/.gitignore
+++ b/vscode/.gitignore
@@ -1,3 +1,4 @@
 out/
+test/out/
 wasm/
 *.vsix
diff --git a/vscode/package.json b/vscode/package.json
index 5789d414..847b209c 100644
--- a/vscode/package.json
+++ b/vscode/package.json
@@ -249,10 +249,12 @@
     ]
   },
   "scripts": {
-    "tsc:check": "node ../node_modules/typescript/bin/tsc -p ./tsconfig.json",
     "build": "npm run tsc:check && node build.mjs",
     "build:watch": "node build.mjs --watch",
-    "run:web": "npx @vscode/test-web --extensionDevelopmentPath . ../samples"
+    "start": "npm exec --no @vscode/test-web --extensionDevelopmentPath . ../samples",
+    "test": "node ./test/runTests.mjs",
+    "pretest": "npm exec --no playwright install && node ./test/buildTests.mjs",
+    "tsc:check": "node ../node_modules/typescript/bin/tsc -p ./tsconfig.json"
   },
   "repository": {
     "type": "git",
diff --git a/vscode/tsconfig.json b/vscode/tsconfig.json
index 49737f02..87f1e5a1 100644
--- a/vscode/tsconfig.json
+++ b/vscode/tsconfig.json
@@ -12,5 +12,6 @@
     "rootDir": "src",
     "strict": true,
     "skipLibCheck": true
-  }
+  },
+  "exclude": ["test"]
 }


Test patch:
diff --git a/compiler/qsc/src/interpret/debug/tests.rs b/compiler/qsc/src/interpret/debug/tests.rs
index b512d801..54130a7a 100644
--- a/compiler/qsc/src/interpret/debug/tests.rs
+++ b/compiler/qsc/src/interpret/debug/tests.rs
@@ -87,7 +87,7 @@ fn stack_traces_can_cross_eval_session_and_file_boundaries() {
                              at Adjoint Test.C in 1.qs
                              at Adjoint Test.B in 1.qs
                              at Adjoint Test2.A in 2.qs
-                             at Z in <expression>
+                             at Z in line_0
                     "#};
             assert_eq!(expectation, stack_trace);
         }
diff --git a/compiler/qsc/src/interpret/stateful/tests.rs b/compiler/qsc/src/interpret/stateful/tests.rs
index 8ec4d97c..c8e13c58 100644
--- a/compiler/qsc/src/interpret/stateful/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/tests.rs
@@ -933,6 +933,27 @@ mod given_interpreter {
             "#]].assert_eq(&res);
         }
 
+        #[test]
+        fn qirgen_entry_expr_profile_incompatible() {
+            let mut interpreter = Interpreter::new(
+                true,
+                SourceMap::default(),
+                PackageType::Lib,
+                TargetProfile::Base,
+            )
+            .expect("interpreter should be created");
+            let res = interpreter
+                .qirgen("1")
+                .expect_err("expected qirgen to fail");
+            is_error(
+                &res,
+                &expect![[r#"
+                non-Result return type in entry expression
+                   [<entry>] [1]
+            "#]],
+            );
+        }
+
         #[test]
         fn run_with_shots() {
             let mut interpreter = get_interpreter();
diff --git a/compiler/qsc_frontend/src/incremental/tests.rs b/compiler/qsc_frontend/src/incremental/tests.rs
index 706aab24..e3d46cc5 100644
--- a/compiler/qsc_frontend/src/incremental/tests.rs
+++ b/compiler/qsc_frontend/src/incremental/tests.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use super::{Compiler, Fragment};
+use super::{Compiler, Increment};
 use crate::compile::{self, CompileUnit, PackageStore, TargetProfile};
 use expect_test::{expect, Expect};
 use indoc::indoc;
@@ -11,7 +11,7 @@ use miette::Diagnostic;
 fn one_callable() {
     let store = PackageStore::new(compile::core());
     let mut compiler = Compiler::new(&store, vec![], TargetProfile::Full);
-    let fragments = compiler
+    let unit = compiler
         .compile_fragments(
             &mut CompileUnit::default(),
             "test_1",
@@ -19,14 +19,38 @@ fn one_callable() {
         )
         .expect("compilation should succeed");
 
-    check_fragment_kinds(
+    check_unit(
         &expect![[r#"
-        [
-            "callable",
-            "namespace",
-        ]
-    "#]],
-        &fragments,
+            ast:
+            Package 0:
+                Namespace 1 [0-44] (Ident 2 [10-13] "Foo"):
+                    Item 3 [16-42]:
+                        Callable 4 [16-42] (Operation):
+                            name: Ident 5 [26-30] "Main"
+                            input: Pat 6 [30-32]: Unit
+                            output: Type 7 [35-39]: Path: Path 8 [35-39] (Ident 9 [35-39] "Unit")
+                            body: Block: Block 10 [40-42]: <empty>
+            names:
+            node_id:2,node_id:5,node_id:8,
+            terms:
+            node_id:6,node_id:10,
+            hir:
+            Package:
+                Item 0 [0-44] (Public):
+                    Namespace (Ident 5 [10-13] "Foo"): Item 1
+                Item 1 [16-42] (Public):
+                    Parent: 0
+                    Callable 0 [16-42] (operation):
+                        name: Ident 1 [26-30] "Main"
+                        input: Pat 2 [30-32] [Type Unit]: Unit
+                        output: Unit
+                        functors: empty set
+                        body: SpecDecl 3 [16-42]: Impl:
+                            Block 4 [40-42]: <empty>
+                        adj: <none>
+                        ctl: <none>
+                        ctl-adj: <none>"#]],
+        &unit,
     );
 }
 
@@ -34,17 +58,28 @@ fn one_callable() {
 fn one_statement() {
     let store = PackageStore::new(compile::core());
     let mut compiler = Compiler::new(&store, vec![], TargetProfile::Full);
-    let fragments = compiler
+    let unit = compiler
         .compile_fragments(&mut CompileUnit::default(), "test_1", "use q = Qubit();")
         .expect("compilation should succeed");
 
-    check_fragment_kinds(
+    check_unit(
         &expect![[r#"
-            [
-                "statement",
-            ]
-        "#]],
-        &fragments,
+            ast:
+            Package 0:
+                Stmt 1 [0-16]: Qubit (Fresh)
+                    Pat 2 [4-5]: Bind:
+                        Ident 3 [4-5] "q"
+                    QubitInit 4 [8-15] Single
+            names:
+            node_id:3,
+            terms:
+            node_id:1,node_id:2,node_id:3,node_id:4,
+            hir:
+            Package:
+                Stmt 0 [0-16]: Qubit (Fresh)
+                    Pat 1 [4-5] [Type Qubit]: Bind: Ident 2 [4-5] "q"
+                    QubitInit 3 [8-15] [Type Qubit]: Single"#]],
+        &unit,
     );
 }
 
@@ -141,18 +176,30 @@ fn errors_across_multiple_lines() {
     .assert_debug_eq(&labels);
 }
 
-fn check_fragment_kinds(expect: &Expect, actual: &[Fragment]) {
-    expect.assert_debug_eq(
-        &actual
+fn check_unit(expect: &Expect, actual: &Increment) {
+    let ast = format!("ast:\n{}", actual.ast.package);
+
+    let names = format!(
+        "\nnames:\n{}",
+        actual
+            .ast
+            .names
             .iter()
-            .map(|f| match f {
-                Fragment::Stmt(_) => "statement",
-                Fragment::Item(item) => match item.kind {
-                    qsc_hir::hir::ItemKind::Callable(_) => "callable",
-                    qsc_hir::hir::ItemKind::Namespace(_, _) => "namespace",
-                    qsc_hir::hir::ItemKind::Ty(_, _) => "ty",
-                },
-            })
-            .collect::<Vec<&str>>(),
+            .map(|n| format!("node_id:{},", n.0))
+            .collect::<String>()
     );
+    let terms = format!(
+        "\nterms:\n{}",
+        actual
+            .ast
+            .tys
+            .terms
+            .iter()
+            .map(|n| format!("node_id:{},", n.0))
+            .collect::<String>()
+    );
+
+    let hir = format!("\nhir:\n{}", actual.hir);
+
+    expect.assert_eq(&[ast, names, terms, hir].into_iter().collect::<String>());
 }
diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs
index 70f31b42..08622744 100644
--- a/compiler/qsc_frontend/src/resolve/tests.rs
+++ b/compiler/qsc_frontend/src/resolve/tests.rs
@@ -7,7 +7,7 @@ use expect_test::{expect, Expect};
 use indoc::indoc;
 use qsc_ast::{
     assigner::Assigner as AstAssigner,
-    ast::{Ident, NodeId, Package, Path},
+    ast::{Ident, NodeId, Package, Path, TopLevelNode},
     mut_visit::MutVisitor,
     visit::{self, Visitor},
 };
@@ -85,7 +85,11 @@ fn compile(input: &str) -> (Package, Names, Vec<Error>) {
     assert!(parse_errors.is_empty(), "parse failed: {parse_errors:#?}");
     let mut package = Package {
         id: NodeId::default(),
-        namespaces: namespaces.into_boxed_slice(),
+        nodes: namespaces
+            .into_iter()
+            .map(TopLevelNode::Namespace)
+            .collect::<Vec<_>>()
+            .into_boxed_slice(),
         entry: None,
     };
 
diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs
index f9f14307..5f2b5424 100644
--- a/compiler/qsc_frontend/src/typeck/tests.rs
+++ b/compiler/qsc_frontend/src/typeck/tests.rs
@@ -10,7 +10,7 @@ use expect_test::{expect, Expect};
 use indoc::indoc;
 use qsc_ast::{
     assigner::Assigner as AstAssigner,
-    ast::{Block, Expr, NodeId, Package, Pat, QubitInit},
+    ast::{Block, Expr, NodeId, Package, Pat, QubitInit, TopLevelNode},
     mut_visit::MutVisitor,
     visit::{self, Visitor},
 };
@@ -122,7 +122,11 @@ fn parse(input: &str, entry_expr: &str) -> Package {
 
     Package {
         id: NodeId::default(),
-        namespaces: namespaces.into_boxed_slice(),
+        nodes: namespaces
+            .into_iter()
+            .map(TopLevelNode::Namespace)
+            .collect::<Vec<_>>()
+            .into_boxed_slice(),
         entry,
     }
 }
diff --git a/compiler/qsc_passes/src/conjugate_invert/tests.rs b/compiler/qsc_passes/src/conjugate_invert/tests.rs
index a5b2f335..e176e0ee 100644
--- a/compiler/qsc_passes/src/conjugate_invert/tests.rs
+++ b/compiler/qsc_passes/src/conjugate_invert/tests.rs
@@ -16,7 +16,7 @@ fn check(file: &str, expect: &Expect) {
     let mut unit = compile(&store, &[], sources, TargetProfile::Full);
     assert!(unit.errors.is_empty(), "{:?}", unit.errors);
 
-    let errors = invert_conjugate_exprs(store.core(), &mut unit);
+    let errors = invert_conjugate_exprs(store.core(), &mut unit.package, &mut unit.assigner);
     Validator::default().visit_package(&unit.package);
     if errors.is_empty() {
         expect.assert_eq(&unit.package.to_string());
diff --git a/compiler/qsc_passes/src/entry_point/tests.rs b/compiler/qsc_passes/src/entry_point/tests.rs
index d5cec1f3..6b021735 100644
--- a/compiler/qsc_passes/src/entry_point/tests.rs
+++ b/compiler/qsc_passes/src/entry_point/tests.rs
@@ -16,7 +16,7 @@ fn check(file: &str, expr: &str, expect: &Expect) {
     );
     assert!(unit.errors.is_empty(), "{:?}", unit.errors);
 
-    let errors = generate_entry_expr(&mut unit);
+    let errors = generate_entry_expr(&mut unit.package, &mut unit.assigner);
     if errors.is_empty() {
         expect.assert_eq(
             &unit
diff --git a/compiler/qsc_passes/src/spec_gen/tests.rs b/compiler/qsc_passes/src/spec_gen/tests.rs
index 732d33e6..0e68ead2 100644
--- a/compiler/qsc_passes/src/spec_gen/tests.rs
+++ b/compiler/qsc_passes/src/spec_gen/tests.rs
@@ -16,7 +16,7 @@ fn check(file: &str, expect: &Expect) {
     let mut unit = compile(&store, &[], sources, TargetProfile::Full);
     assert!(unit.errors.is_empty(), "{:?}", unit.errors);
 
-    let errors = generate_specs(store.core(), &mut unit);
+    let errors = generate_specs(store.core(), &mut unit.package, &mut unit.assigner);
     Validator::default().visit_package(&unit.package);
     if errors.is_empty() {
         expect.assert_eq(&unit.package.to_string());
diff --git a/vscode/test/buildTests.mjs b/vscode/test/buildTests.mjs
new file mode 100644
index 00000000..f2630acc
--- /dev/null
+++ b/vscode/test/buildTests.mjs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//@ts-check
+
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { build } from "esbuild";
+
+const thisDir = dirname(fileURLToPath(import.meta.url));
+
+/** @type {import("esbuild").BuildOptions} */
+const buildOptions = {
+  entryPoints: [
+    join(thisDir, "suites", "language-service", "index.ts"),
+    join(thisDir, "suites", "debugger", "index.ts"),
+  ],
+  outdir: join(thisDir, "out"),
+  bundle: true,
+  mainFields: ["browser", "module", "main"],
+  external: ["vscode"],
+  format: "cjs",
+  platform: "browser",
+  target: ["es2020"],
+  sourcemap: "linked",
+  //logLevel: "debug",
+  define: { "import.meta.url": "undefined" },
+};
+
+console.log("Running esbuild");
+
+build(buildOptions).then(() =>
+  console.log(`Built tests to ${join(thisDir, "out")}`)
+);
diff --git a/vscode/test/runTests.mjs b/vscode/test/runTests.mjs
new file mode 100644
index 00000000..df644291
--- /dev/null
+++ b/vscode/test/runTests.mjs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// @ts-check
+
+// Adapted from the sample at
+// https://github.com/microsoft/vscode-test-web/blob/3f0f858ab15cb65ef3c19564b0f5a6910ea9414e/sample/src/web/test/runTest.ts
+//
+// This script is run using Node.js in the dev environment. It will
+// download the latest Insiders build of VS Code for the Web and launch
+// it in a headless instance of Chromium to run the integration test suite.
+
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { runTests } from "@vscode/test-web";
+
+const thisDir = dirname(fileURLToPath(import.meta.url));
+// The folder containing the Extension Manifest package.json
+const extensionDevelopmentPath = join(thisDir, "..");
+const attachArgName = "--waitForDebugger=";
+const waitForDebugger = process.argv.find((arg) =>
+  arg.startsWith(attachArgName)
+);
+
+try {
+  // Language service tests
+  await runSuite(
+    join(thisDir, "out", "language-service", "index"),
+    join(thisDir, "suites", "language-service", "test-workspace")
+  );
+
+  // Debugger tests
+  await runSuite(
+    join(thisDir, "out", "debugger", "index"),
+    join(thisDir, "..", "..", "samples")
+  );
+} catch (err) {
+  console.error("Failed to run tests", err);
+  process.exit(1);
+}
+
+/**
+ * @param {string} extensionTestsPath - The path to module with the test runner and tests
+ * @param {string} workspacePath - The path to the workspace to be opened in VS Code
+ */
+async function runSuite(extensionTestsPath, workspacePath) {
+  // Start a web server that serves VS Code in a browser, run the tests
+  await runTests({
+    headless: true, // pass false to see VS Code UI
+    browserType: "chromium",
+    extensionDevelopmentPath,
+    extensionTestsPath,
+    folderPath: workspacePath,
+    waitForDebugger: waitForDebugger
+      ? Number(waitForDebugger.slice(attachArgName.length))
+      : undefined,
+  });
+}
diff --git a/vscode/test/suites/debugger/extension.test.ts b/vscode/test/suites/debugger/extension.test.ts
new file mode 100644
index 00000000..e3b28a73
--- /dev/null
+++ b/vscode/test/suites/debugger/extension.test.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import * as vscode from "vscode";
+import { assert } from "chai";
+
+suite("Q# Debugger Tests", () => {
+  const workspaceFolder =
+    vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0];
+  assert(workspaceFolder, "Expecting an open folder");
+
+  test("Placeholder - success", async () => {
+    assert.equal(1, 1);
+  });
+});
diff --git a/vscode/test/suites/debugger/index.ts b/vscode/test/suites/debugger/index.ts
new file mode 100644
index 00000000..82a441be
--- /dev/null
+++ b/vscode/test/suites/debugger/index.ts
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { runMochaTests } from "../run";
+
+export function run(): Promise<void> {
+  return runMochaTests(() => {
+    // We can't use any wildcards or dynamically discovered
+    // paths here since ESBuild needs these modules to be
+    // real paths on disk at bundling time.
+    require("./extension.test");
+  });
+}
diff --git a/vscode/test/suites/language-service/extension.test.ts b/vscode/test/suites/language-service/extension.test.ts
new file mode 100644
index 00000000..e5e27463
--- /dev/null
+++ b/vscode/test/suites/language-service/extension.test.ts
@@ -0,0 +1,117 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import * as vscode from "vscode";
+import { assert } from "chai";
+
+suite("Q# Language Service Tests", () => {
+  const workspaceFolder =
+    vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0];
+  assert(workspaceFolder, "Expecting an open folder");
+
+  const workspaceFolderUri = workspaceFolder.uri;
+  const docUri = vscode.Uri.joinPath(workspaceFolderUri, "test.qs");
+
+  test("Q# language is registered", async () => {
+    const doc = await vscode.workspace.openTextDocument(docUri);
+    assert.equal(
+      doc.languageId,
+      "qsharp",
+      "document language should be `qsharp`"
+    );
+  });
+
+  test("Completions", async () => {
+    await activate();
+    const actualCompletionList = (await vscode.commands.executeCommand(
+      "vscode.executeCompletionItemProvider",
+      docUri,
+      new vscode.Position(0, 0)
+    )) as vscode.CompletionList;
+
+    assert.include(
+      actualCompletionList.items.map((i) => i.label),
+      "operation"
+    );
+  });
+
+  test("Definition", async () => {
+    await activate();
+    const doc = await vscode.workspace.openTextDocument(docUri);
+    const text = doc.getText(
+      new vscode.Range(new vscode.Position(4, 16), new vscode.Position(4, 19))
+    );
+    // Sanity check the test setup - is this the correct position?
+    assert.equal(text, "foo");
+
+    const actualDefinition = (await vscode.commands.executeCommand(
+      "vscode.executeDefinitionProvider",
+      docUri,
+      new vscode.Position(4, 18) // cursor on the usage of foo
+    )) as vscode.Location[];
+
+    const location = actualDefinition[0];
+    assert.equal(location.uri.toString(), docUri.toString());
+    assert.equal(location.range.start.line, 3);
+    assert.equal(location.range.start.character, 12);
+  });
+
+  test("Diagnostics", async () => {
+    await activate();
+
+    const actualDiagnostics = vscode.languages.getDiagnostics(docUri);
+    assert.lengthOf(actualDiagnostics, 1);
+
+    assert.include(actualDiagnostics[0].message, "syntax error");
+    assert.equal(actualDiagnostics[0].range.start.line, 7);
+  });
+
+  test("Hover", async () => {
+    await activate();
+    const doc = await vscode.workspace.openTextDocument(docUri);
+    const text = doc.getText(
+      new vscode.Range(new vscode.Position(4, 16), new vscode.Position(4, 19))
+    );
+    // Sanity check the test setup - is this the correct position?
+    assert.equal(text, "foo");
+
+    const actualHovers = (await vscode.commands.executeCommand(
+      "vscode.executeHoverProvider",
+      docUri,
+      new vscode.Position(4, 18) // cursor on the usage of foo
+    )) as vscode.Hover[];
+
+    assert.lengthOf(actualHovers, 1);
+    assert.lengthOf(actualHovers[0].contents, 1);
+    const md = actualHovers[0].contents[0] as vscode.MarkdownString;
+    assert.include(md.value, "foo : String");
+  });
+
+  test("Signature Help", async () => {
+    await activate();
+    const doc = await vscode.workspace.openTextDocument(docUri);
+    const text = doc.getText(
+      new vscode.Range(new vscode.Position(4, 16), new vscode.Position(4, 19))
+    );
+    // Sanity check the test setup - is this the correct position?
+    assert.equal(text, "foo");
+
+    const actualSignatureHelp = (await vscode.commands.executeCommand(
+      "vscode.executeSignatureHelpProvider",
+      docUri,
+      new vscode.Position(4, 18) // cursor on the usage of foo
+    )) as vscode.SignatureHelp;
+
+    assert.lengthOf(actualSignatureHelp.signatures, 1);
+    assert.include(
+      actualSignatureHelp.signatures[0].label,
+      "function Message(msg : String)"
+    );
+  });
+});
+
+async function activate() {
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  const ext = vscode.extensions.getExtension("quantum.qsharp-lang-vscode-dev")!;
+  await ext.activate();
+}
diff --git a/vscode/test/suites/language-service/index.ts b/vscode/test/suites/language-service/index.ts
new file mode 100644
index 00000000..82a441be
--- /dev/null
+++ b/vscode/test/suites/language-service/index.ts
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { runMochaTests } from "../run";
+
+export function run(): Promise<void> {
+  return runMochaTests(() => {
+    // We can't use any wildcards or dynamically discovered
+    // paths here since ESBuild needs these modules to be
+    // real paths on disk at bundling time.
+    require("./extension.test");
+  });
+}
diff --git a/vscode/test/suites/language-service/test-workspace/test.qs b/vscode/test/suites/language-service/test-workspace/test.qs
new file mode 100644
index 00000000..5daa0e3a
--- /dev/null
+++ b/vscode/test/suites/language-service/test-workspace/test.qs
@@ -0,0 +1,10 @@
+namespace Test {
+    @EntryPoint()
+    operation Test() : Unit {
+        let foo = "hello!";
+        Message(foo);
+    }
+
+    operation BadSyntax() {
+    }
+}
diff --git a/vscode/test/suites/run.ts b/vscode/test/suites/run.ts
new file mode 100644
index 00000000..7901b5b6
--- /dev/null
+++ b/vscode/test/suites/run.ts
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Adapted from the sample at
+// https://github.com/microsoft/vscode-test-web/blob/3f0f858ab15cb65ef3c19564b0f5a6910ea9414e/sample/src/web/test/suite/index.ts
+//
+//
+// This script runs in the VS Code for the Web extension host.
+//
+
+// imports mocha for the browser, defining the `mocha` global.
+require("mocha/mocha");
+
+export function runMochaTests(requireTestModules: () => void): Promise<void> {
+  return new Promise((c, e) => {
+    mocha.setup({
+      ui: "tdd",
+      reporter: undefined,
+    });
+
+    // Load the test suites. This needs to come
+    // after the call to mocha.setup() so that the
+    // suite() global is defined by mocha.
+    requireTestModules();
+
+    try {
+      // Run the mocha test
+      mocha.run((failures) => {
+        if (failures > 0) {
+          e(new Error(`${failures} tests failed.`));
+        } else {
+          c();
+        }
+      });
+    } catch (err) {
+      console.error(err);
+      e(err);
+    }
+  });
+}
diff --git a/vscode/testing.md b/vscode/testing.md
index eb8b5b31..7f56eaf6 100644
--- a/vscode/testing.md
+++ b/vscode/testing.md
@@ -5,11 +5,29 @@
 To run the extension in a local browser environment, see
 <https://code.visualstudio.com/api/extension-guides/web-extensions#test-your-web-extension-in-a-browser-using-vscodetestweb>
 
-Basically, from the command-line in the repo root run:
+From the `vscode` directory, run:
 
 ```bash
-npx @vscode/test-web --extensionDevelopmentPath ./vscode ./samples
+npm run run:web
+```
+
+To run from the repo root, you can pass the `-w` switch to `npm`:
+
+```bash
+ npm -w vscode run run:web
 ```
 
 This will open a Chromium browser hosting the VS Code for Web, with the extension
 loaded from disk, and serving the samples directory as an open workspace.
+
+## Integration tests
+
+To run the integration tests, from the `vscode` folder, run:
+
+```bash
+npm test
+```
+
+This will run the integration tests in an instance of VS Code for the Web.
+See [test/runTests.mjs](test/runTests.mjs) and [test/suite/index.ts](test/runTests.mjs) for more details on how tests are
+discovered and run.