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() await expect(successToastMessage).toBeVisible()
} }
) )
// We updated this test such that you can have multiple exports going at once.
test( 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'] }, { tag: ['@skipLinux', '@skipWin'] },
async ({ page, homePage }) => { async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -441,22 +442,13 @@ extrude001 = extrude(sketch001, length = 50)
const alreadyExportingToastMessage = page.getByText(`Already exporting`) const alreadyExportingToastMessage = page.getByText(`Already exporting`)
const successToastMessage = page.getByText(`Exported successfully`) const successToastMessage = page.getByText(`Exported successfully`)
await test.step('Blocked second export', async () => { await test.step('second export', async () => {
await clickExportButton(page) await clickExportButton(page)
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
await clickExportButton(page) 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 test.step('The first export still succeeds', async () => {
await Promise.all([ await Promise.all([
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }), expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
@ -486,7 +478,7 @@ extrude001 = extrude(sketch001, length = 50)
expect(alreadyExportingToastMessage).not.toBeVisible(), 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/inspect": "^0.8.0",
"@xstate/react": "^4.1.1", "@xstate/react": "^4.1.1",
"bonjour-service": "^1.3.0", "bonjour-service": "^1.3.0",
"bson": "^6.10.3",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
@ -212,6 +213,7 @@
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.26.1",
"vite": "^5.4.12", "vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.1", "vitest": "^1.6.1",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",

1
rust/Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ crate-type = ["cdylib"]
bench = false bench = false
[target.'cfg(target_arch = "wasm32")'.dependencies] [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" console_error_panic_hook = "0.1.7"
data-encoding = "2.6.0" data-encoding = "2.6.0"
futures = "0.3.31" futures = "0.3.31"
@ -22,6 +22,7 @@ gloo-utils = "0.2.0"
js-sys = "0.3.72" js-sys = "0.3.72"
kcl-lib = { path = "../kcl-lib" } kcl-lib = { path = "../kcl-lib" }
kittycad = { workspace = true } kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync"] } tokio = { workspace = true, features = ["sync"] }
toml = "0.8.19" 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())?), 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())?) 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] #[wasm_bindgen]
pub fn parse_wasm(kcl_program_source: &str) -> Result<JsValue, String> { pub fn parse_wasm(kcl_program_source: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();

View File

@ -27,12 +27,6 @@ export const ModelStateIndicator = () => {
name="checkmark" 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 ( return (

View File

@ -8,7 +8,6 @@ import React, {
} from 'react' } from 'react'
import { import {
Actor, Actor,
AnyStateMachine,
ContextFrom, ContextFrom,
Prop, Prop,
SnapshotFrom, SnapshotFrom,
@ -33,8 +32,12 @@ import {
codeManager, codeManager,
editorManager, editorManager,
sceneEntitiesManager, sceneEntitiesManager,
rustContext,
} from 'lib/singletons' } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider' import {
MachineManager,
MachineManagerContext,
} from 'components/MachineManagerProvider'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
@ -53,7 +56,10 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig' import {
ModelingCommandSchema,
modelingMachineCommandConfig,
} from 'lib/commandBarConfigs/modelingCommandConfig'
import { import {
SEGMENT_BODIES, SEGMENT_BODIES,
getParentGroup, getParentGroup,
@ -84,21 +90,17 @@ import {
isCursorInFunctionDefinition, isCursorInFunctionDefinition,
traverse, traverse,
} from 'lang/queryAst' } from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap, reject } from 'lib/trap' import { err, reportRejection, trap, reject } from 'lib/trap'
import { import {
ExportIntent,
EngineConnectionStateType, EngineConnectionStateType,
EngineConnectionEvents, EngineConnectionEvents,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { submitAndAwaitTextToKcl } from 'lib/textToCad' import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { platform, uuidv4 } from 'lib/utils' import { platform, uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { Node } from '@rust/kcl-lib/bindings/Node' import { Node } from '@rust/kcl-lib/bindings/Node'
import { import {
getFaceCodeRef, getFaceCodeRef,
@ -111,15 +113,18 @@ import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine' import { useToken } from 'machines/appMachine'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { useSettings } from 'machines/appMachine' import { useSettings } from 'machines/appMachine'
import { IndexLoaderData } from 'lib/types'
type MachineContext<T extends AnyStateMachine> = { import { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd'
state: StateFrom<T> import { EXPORT_TOAST_MESSAGES, MAKE_TOAST_MESSAGES } from 'lib/constants'
context: ContextFrom<T> import { exportMake } from 'lib/exportMake'
send: Prop<Actor<T>, 'send'> import { exportSave } from 'lib/exportSave'
}
export const ModelingMachineContext = createContext( 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 = ( const commandBarIsClosedSelector = (
@ -524,118 +529,6 @@ export const ModelingMachineProvider = ({
return {} 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 }) => { 'Submit to Text-to-CAD API': ({ event }) => {
if (event.type !== 'Text-to-CAD') return if (event.type !== 'Text-to-CAD') return
const trimmedPrompt = event.data.prompt.trim() const trimmedPrompt = event.data.prompt.trim()
@ -696,14 +589,155 @@ export const ModelingMachineProvider = ({
else if (kclManager.ast.body.length === 0) else if (kclManager.ast.body.length === 0)
errorMessage += 'due to Empty Scene' errorMessage += 'due to Empty Scene'
console.error(errorMessage) console.error(errorMessage)
toast.error(errorMessage, { toast.error(errorMessage)
id: kclManager.engineCommandManager.pendingExport?.toastId,
})
return false return false
} }
}, },
}, },
actors: { 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( 'AST-undo-startSketchOn': fromPromise(
async ({ input: { sketchDetails } }) => { async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return

View File

@ -6,8 +6,8 @@ import {
} from 'lang/wasm' } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { deferExecution, isOverlap, uuidv4 } from 'lib/utils' import { deferExecution, isOverlap, uuidv4 } from 'lib/utils'
import { BSON, Binary as BSONBinary } from 'bson'
import { import {
Themes, Themes,
getThemeColorForEngine, getThemeColorForEngine,
@ -16,14 +16,8 @@ import {
} from 'lib/theme' } from 'lib/theme'
import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph' import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes' import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
import { import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE,
EXPORT_TOAST_MESSAGES,
MAKE_TOAST_MESSAGES,
} from 'lib/constants'
import { KclManager } from 'lang/KclSingleton' import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
@ -47,16 +41,6 @@ interface NewTrackArgs {
mediaStream: MediaStream mediaStream: MediaStream
} }
export enum ExportIntent {
Save = 'save',
Make = 'make',
}
export interface ExportInfo {
intent: ExportIntent
name: string
}
type ClientMetrics = Models['ClientMetrics_type'] type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics { interface WebRTCClientMetrics extends ClientMetrics {
@ -1069,18 +1053,6 @@ class EngineConnection extends EventTarget {
`Error in response to request ${message.request_id}:\n${errorsString} `Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.type}` 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 { } else {
console.error(`Error from server:\n${errorsString}`) console.error(`Error from server:\n${errorsString}`)
} }
@ -1365,10 +1337,6 @@ export type CommandLog =
type: 'execution-done' type: 'execution-done'
data: null data: null
} }
| {
type: 'export-done'
data: null
}
export enum EngineCommandManagerEvents { export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run // engineConnection is available but scene setup may not have run
@ -1432,16 +1400,6 @@ export class EngineCommandManager extends EventTarget {
inSequence = 1 inSequence = 1
engineConnection?: EngineConnection engineConnection?: EngineConnection
commandLogs: CommandLog[] = [] 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 settings: SettingsViaQueryString
streamDimensions = { streamDimensions = {
@ -1452,12 +1410,6 @@ export class EngineCommandManager extends EventTarget {
elVideo: HTMLVideoElement | null = null 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 = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
subscriptions: { subscriptions: {
@ -1509,14 +1461,6 @@ export class EngineCommandManager extends EventTarget {
// The current "manufacturing machine" aka 3D printer, CNC, etc. // The current "manufacturing machine" aka 3D printer, CNC, etc.
public machineManager: MachineManager | null = null public machineManager: MachineManager | null = null
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
}
get exportInfo() {
return this._exportInfo
}
start({ start({
setMediaStream, setMediaStream,
setIsStreamReady, setIsStreamReady,
@ -1691,64 +1635,33 @@ export class EngineCommandManager extends EventTarget {
engineConnection.websocket?.addEventListener('message', (( engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent event: MessageEvent
) => { ) => {
let message: Models['WebSocketResponse_type'] | null = null
if (event.data instanceof ArrayBuffer) { if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command, // BSON deserialize the command.
// because in all other cases we send JSON strings. But in the case of message = BSON.deserialize(
// export we send a binary blob. new Uint8Array(event.data)
// Pass this to our export function. ) as Models['WebSocketResponse_type']
if (this.exportInfo === null || this.pendingExport === undefined) { // The request id comes back as binary and we want to get the uuid
toast.error( // string from that.
'Export intent was not set, but export data was received' if (message.request_id) {
) message.request_id = binaryToUuid(message.request_id)
console.error(
'Export intent was not set, but export data was received'
)
return
} }
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 { } else {
this.pendingExport?.reject('Failed to make export') message = JSON.parse(event.data)
} }
}, this.pendingExport?.reject)
break if (message === null) {
} // We should never get here.
} console.error('Received a null message from the engine', event)
// Set the export intent back to null.
this.exportInfo = null
return return
} }
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
const pending = this.pendingCommands[message.request_id || ''] const pending = this.pendingCommands[message.request_id || '']
if (pending && !message.success) { if (pending && !message.success) {
// handle bad case // handle bad case
pending.reject(`engine error: ${JSON.stringify(message.errors)}`) pending.reject(JSON.stringify(message))
delete this.pendingCommands[message.request_id || ''] delete this.pendingCommands[message.request_id || '']
} }
if ( if (
@ -1756,12 +1669,15 @@ export class EngineCommandManager extends EventTarget {
pending && pending &&
message.success && message.success &&
(message.resp.type === 'modeling' || (message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') message.resp.type === 'modeling_batch' ||
message.resp.type === 'export')
) )
) )
return return
if ( if (message.resp.type === 'export' && message.request_id) {
this.responseMap[message.request_id] = message.resp
} else if (
message.resp.type === 'modeling' && message.resp.type === 'modeling' &&
pending.command.type === 'modeling_cmd_req' && pending.command.type === 'modeling_cmd_req' &&
message.request_id message.request_id
@ -2026,38 +1942,6 @@ export class EngineCommandManager extends EventTarget {
this.outSequence++ this.outSequence++
this.engineConnection?.unreliableSend(command) this.engineConnection?.unreliableSend(command)
return Promise.resolve(null) 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 ( if (
command.cmd.type === 'default_camera_look_at' || command.cmd.type === 'default_camera_look_at' ||
@ -2090,7 +1974,7 @@ export class EngineCommandManager extends EventTarget {
rangeStr: string, rangeStr: string,
commandStr: string, commandStr: string,
idToRangeStr: string idToRangeStr: string
): Promise<string | void> { ): Promise<Uint8Array | void> {
if (this.engineConnection === undefined) return Promise.resolve() if (this.engineConnection === undefined) return Promise.resolve()
if ( if (
!this.engineConnection?.isReady() && !this.engineConnection?.isReady() &&
@ -2118,7 +2002,7 @@ export class EngineCommandManager extends EventTarget {
range, range,
idToRangeMap, idToRangeMap,
}) })
return JSON.stringify(resp[0]) return BSON.serialize(resp[0])
} }
/** /**
* Common send command function used for both modeling and scene commands * Common send command function used for both modeling and scene commands
@ -2266,3 +2150,65 @@ function promiseFactory<T>() {
}) })
return { promise, resolve, reject } 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 { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { components } from './machine-api' import { components } from './machine-api'
@ -6,12 +5,17 @@ import ModelingAppFile from './modelingAppFile'
import { MAKE_TOAST_MESSAGES } from './constants' import { MAKE_TOAST_MESSAGES } from './constants'
// Make files locally from an export call. // Make files locally from an export call.
export async function exportMake( export async function exportMake({
data: ArrayBuffer, files,
name: string, name,
toastId: string, toastId,
machineManager,
}: {
files: ModelingAppFile[]
name: string
toastId: string
machineManager: MachineManager machineManager: MachineManager
): Promise<Response | null> { }): Promise<Response | null> {
if (name === '') { if (name === '') {
console.error(MAKE_TOAST_MESSAGES.NO_NAME) console.error(MAKE_TOAST_MESSAGES.NO_NAME)
toast.error(MAKE_TOAST_MESSAGES.NO_NAME, { id: toastId }) toast.error(MAKE_TOAST_MESSAGES.NO_NAME, { id: toastId })
@ -50,10 +54,8 @@ export async function exportMake(
job_name: name, job_name: name,
} }
try { try {
console.log('params', params)
const formData = new FormData() const formData = new FormData()
formData.append('params', JSON.stringify(params)) formData.append('params', JSON.stringify(params))
let files: ModelingAppFile[] = deserialize_files(new Uint8Array(data))
let file = files[0] let file = files[0]
const fileBlob = new Blob([new Uint8Array(file.contents)], { const fileBlob = new Blob([new Uint8Array(file.contents)], {
type: 'text/plain', type: 'text/plain',

View File

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

View File

@ -1,5 +1,4 @@
import { import {
emptyExecState,
errFromErrWithOutputs, errFromErrWithOutputs,
ExecState, ExecState,
execStateFromRust, execStateFromRust,
@ -17,6 +16,10 @@ import { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes'
import { DefaultPlaneStr, defaultPlaneStrToKey } from 'lib/planes' import { DefaultPlaneStr, defaultPlaneStrToKey } from 'lib/planes'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { EngineCommandManager } from 'lang/std/engineConnection' 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 { export default class RustContext {
private wasmInitFailed: boolean = true private wasmInitFailed: boolean = true
@ -43,20 +46,22 @@ export default class RustContext {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(async () => { this.ensureWasmInit().then(async () => {
await this.create() this.ctxInstance = await this.create()
}) })
} }
// Create a new context instance // Create a new context instance
async create() { async create(): Promise<Context> {
this.rustInstance = getModule() this.rustInstance = getModule()
// We need this await here, DO NOT REMOVE it even if your editor says it's // 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 // unnecessary. The constructor of the module is async and it will not
// resolve if you don't await it. // resolve if you don't await it.
this.ctxInstance = await new this.rustInstance.Context( const ctxInstance = await new this.rustInstance.Context(
this.engineCommandManager, this.engineCommandManager,
fileSystemManager fileSystemManager
) )
return ctxInstance
} }
// Execute a program. // Execute a program.
@ -65,11 +70,10 @@ export default class RustContext {
settings: DeepPartial<Configuration>, settings: DeepPartial<Configuration>,
path?: string path?: string
): Promise<ExecState> { ): Promise<ExecState> {
await this._checkInstance() const instance = await this._checkInstance()
if (this.ctxInstance) {
try { try {
const result = await this.ctxInstance.execute( const result = await instance.execute(
JSON.stringify(node), JSON.stringify(node),
path, path,
JSON.stringify(settings) JSON.stringify(settings)
@ -88,10 +92,6 @@ export default class RustContext {
} }
} }
// You will never get here.
return Promise.reject(emptyExecState())
}
// Execute a program with in mock mode. // Execute a program with in mock mode.
async executeMock( async executeMock(
node: Node<Program>, node: Node<Program>,
@ -99,15 +99,14 @@ export default class RustContext {
path?: string, path?: string,
usePrevMemory?: boolean usePrevMemory?: boolean
): Promise<ExecState> { ): Promise<ExecState> {
await this._checkInstance() const instance = await this._checkInstance()
if (this.ctxInstance) {
try {
if (usePrevMemory === undefined) { if (usePrevMemory === undefined) {
usePrevMemory = true usePrevMemory = true
} }
const result = await this.ctxInstance.executeMock( try {
const result = await instance.executeMock(
JSON.stringify(node), JSON.stringify(node),
path, path,
JSON.stringify(settings), JSON.stringify(settings),
@ -119,8 +118,24 @@ export default class RustContext {
} }
} }
// You will never get here. // Export a scene to a file.
return Promise.reject(emptyExecState()) 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() { async waitForAllEngineCommands() {
@ -169,11 +184,13 @@ export default class RustContext {
} }
// Helper to check if context instance exists // Helper to check if context instance exists
private async _checkInstance() { private async _checkInstance(): Promise<Context> {
if (!this.ctxInstance) { if (!this.ctxInstance) {
// Create the context instance. // Create the context instance.
await this.create() this.ctxInstance = await this.create()
} }
return this.ctxInstance
} }
// Clean up resources // Clean up resources

View File

@ -1283,14 +1283,12 @@ export const modelingMachine = setup({
}, },
} }
}), }),
Make: () => {},
'enable copilot': () => {}, 'enable copilot': () => {},
'disable copilot': () => {}, 'disable copilot': () => {},
'Set selection': () => {}, 'Set selection': () => {},
'Set mouse state': () => {}, 'Set mouse state': () => {},
'Set Segment Overlays': () => {}, 'Set Segment Overlays': () => {},
'Center camera on selection': () => {}, 'Center camera on selection': () => {},
'Engine export': () => {},
'Submit to Text-to-CAD API': () => {}, 'Submit to Text-to-CAD API': () => {},
'Set sketchDetails': () => {}, 'Set sketchDetails': () => {},
'sketch exit execute': () => {}, '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 // end actors
}).createMachine({ }).createMachine({
@ -2528,17 +2540,13 @@ export const modelingMachine = setup({
}, },
Export: { Export: {
target: 'idle', target: 'Exporting',
reenter: false,
guard: 'Has exportable geometry', guard: 'Has exportable geometry',
actions: 'Engine export',
}, },
Make: { Make: {
target: 'idle', target: 'Making',
reenter: false,
guard: 'Has exportable geometry', guard: 'Has exportable geometry',
actions: 'Make',
}, },
'Delete selection': { 'Delete selection': {
@ -3885,6 +3893,35 @@ export const modelingMachine = setup({
onError: ['idle'], 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', initial: 'idle',

View File

@ -3,6 +3,7 @@ import viteTsconfigPaths from 'vite-tsconfig-paths'
import eslint from '@nabla/vite-plugin-eslint' import eslint from '@nabla/vite-plugin-eslint'
import { defineConfig, configDefaults } from 'vitest/config' import { defineConfig, configDefaults } from 'vitest/config'
import version from 'vite-plugin-package-version' import version from 'vite-plugin-package-version'
import topLevelAwait from 'vite-plugin-top-level-await'
// @ts-ignore: No types available // @ts-ignore: No types available
import { lezer } from '@lezer/generator/rollup' import { lezer } from '@lezer/generator/rollup'
@ -62,7 +63,19 @@ const config = defineConfig({
'@rust': '/rust', '@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: { worker: {
plugins: () => [viteTsconfigPaths()], plugins: () => [viteTsconfigPaths()],
}, },

View File

@ -2,6 +2,7 @@ import type { ConfigEnv, UserConfig } from 'vite'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { pluginExposeRenderer } from './vite.base.config' import { pluginExposeRenderer } from './vite.base.config'
import viteTsconfigPaths from 'vite-tsconfig-paths' import viteTsconfigPaths from 'vite-tsconfig-paths'
import topLevelAwait from 'vite-plugin-top-level-await'
// @ts-ignore: No types available // @ts-ignore: No types available
import { lezer } from '@lezer/generator/rollup' import { lezer } from '@lezer/generator/rollup'
@ -18,7 +19,17 @@ export default defineConfig((env) => {
build: { build: {
outDir: `.vite/renderer/${name}`, 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: { worker: {
plugins: () => [viteTsconfigPaths()], plugins: () => [viteTsconfigPaths()],
}, },

105
yarn.lock
View File

@ -1995,6 +1995,11 @@
dependencies: dependencies:
"@codemirror/state" "^6.2.1" "@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": "@rollup/rollup-android-arm-eabi@4.29.1":
version "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" 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" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== 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": "@szmarczak/http-timer@^4.0.5":
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" 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" node-releases "^2.0.19"
update-browserslist-db "^1.1.1" 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: buffer-crc32@~0.2.3:
version "0.2.13" version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@ -9249,6 +9340,11 @@ utrie@^1.0.2:
dependencies: dependencies:
base64-arraybuffer "^1.0.2" 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: uuid@^11.1.0:
version "11.1.0" version "11.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" 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" resolved "https://registry.yarnpkg.com/vite-plugin-package-version/-/vite-plugin-package-version-1.1.0.tgz#7d8088955aa21e4ec93353c98992b3f58c4bf13c"
integrity sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA== 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: vite-tsconfig-paths@^4.3.2:
version "4.3.2" version "4.3.2"
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9" resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"