From ac5172c5d22580d214245cd9fe85c83b0a073895 Mon Sep 17 00:00:00 2001 From: "Beau B. Bruce" Date: Tue, 23 Jun 2026 15:58:18 -0400 Subject: [PATCH 1/5] chore: expand ignore rules for R and AI tooling --- .gitignore | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e7745ec..2ed1985 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,15 @@ app/src/pages/metapop/cached-baseline.json # Rust target/ +# R +.Rproj.user/ +.Rhistory +.RData +.Ruserdata +*.Rproj +packrat/ +renv/ + # Scaffolded test projects test-project-python/ test-project-rust/ @@ -51,7 +60,14 @@ cfasim-ui/docs/* !cfasim-ui/docs/vitest.config.ts !cfasim-ui/docs/drift.test.ts -/CLAUDE.md .vscode/* .DS_Store .cache/ + +# ai + spec kit +/CLAUDE.md +/AGENTS.md +/specs +/.codex +/.agents +/.specify From 768059282badbd44ec630880d41de413bdd92d5d Mon Sep 17 00:00:00 2001 From: "Beau B. Bruce" Date: Tue, 23 Jun 2026 15:58:47 -0400 Subject: [PATCH 2/5] feat(rwasm): add R browser runtime package --- .dockerignore | 13 + .prettierignore | 1 + cfasim-ui/cfasim-ui/package.json | 3 + cfasim-ui/cfasim-ui/src/rwasm-vite.js | 1 + cfasim-ui/cfasim-ui/src/rwasm.ts | 1 + cfasim-ui/docs/drift.test.ts | 2 + cfasim-ui/docs/package.json | 1 + cfasim-ui/rwasm/package.json | 30 ++ cfasim-ui/rwasm/r/cfasim/DESCRIPTION | 8 + cfasim-ui/rwasm/r/cfasim/NAMESPACE | 7 + cfasim-ui/rwasm/r/cfasim/R/model-output.R | 54 ++ cfasim-ui/rwasm/src/index.ts | 8 + cfasim-ui/rwasm/src/rwasm.worker.ts | 279 ++++++++++ cfasim-ui/rwasm/src/rwasmWorkerApi.ts | 214 ++++++++ cfasim-ui/rwasm/src/useModel.ts | 79 +++ cfasim-ui/rwasm/src/vitePlugin.js | 522 +++++++++++++++++++ cfasim-ui/shared/package.json | 3 +- docs/cfasim-ui/index.md | 2 + docs/cfasim-ui/rwasm.md | 56 ++ pnpm-lock.yaml | 607 ++++++++++++++++------ scripts/generate_docs.mjs | 1 + 21 files changed, 1743 insertions(+), 149 deletions(-) create mode 100644 .dockerignore create mode 100644 cfasim-ui/cfasim-ui/src/rwasm-vite.js create mode 100644 cfasim-ui/cfasim-ui/src/rwasm.ts create mode 100644 cfasim-ui/rwasm/package.json create mode 100644 cfasim-ui/rwasm/r/cfasim/DESCRIPTION create mode 100644 cfasim-ui/rwasm/r/cfasim/NAMESPACE create mode 100644 cfasim-ui/rwasm/r/cfasim/R/model-output.R create mode 100644 cfasim-ui/rwasm/src/index.ts create mode 100644 cfasim-ui/rwasm/src/rwasm.worker.ts create mode 100644 cfasim-ui/rwasm/src/rwasmWorkerApi.ts create mode 100644 cfasim-ui/rwasm/src/useModel.ts create mode 100644 cfasim-ui/rwasm/src/vitePlugin.js create mode 100644 docs/cfasim-ui/rwasm.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7048424 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules/ +.git/ +*.log* +.env* +coverage/ +target/ +dist/ +build/ +.venv/ +.Rhistory +.RData +.Ruserdata +.Rproj.user/ diff --git a/.prettierignore b/.prettierignore index 9f82500..e8e74c7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,6 +11,7 @@ cfasim-ui/docs/charts/ cfasim-ui/docs/shared/ cfasim-ui/docs/pyodide/ cfasim-ui/docs/wasm/ +cfasim-ui/docs/rwasm/ cfasim-ui/docs/theme/ # Template source with minijinja conditionals — prettier can't parse {% %} blocks cfasim/src/templates/_shared/interactive/src/env.d.ts diff --git a/cfasim-ui/cfasim-ui/package.json b/cfasim-ui/cfasim-ui/package.json index d916a70..7edd366 100644 --- a/cfasim-ui/cfasim-ui/package.json +++ b/cfasim-ui/cfasim-ui/package.json @@ -23,6 +23,8 @@ "./wasm/vite": "./src/wasm-vite.js", "./pyodide": "./src/pyodide.ts", "./pyodide/vite": "./src/pyodide-vite.js", + "./rwasm": "./src/rwasm.ts", + "./rwasm/vite": "./src/rwasm-vite.js", "./shared": "./src/shared.ts", "./theme": { "default": "./src/theme/cfasim.css" @@ -49,6 +51,7 @@ "@cfasim-ui/docs": "workspace:*", "@cfasim-ui/wasm": "workspace:*", "@cfasim-ui/pyodide": "workspace:*", + "@cfasim-ui/rwasm": "workspace:*", "@cfasim-ui/shared": "workspace:*", "@cfasim-ui/theme": "workspace:*" }, diff --git a/cfasim-ui/cfasim-ui/src/rwasm-vite.js b/cfasim-ui/cfasim-ui/src/rwasm-vite.js new file mode 100644 index 0000000..f5b638f --- /dev/null +++ b/cfasim-ui/cfasim-ui/src/rwasm-vite.js @@ -0,0 +1 @@ +export * from "@cfasim-ui/rwasm/vite"; diff --git a/cfasim-ui/cfasim-ui/src/rwasm.ts b/cfasim-ui/cfasim-ui/src/rwasm.ts new file mode 100644 index 0000000..14f1a69 --- /dev/null +++ b/cfasim-ui/cfasim-ui/src/rwasm.ts @@ -0,0 +1 @@ +export * from "@cfasim-ui/rwasm"; diff --git a/cfasim-ui/docs/drift.test.ts b/cfasim-ui/docs/drift.test.ts index e129779..978a43c 100644 --- a/cfasim-ui/docs/drift.test.ts +++ b/cfasim-ui/docs/drift.test.ts @@ -15,6 +15,8 @@ describe("@cfasim-ui/docs generator", () => { ); const entries = [...index.content.components, ...index.content.charts]; expect(entries.length).toBeGreaterThan(0); + expect(existsSync(resolve(PACKAGE_ROOT, "rwasm/index.ts"))).toBe(true); + expect(existsSync(resolve(PACKAGE_ROOT, "rwasm/vitePlugin.js"))).toBe(true); for (const entry of entries) { for (const field of ["docs", "source"] as const) { diff --git a/cfasim-ui/docs/package.json b/cfasim-ui/docs/package.json index 47a5604..f353847 100644 --- a/cfasim-ui/docs/package.json +++ b/cfasim-ui/docs/package.json @@ -21,6 +21,7 @@ "shared", "pyodide", "wasm", + "rwasm", "theme" ] } diff --git a/cfasim-ui/rwasm/package.json b/cfasim-ui/rwasm/package.json new file mode 100644 index 0000000..ce112ed --- /dev/null +++ b/cfasim-ui/rwasm/package.json @@ -0,0 +1,30 @@ +{ + "name": "@cfasim-ui/rwasm", + "version": "0.5.1", + "type": "module", + "description": "R/WebAssembly integration for cfasim-ui", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/CDCgov/cfa-simulator.git", + "directory": "cfasim-ui/rwasm" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "src", + "r" + ], + "exports": { + ".": "./src/index.ts", + "./vite": "./src/vitePlugin.js" + }, + "dependencies": { + "@cfasim-ui/shared": "workspace:*", + "webr": "^0.5.5" + }, + "peerDependencies": { + "vue": "^3.5.0" + } +} diff --git a/cfasim-ui/rwasm/r/cfasim/DESCRIPTION b/cfasim-ui/rwasm/r/cfasim/DESCRIPTION new file mode 100644 index 0000000..265eceb --- /dev/null +++ b/cfasim-ui/rwasm/r/cfasim/DESCRIPTION @@ -0,0 +1,8 @@ +Package: cfasim +Title: cfasim Model Helpers +Version: 0.5.1 +Description: Minimal R helpers for returning cfasim model outputs. +License: Apache License (>= 2) +Encoding: UTF-8 +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.2 diff --git a/cfasim-ui/rwasm/r/cfasim/NAMESPACE b/cfasim-ui/rwasm/r/cfasim/NAMESPACE new file mode 100644 index 0000000..7df9169 --- /dev/null +++ b/cfasim-ui/rwasm/r/cfasim/NAMESPACE @@ -0,0 +1,7 @@ +export(bool) +export(enum) +export(f64) +export(i32) +export(model_output) +export(model_outputs) +export(u32) diff --git a/cfasim-ui/rwasm/r/cfasim/R/model-output.R b/cfasim-ui/rwasm/r/cfasim/R/model-output.R new file mode 100644 index 0000000..d6acef1 --- /dev/null +++ b/cfasim-ui/rwasm/r/cfasim/R/model-output.R @@ -0,0 +1,54 @@ +f64 <- function(x) { + list(type = "f64", values = as.numeric(x)) +} + +i32 <- function(x) { + list(type = "i32", values = as.integer(x)) +} + +u32 <- function(x) { + list(type = "u32", values = as.integer(x)) +} + +bool <- function(x) { + list(type = "bool", values = as.logical(x)) +} + +enum <- function(indices, labels) { + list( + type = "enum", + values = as.integer(indices), + enumLabels = as.character(labels) + ) +} + +model_output <- function(...) { + columns <- list(...) + data <- unname(lapply(columns, function(col) col$values)) + lengths <- vapply(data, length, integer(1)) + if (length(lengths) > 0 && length(unique(lengths)) != 1) { + stop("cfasim model output columns must have equal length") + } + + descriptors <- unname(Map( + function(name, col) { + descriptor <- list(name = name, type = col$type) + if (!is.null(col$enumLabels)) { + descriptor$enumLabels <- col$enumLabels + } + descriptor + }, + names(columns), + columns + )) + + list( + length = if (length(lengths) == 0) 0 else lengths[[1]], + columns = descriptors, + data = data + ) +} + +model_outputs <- function(...) { + list(`__modelOutputs` = TRUE, outputs = list(...)) +} diff --git a/cfasim-ui/rwasm/src/index.ts b/cfasim-ui/rwasm/src/index.ts new file mode 100644 index 0000000..f4131f2 --- /dev/null +++ b/cfasim-ui/rwasm/src/index.ts @@ -0,0 +1,8 @@ +export { + loadModel, + loadModelOnWorker, + runR, + warmWorkers, + type WorkerName, +} from "./rwasmWorkerApi.js"; +export { useModel } from "./useModel.js"; diff --git a/cfasim-ui/rwasm/src/rwasm.worker.ts b/cfasim-ui/rwasm/src/rwasm.worker.ts new file mode 100644 index 0000000..b28c6ae --- /dev/null +++ b/cfasim-ui/rwasm/src/rwasm.worker.ts @@ -0,0 +1,279 @@ +import { + postErrorWithTransfer, + postModelOutputsWithTransfer, + postWithTransfer, +} from "@cfasim-ui/shared/transfer"; +import type { + ColumnDescriptor, + ModelOutputsWire, +} from "@cfasim-ui/shared/model-output"; +import type { + JsonValue, + RwasmBundleManifest, + RWorkerRequest, +} from "./rwasmWorkerApi.js"; + +interface RSession { + webR: any; + manifest: RwasmBundleManifest; +} + +const sessions = new Map>(); +const baseUrl = import.meta.env.BASE_URL ?? "/"; + +function joinUrl(base: string, path: string): string { + return `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`; +} + +function appUrl(path: string): string { + if (/^https?:\/\//.test(path)) return path; + return joinUrl(`${self.location.origin}${baseUrl}`, path); +} + +export function manifestUrl(model: string): string { + const base = `${self.location.origin}${baseUrl}`; + return joinUrl(base, `rwasm/${model}/manifest.json`); +} + +function assertIdentifier(name: string): void { + if (!/^[A-Za-z.][A-Za-z0-9._]*$/.test(name)) { + throw new Error(`Invalid R function name: ${name}`); + } +} + +function rLiteral(value: JsonValue): string { + if (value === null) return "NULL"; + if (typeof value === "boolean") return value ? "TRUE" : "FALSE"; + if (typeof value === "number") { + if (!Number.isFinite(value)) throw new Error("R arguments must be finite"); + return String(value); + } + if (typeof value === "string") return JSON.stringify(value); + if (Array.isArray(value)) return `list(${value.map(rLiteral).join(", ")})`; + return `list(${Object.entries(value) + .map(([key, val]) => `${key} = ${rLiteral(val)}`) + .join(", ")})`; +} + +function buildCall(fn: string, params?: Record): string { + assertIdentifier(fn); + const args = Object.entries(params ?? {}) + .map(([key, val]) => `${key} = ${rLiteral(val)}`) + .join(", "); + return `${fn}(${args})`; +} + +function asArrayBuffer(value: unknown, type: string): ArrayBuffer { + const values = Array.from(value as Iterable); + switch (type) { + case "f64": + return new Float64Array(values as number[]).buffer; + case "i32": + return new Int32Array(values as number[]).buffer; + case "u32": + case "enum": + return new Uint32Array(values as number[]).buffer; + case "bool": + return new Uint8Array(values.map((v) => (v ? 1 : 0))).buffer; + default: + throw new Error(`Unsupported R output column type: ${type}`); + } +} + +function arrayFromNamedList(value: unknown): unknown[] | null { + if (Array.isArray(value)) return value; + if (value && typeof value === "object") return Object.values(value); + return null; +} + +export function normalizeWebRValue(value: unknown): unknown { + if (value instanceof Map) { + return Object.fromEntries( + Array.from(value, ([key, val]) => [String(key), normalizeWebRValue(val)]), + ); + } + if (Array.isArray(value)) return value.map(normalizeWebRValue); + if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value; + if (isWebRDataJs(value)) return decodeWebRDataJs(value); + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, val]) => [key, normalizeWebRValue(val)]), + ); + } + return value; +} + +function isWebRDataJs(value: unknown): value is { + type: string; + names?: string[] | null; + values?: unknown[]; +} { + return ( + !!value && + typeof value === "object" && + typeof (value as { type?: unknown }).type === "string" && + ("values" in value || (value as { type?: string }).type === "null") + ); +} + +function decodeWebRDataJs(value: { + type: string; + names?: string[] | null; + values?: unknown[]; +}): unknown { + if (value.type === "null") return null; + const values = (value.values ?? []).map(normalizeWebRValue); + const names = value.names ?? null; + if (names?.some(Boolean)) { + return Object.fromEntries(names.map((name, i) => [name, values[i]])); + } + if (value.type === "list") return values; + return values.length === 1 ? values[0] : values; +} + +async function evalRValue(webR: any, code: string): Promise { + const proxy = await webR.evalR(code); + try { + const value = + proxy && typeof proxy.toJs === "function" ? await proxy.toJs() : proxy; + return normalizeWebRValue(value); + } finally { + if (proxy && typeof proxy.destroy === "function") proxy.destroy(); + } +} + +export function convertRModelOutputs(value: unknown): ModelOutputsWire | null { + if (!value || typeof value !== "object") return null; + const maybe = value as { __modelOutputs?: unknown; outputs?: unknown }; + if ( + !maybe.__modelOutputs || + !maybe.outputs || + typeof maybe.outputs !== "object" + ) { + return null; + } + + const outputs: ModelOutputsWire["outputs"] = {}; + for (const [name, output] of Object.entries( + maybe.outputs as Record, + )) { + const columns = arrayFromNamedList(output.columns) as + | ColumnDescriptor[] + | null; + const data = arrayFromNamedList(output.buffers ?? output.data); + if (!columns || !data) { + throw new Error(`Invalid R ModelOutput: ${name}`); + } + + const buffers = columns.map((col, i) => asArrayBuffer(data[i], col.type)); + const firstColumn = data[0] as { length?: number } | undefined; + const length = + typeof output.length === "number" + ? output.length + : (firstColumn?.length ?? 0); + + outputs[name] = { + __modelOutput: true, + length, + columns, + buffers, + }; + } + return { __modelOutputs: true, outputs }; +} + +async function loadManifest(model: string): Promise { + const response = await fetch(manifestUrl(model)); + if (!response.ok) { + throw new Error(`Failed to load R model manifest for ${model}`); + } + return (await response.json()) as RwasmBundleManifest; +} + +async function createSession(model: string): Promise { + const manifest = await loadManifest(model); + const { WebR, ChannelType } = await import("webr"); + const webR = new WebR( + manifest.webRBaseUrl + ? { + baseUrl: appUrl(manifest.webRBaseUrl), + channelType: ChannelType.PostMessage, + } + : { channelType: ChannelType.PostMessage }, + ); + await webR.init(); + + if (manifest.packages.length > 0) { + if (!manifest.repoUrl) { + throw new Error(`R model ${model} lists packages without a repoUrl`); + } + const packages = manifest.packages + .map((pkg) => JSON.stringify(pkg)) + .join(", "); + const repoUrl = appUrl(manifest.repoUrl); + console.log(`[rwasm] installing packages from ${repoUrl}`); + try { + await webR.installPackages(manifest.packages, { + repos: repoUrl, + mount: true, + }); + await webR.evalRVoid( + `missing <- c(${packages})[!vapply(c(${packages}), requireNamespace, logical(1), quietly = TRUE)]; if (length(missing) > 0) stop(paste("Missing installed packages:", paste(missing, collapse = ", ")))`, + ); + console.log(`[rwasm] packages installed successfully`); + } catch (e) { + console.error(`[rwasm] package install failed:`, e); + throw e; + } + } + + if (manifest.libraryImageUrl) { + throw new Error("R filesystem image bundles are not implemented yet"); + } + + if (manifest.modelPackage) { + await webR.evalRVoid( + `library(${JSON.stringify(manifest.modelPackage)}, character.only = TRUE)`, + ); + } else { + const entryUrl = joinUrl(appUrl(manifest.modelBaseUrl), manifest.entry); + const entry = await fetch(entryUrl); + if (!entry.ok) throw new Error(`Failed to load R model entry: ${entryUrl}`); + await webR.evalRVoid(await entry.text()); + } + return { webR, manifest }; +} + +function ensureSession(model: string): Promise { + if (!sessions.has(model)) { + const promise = createSession(model); + promise.catch((error) => { + console.error(`[rwasm] session creation failed for ${model}:`, error); + sessions.delete(model); + }); + sessions.set(model, promise); + } + return sessions.get(model)!; +} + +self.onmessage = async (event: MessageEvent) => { + const { id, type, model, fn, params } = event.data; + try { + const session = await ensureSession(model); + if (type === "loadModel") { + postWithTransfer(self, id, true); + return; + } + + if (!fn) throw new Error("R function name is required"); + const result = await evalRValue(session.webR, buildCall(fn, params)); + const modelOutputs = convertRModelOutputs(result); + if (modelOutputs) { + postModelOutputsWithTransfer(self, id, modelOutputs); + } else { + postWithTransfer(self, id, result); + } + } catch (error) { + postErrorWithTransfer(self, id, error); + } +}; diff --git a/cfasim-ui/rwasm/src/rwasmWorkerApi.ts b/cfasim-ui/rwasm/src/rwasmWorkerApi.ts new file mode 100644 index 0000000..68ff8eb --- /dev/null +++ b/cfasim-ui/rwasm/src/rwasmWorkerApi.ts @@ -0,0 +1,214 @@ +import type { ColumnDescriptor } from "@cfasim-ui/shared/model-output"; +import { unwrapResponse } from "@cfasim-ui/shared/transfer"; +import type { TransferableResponse } from "@cfasim-ui/shared/transfer"; + +export type JsonValue = + | null + | boolean + | number + | string + | JsonValue[] + | { [key: string]: JsonValue }; + +export interface RwasmBundleManifest { + name: string; + entry: string; + modelPackage?: string; + modelBaseUrl: string; + packages: string[]; + repoUrl?: string; + libraryImageUrl?: string; + webRBaseUrl?: string; + createdBy: "cfasimRwasm"; +} + +export interface RWorkerRequest { + id: number; + type: "loadModel" | "run"; + model: string; + fn?: string; + params?: Record; +} + +export interface RWorkerResponse extends TransferableResponse { + id: number; + modelOutputs?: { + outputs: Record< + string, + { length: number; columns: ColumnDescriptor[]; buffers: ArrayBuffer[] } + >; + }; +} + +export type WorkerName = string; + +const DEFAULT_WORKER = "default"; + +let lastId = 0; +function getId(): number { + return ++lastId; +} + +function createWorker(): Worker { + return new Worker(new URL("./rwasm.worker.ts", import.meta.url), { + type: "module", + }); +} + +type OutgoingMessage = Omit; + +export type RwasmResponse = { result?: unknown; error?: string }; + +function describeWorkerError(event: ErrorEvent): string { + const msg = event.message || event.error?.message || ""; + if (msg) { + if (event.filename) return `${msg} (${event.filename}:${event.lineno})`; + return msg; + } + return "R worker failed to load (no error details available)"; +} + +function requestResponse( + entry: WorkerEntry, + msg: OutgoingMessage, +): Promise { + if (entry.deadError) { + return Promise.resolve({ error: entry.deadError }); + } + return new Promise((resolve) => { + const id = getId(); + entry.pending.set(id, resolve); + + function listener(event: MessageEvent) { + if (event.data?.id !== id) return; + entry.worker.removeEventListener("message", listener); + entry.pending.delete(id); + if (event.data.error) { + resolve({ error: event.data.error }); + } else { + resolve({ result: unwrapResponse(event.data) }); + } + } + + entry.worker.addEventListener("message", listener); + entry.worker.postMessage({ id, ...msg } satisfies RWorkerRequest); + }); +} + +interface WorkerEntry { + worker: Worker; + models: Map>; + pending: Map void>; + deadError: string | null; +} + +const workers = new Map(); +const sharedModels = new Set(); + +function getWorker(name: string): WorkerEntry { + let entry = workers.get(name); + if (!entry) { + const worker = createWorker(); + const newEntry: WorkerEntry = { + worker, + models: new Map(), + pending: new Map(), + deadError: null, + }; + function failAll(message: string) { + if (newEntry.deadError) return; + newEntry.deadError = message; + newEntry.models.clear(); + const pending = Array.from(newEntry.pending.values()); + newEntry.pending.clear(); + for (const resolve of pending) resolve({ error: message }); + } + worker.addEventListener("error", (event: ErrorEvent) => { + failAll(describeWorkerError(event)); + }); + worker.addEventListener("messageerror", () => { + failAll("R worker could not deserialize its response"); + }); + entry = newEntry; + workers.set(name, entry); + for (const model of sharedModels) { + ensureModelOn(entry, model); + } + } + return entry; +} + +function ensureModelOn( + entry: WorkerEntry, + model: string, +): Promise { + let promise = entry.models.get(model); + if (!promise) { + promise = requestResponse(entry, { + type: "loadModel", + model, + }); + entry.models.set(model, promise); + promise.then((response) => { + if (response.error) entry.models.delete(model); + }); + } + return promise; +} + +export async function loadModel( + model: string, +): Promise<{ result?: true; error?: string }> { + sharedModels.add(model); + const defaultInstall = ensureModelOn(getWorker(DEFAULT_WORKER), model); + const otherInstalls: Array> = []; + for (const [name, entry] of workers) { + if (name !== DEFAULT_WORKER) { + otherInstalls.push(ensureModelOn(entry, model)); + } + } + await Promise.all(otherInstalls); + const response = await defaultInstall; + return response.error ? { error: response.error } : { result: true }; +} + +export async function loadModelOnWorker( + model: string, + worker: WorkerName, +): Promise<{ result?: true; error?: string }> { + const response = await ensureModelOn(getWorker(worker), model); + return response.error ? { error: response.error } : { result: true }; +} + +export async function runR( + model: string, + fn: string, + params?: Record, + worker: WorkerName = DEFAULT_WORKER, +): Promise<{ result?: unknown; error?: string }> { + const entry = getWorker(worker); + const installResult = await ensureModelOn(entry, model); + if (installResult.error) return { error: installResult.error }; + return requestResponse(entry, { + type: "run", + model, + fn, + params, + }); +} + +export async function warmWorkers(options: { + workers: WorkerName[]; + models?: string[]; +}): Promise { + const models = options.models ?? []; + for (const model of models) sharedModels.add(model); + const installs: Array> = []; + for (const name of options.workers) { + const entry = getWorker(name); + for (const model of models) { + installs.push(ensureModelOn(entry, model)); + } + } + await Promise.all(installs); +} diff --git a/cfasim-ui/rwasm/src/useModel.ts b/cfasim-ui/rwasm/src/useModel.ts new file mode 100644 index 0000000..1c47a26 --- /dev/null +++ b/cfasim-ui/rwasm/src/useModel.ts @@ -0,0 +1,79 @@ +import { ref, toRaw, toValue, watch } from "vue"; +import type { MaybeRef } from "vue"; +import type { ModelOutput } from "@cfasim-ui/shared"; +import { loadModel, runR } from "./rwasmWorkerApi.js"; +import type { JsonValue } from "./rwasmWorkerApi.js"; + +function plainParams( + params: Record | undefined, +): Record | undefined { + if (!params) return undefined; + return Object.fromEntries( + Object.entries(params).map(([key, value]) => [key, toRaw(value)]), + ) as Record; +} + +export function useModel(modelName: string) { + const result = ref(); + const error = ref(); + const loading = ref(true); + + const loaded = loadModel(modelName); + loaded.then((response) => { + if (response.error) error.value = response.error; + loading.value = false; + }); + + async function run(fn: string, params?: Record) { + loading.value = true; + error.value = undefined; + try { + await loaded; + const response = await runR(modelName, fn, plainParams(params)); + if (response.error) { + error.value = response.error; + } else { + result.value = response.result as T; + } + } catch (e) { + error.value = e instanceof Error ? e.message : String(e); + } finally { + loading.value = false; + } + } + + function useOutputs

