diff --git a/e2e/playwright/fixtures/fixtureSetup.ts b/e2e/playwright/fixtures/fixtureSetup.ts index e70a8a1c952..b013ed6d772 100644 --- a/e2e/playwright/fixtures/fixtureSetup.ts +++ b/e2e/playwright/fixtures/fixtureSetup.ts @@ -228,7 +228,10 @@ export class ElectronZoo { }, dims) return this.evaluate(async (dims: { width: number; height: number }) => { - await window.electron.resizeWindow(dims.width, dims.height) + if (!window.electron) { + throw new Error('Electron not defined') + } + await window.electron?.resizeWindow(dims.width, dims.height) window.document.body.style.width = dims.width + 'px' window.document.body.style.height = dims.height + 'px' window.document.documentElement.style.width = dims.width + 'px' diff --git a/interface.d.ts b/interface.d.ts index 75ebfc0ddc9..12d8041e23c 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -1,10 +1,10 @@ -import { MachinesListing } from 'components/MachineManagerProvider' +import type { MachinesListing } from 'components/MachineManagerProvider' import 'electron' -import fs from 'node:fs/promises' -import path from 'path' -import { dialog, shell } from 'electron' +import type fs from 'node:fs/promises' +import type path from 'path' +import type { dialog, shell } from 'electron' import type { WebContentSendPayload } from 'menu/channels' -import { ZooLabel } from 'menu/roles' +import type { ZooLabel } from 'menu/roles' // Extend the interface with additional custom properties declare module 'electron' { @@ -108,7 +108,9 @@ export interface IElectronAPI { declare global { interface Window { - electron: IElectronAPI - openExternalLink: (e: React.MouseEvent) => void + electron: IElectronAPI | undefined + openExternalLink: + | ((e: React.MouseEvent) => void) + | undefined } } diff --git a/package-lock.json b/package-lock.json index cec28b382f5..2e5678b6b72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "rust/kcl-language-server" ], "dependencies": { + "@chainner/node-path": "^0.1.1", "@codemirror/autocomplete": "^6.18.6", "@codemirror/commands": "^6.8.1", "@codemirror/language": "^6.11.0", @@ -2469,6 +2470,12 @@ "tough-cookie": "^4.1.4" } }, + "node_modules/@chainner/node-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@chainner/node-path/-/node-path-0.1.1.tgz", + "integrity": "sha512-uSJQCY1JIqjU1UaTZppaEc6+xxF/8J94nIOu8zyiqzrbkY/rteFThc5/xdx9+3BbG8jJ50BHxBGJVFXMOUt0qw==", + "license": "MIT" + }, "node_modules/@clack/core": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz", diff --git a/package.json b/package.json index c3ce2de5786..10af765babc 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "rust/kcl-language-server" ], "dependencies": { + "@chainner/node-path": "^0.1.1", "@codemirror/autocomplete": "^6.18.6", "@codemirror/commands": "^6.8.1", "@codemirror/language": "^6.11.0", diff --git a/rust/kcl-lib/src/fs/wasm.rs b/rust/kcl-lib/src/fs/wasm.rs index 12b6cebda85..1a257dc8eab 100644 --- a/rust/kcl-lib/src/fs/wasm.rs +++ b/rust/kcl-lib/src/fs/wasm.rs @@ -16,9 +16,6 @@ extern "C" { #[derive(Debug, Clone)] pub type FileSystemManager; - #[wasm_bindgen(constructor)] - pub fn new() -> FileSystemManager; - #[wasm_bindgen(method, js_name = readFile, catch)] fn read_file(this: &FileSystemManager, path: String) -> Result; diff --git a/src/App.tsx b/src/App.tsx index 7540b1579c5..f2c019dae33 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,9 +72,11 @@ import env from '@src/env' // CYCLIC REF sceneInfra.camControls.engineStreamActor = engineStreamActor -maybeWriteToDisk() - .then(() => {}) - .catch(() => {}) +if (window.electron) { + maybeWriteToDisk(window.electron) + .then(() => {}) + .catch(reportRejection) +} export function App() { const { state: modelingState } = useModelingContext() @@ -241,7 +243,7 @@ export function App() { // Only create the native file menus on desktop useEffect(() => { - if (isDesktop()) { + if (window.electron) { window.electron .createModelingPageMenu() .then(() => { diff --git a/src/components/CommandBar/CommandBarPathInput.tsx b/src/components/CommandBar/CommandBarPathInput.tsx index bef4cfda95f..a9e2b77324c 100644 --- a/src/components/CommandBar/CommandBarPathInput.tsx +++ b/src/components/CommandBar/CommandBarPathInput.tsx @@ -65,6 +65,9 @@ function CommandBarPathInput({ configuration.filters = arg.filters } + if (!window.electron) { + return new Error("Can't open file picker without electron") + } const newPath = await window.electron.open(configuration) if (newPath.canceled) return inputRef.current.value = newPath.filePaths[0] @@ -75,7 +78,7 @@ function CommandBarPathInput({ // Fire on component mount, if outside of e2e test context useEffect(() => { - window.electron.process.env.NODE_ENV !== 'test' && + window.electron?.process.env.NODE_ENV !== 'test' && toSync(pickFileThroughNativeDialog, reportRejection)() // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: blanket-ignored fix me! }, []) diff --git a/src/components/Explorer/ProjectExplorer.tsx b/src/components/Explorer/ProjectExplorer.tsx index 7f0553c6f1e..6492d47cd2d 100644 --- a/src/components/Explorer/ProjectExplorer.tsx +++ b/src/components/Explorer/ProjectExplorer.tsx @@ -326,7 +326,7 @@ export const ProjectExplorer = ({ } }, onOpenInNewWindow: () => { - window.electron.openInNewWindow(row.path) + window.electron?.openInNewWindow(row.path) }, onRenameStart: () => { if (readOnly) { @@ -375,16 +375,19 @@ export const ProjectExplorer = ({ const absolutePathToParentDirectory = getParentAbsolutePath( row.path ) - const oldPath = window.electron.path.join( + const oldPath = window.electron?.path.join( absolutePathToParentDirectory, name ) - const newPath = window.electron.path.join( + const newPath = window.electron?.path.join( absolutePathToParentDirectory, requestedName ) const shouldWeNavigate = - file?.path?.startsWith(oldPath) && canNavigate + oldPath !== undefined && + newPath !== undefined && + file?.path?.startsWith(oldPath) && + canNavigate if (shouldWeNavigate && file && file.path) { const requestedFileNameWithExtension = diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index b9f1dfa3d59..e93c887df65 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -24,7 +24,6 @@ import { } from '@src/lib/constants' import { getProjectInfo } from '@src/lib/desktop' import { getNextDirName, getNextFileName } from '@src/lib/desktopFS' -import { isDesktop } from '@src/lib/isDesktop' import { kclCommands } from '@src/lib/kclCommands' import { BROWSER_PATH, PATHS } from '@src/lib/paths' import { markOnce } from '@src/lib/performance' @@ -35,6 +34,7 @@ import { useSettings, useToken } from '@src/lib/singletons' import { commandBarActor } from '@src/lib/singletons' import { fileMachine } from '@src/machines/fileMachine' import { modelingMenuCallbackMostActions } from '@src/menu/register' +import { fsManager } from '@src/lang/std/fileSystemManager' type MachineContext = { state: StateFrom @@ -127,7 +127,7 @@ export const FileMachineProvider = ({ // TODO: Should this be context.selectedDirectory.path? // eslint-disable-next-line @typescript-eslint/restrict-plus-operands context.selectedDirectory + - window.electron.path.sep + + fsManager.path.sep + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands event.output.name )}` @@ -147,20 +147,24 @@ export const FileMachineProvider = ({ } commandBarActor.send({ type: 'Close' }) - window.electron.openInNewWindow(event.data.name) + window.electron?.openInNewWindow(event.data.name) }, }, actors: { readFiles: fromPromise(async ({ input }) => { const newFiles = - (isDesktop() ? (await getProjectInfo(input.path)).children : []) ?? - [] + (window.electron + ? (await getProjectInfo(window.electron, input.path)).children + : []) ?? [] return { ...input, children: newFiles, } }), createAndOpenFile: fromPromise(async ({ input }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } let createdName = input.name.trim() || DEFAULT_FILE_NAME let createdPath: string @@ -172,6 +176,7 @@ export const FileMachineProvider = ({ input.makeDir ) { let { name, path } = getNextDirName({ + electron: window.electron, entryName: input.targetPathToClone ? window.electron.path.basename(input.targetPathToClone) : createdName, @@ -187,7 +192,8 @@ export const FileMachineProvider = ({ input.targetPathToClone && input.selectedDirectory.path.indexOf(input.targetPathToClone) > -1 if (isTargetPathToCloneASubPath) { - const { name, path } = getNextFileName({ + const { name, path } = await getNextFileName({ + electron: window.electron, entryName: input.targetPathToClone ? window.electron.path.basename(input.targetPathToClone) : createdName, @@ -198,7 +204,8 @@ export const FileMachineProvider = ({ createdName = name createdPath = path } else { - const { name, path } = getNextFileName({ + const { name, path } = await getNextFileName({ + electron: window.electron, entryName: input.targetPathToClone ? window.electron.path.basename(input.targetPathToClone) : createdName, @@ -229,11 +236,15 @@ export const FileMachineProvider = ({ } }), createFile: fromPromise(async ({ input }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } let createdName = input.name.trim() || DEFAULT_FILE_NAME let createdPath: string if (input.makeDir) { let { name, path } = getNextDirName({ + electron: window.electron, entryName: createdName, baseDir: input.selectedDirectory.path, }) @@ -241,7 +252,8 @@ export const FileMachineProvider = ({ createdPath = path await window.electron.mkdir(createdPath) } else { - const { name, path } = getNextFileName({ + const { name, path } = await getNextFileName({ + electron: window.electron, entryName: createdName, baseDir: input.selectedDirectory.path, }) @@ -260,6 +272,9 @@ export const FileMachineProvider = ({ } }), renameFile: fromPromise(async ({ input }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const { oldName, newName, isDir } = input const name = newName ? newName.endsWith(FILE_EXT) || isDir @@ -319,6 +334,7 @@ export const FileMachineProvider = ({ } }), deleteFile: fromPromise(async ({ input }) => { + if (!window.electron) return const isDir = !!input.children if (isDir) { @@ -444,7 +460,7 @@ export const FileMachineProvider = ({ const kclCommandMemo = useMemo(() => { const providedOptions = [] - if (isDesktop() && project?.children && file?.path) { + if (window.electron && project?.children && file?.path) { const projectPath = project.path const filePath = file.path let children = project.children diff --git a/src/components/HelpMenu.tsx b/src/components/HelpMenu.tsx index 4d0c2c091e8..93410f4471b 100644 --- a/src/components/HelpMenu.tsx +++ b/src/components/HelpMenu.tsx @@ -118,7 +118,7 @@ export function HelpMenu() { as="button" onClick={() => { close() - window.electron.appCheckForUpdates().catch(reportRejection) + window.electron?.appCheckForUpdates().catch(reportRejection) }} > Check for updates diff --git a/src/components/MachineManagerProvider.tsx b/src/components/MachineManagerProvider.tsx index 3fafed94283..644625f6329 100644 --- a/src/components/MachineManagerProvider.tsx +++ b/src/components/MachineManagerProvider.tsx @@ -1,6 +1,5 @@ import { createContext, useEffect, useState } from 'react' -import { isDesktop } from '@src/lib/isDesktop' import type { components } from '@src/lib/machine-api' import { engineCommandManager } from '@src/lib/singletons' import { reportRejection } from '@src/lib/trap' @@ -56,15 +55,16 @@ export const MachineManagerProvider = ({ } useEffect(() => { - if (!isDesktop()) return + if (!window.electron) return + const electron = window.electron const update = async () => { - const _machineApiIp = await window.electron.getMachineApiIp() + const _machineApiIp = await electron.getMachineApiIp() if (_machineApiIp === null) return setMachineApiIp(_machineApiIp) - const _machines = await window.electron.listMachines(_machineApiIp) + const _machines = await electron.listMachines(_machineApiIp) setMachines(_machines) } diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 84db1e69e7f..092ed0912a2 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -63,7 +63,6 @@ import { } from '@src/lib/constants' import { exportMake } from '@src/lib/exportMake' import { exportSave } from '@src/lib/exportSave' -import { isDesktop } from '@src/lib/isDesktop' import type { FileEntry, Project } from '@src/lib/project' import type { WebContentSendPayload } from '@src/menu/channels' import { @@ -1202,7 +1201,8 @@ export const ModelingMachineProvider = ({ } ) let basePath = '' - if (isDesktop() && theProject?.current?.children) { + if (window.electron && theProject?.current?.children) { + const electron = window.electron // Use the entire project directory as the basePath for prompt to edit, do not use relative subdir paths basePath = theProject?.current?.path const filePromises: Promise[] = [] @@ -1218,17 +1218,17 @@ export const ModelingMachineProvider = ({ } const absolutePathToFileNameWithExtension = file.path - const fileNameWithExtension = window.electron.path.relative( + const fileNameWithExtension = electron.path.relative( basePath, absolutePathToFileNameWithExtension ) - const filePromise = window.electron + const filePromise = electron .readFile(absolutePathToFileNameWithExtension) .then((file): FileMeta => { uploadSize += file.byteLength const decoder = new TextDecoder('utf-8') - const fileType = window.electron.path.extname( + const fileType = electron.path.extname( absolutePathToFileNameWithExtension ) if (fileType === FILE_EXT) { @@ -1274,7 +1274,7 @@ export const ModelingMachineProvider = ({ // route to main.kcl by default for web and desktop let filePath: string = PROJECT_ENTRYPOINT const possibleFileName = file?.path - if (possibleFileName && isDesktop()) { + if (possibleFileName && window.electron) { // When prompt to edit finishes, try to route to the file they were in otherwise go to main.kcl filePath = window.electron.path.relative(basePath, possibleFileName) } diff --git a/src/components/ModelingPageProvider.tsx b/src/components/ModelingPageProvider.tsx index 115ce217604..72dc4698b58 100644 --- a/src/components/ModelingPageProvider.tsx +++ b/src/components/ModelingPageProvider.tsx @@ -6,7 +6,6 @@ import { useMenuListener } from '@src/hooks/useMenu' import { createNamedViewsCommand } from '@src/lib/commandBarConfigs/namedViewsConfig' import { createRouteCommands } from '@src/lib/commandBarConfigs/routeCommandConfig' import { DEFAULT_DEFAULT_LENGTH_UNIT } from '@src/lib/constants' -import { isDesktop } from '@src/lib/isDesktop' import { kclCommands } from '@src/lib/kclCommands' import { BROWSER_PATH, PATHS } from '@src/lib/paths' import { markOnce } from '@src/lib/performance' @@ -118,7 +117,7 @@ export const ModelingPageProvider = ({ const kclCommandMemo = useMemo(() => { const providedOptions = [] - if (isDesktop() && project?.children && file?.path) { + if (window.electron && project?.children && file?.path) { const projectPath = project.path const filePath = file.path let children = project.children diff --git a/src/components/ModelingSidebar/ModelingPanes/index.tsx b/src/components/ModelingSidebar/ModelingPanes/index.tsx index dc77d3b3c94..cf62816d43b 100644 --- a/src/components/ModelingSidebar/ModelingPanes/index.tsx +++ b/src/components/ModelingSidebar/ModelingPanes/index.tsx @@ -214,13 +214,18 @@ export const sidebarPanes: SidebarPane[] = [ requestedFileName: requestedFileName, }, }) - } else if (isRelevantFile(entry.path) && projectRef.current?.path) { + } else if ( + window.electron && + isRelevantFile(entry.path) && + projectRef.current?.path + ) { // Allow insert if it is a importable file + const electron = window.electron toast.custom( ToastInsert({ onInsert: () => { const relativeFilePath = entry.path.replace( - projectRef.current?.path + window.electron.sep, + projectRef.current?.path + electron.path.sep, '' ) commandBarActor.send({ diff --git a/src/components/ProjectCard/ProjectCard.tsx b/src/components/ProjectCard/ProjectCard.tsx index 7ac02516962..9b05dc8901c 100644 --- a/src/components/ProjectCard/ProjectCard.tsx +++ b/src/components/ProjectCard/ProjectCard.tsx @@ -12,6 +12,7 @@ import { PATHS } from '@src/lib/paths' import type { Project } from '@src/lib/project' import { reportRejection } from '@src/lib/trap' import { toSync } from '@src/lib/utils' +import { fsManager } from '@src/lang/std/fileSystemManager' function ProjectCard({ project, @@ -58,12 +59,12 @@ function ProjectCard({ } async function setupImageUrl() { - const projectImagePath = window.electron.path.join( + const projectImagePath = fsManager.path.join( project.path, PROJECT_IMAGE_NAME ) - if (await window.electron.exists(projectImagePath)) { - const imageData = await window.electron.readFile(projectImagePath) + if (await fsManager.exists(projectImagePath)) { + const imageData = await fsManager.readFile(projectImagePath) const blob = new Blob([imageData], { type: 'image/png' }) const imageUrl = URL.createObjectURL(blob) diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index 4e52c347441..e4191694ec4 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -35,7 +35,7 @@ const ProjectSidebarMenu = ({ // Make room for traffic lights on desktop left side. // TODO: make sure this doesn't look like shit on Linux or Windows const trafficLightsOffset = - isDesktop() && window.electron.os.isMac ? 'ml-20' : '' + window.electron && window.electron.os.isMac ? 'ml-20' : '' return (
@@ -242,7 +242,7 @@ function ProjectMenuPopover({ projectName: project?.name || '', sep: '/', filename: - isDesktop() && file?.name + window.electron && file?.name ? file.name.slice(file.name.lastIndexOf(window.electron.path.sep) + 1) : APP_NAME, } diff --git a/src/components/Providers/SystemIOProviderDesktop.tsx b/src/components/Providers/SystemIOProviderDesktop.tsx index 4e2fce212bc..32664fc3a00 100644 --- a/src/components/Providers/SystemIOProviderDesktop.tsx +++ b/src/components/Providers/SystemIOProviderDesktop.tsx @@ -37,6 +37,7 @@ import { useLspContext } from '@src/components/LspProvider' import { useLocation } from 'react-router-dom' import makeUrlPathRelative from '@src/lib/makeUrlPathRelative' import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from '@src/lib/constants' +import { fsManager } from '@src/lang/std/fileSystemManager' export function SystemIOMachineLogicListenerDesktop() { const requestedProjectName = useRequestedProjectName() @@ -214,7 +215,7 @@ export function SystemIOMachineLogicListenerDesktop() { const folderName = systemIOActor.getSnapshot().context.lastProjectDeleteRequest.project - const folderPath = `${projectDirectoryPath}${window.electron.sep}${folderName}` + const folderPath = `${projectDirectoryPath}${fsManager.path.sep}${folderName}` if ( folderName !== NO_PROJECT_DIRECTORY && (eventType === 'unlinkDir' || eventType === 'unlink') && diff --git a/src/components/RouteProvider.tsx b/src/components/RouteProvider.tsx index 3cb61cfc420..4b0f99af8fc 100644 --- a/src/components/RouteProvider.tsx +++ b/src/components/RouteProvider.tsx @@ -10,13 +10,13 @@ import { import { useAuthNavigation } from '@src/hooks/useAuthNavigation' import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher' import { getAppSettingsFilePath } from '@src/lib/desktop' -import { isDesktop } from '@src/lib/isDesktop' import { PATHS } from '@src/lib/paths' import { markOnce } from '@src/lib/performance' import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils' import { trap } from '@src/lib/trap' import type { IndexLoaderData } from '@src/lib/types' import { settingsActor } from '@src/lib/singletons' +import { fsManager } from '@src/lang/std/fileSystemManager' export const RouteProviderContext = createContext({}) @@ -48,20 +48,20 @@ export function RouteProvider({ children }: { children: ReactNode }) { }, [first, navigation, location.pathname]) useEffect(() => { - if (!isDesktop()) return - getAppSettingsFilePath().then(setSettingsPath).catch(trap) + if (!window.electron) return + getAppSettingsFilePath(window.electron).then(setSettingsPath).catch(trap) }, []) useFileSystemWatcher( async (eventType: string) => { // If there is a projectPath but it no longer exists it means - // it was exterally removed. If we let the code past this condition + // it was externally removed. If we let the code past this condition // execute it will recreate the directory due to code in // loadAndValidateSettings trying to recreate files. I do not // wish to change the behavior in case anything else uses it. // Go home. if (loadedProject?.project?.path) { - if (!window.electron.exists(loadedProject?.project?.path)) { + if (!(await fsManager.exists(loadedProject?.project?.path))) { navigate(PATHS.HOME) return } diff --git a/src/components/Settings/AllSettingsFields.tsx b/src/components/Settings/AllSettingsFields.tsx index 4f43114b8e8..232bee0fe39 100644 --- a/src/components/Settings/AllSettingsFields.tsx +++ b/src/components/Settings/AllSettingsFields.tsx @@ -197,7 +197,7 @@ export const AllSettingsFields = forwardRef( if (!finalPath) { return new Error('finalPath undefined') } - window.electron.showInFolder(finalPath) + window.electron?.showInFolder(finalPath) }, reportRejection)} iconStart={{ icon: 'folder', @@ -250,7 +250,7 @@ export const AllSettingsFields = forwardRef( { - window.electron.appCheckForUpdates().catch(reportRejection) + window.electron?.appCheckForUpdates().catch(reportRejection) }} iconStart={{ icon: 'refresh', diff --git a/src/components/ToastTextToCad.tsx b/src/components/ToastTextToCad.tsx index 590e8dcccc4..c39290ff428 100644 --- a/src/components/ToastTextToCad.tsx +++ b/src/components/ToastTextToCad.tsx @@ -48,6 +48,7 @@ import { unescape, } from '@ts-stack/markdown' import { SafeRenderer } from '@src/lib/markdown' +import { fsManager } from '@src/lang/std/fileSystemManager' const CANVAS_SIZE = 128 const PROMPT_TRUNCATE_LENGTH = 128 @@ -419,7 +420,8 @@ export function ToastTextToCadSuccess({ onClick={() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises sendTelemetry(modelId, 'accepted', token) - const path = `${projectDirectoryPath}${window.electron.path.sep}${projectName}${window.electron.sep}${fileName}` + const sep = fsManager.path.sep + const path = `${projectDirectoryPath}${sep}${projectName}${sep}${fileName}` navigate(`${PATHS.FILE}/${encodeURIComponent(path)}`) toast.dismiss(toastId) diff --git a/src/components/UserSidebarMenu.tsx b/src/components/UserSidebarMenu.tsx index b37e2212ca4..5a606f614a9 100644 --- a/src/components/UserSidebarMenu.tsx +++ b/src/components/UserSidebarMenu.tsx @@ -36,11 +36,13 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { useEffect(() => { if (!didListEnvironments) { didListEnvironments = true - listAllEnvironmentsWithTokens() - .then((environmentsWithTokens) => { - setHasMultipleEnvironments(environmentsWithTokens.length > 1) - }) - .catch(reportRejection) + if (window.electron) { + listAllEnvironmentsWithTokens(window.electron) + .then((environmentsWithTokens) => { + setHasMultipleEnvironments(environmentsWithTokens.length > 1) + }) + .catch(reportRejection) + } } }, []) @@ -157,7 +159,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { Element: 'button', hide: !isDesktop(), onClick: () => { - window.electron.appCheckForUpdates().catch(reportRejection) + window.electron?.appCheckForUpdates().catch(reportRejection) }, children: Check for updates, }, diff --git a/src/components/environment/Environment.tsx b/src/components/environment/Environment.tsx index 93db223bfe2..44ffca626a3 100644 --- a/src/components/environment/Environment.tsx +++ b/src/components/environment/Environment.tsx @@ -104,7 +104,15 @@ export function EnvironmentDescription() { onClick={() => { const environment = env().VITE_KITTYCAD_BASE_DOMAIN if (environment) { - writeEnvironmentConfigurationPool(environment, '') + if (!window.electron) { + console.error("Can't access electron") + return + } + writeEnvironmentConfigurationPool( + window.electron, + environment, + '' + ) .then(() => { window.location.reload() }) diff --git a/src/editor/plugins/lsp/worker.ts b/src/editor/plugins/lsp/worker.ts index 3893c0615bd..942f21120fa 100644 --- a/src/editor/plugins/lsp/worker.ts +++ b/src/editor/plugins/lsp/worker.ts @@ -18,7 +18,7 @@ import type { LspWorkerEvent, } from '@src/editor/plugins/lsp/types' import { LspWorker } from '@src/editor/plugins/lsp/types' -import { fileSystemManager } from '@src/lang/std/fileSystemManager' +import { projectFsManager } from '@src/lang/std/fileSystemManager' import { err, reportRejection } from '@src/lib/trap' const intoServer: IntoServer = new IntoServer() @@ -75,7 +75,7 @@ onmessage = function (event: MessageEvent) { const config = new LspServerConfig( intoServer, fromServer, - fileSystemManager + projectFsManager ) console.log('Starting worker', worker) switch (worker) { diff --git a/src/hooks/useFileSystemWatcher.tsx b/src/hooks/useFileSystemWatcher.tsx index 0eb371026c8..42536f93510 100644 --- a/src/hooks/useFileSystemWatcher.tsx +++ b/src/hooks/useFileSystemWatcher.tsx @@ -36,7 +36,7 @@ export const useFileSystemWatcher = ( // On component teardown obliterate all watchers. useEffect(() => { // The hook is useless on web. - if (!isDesktop()) return + if (!window.electron) return const cbWatcher = (eventType: string, path: string) => { setOutput({ eventType, path }) @@ -53,10 +53,11 @@ export const useFileSystemWatcher = ( window.electron.watchFileOn(path, key.current, cbWatcher) } + const electron = window.electron return () => { for (let path of pathsTracked) { // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: blanket-ignored fix me! - window.electron.watchFileOff(path, key.current) + electron.watchFileOff(path, key.current) } } }, [pathsTracked]) diff --git a/src/hooks/useMenu.ts b/src/hooks/useMenu.ts index ef96543ca5d..561e515219e 100644 --- a/src/hooks/useMenu.ts +++ b/src/hooks/useMenu.ts @@ -1,5 +1,4 @@ import { NetworkHealthState } from '@src/hooks/useNetworkStatus' -import { isDesktop } from '@src/lib/isDesktop' import type { ToolbarModeName } from '@src/lib/toolbar' import { reportRejection } from '@src/lib/trap' import { useCommandBarState } from '@src/lib/singletons' @@ -10,15 +9,14 @@ export function useMenuListener( callback: (data: WebContentSendPayload) => void ) { useEffect(() => { - const onDesktop = isDesktop() - if (!onDesktop) { + if (!window.electron) { // NO OP for web return } const removeListener = window.electron.menuOn(callback) return () => { - if (!onDesktop) { + if (!window.electron) { // NO OP for web return } @@ -41,11 +39,11 @@ export function useSketchModeMenuEnableDisable( const commands = commandBarState.context.commands useEffect(() => { - const onDesktop = isDesktop() - if (!onDesktop) { + if (!window.electron) { // NO OP for web return } + const electron = window.electron // Same exact logic as the command bar const disableAllButtons = @@ -58,7 +56,7 @@ export function useSketchModeMenuEnableDisable( menus.forEach(({ menuLabel, commandName, groupId }) => { // If someone goes wrong, disable all the buttons! Engine cannot take this request if (disableAllButtons) { - window.electron.disableMenu(menuLabel).catch(reportRejection) + electron.disableMenu(menuLabel).catch(reportRejection) return } @@ -68,26 +66,26 @@ export function useSketchModeMenuEnableDisable( return command.name === commandName && command.groupId === groupId }) if (!foundCommand) { - window.electron.disableMenu(menuLabel).catch(reportRejection) + electron.disableMenu(menuLabel).catch(reportRejection) } else { if (currentMode === 'sketching') { - window.electron.disableMenu(menuLabel).catch(reportRejection) + electron.disableMenu(menuLabel).catch(reportRejection) } else if (currentMode === 'modeling') { - window.electron.enableMenu(menuLabel).catch(reportRejection) + electron.enableMenu(menuLabel).catch(reportRejection) } } } else { // menu is not tied to a command bar, do the sketch mode check if (currentMode === 'sketching') { - window.electron.disableMenu(menuLabel).catch(reportRejection) + electron.disableMenu(menuLabel).catch(reportRejection) } else if (currentMode === 'modeling') { - window.electron.enableMenu(menuLabel).catch(reportRejection) + electron.enableMenu(menuLabel).catch(reportRejection) } } }) return () => { - if (!onDesktop) { + if (!window.electron) { // NO OP for web return } diff --git a/src/hooks/useProjectsLoader.tsx b/src/hooks/useProjectsLoader.tsx index 0212476b0ba..21b5aa76985 100644 --- a/src/hooks/useProjectsLoader.tsx +++ b/src/hooks/useProjectsLoader.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react' import { ensureProjectDirectoryExists, listProjects } from '@src/lib/desktop' -import { isDesktop } from '@src/lib/isDesktop' import type { Project } from '@src/lib/project' import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils' import { trap } from '@src/lib/trap' @@ -17,7 +16,8 @@ export const useProjectsLoader = (deps?: [number]) => { useEffect(() => { // Useless on web, until we get fake filesystems over there. - if (!isDesktop()) return + if (!window.electron) return + const electron = window.electron if (deps && deps[0] === lastTs) return @@ -26,11 +26,14 @@ export const useProjectsLoader = (deps?: [number]) => { } ;(async () => { const { configuration } = await loadAndValidateSettings() - const _projectsDir = await ensureProjectDirectoryExists(configuration) + const _projectsDir = await ensureProjectDirectoryExists( + electron, + configuration + ) setProjectsDir(_projectsDir) if (projectsDir) { - const _projectPaths = await listProjects(configuration) + const _projectPaths = await listProjects(electron, configuration) setProjectPaths(_projectPaths) } })().catch(trap) diff --git a/src/index.tsx b/src/index.tsx index 7c51042bf2b..8b1abd6bf2b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,7 +10,6 @@ import '@src/index.css' import { initPromise } from '@src/lang/wasmUtils' import { AUTO_UPDATER_TOAST_ID } from '@src/lib/constants' import { initializeWindowExceptionHandler } from '@src/lib/exceptions' -import { isDesktop } from '@src/lib/isDesktop' import { markOnce } from '@src/lib/performance' import { reportRejection } from '@src/lib/trap' import { appActor, systemIOActor, commandBarActor } from '@src/lib/singletons' @@ -72,7 +71,7 @@ root.render( // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals() -if (isDesktop()) { +if (window.electron) { window.electron.onUpdateChecking(() => { const message = `Checking for updates...` console.log(message) @@ -98,6 +97,7 @@ if (isDesktop()) { }) }) + const electron = window.electron window.electron.onUpdateDownloaded(({ version, releaseNotes }) => { const message = `A new update (${version}) was downloaded and will be available next time you open the app.` console.log(message) @@ -106,7 +106,7 @@ if (isDesktop()) { version, releaseNotes, onRestart: () => { - window.electron.appRestart() + electron.appRestart() }, onDismiss: () => {}, }), diff --git a/src/lang/codeManager.ts b/src/lang/codeManager.ts index eac4b828a20..7dbfc31c035 100644 --- a/src/lang/codeManager.ts +++ b/src/lang/codeManager.ts @@ -137,7 +137,8 @@ export default class CodeManager { async writeToFile() { if (this.isBufferMode) return - if (isDesktop()) { + if (window.electron) { + const electron = window.electron // Only write our buffer contents to file once per second. Any faster // and file-system watchers which read, will receive empty data during // writes. @@ -152,7 +153,7 @@ export default class CodeManager { // Wait one event loop to give a chance for params to be set // Save the file to disk - window.electron + electron .writeFile(this._currentFilePath, this.code ?? '') .then(resolve) .catch((err: Error) => { diff --git a/src/lang/std/fileSystemManager.ts b/src/lang/std/fileSystemManager.ts index 462a4d7a1aa..c76bc516d03 100644 --- a/src/lang/std/fileSystemManager.ts +++ b/src/lang/std/fileSystemManager.ts @@ -1,25 +1,37 @@ // Polyfill window.electron fs functions as needed when in a nodejs context -// (INTENDED FOR VITEST SHINANGANS.) +// (INTENDED FOR VITEST SHENANIGANS.) + +import type { IElectronAPI } from '@root/interface' +import type { ObjectEncodingOptions, OpenMode } from 'fs' +import type { Abortable } from 'events' +// @ts-ignore This lib doesn't have types. +import * as nodePath from '@chainner/node-path' + +export interface IFs { + readdir: (path: string) => Promise + readFile: IElectronAPI['readFile'] + stat: (path: string) => Promise> +} + +let testNodeFs if (process.env.NODE_ENV === 'test' && process.env.VITEST) { const fs = require('node:fs/promises') - const path = require('node:path') - Object.assign(window, { - electron: { - readFile: fs.readFile, - stat: fs.stat, - readdir: fs.readdir, - path, - process: {}, - }, - }) + testNodeFs = fs } -/// FileSystemManager is a class that provides a way to read files from the local file system. -/// It assumes that you are in a project since it is solely used by the std lib -/// when executing code. -class FileSystemManager { +/// FileSystemManager is a class that provides a way to read files from the +/// local file system. The module's singleton instance assumes that you are in a +/// project since it is solely used by the std lib when executing code. +export class FileSystemManager { + private _nodePath: IElectronAPI['path'] + private _fs: IFs | undefined private _dir: string | null = null + constructor(nodePath: IElectronAPI['path'], fs: IFs | undefined) { + this._nodePath = nodePath + this._fs = fs + } + get dir() { return this._dir ?? '' } @@ -28,59 +40,101 @@ class FileSystemManager { this._dir = dir } - async join(dir: string, path: string): Promise { + get path() { + return this._nodePath + } + + join(dir: string, path: string): string { if (path.startsWith(dir)) { path = path.slice(dir.length) } - return Promise.resolve(window.electron.path.join(dir, path)) + return this._nodePath.join(dir, path) } - async readFile(path: string): Promise { + /** + * Called from WASM. + */ + async readFile( + path: string, + options?: { + encoding?: null | undefined + flag?: OpenMode | undefined + } | null + ): Promise + async readFile( + path: string, + options: + | { + encoding: BufferEncoding + flag?: OpenMode | undefined + } + | BufferEncoding + ): Promise + async readFile( + path: string, + options?: + | (ObjectEncodingOptions & + Abortable & { + flag?: OpenMode | undefined + }) + | BufferEncoding + | null + ): Promise { // Using local file system only works from desktop and nodejs - if (!window?.electron?.readFile) { + if (!this._fs) { return Promise.reject(new Error('No polyfill found for this function')) } - return this.join(this.dir, path).then((filePath) => { - return window.electron.readFile(filePath) - }) + const filePath = this.join(this.dir, path) + return this._fs.readFile(filePath, options) } - async exists(path: string): Promise { + /** + * Called from WASM. + */ + async exists(path: string): Promise { // Using local file system only works from desktop. - if (!window?.electron?.stat) { + if (!this._fs) { return Promise.reject(new Error('No polyfill found for this function')) } - return this.join(this.dir, path).then(async (file) => { - try { - await window.electron.stat(file) - } catch (e) { - if (e === 'ENOENT') { - return false - } + const file = this.join(this.dir, path) + try { + await this._fs.stat(file) + } catch (e) { + if (e === 'ENOENT') { + return false } - return true - }) + } + return true } - async getAllFiles(path: string): Promise { + /** + * Called from WASM. + */ + async getAllFiles(path: string): Promise { // Using local file system only works from desktop. - if (!window?.electron?.readdir) { + if (!this._fs) { return Promise.reject(new Error('No polyfill found for this function')) } - return this.join(this.dir, path).then((filepath) => { - return window.electron - .readdir(filepath) - .catch((error: Error) => { - return Promise.reject(new Error(`Error reading dir: ${error}`)) - }) - .then((files: string[]) => { - return files.map((filePath) => filePath) - }) - }) + const filepath = this.join(this.dir, path) + return await this._fs + .readdir(filepath) + .catch((error: Error) => { + return Promise.reject(new Error(`Error reading dir: ${error}`)) + }) + .then((files: string[]) => { + return files.map((filePath) => filePath) + }) } } -export const fileSystemManager = new FileSystemManager() +const fsInstance = + (typeof window !== 'undefined' ? window.electron : undefined) ?? testNodeFs +export const fsManager = new FileSystemManager(nodePath, fsInstance) + +/** + * The project directory is set on this. + */ +export const projectFsManager = new FileSystemManager(nodePath, fsInstance) diff --git a/src/lib/commandBarConfigs/applicationCommandConfig.ts b/src/lib/commandBarConfigs/applicationCommandConfig.ts index fffe8325d96..90b067e1cc3 100644 --- a/src/lib/commandBarConfigs/applicationCommandConfig.ts +++ b/src/lib/commandBarConfigs/applicationCommandConfig.ts @@ -237,11 +237,12 @@ export function createApplicationCommands({ systemIOActor, isProjectNew, }) - } else if (data.source === 'local' && data.path) { + } else if (window.electron && data.source === 'local' && data.path) { + const electron = window.electron const clonePath = data.path const fileNameWithExtension = getStringAfterLastSeparator(clonePath) const readFileContentsAndCreateNewFile = async () => { - const text = await window.electron.readFile(clonePath, 'utf8') + const text = await electron.readFile(clonePath, 'utf8') systemIOActor.send({ type: SystemIOMachineEvents.importFileFromURL, data: { @@ -366,7 +367,7 @@ export function createApplicationCommands({ hidden: !isDesktop(), defaultValue: '', valueSummary: (value) => { - return isDesktop() ? window.electron.path.basename(value) : '' + return window.electron ? window.electron.path.basename(value) : '' }, required: (commandContext) => isDesktop() && @@ -462,11 +463,15 @@ export function createApplicationCommands({ icon: 'importFile', groupId: 'application', onSubmit: (data) => { + if (!window.electron) { + console.error(new Error('No file system present')) + return + } if (data) { const requestedEnvironmentFormatted = returnSelfOrGetHostNameFromURL( data.environment ) - writeEnvironmentFile(requestedEnvironmentFormatted) + writeEnvironmentFile(window.electron, requestedEnvironmentFormatted) .then(() => { // Reload the application and it will trigger the correct sign in workflow for the new environment window.location.reload() @@ -490,10 +495,18 @@ export function createApplicationCommands({ icon: 'importFile', groupId: 'application', onSubmit: (data) => { + if (!window.electron) { + console.error(new Error('No file system present')) + return + } if (data) { const environmentName = env().VITE_KITTYCAD_BASE_DOMAIN if (environmentName) - writeEnvironmentConfigurationPool(environmentName, data.pool) + writeEnvironmentConfigurationPool( + window.electron, + environmentName, + data.pool + ) .then(() => { // Reload the application and it will trigger the correct sign in workflow for the new environment window.location.reload() diff --git a/src/lib/coredump.ts b/src/lib/coredump.ts index 40040871fc6..cd726ab35d2 100644 --- a/src/lib/coredump.ts +++ b/src/lib/coredump.ts @@ -78,7 +78,7 @@ export class CoreDumpManager { // Get the os information. getOsInfo(): string { - if (this.isDesktop()) { + if (window.electron) { const osinfo: OsInfo = { platform: window.electron.platform ?? null, arch: window.electron.arch ?? null, diff --git a/src/lib/desktop.test.ts b/src/lib/desktop.test.ts index 669e124ddfc..d51676ed4de 100644 --- a/src/lib/desktop.test.ts +++ b/src/lib/desktop.test.ts @@ -134,11 +134,13 @@ describe('desktop utilities', () => { describe('listProjects', () => { it('does not list .git directories', async () => { - const projects = await listProjects(mockConfig) + if (!window.electron) throw new Error('Electron not found') + const projects = await listProjects(window.electron, mockConfig) expect(projects.map((p) => p.name)).not.toContain('.git') }) it('lists projects excluding hidden and without .kcl files', async () => { - const projects = await listProjects(mockConfig) + if (!window.electron) throw new Error('Electron not found') + const projects = await listProjects(window.electron, mockConfig) // Verify only non-dot projects with .kcl files were included expect(projects.map((p) => p.name)).toEqual([ @@ -161,7 +163,8 @@ describe('desktop utilities', () => { }) it('correctly counts directories and files', async () => { - const projects = await listProjects(mockConfig) + if (!window.electron) throw new Error('Electron not found') + const projects = await listProjects(window.electron, mockConfig) // Verify that directories and files are counted correctly expect(projects[0].directory_count).toEqual(1) expect(projects[0].kcl_file_count).toEqual(2) @@ -170,10 +173,11 @@ describe('desktop utilities', () => { }) it('handles empty project directory', async () => { + if (!window.electron) throw new Error('Electron not found') // Adjust mockFileSystem to simulate empty directory mockFileSystem['/test/projects'] = [] - const projects = await listProjects(mockConfig) + const projects = await listProjects(window.electron, mockConfig) expect(projects).toEqual([]) }) @@ -181,21 +185,33 @@ describe('desktop utilities', () => { describe('getEnvironmentConfigurationPath', () => { it('should return a wonky path because appConfig is not set by default', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = '/appData//envs/development.json' - const actual = await getEnvironmentConfigurationPath('development') + const actual = await getEnvironmentConfigurationPath( + window.electron, + 'development' + ) expect(actual).toBe(expected) }) it('should return path to the configuration file for development', async () => { const expected = '/appData/zoo-modeling-app/envs/development.json' + if (!window.electron) throw new Error('Electron not found') mockElectron.packageJson.name = 'zoo-modeling-app' - const actual = await getEnvironmentConfigurationPath('development') + const actual = await getEnvironmentConfigurationPath( + window.electron, + 'development' + ) mockElectron.packageJson.name = '' expect(actual).toBe(expected) }) it('should return path to the configuration file for production', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = '/appData/zoo-modeling-app/envs/production.json' mockElectron.packageJson.name = 'zoo-modeling-app' - const actual = await getEnvironmentConfigurationPath('production') + const actual = await getEnvironmentConfigurationPath( + window.electron, + 'production' + ) mockElectron.packageJson.name = '' expect(actual).toBe(expected) }) @@ -203,14 +219,16 @@ describe('desktop utilities', () => { describe('getEnvironmentPath', () => { it('should return a wonky path because appConfig is not set by default', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = '/appData//environment.txt' - const actual = await getEnvironmentFilePath() + const actual = await getEnvironmentFilePath(window.electron) expect(actual).toBe(expected) }) it('should return path to the environment.txt file', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = '/appData/zoo-modeling-app/environment.txt' mockElectron.packageJson.name = 'zoo-modeling-app' - const actual = await getEnvironmentFilePath() + const actual = await getEnvironmentFilePath(window.electron) mockElectron.packageJson.name = '' expect(actual).toBe(expected) }) @@ -218,11 +236,16 @@ describe('desktop utilities', () => { describe('readEnvironmentConfigurationFile', () => { it('should return null for development', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = null - const actual = await readEnvironmentConfigurationFile('dev.zoo.dev') + const actual = await readEnvironmentConfigurationFile( + window.electron, + 'dev.zoo.dev' + ) expect(actual).toBe(expected) }) it('should return a empty string object for development', async () => { + if (!window.electron) throw new Error('Electron not found') mockElectron.exists.mockImplementation(() => true) mockElectron.readFile.mockImplementation(() => { return '{"token":"","pool":"","domain":"dev.zoo.dev"}' @@ -233,13 +256,17 @@ describe('desktop utilities', () => { pool: '', token: '', } - const actual = await readEnvironmentConfigurationFile('dev.zoo.dev') + const actual = await readEnvironmentConfigurationFile( + window.electron, + 'dev.zoo.dev' + ) // mock clean up mockElectron.packageJson.name = '' expect(actual).toStrictEqual(expected) }) it('should return an empty string object for production', async () => { + if (!window.electron) throw new Error('Electron not found') mockElectron.exists.mockImplementation(() => true) mockElectron.readFile.mockImplementation(() => { return '{"token":"","pool":"","domain":"zoo.dev"}' @@ -250,7 +277,10 @@ describe('desktop utilities', () => { pool: '', token: '', } - const actual = await readEnvironmentConfigurationFile('zoo.dev') + const actual = await readEnvironmentConfigurationFile( + window.electron, + 'zoo.dev' + ) // mock clean up mockElectron.packageJson.name = '' @@ -260,25 +290,28 @@ describe('desktop utilities', () => { describe('readEnvironmentFile', () => { it('should return the empty string', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = '' - const actual = await readEnvironmentFile() + const actual = await readEnvironmentFile(window.electron) expect(actual).toBe(expected) }) it('should return development', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = 'dev.zoo.dev' mockElectron.exists.mockImplementation(() => true) mockElectron.readFile.mockImplementation(() => 'dev.zoo.dev') mockElectron.packageJson.name = 'zoo-modeling-app' - const actual = await readEnvironmentFile() + const actual = await readEnvironmentFile(window.electron) mockElectron.packageJson.name = '' expect(actual).toBe(expected) }) it('should return production', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = 'zoo.dev' mockElectron.exists.mockImplementation(() => true) mockElectron.readFile.mockImplementation(() => 'zoo.dev') mockElectron.packageJson.name = 'zoo-modeling-app' - const actual = await readEnvironmentFile() + const actual = await readEnvironmentFile(window.electron) mockElectron.packageJson.name = '' expect(actual).toBe(expected) }) @@ -286,35 +319,51 @@ describe('desktop utilities', () => { describe('readEnvironmentConfigurationToken', () => { it('should return the empty string for dev.zoo.dev', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = '' - const actual = await readEnvironmentConfigurationToken('dev.zoo.dev') + const actual = await readEnvironmentConfigurationToken( + window.electron, + 'dev.zoo.dev' + ) expect(actual).toBe(expected) }) it('should return the empty string for production', async () => { + if (!window.electron) throw new Error('Electron not found') const expected = '' - const actual = await readEnvironmentConfigurationToken('zoo.dev') + const actual = await readEnvironmentConfigurationToken( + window.electron, + 'zoo.dev' + ) expect(actual).toBe(expected) }) it('should return the string dog-dog-dog for development', async () => { + if (!window.electron) throw new Error('Electron not found') mockElectron.exists.mockImplementation(() => true) mockElectron.readFile.mockImplementation(() => { return '{"token":"dog-dog-dog","pool":"","domain":"development"}' }) mockElectron.packageJson.name = 'zoo-modeling-app' const expected = 'dog-dog-dog' - const actual = await readEnvironmentConfigurationToken('development') + const actual = await readEnvironmentConfigurationToken( + window.electron, + 'development' + ) // mock clean up mockElectron.packageJson.name = '' expect(actual).toBe(expected) }) it('should return the string cat-cat-cat for production', async () => { + if (!window.electron) throw new Error('Electron not found') mockElectron.exists.mockImplementation(() => true) mockElectron.readFile.mockImplementation(() => { return '{"token":"cat-cat-cat","pool":"","domain":"production"}' }) mockElectron.packageJson.name = 'zoo-modeling-app' const expected = 'cat-cat-cat' - const actual = await readEnvironmentConfigurationToken('production') + const actual = await readEnvironmentConfigurationToken( + window.electron, + 'production' + ) // mock clean up mockElectron.packageJson.name = '' expect(actual).toBe(expected) diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index 2b62ce795a6..19f6ab62ded 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -10,6 +10,7 @@ import { parseProjectSettings, } from '@src/lang/wasm' import { initPromise, relevantFileExtensions } from '@src/lang/wasmUtils' +import { fsManager } from '@src/lang/std/fileSystemManager' import type { EnvironmentConfiguration } from '@src/lib/constants' import { DEFAULT_DEFAULT_LENGTH_UNIT, @@ -29,8 +30,10 @@ import type { DeepPartial } from '@src/lib/types' import { getInVariableCase } from '@src/lib/utils' import { IS_STAGING } from '@src/routes/utils' import { withAPIBaseURL } from '@src/lib/withBaseURL' +import type { IElectronAPI } from '@root/interface' export async function renameProjectDirectory( + electron: IElectronAPI, projectPath: string, newName: string ): Promise { @@ -39,7 +42,7 @@ export async function renameProjectDirectory( } try { - await window.electron.stat(projectPath) + await electron.stat(projectPath) } catch (e) { if (e === 'ENOENT') { return Promise.reject(new Error(`Path ${projectPath} is not a directory`)) @@ -47,12 +50,12 @@ export async function renameProjectDirectory( } // Make sure the new name does not exist. - const newPath = window.electron.path.join( - window.electron.path.dirname(projectPath), + const newPath = fsManager.path.join( + fsManager.path.dirname(projectPath), newName ) try { - await window.electron.stat(newPath) + await electron.stat(newPath) // If we get here it means the stat succeeded and there's a file already // with the same name... return Promise.reject( @@ -63,7 +66,7 @@ export async function renameProjectDirectory( } catch (e) { // Otherwise if it failed and the failure is "it doesn't exist" then rename it! if (e === 'ENOENT') { - await window.electron.rename(projectPath, newPath) + await electron.rename(projectPath, newPath) return newPath } } @@ -71,6 +74,7 @@ export async function renameProjectDirectory( } export async function ensureProjectDirectoryExists( + electron: IElectronAPI, config: DeepPartial ): Promise { const projectDir = config.settings?.project?.directory @@ -79,22 +83,25 @@ export async function ensureProjectDirectoryExists( return Promise.reject(new Error('projectDir is falsey')) } try { - await window.electron.stat(projectDir) + await electron.stat(projectDir) } catch (e) { if (e === 'ENOENT') { - await window.electron.mkdir(projectDir, { recursive: true }) + await electron.mkdir(projectDir, { recursive: true }) } } return projectDir } -export async function mkdirOrNOOP(directoryPath: string) { +export async function mkdirOrNOOP( + electron: IElectronAPI, + directoryPath: string +) { try { - await window.electron.stat(directoryPath) + await electron.stat(directoryPath) } catch (e) { if (e === 'ENOENT') { - await window.electron.mkdir(directoryPath, { recursive: true }) + await electron.mkdir(directoryPath, { recursive: true }) } } @@ -102,17 +109,18 @@ export async function mkdirOrNOOP(directoryPath: string) { } export async function createNewProjectDirectory( + electron: IElectronAPI, projectName: string, initialCode?: string, configuration?: DeepPartial | Error, initialFileName?: string ): Promise { if (!configuration) { - configuration = await readAppSettingsFile() + configuration = await readAppSettingsFile(electron) } if (err(configuration)) return Promise.reject(configuration) - const mainDir = await ensureProjectDirectoryExists(configuration) + const mainDir = await ensureProjectDirectoryExists(electron, configuration) if (!projectName) { return Promise.reject('Project name cannot be empty.') @@ -121,18 +129,18 @@ export async function createNewProjectDirectory( if (!mainDir) { return Promise.reject(new Error('mainDir is falsey')) } - const projectDir = window.electron.path.join(mainDir, projectName) + const projectDir = fsManager.path.join(mainDir, projectName) try { - await window.electron.stat(projectDir) + await electron.stat(projectDir) } catch (e) { if (e === 'ENOENT') { - await window.electron.mkdir(projectDir, { recursive: true }) + await electron.mkdir(projectDir, { recursive: true }) } } const kclFileName = initialFileName || PROJECT_ENTRYPOINT - const projectFile = window.electron.path.join(projectDir, kclFileName) + const projectFile = fsManager.path.join(projectDir, kclFileName) // When initialCode is present, we're loading existing code. If it's not // present, we're creating a new project, and we want to incorporate the // user's settings. @@ -141,10 +149,10 @@ export async function createNewProjectDirectory( configuration?.settings?.modeling?.base_unit ?? DEFAULT_DEFAULT_LENGTH_UNIT ) if (err(codeToWrite)) return Promise.reject(codeToWrite) - await window.electron.writeFile(projectFile, codeToWrite) + await electron.writeFile(projectFile, codeToWrite) let metadata: FileMetadata | null = null try { - metadata = await window.electron.stat(projectFile) + metadata = await electron.stat(projectFile) } catch (e) { if (e === 'ENOENT') { console.error('File does not exist') @@ -168,6 +176,7 @@ export async function createNewProjectDirectory( } export async function listProjects( + electron: IElectronAPI, configuration?: DeepPartial | Error ): Promise { // Make sure we have wasm initialized. @@ -177,22 +186,22 @@ export async function listProjects( } if (configuration === undefined) { - configuration = await readAppSettingsFile().catch((e) => { + configuration = await readAppSettingsFile(electron).catch((e) => { console.error(e) return e }) } if (err(configuration) || !configuration) return Promise.reject(configuration) - const projectDir = await ensureProjectDirectoryExists(configuration) + const projectDir = await ensureProjectDirectoryExists(electron, configuration) const projects = [] if (!projectDir) return Promise.reject(new Error('projectDir was falsey')) // Gotcha: readdir will list all folders at this project directory even if you do not have readwrite access on the directory path - const entries = await window.electron.readdir(projectDir) + const entries = await electron.readdir(projectDir) const { value: canReadWriteProjectDirectory } = - await window.electron.canReadWriteDirectory(projectDir) + await electron.canReadWriteDirectory(projectDir) for (let entry of entries) { // Skip directories that start with a dot @@ -200,16 +209,16 @@ export async function listProjects( continue } - const projectPath = window.electron.path.join(projectDir, entry) + const projectPath = electron.path.join(projectDir, entry) // if it's not a directory ignore. // Gotcha: statIsDirectory will work even if you do not have read write permissions on the project path - const isDirectory = await window.electron.statIsDirectory(projectPath) + const isDirectory = await electron.statIsDirectory(projectPath) if (!isDirectory) { continue } - const project = await getProjectInfo(projectPath) + const project = await getProjectInfo(electron, projectPath) if ( project.kcl_file_count === 0 && @@ -227,15 +236,17 @@ export async function listProjects( } const collectAllFilesRecursiveFrom = async ( + electron: IElectronAPI, path: string, canReadWritePath: boolean, fileExtensionsForFilter: string[] ) => { const isRelevantFile = (filename: string): boolean => fileExtensionsForFilter.some((ext) => filename.endsWith('.' + ext)) + // Make sure the filesystem object exists. try { - await window.electron.stat(path) + await electron.stat(path) } catch (e) { if (e === 'ENOENT') { return Promise.reject(new Error(`Directory ${path} does not exist`)) @@ -243,12 +254,12 @@ const collectAllFilesRecursiveFrom = async ( } // Make sure the path is a directory. - const isPathDir = await window.electron.statIsDirectory(path) + const isPathDir = await electron.statIsDirectory(path) if (!isPathDir) { return Promise.reject(new Error(`Path ${path} is not a directory`)) } - const name = window.electron.path.basename(path) + const name = electron.path.basename(path) let entry: FileEntry = { name: name, @@ -263,7 +274,7 @@ const collectAllFilesRecursiveFrom = async ( const children = [] - const entries = await window.electron.readdir(path) + const entries = await electron.readdir(path) // Sort all entries so files come first and directories last // so a top-most KCL file is returned first. @@ -283,11 +294,12 @@ const collectAllFilesRecursiveFrom = async ( continue } - const ePath = window.electron.path.join(path, e) - const isEDir = await window.electron.statIsDirectory(ePath) + const ePath = electron.path.join(path, e) + const isEDir = await electron.statIsDirectory(ePath) if (isEDir) { const subChildren = await collectAllFilesRecursiveFrom( + electron, ePath, canReadWritePath, fileExtensionsForFilter @@ -314,35 +326,33 @@ const collectAllFilesRecursiveFrom = async ( } export async function getDefaultKclFileForDir( + electron: IElectronAPI, projectDir: string, file: FileEntry ) { // Make sure the dir is a directory. - const isFileEntryDir = await window.electron.statIsDirectory(projectDir) + const isFileEntryDir = await electron.statIsDirectory(projectDir) if (!isFileEntryDir) { return Promise.reject(new Error(`Path ${projectDir} is not a directory`)) } - let defaultFilePath = window.electron.path.join( - projectDir, - PROJECT_ENTRYPOINT - ) + let defaultFilePath = electron.path.join(projectDir, PROJECT_ENTRYPOINT) try { - await window.electron.stat(defaultFilePath) + await electron.stat(defaultFilePath) } catch (e) { if (e === 'ENOENT') { // Find a kcl file in the directory. if (file.children) { for (let entry of file.children) { if (entry.name.endsWith('.kcl')) { - return window.electron.path.join(projectDir, entry.name) + return electron.path.join(projectDir, entry.name) } else if ((entry.children?.length ?? 0) > 0) { // Recursively find a kcl file in the directory. - return getDefaultKclFileForDir(entry.path, entry) + return getDefaultKclFileForDir(electron, entry.path, entry) } } // If we didn't find a kcl file, create one. - await window.electron.writeFile(defaultFilePath, '') + await electron.writeFile(defaultFilePath, '') return defaultFilePath } } @@ -386,11 +396,14 @@ const directoryCount = (file: FileEntry) => { return count } -export async function getProjectInfo(projectPath: string): Promise { +export async function getProjectInfo( + electron: IElectronAPI, + projectPath: string +): Promise { // Check the directory. let metadata try { - metadata = await window.electron.stat(projectPath) + metadata = await electron.stat(projectPath) } catch (e) { if (e === 'ENOENT') { return Promise.reject( @@ -400,7 +413,7 @@ export async function getProjectInfo(projectPath: string): Promise { } // Make sure it is a directory. - const projectPathIsDir = await window.electron.statIsDirectory(projectPath) + const projectPathIsDir = await electron.statIsDirectory(projectPath) if (!projectPathIsDir) { return Promise.reject( @@ -410,11 +423,12 @@ export async function getProjectInfo(projectPath: string): Promise { // Detect the projectPath has read write permission const { value: canReadWriteProjectPath } = - await window.electron.canReadWriteDirectory(projectPath) + await electron.canReadWriteDirectory(projectPath) const fileExtensionsForFilter = relevantFileExtensions() // Return walked early if canReadWriteProjectPath is false let walked = await collectAllFilesRecursiveFrom( + electron, projectPath, canReadWriteProjectPath, fileExtensionsForFilter @@ -424,7 +438,7 @@ export async function getProjectInfo(projectPath: string): Promise { let default_file = '' if (canReadWriteProjectPath) { // Create the default main.kcl file only if the project path has read write permissions - default_file = await getDefaultKclFileForDir(projectPath, walked) + default_file = await getDefaultKclFileForDir(electron, projectPath, walked) } let project = { @@ -457,12 +471,16 @@ export async function getProjectInfo(projectPath: string): Promise { // Write project settings file. export async function writeProjectSettingsFile( + electron: IElectronAPI, projectPath: string, tomlStr: string ): Promise { - const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath) + const projectSettingsFilePath = await getProjectSettingsFilePath( + electron, + projectPath + ) if (err(tomlStr)) return Promise.reject(tomlStr) - return window.electron.writeFile(projectSettingsFilePath, tomlStr) + return electron.writeFile(projectSettingsFilePath, tomlStr) } // Important for saving settings. @@ -471,170 +489,174 @@ const APP_ID = IS_STAGING ? 'dev.zoo.modeling-app-staging' : 'dev.zoo.modeling-app' -const getAppFolderName = () => { - if (window.electron.os.isMac || window.electron.os.isWindows) { +const getAppFolderName = (electron: IElectronAPI) => { + if (electron.os.isMac || electron.os.isWindows) { return APP_ID } // TODO: we need to make linux use the same convention this is weird // This variable below gets the -staging suffix on staging too thru scripts/flip-files-to-staging.sh // But it should be consistent with the reserve domain app id we use on Windows and Linux - return window.electron.packageJson.name + return electron.packageJson.name } -export const getAppSettingsFilePath = async () => { - const isTestEnv = window.electron.process.env.NODE_ENV === 'test' - const testSettingsPath = await window.electron.getAppTestProperty( +export const getAppSettingsFilePath = async (electron: IElectronAPI) => { + const isTestEnv = electron.process.env.NODE_ENV === 'test' + const testSettingsPath = await electron.getAppTestProperty( 'TEST_SETTINGS_FILE_KEY' ) - const appConfig = await window.electron.getPath('appData') + const appConfig = await electron.getPath('appData') const fullPath = isTestEnv - ? window.electron.path.resolve(testSettingsPath, '..') - : window.electron.path.resolve(appConfig, getAppFolderName()) + ? fsManager.path.resolve(testSettingsPath, '..') + : fsManager.path.resolve(appConfig, getAppFolderName(electron)) try { - await window.electron.stat(fullPath) + await electron.stat(fullPath) } catch (e) { // File/path doesn't exist if (e === 'ENOENT') { - await window.electron.mkdir(fullPath, { recursive: true }) + await electron.mkdir(fullPath, { recursive: true }) } } - return window.electron.path.join(fullPath, SETTINGS_FILE_NAME) + return fsManager.path.join(fullPath, SETTINGS_FILE_NAME) } -export const getEnvironmentConfigurationFolderPath = async () => { - const isTestEnv = window.electron.process.env.NODE_ENV === 'test' - const testSettingsPath = await window.electron.getAppTestProperty( +export const getEnvironmentConfigurationFolderPath = async ( + electron: IElectronAPI +) => { + const isTestEnv = electron.process.env.NODE_ENV === 'test' + const testSettingsPath = await electron.getAppTestProperty( 'TEST_SETTINGS_FILE_KEY' ) - const appConfig = await window.electron.getPath('appData') + const appConfig = await electron.getPath('appData') const fullPath = isTestEnv - ? window.electron.path.resolve(testSettingsPath, '..') - : window.electron.path.join( + ? electron.path.resolve(testSettingsPath, '..') + : electron.path.join( appConfig, - getAppFolderName(), + getAppFolderName(electron), ENVIRONMENT_CONFIGURATION_FOLDER ) return fullPath } export const getEnvironmentConfigurationPath = async ( + electron: IElectronAPI, environmentName: string ) => { - const fullPath = await getEnvironmentConfigurationFolderPath() + const fullPath = await getEnvironmentConfigurationFolderPath(electron) try { - await window.electron.stat(fullPath) + await electron.stat(fullPath) } catch (e) { // File/path doesn't exist if (e === 'ENOENT') { - await window.electron.mkdir(fullPath, { recursive: true }) + await electron.mkdir(fullPath, { recursive: true }) } } // /envs/.json e.g. /envs/dev.zoo.dev.json - return window.electron.path.join(fullPath, environmentName + '.json') + return electron.path.join(fullPath, environmentName + '.json') } -export const getEnvironmentFilePath = async () => { - const isTestEnv = window.electron.process.env.NODE_ENV === 'test' - const testSettingsPath = await window.electron.getAppTestProperty( +export const getEnvironmentFilePath = async (electron: IElectronAPI) => { + const isTestEnv = electron.process.env.NODE_ENV === 'test' + const testSettingsPath = await electron.getAppTestProperty( 'TEST_SETTINGS_FILE_KEY' ) - const appConfig = await window.electron.getPath('appData') + const appConfig = await electron.getPath('appData') const fullPath = isTestEnv - ? window.electron.path.resolve(testSettingsPath, '..') - : window.electron.path.join(appConfig, getAppFolderName()) + ? electron.path.resolve(testSettingsPath, '..') + : electron.path.join(appConfig, getAppFolderName(electron)) try { - await window.electron.stat(fullPath) + await electron.stat(fullPath) } catch (e) { // File/path doesn't exist if (e === 'ENOENT') { - await window.electron.mkdir(fullPath, { recursive: true }) + await electron.mkdir(fullPath, { recursive: true }) } } - return window.electron.path.join(fullPath, ENVIRONMENT_FILE_NAME) + return electron.path.join(fullPath, ENVIRONMENT_FILE_NAME) } -const getTelemetryFilePath = async () => { - const isTestEnv = window.electron.process.env.NODE_ENV === 'test' - const testSettingsPath = await window.electron.getAppTestProperty( +const getTelemetryFilePath = async (electron: IElectronAPI) => { + const isTestEnv = electron.process.env.NODE_ENV === 'test' + const testSettingsPath = await electron.getAppTestProperty( 'TEST_SETTINGS_FILE_KEY' ) - const appConfig = await window.electron.getPath('appData') + const appConfig = await electron.getPath('appData') const fullPath = isTestEnv - ? window.electron.path.resolve(testSettingsPath, '..') - : window.electron.path.join(appConfig, getAppFolderName()) + ? electron.path.resolve(testSettingsPath, '..') + : electron.path.join(appConfig, getAppFolderName(electron)) try { - await window.electron.stat(fullPath) + await electron.stat(fullPath) } catch (e) { // File/path doesn't exist if (e === 'ENOENT') { - await window.electron.mkdir(fullPath, { recursive: true }) + await electron.mkdir(fullPath, { recursive: true }) } } - return window.electron.path.join(fullPath, TELEMETRY_FILE_NAME) + return electron.path.join(fullPath, TELEMETRY_FILE_NAME) } -const getRawTelemetryFilePath = async () => { - const isTestEnv = window.electron.process.env.NODE_ENV === 'test' - const testSettingsPath = await window.electron.getAppTestProperty( +const getRawTelemetryFilePath = async (electron: IElectronAPI) => { + const isTestEnv = electron.process.env.NODE_ENV === 'test' + const testSettingsPath = await electron.getAppTestProperty( 'TEST_SETTINGS_FILE_KEY' ) - const appConfig = await window.electron.getPath('appData') + const appConfig = await electron.getPath('appData') const fullPath = isTestEnv - ? window.electron.path.resolve(testSettingsPath, '..') - : window.electron.path.join(appConfig, getAppFolderName()) + ? electron.path.resolve(testSettingsPath, '..') + : electron.path.join(appConfig, getAppFolderName(electron)) try { - await window.electron.stat(fullPath) + await electron.stat(fullPath) } catch (e) { // File/path doesn't exist if (e === 'ENOENT') { - await window.electron.mkdir(fullPath, { recursive: true }) + await electron.mkdir(fullPath, { recursive: true }) } } - return window.electron.path.join(fullPath, TELEMETRY_RAW_FILE_NAME) + return electron.path.join(fullPath, TELEMETRY_RAW_FILE_NAME) } -const getProjectSettingsFilePath = async (projectPath: string) => { +const getProjectSettingsFilePath = async ( + electron: IElectronAPI, + projectPath: string +) => { try { - await window.electron.stat(projectPath) + await electron.stat(projectPath) } catch (e) { if (e === 'ENOENT') { - await window.electron.mkdir(projectPath, { recursive: true }) + await electron.mkdir(projectPath, { recursive: true }) } } - return window.electron.path.join(projectPath, PROJECT_SETTINGS_FILE_NAME) + return fsManager.path.join(projectPath, PROJECT_SETTINGS_FILE_NAME) } -export const getInitialDefaultDir = async () => { - const isTestEnv = window.electron.process.env.NODE_ENV === 'test' - const testSettingsPath = await window.electron.getAppTestProperty( +export const getInitialDefaultDir = async (electron: IElectronAPI) => { + const isTestEnv = electron.process.env.NODE_ENV === 'test' + const testSettingsPath = await electron.getAppTestProperty( 'TEST_SETTINGS_FILE_KEY' ) - if (!window.electron) { - return '' - } if (isTestEnv) { return testSettingsPath } - const dir = await window.electron.getPath('documents') - return window.electron.path.join(dir, PROJECT_FOLDER) + const dir = await electron.getPath('documents') + return electron.path.join(dir, PROJECT_FOLDER) } export const readProjectSettingsFile = async ( + electron: IElectronAPI, projectPath: string ): Promise> => { - let settingsPath = await getProjectSettingsFilePath(projectPath) + let settingsPath = await getProjectSettingsFilePath(electron, projectPath) // Check if this file exists. try { - await window.electron.stat(settingsPath) + await electron.stat(settingsPath) } catch (e) { if (e === 'ENOENT') { // Return the default configuration. @@ -642,7 +664,7 @@ export const readProjectSettingsFile = async ( } } - const configToml = await window.electron.readFile(settingsPath, { + const configToml = await fsManager.readFile(settingsPath, { encoding: 'utf-8', }) const configObj = parseProjectSettings(configToml) @@ -655,15 +677,15 @@ export const readProjectSettingsFile = async ( /** * Read the app settings file, or creates an initial one if it doesn't exist. */ -export const readAppSettingsFile = async () => { - let settingsPath = await getAppSettingsFilePath() +export const readAppSettingsFile = async (electron: IElectronAPI) => { + let settingsPath = await getAppSettingsFilePath(electron) const initialProjectDirConfig: DeepPartial< NonNullable['settings']['project']> - > = { directory: await getInitialDefaultDir() } + > = { directory: await getInitialDefaultDir(electron) } // The file exists, read it and parse it. - if (window.electron.exists(settingsPath)) { - const configToml = await window.electron.readFile(settingsPath, { + if (await fsManager.exists(settingsPath)) { + const configToml = await fsManager.readFile(settingsPath, { encoding: 'utf-8', }) const parsedAppConfig = parseAppSettings(configToml) @@ -714,18 +736,22 @@ export const readAppSettingsFile = async () => { return mergedDefaultConfig } -export const writeAppSettingsFile = async (tomlStr: string) => { - const appSettingsFilePath = await getAppSettingsFilePath() +export const writeAppSettingsFile = async ( + electron: IElectronAPI, + tomlStr: string +) => { + const appSettingsFilePath = await getAppSettingsFilePath(electron) if (err(tomlStr)) return Promise.reject(tomlStr) - return window.electron.writeFile(appSettingsFilePath, tomlStr) + return electron.writeFile(appSettingsFilePath, tomlStr) } export const readEnvironmentConfigurationFile = async ( + electron: IElectronAPI, environmentName: string ): Promise => { - const path = await getEnvironmentConfigurationPath(environmentName) - if (window.electron.exists(path)) { - const configurationJSON: string = await window.electron.readFile(path, { + const path = await getEnvironmentConfigurationPath(electron, environmentName) + if (electron.exists(path)) { + const configurationJSON: string = await electron.readFile(path, { encoding: 'utf-8', }) if (!configurationJSON) return null @@ -735,40 +761,49 @@ export const readEnvironmentConfigurationFile = async ( } export const writeEnvironmentConfigurationToken = async ( + electron: IElectronAPI, environmentName: string, token: string ) => { environmentName = environmentName.trim() - const path = await getEnvironmentConfigurationPath(environmentName) - const environmentConfiguration = - await getEnvironmentConfigurationObject(environmentName) + const path = await getEnvironmentConfigurationPath(electron, environmentName) + const environmentConfiguration = await getEnvironmentConfigurationObject( + electron, + environmentName + ) environmentConfiguration.token = token const requestedConfiguration = JSON.stringify(environmentConfiguration) - const result = await window.electron.writeFile(path, requestedConfiguration) + const result = await electron.writeFile(path, requestedConfiguration) console.log(`wrote ${environmentName}.json to disk`) return result } export const writeEnvironmentConfigurationPool = async ( + electron: IElectronAPI, environmentName: string, pool: string ) => { pool = pool.trim() - const path = await getEnvironmentConfigurationPath(environmentName) - const environmentConfiguration = - await getEnvironmentConfigurationObject(environmentName) + const path = await getEnvironmentConfigurationPath(electron, environmentName) + const environmentConfiguration = await getEnvironmentConfigurationObject( + electron, + environmentName + ) environmentConfiguration.pool = pool const requestedConfiguration = JSON.stringify(environmentConfiguration) - const result = await window.electron.writeFile(path, requestedConfiguration) + const result = await electron.writeFile(path, requestedConfiguration) console.log(`wrote ${environmentName}.json to disk`) return result } export const getEnvironmentConfigurationObject = async ( + electron: IElectronAPI, environmentName: string ) => { - let environmentConfiguration = - await readEnvironmentConfigurationFile(environmentName) + let environmentConfiguration = await readEnvironmentConfigurationFile( + electron, + environmentName + ) if (environmentConfiguration === null) { const initialConfiguration: EnvironmentConfiguration = { token: '', @@ -781,33 +816,36 @@ export const getEnvironmentConfigurationObject = async ( } export const readEnvironmentConfigurationPool = async ( + electron: IElectronAPI, environmentName: string ) => { - const environmentConfiguration = - await readEnvironmentConfigurationFile(environmentName) + const environmentConfiguration = await readEnvironmentConfigurationFile( + electron, + environmentName + ) if (!environmentConfiguration?.pool) return '' return environmentConfiguration.pool.trim() } export const readEnvironmentConfigurationToken = async ( + electron: IElectronAPI, environmentName: string ) => { - const environmentConfiguration = - await readEnvironmentConfigurationFile(environmentName) + const environmentConfiguration = await readEnvironmentConfigurationFile( + electron, + environmentName + ) if (!environmentConfiguration?.token) return '' return environmentConfiguration.token.trim() } -export const readEnvironmentFile = async () => { - let environmentFilePath = await getEnvironmentFilePath() +export const readEnvironmentFile = async (electron: IElectronAPI) => { + let environmentFilePath = await getEnvironmentFilePath(electron) - if (window.electron.exists(environmentFilePath)) { - const environment: string = await window.electron.readFile( - environmentFilePath, - { - encoding: 'utf-8', - } - ) + if (electron.exists(environmentFilePath)) { + const environment: string = await electron.readFile(environmentFilePath, { + encoding: 'utf-8', + }) if (!environment) return '' return environment.trim() } @@ -818,18 +856,22 @@ export const readEnvironmentFile = async () => { * Store the last selected environment on disk to allow us to sign back into the correct * environment when they refresh the application or update the application. */ -export const writeEnvironmentFile = async (environment: string) => { +export const writeEnvironmentFile = async ( + electron: IElectronAPI, + environment: string +) => { environment = environment.trim() - const environmentFilePath = await getEnvironmentFilePath() + const environmentFilePath = await getEnvironmentFilePath(electron) if (err(environment)) return Promise.reject(environment) - const result = window.electron.writeFile(environmentFilePath, environment) + const result = electron.writeFile(environmentFilePath, environment) console.log('environment written to disk') return result } -export const listAllEnvironments = async () => { - const environmentFolder = await getEnvironmentConfigurationFolderPath() - const files = await window.electron.readdir(environmentFolder) +export const listAllEnvironments = async (electron: IElectronAPI) => { + const environmentFolder = + await getEnvironmentConfigurationFolderPath(electron) + const files = await electron.readdir(environmentFolder) const suffix = '.json' return files .filter((fileName: string) => { @@ -840,12 +882,12 @@ export const listAllEnvironments = async () => { }) } -export const listAllEnvironmentsWithTokens = async () => { - const environments = await listAllEnvironments() +export const listAllEnvironmentsWithTokens = async (electron: IElectronAPI) => { + const environments = await listAllEnvironments(electron) const environmentsWithTokens = [] for (let i = 0; i < environments.length; i++) { const environment = environments[i] - const token = await readEnvironmentConfigurationToken(environment) + const token = await readEnvironmentConfigurationToken(electron, environment) if (token) { environmentsWithTokens.push(environment) } @@ -853,16 +895,22 @@ export const listAllEnvironmentsWithTokens = async () => { return environmentsWithTokens } -export const writeTelemetryFile = async (content: string) => { - const telemetryFilePath = await getTelemetryFilePath() +export const writeTelemetryFile = async ( + electron: IElectronAPI, + content: string +) => { + const telemetryFilePath = await getTelemetryFilePath(electron) if (err(content)) return Promise.reject(content) - return window.electron.writeFile(telemetryFilePath, content) + return electron.writeFile(telemetryFilePath, content) } -export const writeRawTelemetryFile = async (content: string) => { - const rawTelemetryFilePath = await getRawTelemetryFilePath() +export const writeRawTelemetryFile = async ( + electron: IElectronAPI, + content: string +) => { + const rawTelemetryFilePath = await getRawTelemetryFilePath(electron) if (err(content)) return Promise.reject(content) - return window.electron.writeFile(rawTelemetryFilePath, content) + return electron.writeFile(rawTelemetryFilePath, content) } let appStateStore: Project | undefined = undefined @@ -890,26 +938,24 @@ export const getUser = async (token: string): Promise => { } export const writeProjectThumbnailFile = async ( + electron: IElectronAPI, dataUrl: string, projectDirectoryPath: string ) => { - const filePath = window.electron.path.join( - projectDirectoryPath, - PROJECT_IMAGE_NAME - ) + const filePath = electron.path.join(projectDirectoryPath, PROJECT_IMAGE_NAME) const data = atob(dataUrl.substring('data:image/png;base64,'.length)) const asArray = new Uint8Array(data.length) for (let i = 0, len = data.length; i < len; ++i) { asArray[i] = data.charCodeAt(i) } - return window.electron.writeFile(filePath, asArray) + return electron.writeFile(filePath, asArray) } export function getPathFilenameInVariableCase(path: string) { // from https://nodejs.org/en/learn/manipulating-files/nodejs-file-paths#example - const basenameNoExt = window.electron.path.basename( + const basenameNoExt = fsManager.path.basename( path, - window.electron.path.extname(path) + fsManager.path.extname(path) ) return getInVariableCase(basenameNoExt) } diff --git a/src/lib/desktopFS.ts b/src/lib/desktopFS.ts index e12225129ca..a15f27f7af5 100644 --- a/src/lib/desktopFS.ts +++ b/src/lib/desktopFS.ts @@ -1,6 +1,7 @@ +import type { IElectronAPI } from '@root/interface' +import { fsManager } from '@src/lang/std/fileSystemManager' import { relevantFileExtensions } from '@src/lang/wasmUtils' import { FILE_EXT, INDEX_IDENTIFIER, MAX_PADDING } from '@src/lib/constants' -import { isDesktop } from '@src/lib/isDesktop' import type { FileEntry } from '@src/lib/project' export const isHidden = (fileOrDir: FileEntry) => @@ -111,7 +112,7 @@ function getPaddedIdentifierRegExp() { } export async function getSettingsFolderPaths(projectPath?: string) { - const user = isDesktop() ? await window.electron.getPath('appData') : '/' + const user = window.electron ? await window.electron.getPath('appData') : '/' const project = projectPath !== undefined ? projectPath : undefined return { @@ -123,28 +124,30 @@ export async function getSettingsFolderPaths(projectPath?: string) { /** * Get the next available file name by appending a hyphen and number to the end of the name */ -export function getNextFileName({ +export async function getNextFileName({ + electron: _electron, entryName, baseDir, }: { + electron: IElectronAPI entryName: string baseDir: string }) { // Preserve the extension in case of a relevant but foreign file - let extension = window.electron.path.extname(entryName) + let extension = fsManager.path.extname(entryName) if (!relevantFileExtensions().includes(extension.replace('.', ''))) { extension = FILE_EXT } // Remove any existing index from the name before adding a new one let createdName = entryName.replace(extension, '') + extension - let createdPath = window.electron.path.join(baseDir, createdName) + let createdPath = fsManager.path.join(baseDir, createdName) let i = 1 - while (window.electron.exists(createdPath)) { + while (await fsManager.exists(createdPath)) { const matchOnIndexAndExtension = new RegExp(`(-\\d+)?(${extension})?$`) createdName = entryName.replace(matchOnIndexAndExtension, '') + `-${i}` + extension - createdPath = window.electron.path.join(baseDir, createdName) + createdPath = fsManager.path.join(baseDir, createdName) i++ } return { @@ -157,18 +160,20 @@ export function getNextFileName({ * Get the next available directory name by appending a hyphen and number to the end of the name */ export function getNextDirName({ + electron, entryName, baseDir, }: { + electron: IElectronAPI entryName: string baseDir: string }) { let createdName = entryName - let createdPath = window.electron.path.join(baseDir, createdName) + let createdPath = electron.path.join(baseDir, createdName) let i = 1 - while (window.electron.exists(createdPath)) { + while (electron.exists(createdPath)) { createdName = entryName.replace(/-\d+$/, '') + `-${i}` - createdPath = window.electron.path.join(baseDir, createdName) + createdPath = electron.path.join(baseDir, createdName) i++ } return { diff --git a/src/lib/exportSave.ts b/src/lib/exportSave.ts index 1272e9f3ffa..d75694d9349 100644 --- a/src/lib/exportSave.ts +++ b/src/lib/exportSave.ts @@ -3,12 +3,11 @@ import toast from 'react-hot-toast' import { browserSaveFile } from '@src/lib/browserSaveFile' import { EXPORT_TOAST_MESSAGES } from '@src/lib/constants' -import { isDesktop } from '@src/lib/isDesktop' import type ModelingAppFile from '@src/lib/modelingAppFile' const save_ = async (file: ModelingAppFile, toastId: string) => { try { - if (isDesktop()) { + if (window.electron) { const extension = file.name.split('.').pop() || null let extensions: string[] = [] if (extension !== null) { diff --git a/src/lib/openWindow.ts b/src/lib/openWindow.ts index e48a2e1ea00..67ebf6f3d1e 100644 --- a/src/lib/openWindow.ts +++ b/src/lib/openWindow.ts @@ -1,13 +1,11 @@ import type { MouseEventHandler } from 'react' -import { isDesktop } from '@src/lib/isDesktop' import { reportRejection } from '@src/lib/trap' export const openExternalBrowserIfDesktop = (to?: string) => function (e) { - if (isDesktop()) { - // Ignoring because currentTarget could be a few different things - // @ts-ignore + if (window.electron) { + // currentTarget could be a few different things window.electron .openExternal(to || e.currentTarget?.href) .catch(reportRejection) @@ -19,7 +17,7 @@ export const openExternalBrowserIfDesktop = (to?: string) => // Open a new browser window desktop style or browser style. export default async function openWindow(url: string) { - if (isDesktop()) { + if (window.electron) { await window.electron.openExternal(url) } else { window.open(url, '_blank') diff --git a/src/lib/paths.ts b/src/lib/paths.ts index 677c397768a..e9ff0401f2b 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -7,9 +7,10 @@ import { BROWSER_PROJECT_NAME, FILE_EXT, } from '@src/lib/constants' -import { isDesktop } from '@src/lib/isDesktop' import { err } from '@src/lib/trap' import type { DeepPartial } from '@src/lib/types' +import type { IElectronAPI } from '@root/interface' +import { fsManager } from '@src/lang/std/fileSystemManager' const SETTINGS = '/settings' @@ -35,19 +36,20 @@ export const PATHS = { export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}` export async function getProjectMetaByRouteId( - readAppSettingsFile: () => Promise>, + readAppSettingsFile: ( + electron: IElectronAPI + ) => Promise>, readLocalStorageAppSettingsFile: () => DeepPartial | Error, id?: string, configuration?: DeepPartial | Error ): Promise { if (!id) return undefined - const onDesktop = isDesktop() const isPlaywright = localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true' if (configuration === undefined || isPlaywright) { - configuration = onDesktop - ? await readAppSettingsFile() + configuration = window.electron + ? await readAppSettingsFile(window.electron) : readLocalStorageAppSettingsFile() } @@ -169,7 +171,7 @@ export function safeEncodeForRouterPaths(dynamicValue: string): string { * Works on all OS! */ export function getStringAfterLastSeparator(path: string): string { - return path.split(window.electron.sep).pop() || '' + return path.split(fsManager.path.sep).pop() || '' } /** @@ -236,13 +238,13 @@ export function webSafeJoin(paths: string[]): string { * Splits any paths safely based on the runtime */ export function desktopSafePathSplit(path: string): string[] { - return isDesktop() - ? path.split(window?.electron?.sep) + return window.electron + ? path.split(window.electron.sep) : webSafePathSplit(path) } export function desktopSafePathJoin(paths: string[]): string { - return isDesktop() ? paths.join(window?.electron?.sep) : webSafeJoin(paths) + return window.electron ? paths.join(window.electron.sep) : webSafeJoin(paths) } /** diff --git a/src/lib/routeLoaders.ts b/src/lib/routeLoaders.ts index 03d8980f745..f92283ea350 100644 --- a/src/lib/routeLoaders.ts +++ b/src/lib/routeLoaders.ts @@ -2,7 +2,7 @@ import type { LoaderFunction } from 'react-router-dom' import { redirect } from 'react-router-dom' import { waitFor } from 'xstate' -import { fileSystemManager } from '@src/lang/std/fileSystemManager' +import { projectFsManager } from '@src/lang/std/fileSystemManager' import { normalizeLineEndings } from '@src/lib/codeEditor' import { BROWSER_FILE_NAME, @@ -39,7 +39,7 @@ export const fileLoader: LoaderFunction = async ( const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH) const heuristicProjectFilePath = - isDesktop() && params.id + window.electron && params.id ? params.id .split(window.electron.sep) .slice(0, -1) @@ -63,11 +63,11 @@ export const fileLoader: LoaderFunction = async ( const urlObj = new URL(routerData.request.url) if (!urlObj.pathname.endsWith('/settings')) { - const fallbackFile = isDesktop() - ? (await getProjectInfo(projectPath)).default_file + const fallbackFile = window.electron + ? (await getProjectInfo(window.electron, projectPath)).default_file : '' let fileExists = isDesktop() - if (currentFilePath && fileExists) { + if (currentFilePath && fileExists && window.electron) { try { await window.electron.stat(currentFilePath) } catch (e) { @@ -88,7 +88,13 @@ export const fileLoader: LoaderFunction = async ( return redirect(requestUrlWithDefaultFile) } - if (!fileExists || !currentFileName || !currentFilePath || !projectName) { + if ( + !fileExists || + !currentFileName || + !currentFilePath || + !projectName || + !window.electron + ) { return redirect( `${PATHS.FILE}/${encodeURIComponent( isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT @@ -118,7 +124,7 @@ export const fileLoader: LoaderFunction = async ( // Set the file system manager to the project path // So that WASM gets an updated path for operations - fileSystemManager.dir = projectPath + projectFsManager.dir = projectPath const defaultProjectData = { name: projectName || 'unnamed', @@ -131,8 +137,8 @@ export const fileLoader: LoaderFunction = async ( readWriteAccess: true, } - const maybeProjectInfo = isDesktop() - ? await getProjectInfo(projectPath) + const maybeProjectInfo = window.electron + ? await getProjectInfo(window.electron, projectPath) : null const project = maybeProjectInfo ?? defaultProjectData diff --git a/src/lib/rustContext.ts b/src/lib/rustContext.ts index d592d73891d..6c829cc6436 100644 --- a/src/lib/rustContext.ts +++ b/src/lib/rustContext.ts @@ -11,7 +11,7 @@ import { BSON } from 'bson' import type { Models } from '@kittycad/lib/dist/types/src' import type { EngineCommandManager } from '@src/lang/std/engineConnection' -import { fileSystemManager } from '@src/lang/std/fileSystemManager' +import { projectFsManager } from '@src/lang/std/fileSystemManager' import type { ExecState } from '@src/lang/wasm' import { errFromErrWithOutputs, execStateFromRust } from '@src/lang/wasm' import { initPromise } from '@src/lang/wasmUtils' @@ -62,7 +62,7 @@ export default class RustContext { // resolve if you don't await it. const ctxInstance = await new this.rustInstance.Context( this.engineCommandManager, - fileSystemManager + projectFsManager ) return ctxInstance diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index 0efd8949319..106929451b3 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -1,4 +1,3 @@ -import { isDesktop } from '@src/lib/isDesktop' import { writeProjectThumbnailFile } from '@src/lib/desktop' export function takeScreenshotOfVideoStreamCanvas() { @@ -50,14 +49,19 @@ export function createThumbnailPNGOnDesktop({ }: { projectDirectoryWithoutEndingSlash: string }) { - if (isDesktop()) { + if (window.electron) { + const electron = window.electron setTimeout(() => { if (!projectDirectoryWithoutEndingSlash) { return } const dataUrl: string = takeScreenshotOfVideoStreamCanvas() // zoom to fit command does not wait, wait 500ms to see if zoom to fit finishes - writeProjectThumbnailFile(dataUrl, projectDirectoryWithoutEndingSlash) + writeProjectThumbnailFile( + electron, + dataUrl, + projectDirectoryWithoutEndingSlash + ) .then(() => {}) .catch((e) => { console.error( diff --git a/src/lib/settings/initialSettings.tsx b/src/lib/settings/initialSettings.tsx index 48983811b0f..f833cbdc0a4 100644 --- a/src/lib/settings/initialSettings.tsx +++ b/src/lib/settings/initialSettings.tsx @@ -333,6 +333,9 @@ export function createSettings() { ) { updateValue(inputRefVal) } else { + if (!window.electron) { + return Promise.reject(new Error("Can't open file dialog")) + } const newPath = await window.electron.open({ properties: ['openDirectory', 'createDirectory'], defaultPath: value, diff --git a/src/lib/settings/settingsUtils.ts b/src/lib/settings/settingsUtils.ts index a5a01613584..a7645603a53 100644 --- a/src/lib/settings/settingsUtils.ts +++ b/src/lib/settings/settingsUtils.ts @@ -322,11 +322,9 @@ export async function loadAndValidateSettings( // Make sure we have wasm initialized. await initPromise - const onDesktop = isDesktop() - // Load the app settings from the file system or localStorage. - const appSettingsPayload = onDesktop - ? await readAppSettingsFile() + const appSettingsPayload = window.electron + ? await readAppSettingsFile(window.electron) : readLocalStorageAppSettingsFile() if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload) @@ -334,8 +332,10 @@ export async function loadAndValidateSettings( let settingsNext = createSettings() // Because getting the default directory is async, we need to set it after - if (onDesktop) { - settingsNext.app.projectDirectory.default = await getInitialDefaultDir() + if (window.electron) { + settingsNext.app.projectDirectory.default = await getInitialDefaultDir( + window.electron + ) } settingsNext = setSettingsAtLevel( @@ -346,8 +346,8 @@ export async function loadAndValidateSettings( // Load the project settings if they exist if (projectPath) { - let projectSettings = onDesktop - ? await readProjectSettingsFile(projectPath) + let projectSettings = window.electron + ? await readProjectSettingsFile(window.electron, projectPath) : readLocalStorageProjectSettingsFile() if (err(projectSettings)) @@ -376,8 +376,12 @@ export async function loadAndValidateSettings( return Promise.reject( new Error('Could not serialize project configuration') ) - if (onDesktop) { - await writeProjectSettingsFile(projectPath, projectTomlString) + if (window.electron) { + await writeProjectSettingsFile( + window.electron, + projectPath, + projectTomlString + ) } else { localStorage.setItem( localStorageProjectSettingsPath(), @@ -410,7 +414,6 @@ export async function saveSettings( ) { // Make sure we have wasm initialized. await initPromise - const onDesktop = isDesktop() // Get the user settings. const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user') @@ -420,8 +423,8 @@ export async function saveSettings( if (err(appTomlString)) return // Write the app settings. - if (onDesktop) { - await writeAppSettingsFile(appTomlString) + if (window.electron) { + await writeAppSettingsFile(window.electron, appTomlString) } else { localStorage.setItem(localStorageAppSettingsPath(), appTomlString) } @@ -439,8 +442,12 @@ export async function saveSettings( if (err(projectTomlString)) return // Write the project settings. - if (onDesktop) { - await writeProjectSettingsFile(projectPath, projectTomlString) + if (window.electron) { + await writeProjectSettingsFile( + window.electron, + projectPath, + projectTomlString + ) } else { localStorage.setItem(localStorageProjectSettingsPath(), projectTomlString) } diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts index ac159714be5..2b6325523b1 100644 --- a/src/lib/telemetry.ts +++ b/src/lib/telemetry.ts @@ -1,3 +1,4 @@ +import type { IElectronAPI } from '@root/interface' import { writeRawTelemetryFile, writeTelemetryFile } from '@src/lib/desktop' import type { PerformanceMark } from '@src/lib/performance' import { getMarks } from '@src/lib/performance' @@ -153,18 +154,18 @@ export function printInvocationCount(marks: Array): string[] { return printMarkDownTable(formattedCounts) } -export async function maybeWriteToDisk() { +export async function maybeWriteToDisk(electron: IElectronAPI) { if (!args) { - args = await window.electron.getArgvParsed() + args = await electron.getArgvParsed() } if (args.telemetry) { setInterval(() => { const marks = getMarks() const deltaTotalTable = printDeltaTotal(marks) - writeTelemetryFile(deltaTotalTable.join('\n')) + writeTelemetryFile(electron, deltaTotalTable.join('\n')) .then(() => {}) .catch(() => {}) - writeRawTelemetryFile(JSON.stringify(marks)) + writeRawTelemetryFile(electron, JSON.stringify(marks)) .then(() => {}) .catch(() => {}) }, 5000) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 912fbb969cd..a410e38d942 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,7 +2,6 @@ import type { Binary as BSONBinary } from 'bson' import { v4 } from 'uuid' import type { AnyMachineSnapshot } from 'xstate' import type { CallExpressionKw, ExecState, SourceRange } from '@src/lang/wasm' -import { isDesktop } from '@src/lib/isDesktop' import type { AsyncFn } from '@src/lib/types' import * as THREE from 'three' @@ -232,7 +231,7 @@ export function getNormalisedCoordinates( export type Platform = 'macos' | 'windows' | 'linux' | '' export function platform(): Platform { - if (isDesktop()) { + if (window.electron) { const platform = window.electron.platform ?? '' // https://nodejs.org/api/process.html#processplatform switch (platform) { diff --git a/src/machines/authMachine.ts b/src/machines/authMachine.ts index 0710beb7194..5ce99bc366b 100644 --- a/src/machines/authMachine.ts +++ b/src/machines/authMachine.ts @@ -162,13 +162,18 @@ export const authMachine = setup({ }) async function getUser(input: { token?: string }) { - if (isDesktop()) { + if (window.electron) { const environment = - (await readEnvironmentFile()) || env().VITE_KITTYCAD_BASE_DOMAIN || '' + (await readEnvironmentFile(window.electron)) || + env().VITE_KITTYCAD_BASE_DOMAIN || + '' updateEnvironment(environment) // Update the pool - const cachedPool = await readEnvironmentConfigurationPool(environment) + const cachedPool = await readEnvironmentConfigurationPool( + window.electron, + environment + ) updateEnvironmentPool(environment, cachedPool) } @@ -260,8 +265,11 @@ async function getAndSyncStoredToken(input: { const inputToken = input.token && input.token !== '' ? input.token : '' const cookieToken = getCookie(COOKIE_NAME) const fileToken = - isDesktop() && environmentName - ? await readEnvironmentConfigurationToken(environmentName) + window.electron && environmentName + ? await readEnvironmentConfigurationToken( + window.electron, + environmentName + ) : '' const token = inputToken || cookieToken || fileToken @@ -277,10 +285,14 @@ async function getAndSyncStoredToken(input: { // If you found a token if (token) { // Write it to disk to sync it for desktop! - if (isDesktop()) { + if (window.electron) { // has just logged in, update storage if (environmentName) - await writeEnvironmentConfigurationToken(environmentName, token) + await writeEnvironmentConfigurationToken( + window.electron, + environmentName, + token + ) } return token } @@ -306,12 +318,12 @@ async function logout() { async function logoutEnvironment(requestedDomain?: string) { // TODO: 7/10/2025 Remove this months from now, we want to clear the localStorage of the key. localStorage.removeItem(TOKEN_PERSIST_KEY) - if (isDesktop()) { + if (window.electron) { try { const domain = requestedDomain || env().VITE_KITTYCAD_BASE_DOMAIN let token = '' if (domain) { - token = await readEnvironmentConfigurationToken(domain) + token = await readEnvironmentConfigurationToken(window.electron, domain) } else { return new Error('Unable to logout, cannot find domain') } @@ -334,9 +346,9 @@ async function logoutEnvironment(requestedDomain?: string) { } if (domain) { - await writeEnvironmentConfigurationToken(domain, '') + await writeEnvironmentConfigurationToken(window.electron, domain, '') } - await writeEnvironmentFile('') + await writeEnvironmentFile(window.electron, '') return Promise.resolve(null) } } catch (e) { @@ -355,10 +367,10 @@ async function logoutEnvironment(requestedDomain?: string) { * will not be sufficient. */ async function logoutAllEnvironments() { - if (!isDesktop()) { + if (!window.electron) { return new Error('unimplemented for web') } - const environments = await listAllEnvironments() + const environments = await listAllEnvironments(window.electron) for (let i = 0; i < environments.length; i++) { const environmentName = environments[i] // Make the oauth2/token/revoke request per environment diff --git a/src/machines/modifyWithTTC.test.ts b/src/machines/modifyWithTTC.test.ts index 07105bdcc43..7d5055286e1 100644 --- a/src/machines/modifyWithTTC.test.ts +++ b/src/machines/modifyWithTTC.test.ts @@ -667,8 +667,8 @@ async function setupTestProjectWithImports(testFiles: Record) { } // Configure the FileSystemManager to use our test directory - const { fileSystemManager } = await import('@src/lang/std/fileSystemManager') - fileSystemManager.dir = testProjectDir + const { projectFsManager } = await import('@src/lang/std/fileSystemManager') + projectFsManager.dir = testProjectDir return { projectDir: testProjectDir, diff --git a/src/machines/systemIO/systemIOMachineDesktop.ts b/src/machines/systemIO/systemIOMachineDesktop.ts index 8d9f5a702ac..7437b297b18 100644 --- a/src/machines/systemIO/systemIOMachineDesktop.ts +++ b/src/machines/systemIO/systemIOMachineDesktop.ts @@ -29,10 +29,13 @@ import { getStringAfterLastSeparator, parentPathRelativeToProject, } from '@src/lib/paths' +import type { IElectronAPI } from '@root/interface' const sharedBulkCreateWorkflow = async ({ + electron, input, }: { + electron: IElectronAPI input: { context: SystemIOContext files: RequestedKCLFile[] @@ -40,7 +43,7 @@ const sharedBulkCreateWorkflow = async ({ override?: boolean } }) => { - const configuration = await readAppSettingsFile() + const configuration = await readAppSettingsFile(electron) for (let fileIndex = 0; fileIndex < input.files.length; fileIndex++) { const file = input.files[fileIndex] const requestedProjectName = file.requestedProjectName @@ -66,20 +69,24 @@ const sharedBulkCreateWorkflow = async ({ ) } - const baseDir = window.electron.join( + const baseDir = electron.path.join( input.context.projectDirectoryPath, newProjectName ) // If override is true, use the requested filename directly const fileName = input.override ? requestedFileName - : getNextFileName({ - entryName: requestedFileName, - baseDir, - }).name + : ( + await getNextFileName({ + electron, + entryName: requestedFileName, + baseDir, + }) + ).name // Create the project around the file if newProject await createNewProjectDirectory( + electron, newProjectName, requestedCode, configuration, @@ -103,12 +110,15 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ actors: { [SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise( async ({ input: context }: { input: SystemIOContext }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const projects = [] const projectDirectoryPath = context.projectDirectoryPath if (projectDirectoryPath === NO_PROJECT_DIRECTORY) { return [] } - await mkdirOrNOOP(projectDirectoryPath) + await mkdirOrNOOP(window.electron, projectDirectoryPath) // Gotcha: readdir will list all folders at this project directory even if you do not have readwrite access on the directory path const entries = await window.electron.readdir(projectDirectoryPath) const { value: canReadWriteProjectDirectory } = @@ -130,7 +140,10 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ if (!isDirectory) { continue } - const project: Project = await getProjectInfo(projectPath) + const project: Project = await getProjectInfo( + window.electron, + projectPath + ) if ( project.kcl_file_count === 0 && project.readWriteAccess && @@ -149,10 +162,13 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ }: { input: { context: SystemIOContext; requestedProjectName: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const folders = input.context.folders const requestedProjectName = input.requestedProjectName const uniqueName = getUniqueProjectName(requestedProjectName, folders) - await createNewProjectDirectory(uniqueName) + await createNewProjectDirectory(window.electron, uniqueName) return { message: `Successfully created "${uniqueName}"`, name: uniqueName, @@ -169,6 +185,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ projectName: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const folders = input.context.folders const requestedProjectName = input.requestedProjectName const projectName = input.projectName @@ -189,6 +208,7 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ } await renameProjectDirectory( + window.electron, window.electron.path.join( input.context.projectDirectoryPath, projectName @@ -209,6 +229,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ }: { input: { context: SystemIOContext; requestedProjectName: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } await window.electron.rm( window.electron.path.join( input.context.projectDirectoryPath, @@ -238,6 +261,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedSubRoute?: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const requestedProjectName = input.requestedProjectName const requestedFileNameWithExtension = input.requestedFileNameWithExtension @@ -267,15 +293,17 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ input.context.projectDirectoryPath, newProjectName ) - const { name: newFileName } = getNextFileName({ + const { name: newFileName } = await getNextFileName({ + electron: window.electron, entryName: requestedFileNameWithExtension, baseDir, }) - const configuration = await readAppSettingsFile() + const configuration = await readAppSettingsFile(window.electron) // Create the project around the file if newProject await createNewProjectDirectory( + window.electron, newProjectName, requestedCode, configuration, @@ -299,6 +327,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedProjectDirectoryPath: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const requestProjectDirectoryPath = input.requestedProjectDirectoryPath if (!requestProjectDirectoryPath) { return { value: true, error: undefined } @@ -319,6 +350,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedFileName: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const path = window.electron.path.join( input.context.projectDirectoryPath, input.requestedProjectName, @@ -342,7 +376,13 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ rootContext: AppMachineContext } }) => { - const message = await sharedBulkCreateWorkflow({ input }) + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } + const message = await sharedBulkCreateWorkflow({ + electron: window.electron, + input, + }) return { ...message, subRoute: '', @@ -362,7 +402,11 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedSubRoute?: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const message = await sharedBulkCreateWorkflow({ + electron: window.electron, input: { ...input, override: input.override, @@ -389,7 +433,11 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedSubRoute?: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const message = await sharedBulkCreateWorkflow({ + electron: window.electron, input: { ...input, override: input.override, @@ -417,6 +465,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedFileNameWithExtension?: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const { folderName, requestedFolderName, @@ -480,6 +531,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ absolutePathToParentDirectory: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const { fileNameWithExtension, requestedFileNameWithExtension, @@ -543,6 +597,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedProjectName?: string | undefined } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } await window.electron.rm(input.requestedPath, { recursive: true }) let response = { message: 'File deleted successfully', @@ -562,6 +619,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedAbsolutePath: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const fileNameWithExtension = getStringAfterLastSeparator( input.requestedAbsolutePath ) @@ -592,6 +652,9 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ requestedAbsolutePath: string } }) => { + if (!window.electron) { + return Promise.reject(new Error('No file system present')) + } const folderName = getStringAfterLastSeparator( input.requestedAbsolutePath ) diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 1f416f83cf6..690a57c68be 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -88,7 +88,7 @@ const Home = () => { // Only create the native file menus on desktop useEffect(() => { - if (isDesktop()) { + if (window.electron) { window.electron .createHomePageMenu() .then(() => { diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index 3c6cfe55a91..b3b415e0812 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -24,6 +24,7 @@ import { writeEnvironmentFile, } from '@src/lib/desktop' import { AdvancedSignInOptions } from '@src/routes/AdvancedSignInOptions' +import type { IElectronAPI } from '@root/interface' const subtleBorder = 'border border-solid border-chalkboard-30 dark:border-chalkboard-80' @@ -33,7 +34,7 @@ let didReadFromDiskCacheForEnvironment = false const SignIn = () => { // Only create the native file menus on desktop - if (isDesktop()) { + if (window.electron) { window.electron.createFallbackMenu().catch(reportRejection) // Disable these since they cannot be accessed within the sign in page. window.electron @@ -59,9 +60,10 @@ const SignIn = () => { } useEffect(() => { - if (isDesktop() && !didReadFromDiskCacheForEnvironment) { + if (window.electron && !didReadFromDiskCacheForEnvironment) { + const electron = window.electron didReadFromDiskCacheForEnvironment = true - readEnvironmentFile() + readEnvironmentFile(electron) .then((environment) => { if (environment) { setSelectedEnvironmentFormatter(environment) @@ -71,7 +73,7 @@ const SignIn = () => { .then((environment) => { const defaultOrDiskEnvironment = environment || selectedEnvironment if (defaultOrDiskEnvironment) { - readEnvironmentConfigurationPool(defaultOrDiskEnvironment) + readEnvironmentConfigurationPool(electron, defaultOrDiskEnvironment) .then((pool) => { setPool(pool) }) @@ -103,12 +105,12 @@ const SignIn = () => { [theme.current] ) - const signInDesktop = async () => { + const signInDesktop = async (electron: IElectronAPI) => { updateEnvironment(selectedEnvironment) updateEnvironmentPool(selectedEnvironment, pool) // We want to invoke our command to login via device auth. - const userCodeToDisplay = await window.electron + const userCodeToDisplay = await electron .startDeviceFlow(withAPIBaseURL(location.search)) .catch(reportError) if (!userCodeToDisplay) { @@ -119,18 +121,20 @@ const SignIn = () => { setUserCode(userCodeToDisplay) // Now that we have the user code, we can kick off the final login step. - const token = await window.electron.loginWithDeviceFlow().catch(reportError) + const token = await electron.loginWithDeviceFlow().catch(reportError) if (!token) { console.error('No token received while trying to log in') toast.error('Error while trying to log in') - await writeEnvironmentFile('') + await writeEnvironmentFile(electron, '') return } - writeEnvironmentFile(selectedEnvironment).catch(reportRejection) - writeEnvironmentConfigurationPool(selectedEnvironment, pool).catch( - reportRejection - ) + writeEnvironmentFile(electron, selectedEnvironment).catch(reportRejection) + writeEnvironmentConfigurationPool( + electron, + selectedEnvironment, + pool + ).catch(reportRejection) authActor.send({ type: 'Log in', token }) } @@ -176,12 +180,19 @@ const SignIn = () => { collaborate with ML tools like Zoo Text-To-CAD to design parts and libraries fast.

- {isDesktop() ? ( + {window.electron ? (
{!userCode ? ( <>