move export to the rust side to make the interface way more clean (#5855)

* move export

Signed-off-by: Jess Frazelle <github@jessfraz.com>

testing

Signed-off-by: Jess Frazelle <github@jessfraz.com>

remove debugs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fix main

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fices

Signed-off-by: Jess Frazelle <github@jessfraz.com>

get rid of logs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Convert async actions anti-pattern to fromPromise actors

* Fix tsc by removing a generic type

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update rustContext.ts

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove weird file

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
This commit is contained in:
Jess Frazelle
2025-03-18 20:25:51 -07:00
committed by GitHub
parent 3b1d1307c4
commit 859bfc7b28
20 changed files with 550 additions and 421 deletions

View File

@ -405,8 +405,9 @@ extrude001 = extrude(sketch001, length = 50)
await expect(successToastMessage).toBeVisible()
}
)
// We updated this test such that you can have multiple exports going at once.
test(
'ensure you can not export while an export is already going',
'ensure you CAN export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] },
async ({ page, homePage }) => {
const u = await getUtils(page)
@ -441,22 +442,13 @@ extrude001 = extrude(sketch001, length = 50)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
const successToastMessage = page.getByText(`Exported successfully`)
await test.step('Blocked second export', async () => {
await test.step('second export', async () => {
await clickExportButton(page)
await expect(exportingToastMessage).toBeVisible()
await clickExportButton(page)
await test.step('The second export is blocked', async () => {
// Find the toast.
// Look out for the toast message
await Promise.all([
expect(exportingToastMessage.first()).toBeVisible(),
expect(alreadyExportingToastMessage).toBeVisible(),
])
})
await test.step('The first export still succeeds', async () => {
await Promise.all([
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
@ -486,7 +478,7 @@ extrude001 = extrude(sketch001, length = 50)
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toBeVisible()
await expect(successToastMessage).toHaveCount(2)
})
}
)

View File

@ -36,6 +36,7 @@
"@xstate/inspect": "^0.8.0",
"@xstate/react": "^4.1.1",
"bonjour-service": "^1.3.0",
"bson": "^6.10.3",
"chokidar": "^4.0.1",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
@ -212,6 +213,7 @@
"typescript-eslint": "^8.26.1",
"vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0",
"vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.1",
"vitest-webgl-canvas-mock": "^1.1.0",

1
rust/Cargo.lock generated
View File

@ -1982,6 +1982,7 @@ dependencies = [
"js-sys",
"kcl-lib",
"kittycad",
"kittycad-modeling-cmds",
"serde_json",
"tokio",
"toml",

View File

@ -30,6 +30,7 @@ debug = "line-tables-only"
[workspace.dependencies]
async-trait = "0.1.85"
anyhow = { version = "1" }
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
clap = { version = "4.5.31", features = ["derive"] }
dashmap = { version = "6.1.0" }
http = "1"

View File

@ -24,6 +24,7 @@ anyhow = { workspace = true, features = ["backtrace"] }
async-recursion = "1.1.1"
async-trait = { workspace = true }
base64 = "0.22.1"
bson = { workspace = true }
chrono = "0.4.38"
clap = { version = "4.5.27", default-features = false, optional = true, features = [
"std",
@ -95,7 +96,6 @@ wasm-bindgen-futures = "0.4.49"
web-sys = { version = "0.3.76", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
tokio = { workspace = true, features = ["full"] }
tokio-tungstenite = { version = "0.24.0", features = [
"rustls-tls-native-roots",

View File

@ -104,23 +104,29 @@ impl EngineConnection {
})?;
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range],
})
// Try to parse the error as an engine error.
let err_str = e.as_string().unwrap_or_default();
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
serde_json::from_str(&err_str)
{
KclError::Engine(KclErrorDetails {
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
source_ranges: vec![source_range],
})
} else {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from send modeling command: {:?}", e),
source_ranges: vec![source_range],
})
}
})?;
// Parse the value as a string.
let s = value.as_string().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get string from response from engine: `{:?}`", value),
source_ranges: vec![source_range],
})
})?;
// Convert JsValue to a Uint8Array
let data = js_sys::Uint8Array::from(value);
let ws_result: WebSocketResponse = serde_json::from_str(&s).map_err(|e| {
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to deserialize response from engine: {:?}", e),
message: format!("Failed to deserialize bson response from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;

View File

@ -11,7 +11,7 @@ crate-type = ["cdylib"]
bench = false
[target.'cfg(target_arch = "wasm32")'.dependencies]
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
bson = { workspace = true, features = ["uuid-1", "chrono"] }
console_error_panic_hook = "0.1.7"
data-encoding = "2.6.0"
futures = "0.3.31"
@ -22,6 +22,7 @@ gloo-utils = "0.2.0"
js-sys = "0.3.72"
kcl-lib = { path = "../kcl-lib" }
kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
toml = "0.8.19"

View File

@ -106,4 +106,22 @@ impl Context {
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
}
}
/// Export a scene to a file.
#[wasm_bindgen]
pub async fn export(&self, format_json: &str, settings: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let format: kittycad_modeling_cmds::format::OutputFormat3d =
serde_json::from_str(format_json).map_err(|e| e.to_string())?;
let ctx = self.create_executor_ctx(settings, None, false)?;
match ctx.export(format).await {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
}
}
}

View File

@ -18,25 +18,6 @@ pub async fn kcl_lint(program_ast_json: &str) -> Result<JsValue, JsValue> {
Ok(JsValue::from_serde(&findings).map_err(|e| e.to_string())?)
}
#[wasm_bindgen]
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
console_error_panic_hook::set_once();
let ws_resp: kittycad::types::WebSocketResponse = bson::from_slice(data)?;
if let Some(success) = ws_resp.success {
if !success {
return Err(JsError::new(&format!("Server returned error: {:?}", ws_resp.errors)));
}
}
if let Some(kittycad::types::OkWebSocketResponseData::Export { files }) = ws_resp.resp {
return Ok(JsValue::from_serde(&files)?);
}
Err(JsError::new(&format!("Invalid response type, got: {:?}", ws_resp)))
}
#[wasm_bindgen]
pub fn parse_wasm(kcl_program_source: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();

View File

@ -27,12 +27,6 @@ export const ModelStateIndicator = () => {
name="checkmark"
/>
)
} else if (lastCommandType === 'export-done') {
className +=
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
icon = (
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
)
}
return (

View File

@ -8,7 +8,6 @@ import React, {
} from 'react'
import {
Actor,
AnyStateMachine,
ContextFrom,
Prop,
SnapshotFrom,
@ -33,8 +32,12 @@ import {
codeManager,
editorManager,
sceneEntitiesManager,
rustContext,
} from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import {
MachineManager,
MachineManagerContext,
} from 'components/MachineManagerProvider'
import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
@ -53,7 +56,10 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import {
ModelingCommandSchema,
modelingMachineCommandConfig,
} from 'lib/commandBarConfigs/modelingCommandConfig'
import {
SEGMENT_BODIES,
getParentGroup,
@ -84,21 +90,17 @@ import {
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap, reject } from 'lib/trap'
import {
ExportIntent,
EngineConnectionStateType,
EngineConnectionEvents,
} from 'lang/std/engineConnection'
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext'
import { platform, uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { Node } from '@rust/kcl-lib/bindings/Node'
import {
getFaceCodeRef,
@ -111,15 +113,18 @@ import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { useSettings } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<Actor<T>, 'send'>
}
import { IndexLoaderData } from 'lib/types'
import { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd'
import { EXPORT_TOAST_MESSAGES, MAKE_TOAST_MESSAGES } from 'lib/constants'
import { exportMake } from 'lib/exportMake'
import { exportSave } from 'lib/exportSave'
export const ModelingMachineContext = createContext(
{} as MachineContext<typeof modelingMachine>
{} as {
state: StateFrom<typeof modelingMachine>
context: ContextFrom<typeof modelingMachine>
send: Prop<Actor<typeof modelingMachine>, 'send'>
}
)
const commandBarIsClosedSelector = (
@ -524,118 +529,6 @@ export const ModelingMachineProvider = ({
return {}
}
),
Make: ({ context, event }) => {
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportInfo) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportInfo = {
intent: ExportIntent.Make,
name: file?.name || '',
}
// Set the current machine.
// Due to our use of singeton pattern, we need to do this to reliably
// update this object across React and non-React boundary.
// We need to do this eagerly because of the exportToEngine call below.
if (engineCommandManager.machineManager === null) {
console.warn(
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
)
return
} else {
engineCommandManager.machineManager.currentMachine =
event.data.machine
}
// Update the rest of the UI that needs to know the current machine
context.machineManager.setCurrentMachine(event.data.machine)
const format: Models['OutputFormat_type'] = {
type: 'stl',
coords: {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
},
storage: 'ascii',
// Convert all units to mm since that is what the slicer expects.
units: 'mm',
selection: { type: 'default_scene' },
}
exportFromEngine({
format: format,
}).catch(reportRejection)
},
'Engine export': ({ event }) => {
if (event.type !== 'Export') return
if (engineCommandManager.exportInfo) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportInfo = {
intent: ExportIntent.Save,
// This never gets used its only for make.
name: file?.name?.replace('.kcl', `.${event.data.type}`) || '',
}
const format = {
...event.data,
} as Partial<Models['OutputFormat_type']>
// Set all the un-configurable defaults here.
if (format.type === 'gltf') {
format.presentation = 'pretty'
}
if (
format.type === 'obj' ||
format.type === 'ply' ||
format.type === 'step' ||
format.type === 'stl'
) {
// Set the default coords.
// In the future we can make this configurable.
// But for now, its probably best to keep it consistent with the
// UI.
format.coords = {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
}
}
if (
format.type === 'obj' ||
format.type === 'stl' ||
format.type === 'ply'
) {
format.units = defaultUnit.current
}
if (format.type === 'ply' || format.type === 'stl') {
format.selection = { type: 'default_scene' }
}
exportFromEngine({
format: format as Models['OutputFormat_type'],
}).catch(reportRejection)
},
'Submit to Text-to-CAD API': ({ event }) => {
if (event.type !== 'Text-to-CAD') return
const trimmedPrompt = event.data.prompt.trim()
@ -696,14 +589,155 @@ export const ModelingMachineProvider = ({
else if (kclManager.ast.body.length === 0)
errorMessage += 'due to Empty Scene'
console.error(errorMessage)
toast.error(errorMessage, {
id: kclManager.engineCommandManager.pendingExport?.toastId,
})
toast.error(errorMessage)
return false
}
},
},
actors: {
exportFromEngine: fromPromise(
async ({ input }: { input?: ModelingCommandSchema['Export'] }) => {
if (!input) {
return new Error('No input provided')
}
let fileName = file?.name?.replace('.kcl', `.${input.type}`) || ''
console.log('fileName', fileName)
// Ensure the file has an extension.
if (!fileName.includes('.')) {
fileName += `.${input.type}`
}
const format = {
...input,
} as Partial<OutputFormat3d>
// Set all the un-configurable defaults here.
if (format.type === 'gltf') {
format.presentation = 'pretty'
}
if (
format.type === 'obj' ||
format.type === 'ply' ||
format.type === 'step' ||
format.type === 'stl'
) {
// Set the default coords.
// In the future we can make this configurable.
// But for now, its probably best to keep it consistent with the
// UI.
format.coords = {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
}
}
if (
format.type === 'obj' ||
format.type === 'stl' ||
format.type === 'ply'
) {
format.units = defaultUnit.current
}
if (format.type === 'ply' || format.type === 'stl') {
format.selection = { type: 'default_scene' }
}
const toastId = toast.loading(EXPORT_TOAST_MESSAGES.START)
const files = await rustContext.export(
format,
{
settings: { modeling: { base_unit: defaultUnit.current } },
},
toastId
)
if (files === undefined) {
// We already sent the toast message in the export function.
return
}
await exportSave({ files, toastId, fileName })
}
),
makeFromEngine: fromPromise(
async ({
input,
}: {
input?: {
machineManager: MachineManager
} & ModelingCommandSchema['Make']
}) => {
if (input === undefined) {
return new Error('No input provided')
}
const name = file?.name || ''
// Set the current machine.
// Due to our use of singeton pattern, we need to do this to reliably
// update this object across React and non-React boundary.
// We need to do this eagerly because of the exportToEngine call below.
if (engineCommandManager.machineManager === null) {
console.warn(
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
)
return new Error('Machine manager is not set')
} else {
engineCommandManager.machineManager.currentMachine = input.machine
}
// Update the rest of the UI that needs to know the current machine
input.machineManager.setCurrentMachine(input.machine)
const format: OutputFormat3d = {
type: 'stl',
coords: {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
},
storage: 'ascii',
// Convert all units to mm since that is what the slicer expects.
units: 'mm',
selection: { type: 'default_scene' },
}
const toastId = toast.loading(MAKE_TOAST_MESSAGES.START)
const files = await rustContext.export(
format,
{
settings: { modeling: { base_unit: 'mm' } },
},
toastId
)
if (files === undefined) {
// We already sent the toast message in the export function.
return
}
await exportMake({
files,
toastId,
name,
machineManager: engineCommandManager.machineManager,
})
}
),
'AST-undo-startSketchOn': fromPromise(
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return

View File

@ -6,8 +6,8 @@ import {
} from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { deferExecution, isOverlap, uuidv4 } from 'lib/utils'
import { BSON, Binary as BSONBinary } from 'bson'
import {
Themes,
getThemeColorForEngine,
@ -16,14 +16,8 @@ import {
} from 'lib/theme'
import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph'
import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
import {
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE,
EXPORT_TOAST_MESSAGES,
MAKE_TOAST_MESSAGES,
} from 'lib/constants'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance'
@ -47,16 +41,6 @@ interface NewTrackArgs {
mediaStream: MediaStream
}
export enum ExportIntent {
Save = 'save',
Make = 'make',
}
export interface ExportInfo {
intent: ExportIntent
name: string
}
type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics {
@ -1069,18 +1053,6 @@ class EngineConnection extends EventTarget {
`Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.type}`
)
// Check if this was a pending export command.
if (
this.engineCommandManager.pendingExport?.commandId ===
message.request_id
) {
// Reject the promise with the error.
this.engineCommandManager.pendingExport.reject(errorsString)
toast.error(errorsString, {
id: this.engineCommandManager.pendingExport.toastId,
})
this.engineCommandManager.pendingExport = undefined
}
} else {
console.error(`Error from server:\n${errorsString}`)
}
@ -1365,10 +1337,6 @@ export type CommandLog =
type: 'execution-done'
data: null
}
| {
type: 'export-done'
data: null
}
export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run
@ -1432,16 +1400,6 @@ export class EngineCommandManager extends EventTarget {
inSequence = 1
engineConnection?: EngineConnection
commandLogs: CommandLog[] = []
pendingExport?: {
/** The id of the shared loading/success/error toast for export */
toastId: string
/** An on-success callback */
resolve: (a: null) => void
/** An on-error callback */
reject: (reason: string) => void
/** The engine command uuid */
commandId: string
}
settings: SettingsViaQueryString
streamDimensions = {
@ -1452,12 +1410,6 @@ export class EngineCommandManager extends EventTarget {
elVideo: HTMLVideoElement | null = null
/**
* Export intent tracks the intent of the export. If it is null there is no
* export in progress. Otherwise it is an enum value of the intent.
* Another export cannot be started if one is already in progress.
*/
private _exportInfo: ExportInfo | null = null
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
subscriptions: {
@ -1509,14 +1461,6 @@ export class EngineCommandManager extends EventTarget {
// The current "manufacturing machine" aka 3D printer, CNC, etc.
public machineManager: MachineManager | null = null
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
}
get exportInfo() {
return this._exportInfo
}
start({
setMediaStream,
setIsStreamReady,
@ -1691,64 +1635,33 @@ export class EngineCommandManager extends EventTarget {
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
let message: Models['WebSocketResponse_type'] | null = null
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
if (this.exportInfo === null || this.pendingExport === undefined) {
toast.error(
'Export intent was not set, but export data was received'
)
console.error(
'Export intent was not set, but export data was received'
)
return
// BSON deserialize the command.
message = BSON.deserialize(
new Uint8Array(event.data)
) as Models['WebSocketResponse_type']
// The request id comes back as binary and we want to get the uuid
// string from that.
if (message.request_id) {
message.request_id = binaryToUuid(message.request_id)
}
} else {
message = JSON.parse(event.data)
}
switch (this.exportInfo.intent) {
case ExportIntent.Save: {
exportSave({
data: event.data,
fileName: this.exportInfo.name,
toastId: this.pendingExport.toastId,
}).then(() => {
this.pendingExport?.resolve(null)
}, this.pendingExport?.reject)
break
}
case ExportIntent.Make: {
if (!this.machineManager) {
console.warn('Some how, no manufacturing machine is selected.')
break
}
exportMake(
event.data,
this.exportInfo.name,
this.pendingExport.toastId,
this.machineManager
).then((result) => {
if (result) {
this.pendingExport?.resolve(null)
} else {
this.pendingExport?.reject('Failed to make export')
}
}, this.pendingExport?.reject)
break
}
}
// Set the export intent back to null.
this.exportInfo = null
if (message === null) {
// We should never get here.
console.error('Received a null message from the engine', event)
return
}
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
const pending = this.pendingCommands[message.request_id || '']
if (pending && !message.success) {
// handle bad case
pending.reject(`engine error: ${JSON.stringify(message.errors)}`)
pending.reject(JSON.stringify(message))
delete this.pendingCommands[message.request_id || '']
}
if (
@ -1756,12 +1669,15 @@ export class EngineCommandManager extends EventTarget {
pending &&
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch')
message.resp.type === 'modeling_batch' ||
message.resp.type === 'export')
)
)
return
if (
if (message.resp.type === 'export' && message.request_id) {
this.responseMap[message.request_id] = message.resp
} else if (
message.resp.type === 'modeling' &&
pending.command.type === 'modeling_cmd_req' &&
message.request_id
@ -2026,38 +1942,6 @@ export class EngineCommandManager extends EventTarget {
this.outSequence++
this.engineConnection?.unreliableSend(command)
return Promise.resolve(null)
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
if (this.exportInfo === null) {
if (this.exportInfo === null) {
toast.error('Export intent was not set, but export is being sent')
console.error('Export intent was not set, but export is being sent')
return
}
}
const toastId = toast.loading(
this.exportInfo.intent === ExportIntent.Save
? EXPORT_TOAST_MESSAGES.START
: MAKE_TOAST_MESSAGES.START
)
this.pendingExport = {
toastId,
resolve: (passThrough) => {
this.addCommandLog({
type: 'export-done',
data: null,
})
resolve(passThrough)
},
reject: (reason: string) => {
this.exportInfo = null
reject(reason)
},
commandId: command.cmd_id,
}
})
this.engineConnection?.send(command)
return promise
}
if (
command.cmd.type === 'default_camera_look_at' ||
@ -2090,7 +1974,7 @@ export class EngineCommandManager extends EventTarget {
rangeStr: string,
commandStr: string,
idToRangeStr: string
): Promise<string | void> {
): Promise<Uint8Array | void> {
if (this.engineConnection === undefined) return Promise.resolve()
if (
!this.engineConnection?.isReady() &&
@ -2118,7 +2002,7 @@ export class EngineCommandManager extends EventTarget {
range,
idToRangeMap,
})
return JSON.stringify(resp[0])
return BSON.serialize(resp[0])
}
/**
* Common send command function used for both modeling and scene commands
@ -2266,3 +2150,65 @@ function promiseFactory<T>() {
})
return { promise, resolve, reject }
}
/**
* Converts a binary buffer to a UUID string.
*
* @param buffer - The binary buffer containing the UUID bytes.
* @returns A string representation of the UUID in the format 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.
*/
function binaryToUuid(
binaryData: Buffer | Uint8Array | BSONBinary | string
): string {
if (typeof binaryData === 'string') {
return binaryData
}
let buffer: Uint8Array
// Handle MongoDB BSON Binary object
if (
binaryData &&
'_bsontype' in binaryData &&
binaryData._bsontype === 'Binary'
) {
// Extract the buffer from the BSON Binary object
buffer = binaryData.buffer
}
// Handle case where buffer property exists (some MongoDB drivers structure)
else if (binaryData && binaryData.buffer instanceof Uint8Array) {
buffer = binaryData.buffer
}
// Handle direct Buffer or Uint8Array
else if (binaryData instanceof Uint8Array || Buffer.isBuffer(binaryData)) {
buffer = binaryData
} else {
console.error(
'Invalid input type: expected MongoDB BSON Binary, Buffer, or Uint8Array'
)
return ''
}
// Ensure we have exactly 16 bytes (128 bits) for a UUID
if (buffer.length !== 16) {
// For debugging
console.log('Buffer length:', buffer.length)
console.log('Buffer content:', Array.from(buffer))
console.error('UUID must be exactly 16 bytes')
return ''
}
// Convert each byte to a hex string and pad with zeros if needed
const hexValues = Array.from(buffer).map((byte) =>
byte.toString(16).padStart(2, '0')
)
// Format into UUID structure (8-4-4-4-12 characters)
return [
hexValues.slice(0, 4).join(''),
hexValues.slice(4, 6).join(''),
hexValues.slice(6, 8).join(''),
hexValues.slice(8, 10).join(''),
hexValues.slice(10, 16).join(''),
].join('-')
}

View File

@ -1,26 +0,0 @@
import { engineCommandManager } from 'lib/singletons'
import { type Models } from '@kittycad/lib'
import { uuidv4 } from 'lib/utils'
// Isolating a function to call the engine to export the current scene.
// Because it has given us trouble in automated testing environments.
export async function exportFromEngine({
format,
}: {
format: Models['OutputFormat_type']
}): Promise<Models['WebSocketResponse_type'] | null> {
let exportPromise = engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'export',
// By default let's leave this blank to export the whole scene.
// In the future we might want to let the user choose which entities
// in the scene to export. In that case, you'd pass the IDs thru here.
entity_ids: [],
format,
},
cmd_id: uuidv4(),
})
return exportPromise
}

View File

@ -1,4 +1,3 @@
import { deserialize_files } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast'
import { components } from './machine-api'
@ -6,12 +5,17 @@ import ModelingAppFile from './modelingAppFile'
import { MAKE_TOAST_MESSAGES } from './constants'
// Make files locally from an export call.
export async function exportMake(
data: ArrayBuffer,
name: string,
toastId: string,
export async function exportMake({
files,
name,
toastId,
machineManager,
}: {
files: ModelingAppFile[]
name: string
toastId: string
machineManager: MachineManager
): Promise<Response | null> {
}): Promise<Response | null> {
if (name === '') {
console.error(MAKE_TOAST_MESSAGES.NO_NAME)
toast.error(MAKE_TOAST_MESSAGES.NO_NAME, { id: toastId })
@ -50,10 +54,8 @@ export async function exportMake(
job_name: name,
}
try {
console.log('params', params)
const formData = new FormData()
formData.append('params', JSON.stringify(params))
let files: ModelingAppFile[] = deserialize_files(new Uint8Array(data))
let file = files[0]
const fileBlob = new Blob([new Uint8Array(file.contents)], {
type: 'text/plain',

View File

@ -1,5 +1,4 @@
import { isDesktop } from './isDesktop'
import { deserialize_files } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
import { browserSaveFile } from './browserSaveFile'
import JSZip from 'jszip'
@ -78,19 +77,14 @@ const save_ = async (file: ModelingAppFile, toastId: string) => {
// Saves files locally from an export call.
// We override the file's name with one passed in from the client side.
export async function exportSave({
data,
files,
fileName,
toastId,
}: {
data: ArrayBuffer
files: ModelingAppFile[]
fileName: string
toastId: string
}) {
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
let uintArray = new Uint8Array(data)
let files: ModelingAppFile[] = deserialize_files(uintArray)
if (files.length > 1) {
let zip = new JSZip()
for (const file of files) {

View File

@ -1,5 +1,4 @@
import {
emptyExecState,
errFromErrWithOutputs,
ExecState,
execStateFromRust,
@ -17,6 +16,10 @@ import { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes'
import { DefaultPlaneStr, defaultPlaneStrToKey } from 'lib/planes'
import { err } from 'lib/trap'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd'
import ModelingAppFile from './modelingAppFile'
import toast from 'react-hot-toast'
import { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError'
export default class RustContext {
private wasmInitFailed: boolean = true
@ -43,20 +46,22 @@ export default class RustContext {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(async () => {
await this.create()
this.ctxInstance = await this.create()
})
}
// Create a new context instance
async create() {
async create(): Promise<Context> {
this.rustInstance = getModule()
// We need this await here, DO NOT REMOVE it even if your editor says it's
// unnecessary. The constructor of the module is async and it will not
// resolve if you don't await it.
this.ctxInstance = await new this.rustInstance.Context(
const ctxInstance = await new this.rustInstance.Context(
this.engineCommandManager,
fileSystemManager
)
return ctxInstance
}
// Execute a program.
@ -65,31 +70,26 @@ export default class RustContext {
settings: DeepPartial<Configuration>,
path?: string
): Promise<ExecState> {
await this._checkInstance()
const instance = await this._checkInstance()
if (this.ctxInstance) {
try {
const result = await this.ctxInstance.execute(
JSON.stringify(node),
path,
JSON.stringify(settings)
)
/* Set the default planes, safe to call after execute. */
const outcome = execStateFromRust(result, node)
try {
const result = await instance.execute(
JSON.stringify(node),
path,
JSON.stringify(settings)
)
/* Set the default planes, safe to call after execute. */
const outcome = execStateFromRust(result, node)
this._defaultPlanes = outcome.defaultPlanes
this._defaultPlanes = outcome.defaultPlanes
// Return the result.
return outcome
} catch (e: any) {
const err = errFromErrWithOutputs(e)
this._defaultPlanes = err.defaultPlanes
return Promise.reject(err)
}
// Return the result.
return outcome
} catch (e: any) {
const err = errFromErrWithOutputs(e)
this._defaultPlanes = err.defaultPlanes
return Promise.reject(err)
}
// You will never get here.
return Promise.reject(emptyExecState())
}
// Execute a program with in mock mode.
@ -99,28 +99,43 @@ export default class RustContext {
path?: string,
usePrevMemory?: boolean
): Promise<ExecState> {
await this._checkInstance()
const instance = await this._checkInstance()
if (this.ctxInstance) {
try {
if (usePrevMemory === undefined) {
usePrevMemory = true
}
const result = await this.ctxInstance.executeMock(
JSON.stringify(node),
path,
JSON.stringify(settings),
usePrevMemory
)
return mockExecStateFromRust(result)
} catch (e: any) {
return Promise.reject(errFromErrWithOutputs(e))
}
if (usePrevMemory === undefined) {
usePrevMemory = true
}
// You will never get here.
return Promise.reject(emptyExecState())
try {
const result = await instance.executeMock(
JSON.stringify(node),
path,
JSON.stringify(settings),
usePrevMemory
)
return mockExecStateFromRust(result)
} catch (e: any) {
return Promise.reject(errFromErrWithOutputs(e))
}
}
// Export a scene to a file.
async export(
format: DeepPartial<OutputFormat3d>,
settings: DeepPartial<Configuration>,
toastId: string
): Promise<ModelingAppFile[] | undefined> {
const instance = await this._checkInstance()
try {
return await instance.export(
JSON.stringify(format),
JSON.stringify(settings)
)
} catch (e: any) {
const parsed: RustKclError = JSON.parse(e.toString())
toast.error(parsed.msg, { id: toastId })
return
}
}
async waitForAllEngineCommands() {
@ -169,11 +184,13 @@ export default class RustContext {
}
// Helper to check if context instance exists
private async _checkInstance() {
private async _checkInstance(): Promise<Context> {
if (!this.ctxInstance) {
// Create the context instance.
await this.create()
this.ctxInstance = await this.create()
}
return this.ctxInstance
}
// Clean up resources

View File

@ -1283,14 +1283,12 @@ export const modelingMachine = setup({
},
}
}),
Make: () => {},
'enable copilot': () => {},
'disable copilot': () => {},
'Set selection': () => {},
'Set mouse state': () => {},
'Set Segment Overlays': () => {},
'Center camera on selection': () => {},
'Engine export': () => {},
'Submit to Text-to-CAD API': () => {},
'Set sketchDetails': () => {},
'sketch exit execute': () => {},
@ -2469,6 +2467,20 @@ export const modelingMachine = setup({
}
}
),
exportFromEngine: fromPromise(
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
return undefined as Error | undefined
}
),
makeFromEngine: fromPromise(
async ({}: {
input?: {
machineManager: MachineManager
} & ModelingCommandSchema['Make']
}) => {
return undefined as Error | undefined
}
),
},
// end actors
}).createMachine({
@ -2528,17 +2540,13 @@ export const modelingMachine = setup({
},
Export: {
target: 'idle',
reenter: false,
target: 'Exporting',
guard: 'Has exportable geometry',
actions: 'Engine export',
},
Make: {
target: 'idle',
reenter: false,
target: 'Making',
guard: 'Has exportable geometry',
actions: 'Make',
},
'Delete selection': {
@ -3885,6 +3893,35 @@ export const modelingMachine = setup({
onError: ['idle'],
},
},
Exporting: {
invoke: {
src: 'exportFromEngine',
id: 'exportFromEngine',
input: ({ event }) => {
if (event.type !== 'Export') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
Making: {
invoke: {
src: 'makeFromEngine',
id: 'makeFromEngine',
input: ({ event, context }) => {
if (event.type !== 'Make' || !context.machineManager) return undefined
return {
machineManager: context.machineManager,
...event.data,
}
},
onDone: ['idle'],
onError: ['idle'],
},
},
},
initial: 'idle',

View File

@ -3,6 +3,7 @@ import viteTsconfigPaths from 'vite-tsconfig-paths'
import eslint from '@nabla/vite-plugin-eslint'
import { defineConfig, configDefaults } from 'vitest/config'
import version from 'vite-plugin-package-version'
import topLevelAwait from 'vite-plugin-top-level-await'
// @ts-ignore: No types available
import { lezer } from '@lezer/generator/rollup'
@ -62,7 +63,19 @@ const config = defineConfig({
'@rust': '/rust',
},
},
plugins: [react(), viteTsconfigPaths(), eslint(), version(), lezer()],
plugins: [
react(),
viteTsconfigPaths(),
eslint(),
version(),
lezer(),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`,
}),
],
worker: {
plugins: () => [viteTsconfigPaths()],
},

View File

@ -2,6 +2,7 @@ import type { ConfigEnv, UserConfig } from 'vite'
import { defineConfig } from 'vite'
import { pluginExposeRenderer } from './vite.base.config'
import viteTsconfigPaths from 'vite-tsconfig-paths'
import topLevelAwait from 'vite-plugin-top-level-await'
// @ts-ignore: No types available
import { lezer } from '@lezer/generator/rollup'
@ -18,7 +19,17 @@ export default defineConfig((env) => {
build: {
outDir: `.vite/renderer/${name}`,
},
plugins: [pluginExposeRenderer(name), viteTsconfigPaths(), lezer()],
plugins: [
pluginExposeRenderer(name),
viteTsconfigPaths(),
lezer(),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`,
}),
],
worker: {
plugins: () => [viteTsconfigPaths()],
},

105
yarn.lock
View File

@ -1995,6 +1995,11 @@
dependencies:
"@codemirror/state" "^6.2.1"
"@rollup/plugin-virtual@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz#17e17eeecb4c9fa1c0a6e72c9e5f66382fddbb82"
integrity sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==
"@rollup/rollup-android-arm-eabi@4.29.1":
version "4.29.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz#9bd38df6a29afb7f0336d988bc8112af0c8816c0"
@ -2105,6 +2110,87 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
"@swc/core-darwin-arm64@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.11.tgz#e4b5fc99bab657f8f72217fd4976956faf4132b3"
integrity sha512-vJcjGVDB8cZH7zyOkC0AfpFYI/7GHKG0NSsH3tpuKrmoAXJyCYspKPGid7FT53EAlWreN7+Pew+bukYf5j+Fmg==
"@swc/core-darwin-x64@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.11.tgz#0f4e810a2cd9c2993a7ccc3b38d1f92ef49894d8"
integrity sha512-/N4dGdqEYvD48mCF3QBSycAbbQd3yoZ2YHSzYesQf8usNc2YpIhYqEH3sql02UsxTjEFOJSf1bxZABDdhbSl6A==
"@swc/core-linux-arm-gnueabihf@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.11.tgz#72b4b1e403bca37f051fd194eb0518cda83fad9f"
integrity sha512-hsBhKK+wVXdN3x9MrL5GW0yT8o9GxteE5zHAI2HJjRQel3HtW7m5Nvwaq+q8rwMf4YQRd8ydbvwl4iUOZx7i2Q==
"@swc/core-linux-arm64-gnu@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.11.tgz#ea87e183ec53db9e121cca581cef538e9652193f"
integrity sha512-YOCdxsqbnn/HMPCNM6nrXUpSndLXMUssGTtzT7ffXqr7WuzRg2e170FVDVQFIkb08E7Ku5uOnnUVAChAJQbMOQ==
"@swc/core-linux-arm64-musl@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.11.tgz#33db0f45b2286bbca9baf2ed84d1f2405c657600"
integrity sha512-nR2tfdQRRzwqR2XYw9NnBk9Fdvff/b8IiJzDL28gRR2QiJWLaE8LsRovtWrzCOYq6o5Uu9cJ3WbabWthLo4jLw==
"@swc/core-linux-x64-gnu@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.11.tgz#4a1fe41baa968008bb0fffc7754fd6ee824e76e1"
integrity sha512-b4gBp5HA9xNWNC5gsYbdzGBJWx4vKSGybGMGOVWWuF+ynx10+0sA/o4XJGuNHm8TEDuNh9YLKf6QkIO8+GPJ1g==
"@swc/core-linux-x64-musl@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.11.tgz#972d3530d740b3681191590ee08bb9ab7bb6706d"
integrity sha512-dEvqmQVswjNvMBwXNb8q5uSvhWrJLdttBSef3s6UC5oDSwOr00t3RQPzyS3n5qmGJ8UMTdPRmsopxmqaODISdg==
"@swc/core-win32-arm64-msvc@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.11.tgz#179846f1f9e3e806a4bf6d8f35af97f577c1a0b3"
integrity sha512-aZNZznem9WRnw2FbTqVpnclvl8Q2apOBW2B316gZK+qxbe+ktjOUnYaMhdCG3+BYggyIBDOnaJeQrXbKIMmNdw==
"@swc/core-win32-ia32-msvc@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.11.tgz#b098b72c1b45e237a9598b7b5e83e6c5ecb9ac69"
integrity sha512-DjeJn/IfjgOddmJ8IBbWuDK53Fqw7UvOz7kyI/728CSdDYC3LXigzj3ZYs4VvyeOt+ZcQZUB2HA27edOifomGw==
"@swc/core-win32-x64-msvc@1.11.11":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.11.tgz#1d5610c585b903b8c1f4a452725d77ac96f27e84"
integrity sha512-Gp/SLoeMtsU4n0uRoKDOlGrRC6wCfifq7bqLwSlAG8u8MyJYJCcwjg7ggm0rhLdC2vbiZ+lLVl3kkETp+JUvKg==
"@swc/core@^1.10.16":
version "1.11.11"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.11.tgz#bac3256d7a113f0dd6965206cf428e826981cf0d"
integrity sha512-pCVY2Wn6dV/labNvssk9b3Owi4WOYsapcbWm90XkIj4xH/56Z6gzja9fsU+4MdPuEfC2Smw835nZHcdCFGyX6A==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.19"
optionalDependencies:
"@swc/core-darwin-arm64" "1.11.11"
"@swc/core-darwin-x64" "1.11.11"
"@swc/core-linux-arm-gnueabihf" "1.11.11"
"@swc/core-linux-arm64-gnu" "1.11.11"
"@swc/core-linux-arm64-musl" "1.11.11"
"@swc/core-linux-x64-gnu" "1.11.11"
"@swc/core-linux-x64-musl" "1.11.11"
"@swc/core-win32-arm64-msvc" "1.11.11"
"@swc/core-win32-ia32-msvc" "1.11.11"
"@swc/core-win32-x64-msvc" "1.11.11"
"@swc/counter@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/types@^0.1.19":
version "0.1.19"
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.19.tgz#65d9fe81e0a1dc7e861ad698dd581abe3703a2d2"
integrity sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==
dependencies:
"@swc/counter" "^0.1.3"
"@szmarczak/http-timer@^4.0.5":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807"
@ -3284,6 +3370,11 @@ browserslist@^4.24.0, browserslist@^4.24.4:
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
bson@^6.10.3:
version "6.10.3"
resolved "https://registry.yarnpkg.com/bson/-/bson-6.10.3.tgz#5f9a463af6b83e264bedd08b236d1356a30eda47"
integrity sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@ -9249,6 +9340,11 @@ utrie@^1.0.2:
dependencies:
base64-arraybuffer "^1.0.2"
uuid@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
uuid@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
@ -9297,6 +9393,15 @@ vite-plugin-package-version@^1.1.0:
resolved "https://registry.yarnpkg.com/vite-plugin-package-version/-/vite-plugin-package-version-1.1.0.tgz#7d8088955aa21e4ec93353c98992b3f58c4bf13c"
integrity sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==
vite-plugin-top-level-await@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.5.0.tgz#e3f76302921152bf29d1658f169d168f8937e78b"
integrity sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ==
dependencies:
"@rollup/plugin-virtual" "^3.0.2"
"@swc/core" "^1.10.16"
uuid "^10.0.0"
vite-tsconfig-paths@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"