>( + fn: string, + params: MaybeRef

, + ) { + const outputs = ref>(); + const outputsError = ref(); + const outputsLoading = ref(true); + + watch( + () => toValue(params), + async (p) => { + outputsLoading.value = true; + outputsError.value = undefined; + try { + await loaded; + const response = await runR(modelName, fn, plainParams(p)); + if (response.error) { + outputsError.value = response.error; + } else { + outputs.value = response.result as Record; + } + } catch (e) { + outputsError.value = e instanceof Error ? e.message : String(e); + } finally { + outputsLoading.value = false; + } + }, + { immediate: true, deep: true }, + ); + + return { outputs, error: outputsError, loading: outputsLoading }; + } + + return { run, result, error, loading, useOutputs }; +} diff --git a/cfasim-ui/rwasm/src/vitePlugin.js b/cfasim-ui/rwasm/src/vitePlugin.js new file mode 100644 index 0000000..f54808c --- /dev/null +++ b/cfasim-ui/rwasm/src/vitePlugin.js @@ -0,0 +1,522 @@ +import { execFileSync } from "node:child_process"; +import { + cpSync, + existsSync, + mkdtempSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { + basename, + dirname, + isAbsolute, + join, + relative, + resolve, +} from "node:path"; +import { tmpdir } from "node:os"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import { createHash } from "node:crypto"; + +const require = createRequire(import.meta.url); + +const DEFAULT_IMAGE_REPOSITORY = "ghcr.io/r-wasm/webr"; +const CFASIM_PACKAGE_NAME = "cfasim"; +const CFASIM_PACKAGE_MOUNT = "/cfasim-package"; +const DEFAULT_RWASM_PACKAGE_REF = "r-wasm/rwasm"; +const WEBR_R_DESCRIPTION = + "dist/vfs/usr/lib/R/library/translations/DESCRIPTION"; +const WEBR_DIST_DIR = "dist"; +const DEFAULT_CFASIM_PACKAGE_DIR = resolve( + dirname(fileURLToPath(import.meta.url)), + "../r/cfasim", +); + +let webRPackageInfo; + +function normalizeName(name) { + return name.replace(/-/g, "_"); +} + +function resolveEntry(modelDir, entry) { + if (entry) return entry; + if (existsSync(resolve(modelDir, "R", "model.R"))) return "R/model.R"; + return "model.R"; +} + +function dockerOptions(options) { + if (typeof options?.docker === "object") return options.docker; + return {}; +} + +function rVector(items) { + return `c(${items.map((pkg) => `"${pkg}"`).join(", ")})`; +} + +function rString(value) { + return JSON.stringify(value); +} + +function findPackageRoot(packageName) { + let current = dirname(require.resolve(packageName)); + while (current !== dirname(current)) { + const packagePath = join(current, "package.json"); + if (existsSync(packagePath)) { + const packageJson = JSON.parse(readFileSync(packagePath, "utf-8")); + if (packageJson.name === packageName) { + return { packageJson, packageRoot: current }; + } + } + current = dirname(current); + } + throw new Error(`Unable to locate ${packageName} package root`); +} + +function readWebRPackageInfo() { + if (webRPackageInfo) return webRPackageInfo; + + const { packageJson, packageRoot } = findPackageRoot("webr"); + const descriptionPath = join(packageRoot, WEBR_R_DESCRIPTION); + const description = readFileSync(descriptionPath, "utf-8"); + const rVersion = description.match(/^Version:\s*(\d+)\.(\d+)(?:\.\d+)?\s*$/m); + if (!rVersion) { + throw new Error(`Unable to detect webR R version from ${descriptionPath}`); + } + + webRPackageInfo = { + packageVersion: packageJson.version, + binaryVersion: `${rVersion[1]}.${rVersion[2]}`, + packageRoot, + }; + return webRPackageInfo; +} + +function defaultDockerImage() { + return `${DEFAULT_IMAGE_REPOSITORY}:v${readWebRPackageInfo().packageVersion}`; +} + +function hostOwnership() { + if ( + typeof process.getuid !== "function" || + typeof process.getgid !== "function" + ) { + return undefined; + } + return { uid: process.getuid(), gid: process.getgid() }; +} + +export function dockerCommand({ + modelDir, + outDir, + cfasimPackageDir, + modelPackage, + packages, + binaryVersion, + rwasmPackage = DEFAULT_RWASM_PACKAGE_REF, + docker, +}) { + const image = docker.image ?? defaultDockerImage(); + const externalPackages = packages.filter( + (pkg) => pkg !== CFASIM_PACKAGE_NAME && pkg !== modelPackage, + ); + const buildExternal = + externalPackages.length > 0 + ? [ + `rwasm::build(${rVector(externalPackages)}, out_dir = bin, dependencies = NA)`, + ] + : []; + const buildModel = modelPackage + ? [`rwasm::build("/model", out_dir = bin, dependencies = FALSE)`] + : []; + const rCommand = + docker.rCommand ?? + [ + `pak::pkg_install(${rString(rwasmPackage)}, upgrade = FALSE)`, + `pak::pkg_install("${CFASIM_PACKAGE_MOUNT}")`, + `rv <- getRversion()`, + `r_minor <- strsplit(as.character(rv$minor), ".", fixed = TRUE)[[1]][1]`, + `r_binary <- paste(rv$major, r_minor, sep = ".")`, + `expected_binary <- ${rString(binaryVersion)}`, + `if (!identical(r_binary, expected_binary)) stop(sprintf("webR Docker image builds R %s packages, but installed webR runtime expects R %s packages", r_binary, expected_binary))`, + `bin <- file.path("/output/repo/bin/emscripten/contrib", expected_binary)`, + `dir.create(bin, recursive = TRUE, showWarnings = FALSE)`, + `dir.create("/output/repo/src/contrib", recursive = TRUE, showWarnings = FALSE)`, + `rwasm::build("${CFASIM_PACKAGE_MOUNT}", out_dir = bin, dependencies = FALSE)`, + ...buildExternal, + ...buildModel, + `file.copy(list.files(bin, pattern = "\\\\.tgz$", full.names = TRUE), "/output/repo/src/contrib", overwrite = TRUE)`, + `rwasm::write_packages("/output/repo")`, + `file.copy(list.files(bin, pattern = "^PACKAGES(\\\\.gz|\\\\.rds)?$", full.names = TRUE), "/output/repo/src/contrib", overwrite = TRUE)`, + ].join("; "); + const mounts = cfasimPackageDir + ? ["-v", `${cfasimPackageDir}:${CFASIM_PACKAGE_MOUNT}:ro`] + : []; + const ownership = hostOwnership(); + const user = ownership ? ["--user", `${ownership.uid}:${ownership.gid}`] : []; + + return { + file: "docker", + args: [ + "run", + "--rm", + ...user, + "-e", + "HOME=/tmp", + "-v", + `${modelDir}:/model:ro`, + ...mounts, + "-v", + `${outDir}:/output`, + "-w", + "/output", + image, + "R", + "-q", + "-e", + rCommand, + ], + }; +} + +function shouldRunDocker( + outDir, + packages, + binaryVersion, + docker, + cachePath, + cacheKey, +) { + if (packages.length === 0) return false; + const rebuild = docker.rebuild ?? "auto"; + if (rebuild === "always") return true; + if (rebuild === "never") return false; + return ( + !repoHasBinaryPackages(join(outDir, "repo"), packages, binaryVersion) || + readBuildCacheKey(cachePath) !== cacheKey + ); +} + +function readPackageIndexes(repoDir) { + if (!existsSync(repoDir)) return []; + const indexes = []; + for (const entry of readdirSync(repoDir)) { + const path = join(repoDir, entry); + const stat = statSync(path); + if (stat.isDirectory()) indexes.push(...readPackageIndexes(path)); + if (stat.isFile() && entry === "PACKAGES") { + indexes.push(readFileSync(path, "utf-8")); + } + } + return indexes; +} + +function repoHasPackages(repoDir, packages) { + const indexes = readPackageIndexes(repoDir).join("\n"); + return packages.every((pkg) => + new RegExp( + `^Package:\\s*${pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, + "m", + ).test(indexes), + ); +} + +function repoHasBinaryPackages(repoDir, packages, binaryVersion) { + const expectedRepo = join( + repoDir, + "bin", + "emscripten", + "contrib", + binaryVersion, + ); + return existsSync(expectedRepo) && repoHasPackages(expectedRepo, packages); +} + +function isWithin(path, dir) { + const rel = relative(dir, path); + return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel)); +} + +function copyModel(modelDir, outDir, excludePaths = []) { + const target = join(outDir, "model"); + rmSync(target, { recursive: true, force: true }); + mkdirSync(target, { recursive: true }); + const modelRoot = resolve(modelDir); + const copyTarget = isWithin(target, modelRoot) + ? join(mkdtempSync(join(tmpdir(), "cfasim-rwasm-model-")), "model") + : target; + const ignoredNames = new Set([".git", "dist", "node_modules"]); + const excluded = [target, ...excludePaths] + .map((path) => resolve(path)) + .filter((path) => path !== modelRoot && isWithin(path, modelRoot)); + + cpSync(modelDir, copyTarget, { + recursive: true, + filter(src) { + const resolved = resolve(src); + if (resolved !== modelRoot && ignoredNames.has(basename(resolved))) { + return false; + } + return !excluded.some((path) => isWithin(resolved, path)); + }, + }); + if (copyTarget !== target) { + cpSync(copyTarget, target, { recursive: true }); + rmSync(dirname(copyTarget), { recursive: true, force: true }); + } + return target; +} + +function uniquePackages(packages) { + return Array.from(new Set(packages.filter(Boolean))); +} + +function readDescriptionFields(modelDir) { + const descriptionPath = resolve(modelDir, "DESCRIPTION"); + if (!existsSync(descriptionPath)) return new Map(); + + const fields = new Map(); + let current; + for (const line of readFileSync(descriptionPath, "utf-8").split(/\r?\n/)) { + const field = line.match(/^([^:\s][^:]*):\s*(.*)$/); + if (field) { + current = field[1].trim(); + fields.set(current, field[2].trim()); + continue; + } + if (current && /^[ \t]/.test(line)) { + fields.set(current, `${fields.get(current)}\n${line.trim()}`.trim()); + } + } + return fields; +} + +function readDescriptionPackageName(modelDir) { + return readDescriptionFields(modelDir).get("Package"); +} + +function readDescriptionPackages(modelDir) { + const fields = readDescriptionFields(modelDir); + return ["Imports", "Depends"].flatMap((field) => + (fields.get(field) ?? "") + .split(",") + .map((pkg) => pkg.trim().replace(/\s*\(.+\)$/, "")) + .filter((pkg) => pkg && pkg !== "R"), + ); +} + +function hashString(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function hashDirectory(dir) { + const hash = createHash("sha256"); + const root = resolve(dir); + + function visit(path) { + const stat = statSync(path); + const rel = relative(root, path); + if (stat.isDirectory()) { + hash.update(`dir:${rel}\0`); + for (const entry of readdirSync(path).sort()) { + visit(join(path, entry)); + } + return; + } + if (stat.isFile()) { + hash.update(`file:${rel}\0`); + hash.update(readFileSync(path)); + } + } + + visit(root); + return hash.digest("hex"); +} + +function readBuildCacheKey(cachePath) { + try { + return JSON.parse(readFileSync(cachePath, "utf-8")).key; + } catch { + return undefined; + } +} + +function writeBuildCacheKey(cachePath, key) { + mkdirSync(dirname(cachePath), { recursive: true }); + writeFileSync(cachePath, `${JSON.stringify({ key }, null, 2)}\n`); +} + +function buildCacheKey({ + stagedModelDir, + cfasimPackageDir, + packages, + binaryVersion, + rwasmPackage, + dockerImage, + entry, + modelPackage, +}) { + return hashString( + JSON.stringify({ + version: 1, + modelHash: hashDirectory(stagedModelDir), + cfasimPackageHash: hashDirectory(cfasimPackageDir), + packages, + binaryVersion, + rwasmPackage, + dockerImage, + entry, + modelPackage, + }), + ); +} + +function buildCachePath(root, name) { + return resolve( + root, + "node_modules", + ".cache", + "cfasim-rwasm", + `${name}.json`, + ); +} + +function writeManifest({ + outDir, + name, + entry, + modelPackage, + packages, + webRBaseUrl, +}) { + const manifest = { + name, + entry, + modelPackage, + modelBaseUrl: `rwasm/${name}/model/`, + packages, + repoUrl: packages.length > 0 ? `rwasm/${name}/repo/` : undefined, + webRBaseUrl, + createdBy: "cfasimRwasm", + }; + writeFileSync( + join(outDir, "manifest.json"), + `${JSON.stringify(manifest, null, 2)}\n`, + ); +} + +function ensureHostedWebRAssets(root) { + const { packageVersion, packageRoot } = readWebRPackageInfo(); + const target = resolve(root, "public", "rwasm", "webr", `v${packageVersion}`); + mkdirSync(dirname(target), { recursive: true }); + cpSync(join(packageRoot, WEBR_DIST_DIR), target, { recursive: true }); + return `rwasm/webr/v${packageVersion}/`; +} + +export function buildRwasmBundle(root, options = {}) { + const modelDir = resolve(root, options.model ?? "model"); + if (!existsSync(modelDir)) + throw new Error(`R model directory not found: ${modelDir}`); + + const name = normalizeName(options.name ?? basename(root)); + const entry = resolveEntry(modelDir, options.entry); + const entryPath = resolve(modelDir, entry); + if (!existsSync(entryPath)) + throw new Error(`R model entry not found: ${entryPath}`); + + const outDir = resolve(root, "public", "rwasm", name); + mkdirSync(outDir, { recursive: true }); + rmSync(join(outDir, ".cfasim-rwasm-build.json"), { force: true }); + const stagedModelDir = copyModel(modelDir, outDir, [root]); + + const cfasimPackageDir = resolve( + root, + options.cfasimPackage ?? DEFAULT_CFASIM_PACKAGE_DIR, + ); + if (!existsSync(cfasimPackageDir)) { + throw new Error( + `cfasim R package directory not found: ${cfasimPackageDir}`, + ); + } + + const modelPackage = + options.modelPackage ?? readDescriptionPackageName(modelDir); + const packages = uniquePackages([ + CFASIM_PACKAGE_NAME, + modelPackage, + ...(options.packages ?? readDescriptionPackages(modelDir)), + ]); + const binaryVersion = + options.webRBinaryVersion ?? readWebRPackageInfo().binaryVersion; + + const docker = + options.docker === false ? { rebuild: "never" } : dockerOptions(options); + const rwasmPackage = docker.rwasmPackage ?? DEFAULT_RWASM_PACKAGE_REF; + const cachePath = buildCachePath(root, name); + const cacheKey = buildCacheKey({ + stagedModelDir, + cfasimPackageDir, + packages, + binaryVersion, + rwasmPackage, + dockerImage: docker.image ?? defaultDockerImage(), + entry, + modelPackage, + }); + + const dockerRan = shouldRunDocker( + outDir, + packages, + binaryVersion, + docker, + cachePath, + cacheKey, + ); + if (dockerRan) { + const command = dockerCommand({ + modelDir: stagedModelDir, + outDir, + cfasimPackageDir, + modelPackage, + packages, + binaryVersion, + rwasmPackage, + docker, + }); + execFileSync(command.file, command.args, { cwd: root, stdio: "pipe" }); + } + + if ( + packages.length > 0 && + !repoHasBinaryPackages(join(outDir, "repo"), packages, binaryVersion) + ) { + throw new Error( + `R package assets missing for webR R ${binaryVersion}: ${join(outDir, "repo")}`, + ); + } + if (docker.rebuild !== "never") { + writeBuildCacheKey(cachePath, cacheKey); + } + + writeManifest({ + outDir, + name, + entry, + modelPackage, + packages, + webRBaseUrl: options.webRBaseUrl ?? ensureHostedWebRAssets(root), + }); +} + +export function cfasimRwasm(options) { + return { + name: "cfasim-rwasm", + configResolved(config) { + buildRwasmBundle(config.root, options); + }, + }; +} diff --git a/cfasim-ui/shared/package.json b/cfasim-ui/shared/package.json index 0817840..207bc49 100644 --- a/cfasim-ui/shared/package.json +++ b/cfasim-ui/shared/package.json @@ -17,7 +17,8 @@ ], "exports": { ".": "./src/index.ts", - "./transfer": "./src/transferUtils.ts" + "./transfer": "./src/transferUtils.ts", + "./model-output": "./src/ModelOutput.ts" }, "peerDependencies": { "vue": "^3.5.0" diff --git a/docs/cfasim-ui/index.md b/docs/cfasim-ui/index.md index 1b6022e..336987c 100644 --- a/docs/cfasim-ui/index.md +++ b/docs/cfasim-ui/index.md @@ -11,6 +11,7 @@ cfasim-ui is the shared component and theming library you use to make simulators | `@cfasim-ui/theme` | Design tokens, reset, and utility classes | | `@cfasim-ui/pyodide` | Run Python models in the browser via [Pyodide](https://pyodide.org) Web Workers | | `@cfasim-ui/wasm` | Run Rust/WASM models in the browser via a Web Worker | +| `@cfasim-ui/rwasm` | Run bundled R models in the browser via webR | | `@cfasim-ui/shared` | Shared utilities, including the [`useUrlParams`](./shared) composable | ## Components @@ -42,3 +43,4 @@ cfasim-ui is the shared component and theming library you use to make simulators - [Pyodide](./pyodide) — run Python models via Pyodide Web Workers - [WASM](./wasm) — run Rust/WASM models via a Web Worker +- [rwasm](./rwasm) — run bundled R models via webR diff --git a/docs/cfasim-ui/rwasm.md b/docs/cfasim-ui/rwasm.md new file mode 100644 index 0000000..d8fad98 --- /dev/null +++ b/docs/cfasim-ui/rwasm.md @@ -0,0 +1,56 @@ +# rwasm + +`@cfasim-ui/rwasm` runs bundled R models in the browser with webR. + +## useModel + +```ts +import { useModel } from "@cfasim-ui/rwasm"; + +const { useOutputs } = useModel("my.model"); +const { outputs, error, loading } = useOutputs("simulate", params); +``` + +`useOutputs(fn, params)` matches the Python and Rust runtime packages. R functions return `cfasim` helper output, and the worker converts it into named `ModelOutput` tables for display. + +## Vite + +```ts +import { cfasimRwasm } from "@cfasim-ui/rwasm/vite"; + +cfasimRwasm({ + model: "model", + name: "my.model", + packages: ["jsonlite"], + docker: true, +}); +``` + +Docker is the recommended build environment for R package dependencies. The `packages` option is for external R dependencies; the local `cfasim` helper package and local model package are included automatically. The deployed app serves static assets only. + +## R model packages + +R models are ordinary minimal R packages. Add `cfasim` to `DESCRIPTION`: + +```text +Imports: cfasim +``` + +Export callable model functions and import the helper package in `NAMESPACE`: + +```r +export(simulate) +import(cfasim) +``` + +The `cfasim` package provides: + +- `model_outputs(...)` +- `model_output(...)` +- `f64(x)` +- `i32(x)` +- `u32(x)` +- `bool(x)` +- `enum(indices, labels)` + +These helpers create the same structured model-output contract used by the Python `cfasim_model` module and Rust `cfasim-model` crate. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bf9d5f..de2e3ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,10 +25,10 @@ importers: version: 6.0.3 vite: specifier: ^8.0.14 - version: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + version: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) vitest: specifier: ^4.1.7 - version: 4.1.7(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + version: 4.1.7(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) vue-tsc: specifier: ^3.3.2 version: 3.3.2(typescript@6.0.3) @@ -47,6 +47,9 @@ importers: '@cfasim-ui/pyodide': specifier: workspace:* version: link:../pyodide + '@cfasim-ui/rwasm': + specifier: workspace:* + version: link:../rwasm '@cfasim-ui/shared': specifier: workspace:* version: link:../shared @@ -107,7 +110,7 @@ importers: version: 1.0.5 '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) + version: 6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 @@ -119,10 +122,10 @@ importers: version: 3.0.1 vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@25.5.0)(rollup@4.59.1)(typescript@6.0.3)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + version: 4.5.4(@types/node@25.5.0)(rollup@4.59.1)(typescript@6.0.3)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) vitest: specifier: ^4.1.0 - version: 4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + version: 4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) cfasim-ui/components: dependencies: @@ -168,7 +171,7 @@ importers: version: 0.44.10 '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) + version: 6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 @@ -177,13 +180,13 @@ importers: version: 20.8.9 vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@25.5.0)(rollup@4.59.1)(typescript@6.0.3)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + version: 4.5.4(@types/node@25.5.0)(rollup@4.59.1)(typescript@6.0.3)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) vite-svg-loader: specifier: ^5.1.1 version: 5.1.1(vue@3.5.35(typescript@6.0.3)) vitest: specifier: ^4.1.0 - version: 4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + version: 4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) cfasim-ui/docs: {} @@ -199,6 +202,18 @@ importers: specifier: 3.5.35 version: 3.5.35(typescript@6.0.3) + cfasim-ui/rwasm: + dependencies: + '@cfasim-ui/shared': + specifier: workspace:* + version: link:../shared + vue: + specifier: 3.5.35 + version: 3.5.35(typescript@6.0.3) + webr: + specifier: ^0.5.5 + version: 0.5.9 + cfasim-ui/shared: dependencies: '@types/sprintf-js': @@ -219,7 +234,7 @@ importers: version: 20.8.9 vitest: specifier: ^4.1.0 - version: 4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + version: 4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) cfasim-ui/theme: dependencies: @@ -258,20 +273,20 @@ importers: version: 3.5.35(typescript@6.0.3) vue-router: specifier: ^5.1.0 - version: 5.1.0(@vue/compiler-sfc@3.5.35)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) + version: 5.1.0(@vue/compiler-sfc@3.5.35)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) devDependencies: '@playwright/test': specifier: ^1.60.0 version: 1.60.0 '@vitejs/plugin-vue': specifier: ^6.0.7 - version: 6.0.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) + version: 6.0.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)) vite-svg-loader: specifier: ^5.1.1 version: 5.1.1(vue@3.5.35(typescript@6.0.3)) vitepress: specifier: ^1.6.4 - version: 1.6.4(@algolia/client-search@5.49.2)(@types/node@25.5.0)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3) + version: 1.6.4(@algolia/client-search@5.49.2)(@types/node@25.5.0)(lightningcss@1.32.0)(postcss@8.5.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3) models: dependencies: @@ -284,6 +299,9 @@ importers: '@cfasim-ui/pyodide': specifier: workspace:* version: link:../cfasim-ui/pyodide + '@cfasim-ui/rwasm': + specifier: workspace:* + version: link:../cfasim-ui/rwasm '@cfasim-ui/shared': specifier: workspace:* version: link:../cfasim-ui/shared @@ -308,7 +326,7 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3)) + version: 6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -514,8 +532,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -526,8 +544,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -538,8 +556,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -550,8 +568,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -562,8 +580,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -574,8 +592,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -586,8 +604,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -598,8 +616,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -610,8 +628,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -622,8 +640,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -634,8 +652,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -646,8 +664,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -658,8 +676,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -670,8 +688,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -682,8 +700,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -694,8 +712,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -706,14 +724,14 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -724,14 +742,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -742,14 +760,14 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -760,8 +778,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -772,8 +790,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -784,8 +802,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -796,8 +814,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -880,6 +898,10 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@msgpack/msgpack@2.8.0': + resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -1513,6 +1535,14 @@ packages: peerDependencies: vue: 3.5.35 + '@xterm/addon-fit@0.10.0': + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -1623,6 +1653,12 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + codemirror-lang-r@0.1.1: + resolution: {integrity: sha512-ke9Bm7IPKOoEk0p8LxZJaRlqp8CGOOZns9eKyj/WUaNV58h4uEeWbMpWeJJhVIPvfiuXYkv4FG1hD70gguWJLQ==} + codemirror@6.0.2: resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} @@ -1666,6 +1702,9 @@ packages: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -1817,8 +1856,8 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true @@ -1923,10 +1962,16 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -1950,6 +1995,9 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1968,6 +2016,9 @@ packages: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -1988,6 +2039,9 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -1995,6 +2049,12 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lezer-r@0.1.3: + resolution: {integrity: sha512-tk+7Q54+ZYHKlLZj69GuZNC8+nMYPIFhGjrSe2fTyQAk9GyUsxgRsmF8V4r7cUiB65+lRu5/SrePeTEKQx5ZAQ==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -2072,6 +2132,10 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2158,6 +2222,10 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -2170,6 +2238,12 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2236,6 +2310,12 @@ packages: engines: {node: '>=14'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -2249,6 +2329,46 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + react-accessible-treeview@2.11.2: + resolution: {integrity: sha512-qui0g/gBDpP7VbtqelgJezAzAjKOY3IVi1Rq1NRJ7Z627RXKyKiQ4ooxLK2yauxTvNyU0ke9S0a2d9YUMbJJbA==} + peerDependencies: + classnames: ^2.2.6 + prop-types: ^15.7.2 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-data-grid@7.0.0-beta.59: + resolution: {integrity: sha512-iAp/UYWjfmXYFsyKDtGDMP1IvhwtQSjCP6G/wFEbMNuumWGOEZF8Ut1S2Bp4XxVpOrBkEVKXn+QC3rs14AcB7A==} + peerDependencies: + react: ^19.2 + react-dom: ^19.2 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-icons@4.12.0: + resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==} + peerDependencies: + react: '*' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-resizable-panels@2.1.9: + resolution: {integrity: sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -2289,10 +2409,16 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -2313,6 +2439,9 @@ packages: engines: {node: '>=10'} hasBin: true + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2374,6 +2503,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -2445,6 +2577,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2491,6 +2628,9 @@ packages: us-atlas@3.0.1: resolution: {integrity: sha512-wEIZCq0ImPvGblTd8gZMqNNCPkQshugMUG/8nkSWXb02+XqNFREg9atHOKP9w6prLZTpqcLhSvdBW81MkV3/0Q==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2739,6 +2879,10 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webr@0.5.9: + resolution: {integrity: sha512-Hzg6AK7+GiegMz+nrHZQLWA3q3F5Ku6WNkuUPqQN2yJXYFtRXdikMebfXUXu5MAixvoeXzWXu+bZQhJgxuz1Jg==} + engines: {node: '>=17.0.0'} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -2785,6 +2929,15 @@ packages: utf-8-validate: optional: true + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + xterm-readline@1.2.2: + resolution: {integrity: sha512-+jKS8fkP1kF7cNWyznAt2TvLB8/MzPMO4T/ON5FgsRQQfE87YO/Krh0sGnpPxr4B5Isipyt66RDJS+4eEy1RYw==} + peerDependencies: + '@xterm/xterm': ^5.5.0 || ^6.0.0 + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -3025,9 +3178,9 @@ snapshots: '@docsearch/css@3.8.2': {} - '@docsearch/js@3.8.2(@algolia/client-search@5.49.2)(search-insights@2.17.3)': + '@docsearch/js@3.8.2(@algolia/client-search@5.49.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': dependencies: - '@docsearch/react': 3.8.2(@algolia/client-search@5.49.2)(search-insights@2.17.3) + '@docsearch/react': 3.8.2(@algolia/client-search@5.49.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) preact: 10.29.0 transitivePeerDependencies: - '@algolia/client-search' @@ -3036,13 +3189,15 @@ snapshots: - react-dom - search-insights - '@docsearch/react@3.8.2(@algolia/client-search@5.49.2)(search-insights@2.17.3)': + '@docsearch/react@3.8.2(@algolia/client-search@5.49.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.49.2)(algoliasearch@5.49.2)(search-insights@2.17.3) '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.49.2)(algoliasearch@5.49.2) '@docsearch/css': 3.8.2 algoliasearch: 5.49.2 optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' @@ -3066,148 +3221,148 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.27.4': + '@esbuild/aix-ppc64@0.28.0': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.27.4': + '@esbuild/android-arm64@0.28.0': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.27.4': + '@esbuild/android-arm@0.28.0': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.27.4': + '@esbuild/android-x64@0.28.0': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.27.4': + '@esbuild/darwin-arm64@0.28.0': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.27.4': + '@esbuild/darwin-x64@0.28.0': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.27.4': + '@esbuild/freebsd-arm64@0.28.0': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.27.4': + '@esbuild/freebsd-x64@0.28.0': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.27.4': + '@esbuild/linux-arm64@0.28.0': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.27.4': + '@esbuild/linux-arm@0.28.0': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.27.4': + '@esbuild/linux-ia32@0.28.0': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.27.4': + '@esbuild/linux-loong64@0.28.0': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.27.4': + '@esbuild/linux-mips64el@0.28.0': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.27.4': + '@esbuild/linux-ppc64@0.28.0': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.27.4': + '@esbuild/linux-riscv64@0.28.0': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.27.4': + '@esbuild/linux-s390x@0.28.0': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.27.4': + '@esbuild/linux-x64@0.28.0': optional: true - '@esbuild/netbsd-arm64@0.27.4': + '@esbuild/netbsd-arm64@0.28.0': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.27.4': + '@esbuild/netbsd-x64@0.28.0': optional: true - '@esbuild/openbsd-arm64@0.27.4': + '@esbuild/openbsd-arm64@0.28.0': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.27.4': + '@esbuild/openbsd-x64@0.28.0': optional: true - '@esbuild/openharmony-arm64@0.27.4': + '@esbuild/openharmony-arm64@0.28.0': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.27.4': + '@esbuild/sunos-x64@0.28.0': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.27.4': + '@esbuild/win32-arm64@0.28.0': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.27.4': + '@esbuild/win32-ia32@0.28.0': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.27.4': + '@esbuild/win32-x64@0.28.0': optional: true '@floating-ui/core@1.7.5': @@ -3343,6 +3498,8 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@msgpack/msgpack@2.8.0': {} + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -3686,22 +3843,22 @@ snapshots: vite: 5.4.21(@types/node@25.5.0)(lightningcss@1.32.0) vue: 3.5.35(typescript@6.0.3) - '@vitejs/plugin-vue@6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) vue: 3.5.35(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) vue: 3.5.35(typescript@6.0.3) - '@vitejs/plugin-vue@6.0.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) vue: 3.5.35(typescript@6.0.3) '@vitest/expect@4.1.0': @@ -3722,21 +3879,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.0(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) '@vitest/pretty-format@4.1.0': dependencies: @@ -3987,6 +4144,12 @@ snapshots: dependencies: vue: 3.5.35(typescript@6.0.3) + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/xterm@5.5.0': {} + abbrev@2.0.0: {} acorn@8.16.0: {} @@ -4086,6 +4249,13 @@ snapshots: dependencies: readdirp: 5.0.0 + classnames@2.5.1: {} + + codemirror-lang-r@0.1.1: + dependencies: + '@codemirror/language': 6.12.3 + lezer-r: 0.1.3 + codemirror@6.0.2: dependencies: '@codemirror/autocomplete': 6.20.2 @@ -4127,6 +4297,8 @@ snapshots: dependencies: is-what: 5.5.0 + core-util-is@1.0.3: {} + crelt@1.0.6: {} cross-spawn@7.0.6: @@ -4288,35 +4460,34 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.27.4: + esbuild@0.28.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 - optional: true + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 esprima@4.0.1: {} @@ -4425,8 +4596,12 @@ snapshots: html-void-elements@3.0.0: {} + immediate@3.0.6: {} + import-lazy@4.0.0: {} + inherits@2.0.4: {} + ini@1.3.8: {} internmap@2.0.3: {} @@ -4441,6 +4616,8 @@ snapshots: is-what@5.5.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} jackspeak@3.4.3: @@ -4461,6 +4638,8 @@ snapshots: js-cookie@3.0.5: {} + js-tokens@4.0.0: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -4478,10 +4657,26 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + kind-of@6.0.3: {} kolorist@1.8.0: {} + lezer-r@0.1.3: + dependencies: + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -4539,6 +4734,10 @@ snapshots: lodash@4.18.1: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@10.4.3: {} lru-cache@6.0.0: @@ -4625,6 +4824,8 @@ snapshots: dependencies: boolbase: 1.0.0 + object-assign@4.1.1: {} + obug@2.1.1: {} ohash@2.0.11: {} @@ -4637,6 +4838,10 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + + pako@2.1.0: {} + path-browserify@1.0.1: {} path-key@3.1.1: {} @@ -4696,6 +4901,14 @@ snapshots: prettier@3.8.3: {} + process-nextick-args@2.0.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + property-information@7.1.0: {} proto-list@1.2.4: {} @@ -4710,6 +4923,49 @@ snapshots: quansync@0.2.11: {} + react-accessible-treeview@2.11.2(classnames@2.5.1)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + classnames: 2.5.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-data-grid@7.0.0-beta.59(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-icons@4.12.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-resizable-panels@2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@5.0.0: {} regex-recursion@6.0.2: @@ -4816,8 +5072,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.1 fsevents: 2.3.3 + safe-buffer@5.1.2: {} + sax@1.6.0: {} + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + scule@1.3.0: {} search-insights@2.17.3: {} @@ -4833,6 +5095,8 @@ snapshots: semver@7.7.4: {} + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4886,6 +5150,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -4951,6 +5219,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.22.4: + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + typescript@5.9.3: {} typescript@6.0.3: {} @@ -4997,6 +5271,8 @@ snapshots: us-atlas@3.0.1: {} + util-deprecate@1.0.2: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -5007,7 +5283,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-dts@4.5.4(@types/node@25.5.0)(rollup@4.59.1)(typescript@6.0.3)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)): + vite-plugin-dts@4.5.4(@types/node@25.5.0)(rollup@4.59.1)(typescript@6.0.3)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@microsoft/api-extractor': 7.58.1(@types/node@25.5.0) '@rollup/pluginutils': 5.3.0(rollup@4.59.1) @@ -5020,7 +5296,7 @@ snapshots: magic-string: 0.30.21 typescript: 6.0.3 optionalDependencies: - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - rollup @@ -5044,7 +5320,7 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 - vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0): + vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -5053,14 +5329,15 @@ snapshots: tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.5.0 - esbuild: 0.27.4 + esbuild: 0.28.0 fsevents: 2.3.3 + tsx: 4.22.4 yaml: 2.9.0 - vitepress@1.6.4(@algolia/client-search@5.49.2)(@types/node@25.5.0)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3): + vitepress@1.6.4(@algolia/client-search@5.49.2)(@types/node@25.5.0)(lightningcss@1.32.0)(postcss@8.5.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3): dependencies: '@docsearch/css': 3.8.2 - '@docsearch/js': 3.8.2(@algolia/client-search@5.49.2)(search-insights@2.17.3) + '@docsearch/js': 3.8.2(@algolia/client-search@5.49.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) '@iconify-json/simple-icons': 1.2.74 '@shikijs/core': 2.5.0 '@shikijs/transformers': 2.5.0 @@ -5106,10 +5383,10 @@ snapshots: - typescript - universal-cookie - vitest@4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)): + vitest@4.1.0(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.0(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -5126,7 +5403,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.5.0 @@ -5134,10 +5411,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.7(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)): + vitest@4.1.7(@types/node@25.5.0)(happy-dom@20.8.9)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -5154,7 +5431,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.5.0 @@ -5188,7 +5465,7 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.35(typescript@5.9.3) - vue-router@5.1.0(@vue/compiler-sfc@3.5.35)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)): + vue-router@5.1.0(@vue/compiler-sfc@3.5.35)(vite@8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3)): dependencies: '@babel/generator': 8.0.0-rc.6 '@vue-macros/common': 3.1.2(vue@3.5.35(typescript@6.0.3)) @@ -5210,7 +5487,7 @@ snapshots: yaml: 2.9.0 optionalDependencies: '@vue/compiler-sfc': 3.5.35 - vite: 8.0.14(@types/node@25.5.0)(esbuild@0.27.4)(yaml@2.9.0) + vite: 8.0.14(@types/node@25.5.0)(esbuild@0.28.0)(tsx@4.22.4)(yaml@2.9.0) vue-tsc@3.3.2(typescript@6.0.3): dependencies: @@ -5242,6 +5519,33 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webr@0.5.9: + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@msgpack/msgpack': 2.8.0 + '@xterm/addon-fit': 0.10.0(@xterm/xterm@5.5.0) + '@xterm/xterm': 5.5.0 + classnames: 2.5.1 + codemirror: 6.0.2 + codemirror-lang-r: 0.1.1 + jszip: 3.10.1 + lezer-r: 0.1.3 + lightningcss: 1.32.0 + pako: 2.1.0 + prop-types: 15.8.1 + react: 18.3.1 + react-accessible-treeview: 2.11.2(classnames@2.5.1)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-data-grid: 7.0.0-beta.59(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + react-icons: 4.12.0(react@18.3.1) + react-resizable-panels: 2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tsx: 4.22.4 + xmlhttprequest-ssl: 2.1.2 + xterm-readline: 1.2.2(@xterm/xterm@5.5.0) + whatwg-mimetype@3.0.0: {} which@2.0.2: @@ -5269,6 +5573,13 @@ snapshots: ws@8.20.0: {} + xmlhttprequest-ssl@2.1.2: {} + + xterm-readline@1.2.2(@xterm/xterm@5.5.0): + dependencies: + '@xterm/xterm': 5.5.0 + string-width: 4.2.3 + yallist@4.0.0: {} yaml@2.9.0: {} diff --git a/scripts/generate_docs.mjs b/scripts/generate_docs.mjs index ac52429..749e687 100644 --- a/scripts/generate_docs.mjs +++ b/scripts/generate_docs.mjs @@ -41,6 +41,7 @@ const WORKSPACE_PACKAGES = [ "shared", "pyodide", "wasm", + "rwasm", "theme", ]; From 33671c3af4e175c590469204342cf3dfa482bf35 Mon Sep 17 00:00:00 2001 From: "Beau B. Bruce" Date: Tue, 23 Jun 2026 15:59:15 -0400 Subject: [PATCH 3/5] feat(cli): scaffold R projects --- cfasim/init.spec.ts | 44 ++++++++++++++++--- cfasim/playwright.config.ts | 5 ++- cfasim/src/init.rs | 14 ++++++ cfasim/src/main.rs | 6 ++- cfasim/src/project.rs | 23 +++++++++- cfasim/src/templates/R/AGENTS.md | 15 +++++++ cfasim/src/templates/R/CLAUDE.md | 3 ++ cfasim/src/templates/R/DESCRIPTION | 9 ++++ cfasim/src/templates/R/NAMESPACE | 2 + cfasim/src/templates/R/R/model.R | 15 +++++++ .../src/templates/R/interactive/src/App.vue | 35 +++++++++++++++ .../templates/R/tests/testthat/test-model.R | 8 ++++ cfasim/src/templates/R/vite.config.ts | 9 ++++ cfasim/src/templates/_shared/.gitignore | 11 +++++ .../src/templates/_shared/pnpm-workspace.yaml | 1 + cfasim/src/test.rs | 10 +++++ 16 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 cfasim/src/templates/R/AGENTS.md create mode 100644 cfasim/src/templates/R/CLAUDE.md create mode 100644 cfasim/src/templates/R/DESCRIPTION create mode 100644 cfasim/src/templates/R/NAMESPACE create mode 100644 cfasim/src/templates/R/R/model.R create mode 100644 cfasim/src/templates/R/interactive/src/App.vue create mode 100644 cfasim/src/templates/R/tests/testthat/test-model.R create mode 100644 cfasim/src/templates/R/vite.config.ts diff --git a/cfasim/init.spec.ts b/cfasim/init.spec.ts index 0fcafc8..51f5d44 100644 --- a/cfasim/init.spec.ts +++ b/cfasim/init.spec.ts @@ -1,14 +1,26 @@ import { test, expect } from "@playwright/test"; import { execSync, spawn } from "node:child_process"; import type { ChildProcess } from "node:child_process"; -import { existsSync, rmSync, mkdtempSync } from "node:fs"; -import { resolve } from "node:path"; +import { existsSync, readFileSync, rmSync, mkdtempSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, resolve } from "node:path"; import { tmpdir } from "node:os"; const ROOT = resolve(import.meta.dirname, ".."); const CLI = resolve(ROOT, "target/debug/cfasim"); - -type Template = "python" | "rust" | "ixa"; +const require = createRequire(import.meta.url); +const rwasmRequire = createRequire( + resolve(ROOT, "cfasim-ui/rwasm/package.json"), +); +const WEBR_ENTRY = rwasmRequire.resolve("webr"); +const WEBR_VERSION = JSON.parse( + readFileSync(resolve(dirname(dirname(WEBR_ENTRY)), "package.json"), "utf8"), +).version; +const SERVER_START_TIMEOUT_MS = Number( + process.env.CFASIM_E2E_SERVER_TIMEOUT_MS ?? "300000", +); + +type Template = "python" | "rust" | "ixa" | "R"; const TMP_DIR = mkdtempSync(resolve(tmpdir(), "cfasim-test-")); @@ -45,7 +57,7 @@ function startVite( return { proc, url: `http://localhost:${port}` }; } -async function waitForServer(url: string, timeoutMs = 30_000) { +async function waitForServer(url: string, timeoutMs = SERVER_START_TIMEOUT_MS) { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { @@ -84,6 +96,12 @@ test.describe("cfasim init", () => { port: 7203, paramLabel: "Population", }, + { + name: "test-project-R", + template: "R", + port: 7204, + paramLabel: "Steps", + }, ]; const procs: ChildProcess[] = []; @@ -104,13 +122,27 @@ test.describe("cfasim init", () => { for (const p of projects) { scaffoldProject(p.name, p.template); execSync("pnpm install", { cwd: resolve(TMP_DIR, p.name) }); + if (p.template === "R") { + execSync("pnpm run build", { + cwd: resolve(TMP_DIR, p.name), + stdio: "pipe", + }); + const webrWorker = resolve( + TMP_DIR, + p.name, + `dist/rwasm/webr/v${WEBR_VERSION}/webr-worker.js`, + ); + if (!existsSync(webrWorker)) { + throw new Error(`Missing hosted webR worker asset: ${webrWorker}`); + } + } } // Pre-build wasm so `waitForServer` doesn't time out waiting on the // `cfasimWasm` plugin's cold cargo build during vite startup. Mirrors // the plugin's invocation in cfasim-ui/wasm/src/vitePlugin.js. for (const p of projects) { - if (p.template === "python") continue; // pyodide template, no wasm-pack + if (p.template === "python" || p.template === "R") continue; const moduleName = p.name.replace(/-/g, "_"); execSync( `wasm-pack build .. --target web --out-dir public/wasm/${moduleName}`, diff --git a/cfasim/playwright.config.ts b/cfasim/playwright.config.ts index 566c408..47cae7c 100644 --- a/cfasim/playwright.config.ts +++ b/cfasim/playwright.config.ts @@ -4,5 +4,8 @@ export default defineConfig({ testDir: ".", testMatch: "*.spec.ts", testIgnore: ["src/templates/**", "**/node_modules/**"], - timeout: 120_000, + timeout: 300_000, + use: { + ignoreHTTPSErrors: true, + }, }); diff --git a/cfasim/src/init.rs b/cfasim/src/init.rs index 953b33b..c797fc5 100644 --- a/cfasim/src/init.rs +++ b/cfasim/src/init.rs @@ -19,6 +19,7 @@ pub enum Template { Python, Rust, Ixa, + R, } impl Template { @@ -27,6 +28,7 @@ impl Template { Template::Python => "python", Template::Rust => "rust", Template::Ixa => "ixa", + Template::R => "R", } } @@ -35,6 +37,7 @@ impl Template { Template::Python => "python", Template::Rust => "rust", Template::Ixa => "ixa", + Template::R => "R", } } } @@ -45,6 +48,7 @@ impl fmt::Display for Template { Template::Python => write!(f, "Python"), Template::Rust => write!(f, "Rust (WASM)"), Template::Ixa => write!(f, "Ixa (Rust agent-based model on WASM)"), + Template::R => write!(f, "R"), } } } @@ -147,6 +151,10 @@ fn to_module_name(name: &str) -> String { name.replace('-', "_") } +fn to_package_name(name: &str) -> String { + name.replace(['-', '_'], ".") +} + fn build_env() -> Environment<'static> { let mut env = Environment::new(); // Don't auto-escape — these aren't HTML, they're source files. @@ -167,6 +175,7 @@ fn render( context! { project_name => name, module_name => to_module_name(name), + package_name => to_package_name(name), cfasim_version => env!("CARGO_PKG_VERSION"), runtime => runtime, // Set by the e2e tests to the cfa-simulator repo root so the @@ -317,6 +326,11 @@ pub fn run( "Ixa", "Agent-based model using the ixa framework on WASM", ) + .item( + Template::R, + "R", + "Bundles R packages for WebAssembly via rwasm", + ) .interact()?, }; diff --git a/cfasim/src/main.rs b/cfasim/src/main.rs index 603cf4d..3cc4d4f 100644 --- a/cfasim/src/main.rs +++ b/cfasim/src/main.rs @@ -27,7 +27,7 @@ enum Commands { #[arg(long, default_missing_value = ".")] dir: Option, - /// Template: python or rust (skips interactive prompt) + /// Template: python, rust, ixa, or R (skips interactive prompt) #[arg(long)] template: Option, @@ -71,6 +71,9 @@ enum TemplateArg { Python, Rust, Ixa, + // Keep the CLI value uppercase; clap would otherwise expose this as `r`. + #[value(name = "R")] + R, } fn main() -> Result<()> { @@ -93,6 +96,7 @@ fn main() -> Result<()> { TemplateArg::Python => init::Template::Python, TemplateArg::Rust => init::Template::Rust, TemplateArg::Ixa => init::Template::Ixa, + TemplateArg::R => init::Template::R, }); init::run(dir, template, local).map_err(|e| anyhow::anyhow!("{e}")) } diff --git a/cfasim/src/project.rs b/cfasim/src/project.rs index b8570be..82b05fe 100644 --- a/cfasim/src/project.rs +++ b/cfasim/src/project.rs @@ -4,6 +4,7 @@ use std::path::Path; pub enum Kind { Python, Rust, + R, } pub struct Project { @@ -18,14 +19,14 @@ pub fn detect_or_fail(dir: &Path) -> anyhow::Result { detect(dir).ok_or_else(|| { anyhow::anyhow!( "not inside a cfasim project (expected package.json with cfasim-ui \ - dep, plus pyproject.toml or Cargo.toml)" + dep, plus pyproject.toml, Cargo.toml, or DESCRIPTION with R/)" ) }) } /// Detect a cfasim project rooted at `dir`. Returns `None` if `dir` isn't a /// cfasim project (no `package.json` with a `cfasim-ui` dep) or doesn't match -/// either of the python/rust template shapes. +/// one of the python/rust/R template shapes. pub fn detect(dir: &Path) -> Option { let text = std::fs::read_to_string(dir.join("package.json")).ok()?; let value: serde_json::Value = serde_json::from_str(&text).ok()?; @@ -36,6 +37,8 @@ pub fn detect(dir: &Path) -> Option { Kind::Python } else if dir.join("Cargo.toml").is_file() { Kind::Rust + } else if dir.join("DESCRIPTION").is_file() && dir.join("R").is_dir() { + Kind::R } else { return None; }; @@ -101,6 +104,22 @@ mod tests { assert!(!p.has_playwright); } + #[test] + fn detects_r_template() { + let dir = tempfile::TempDir::new().unwrap(); + write( + dir.path(), + "package.json", + r#"{"dependencies":{"cfasim-ui":"^0.3.0"}}"#, + ); + write(dir.path(), "DESCRIPTION", "Package: x\n"); + std::fs::create_dir(dir.path().join("R")).unwrap(); + write(dir.path(), "playwright.config.ts", ""); + let p = detect(dir.path()).unwrap(); + assert_eq!(p.kind, Kind::R); + assert!(p.has_playwright); + } + #[test] fn python_wins_when_both_manifests_exist() { let dir = tempfile::TempDir::new().unwrap(); diff --git a/cfasim/src/templates/R/AGENTS.md b/cfasim/src/templates/R/AGENTS.md new file mode 100644 index 0000000..0db197e --- /dev/null +++ b/cfasim/src/templates/R/AGENTS.md @@ -0,0 +1,15 @@ +# AGENTS.md + +`{{ project_name }}` is a cfasim simulation built with R, Vue 3, rwasm, and webR. + +- R model package: `DESCRIPTION`, `NAMESPACE`, and `R/model.R` +- Vue frontend: `interactive/src/App.vue` +- Runtime hook: `useModel` from `cfasim-ui/rwasm` + +## Commands + +- `pnpm dev` — start Vite +- `pnpm build` — build static assets +- `pnpm typecheck` — run Vue type checking +- `Rscript -e 'testthat::test_dir("tests/testthat")'` — run R model unit tests +- `pnpm test:e2e` — run Playwright diff --git a/cfasim/src/templates/R/CLAUDE.md b/cfasim/src/templates/R/CLAUDE.md new file mode 100644 index 0000000..3c04bb2 --- /dev/null +++ b/cfasim/src/templates/R/CLAUDE.md @@ -0,0 +1,3 @@ +# CLAUDE.md + +See `AGENTS.md`. diff --git a/cfasim/src/templates/R/DESCRIPTION b/cfasim/src/templates/R/DESCRIPTION new file mode 100644 index 0000000..c81851a --- /dev/null +++ b/cfasim/src/templates/R/DESCRIPTION @@ -0,0 +1,9 @@ +Package: {{ package_name }} +Title: {{ project_name }} +Version: 0.1.0 +Description: cfasim R model. +License: CC0 +Encoding: UTF-8 +Imports: cfasim +Suggests: testthat (>= 3.0.0) +Config/testthat/edition: 3 diff --git a/cfasim/src/templates/R/NAMESPACE b/cfasim/src/templates/R/NAMESPACE new file mode 100644 index 0000000..72c0957 --- /dev/null +++ b/cfasim/src/templates/R/NAMESPACE @@ -0,0 +1,2 @@ +export(simulate) +import(cfasim) diff --git a/cfasim/src/templates/R/R/model.R b/cfasim/src/templates/R/R/model.R new file mode 100644 index 0000000..4ac21f8 --- /dev/null +++ b/cfasim/src/templates/R/R/model.R @@ -0,0 +1,15 @@ +compute_series <- function(steps, rate) { + time <- seq(0, steps - 1) + values <- time * rate + list(time = time, values = values) +} + +simulate <- function(steps, rate) { + series <- compute_series(steps, rate) + model_outputs( + series = model_output( + time = f64(series$time), + values = f64(series$values) + ) + ) +} diff --git a/cfasim/src/templates/R/interactive/src/App.vue b/cfasim/src/templates/R/interactive/src/App.vue new file mode 100644 index 0000000..c7486d1 --- /dev/null +++ b/cfasim/src/templates/R/interactive/src/App.vue @@ -0,0 +1,35 @@ + + + diff --git a/cfasim/src/templates/R/tests/testthat/test-model.R b/cfasim/src/templates/R/tests/testthat/test-model.R new file mode 100644 index 0000000..9b1fb15 --- /dev/null +++ b/cfasim/src/templates/R/tests/testthat/test-model.R @@ -0,0 +1,8 @@ +source(file.path("..", "..", "R", "model.R")) + +test_that("compute_series creates matching time and value vectors", { + series <- compute_series(steps = 4, rate = 3) + + expect_equal(series$time, c(0, 1, 2, 3)) + expect_equal(series$values, c(0, 3, 6, 9)) +}) diff --git a/cfasim/src/templates/R/vite.config.ts b/cfasim/src/templates/R/vite.config.ts new file mode 100644 index 0000000..3943cc1 --- /dev/null +++ b/cfasim/src/templates/R/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import { cfasimRwasm } from "cfasim-ui/rwasm/vite"; + +export default defineConfig({ + root: "interactive", + build: { outDir: "../dist", emptyOutDir: true }, + plugins: [vue(), cfasimRwasm({ model: "..", name: "{{ package_name }}" })], +}); diff --git a/cfasim/src/templates/_shared/.gitignore b/cfasim/src/templates/_shared/.gitignore index 186a0e9..4cc023d 100644 --- a/cfasim/src/templates/_shared/.gitignore +++ b/cfasim/src/templates/_shared/.gitignore @@ -7,6 +7,15 @@ __pycache__/ {%- elif runtime == "rust" -%} # Rust target/ +{%- elif runtime == "R" -%} +# R +.Rproj.user/ +.Rhistory +.RData +.Ruserdata +*.Rproj +packrat/ +renv/ {%- endif %} # UI @@ -17,6 +26,8 @@ interactive/public/*.whl interactive/public/wheels.json {%- elif runtime == "rust" -%} interactive/public/wasm/ +{%- elif runtime == "R" -%} +interactive/public/rwasm/ {%- endif %} # Tests diff --git a/cfasim/src/templates/_shared/pnpm-workspace.yaml b/cfasim/src/templates/_shared/pnpm-workspace.yaml index 76b073f..cbb3314 100644 --- a/cfasim/src/templates/_shared/pnpm-workspace.yaml +++ b/cfasim/src/templates/_shared/pnpm-workspace.yaml @@ -19,6 +19,7 @@ overrides: "@cfasim-ui/shared": "file:{{ local_ui_dir }}/cfasim-ui/shared" "@cfasim-ui/wasm": "file:{{ local_ui_dir }}/cfasim-ui/wasm" "@cfasim-ui/pyodide": "file:{{ local_ui_dir }}/cfasim-ui/pyodide" + "@cfasim-ui/rwasm": "file:{{ local_ui_dir }}/cfasim-ui/rwasm" "@cfasim-ui/theme": "file:{{ local_ui_dir }}/cfasim-ui/theme" "@cfasim-ui/docs": "file:{{ local_ui_dir }}/cfasim-ui/docs" {% endif %} diff --git a/cfasim/src/test.rs b/cfasim/src/test.rs index cc0d977..853f617 100644 --- a/cfasim/src/test.rs +++ b/cfasim/src/test.rs @@ -36,6 +36,16 @@ fn run_unit(cwd: &Path, project: &Project) -> Result<()> { .context("failed to spawn `cargo test` (is cargo installed?)")? .check("unit tests") } + Kind::R => { + section("Unit tests — Rscript -e testthat::test_dir('tests/testthat')"); + spawn_and_wait( + Command::new("Rscript") + .args(["-e", "testthat::test_dir('tests/testthat')"]) + .current_dir(cwd), + ) + .context("failed to spawn `Rscript` (is R installed?)")? + .check("unit tests") + } } } From d74aa69d71eedd607981ecb604235574a6079b7e Mon Sep 17 00:00:00 2001 From: "Beau B. Bruce" Date: Tue, 23 Jun 2026 15:59:45 -0400 Subject: [PATCH 4/5] feat(models): add R example app --- models/models.spec.ts | 75 ++++++++++++++++++++++++-- models/package.json | 1 + models/playwright.config.ts | 9 +++- models/src/r-example/Page.vue | 37 +++++++++++++ models/src/r-example/model/DESCRIPTION | 7 +++ models/src/r-example/model/NAMESPACE | 2 + models/src/r-example/model/R/model.R | 10 ++++ models/src/router.ts | 6 +++ models/vite.config.ts | 5 ++ plz.toml | 2 +- 10 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 models/src/r-example/Page.vue create mode 100644 models/src/r-example/model/DESCRIPTION create mode 100644 models/src/r-example/model/NAMESPACE create mode 100644 models/src/r-example/model/R/model.R diff --git a/models/models.spec.ts b/models/models.spec.ts index 91d5810..ac17d44 100644 --- a/models/models.spec.ts +++ b/models/models.spec.ts @@ -1,14 +1,51 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, type Page } from "@playwright/test"; + +function attachCiBrowserDiagnostics(page: Page, testTitle: string) { + if (!process.env.CI || !testTitle.toLowerCase().includes("r example")) { + return; + } + + page.on("console", (message) => { + // Emit browser logs in CI so R/webR errors are visible in job output. + console.log(`[browser:${message.type()}] ${message.text()}`); + }); + + page.on("pageerror", (error) => { + console.log(`[pageerror] ${error.message}`); + }); + + page.on("requestfailed", (request) => { + console.log( + `[requestfailed] ${request.method()} ${request.url()} (${request.failure()?.errorText ?? "unknown error"})`, + ); + }); + + page.on("response", (response) => { + if (response.ok()) return; + + const url = response.url(); + if (url.includes("/rwasm/") || url.includes("/webr")) { + console.log( + `[bad-response] ${response.status()} ${response.request().method()} ${url}`, + ); + } + }); +} + +test.beforeEach(async ({ page }, testInfo) => { + attachCiBrowserDiagnostics(page, testInfo.title); +}); test("home page lists all models", async ({ page }) => { await page.goto("/"); await expect(page.locator("h1")).toContainText("Models"); const cards = page.locator(".model-card"); - await expect(cards).toHaveCount(4); + await expect(cards).toHaveCount(5); await expect(cards.nth(0)).toContainText("Reed-Frost Epidemic"); await expect(cards.nth(1)).toContainText("Ixa Example"); await expect(cards.nth(2)).toContainText("Python Example"); - await expect(cards.nth(3)).toContainText("Fetch Example"); + await expect(cards.nth(3)).toContainText("R Example"); + await expect(cards.nth(4)).toContainText("Fetch Example"); }); test("reed-frost model renders", async ({ page }) => { @@ -57,6 +94,12 @@ test("fetch example renders", async ({ page }) => { await expect(page.locator("h1")).toContainText("NSSP Emergency Department"); }); +test("r example renders", async ({ page }) => { + await page.goto("/r-example"); + await expect(page.locator("h1")).toContainText("R Example"); + await expect(page.getByLabel("Steps")).toBeVisible(); +}); + test("python-example syncs params to URL and hydrates from URL", async ({ page, }) => { @@ -85,3 +128,29 @@ test("python-example syncs params to URL and hydrates from URL", async ({ await expect.poll(() => new URL(page.url()).search).toBe(""); await expect(stepsInput).toHaveValue("10"); }); + +test("r-example syncs params to URL and hydrates from URL", async ({ + page, +}) => { + await page.goto("/r-example?steps=25&rate=4.5"); + const stepsInput = page.getByLabel("Steps"); + const rateInput = page.getByLabel("Rate"); + await expect(stepsInput).toHaveValue("25"); + await expect(rateInput).toHaveValue("4.5"); + + await stepsInput.fill("40"); + await stepsInput.press("Tab"); + await expect + .poll(() => new URL(page.url()).searchParams.get("steps")) + .toBe("40"); + + await rateInput.fill("2.5"); + await rateInput.press("Tab"); + await expect + .poll(() => new URL(page.url()).searchParams.get("rate")) + .toBeNull(); + + await page.getByRole("button", { name: "Reset" }).click(); + await expect.poll(() => new URL(page.url()).search).toBe(""); + await expect(stepsInput).toHaveValue("10"); +}); diff --git a/models/package.json b/models/package.json index 942b301..208dfdc 100644 --- a/models/package.json +++ b/models/package.json @@ -10,6 +10,7 @@ "@cfasim-ui/charts": "workspace:*", "@cfasim-ui/components": "workspace:*", "@cfasim-ui/pyodide": "workspace:*", + "@cfasim-ui/rwasm": "workspace:*", "@cfasim-ui/shared": "workspace:*", "@cfasim-ui/theme": "workspace:*", "@cfasim-ui/wasm": "workspace:*", diff --git a/models/playwright.config.ts b/models/playwright.config.ts index 50bd0bb..29afd52 100644 --- a/models/playwright.config.ts +++ b/models/playwright.config.ts @@ -1,13 +1,18 @@ import { defineConfig } from "@playwright/test"; +const WEB_SERVER_TIMEOUT_MS = Number( + process.env.PLAYWRIGHT_WEB_SERVER_TIMEOUT_MS ?? "300000", +); + export default defineConfig({ testDir: ".", testMatch: "*.spec.ts", - timeout: 60_000, + timeout: 90_000, webServer: { - command: "pnpm exec vite --port 7300", + command: "pnpm exec vite --port 7300 --strictPort", port: 7300, reuseExistingServer: false, + timeout: WEB_SERVER_TIMEOUT_MS, }, use: { baseURL: "http://localhost:7300", diff --git a/models/src/r-example/Page.vue b/models/src/r-example/Page.vue new file mode 100644 index 0000000..15d9106 --- /dev/null +++ b/models/src/r-example/Page.vue @@ -0,0 +1,37 @@ + + + diff --git a/models/src/r-example/model/DESCRIPTION b/models/src/r-example/model/DESCRIPTION new file mode 100644 index 0000000..c15e120 --- /dev/null +++ b/models/src/r-example/model/DESCRIPTION @@ -0,0 +1,7 @@ +Package: r.example +Title: cfasim R Example +Version: 0.0.1 +Description: Minimal cfasim R model example. +License: CC0 +Encoding: UTF-8 +Imports: cfasim diff --git a/models/src/r-example/model/NAMESPACE b/models/src/r-example/model/NAMESPACE new file mode 100644 index 0000000..72c0957 --- /dev/null +++ b/models/src/r-example/model/NAMESPACE @@ -0,0 +1,2 @@ +export(simulate) +import(cfasim) diff --git a/models/src/r-example/model/R/model.R b/models/src/r-example/model/R/model.R new file mode 100644 index 0000000..57f8c8e --- /dev/null +++ b/models/src/r-example/model/R/model.R @@ -0,0 +1,10 @@ +simulate <- function(steps, rate) { + time <- seq(0, steps - 1) + values <- time * rate + model_outputs( + series = model_output( + time = f64(time), + values = f64(values) + ) + ) +} diff --git a/models/src/router.ts b/models/src/router.ts index 4879257..4d25308 100644 --- a/models/src/router.ts +++ b/models/src/router.ts @@ -21,6 +21,12 @@ export const models = [ description: "Simple simulation model running via Pyodide", component: () => import("./python-example/Page.vue"), }, + { + path: "/r-example", + name: "R Example", + description: "Simple simulation model running via webR", + component: () => import("./r-example/Page.vue"), + }, { path: "/fetch-example", name: "Fetch Example", diff --git a/models/vite.config.ts b/models/vite.config.ts index f3663af..9419758 100644 --- a/models/vite.config.ts +++ b/models/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import { cfasimWasm } from "@cfasim-ui/wasm/vite"; import { cfasimPyodide } from "@cfasim-ui/pyodide/vite"; +import { cfasimRwasm } from "@cfasim-ui/rwasm/vite"; export default defineConfig({ base: process.env.BASE_URL || "/", @@ -13,5 +14,9 @@ export default defineConfig({ model: "src/python-example/model", pypiDeps: ["cfasim-model"], }), + cfasimRwasm({ + model: "src/r-example/model", + name: "r_example", + }), ], }); diff --git a/plz.toml b/plz.toml index bc9d26b..9341ceb 100644 --- a/plz.toml +++ b/plz.toml @@ -105,7 +105,7 @@ run = "pnpm exec vite build" # E2E tests for models [taskgroup.models.test] -depends = ["build"] +depends = ["models.build"] run = "pnpm exec playwright test --config playwright.config.ts" git_hook = "pre-push" From 174a4fb512a5b845faac3b954ca02d6bea54eae8 Mon Sep 17 00:00:00 2001 From: "Beau B. Bruce" Date: Tue, 23 Jun 2026 16:00:12 -0400 Subject: [PATCH 5/5] docs(rwasm): document R project workflow --- docs/cfasim-ui/rwasm.md | 5 ++ docs/getting-started.md | 8 ++- docs/guide/r.md | 133 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 docs/guide/r.md diff --git a/docs/cfasim-ui/rwasm.md b/docs/cfasim-ui/rwasm.md index d8fad98..7adf49f 100644 --- a/docs/cfasim-ui/rwasm.md +++ b/docs/cfasim-ui/rwasm.md @@ -54,3 +54,8 @@ The `cfasim` package provides: - `enum(indices, labels)` These helpers create the same structured model-output contract used by the Python `cfasim_model` module and Rust `cfasim-model` crate. + +For simple models, `cfasimRwasm` can also source a loose `model.R` file when no +R package is present. Put `library(cfasim)` in that script before using the +helper functions. Prefer the package structure for models with dependencies, +exports, tests, or anything you expect to maintain beyond a small example. diff --git a/docs/getting-started.md b/docs/getting-started.md index 5d797c7..721a8e6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,6 @@ # Getting Started -cfasim-ui is a Vue 3 component library for building interactive simulations. It runs your model — written in Python or Rust — directly in the browser using Web Workers. +cfasim-ui is a Vue 3 component library for building interactive simulations. It runs your model — written in Python, Rust, or R — directly in the browser using Web Workers. ## Create a new project @@ -63,7 +63,7 @@ And finally, create a new project: cfasim init ``` -`cfasim init` prompts for a project name and language (Python or Rust), then generates a Vite + Vue app wired up to run your model in the browser. +`cfasim init` prompts for a project name and language (Python, Rust, or R), then generates a Vite + Vue app wired up to run your model in the browser. ## Packages @@ -74,6 +74,7 @@ cfasim init | `@cfasim-ui/theme` | Design tokens, reset, and utility classes | | `@cfasim-ui/pyodide` | Run Python models in the browser via Pyodide | | `@cfasim-ui/wasm` | Run Rust/WASM models in the browser | +| `@cfasim-ui/rwasm` | Run bundled R models in the browser via webR | ## Choose your guide @@ -81,5 +82,6 @@ Follow the guide for the language your model is written in: - **[Python Projects](./guide/python)** — Vite + Vue + Pyodide. Write your model in Python, build it as a wheel, and run it in the browser. - **[Rust Projects](./guide/rust)** — Vite + Vue + WebAssembly. Write your model in Rust, compile to WASM, and run it in the browser. +- **[R Projects](./guide/r)** — Vite + Vue + webR. Write your model in R, bundle dependencies with rwasm, and run it in the browser. -Both guides walk through project setup, model creation, Vite configuration, and wiring up the UI from scratch. +These guides walk through project setup, model creation, Vite configuration, and wiring up the UI from scratch. diff --git a/docs/guide/r.md b/docs/guide/r.md new file mode 100644 index 0000000..5598412 --- /dev/null +++ b/docs/guide/r.md @@ -0,0 +1,133 @@ +# R Projects + +This guide walks through setting up a cfasim-ui project that runs an R model in the browser with [webR](https://docs.r-wasm.org/webr/latest/) and bundles R package assets with rwasm. + +## Prerequisites + +- [Node.js](https://nodejs.org/) v24+ +- [pnpm](https://pnpm.io/) v10+ (enabled via `corepack enable`) +- Docker, for building browser-compatible R package assets + +## Model Package + +R models are ordinary minimal R packages. In a scaffolded project the model package lives at the project root; in the examples app it lives under `models/src/r-example/model`. + +```text +model/ +├── DESCRIPTION +├── NAMESPACE +└── R/ + └── model.R +``` + +Add the helper package to `DESCRIPTION`: + +```text +Imports: cfasim +``` + +Export callable model functions in `NAMESPACE`: + +```r +export(simulate) +import(cfasim) +``` + +Then define those functions in `model/R/model.R` or other `.R` files in the +`model/R` directory: + +```r +simulate <- function(steps, rate) { + time <- seq(0, steps - 1) + values <- time * rate + model_outputs( + series = model_output( + time = f64(time), + values = f64(values) + ) + ) +} +``` + +The model package is built and installed into webR, then the frontend calls the exported function by name. Internal helper functions can stay unexported. + +For very small models, the Vite plugin can also source a loose `model.R` file +from the model directory instead of loading an R package. In that case, put +`library(cfasim)` at the top of the script and define callable functions +directly: + +```r +library(cfasim) + +simulate <- function(steps, rate) { + time <- seq(0, steps - 1) + values <- time * rate + model_outputs( + series = model_output( + time = f64(time), + values = f64(values) + ) + ) +} +``` + +The package structure is still preferred for most projects because it gives you +normal R dependency metadata, exported function declarations, and model unit +tests. + +## Vite + +`cfasim-ui/rwasm/vite` provides a Vite plugin that copies the model package, builds the local `cfasim` helper package and model package with rwasm, and writes static assets under `public/rwasm/{name}/`. + +```ts +import { cfasimRwasm } from "cfasim-ui/rwasm/vite"; + +plugins: [ + vue(), + cfasimRwasm({ + model: "..", + name: "my_model", + docker: true, + }), +]; +``` + +`name` is the browser bundle name used by `useModel`. The plugin includes the local `cfasim` helper package and model package automatically. Use `packages` for additional external R dependencies: + +```ts +cfasimRwasm({ + model: "..", + name: "my_model", + packages: ["jsonlite"], +}); +``` + +Docker is required at build time. Runtime deployment is static. + +## UI + +```ts +import { useModel } from "cfasim-ui/rwasm"; + +const { useOutputs } = useModel("my_model"); +const { outputs, loading, error } = useOutputs("simulate", params); +``` + +`useModel("my_model")` must match the Vite plugin `name`. `useOutputs("simulate", params)` watches the reactive params object, calls the exported R function, and returns named `ModelOutput` tables for charts and tables. + +## Run It + +From the project root: + +```bash +pnpm install +pnpm run dev +``` + +The Vite plugin builds the R package assets on startup. `pnpm run build` produces a static site in `dist/`. + +By default the plugin also copies the matching `webR` runtime files into the +app bundle and points the generated manifest at those local assets. That keeps +generated projects portable and avoids depending on the public webR CDN at app +startup. `webRBaseUrl` remains available when you want to override that with a +self-hosted location.