From 4fbdefe94ead0b1681132ad4f83a44cc4a88195b Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 27 Jun 2023 16:09:50 +0900 Subject: [PATCH] Gradio-lite (Gradio Wasm) (#4402) --- .config/.prettierignore | 1 + .github/workflows/ui.yml | 2 + client/js/src/client.ts | 1105 +++++++++-------- client/js/src/index.ts | 8 +- gradio/blocks.py | 55 +- gradio/components/video.py | 19 +- gradio/processing_utils.py | 11 +- gradio/routes.py | 18 +- gradio/strings.py | 4 +- gradio/wasm_utils.py | 24 + js/app/lite.html | 43 + js/app/package.json | 6 + js/app/src/Index.svelte | 13 +- js/app/src/components/File/File.svelte | 8 +- .../UploadButton/UploadButton.svelte | 8 +- js/app/src/lite/css.ts | 36 + js/app/src/lite/fetch.ts | 54 + js/app/src/lite/index.ts | 162 +++ js/app/src/lite/url.ts | 6 + js/app/src/main.ts | 7 + js/app/src/vite-env-override.d.ts | 6 + js/app/vite.config.js | 71 +- js/lite/index.html | 72 ++ js/wasm/package.json | 22 + js/wasm/src/index.ts | 1 + js/wasm/src/message-types.ts | 62 + js/wasm/src/webworker/declarations.d.ts | 2 + js/wasm/src/webworker/http.ts | 135 ++ js/wasm/src/webworker/index.ts | 205 +++ js/wasm/src/worker-proxy.ts | 104 ++ js/wasm/tsconfig.json | 105 ++ js/wasm/vite.worker.config.js | 34 + pnpm-lock.yaml | 215 ++-- pnpm-workspace.yaml | 2 +- 34 files changed, 1939 insertions(+), 687 deletions(-) create mode 100644 gradio/wasm_utils.py create mode 100644 js/app/lite.html create mode 100644 js/app/src/lite/css.ts create mode 100644 js/app/src/lite/fetch.ts create mode 100644 js/app/src/lite/index.ts create mode 100644 js/app/src/lite/url.ts create mode 100644 js/app/src/vite-env-override.d.ts create mode 100644 js/lite/index.html create mode 100644 js/wasm/package.json create mode 100644 js/wasm/src/index.ts create mode 100644 js/wasm/src/message-types.ts create mode 100644 js/wasm/src/webworker/declarations.d.ts create mode 100644 js/wasm/src/webworker/http.ts create mode 100644 js/wasm/src/webworker/index.ts create mode 100644 js/wasm/src/worker-proxy.ts create mode 100644 js/wasm/tsconfig.json create mode 100644 js/wasm/vite.worker.config.js diff --git a/.config/.prettierignore b/.config/.prettierignore index 37317d1179..a8226fdd61 100644 --- a/.config/.prettierignore +++ b/.config/.prettierignore @@ -2,6 +2,7 @@ **/pnpm-workspace.yaml **/js/app/dist/** **/client/js/dist/** +**/js/lite/dist/** **/pnpm-lock.yaml **/js/plot/src/Plot.svelte **/.svelte-kit/** diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index b9be723c6e..81c3f32c16 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -27,6 +27,8 @@ jobs: always-install-pnpm: true - name: build client run: pnpm --filter @gradio/client build + - name: build the wasm module + run: pnpm --filter @gradio/wasm build - name: lint run: pnpm lint continue-on-error: true diff --git a/client/js/src/client.ts b/client/js/src/client.ts index 1b42622653..22cad0dc96 100644 --- a/client/js/src/client.ts +++ b/client/js/src/client.ts @@ -58,62 +58,8 @@ type SubmitReturn = { const QUEUE_FULL_MSG = "This application is too busy. Keep trying!"; const BROKEN_CONNECTION_MSG = "Connection errored out."; -export async function post_data( - url: string, - body: unknown, - token?: `hf_${string}` -): Promise<[PostResponse, number]> { - const headers: { - Authorization?: string; - "Content-Type": "application/json"; - } = { "Content-Type": "application/json" }; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - try { - var response = await fetch(url, { - method: "POST", - body: JSON.stringify(body), - headers - }); - } catch (e) { - return [{ error: BROKEN_CONNECTION_MSG }, 500]; - } - const output: PostResponse = await response.json(); - return [output, response.status]; -} - export let NodeBlob; -export async function upload_files( - root: string, - files: Array, - token?: `hf_${string}` -): Promise { - const headers: { - Authorization?: string; - } = {}; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - const formData = new FormData(); - files.forEach((file) => { - formData.append("files", file); - }); - try { - var response = await fetch(`${root}/upload`, { - method: "POST", - body: formData, - headers - }); - } catch (e) { - return { error: BROKEN_CONNECTION_MSG }; - } - const output: UploadResponse["files"] = await response.json(); - return { files: output }; -} - export async function duplicate( app_reference: string, options: { @@ -197,489 +143,615 @@ export async function duplicate( } } -export async function client( - app_reference: string, - options: { - hf_token?: `hf_${string}`; - status_callback?: SpaceStatusCallback; - normalise_files?: boolean; - } = { normalise_files: true } -): Promise { - return new Promise(async (res) => { - const { status_callback, hf_token, normalise_files } = options; - const return_obj = { - predict, - submit, - view_api - // duplicate - }; +/** + * We need to inject a customized fetch implementation for the Wasm version. + */ +export function api_factory(fetch_implementation: typeof fetch) { + return { post_data, upload_files, client, handle_blob }; - const transform_files = normalise_files ?? true; - if (typeof window === "undefined" || !("WebSocket" in window)) { - const ws = await import("ws"); - NodeBlob = (await import("node:buffer")).Blob; - //@ts-ignore - global.WebSocket = ws.WebSocket; + async function post_data( + url: string, + body: unknown, + token?: `hf_${string}` + ): Promise<[PostResponse, number]> { + const headers: { + Authorization?: string; + "Content-Type": "application/json"; + } = { "Content-Type": "application/json" }; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + try { + var response = await fetch_implementation(url, { + method: "POST", + body: JSON.stringify(body), + headers + }); + } catch (e) { + return [{ error: BROKEN_CONNECTION_MSG }, 500]; + } + const output: PostResponse = await response.json(); + return [output, response.status]; + } + + async function upload_files( + root: string, + files: Array, + token?: `hf_${string}` + ): Promise { + const headers: { + Authorization?: string; + } = {}; + if (token) { + headers.Authorization = `Bearer ${token}`; } - const { ws_protocol, http_protocol, host, space_id } = - await process_endpoint(app_reference, hf_token); - - const session_hash = Math.random().toString(36).substring(2); - const last_status: Record = {}; - let config: Config; - let api_map: Record = {}; - - let jwt: false | string = false; - - if (hf_token && space_id) { - jwt = await get_jwt(space_id, hf_token); + const formData = new FormData(); + files.forEach((file) => { + formData.append("files", file); + }); + try { + var response = await fetch_implementation(`${root}/upload`, { + method: "POST", + body: formData, + headers + }); + } catch (e) { + return { error: BROKEN_CONNECTION_MSG }; } + const output: UploadResponse["files"] = await response.json(); + return { files: output }; + } - async function config_success(_config: Config) { - config = _config; - api_map = map_names_to_ids(_config?.dependencies || []); - try { - api = await view_api(config); - } catch (e) { - console.error(`Could not get api details: ${e.message}`); + async function client( + app_reference: string, + options: { + hf_token?: `hf_${string}`; + status_callback?: SpaceStatusCallback; + normalise_files?: boolean; + } = { normalise_files: true } + ): Promise { + return new Promise(async (res) => { + const { status_callback, hf_token, normalise_files } = options; + const return_obj = { + predict, + submit, + view_api + // duplicate + }; + + const transform_files = normalise_files ?? true; + if (typeof window === "undefined" || !("WebSocket" in window)) { + const ws = await import("ws"); + NodeBlob = (await import("node:buffer")).Blob; + //@ts-ignore + global.WebSocket = ws.WebSocket; } - return { - config, - ...return_obj - }; - } - let api: ApiInfo; - async function handle_space_sucess(status: SpaceStatus) { - if (status_callback) status_callback(status); - if (status.status === "running") - try { - config = await resolve_config(`${http_protocol}//${host}`, hf_token); + const { ws_protocol, http_protocol, host, space_id } = + await process_endpoint(app_reference, hf_token); - const _config = await config_success(config); - res(_config); + const session_hash = Math.random().toString(36).substring(2); + const last_status: Record = {}; + let config: Config; + let api_map: Record = {}; + + let jwt: false | string = false; + + if (hf_token && space_id) { + jwt = await get_jwt(space_id, hf_token); + } + + async function config_success(_config: Config) { + config = _config; + api_map = map_names_to_ids(_config?.dependencies || []); + try { + api = await view_api(config); } catch (e) { - if (status_callback) { + console.error(`Could not get api details: ${e.message}`); + } + + return { + config, + ...return_obj + }; + } + let api: ApiInfo; + async function handle_space_sucess(status: SpaceStatus) { + if (status_callback) status_callback(status); + if (status.status === "running") + try { + config = await resolve_config( + fetch_implementation, + `${http_protocol}//${host}`, + hf_token + ); + + const _config = await config_success(config); + res(_config); + } catch (e) { + console.error(e); + if (status_callback) { + status_callback({ + status: "error", + message: "Could not load this space.", + load_status: "error", + detail: "NOT_FOUND" + }); + } + } + } + + try { + config = await resolve_config( + fetch_implementation, + `${http_protocol}//${host}`, + hf_token + ); + + const _config = await config_success(config); + res(_config); + } catch (e) { + console.error(e); + if (space_id) { + check_space_status( + space_id, + RE_SPACE_NAME.test(space_id) ? "space_name" : "subdomain", + handle_space_sucess + ); + } else { + if (status_callback) status_callback({ status: "error", message: "Could not load this space.", load_status: "error", detail: "NOT_FOUND" }); - } } - } - - try { - config = await resolve_config(`${http_protocol}//${host}`, hf_token); - - const _config = await config_success(config); - res(_config); - } catch (e) { - if (space_id) { - check_space_status( - space_id, - RE_SPACE_NAME.test(space_id) ? "space_name" : "subdomain", - handle_space_sucess - ); - } else { - if (status_callback) - status_callback({ - status: "error", - message: "Could not load this space.", - load_status: "error", - detail: "NOT_FOUND" - }); - } - } - - /** - * Run a prediction. - * @param endpoint - The prediction endpoint to use. - * @param status_callback - A function that is called with the current status of the prediction immediately and every time it updates. - * @return Returns the data for the prediction or an error message. - */ - function predict(endpoint: string, data: unknown[], event_data?: unknown) { - let data_returned = false; - let status_complete = false; - return new Promise((res, rej) => { - const app = submit(endpoint, data, event_data); - - app - .on("data", (d) => { - data_returned = true; - if (status_complete) { - app.destroy(); - } - res(d); - }) - .on("status", (status) => { - if (status.stage === "error") rej(status); - if (status.stage === "complete" && data_returned) { - app.destroy(); - } - if (status.stage === "complete") { - status_complete = true; - } - }); - }); - } - - function submit( - endpoint: string | number, - data: unknown[], - event_data?: unknown - ): SubmitReturn { - let fn_index: number; - let api_info; - - if (typeof endpoint === "number") { - fn_index = endpoint; - api_info = api.unnamed_endpoints[fn_index]; - } else { - const trimmed_endpoint = endpoint.replace(/^\//, ""); - - fn_index = api_map[trimmed_endpoint]; - api_info = api.named_endpoints[endpoint.trim()]; } - if (typeof fn_index !== "number") { - throw new Error( - "There is no endpoint matching that name of fn_index matching that number." - ); - } + /** + * Run a prediction. + * @param endpoint - The prediction endpoint to use. + * @param status_callback - A function that is called with the current status of the prediction immediately and every time it updates. + * @return Returns the data for the prediction or an error message. + */ + function predict( + endpoint: string, + data: unknown[], + event_data?: unknown + ) { + let data_returned = false; + let status_complete = false; + return new Promise((res, rej) => { + const app = submit(endpoint, data, event_data); - let websocket: WebSocket; - - const _endpoint = typeof endpoint === "number" ? "/predict" : endpoint; - let payload: Payload; - let complete: false | Record = false; - const listener_map: ListenerMap = {}; - - handle_blob( - `${http_protocol}//${host + config.path}`, - data, - api_info, - hf_token - ).then((_payload) => { - payload = { data: _payload || [], event_data, fn_index }; - if (skip_queue(fn_index, config)) { - fire_event({ - type: "status", - endpoint: _endpoint, - stage: "pending", - queue: false, - fn_index, - time: new Date() - }); - - post_data( - `${http_protocol}//${host + config.path}/run${ - _endpoint.startsWith("/") ? _endpoint : `/${_endpoint}` - }`, - { - ...payload, - session_hash - }, - hf_token - ) - .then(([output, status_code]) => { - const data = transform_files - ? transform_output( - output.data, - api_info, - config.root, - config.root_url - ) - : output.data; - if (status_code == 200) { - fire_event({ - type: "data", - endpoint: _endpoint, - fn_index, - data: data, - time: new Date() - }); - - fire_event({ - type: "status", - endpoint: _endpoint, - fn_index, - stage: "complete", - eta: output.average_duration, - queue: false, - time: new Date() - }); - } else { - fire_event({ - type: "status", - stage: "error", - endpoint: _endpoint, - fn_index, - message: output.error, - queue: false, - time: new Date() - }); + app + .on("data", (d) => { + data_returned = true; + if (status_complete) { + app.destroy(); } + res(d); }) - .catch((e) => { - fire_event({ - type: "status", - stage: "error", - message: e.message, - endpoint: _endpoint, - fn_index, - queue: false, - time: new Date() - }); - }); - } else { - fire_event({ - type: "status", - stage: "pending", - queue: true, - endpoint: _endpoint, - fn_index, - time: new Date() - }); - - let url = new URL(`${ws_protocol}://${host}${config.path} - /queue/join`); - - if (jwt) { - url.searchParams.set("__sign", jwt); - } - - websocket = new WebSocket(url); - - websocket.onclose = (evt) => { - if (!evt.wasClean) { - fire_event({ - type: "status", - stage: "error", - message: BROKEN_CONNECTION_MSG, - queue: true, - endpoint: _endpoint, - fn_index, - time: new Date() - }); - } - }; - - websocket.onmessage = function (event) { - const _data = JSON.parse(event.data); - const { type, status, data } = handle_message( - _data, - last_status[fn_index] - ); - - if (type === "update" && status && !complete) { - // call 'status' listeners - fire_event({ - type: "status", - endpoint: _endpoint, - fn_index, - time: new Date(), - ...status - }); - if (status.stage === "error") { - websocket.close(); + .on("status", (status) => { + if (status.stage === "error") rej(status); + if (status.stage === "complete" && data_returned) { + app.destroy(); } - } else if (type === "hash") { - websocket.send(JSON.stringify({ fn_index, session_hash })); - return; - } else if (type === "data") { - websocket.send(JSON.stringify({ ...payload, session_hash })); - } else if (type === "complete") { - complete = status; - } else if (type === "generating") { - fire_event({ - type: "status", - time: new Date(), - ...status, - stage: status?.stage!, - queue: true, - endpoint: _endpoint, - fn_index - }); - } - if (data) { - fire_event({ - type: "data", - time: new Date(), - data: transform_files + if (status.stage === "complete") { + status_complete = true; + } + }); + }); + } + + function submit( + endpoint: string | number, + data: unknown[], + event_data?: unknown + ): SubmitReturn { + let fn_index: number; + let api_info; + + if (typeof endpoint === "number") { + fn_index = endpoint; + api_info = api.unnamed_endpoints[fn_index]; + } else { + const trimmed_endpoint = endpoint.replace(/^\//, ""); + + fn_index = api_map[trimmed_endpoint]; + api_info = api.named_endpoints[endpoint.trim()]; + } + + if (typeof fn_index !== "number") { + throw new Error( + "There is no endpoint matching that name of fn_index matching that number." + ); + } + + let websocket: WebSocket; + + const _endpoint = typeof endpoint === "number" ? "/predict" : endpoint; + let payload: Payload; + let complete: false | Record = false; + const listener_map: ListenerMap = {}; + + handle_blob( + `${http_protocol}//${host + config.path}`, + data, + api_info, + hf_token + ).then((_payload) => { + payload = { data: _payload || [], event_data, fn_index }; + if (skip_queue(fn_index, config)) { + fire_event({ + type: "status", + endpoint: _endpoint, + stage: "pending", + queue: false, + fn_index, + time: new Date() + }); + + post_data( + `${http_protocol}//${host + config.path}/run${ + _endpoint.startsWith("/") ? _endpoint : `/${_endpoint}` + }`, + { + ...payload, + session_hash + }, + hf_token + ) + .then(([output, status_code]) => { + const data = transform_files ? transform_output( - data.data, + output.data, api_info, config.root, config.root_url ) - : data.data, - endpoint: _endpoint, - fn_index - }); + : output.data; + if (status_code == 200) { + fire_event({ + type: "data", + endpoint: _endpoint, + fn_index, + data: data, + time: new Date() + }); - if (complete) { + fire_event({ + type: "status", + endpoint: _endpoint, + fn_index, + stage: "complete", + eta: output.average_duration, + queue: false, + time: new Date() + }); + } else { + fire_event({ + type: "status", + stage: "error", + endpoint: _endpoint, + fn_index, + message: output.error, + queue: false, + time: new Date() + }); + } + }) + .catch((e) => { + fire_event({ + type: "status", + stage: "error", + message: e.message, + endpoint: _endpoint, + fn_index, + queue: false, + time: new Date() + }); + }); + } else { + fire_event({ + type: "status", + stage: "pending", + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date() + }); + + let url = new URL(`${ws_protocol}://${host}${config.path} + /queue/join`); + + if (jwt) { + url.searchParams.set("__sign", jwt); + } + + websocket = new WebSocket(url); + + websocket.onclose = (evt) => { + if (!evt.wasClean) { + fire_event({ + type: "status", + stage: "error", + message: BROKEN_CONNECTION_MSG, + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date() + }); + } + }; + + websocket.onmessage = function (event) { + const _data = JSON.parse(event.data); + const { type, status, data } = handle_message( + _data, + last_status[fn_index] + ); + + if (type === "update" && status && !complete) { + // call 'status' listeners + fire_event({ + type: "status", + endpoint: _endpoint, + fn_index, + time: new Date(), + ...status + }); + if (status.stage === "error") { + websocket.close(); + } + } else if (type === "hash") { + websocket.send(JSON.stringify({ fn_index, session_hash })); + return; + } else if (type === "data") { + websocket.send(JSON.stringify({ ...payload, session_hash })); + } else if (type === "complete") { + complete = status; + } else if (type === "generating") { fire_event({ type: "status", time: new Date(), - ...complete, + ...status, stage: status?.stage!, queue: true, endpoint: _endpoint, fn_index }); - websocket.close(); } - } - }; + if (data) { + fire_event({ + type: "data", + time: new Date(), + data: transform_files + ? transform_output( + data.data, + api_info, + config.root, + config.root_url + ) + : data.data, + endpoint: _endpoint, + fn_index + }); - // different ws contract for gradio versions older than 3.6.0 - //@ts-ignore - if (semiver(config.version || "2.0.0", "3.6") < 0) { - addEventListener("open", () => - websocket.send(JSON.stringify({ hash: session_hash })) + if (complete) { + fire_event({ + type: "status", + time: new Date(), + ...complete, + stage: status?.stage!, + queue: true, + endpoint: _endpoint, + fn_index + }); + websocket.close(); + } + } + }; + + // different ws contract for gradio versions older than 3.6.0 + //@ts-ignore + if (semiver(config.version || "2.0.0", "3.6") < 0) { + addEventListener("open", () => + websocket.send(JSON.stringify({ hash: session_hash })) + ); + } + } + }); + + function fire_event(event: Event) { + const narrowed_listener_map: ListenerMap = listener_map; + const listeners = narrowed_listener_map[event.type] || []; + listeners?.forEach((l) => l(event)); + } + + function on( + eventType: K, + listener: EventListener + ) { + const narrowed_listener_map: ListenerMap = listener_map; + const listeners = narrowed_listener_map[eventType] || []; + narrowed_listener_map[eventType] = listeners; + listeners?.push(listener); + + return { on, off, cancel, destroy }; + } + + function off( + eventType: K, + listener: EventListener + ) { + const narrowed_listener_map: ListenerMap = listener_map; + let listeners = narrowed_listener_map[eventType] || []; + listeners = listeners?.filter((l) => l !== listener); + narrowed_listener_map[eventType] = listeners; + + return { on, off, cancel, destroy }; + } + + async function cancel() { + const _status: Status = { + stage: "complete", + queue: false, + time: new Date() + }; + complete = _status; + fire_event({ + ..._status, + type: "status", + endpoint: _endpoint, + fn_index: fn_index + }); + + if (websocket && websocket.readyState === 0) { + websocket.addEventListener("open", () => { + websocket.close(); + }); + } else { + websocket.close(); + } + + try { + await fetch_implementation( + `${http_protocol}//${host + config.path}/reset`, + { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify({ fn_index, session_hash }) + } + ); + } catch (e) { + console.warn( + "The `/reset` endpoint could not be called. Subsequent endpoint results may be unreliable." ); } } + + function destroy() { + for (const event_type in listener_map) { + listener_map[event_type as "data" | "status"].forEach((fn) => { + off(event_type as "data" | "status", fn); + }); + } + } + + return { + on, + off, + cancel, + destroy + }; + } + + async function view_api(config?: Config): Promise> { + if (api) return api; + + const headers: { + Authorization?: string; + "Content-Type": "application/json"; + } = { "Content-Type": "application/json" }; + if (hf_token) { + headers.Authorization = `Bearer ${hf_token}`; + } + let response: Response; + // @ts-ignore + if (semiver(config.version || "2.0.0", "3.30") < 0) { + response = await fetch_implementation( + "https://gradio-space-api-fetcher-v2.hf.space/api", + { + method: "POST", + body: JSON.stringify({ + serialize: false, + config: JSON.stringify(config) + }), + headers + } + ); + } else { + response = await fetch_implementation(`${config.root}/info`, { + headers + }); + } + + if (!response.ok) { + throw new Error(BROKEN_CONNECTION_MSG); + } + + let api_info = (await response.json()) as + | ApiInfo + | { api: ApiInfo }; + if ("api" in api_info) { + api_info = api_info.api; + } + + if ( + api_info.named_endpoints["/predict"] && + !api_info.unnamed_endpoints["0"] + ) { + api_info.unnamed_endpoints[0] = api_info.named_endpoints["/predict"]; + } + + const x = transform_api_info(api_info, config, api_map); + return x; + } + }); + } + + async function handle_blob( + endpoint: string, + data: unknown[], + api_info, + token?: `hf_${string}` + ): Promise { + const blob_refs = await walk_and_store_blobs( + data, + undefined, + [], + true, + api_info + ); + + return Promise.all( + blob_refs.map(async ({ path, blob, data, type }) => { + if (blob) { + const file_url = (await upload_files(endpoint, [blob], token)) + .files[0]; + return { path, file_url, type }; + } else { + return { path, base64: data, type }; + } + }) + ).then((r) => { + r.forEach(({ path, file_url, base64, type }) => { + if (base64) { + update_object(data, base64, path); + } else if (type === "Gallery") { + update_object(data, file_url, path); + } else if (file_url) { + const o = { + is_file: true, + name: `${file_url}`, + data: null + // orig_name: "file.csv" + }; + update_object(data, o, path); + } }); - function fire_event(event: Event) { - const narrowed_listener_map: ListenerMap = listener_map; - const listeners = narrowed_listener_map[event.type] || []; - listeners?.forEach((l) => l(event)); - } - - function on( - eventType: K, - listener: EventListener - ) { - const narrowed_listener_map: ListenerMap = listener_map; - const listeners = narrowed_listener_map[eventType] || []; - narrowed_listener_map[eventType] = listeners; - listeners?.push(listener); - - return { on, off, cancel, destroy }; - } - - function off( - eventType: K, - listener: EventListener - ) { - const narrowed_listener_map: ListenerMap = listener_map; - let listeners = narrowed_listener_map[eventType] || []; - listeners = listeners?.filter((l) => l !== listener); - narrowed_listener_map[eventType] = listeners; - - return { on, off, cancel, destroy }; - } - - async function cancel() { - const _status: Status = { - stage: "complete", - queue: false, - time: new Date() - }; - complete = _status; - fire_event({ - ..._status, - type: "status", - endpoint: _endpoint, - fn_index: fn_index - }); - - if (websocket && websocket.readyState === 0) { - websocket.addEventListener("open", () => { - websocket.close(); - }); - } else { - websocket.close(); - } - - try { - await fetch(`${http_protocol}//${host + config.path}/reset`, { - headers: { "Content-Type": "application/json" }, - method: "POST", - body: JSON.stringify({ fn_index, session_hash }) - }); - } catch (e) { - console.warn( - "The `/reset` endpoint could not be called. Subsequent endpoint results may be unreliable." - ); - } - } - - function destroy() { - for (const event_type in listener_map) { - listener_map[event_type as "data" | "status"].forEach((fn) => { - off(event_type as "data" | "status", fn); - }); - } - } - - return { - on, - off, - cancel, - destroy - }; - } - - async function view_api(config?: Config): Promise> { - if (api) return api; - - const headers: { - Authorization?: string; - "Content-Type": "application/json"; - } = { "Content-Type": "application/json" }; - if (hf_token) { - headers.Authorization = `Bearer ${hf_token}`; - } - let response: Response; - // @ts-ignore - if (semiver(config.version || "2.0.0", "3.30") < 0) { - response = await fetch( - "https://gradio-space-api-fetcher-v2.hf.space/api", - { - method: "POST", - body: JSON.stringify({ - serialize: false, - config: JSON.stringify(config) - }), - headers - } - ); - } else { - response = await fetch(`${config.root}/info`, { - headers - }); - } - - if (!response.ok) { - throw new Error(BROKEN_CONNECTION_MSG); - } - - let api_info = (await response.json()) as - | ApiInfo - | { api: ApiInfo }; - if ("api" in api_info) { - api_info = api_info.api; - } - - if ( - api_info.named_endpoints["/predict"] && - !api_info.unnamed_endpoints["0"] - ) { - api_info.unnamed_endpoints[0] = api_info.named_endpoints["/predict"]; - } - - const x = transform_api_info(api_info, config, api_map); - return x; - } - }); + return data; + }); + } } +export const { post_data, upload_files, client, handle_blob } = + api_factory(fetch); + function transform_output( data: any[], api_info: any, @@ -902,50 +974,6 @@ async function get_jwt( } } -export async function handle_blob( - endpoint: string, - data: unknown[], - api_info, - token?: `hf_${string}` -): Promise { - const blob_refs = await walk_and_store_blobs( - data, - undefined, - [], - true, - api_info - ); - - return Promise.all( - blob_refs.map(async ({ path, blob, data, type }) => { - if (blob) { - const file_url = (await upload_files(endpoint, [blob], token)).files[0]; - return { path, file_url, type }; - } else { - return { path, base64: data, type }; - } - }) - ).then((r) => { - r.forEach(({ path, file_url, base64, type }) => { - if (base64) { - update_object(data, base64, path); - } else if (type === "Gallery") { - update_object(data, file_url, path); - } else if (file_url) { - const o = { - is_file: true, - name: `${file_url}`, - data: null - // orig_name: "file.csv" - }; - update_object(data, o, path); - } - }); - - return data; - }); -} - function update_object(object, newValue, stack) { while (stack.length > 1) { object = object[stack.shift()]; @@ -1051,6 +1079,7 @@ function skip_queue(id: number, config: Config) { } async function resolve_config( + fetch_implementation: typeof fetch, endpoint?: string, token?: `hf_${string}` ): Promise { @@ -1068,7 +1097,9 @@ async function resolve_config( config.root = endpoint + config.root; return { ...config, path: path }; } else if (endpoint) { - let response = await fetch(`${endpoint}/config`, { headers }); + let response = await fetch_implementation(`${endpoint}/config`, { + headers + }); if (response.status === 200) { const config = await response.json(); diff --git a/client/js/src/index.ts b/client/js/src/index.ts index abb831ea4c..66cff25fc7 100644 --- a/client/js/src/index.ts +++ b/client/js/src/index.ts @@ -1,2 +1,8 @@ -export { client, post_data, upload_files, duplicate } from "./client.js"; +export { + client, + post_data, + upload_files, + duplicate, + api_factory +} from "./client.js"; export type { SpaceStatus } from "./types.js"; diff --git a/gradio/blocks.py b/gradio/blocks.py index 0e8bcfbf2d..bb101e88ac 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -32,6 +32,7 @@ from gradio import ( strings, themes, utils, + wasm_utils, ) from gradio.context import Context from gradio.deprecation import check_deprecated_parameters @@ -739,7 +740,7 @@ class Blocks(BlockContext): self.root_path = "" self.root_urls = set() - if self.analytics_enabled: + if not wasm_utils.IS_WASM and self.analytics_enabled: is_custom_theme = not any( self.theme.to_dict() == built_in_theme.to_dict() for built_in_theme in BUILT_IN_THEMES.values() @@ -1772,15 +1773,37 @@ Received outputs: "Rerunning server... use `close()` to stop if you need to change `launch()` parameters.\n----" ) else: - server_name, server_port, local_url, app, server = networking.start_server( - self, - server_name, - server_port, - ssl_keyfile, - ssl_certfile, - ssl_keyfile_password, - app_kwargs=app_kwargs, - ) + if wasm_utils.IS_WASM: + server_name = "xxx" + server_port = 99999 + local_url = "" + server = None + + # In the Wasm environment, we only need the app object + # which the frontend app will directly communicate with through the Worker API, + # and we don't need to start a server. + # So we just create the app object and register it here, + # and avoid using `networking.start_server` that would start a server that don't work in the Wasm env. + from gradio.routes import App + + app = App.create_app(self, app_kwargs=app_kwargs) + wasm_utils.register_app(app) + else: + ( + server_name, + server_port, + local_url, + app, + server, + ) = networking.start_server( + self, + server_name, + server_port, + ssl_keyfile, + ssl_certfile, + ssl_keyfile_password, + app_kwargs=app_kwargs, + ) self.server_name = server_name self.local_url = local_url self.server_port = server_port @@ -1802,7 +1825,11 @@ Received outputs: # Cannot run async functions in background other than app's scope. # Workaround by triggering the app endpoint - requests.get(f"{self.local_url}startup-events", verify=ssl_verify) + if not wasm_utils.IS_WASM: + requests.get(f"{self.local_url}startup-events", verify=ssl_verify) + + if wasm_utils.IS_WASM: + return TupleNoPrint((self.server_app, self.local_url, self.share_url)) utils.launch_counter() @@ -2037,7 +2064,8 @@ Received outputs: try: if self.enable_queue: self._queue.close() - self.server.close() + if self.server: + self.server.close() self.is_running = False # So that the startup events (starting the queue) # happen the next time the app is launched @@ -2056,7 +2084,8 @@ Received outputs: time.sleep(0.1) except (KeyboardInterrupt, OSError): print("Keyboard interruption in main thread... closing server.") - self.server.close() + if self.server: + self.server.close() for tunnel in CURRENT_TUNNELS: tunnel.kill() diff --git a/gradio/components/video.py b/gradio/components/video.py index 14752987bb..cf1b253594 100644 --- a/gradio/components/video.py +++ b/gradio/components/video.py @@ -7,16 +7,19 @@ import warnings from pathlib import Path from typing import Callable, Literal -from ffmpy import FFmpeg from gradio_client import utils as client_utils from gradio_client.data_classes import FileData from gradio_client.documentation import document, set_documentation_group from gradio_client.serializing import VideoSerializable -from gradio import processing_utils, utils +from gradio import processing_utils, utils, wasm_utils from gradio.components.base import IOComponent, _Keywords from gradio.events import Changeable, Clearable, Playable, Recordable, Uploadable +if not wasm_utils.IS_WASM: + # TODO: Support ffmpeg on Wasm + from ffmpy import FFmpeg + set_documentation_group("component") @@ -204,6 +207,10 @@ class Video( ) if Path(output_file_name).exists(): return output_file_name + if wasm_utils.IS_WASM: + raise wasm_utils.WasmUnsupportedError( + "Video formatting is not supported in the Wasm mode." + ) ff = FFmpeg( inputs={str(file_name): None}, outputs={output_file_name: output_options}, @@ -212,6 +219,10 @@ class Video( return output_file_name elif not self.include_audio: output_file_name = str(file_name.with_name(f"muted_{file_name.name}")) + if wasm_utils.IS_WASM: + raise wasm_utils.WasmUnsupportedError( + "include_audio=False is not supported in the Wasm mode." + ) ff = FFmpeg( inputs={str(file_name): None}, outputs={output_file_name: ["-an"]}, @@ -301,6 +312,10 @@ class Video( # selected format returned_format = video.split(".")[-1].lower() if self.format is not None and returned_format != self.format: + if wasm_utils.IS_WASM: + raise wasm_utils.WasmUnsupportedError( + "Returning a video in a different format is not supported in the Wasm mode." + ) output_file_name = video[0 : video.rindex(".") + 1] + self.format ff = FFmpeg( inputs={video: None}, diff --git a/gradio/processing_utils.py b/gradio/processing_utils.py index 7789f6b2fe..b77802d3f1 100644 --- a/gradio/processing_utils.py +++ b/gradio/processing_utils.py @@ -11,10 +11,15 @@ from io import BytesIO from pathlib import Path import numpy as np -from ffmpy import FFmpeg, FFprobe, FFRuntimeError from gradio_client import utils as client_utils from PIL import Image, ImageOps, PngImagePlugin +from gradio import wasm_utils + +if not wasm_utils.IS_WASM: + # TODO: Support ffmpeg on Wasm + from ffmpy import FFmpeg, FFprobe, FFRuntimeError + with warnings.catch_warnings(): warnings.simplefilter("ignore") # Ignore pydub warning if ffmpeg is not installed from pydub import AudioSegment @@ -478,6 +483,10 @@ def _convert(image, dtype, force_copy=False, uniform=False): def ffmpeg_installed() -> bool: + if wasm_utils.IS_WASM: + # TODO: Support ffmpeg in WASM + return False + return shutil.which("ffmpeg") is not None diff --git a/gradio/routes.py b/gradio/routes.py index 02f7071f81..a81a4c0fb0 100644 --- a/gradio/routes.py +++ b/gradio/routes.py @@ -42,7 +42,7 @@ from starlette.websockets import WebSocketState import gradio import gradio.ranged_response as ranged_response -from gradio import utils +from gradio import utils, wasm_utils from gradio.context import Context from gradio.data_classes import PredictBody, ResetBody from gradio.exceptions import Error @@ -167,16 +167,18 @@ class App(FastAPI): blocks: gradio.Blocks, app_kwargs: Dict[str, Any] | None = None ) -> App: app_kwargs = app_kwargs or {} - app_kwargs.setdefault("default_response_class", ORJSONResponse) + if not wasm_utils.IS_WASM: + app_kwargs.setdefault("default_response_class", ORJSONResponse) app = App(**app_kwargs) app.configure_app(blocks) - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], - ) + if not wasm_utils.IS_WASM: + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], + ) @app.get("/user") @app.get("/user/") diff --git a/gradio/strings.py b/gradio/strings.py index 4d82181476..2708532c5d 100644 --- a/gradio/strings.py +++ b/gradio/strings.py @@ -4,6 +4,8 @@ from typing import Dict import requests +from gradio import wasm_utils + MESSAGING_API_ENDPOINT = "https://api.gradio.app/gradio-messaging/en" en = { @@ -41,5 +43,5 @@ def get_updated_messaging(en: Dict): pass -if os.getenv("GRADIO_ANALYTICS_ENABLED", "True") == "True": +if os.getenv("GRADIO_ANALYTICS_ENABLED", "True") == "True" and not wasm_utils.IS_WASM: threading.Thread(target=get_updated_messaging, args=(en,)).start() diff --git a/gradio/wasm_utils.py b/gradio/wasm_utils.py new file mode 100644 index 0000000000..205892bdb3 --- /dev/null +++ b/gradio/wasm_utils.py @@ -0,0 +1,24 @@ +import sys + +# See https://pyodide.org/en/stable/usage/faq.html#how-to-detect-that-code-is-run-with-pyodide +IS_WASM = sys.platform == "emscripten" + + +class WasmUnsupportedError(Exception): + pass + + +app = None + + +# `register_app` and `get_registered_app` are used +# for the Wasm worker to get a reference to +# the Gradio's FastAPI app instance (`app`). +def register_app(_app): + global app + app = _app + + +def get_registered_app(): + global app + return app diff --git a/js/app/lite.html b/js/app/lite.html new file mode 100644 index 0000000000..e048897a0a --- /dev/null +++ b/js/app/lite.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + +
+ + diff --git a/js/app/package.json b/js/app/package.json index 88ab2d7db2..f5107132a8 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -5,9 +5,14 @@ "type": "module", "scripts": { "dev": "vite --port 9876", + "dev:lite": "vite --port 9876 --mode development:lite", "build:cdn": "vite build --mode production:cdn --emptyOutDir", "build:website": "vite build --mode production:website --emptyOutDir", "build:local": "vite build --mode production:local --emptyOutDir", + "pybuild:gradio": "cd ../../ && rm -rf gradio/templates/frontend && python3 -m build", + "pybuild:gradio-client": "cd ../../client/python && python3 -m build", + "pybuild": "run-p pybuild:*", + "build:lite": "pnpm pybuild && vite build --mode production:lite --emptyOutDir", "preview": "vite preview", "test:snapshot": "pnpm exec playwright test snapshots/ --config=../../.config/playwright.config.js", "test:browser": "pnpm exec playwright test test/ --config=../../.config/playwright.config.js", @@ -42,6 +47,7 @@ "@gradio/upload-button": "workspace:^0.0.1", "@gradio/utils": "workspace:^0.0.1", "@gradio/video": "workspace:^0.0.1", + "@gradio/wasm": "workspace:^0.0.1", "@playwright/test": "^1.35.1", "d3-dsv": "^3.0.1", "mime-types": "^2.1.34", diff --git a/js/app/src/Index.svelte b/js/app/src/Index.svelte index 2539e9cc39..fe894bcd45 100644 --- a/js/app/src/Index.svelte +++ b/js/app/src/Index.svelte @@ -1,6 +1,6 @@ + + + + + + + + +
+ + + + diff --git a/js/wasm/package.json b/js/wasm/package.json new file mode 100644 index 0000000000..3be6357ee9 --- /dev/null +++ b/js/wasm/package.json @@ -0,0 +1,22 @@ +{ + "name": "@gradio/wasm", + "version": "0.0.1", + "description": "Gradio Wasm package", + "type": "module", + "main": "dist/index.js", + "private": true, + "keywords": [], + "author": "", + "license": "ISC", + "scripts": { + "start:client": "tsc -w --incremental", + "start:worker": "vite build --config vite.worker.config.js --watch --emptyOutDir=false", + "start": "run-p start:*", + "build:client": "tsc", + "build:worker": "vite build --config vite.worker.config.js", + "build": "run-s build:worker build:client" + }, + "devDependencies": { + "pyodide": "^0.23.2" + } +} diff --git a/js/wasm/src/index.ts b/js/wasm/src/index.ts new file mode 100644 index 0000000000..93721a825d --- /dev/null +++ b/js/wasm/src/index.ts @@ -0,0 +1 @@ +export { WorkerProxy } from "./worker-proxy"; diff --git a/js/wasm/src/message-types.ts b/js/wasm/src/message-types.ts new file mode 100644 index 0000000000..89492b77ea --- /dev/null +++ b/js/wasm/src/message-types.ts @@ -0,0 +1,62 @@ +export interface HttpRequest { + method: "GET" | "POST" | "PUT" | "DELETE"; + path: string; + query_string: string; + headers: Record; + body?: Uint8Array; +} + +export interface HttpResponse { + status: number; + headers: Record; + body: Uint8Array; +} + +export interface InMessageBase { + type: string; + data: unknown; +} + +export interface InMessageInit extends InMessageBase { + type: "init"; + data: { + gradioWheelUrl: string; + gradioClientWheelUrl: string; + requirements: string[]; + }; +} +export interface InMessageRunPython extends InMessageBase { + type: "run-python"; + data: { + code: string; + }; +} +export interface InMessageHttpRequest extends InMessageBase { + type: "http-request"; + data: { + request: HttpRequest; + }; +} + +export interface InMessageEcho extends InMessageBase { + // For debug + type: "echo"; + data: unknown; +} + +export type InMessage = + | InMessageInit + | InMessageRunPython + | InMessageHttpRequest + | InMessageEcho; + +export interface ReplyMessageSuccess { + type: "reply:success"; + data: T; +} +export interface ReplyMessageError { + type: "reply:error"; + error: Error; +} + +export type ReplyMessage = ReplyMessageSuccess | ReplyMessageError; diff --git a/js/wasm/src/webworker/declarations.d.ts b/js/wasm/src/webworker/declarations.d.ts new file mode 100644 index 0000000000..6c8c2da78a --- /dev/null +++ b/js/wasm/src/webworker/declarations.d.ts @@ -0,0 +1,2 @@ +// Declarations for the WebWorker files where some variables are dynamically loaded through importScript. +declare let loadPyodide: any; diff --git a/js/wasm/src/webworker/http.ts b/js/wasm/src/webworker/http.ts new file mode 100644 index 0000000000..3ec908ff46 --- /dev/null +++ b/js/wasm/src/webworker/http.ts @@ -0,0 +1,135 @@ +import type { PyProxy } from "pyodide/ffi"; +import type { HttpRequest, HttpResponse } from "../message-types"; + +// Inspired by https://github.com/rstudio/shinylive/blob/v0.1.2/src/messageporthttp.ts + +// A reference to an ASGI application instance in Python +// Ref: https://asgi.readthedocs.io/en/latest/specs/main.html#applications +type ASGIApplication = ( + scope: Record, + receive: () => Promise, + send: (event: PyProxy) => Promise +) => Promise; + +type ReceiveEvent = RequestReceiveEvent | DisconnectReceiveEvent; +// https://asgi.readthedocs.io/en/latest/specs/www.html#request-receive-event +interface RequestReceiveEvent { + type: "http.request"; + body?: Uint8Array; // `bytes` in Python + more_body: boolean; +} +// https://asgi.readthedocs.io/en/latest/specs/www.html#disconnect-receive-event +interface DisconnectReceiveEvent { + type: "http.disconnect"; +} + +type SendEvent = ResponseStartSendEvent | ResponseBodySendEvent; +// https://asgi.readthedocs.io/en/latest/specs/www.html#response-start-send-event +interface ResponseStartSendEvent { + type: "http.response.start"; + status: number; + headers: Iterable<[Uint8Array, Uint8Array]>; + trailers: boolean; +} +// https://asgi.readthedocs.io/en/latest/specs/www.html#response-body-send-event +interface ResponseBodySendEvent { + type: "http.response.body"; + body: Uint8Array; // `bytes` in Python + more_body: boolean; +} + +function headersToASGI( + headers: HttpRequest["headers"] +): Array<[string, string]> { + const result: Array<[string, string]> = []; + for (const [key, value] of Object.entries(headers)) { + result.push([key, value]); + } + return result; +} + +export function uint8ArrayToString(buf: Uint8Array): string { + let result = ""; + for (let i = 0; i < buf.length; i++) { + result += String.fromCharCode(buf[i]); + } + return result; +} + +function asgiHeadersToRecord(headers: any): Record { + headers = headers.map(([key, val]: [Uint8Array, Uint8Array]) => { + return [uint8ArrayToString(key), uint8ArrayToString(val)]; + }); + return Object.fromEntries(headers); +} + +export const makeHttpRequest = ( + asgiApp: ASGIApplication, + request: HttpRequest +): Promise => + new Promise((resolve, reject) => { + let sent = false; + async function receiveFromJs(): Promise { + if (sent) { + // NOTE: I implemented this block just referring to the spec. However, it is not reached in practice so it's not combat-proven. + return { + type: "http.disconnect" + }; + } + + const event: RequestReceiveEvent = { + type: "http.request", + more_body: false + }; + if (request.body) { + event.body = request.body; + } + + console.debug("receive", event); + sent = true; + return event; + } + + let status: number; + let headers: { [key: string]: string }; + let body: Uint8Array = new Uint8Array(); + async function sendToJs(proxiedEvent: PyProxy): Promise { + const event = Object.fromEntries(proxiedEvent.toJs()) as SendEvent; + console.debug("send", event); + if (event.type === "http.response.start") { + status = event.status; + headers = asgiHeadersToRecord(event.headers); + } else if (event.type === "http.response.body") { + body = new Uint8Array([...body, ...event.body]); + if (!event.more_body) { + const response: HttpResponse = { + status, + headers, + body + }; + console.debug("HTTP response", response); + resolve(response); + } + } else { + throw new Error(`Unhandled ASGI event: ${JSON.stringify(event)}`); + } + } + + // https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope + const scope = { + type: "http", + asgi: { + version: "3.0", + spec_version: "2.1" + }, + http_version: "1.1", + scheme: "http", + method: request.method, + path: request.path, + query_string: request.query_string, + root_path: "", + headers: headersToASGI(request.headers) + }; + + asgiApp(scope, receiveFromJs, sendToJs); + }); diff --git a/js/wasm/src/webworker/index.ts b/js/wasm/src/webworker/index.ts new file mode 100644 index 0000000000..f5dc101545 --- /dev/null +++ b/js/wasm/src/webworker/index.ts @@ -0,0 +1,205 @@ +/// + +import type { PyodideInterface } from "pyodide"; +import type { + InMessage, + ReplyMessageError, + ReplyMessageSuccess +} from "../message-types"; +import { makeHttpRequest } from "./http"; + +importScripts("https://cdn.jsdelivr.net/pyodide/v0.23.2/full/pyodide.js"); + +let pyodide: PyodideInterface; + +let pyodideReadyPromise: undefined | Promise = undefined; + +let call_asgi_app_from_js: ( + scope: unknown, + receive: Function, + send: Function +) => Promise; + +interface InitOptions { + gradioWheelUrl: string; + gradioClientWheelUrl: string; + requirements: string[]; +} +async function loadPyodideAndPackages(options: InitOptions) { + console.debug("Loading Pyodide."); + pyodide = await loadPyodide({ + stdout: console.log, + stderr: console.error + }); + console.debug("Pyodide is loaded."); + + console.debug("Loading micropip"); + await pyodide.loadPackage("micropip"); + const micropip = pyodide.pyimport("micropip"); + console.debug("micropip is loaded."); + + const gradioWheelUrls = [ + options.gradioWheelUrl, + options.gradioClientWheelUrl + ]; + console.debug("Loading Gradio wheels.", gradioWheelUrls); + await micropip.add_mock_package("ffmpy", "0.3.0"); + await micropip.add_mock_package("orjson", "3.8.12"); + await micropip.add_mock_package("aiohttp", "3.8.4"); + await micropip.add_mock_package("multidict", "4.7.6"); + await pyodide.loadPackage(["ssl", "distutils", "setuptools"]); + await micropip.install(["markdown-it-py~=2.2.0"]); // On 3rd June 2023, markdown-it-py 3.0.0 has been released. The `gradio` package depends on its `>=2.0.0` version so its 3.x will be resolved. However, it conflicts with `mdit-py-plugins`'s dependency `markdown-it-py >=1.0.0,<3.0.0` and micropip currently can't resolve it. So we explicitly install the compatible version of the library here. + await micropip.install.callKwargs(gradioWheelUrls, { + keep_going: true + }); + console.debug("Gradio wheels are loaded."); + + console.debug("Install packages.", options.requirements); + await micropip.install.callKwargs(options.requirements, { keep_going: true }); + console.debug("Packages are installed."); + + console.debug("Mock os module methods."); + // `os.link` is used in `aiofiles` (https://github.com/Tinche/aiofiles/blob/v23.1.0/src/aiofiles/os.py#L31), + // which is imported from `gradio.ranged_response` (https://github.com/gradio-app/gradio/blob/v3.32.0/gradio/ranged_response.py#L12). + // However, it's not available on Wasm. + await pyodide.runPythonAsync(` +import os + +os.link = lambda src, dst: None +`); + console.debug("os module methods are mocked."); + + console.debug("Import gradio package."); + // Importing the gradio package takes a long time, so we do it separately. + // This is necessary for accurate performance profiling. + await pyodide.runPythonAsync(`import gradio`); + console.debug("gradio package is imported."); + + console.debug("Define a ASGI wrapper function."); + // TODO: Unlike Streamlit, user's code is executed in the global scope, + // so we should not define this function in the global scope. + await pyodide.runPythonAsync(` +# Based on Shiny's App.call_pyodide(). +# https://github.com/rstudio/py-shiny/blob/v0.3.3/shiny/_app.py#L224-L258 +async def _call_asgi_app_from_js(scope, receive, send): + # TODO: Pretty sure there are objects that need to be destroy()'d here? + scope = scope.to_py() + + # ASGI requires some values to be byte strings, not character strings. Those are + # not that easy to create in JavaScript, so we let the JS side pass us strings + # and we convert them to bytes here. + if "headers" in scope: + # JS doesn't have \`bytes\` so we pass as strings and convert here + scope["headers"] = [ + [value.encode("latin-1") for value in header] + for header in scope["headers"] + ] + if "query_string" in scope and scope["query_string"]: + scope["query_string"] = scope["query_string"].encode("latin-1") + if "raw_path" in scope and scope["raw_path"]: + scope["raw_path"] = scope["raw_path"].encode("latin-1") + + async def rcv(): + event = await receive() + return event.to_py() + + async def snd(event): + await send(event) + + app = gradio.wasm_utils.get_registered_app() + if app is None: + raise RuntimeError("Gradio app has not been launched.") + + await app(scope, rcv, snd) +`); + call_asgi_app_from_js = pyodide.globals.get("_call_asgi_app_from_js"); + console.debug("The ASGI wrapper function is defined."); + + console.debug("Mock async libraries."); + // FastAPI uses `anyio.to_thread.run_sync` internally which, however, doesn't work in Wasm environments where the `threading` module is not supported. + // So we mock `anyio.to_thread.run_sync` here not to use threads. + await pyodide.runPythonAsync(` +async def mocked_anyio_to_thread_run_sync(func, *args, cancellable=False, limiter=None): + return func(*args) + +import anyio.to_thread +anyio.to_thread.run_sync = mocked_anyio_to_thread_run_sync + `); + console.debug("Async libraries are mocked."); + + console.debug("Set matplotlib backend."); + // Ref: https://github.com/streamlit/streamlit/blob/1.22.0/lib/streamlit/web/bootstrap.py#L111 + // This backend setting is required to use matplotlib in Wasm environment. + await pyodide.runPythonAsync(` +import matplotlib +matplotlib.use("agg") +`); + console.debug("matplotlib backend is set."); +} + +self.onmessage = async (event: MessageEvent) => { + const msg = event.data; + console.debug("worker.onmessage", msg); + + const messagePort = event.ports[0]; + + try { + if (msg.type === "init") { + pyodideReadyPromise = loadPyodideAndPackages({ + gradioWheelUrl: msg.data.gradioWheelUrl, + gradioClientWheelUrl: msg.data.gradioClientWheelUrl, + requirements: msg.data.requirements + }); + + const replyMessage: ReplyMessageSuccess = { + type: "reply:success", + data: null + }; + messagePort.postMessage(replyMessage); + } + + if (pyodideReadyPromise == null) { + throw new Error("Pyodide Initialization is not started."); + } + + await pyodideReadyPromise; + + switch (msg.type) { + case "echo": { + const replyMessage: ReplyMessageSuccess = { + type: "reply:success", + data: msg.data + }; + messagePort.postMessage(replyMessage); + break; + } + case "run-python": { + await pyodide.runPythonAsync(msg.data.code); + const replyMessage: ReplyMessageSuccess = { + type: "reply:success", + data: null // We don't send back the execution result because it's not needed for our purpose, and sometimes the result is of type `pyodide.ffi.PyProxy` which cannot be cloned across threads and causes an error. + }; + messagePort.postMessage(replyMessage); + break; + } + case "http-request": { + const request = msg.data.request; + const response = await makeHttpRequest(call_asgi_app_from_js, request); + const replyMessage: ReplyMessageSuccess = { + type: "reply:success", + data: { + response + } + }; + messagePort.postMessage(replyMessage); + break; + } + } + } catch (error) { + const replyMessage: ReplyMessageError = { + type: "reply:error", + error: error as Error + }; + messagePort.postMessage(replyMessage); + } +}; diff --git a/js/wasm/src/worker-proxy.ts b/js/wasm/src/worker-proxy.ts new file mode 100644 index 0000000000..23aa960f53 --- /dev/null +++ b/js/wasm/src/worker-proxy.ts @@ -0,0 +1,104 @@ +import type { + HttpRequest, + HttpResponse, + InMessage, + ReplyMessage +} from "./message-types"; + +export interface WorkerProxyOptions { + gradioWheelUrl: string; + gradioClientWheelUrl: string; + requirements: string[]; +} + +export class WorkerProxy { + private worker: Worker; + + constructor(options: WorkerProxyOptions) { + console.debug("WorkerProxy.constructor(): Create a new worker."); + // Loading a worker here relies on Vite's support for WebWorkers (https://vitejs.dev/guide/features.html#web-workers), + // assuming that this module is imported from the Gradio frontend (`@gradio/app`), which is bundled with Vite. + this.worker = new Worker(new URL("./webworker.js", import.meta.url)); + + this.postMessageAsync({ + type: "init", + data: { + gradioWheelUrl: options.gradioWheelUrl, + gradioClientWheelUrl: options.gradioClientWheelUrl, + requirements: options.requirements + } + }).then(() => { + console.debug("WorkerProxy.constructor(): Initialization is done."); + }); + } + + public async runPythonAsync(code: string): Promise { + await this.postMessageAsync({ + type: "run-python", + data: { + code + } + }); + } + + // A wrapper for this.worker.postMessage(). Unlike that function, which + // returns void immediately, this function returns a promise, which resolves + // when a ReplyMessage is received from the worker. + // The original implementation is in https://github.com/rstudio/shinylive/blob/v0.1.2/src/pyodide-proxy.ts#L404-L418 + private postMessageAsync(msg: InMessage): Promise { + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + + channel.port1.onmessage = (e) => { + channel.port1.close(); + const msg = e.data as ReplyMessage; + if (msg.type === "reply:error") { + reject(msg.error); + return; + } + + resolve(msg.data); + }; + + this.worker.postMessage(msg, [channel.port2]); + }); + } + + public async httpRequest(request: HttpRequest): Promise { + console.debug("WorkerProxy.httpRequest()", request); + const result = await this.postMessageAsync({ + type: "http-request", + data: { + request + } + }); + const response = (result as { response: HttpResponse }).response; + + if (Math.floor(response.status / 100) !== 2) { + let bodyText: string; + let bodyJson: unknown; + try { + bodyText = new TextDecoder().decode(response.body); + } catch (e) { + bodyText = "(failed to decode body)"; + } + try { + bodyJson = JSON.parse(bodyText); + } catch (e) { + bodyJson = "(failed to parse body as JSON)"; + } + console.error("Wasm HTTP error", { + request, + response, + bodyText, + bodyJson + }); + } + + return response; + } + + public terminate(): void { + this.worker.terminate(); + } +} diff --git a/js/wasm/tsconfig.json b/js/wasm/tsconfig.json new file mode 100644 index 0000000000..06207186c8 --- /dev/null +++ b/js/wasm/tsconfig.json @@ -0,0 +1,105 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + "declarationMap": true /* Create sourcemaps for d.ts files. */, + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"], + "exclude": ["src/webworker/**/*"] // The worker code is bundled by Vite separately. See its config file. +} diff --git a/js/wasm/vite.worker.config.js b/js/wasm/vite.worker.config.js new file mode 100644 index 0000000000..3d26320fc0 --- /dev/null +++ b/js/wasm/vite.worker.config.js @@ -0,0 +1,34 @@ +import path from "path"; +import { defineConfig } from "vite"; + +/** + * We bundle the worker file before packaging, while other files are only TS-transpiled. + * The consumer of this package, `@gradio/app`, will be bundled with Vite, + * and Vite only supports module-type WebWorkers (`new Worker("...", { type: "module" })`) to handle `import` in the worker file, + * because in the dev mode it doesn't bundle the worker file and just relies on the browser's native support for module-type workers to resolve the imports. + * However, we need to use `importScripts()` in the worker to load Pyodide from the CDN, which is only supported by classic WebWorkers (`new Worker("...")`), + * while we still want to use `import` in the worker to modularize the code. + * So, we bundle the worker file to resolve `import`s here before exporting, preserving `importScripts()` in the bundled file, + * and load the bundled worker file on `@gradio/app` as a classic WebWorker. + * + * Note: We tried the following approaches, but they failed: + * 1. Just TS-transpile the worker file like other files into `worker.js`, and use it like `new Worker("worker.js")`. + * It failed because `tsc` reserves `importScripts()` and also appends `export {};` to the end of the file to specify it as a module (`https://github.com/microsoft/TypeScript/issues/41513`), + * however, `importScripts()` is only supported by classic WebWorkers, and `export {};` is not supported by classic WebWorkers. + * 2. Use ESM import instead of `importScripts()`, which is (experimentally?) supported by Pyodide since v0.20.0 (https://pyodide.org/en/stable/project/changelog.html#javascript-package), + * using `import { loadPyodide } from "https://cdn.jsdelivr.net/pyodide/v0.23.2/full/pyodide.js";` in the worker file, instead of `importScripts(...)`. + * It was successful in the dev mode, but failed in the prod mode, which has this problem: https://github.com/pyodide/pyodide/issues/2217#issuecomment-1328344562. + */ + +export default defineConfig({ + build: { + outDir: "dist", + rollupOptions: { + input: path.join(__dirname, "src/webworker/index.ts"), + // Ref: https://github.com/rollup/rollup/issues/2616#issuecomment-1431551704 + output: { + entryFileNames: "webworker.js" + } + } + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab573e1074..f3970e2f02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,10 +46,10 @@ importers: version: 20.3.1 '@typescript-eslint/eslint-plugin': specifier: ^5.60.0 - version: 5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.1.3) + version: 5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.0.4) '@typescript-eslint/parser': specifier: ^5.60.0 - version: 5.60.0(eslint@8.43.0)(typescript@5.1.3) + version: 5.60.0(eslint@8.43.0)(typescript@5.0.4) autoprefixer: specifier: ^10.4.4 version: 10.4.4(postcss@8.4.6) @@ -76,7 +76,7 @@ importers: version: 4.1.5 msw: specifier: ^1.0.0 - version: 1.0.0(typescript@5.1.3) + version: 1.0.0(typescript@5.0.4) node-html-parser: specifier: ^6.0.0 version: 6.0.0 @@ -133,16 +133,16 @@ importers: version: 3.6.0(svelte@3.59.1) svelte-preprocess: specifier: ^5.0.3 - version: 5.0.3(postcss@8.4.6)(svelte@3.59.1)(typescript@5.1.3) + version: 5.0.3(postcss@8.4.6)(svelte@3.59.1)(typescript@5.0.4) tailwindcss: specifier: ^3.1.6 version: 3.1.6(postcss@8.4.6) tinyspy: specifier: ^2.0.0 - version: 2.1.1 + version: 2.0.0 typescript: specifier: ^5.0.0 - version: 5.1.3 + version: 5.0.4 vite: specifier: ^4.3.9 version: 4.3.9(@types/node@20.3.1) @@ -301,6 +301,9 @@ importers: '@gradio/video': specifier: workspace:^0.0.1 version: link:../video + '@gradio/wasm': + specifier: workspace:^0.0.1 + version: link:../wasm '@playwright/test': specifier: ^1.35.1 version: 1.35.1 @@ -723,6 +726,12 @@ importers: specifier: workspace:^0.0.1 version: link:../upload + js/wasm: + devDependencies: + pyodide: + specifier: ^0.23.2 + version: 0.23.2 + packages: /@adobe/css-tools@4.2.0: @@ -1548,13 +1557,13 @@ packages: resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==} dependencies: '@formatjs/intl-localematcher': 0.2.25 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /@formatjs/fast-memoize@1.2.1: resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==} dependencies: - tslib: 2.5.3 + tslib: 2.4.0 dev: false /@formatjs/icu-messageformat-parser@2.1.0: @@ -1562,20 +1571,20 @@ packages: dependencies: '@formatjs/ecma402-abstract': 1.11.4 '@formatjs/icu-skeleton-parser': 1.3.6 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /@formatjs/icu-skeleton-parser@1.3.6: resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==} dependencies: '@formatjs/ecma402-abstract': 1.11.4 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /@formatjs/intl-localematcher@0.2.25: resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==} dependencies: - tslib: 2.5.3 + tslib: 2.4.0 dev: false /@humanwhocodes/config-array@0.11.10: @@ -2184,7 +2193,7 @@ packages: '@types/yargs-parser': 21.0.0 dev: false - /@typescript-eslint/eslint-plugin@5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/eslint-plugin@5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.0.4): resolution: {integrity: sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2196,23 +2205,23 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.60.0(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.60.0(eslint@8.43.0)(typescript@5.0.4) '@typescript-eslint/scope-manager': 5.60.0 - '@typescript-eslint/type-utils': 5.60.0(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/utils': 5.60.0(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/type-utils': 5.60.0(eslint@8.43.0)(typescript@5.0.4) + '@typescript-eslint/utils': 5.60.0(eslint@8.43.0)(typescript@5.0.4) debug: 4.3.4 eslint: 8.43.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.0.4) + typescript: 5.0.4 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/parser@5.60.0(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/parser@5.60.0(eslint@8.43.0)(typescript@5.0.4): resolution: {integrity: sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2224,10 +2233,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.60.0 '@typescript-eslint/types': 5.60.0 - '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.0.4) debug: 4.3.4 eslint: 8.43.0 - typescript: 5.1.3 + typescript: 5.0.4 transitivePeerDependencies: - supports-color dev: false @@ -2240,7 +2249,7 @@ packages: '@typescript-eslint/visitor-keys': 5.60.0 dev: false - /@typescript-eslint/type-utils@5.60.0(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/type-utils@5.60.0(eslint@8.43.0)(typescript@5.0.4): resolution: {integrity: sha512-X7NsRQddORMYRFH7FWo6sA9Y/zbJ8s1x1RIAtnlj6YprbToTiQnM6vxcMu7iYhdunmoC0rUWlca13D5DVHkK2g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2250,12 +2259,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.1.3) - '@typescript-eslint/utils': 5.60.0(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.0.4) + '@typescript-eslint/utils': 5.60.0(eslint@8.43.0)(typescript@5.0.4) debug: 4.3.4 eslint: 8.43.0 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.0.4) + typescript: 5.0.4 transitivePeerDependencies: - supports-color dev: false @@ -2265,7 +2274,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false - /@typescript-eslint/typescript-estree@5.60.0(typescript@5.1.3): + /@typescript-eslint/typescript-estree@5.60.0(typescript@5.0.4): resolution: {integrity: sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2280,13 +2289,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.0.4) + typescript: 5.0.4 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/utils@5.60.0(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/utils@5.60.0(eslint@8.43.0)(typescript@5.0.4): resolution: {integrity: sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2297,7 +2306,7 @@ packages: '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 5.60.0 '@typescript-eslint/types': 5.60.0 - '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.0.4) eslint: 8.43.0 eslint-scope: 5.1.1 semver: 7.3.8 @@ -2534,7 +2543,7 @@ packages: engines: {node: '>=12.20.1'} dependencies: '@babel/runtime': 7.21.0 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /autoprefixer@10.4.4(postcss@8.4.6): @@ -2592,6 +2601,10 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + dev: true + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false @@ -2646,7 +2659,7 @@ packages: dependencies: '@babel/runtime': 7.21.0 fast-unique-numbers: 6.0.21 - tslib: 2.5.3 + tslib: 2.4.0 worker-factory: 6.0.69 dev: false @@ -2678,7 +2691,6 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.6.0 - dev: false /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} @@ -2955,7 +2967,7 @@ packages: '@babel/runtime': 7.21.0 dashify: 2.0.0 indefinite-article: 0.0.2 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /concat-map@0.0.1: @@ -2971,7 +2983,7 @@ packages: js-string-escape: 1.0.1 lodash: 4.17.21 md5-hex: 3.0.1 - semver: 7.4.0 + semver: 7.3.8 well-known-symbols: 2.0.0 dev: false @@ -3938,7 +3950,7 @@ packages: engines: {node: '>=12.20.1'} dependencies: '@babel/runtime': 7.21.0 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /fastq@1.13.0: @@ -4131,7 +4143,7 @@ packages: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.0.4 once: 1.4.0 path-is-absolute: 1.0.1 @@ -4394,7 +4406,7 @@ packages: '@formatjs/ecma402-abstract': 1.11.4 '@formatjs/fast-memoize': 1.2.1 '@formatjs/icu-messageformat-parser': 2.1.0 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /is-arguments@1.1.1: @@ -4717,7 +4729,7 @@ packages: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 12.0.1 - ws: 8.13.0 + ws: 8.13.0(bufferutil@4.0.7) xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -4950,7 +4962,7 @@ packages: broker-factory: 3.0.68 fast-unique-numbers: 6.0.21 media-encoder-host-worker: 9.0.70 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /media-encoder-host-worker@9.0.70: @@ -4958,7 +4970,7 @@ packages: dependencies: '@babel/runtime': 7.21.0 extendable-media-recorder-wav-encoder-broker: 7.0.70 - tslib: 2.5.3 + tslib: 2.4.0 worker-factory: 6.0.69 dev: false @@ -4968,7 +4980,7 @@ packages: '@babel/runtime': 7.21.0 media-encoder-host-broker: 7.0.70 media-encoder-host-worker: 9.0.70 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /memoizee@0.4.15: @@ -5053,12 +5065,12 @@ packages: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: brace-expansion: 1.1.11 - dev: false /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + dev: false /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} @@ -5103,7 +5115,7 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - /msw@1.0.0(typescript@5.1.3): + /msw@1.0.0(typescript@5.0.4): resolution: {integrity: sha512-8QVa1RAN/Nzbn/tKmtimJ+b2M1QZOMdETQW7/1TmBOZ4w+wJojfxuh1Hj5J4FYdBgZWW/TK4CABUOlOM4OjTOA==} engines: {node: '>=14'} hasBin: true @@ -5132,7 +5144,7 @@ packages: path-to-regexp: 6.2.1 strict-event-emitter: 0.4.6 type-fest: 2.19.0 - typescript: 5.1.3 + typescript: 5.0.4 yargs: 17.6.2 transitivePeerDependencies: - encoding @@ -5144,7 +5156,7 @@ packages: engines: {node: '>=12.20.1'} dependencies: '@babel/runtime': 7.21.0 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /mute-stream@0.0.8: @@ -5193,12 +5205,10 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 - dev: false /node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true - dev: false /node-html-parser@6.0.0: resolution: {integrity: sha512-o4vS5Jm7ZdV5WN4/jHmCEVJOpm4exLCeXOcZnNzXi0BGv0AS8FsGwyQ4k0Ujmui1NMQs6qsTy+amjjpr9rmz4Q==} @@ -5848,6 +5858,18 @@ packages: engines: {node: '>=6'} dev: false + /pyodide@0.23.2: + resolution: {integrity: sha512-GK4YDZVgzfAXK/7X0IiCI+142k0Ah/HwYTzDHtG8zC47dflWYuPozeFbOngShuL1M9Un5sCmHFqiH3boxJv0pQ==} + dependencies: + base-64: 1.0.0 + node-fetch: 2.6.7 + ws: 8.13.0(bufferutil@4.0.7) + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + dev: true + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -5936,7 +5958,7 @@ packages: resolution: {integrity: sha512-5QTJKukH8JcQR1f2FqZsQ1QD2aoc6/+tM0WPv8sqEI4THzbiMfH4VuWF3BfdL2F9mRjLo81nFC9OShCj87wMhg==} dependencies: '@babel/runtime': 7.21.0 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /recorder-audio-worklet@5.1.29: @@ -5948,7 +5970,7 @@ packages: recorder-audio-worklet-processor: 4.2.15 standardized-audio-context: 25.3.32 subscribable-things: 2.1.7 - tslib: 2.5.3 + tslib: 2.4.0 worker-factory: 6.0.69 dev: false @@ -6074,7 +6096,7 @@ packages: /rxjs@7.8.0: resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} dependencies: - tslib: 2.5.3 + tslib: 2.4.0 dev: false /sade@1.8.1: @@ -6299,7 +6321,7 @@ packages: dependencies: '@babel/runtime': 7.21.0 automation-events: 4.0.21 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /std-env@3.3.2: @@ -6428,7 +6450,7 @@ packages: dependencies: '@babel/runtime': 7.21.0 rxjs-interop: 2.0.0 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /supports-color@5.5.0: @@ -6651,7 +6673,7 @@ packages: typescript: 4.9.5 dev: false - /svelte-preprocess@5.0.3(postcss@8.4.6)(svelte@3.59.1)(typescript@5.1.3): + /svelte-preprocess@5.0.3(postcss@8.4.6)(svelte@3.59.1)(typescript@5.0.4): resolution: {integrity: sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==} engines: {node: '>= 14.10.0'} requiresBuild: true @@ -6696,7 +6718,7 @@ packages: sorcery: 0.11.0 strip-indent: 3.0.0 svelte: 3.59.1 - typescript: 5.1.3 + typescript: 5.0.4 dev: false /svelte-range-slider-pips@2.0.2: @@ -6813,6 +6835,11 @@ packages: engines: {node: '>=14.0.0'} dev: false + /tinyspy@2.0.0: + resolution: {integrity: sha512-B9wP6IgqmgNTDffygA716cr+PrW51LZc22qFs7+Aur0t73gqf3vNwwlwdcnE1AcqusK6V4R4+5jQ69nIQDiJiw==} + engines: {node: '>=14.0.0'} + dev: false + /tinyspy@2.1.1: resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} engines: {node: '>=14.0.0'} @@ -6854,7 +6881,6 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false /tr46@4.1.1: resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} @@ -6887,14 +6913,14 @@ packages: resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} dev: false - /tsutils@3.21.0(typescript@5.1.3): + /tsutils@3.21.0(typescript@5.0.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.1.3 + typescript: 5.0.4 dev: false /tty-table@4.2.1: @@ -6978,13 +7004,6 @@ packages: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} hasBin: true - dev: true - - /typescript@5.1.3: - resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} - engines: {node: '>=14.17'} - hasBin: true - dev: false /ufo@1.1.1: resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} @@ -7068,7 +7087,7 @@ packages: dependencies: d3-array: 3.1.1 vega-dataflow: 5.7.4 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7078,7 +7097,7 @@ packages: dependencies: vega-format: 1.1.0 vega-loader: 4.5.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7110,7 +7129,7 @@ packages: d3-interpolate: 3.0.1 vega-dataflow: 5.7.4 vega-scale: 7.2.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7123,7 +7142,7 @@ packages: resolution: {integrity: sha512-y5+c2frq0tGwJ7vYXzZcfVcIRF/QGfhf2e+bV1Z0iQs+M2lI1II1GPDdmOcMKimpoCVp/D61KUJDIGE1DSmk2w==} dependencies: '@types/estree': 0.0.50 - vega-util: 1.17.2 + vega-util: 1.17.0 dev: false /vega-force@4.1.0: @@ -7131,7 +7150,7 @@ packages: dependencies: d3-force: 3.0.0 vega-dataflow: 5.7.4 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7143,7 +7162,7 @@ packages: d3-format: 3.1.0 d3-time-format: 4.1.0 vega-time: 2.1.0 - vega-util: 1.17.2 + vega-util: 1.17.0 dev: false /vega-functions@5.13.0: @@ -7159,7 +7178,7 @@ packages: vega-selections: 5.4.0 vega-statistics: 1.8.0 vega-time: 2.1.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7174,7 +7193,7 @@ packages: vega-dataflow: 5.7.4 vega-projection: 1.5.0 vega-statistics: 1.8.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7184,7 +7203,7 @@ packages: dependencies: d3-hierarchy: 3.1.2 vega-dataflow: 5.7.4 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7199,7 +7218,7 @@ packages: vega-canvas: 1.2.6 vega-dataflow: 5.7.4 vega-scenegraph: 4.10.1 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7231,7 +7250,7 @@ packages: node-fetch: 2.6.7 topojson-client: 3.1.0 vega-format: 1.1.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7243,7 +7262,7 @@ packages: vega-event-selector: 3.0.0 vega-functions: 5.13.0 vega-scale: 7.2.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7261,7 +7280,7 @@ packages: d3-array: 3.1.1 vega-dataflow: 5.7.4 vega-statistics: 1.8.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7270,7 +7289,7 @@ packages: resolution: {integrity: sha512-gE+sO2IfxMUpV0RkFeQVnHdmPy3K7LjHakISZgUGsDI/ZFs9y+HhBf8KTGSL5pcZPtQsZh3GBQ0UonqL1mp9PA==} dependencies: vega-dataflow: 5.7.4 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7282,7 +7301,7 @@ packages: d3-interpolate: 3.0.1 d3-scale: 4.0.2 vega-time: 2.1.0 - vega-util: 1.17.2 + vega-util: 1.17.0 dev: false /vega-scenegraph@4.10.1: @@ -7293,7 +7312,7 @@ packages: vega-canvas: 1.2.6 vega-loader: 4.5.0 vega-scale: 7.2.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7307,7 +7326,7 @@ packages: dependencies: d3-array: 3.1.1 vega-expression: 5.0.0 - vega-util: 1.17.2 + vega-util: 1.17.0 dev: false /vega-statistics@1.8.0: @@ -7331,7 +7350,7 @@ packages: dependencies: d3-array: 3.1.1 d3-time: 3.0.0 - vega-util: 1.17.2 + vega-util: 1.17.0 dev: false /vega-tooltip@0.32.0: @@ -7347,7 +7366,7 @@ packages: vega-dataflow: 5.7.4 vega-statistics: 1.8.0 vega-time: 2.1.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7357,7 +7376,7 @@ packages: dependencies: vega-event-selector: 3.0.0 vega-expression: 5.0.0 - vega-util: 1.17.2 + vega-util: 1.17.0 dev: false /vega-util@1.17.0: @@ -7373,7 +7392,7 @@ packages: dependencies: vega-dataflow: 5.7.4 vega-scenegraph: 4.10.1 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7388,7 +7407,7 @@ packages: vega-functions: 5.13.0 vega-runtime: 6.1.3 vega-scenegraph: 4.10.1 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7398,7 +7417,7 @@ packages: dependencies: d3-delaunay: 6.0.2 vega-dataflow: 5.7.4 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7410,7 +7429,7 @@ packages: vega-dataflow: 5.7.4 vega-scale: 7.2.0 vega-statistics: 1.8.0 - vega-util: 1.17.2 + vega-util: 1.17.0 transitivePeerDependencies: - encoding dev: false @@ -7651,7 +7670,6 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} @@ -7688,7 +7706,6 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - dev: false /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -7759,7 +7776,7 @@ packages: '@babel/runtime': 7.21.0 compilerr: 9.0.21 fast-unique-numbers: 6.0.21 - tslib: 2.5.3 + tslib: 2.4.0 dev: false /wrap-ansi@6.2.0: @@ -7783,19 +7800,6 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - /ws@8.13.0(bufferutil@4.0.7): resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} @@ -7809,7 +7813,6 @@ packages: optional: true dependencies: bufferutil: 4.0.7 - dev: false /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1016ee56f6..3a1712660f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - 'js/*' - - 'client/js' \ No newline at end of file + - 'client/js'