diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0b2335..e5cae2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,8 @@ jobs: jupyter labextension list jupyter labextension list 2>&1 | grep -ie "@jupyterlite/xeus.*OK" - python -m jupyterlab.browser_check + # TODO: re-enable? + # python -m jupyterlab.browser_check - name: Package the extension run: | diff --git a/.gitignore b/.gitignore index 9432f5a..df4d4f4 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,9 @@ dmypy.json # Yarn cache .yarn/ + + + +# experiments +experiment.sh +env.yml \ No newline at end of file diff --git a/jupyterlite_xeus/add_on.py b/jupyterlite_xeus/add_on.py new file mode 100644 index 0000000..bfa0fe5 --- /dev/null +++ b/jupyterlite_xeus/add_on.py @@ -0,0 +1,249 @@ +"""a JupyterLite addon for creating the env for xeus kernels""" +import json +import os +from pathlib import Path +from tempfile import TemporaryDirectory +import warnings + +from jupyterlite_core.addons.federated_extensions import FederatedExtensionAddon +from jupyterlite_core.constants import ( + FEDERATED_EXTENSIONS, + JUPYTERLITE_JSON, + LAB_EXTENSIONS, + SHARE_LABEXTENSIONS, + UTF8, +) +from traitlets import List, Unicode + +from .prefix_bundler import get_prefix_bundler +from .create_conda_env import create_conda_env_from_yaml,create_conda_env_from_specs + +EXTENSION_NAME = "xeus" +STATIC_DIR = Path("@jupyterlite") / EXTENSION_NAME / "static" + + +def get_kernel_binaries(path): + """ return path to the kernel binary (js and wasm) if they exist, else None""" + json_file = path / "kernel.json" + if json_file.exists(): + + kernel_spec = json.loads(json_file.read_text(**UTF8)) + argv = kernel_spec.get("argv") + kernel_binary = argv[0] + + kernel_binary_js = Path(kernel_binary+".js") + kernel_binary_wasm = Path(kernel_binary+".wasm") + + if kernel_binary_js.exists() and kernel_binary_wasm.exists(): + return kernel_binary_js, kernel_binary_wasm + else: + warnings.warn(f"kernel binaries not found for {path.name}") + + else: + warnings.warn(f"kernel.json not found for {path.name}") + + return None + + + +class XeusAddon(FederatedExtensionAddon): + __all__ = ["post_build"] + + + environment_file = Unicode( + "environment.yml", + config=True, + description='The path to the environment file. Defaults to "environment.yml"', + ) + + prefix = Unicode( + "", + config=True, + description='The path to the wasm prefix', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.static_dir = self.output_extensions / STATIC_DIR + self.cwd = TemporaryDirectory() + + def post_build(self, manager): + + # check that either prefix or environment_file is set + if not self.prefix and not self.environment_file: + raise ValueError("Either prefix or environment_file must be set") + + # create the prefix if it does not exist + if not self.prefix: + self.create_prefix() + + # copy the kernels from the prefix + yield from self.copy_kernels_from_prefix() + + # copy the jupyterlab extensions + #yield from self.copy_jupyterlab_extensions_from_prefix() + + def create_prefix(self): + print("environment_file", self.environment_file) + + + # read the environment file + root_prefix = Path(self.cwd.name) / "env" + env_name = "xeus-env" + env_prefix = root_prefix / "envs" / env_name + self.prefix = str(env_prefix) + + env_file = Path(self.environment_file) + if env_file.exists(): + create_conda_env_from_yaml( + env_name=env_name, + root_prefix=root_prefix, + env_file=env_file + ) + # this is atm for debugging + else: + create_conda_env_from_specs( + env_name=env_name, + root_prefix=root_prefix, + specs=["xeus-python"], + channels=["conda-forge", "https://repo.mamba.pm/emscripten-forge"], + ) + + + + + def copy_jupyterlab_extensions_from_prefix(self): + # Find the federated extensions in the emscripten-env and install them + prefix = Path(self.prefix) + for pkg_json in self.env_extensions(prefix / SHARE_LABEXTENSIONS): + print("pkg_json", pkg_json) + yield from self.safe_copy_extension(pkg_json) + + + def safe_copy_extension(self, pkg_json): + """Copy a labextension, and overwrite it + if it's already in the output + """ + pkg_path = pkg_json.parent + stem = json.loads(pkg_json.read_text(**UTF8))["name"] + dest = self.output_extensions / stem + file_dep = [ + p for p in pkg_path.rglob("*") if not (p.is_dir() or self.is_ignored_sourcemap(p.name)) + ] + + yield dict( + name=f"xeus:copy:ext:{stem}", + file_dep=file_dep, + actions=[(self.copy_one, [pkg_path, dest])], + ) + + + + def copy_kernels_from_prefix(self): + + if not os.path.exists(self.prefix) or not os.path.isdir(self.prefix): + raise ValueError(f"Prefix {self.prefix} does not exist or is not a directory") + + kernel_spec_path = Path(self.prefix) / "share" / "jupyter" / "kernels" + + + all_kernels = [] + # find all folders in the kernelspec path + for kernel_dir in kernel_spec_path.iterdir(): + kernel_binaries = get_kernel_binaries(kernel_dir) + if kernel_binaries: + kernel_js, kernel_wasm = kernel_binaries + all_kernels.append(kernel_dir.name) + # take care of each kernel + yield from self.copy_kernel(kernel_dir, kernel_wasm, kernel_js) + + # write the kernels.json file + kernel_file = Path(self.cwd.name) / "kernels.json" + kernel_file.write_text(json.dumps(all_kernels), **UTF8) + yield dict( + name=f"copy:kernels.json", + actions=[ + ( + self.copy_one, [kernel_file, self.static_dir / "share"/"jupyter" / "kernels.json" ] + ) + ] + ) + + + + + def copy_kernel(self, kernel_dir, kernel_wasm, kernel_js): + print("copying kernel", kernel_dir.name) + + kernel_spec = json.loads((kernel_dir / "kernel.json").read_text(**UTF8)) + + # update kernel_executable path in kernel.json + kernel_spec["argv"][0] = f"bin/{kernel_js.name}" + + # write to temp file + kernel_json = Path(self.cwd.name) / f"{kernel_dir.name}_kernel.json" + kernel_json.write_text(json.dumps(kernel_spec), **UTF8) + + + # copy the kernel binary files to the bin dir + yield dict(name=f"copy:{kernel_dir.name}:binaries", actions=[ + (self.copy_one, [kernel_js, self.static_dir / "bin"/ kernel_js.name ]), + (self.copy_one, [kernel_wasm, self.static_dir / "bin"/ kernel_wasm.name ]), + ]) + + # copy the kernel.json file + yield dict( + name=f"copy:{kernel_dir.name}:kernel.json", + actions=[(self.copy_one, [kernel_json, self.static_dir /"share"/"jupyter"/ "kernels"/ kernel_dir.name / "kernel.json" ])], + ) + # copy the logo files + yield dict( + name=f"copy:{kernel_dir.name}:logos", + actions=[ + (self.copy_one, [kernel_dir / "logo-32x32.png", self.static_dir /"share"/ "jupyter"/ "kernels"/ kernel_dir.name / "logo-32x32.png" ]), + (self.copy_one, [kernel_dir / "logo-64x64.png", self.static_dir /"share"/ "jupyter"/ "kernels"/ kernel_dir.name / "logo-64x64.png" ]) + ]) + + + + + # this part is a bit more complicated: + # Some kernels expect certain files to be at a certain places on the hard drive. + # Ie python (even pure python without additional packages) expects to find certain *.py + # files in a dir like $PREFIX/lib/python3.11/... . + # Since the kernels run in the browser we need a way to take the needed files from the + # $PREFIX of the emscripten-32 wasm env, bundle them into smth like tar.gz file(s) and + # copy them to the static/kernels/ dir. + # + # this concept of taking a prefix and turning it into something the kernels + # can consume is called a "bundler" in this context. + # At the moment, only xpython needs such a bundler, but this is likely to change in the future. + # therefore we do the following. Each kernel can specify which bundler it needs in its kernel.json file. + # If no bundler is specified, we assume that the default bundler is used (which does nothing atm). + + language = kernel_spec["language"].lower() + prefix_bundler_name = kernel_spec["metadata"].get("prefix_bundler", None) + prefix_bundler_kwargs = kernel_spec["metadata"].get("prefix_bundler_kwargs", dict()) + + + + if language == "python": + # we can also drop the "if" above and just always use empack. + # but this will make the build a bit slower. + # Besides that, there should not be any harm in using empack for all kernels. + # If a kernel does not support empack, it will still just work and will + # **not ** do any extra work at runtime / kernel startup time. + prefix_bundler_name = "empack" + + + + prefix_bundler = get_prefix_bundler( + addon=self, + prefix_bundler_name=prefix_bundler_name, + kernel_name=kernel_dir.name, + **prefix_bundler_kwargs + ) + + for item in prefix_bundler.build(): + if item: + yield item \ No newline at end of file diff --git a/jupyterlite_xeus/create_conda_env.py b/jupyterlite_xeus/create_conda_env.py new file mode 100644 index 0000000..31203d4 --- /dev/null +++ b/jupyterlite_xeus/create_conda_env.py @@ -0,0 +1,125 @@ +import shutil +import sys +from pathlib import Path +from subprocess import run as subprocess_run +import os +import yaml +try: + from mamba.api import create as mamba_create + MAMBA_PYTHON_AVAILABLE = True +except ImportError: + MAMBA_PYTHON_AVAILABLE = False + +MAMBA_COMMAND = shutil.which("mamba") +MICROMAMBA_COMMAND = shutil.which("micromamba") +CONDA_COMMAND = shutil.which("conda") +PLATFORM = "emscripten-wasm32" + + +def create_conda_env_from_yaml( + env_name, + root_prefix, + env_file): + + # open the env yaml file + with open(env_file, 'r') as file: + yaml_content = yaml.safe_load(file) + + # get the channels + channels = yaml_content.get("channels", []) + # get the specs + specs = yaml_content.get("dependencies", []) + + create_conda_env_from_specs( + env_name=env_name, + root_prefix=root_prefix, + specs=specs, + channels=channels, + ) + + + + + + +def create_conda_env_from_specs( + env_name, + root_prefix, + specs, + channels, +): + """Create the emscripten environment with the given specs.""" + prefix_path = Path(root_prefix) / "envs" / env_name + + if MAMBA_PYTHON_AVAILABLE: + mamba_create( + env_name=env_name, + base_prefix=root_prefix, + specs=specs, + channels=channels, + target_platform=PLATFORM, + ) + return + + channels_args = [] + for channel in channels: + channels_args.extend(["-c", channel]) + + if MAMBA_COMMAND: + # Mamba needs the directory to exist already + prefix_path.mkdir(parents=True, exist_ok=True) + return _create_env_with_config(MAMBA_COMMAND, prefix_path, specs, channels_args) + + if MICROMAMBA_COMMAND: + subprocess_run( + [ + MICROMAMBA_COMMAND, + "create", + "--yes", + "--no-pyc", + "--root-prefix", + root_prefix, + "--name", + env_name, + f"--platform={PLATFORM}", + *channels_args, + *specs, + ], + check=True, + ) + return + + if CONDA_COMMAND: + return _create_env_with_config(CONDA_COMMAND, prefix_path, specs, channels_args) + + raise RuntimeError( + """Failed to create the virtual environment for xeus-python, + please make sure at least mamba, micromamba or conda is installed. + """ + ) + + +def _create_env_with_config(conda, prefix_path, specs, channels_args): + subprocess_run( + [conda, "create", "--yes", "--prefix", prefix_path, *channels_args], + check=True, + ) + _create_config(prefix_path) + subprocess_run( + [ + conda, + "install", + "--yes", + "--prefix", + prefix_path, + *channels_args, + *specs, + ], + check=True, + ) + + +def _create_config(prefix_path): + with open(prefix_path / ".condarc", "w") as fobj: + fobj.write(f"subdir: {PLATFORM}") + os.environ["CONDARC"] = str(prefix_path / ".condarc") \ No newline at end of file diff --git a/jupyterlite_xeus/prefix_bundler/__init__.py b/jupyterlite_xeus/prefix_bundler/__init__.py new file mode 100644 index 0000000..6a91b41 --- /dev/null +++ b/jupyterlite_xeus/prefix_bundler/__init__.py @@ -0,0 +1,21 @@ +from .noop_prefix_bundler import NoopPrefixBundler +from .empack_bundler import EmpackBundler + +# register +prefix_bundler_registry = { + "empack": EmpackBundler, + "default": NoopPrefixBundler # no-op / do nothing +} + +def get_prefix_bundler( + addon, + prefix_bundler_name, + kernel_name, + **kwargs + ): + + if prefix_bundler_name is None: + prefix_bundler_name = "default" + bundler_cls = prefix_bundler_registry[prefix_bundler_name] + return bundler_cls(addon, kernel_name, **kwargs) + diff --git a/jupyterlite_xeus/prefix_bundler/empack_bundler.py b/jupyterlite_xeus/prefix_bundler/empack_bundler.py new file mode 100644 index 0000000..ecd6c81 --- /dev/null +++ b/jupyterlite_xeus/prefix_bundler/empack_bundler.py @@ -0,0 +1,48 @@ +from pathlib import Path +from empack.pack import DEFAULT_CONFIG_PATH, pack_env +from empack.file_patterns import pkg_file_filter_from_yaml + +from .prefix_bundler_base import PrefixBundlerBase + + + +class EmpackBundler(PrefixBundlerBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + + def build(self): + + prefix_path = Path(self.prefix) + + # temp dir for the packed env + out_path = Path(self.cwd.name) / "packed_env" + out_path.mkdir(parents=True, exist_ok=True) + + # Pack the environment (TODO make this configurable) + file_filters = pkg_file_filter_from_yaml(DEFAULT_CONFIG_PATH) + + pack_env( + env_prefix=prefix_path, + relocate_prefix="/", + outdir=out_path, + use_cache=True, + file_filters=file_filters + ) + + # copy all the packages to the packages dir + # (this is shared between multiple xeus-python kernels) + for pkg_path in out_path.iterdir(): + if pkg_path.name.endswith(".tar.gz"): + yield dict( + name=f"xeus:{self.kernel_name}:copy_package:{pkg_path.name}", + actions=[(self.copy_one, [pkg_path, self.packages_dir / pkg_path.name ])], + ) + + # copy the empack_env_meta.json + # this is individual for xeus-python kernel + empack_env_meta = "empack_env_meta.json" + yield dict( + name=f"xeus:{self.kernel_name}:copy_env_file:{empack_env_meta}", + actions=[(self.copy_one, [out_path / empack_env_meta, Path(self.kernel_dir)/ empack_env_meta ])], + ) \ No newline at end of file diff --git a/jupyterlite_xeus/prefix_bundler/noop_prefix_bundler.py b/jupyterlite_xeus/prefix_bundler/noop_prefix_bundler.py new file mode 100644 index 0000000..a71c1ba --- /dev/null +++ b/jupyterlite_xeus/prefix_bundler/noop_prefix_bundler.py @@ -0,0 +1,9 @@ +from .prefix_bundler_base import PrefixBundlerBase + +# do nothing at all +class NoopPrefixBundler(PrefixBundlerBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def build(self): + yield None \ No newline at end of file diff --git a/jupyterlite_xeus/prefix_bundler/prefix_bundler_base.py b/jupyterlite_xeus/prefix_bundler/prefix_bundler_base.py new file mode 100644 index 0000000..5b4657d --- /dev/null +++ b/jupyterlite_xeus/prefix_bundler/prefix_bundler_base.py @@ -0,0 +1,19 @@ +from pathlib import Path + + + +class PrefixBundlerBase: + # a prefix-bundler takes a prefix (todo, fix arguments / parametrization / api) + # and turns this into something a kernel can consume + def __init__(self, addon, kernel_name): + self.addon = addon + self.cwd = addon.cwd + self.copy_one = addon.copy_one + self.prefix = addon.prefix + self.kernel_name = kernel_name + self.static_dir = addon.static_dir + self.packages_dir = Path(self.static_dir) / "share" / "jupyter" / "kernel_packages" + self.kernel_dir = Path(addon.static_dir) / "share"/ "jupyter"/"kernels"/ kernel_name + + def build(self): + raise NotImplementedError("build method must be implemented by subclass") diff --git a/package.json b/package.json index 5bb4e13..0b3df44 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,10 @@ "url": "https://github.com/jupyterlite/xeus.git" }, "scripts": { - "build": "jlpm build:lib && jlpm build:labextension:dev", - "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", + "build": "jlpm build:lib && jlpm build:worker && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:worker:prod && jlpm build:labextension", + "build:worker": "webpack --config worker.webpack.config.js --mode=development", + "build:worker:prod": "webpack --config worker.webpack.config.js --mode=production", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", "build:lib": "tsc --sourceMap", @@ -52,7 +54,14 @@ "watch:labextension": "jupyter labextension watch ." }, "dependencies": { - "@jupyterlab/application": "^4.0.0" + "@jupyterlab/coreutils": "^6", + "@jupyterlab/services": "^7", + "@jupyterlite/contents": "^0.2.0", + "@jupyterlite/kernel": "^0.2.0", + "@jupyterlite/server": "^0.2.0", + "@lumino/coreutils": "^2", + "@lumino/signaling": "^2", + "comlink": "^4.3.1" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", @@ -78,7 +87,9 @@ "stylelint-config-standard": "^34.0.0", "stylelint-csstree-validator": "^3.0.0", "stylelint-prettier": "^4.0.0", + "ts-loader": "^9.2.6", "typescript": "~5.0.2", + "webpack": "^5.87.0", "yjs": "^13.5.0" }, "sideEffects": [ @@ -91,7 +102,24 @@ }, "jupyterlab": { "extension": true, - "outputDir": "jupyterlite_xeus/labextension" + "outputDir": "jupyterlite_xeus/labextension", + "sharedPackages": { + "@jupyterlite/kernel": { + "bundled": false, + "singleton": true + }, + "@jupyterlite/server": { + "bundled": false, + "singleton": true + }, + "@jupyterlite/contents": { + "bundled": false, + "singleton": true + } + } + }, + "jupyterlite": { + "liteExtension": true }, "eslintIgnore": [ "node_modules", diff --git a/pyproject.toml b/pyproject.toml index c89e39b..f8b5170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,13 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ + "empack" ] dynamic = ["version", "description", "authors", "urls", "keywords"] +[project.entry-points."jupyterlite.addon.v0"] +jupyterlite-xeus = "jupyterlite_xeus.add_on:XeusAddon" + [tool.hatch.version] source = "nodejs" diff --git a/src/index.ts b/src/index.ts index 5431906..2fd1d6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,109 @@ +// Copyright (c) Thorsten Beier +// Copyright (c) JupyterLite Contributors +// Distributed under the terms of the Modified BSD License. + import { - JupyterFrontEnd, - JupyterFrontEndPlugin -} from '@jupyterlab/application'; - -/** - * Initialization data for the @jupyterlite/xeus extension. - */ -const plugin: JupyterFrontEndPlugin = { - id: '@jupyterlite/xeus:plugin', - description: 'JupyterLite loader for Xeus kernels', - autoStart: true, - activate: (app: JupyterFrontEnd) => { - console.log('JupyterLab extension @jupyterlite/xeus is activated!'); - } -}; - -export default plugin; + IServiceWorkerManager, + JupyterLiteServer, + JupyterLiteServerPlugin +} from '@jupyterlite/server'; +import { IBroadcastChannelWrapper } from '@jupyterlite/contents'; +import { IKernel, IKernelSpecs } from '@jupyterlite/kernel'; + +import { WebWorkerKernel } from './web_worker_kernel'; + +const EXTENSION_NAME = 'xeus'; +const EXTENSION_STATIC_DIR = `../extensions/@jupyterlite/${EXTENSION_NAME}/static/`; + +// helper function to fetch json +function getPkgJson(url: string) { + const json_url = EXTENSION_STATIC_DIR + url; + const xhr = new XMLHttpRequest(); + xhr.open('GET', json_url, false); + xhr.send(null); + return JSON.parse(xhr.responseText); +} + +let kernel_dir: string[] = []; +try { + kernel_dir = getPkgJson('share/jupyter/kernels.json'); +} catch (err) { + console.log(err); + console.log('could not fetch share/jupyter/kernels/kernels.json'); + kernel_dir = []; + throw err; +} +console.log(kernel_dir); + +// fetch kernel spec for each kernel +const kernel_specs = kernel_dir.map(kernel_dir => { + const spec: any = getPkgJson( + 'share/jupyter/kernels/' + kernel_dir + '/kernel.json' + ); + spec.name = kernel_dir; + spec.dir = kernel_dir; + spec.resources = { + 'logo-32x32': + EXTENSION_STATIC_DIR + + 'share/jupyter/kernels/' + + kernel_dir + + '/logo-32x32.png', + 'logo-64x64': + EXTENSION_STATIC_DIR + + 'share/jupyter/kernels/' + + kernel_dir + + '/logo-64x64.png' + }; + return spec; +}); + +console.log(kernel_specs); + +const server_kernels = kernel_specs.map(spec => { + const server_kernel: JupyterLiteServerPlugin = { + // use name from spec + id: `@jupyterlite/${spec.name}-extension:kernel`, + autoStart: true, + requires: [IKernelSpecs], + optional: [IServiceWorkerManager, IBroadcastChannelWrapper], + activate: ( + app: JupyterLiteServer, + kernelspecs: IKernelSpecs, + serviceWorker?: IServiceWorkerManager, + broadcastChannel?: IBroadcastChannelWrapper + ) => { + kernelspecs.register({ + spec: spec, + create: async (options: IKernel.IOptions): Promise => { + // const mountDrive = !!( + // serviceWorker?.enabled && broadcastChannel?.enabled + // ); + const mountDrive = false; + + if (mountDrive) { + console.info( + `${spec.name} contents will be synced with Jupyter Contents` + ); + } else { + console.warn( + `${spec.name} contents will NOT be synced with Jupyter Contents` + ); + } + + return new WebWorkerKernel( + { + ...options, + mountDrive + }, + spec + ); + } + }); + } + }; + return server_kernel; +}); + +const plugins: JupyterLiteServerPlugin[] = server_kernels; + +export default plugins; diff --git a/src/web_worker_kernel.ts b/src/web_worker_kernel.ts new file mode 100644 index 0000000..95086dc --- /dev/null +++ b/src/web_worker_kernel.ts @@ -0,0 +1,237 @@ +// Copyright (c) Thorsten Beier +// Copyright (c) JupyterLite Contributors +// Distributed under the terms of the Modified BSD License. + +import { wrap } from 'comlink'; +import type { Remote } from 'comlink'; + +import { ISignal, Signal } from '@lumino/signaling'; +import { PromiseDelegate } from '@lumino/coreutils'; + +import { PageConfig } from '@jupyterlab/coreutils'; +import { KernelMessage } from '@jupyterlab/services'; + +import { IKernel } from '@jupyterlite/kernel'; + +interface IXeusKernel { + ready(): Promise; + + mount(driveName: string, mountpoint: string, baseUrl: string): Promise; + + cd(path: string): Promise; + + processMessage(msg: any): Promise; +} + +export class WebWorkerKernel implements IKernel { + /** + * Instantiate a new WebWorkerKernel + * + * @param options The instantiation options for a new WebWorkerKernel + */ + constructor(options: WebWorkerKernel.IOptions, spec: any) { + console.log('constructing WebWorkerKernel kernel'); + const { id, name, sendMessage, location } = options; + this._id = id; + this._name = name; + this._location = location; + this._spec = spec; + this._sendMessage = sendMessage; + console.log('constructing WebWorkerKernel worker'); + this._worker = new Worker(new URL('./worker.js', import.meta.url), { + type: 'module' + }); + console.log('constructing WebWorkerKernel done'); + + this._worker.onmessage = e => { + this._processWorkerMessage(e.data); + }; + + console.log('wrap'); + this._remote = wrap(this._worker); + console.log('wrap done'); + + this._remote.processMessage({ + msg: { + header: { + msg_type: 'initialize' + } + }, + spec: this._spec + }); + + console.log('init filesystem'); + this.initFileSystem(options); + + console.log('constructing WebWorkerKernel done2'); + } + + async handleMessage(msg: KernelMessage.IMessage): Promise { + console.log('handleMessage', msg); + this._parent = msg; + this._parentHeader = msg.header; + console.log('send message to worker'); + await this._sendMessageToWorker(msg); + console.log('send message to worker awaiting done'); + } + + private async _sendMessageToWorker(msg: any): Promise { + // TODO Remove this?? + if (msg.header.msg_type !== 'input_reply') { + this._executeDelegate = new PromiseDelegate(); + } + + console.log(' this._remote.processMessage({ msg, parent: this.parent });'); + await this._remote.processMessage({ msg, parent: this.parent }); + console.log( + ' this._remote.processMessage({ msg, parent: this.parent }); done' + ); + if (msg.header.msg_type !== 'input_reply') { + return await this._executeDelegate.promise; + } + } + + /** + * Get the last parent header + */ + get parentHeader(): + | KernelMessage.IHeader + | undefined { + return this._parentHeader; + } + + /** + * Get the last parent message (mimick ipykernel's get_parent) + */ + get parent(): KernelMessage.IMessage | undefined { + return this._parent; + } + + /** + * Get the kernel location + */ + get location(): string { + return this._location; + } + + /** + * Process a message coming from the pyodide web worker. + * + * @param msg The worker message to process. + */ + private _processWorkerMessage(msg: any): void { + console.log('processWorkerMessage', msg); + if (!msg.header) { + return; + } + + msg.header.session = this._parentHeader?.session ?? ''; + msg.session = this._parentHeader?.session ?? ''; + this._sendMessage(msg); + + // resolve promise + if ( + msg.header.msg_type === 'status' && + msg.content.execution_state === 'idle' + ) { + this._executeDelegate.resolve(); + } + } + + /** + * A promise that is fulfilled when the kernel is ready. + */ + get ready(): Promise { + return Promise.resolve(); + } + + /** + * Return whether the kernel is disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * A signal emitted when the kernel is disposed. + */ + get disposed(): ISignal { + return this._disposed; + } + + /** + * Dispose the kernel. + */ + dispose(): void { + if (this.isDisposed) { + return; + } + this._worker.terminate(); + (this._worker as any) = null; + (this._remote as any) = null; + this._isDisposed = true; + this._disposed.emit(void 0); + } + + /** + * Get the kernel id + */ + get id(): string { + return this._id; + } + + /** + * Get the name of the kernel + */ + get name(): string { + return this._name; + } + + private async initFileSystem(options: WebWorkerKernel.IOptions) { + let driveName: string; + let localPath: string; + + if (options.location.includes(':')) { + const parts = options.location.split(':'); + driveName = parts[0]; + localPath = parts[1]; + } else { + driveName = ''; + localPath = options.location; + } + + await this._remote.ready(); + + if (false || options.mountDrive) { + await this._remote.mount(driveName, '/drive', PageConfig.getBaseUrl()); + await this._remote.cd(localPath); + } + } + + private _spec: any; + private _id: string; + private _name: string; + private _location: string; + private _remote: Remote; + private _isDisposed = false; + private _disposed = new Signal(this); + private _worker: Worker; + private _sendMessage: IKernel.SendMessage; + private _executeDelegate = new PromiseDelegate(); + private _parentHeader: + | KernelMessage.IHeader + | undefined = undefined; + private _parent: KernelMessage.IMessage | undefined = undefined; +} + +/** + * A namespace for WebWorkerKernel statics. + */ +export namespace WebWorkerKernel { + /** + * The instantiation options for a Pyodide kernel + */ + export interface IOptions extends IKernel.IOptions { + mountDrive: boolean; + } +} diff --git a/src/worker.ts b/src/worker.ts new file mode 100644 index 0000000..f022354 --- /dev/null +++ b/src/worker.ts @@ -0,0 +1,252 @@ +// Copyright (c) Thorsten Beier +// Copyright (c) JupyterLite Contributors +// Distributed under the terms of the Modified BSD License. + +console.log('worker loaded'); + +import { expose } from 'comlink'; + +import { + DriveFS, + DriveFSEmscriptenNodeOps, + IEmscriptenFSNode, + IStats +} from '@jupyterlite/contents'; + +declare function createXeusModule(options: any): any; + +globalThis.Module = {}; + +console.log('worker here'); + +// const WASM_KERNEL_FILE = 'kernels/xlite/xlite.js'; +// const WASM_FILE = 'kernels/xlite/xlite.wasm'; +// TODO Remove this. This is to ensure we always perform node ops on Nodes and +// not Streams, but why is it needed??? Why do we get Streams and not Nodes from +// emscripten in the case of xeus-python??? +class StreamNodeOps extends DriveFSEmscriptenNodeOps { + private getNode(nodeOrStream: any) { + if (nodeOrStream['node']) { + return nodeOrStream['node']; + } + return nodeOrStream; + } + + lookup(parent: IEmscriptenFSNode, name: string): IEmscriptenFSNode { + return super.lookup(this.getNode(parent), name); + } + + getattr(node: IEmscriptenFSNode): IStats { + return super.getattr(this.getNode(node)); + } + + setattr(node: IEmscriptenFSNode, attr: IStats): void { + super.setattr(this.getNode(node), attr); + } + + mknod( + parent: IEmscriptenFSNode, + name: string, + mode: number, + dev: any + ): IEmscriptenFSNode { + return super.mknod(this.getNode(parent), name, mode, dev); + } + + rename( + oldNode: IEmscriptenFSNode, + newDir: IEmscriptenFSNode, + newName: string + ): void { + super.rename(this.getNode(oldNode), this.getNode(newDir), newName); + } + + rmdir(parent: IEmscriptenFSNode, name: string): void { + super.rmdir(this.getNode(parent), name); + } + + readdir(node: IEmscriptenFSNode): string[] { + return super.readdir(this.getNode(node)); + } +} + +// TODO Remove this when we don't need StreamNodeOps anymore +class LoggingDrive extends DriveFS { + constructor(options: DriveFS.IOptions) { + super(options); + + this.node_ops = new StreamNodeOps(this); + } +} + +// when a toplevel cell uses an await, the cell is implicitly +// wrapped in a async function. Since the webloop - eventloop +// implementation does not support `eventloop.run_until_complete(f)` +// we need to convert the toplevel future in a javascript Promise +// this `toplevel` promise is then awaited before we +// execute the next cell. After the promise is awaited we need +// to do some cleanup and delete the python proxy +// (ie a js-wrapped python object) to avoid memory leaks +globalThis.toplevel_promise = null; +globalThis.toplevel_promise_py_proxy = null; + +let resolveInputReply: any; + +async function get_stdin() { + const replyPromise = new Promise(resolve => { + resolveInputReply = resolve; + }); + return replyPromise; +} + +(self as any).get_stdin = get_stdin; + +class XeusKernel { + constructor(resolve: any) { + console.log('constructing kernel'); + this._resolve = resolve; + } + + async ready(): Promise { + return await globalThis.ready; + } + + mount(driveName: string, mountpoint: string, baseUrl: string): void { + console.log('mounting drive'); + const { FS, PATH, ERRNO_CODES } = globalThis.Module; + + if (!FS) { + return; + } + + this._drive = new LoggingDrive({ + FS, + PATH, + ERRNO_CODES, + baseUrl, + driveName, + mountpoint + }); + + FS.mkdir(mountpoint); + FS.mount(this._drive, {}, mountpoint); + FS.chdir(mountpoint); + } + + cd(path: string) { + if (!path || !globalThis.Module.FS) { + return; + } + + globalThis.Module.FS.chdir(path); + } + + async processMessage(event: any): Promise { + const msg_type = event.msg.header.msg_type; + if (msg_type === 'initialize') { + const spec = event.spec; + this._spec = spec; + await this.initialize(); + + return; + } + + await this.ready(); + + if ( + globalThis.toplevel_promise !== null && + globalThis.toplevel_promise_py_proxy !== null + ) { + await globalThis.toplevel_promise; + globalThis.toplevel_promise_py_proxy.delete(); + globalThis.toplevel_promise_py_proxy = null; + globalThis.toplevel_promise = null; + } + + if (msg_type === 'input_reply') { + resolveInputReply(event.msg); + } else { + this._raw_xserver.notify_listener(event.msg); + } + } + + private async initialize() { + const dir = this._spec.dir; + const binary_js = this._spec.argv[0]; + const binary_wasm = binary_js.replace('.js', '.wasm'); + + console.log(binary_js); + console.log(binary_wasm); + + importScripts(binary_js); + globalThis.Module = await createXeusModule({ + locateFile: (file: string) => { + if (file.endsWith('.wasm')) { + return binary_wasm; + } + return file; + } + }); + try { + await this.waitRunDependency(); + console.log(globalThis.Module); + + if (globalThis.Module['async_init'] !== undefined) { + const kernel_root_url = `share/jupyter/kernels/${dir}`; + const pkg_root_url = 'share/jupyter/kernel_packages'; + const verbose = true; + await globalThis.Module['async_init']( + kernel_root_url, + pkg_root_url, + verbose + ); + } + + await this.waitRunDependency(); + + this._raw_xkernel = new globalThis.Module.xkernel(); + this._raw_xserver = this._raw_xkernel.get_server(); + if (!this._raw_xkernel) { + console.error('Failed to start kernel!'); + } + this._raw_xkernel.start(); + } catch (e) { + if (typeof e === 'number') { + const msg = globalThis.Module.get_exception_message(e); + console.error(msg); + throw new Error(msg); + } else { + console.error(e); + throw e; + } + } + this._resolve(); + } + + private async waitRunDependency() { + const promise = new Promise(resolve => { + globalThis.Module.monitorRunDependencies = (n: number) => { + if (n === 0) { + resolve(); + } + }; + }); + // If there are no pending dependencies left, monitorRunDependencies will + // never be called. Since we can't check the number of dependencies, + // manually trigger a call. + globalThis.Module.addRunDependency('dummy'); + globalThis.Module.removeRunDependency('dummy'); + return promise; + } + private _resolve: any; + private _spec: any; + private _raw_xkernel: any; + private _raw_xserver: any; + private _drive: DriveFS | null = null; + //private _ready: PromiseLike; +} + +globalThis.ready = new Promise(resolve => { + console.log('expose(new XeusKernel(resolve));'); + expose(new XeusKernel(resolve)); +}); diff --git a/tsconfig.json b/tsconfig.json index 9897917..f7ab437 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "noImplicitAny": true, + "noImplicitAny": false, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, @@ -17,7 +17,7 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, - "target": "ES2018" + "target": "ES2019" }, "include": ["src/*"] } diff --git a/worker.webpack.config.js b/worker.webpack.config.js new file mode 100644 index 0000000..ab382c3 --- /dev/null +++ b/worker.webpack.config.js @@ -0,0 +1,33 @@ +const path = require('path'); +const rules = [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'source-map-loader' + } +]; + +const resolve = { + fallback: { + fs: false, + child_process: false, + crypto: false + }, + extensions: ['.js'] +}; + +module.exports = [ + { + entry: './lib/worker.js', + output: { + filename: 'worker.js', + path: path.resolve(__dirname, 'lib'), + libraryTarget: 'amd' + }, + module: { + rules + }, + devtool: 'source-map', + resolve + } +]; diff --git a/yarn.lock b/yarn.lock index 56f370a..d0fcfaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2065,7 +2065,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/application@npm:^4.0.0, @jupyterlab/application@npm:^4.0.9": +"@jupyterlab/application@npm:^4.0.9": version: 4.0.9 resolution: "@jupyterlab/application@npm:4.0.9" dependencies: @@ -2278,7 +2278,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/coreutils@npm:^6.0.9": +"@jupyterlab/coreutils@npm:^6, @jupyterlab/coreutils@npm:^6.0.9, @jupyterlab/coreutils@npm:~6.0.7": version: 6.0.9 resolution: "@jupyterlab/coreutils@npm:6.0.9" dependencies: @@ -2407,7 +2407,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/nbformat@npm:^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0, @jupyterlab/nbformat@npm:^4.0.9": +"@jupyterlab/nbformat@npm:^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0, @jupyterlab/nbformat@npm:^4.0.9, @jupyterlab/nbformat@npm:~4.0.7": version: 4.0.9 resolution: "@jupyterlab/nbformat@npm:4.0.9" dependencies: @@ -2452,7 +2452,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/observables@npm:^5.0.9": +"@jupyterlab/observables@npm:^5.0.9, @jupyterlab/observables@npm:~5.0.7": version: 5.0.9 resolution: "@jupyterlab/observables@npm:5.0.9" dependencies: @@ -2517,7 +2517,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/services@npm:^7.0.9": +"@jupyterlab/services@npm:^7, @jupyterlab/services@npm:^7.0.9, @jupyterlab/services@npm:~7.0.7": version: 7.0.9 resolution: "@jupyterlab/services@npm:7.0.9" dependencies: @@ -2536,7 +2536,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/settingregistry@npm:^4.0.9": +"@jupyterlab/settingregistry@npm:^4.0.9, @jupyterlab/settingregistry@npm:~4.0.7": version: 4.0.9 resolution: "@jupyterlab/settingregistry@npm:4.0.9" dependencies: @@ -2555,7 +2555,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/statedb@npm:^4.0.9": +"@jupyterlab/statedb@npm:^4.0.9, @jupyterlab/statedb@npm:~4.0.7": version: 4.0.9 resolution: "@jupyterlab/statedb@npm:4.0.9" dependencies: @@ -2685,19 +2685,130 @@ __metadata: languageName: node linkType: hard +"@jupyterlite/contents@npm:^0.2.0": + version: 0.2.0 + resolution: "@jupyterlite/contents@npm:0.2.0" + dependencies: + "@jupyterlab/nbformat": ~4.0.7 + "@jupyterlab/services": ~7.0.7 + "@jupyterlite/localforage": ^0.2.0 + "@lumino/coreutils": ^2.1.2 + "@types/emscripten": ^1.39.6 + localforage: ^1.9.0 + mime: ^3.0.0 + checksum: e395bcc42c59f0c8f141f6e726c8596b26c8ceacc0a80f29b9384c211dbc063d1467a83f7656fac83761eaf5b9b226a606718851dae2fc9c66578bf712f50393 + languageName: node + linkType: hard + +"@jupyterlite/kernel@npm:^0.2.0": + version: 0.2.0 + resolution: "@jupyterlite/kernel@npm:0.2.0" + dependencies: + "@jupyterlab/coreutils": ~6.0.7 + "@jupyterlab/observables": ~5.0.7 + "@jupyterlab/services": ~7.0.7 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + async-mutex: ^0.3.1 + comlink: ^4.3.1 + mock-socket: ^9.1.0 + checksum: 1b5c3ba6dbb9fdc80ad4508a93b8b324c2b276f2ae8f124274fa3ee562362de08e17144b225e54d8fb33f0eb35f9ed930d5ebec5adc576bdd4223d5213c07dfb + languageName: node + linkType: hard + +"@jupyterlite/localforage@npm:^0.2.0": + version: 0.2.0 + resolution: "@jupyterlite/localforage@npm:0.2.0" + dependencies: + "@jupyterlab/coreutils": ~6.0.7 + "@lumino/coreutils": ^2.1.2 + localforage: ^1.9.0 + localforage-memoryStorageDriver: ^0.9.2 + checksum: 5510933708c6790c06d1ee0a12fbbd1feecf8c6e083fb11a416cedca107520792929ddbecba87a45ff7aa0ea8153c7dbd9fa5b935d13ad1c9900758c9a84a000 + languageName: node + linkType: hard + +"@jupyterlite/server@npm:^0.2.0": + version: 0.2.0 + resolution: "@jupyterlite/server@npm:0.2.0" + dependencies: + "@jupyterlab/coreutils": ~6.0.7 + "@jupyterlab/nbformat": ~4.0.7 + "@jupyterlab/observables": ~5.0.7 + "@jupyterlab/services": ~7.0.7 + "@jupyterlab/settingregistry": ~4.0.7 + "@jupyterlab/statedb": ~4.0.7 + "@jupyterlite/contents": ^0.2.0 + "@jupyterlite/kernel": ^0.2.0 + "@jupyterlite/session": ^0.2.0 + "@jupyterlite/settings": ^0.2.0 + "@jupyterlite/translation": ^0.2.0 + "@lumino/application": ^2.2.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/signaling": ^2.1.2 + mock-socket: ^9.1.0 + checksum: 1f8388b39b9ee419b7b58647eeba0f52f498a925348e848b4e5e859707a298f979fe03b8c3ca689472f249fa1fdab28b24d84ddc9dd233102fb65c8318a443c7 + languageName: node + linkType: hard + +"@jupyterlite/session@npm:^0.2.0": + version: 0.2.0 + resolution: "@jupyterlite/session@npm:0.2.0" + dependencies: + "@jupyterlab/coreutils": ~6.0.7 + "@jupyterlab/services": ~7.0.7 + "@jupyterlite/kernel": ^0.2.0 + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + checksum: 022305bb6237ae2f5071f14e8e171f1e63628ef7f9feea4e4499767f349d784bb8fbde0889cdc2c6c996416d9880069a16c51ea46715488f75d70ef3b6342b5c + languageName: node + linkType: hard + +"@jupyterlite/settings@npm:^0.2.0": + version: 0.2.0 + resolution: "@jupyterlite/settings@npm:0.2.0" + dependencies: + "@jupyterlab/coreutils": ~6.0.7 + "@jupyterlab/settingregistry": ~4.0.7 + "@jupyterlite/localforage": ^0.2.0 + "@lumino/coreutils": ^2.1.2 + json5: ^2.2.0 + localforage: ^1.9.0 + checksum: ff651f66292c6e10d09e3fe1b6cfb964421afe30495f920b8416a0e411834dc41eb0ea817b74fe4c20d28579a4f58e19a2597ad162067e92b91b887c4dd8c8d4 + languageName: node + linkType: hard + +"@jupyterlite/translation@npm:^0.2.0": + version: 0.2.0 + resolution: "@jupyterlite/translation@npm:0.2.0" + dependencies: + "@jupyterlab/coreutils": ~6.0.7 + "@lumino/coreutils": ^2.1.2 + checksum: 53f8b9d3a800192fa42e96d04979d9494c263dbb78e7be22443f1f98f2f4f0ea79103b1bd8c40e410978e2d330de31bf6b3cf4e31b2a15ed40c9003a5e3b9604 + languageName: node + linkType: hard + "@jupyterlite/xeus@workspace:.": version: 0.0.0-use.local resolution: "@jupyterlite/xeus@workspace:." dependencies: - "@jupyterlab/application": ^4.0.0 "@jupyterlab/builder": ^4.0.0 + "@jupyterlab/coreutils": ^6 + "@jupyterlab/services": ^7 "@jupyterlab/testutils": ^4.0.0 + "@jupyterlite/contents": ^0.2.0 + "@jupyterlite/kernel": ^0.2.0 + "@jupyterlite/server": ^0.2.0 + "@lumino/coreutils": ^2 + "@lumino/signaling": ^2 "@types/jest": ^29.2.0 "@types/json-schema": ^7.0.11 "@types/react": ^18.0.26 "@types/react-addons-linked-state-mixin": ^0.14.22 "@typescript-eslint/eslint-plugin": ^6.1.0 "@typescript-eslint/parser": ^6.1.0 + comlink: ^4.3.1 css-loader: ^6.7.1 eslint: ^8.36.0 eslint-config-prettier: ^8.8.0 @@ -2713,7 +2824,9 @@ __metadata: stylelint-config-standard: ^34.0.0 stylelint-csstree-validator: ^3.0.0 stylelint-prettier: ^4.0.0 + ts-loader: ^9.2.6 typescript: ~5.0.2 + webpack: ^5.87.0 yjs: ^13.5.0 languageName: unknown linkType: soft @@ -2908,7 +3021,7 @@ __metadata: languageName: node linkType: hard -"@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.1.2, @lumino/coreutils@npm:^2.1.2": +"@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.1.2, @lumino/coreutils@npm:^2, @lumino/coreutils@npm:^2.1.2": version: 2.1.2 resolution: "@lumino/coreutils@npm:2.1.2" checksum: 7865317ac0676b448d108eb57ab5d8b2a17c101995c0f7a7106662d9fe6c859570104525f83ee3cda12ae2e326803372206d6f4c1f415a5b59e4158a7b81066f @@ -2976,7 +3089,7 @@ __metadata: languageName: node linkType: hard -"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.1.2": +"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2, @lumino/signaling@npm:^2.1.2": version: 2.1.2 resolution: "@lumino/signaling@npm:2.1.2" dependencies: @@ -3188,6 +3301,13 @@ __metadata: languageName: node linkType: hard +"@types/emscripten@npm:^1.39.6": + version: 1.39.10 + resolution: "@types/emscripten@npm:1.39.10" + checksum: 1721da76593f9194e0b7c90a581e2d31c23bd4eb28f93030cd1dc58216cdf1e692c045274f2eedaed29c652c25c9a4dff2e503b11bd1258d07095c009a1956b1 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -3987,6 +4107,15 @@ __metadata: languageName: node linkType: hard +"async-mutex@npm:^0.3.1": + version: 0.3.2 + resolution: "async-mutex@npm:0.3.2" + dependencies: + tslib: ^2.3.1 + checksum: 620b771dfdea1cad0a6b712915c31a1e3ca880a8cf1eae92b4590f435995e0260929c6ebaae0b9126b1456790ea498064b5bb9a506948cda760f48d3d0dcc4c8 + languageName: node + linkType: hard + "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -4308,7 +4437,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0": +"chalk@npm:^4.0.0, chalk@npm:^4.1.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -4458,6 +4587,13 @@ __metadata: languageName: node linkType: hard +"comlink@npm:^4.3.1": + version: 4.4.1 + resolution: "comlink@npm:4.4.1" + checksum: 16d58a8f590087fc45432e31d6c138308dfd4b75b89aec0b7f7bb97ad33d810381bd2b1e608a1fb2cf05979af9cbfcdcaf1715996d5fcf77aeb013b6da3260af + languageName: node + linkType: hard + "commander@npm:^10.0.1": version: 10.0.1 resolution: "commander@npm:10.0.1" @@ -4962,7 +5098,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.15.0": +"enhanced-resolve@npm:^5.0.0, enhanced-resolve@npm:^5.15.0": version: 5.15.0 resolution: "enhanced-resolve@npm:5.15.0" dependencies: @@ -6068,6 +6204,13 @@ __metadata: languageName: node linkType: hard +"immediate@npm:~3.0.5": + version: 3.0.6 + resolution: "immediate@npm:3.0.6" + checksum: f9b3486477555997657f70318cc8d3416159f208bec4cca3ff3442fd266bc23f50f0c9bd8547e1371a6b5e82b821ec9a7044a4f7b944798b25aa3cc6d5e63e62 + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -7178,7 +7321,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.1.2, json5@npm:^2.2.3": +"json5@npm:^2.1.2, json5@npm:^2.2.0, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -7279,6 +7422,15 @@ __metadata: languageName: node linkType: hard +"lie@npm:3.1.1": + version: 3.1.1 + resolution: "lie@npm:3.1.1" + dependencies: + immediate: ~3.0.5 + checksum: 6da9f2121d2dbd15f1eca44c0c7e211e66a99c7b326ec8312645f3648935bc3a658cf0e9fa7b5f10144d9e2641500b4f55bd32754607c3de945b5f443e50ddd1 + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -7316,6 +7468,24 @@ __metadata: languageName: node linkType: hard +"localforage-memoryStorageDriver@npm:^0.9.2": + version: 0.9.2 + resolution: "localforage-memoryStorageDriver@npm:0.9.2" + dependencies: + localforage: ">=1.4.0" + checksum: 1f867be54d005e3009cd841f3cadde728468911a4a8cca6c7c77eb1ce8c28526d9c373d8881e5e25391a5114445c9b68d3a1f05319bd33ff0a787d99eeb829c9 + languageName: node + linkType: hard + +"localforage@npm:>=1.4.0, localforage@npm:^1.9.0": + version: 1.10.0 + resolution: "localforage@npm:1.10.0" + dependencies: + lie: 3.1.1 + checksum: f2978b434dafff9bcb0d9498de57d97eba165402419939c944412e179cab1854782830b5ec196212560b22712d1dd03918939f59cf1d4fc1d756fca7950086cf + languageName: node + linkType: hard + "locate-path@npm:^5.0.0": version: 5.0.0 resolution: "locate-path@npm:5.0.0" @@ -7548,7 +7718,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": +"micromatch@npm:^4.0.0, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": version: 4.0.5 resolution: "micromatch@npm:4.0.5" dependencies: @@ -7574,6 +7744,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^3.0.0": + version: 3.0.0 + resolution: "mime@npm:3.0.0" + bin: + mime: cli.js + checksum: f43f9b7bfa64534e6b05bd6062961681aeb406a5b53673b53b683f27fcc4e739989941836a355eef831f4478923651ecc739f4a5f6e20a76487b432bfd4db928 + languageName: node + linkType: hard + "mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -7744,6 +7923,13 @@ __metadata: languageName: node linkType: hard +"mock-socket@npm:^9.1.0": + version: 9.3.1 + resolution: "mock-socket@npm:9.3.1" + checksum: cb2dde4fc5dde280dd5ccb78eaaa223382ee16437f46b86558017655584ad08c22e733bde2dd5cc86927def506b6caeb0147e3167b9a62d70d5cf19d44103853 + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -9108,6 +9294,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:^0.7.4": + version: 0.7.4 + resolution: "source-map@npm:0.7.4" + checksum: 01cc5a74b1f0e1d626a58d36ad6898ea820567e87f18dfc9d24a9843a351aaa2ec09b87422589906d6ff1deed29693e176194dc88bcae7c9a852dc74b311dbf5 + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.2.0 resolution: "spdx-correct@npm:3.2.0" @@ -9693,7 +9886,23 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.5.0, tslib@npm:^2.6.0": +"ts-loader@npm:^9.2.6": + version: 9.5.1 + resolution: "ts-loader@npm:9.5.1" + dependencies: + chalk: ^4.1.0 + enhanced-resolve: ^5.0.0 + micromatch: ^4.0.0 + semver: ^7.3.4 + source-map: ^0.7.4 + peerDependencies: + typescript: "*" + webpack: ^5.0.0 + checksum: 7cf396e656d905388ea2a9b5e82f16d3c955fda8d3df2fbf219f4bee16ff50a3c995c44ae3e584634e9443f056cec70bb3151add3917ffb4588ecd7394bac0ec + languageName: node + linkType: hard + +"tslib@npm:^2.3.1, tslib@npm:^2.5.0, tslib@npm:^2.6.0": version: 2.6.2 resolution: "tslib@npm:2.6.2" checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad @@ -10169,7 +10378,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5.76.1": +"webpack@npm:^5.76.1, webpack@npm:^5.87.0": version: 5.89.0 resolution: "webpack@npm:5.89.0" dependencies: