Lf94/tauri to electron (#3315)

* Get electron building something at all

* Merge Frank test setup work (#3418)

* Working window.electron.getPath

* Loading project-specific settings in electron tests

* Simplify test until we can get snapshots/traces working in electron tests

* test tweaks

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>

* add test #3375 and #3420

* put kcl files together

* move files

* can sort projects #3362

* File in the file pane should open with a single click #3385

* pressing delete on home screen should do nothing #3387

* add aria labels to icons

* Rename and delete projects, also spam arrow keys when renaming #3364 #3365 #3259

* Fix up paths

* Update flake.nix to support Electron

* Remove a layer of indirection

* Work without a web server

* Fix settings#projectDir link on home

* Fix login (requires new @kittycad/lib WHICH IS NOT INCLUDED HERE)

* Lee: Tests are broken because auth skip needs to happen

* get setting override envs passed through

* tweak eletron CI

* yml tweak

* fmt

* NUKE tauri shit post merge with main

* another test auth tweak

* Revert "another test auth tweak"

This reverts commit b2254b10af.

* try CI again

* CI tweaks

* SKIP_AUTH true now on playwright

* Skipping auth when NODE_ENV=development now

* fmt

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

* Use BASE_URL()

* fix exists

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

* fix foldername for macos

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

* update for windows

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

* fix version in lower right

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

* cleanup unused imports

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

* progress on is playwright

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

* fix test folders

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

* fix fmt

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

* remove tauri from actions bullshit

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

* remove tauri dir

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

* fixups the coredump async shit

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

* updates

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

* node env dev

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

* fix cancellable

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

* cleanup unnessary things

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

* fix

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

* env vars

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

* Bring back fix for NOT using hardcoded main.kcl

* env

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

* fmt

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

* updates

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

* Revert "updates"

This reverts commit da5d9f1043eb94404e8b3f8044088e990e34a4ef.

* updates

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

* remove tauri clippuy

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

* less retries for now, no debug

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

* updates

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

* tsconfig

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

* small tsc fix

* update some tsc

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

* tsc env

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

* fix other tsc

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

* small change for routeLoaders

* rm old screenshot

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

* fix auth

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

* fix last onew

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

* auth clean up

* fix package.json

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

* fix

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

* dissmissed screen on tests

* add waits between files being written

* put back retried

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

* fix weird programMemory Map issue

* put private back

* Revert "put private back"

This reverts commit d311b978ca.

* Revert "fix weird programMemory Map issue"

This reverts commit 6c387bdf62.

* remove serde-wasm-bindgen

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

* add env

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* fix tests

* more test tweaks

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* another tweak

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* more test tweaks

* more tweaks

* increase macos timeout

* try fix macos

* disable macos playwright tests

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Adam Sunderland <iterion@gmail.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
49fl
2024-08-16 07:15:42 -04:00
committed by GitHub
parent d916c79874
commit 8400e06dd6
196 changed files with 7133 additions and 13052 deletions

509
src/lib/desktop.ts Normal file
View File

@ -0,0 +1,509 @@
import { err } from 'lib/trap'
import { Models } from '@kittycad/lib'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import {
defaultAppSettings,
tomlStringify,
parseAppSettings,
parseProjectSettings,
} from 'lang/wasm'
import {
DEFAULT_HOST,
PROJECT_ENTRYPOINT,
PROJECT_FOLDER,
PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME,
} from './constants'
export { parseProjectRoute } from 'lang/wasm'
export async function renameProjectDirectory(
projectPath: string,
newName: string
): Promise<string> {
if (!newName) {
return Promise.reject(new Error(`New name for project cannot be empty`))
}
try {
await window.electron.stat(projectPath)
} catch (e) {
if (e === 'ENOENT') {
return Promise.reject(new Error(`Path ${projectPath} is not a directory`))
}
}
// Make sure the new name does not exist.
const newPath = window.electron.path.join(
projectPath.split('/').slice(0, -1).join('/'),
newName
)
try {
await window.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(
new Error(
`Path ${newPath} already exists, cannot rename to an existing path`
)
)
} 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)
return newPath
}
}
return Promise.reject(new Error('Unreachable'))
}
export async function ensureProjectDirectoryExists(
config: Partial<SaveSettingsPayload>
): Promise<string | undefined> {
const projectDir = config.app?.projectDirectory
if (!projectDir) {
return Promise.reject(new Error('projectDir is falsey'))
}
try {
await window.electron.stat(projectDir)
} catch (e) {
if (e === 'ENOENT') {
await window.electron.mkdir(projectDir, { recursive: true })
}
}
return projectDir
}
export async function createNewProjectDirectory(
projectName: string,
initialCode?: string,
configuration?: Partial<SaveSettingsPayload>
): Promise<Project> {
if (!configuration) {
configuration = await readAppSettingsFile()
}
const mainDir = await ensureProjectDirectoryExists(configuration)
if (!projectName) {
return Promise.reject('Project name cannot be empty.')
}
if (!mainDir) {
return Promise.reject(new Error('mainDir is falsey'))
}
const projectDir = window.electron.path.join(mainDir, projectName)
try {
await window.electron.stat(projectDir)
} catch (e) {
if (e === 'ENOENT') {
await window.electron.mkdir(projectDir, { recursive: true })
}
}
const projectFile = window.electron.path.join(projectDir, PROJECT_ENTRYPOINT)
await window.electron.writeFile(projectFile, initialCode ?? '')
const metadata = await window.electron.stat(projectFile)
return {
path: projectDir,
name: projectName,
// We don't need to recursively get all files in the project directory.
// Because we just created it and it's empty.
children: null,
default_file: projectFile,
metadata,
kcl_file_count: 1,
directory_count: 0,
}
}
export async function listProjects(
configuration?: Partial<SaveSettingsPayload>
): Promise<Project[]> {
if (configuration === undefined) {
configuration = await readAppSettingsFile()
}
const projectDir = await ensureProjectDirectoryExists(configuration)
const projects = []
if (!projectDir) return Promise.reject(new Error('projectDir was falsey'))
const entries = await window.electron.readdir(projectDir)
for (let entry of entries) {
const projectPath = window.electron.path.join(projectDir, entry)
// if it's not a directory ignore.
const isDirectory = await window.electron.statIsDirectory(projectPath)
if (!isDirectory) {
continue
}
const project = await getProjectInfo(projectPath)
// Needs at least one file to be added to the projects list
if (project.kcl_file_count === 0) {
continue
}
projects.push(project)
}
return projects
}
const IMPORT_FILE_EXTENSIONS = [
// TODO Use ImportFormat enum
'stp',
'glb',
'fbxb',
'kcl',
]
const isRelevantFile = (filename: string): boolean =>
IMPORT_FILE_EXTENSIONS.some((ext) => filename.endsWith('.' + ext))
const collectAllFilesRecursiveFrom = async (path: string) => {
// Make sure the filesystem object exists.
try {
await window.electron.stat(path)
} catch (e) {
if (e === 'ENOENT') {
return Promise.reject(new Error(`Directory ${path} does not exist`))
}
}
// Make sure the path is a directory.
const isPathDir = await window.electron.statIsDirectory(path)
if (!isPathDir) {
return Promise.reject(new Error(`Path ${path} is not a directory`))
}
const pathParts = path.split('/')
let entry: FileEntry = {
name: pathParts.slice(-1)[0],
path,
children: [],
}
const children = []
const entries = await window.electron.readdir(path)
// Sort all entries so files come first and directories last
// so a top-most KCL file is returned first.
entries.sort((a: string, b: string) => {
if (a.endsWith('.kcl') && !b.endsWith('.kcl')) {
return -1
}
if (!a.endsWith('.kcl') && b.endsWith('.kcl')) {
return 1
}
return 0
})
for (let e of entries) {
// ignore hidden files and directories (starting with a dot)
if (e.indexOf('.') === 0) {
continue
}
const ePath = window.electron.path.join(path, e)
const isEDir = await window.electron.statIsDirectory(ePath)
if (isEDir) {
const subChildren = await collectAllFilesRecursiveFrom(ePath)
children.push(subChildren)
} else {
if (!isRelevantFile(ePath)) {
continue
}
children.push(
/* FileEntry */ {
name: e,
path: ePath,
children: null,
}
)
}
}
// We don't set this to none if there are no children, because it's a directory.
entry.children = children
return entry
}
export async function getDefaultKclFileForDir(
projectDir: string,
file: FileEntry
) {
// Make sure the dir is a directory.
const isFileEntryDir = await window.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
)
try {
await window.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)
} else if ((entry.children?.length ?? 0) > 0) {
// Recursively find a kcl file in the directory.
return getDefaultKclFileForDir(entry.path, entry)
}
}
// If we didn't find a kcl file, create one.
await window.electron.writeFile(defaultFilePath, '')
return defaultFilePath
}
}
}
if (!file.children) {
return file.name
}
return defaultFilePath
}
const kclFileCount = (file: FileEntry) => {
let count = 0
if (file.children) {
for (let entry of file.children) {
if (entry.name.endsWith('.kcl')) {
count += 1
} else {
count += kclFileCount(entry)
}
}
}
return count
}
/// Populate the number of directories in the project.
const directoryCount = (file: FileEntry) => {
let count = 0
if (file.children) {
for (let entry of file.children) {
count += 1
directoryCount(entry)
}
}
return count
}
export async function getProjectInfo(projectPath: string): Promise<Project> {
// Check the directory.
try {
await window.electron.stat(projectPath)
} catch (e) {
if (e === 'ENOENT') {
return Promise.reject(
new Error(`Project directory does not exist: ${projectPath}`)
)
}
}
// Make sure it is a directory.
const projectPathIsDir = await window.electron.statIsDirectory(projectPath)
if (!projectPathIsDir) {
return Promise.reject(
new Error(`Project path is not a directory: ${projectPath}`)
)
}
let walked = await collectAllFilesRecursiveFrom(projectPath)
let default_file = await getDefaultKclFileForDir(projectPath, walked)
const metadata = await window.electron.stat(projectPath)
let project = {
...walked,
// We need to map from node fs.Stats to FileMetadata
metadata: {
modified: metadata.mtimeMs,
accessed: metadata.atimeMs,
created: metadata.ctimeMs,
// this is not used anywhere and we use statIsDirectory in other places
// that need to know if it's a file or directory.
type: null,
size: metadata.size,
permission: metadata.mode,
},
kcl_file_count: 0,
directory_count: 0,
default_file,
}
// Populate the number of KCL files in the project.
project.kcl_file_count = kclFileCount(project)
//Populate the number of directories in the project.
project.directory_count = directoryCount(project)
return project
}
// Write project settings file.
export async function writeProjectSettingsFile(
projectPath: string,
configuration: Partial<SaveSettingsPayload>
): Promise<void> {
const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath)
const tomlStr = tomlStringify({ settings: configuration })
if (err(tomlStr)) return Promise.reject(tomlStr)
return window.electron.writeFile(projectSettingsFilePath, tomlStr)
}
// Since we want backwards compatibility with the old settings file, we need to
// rename the folder for macos.
const MACOS_APP_NAME = 'dev.zoo.modeling-app'
const getAppFolderName = () => {
if (window.electron.os.isMac || window.electron.os.isWindows) {
return MACOS_APP_NAME
}
return window.electron.packageJson.name
}
const getAppSettingsFilePath = async () => {
const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true'
const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY
const appConfig = await window.electron.getPath('appData')
const fullPath = isTestEnv
? testSettingsPath
: window.electron.path.join(appConfig, getAppFolderName())
try {
await window.electron.stat(fullPath)
} catch (e) {
// File/path doesn't exist
if (e === 'ENOENT') {
await window.electron.mkdir(fullPath, { recursive: true })
}
}
return window.electron.path.join(fullPath, SETTINGS_FILE_NAME)
}
const getProjectSettingsFilePath = async (projectPath: string) => {
try {
await window.electron.stat(projectPath)
} catch (e) {
if (e === 'ENOENT') {
await window.electron.mkdir(projectPath, { recursive: true })
}
}
return window.electron.path.join(projectPath, PROJECT_SETTINGS_FILE_NAME)
}
export const getInitialDefaultDir = async () => {
const dir = await window.electron.getPath('documents')
return window.electron.path.join(dir, PROJECT_FOLDER)
}
export const readProjectSettingsFile = async (
projectPath: string
): Promise<Partial<SaveSettingsPayload>> => {
let settingsPath = await getProjectSettingsFilePath(projectPath)
// Check if this file exists.
try {
await window.electron.stat(settingsPath)
} catch (e) {
if (e === 'ENOENT') {
// Return the default configuration.
return {}
}
}
const configToml = await window.electron.readFile(settingsPath)
const configObj = parseProjectSettings(configToml)
return configObj
}
export const readAppSettingsFile = async () => {
let settingsPath = await getAppSettingsFilePath()
try {
await window.electron.stat(settingsPath)
} catch (e) {
if (e === 'ENOENT') {
const config = defaultAppSettings()
if (!config.app) {
return Promise.reject(new Error('config.app is falsey'))
}
config.app.projectDirectory = await getInitialDefaultDir()
return config
}
}
const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml)
return configObj
}
export const writeAppSettingsFile = async (
config: Partial<SaveSettingsPayload>
) => {
const appSettingsFilePath = await getAppSettingsFilePath()
const tomlStr = tomlStringify({ settings: config })
if (err(tomlStr)) return Promise.reject(tomlStr)
return window.electron.writeFile(appSettingsFilePath, tomlStr)
}
let appStateStore: ProjectState | undefined = undefined
export const getState = async (): Promise<ProjectState | undefined> => {
return Promise.resolve(appStateStore)
}
export const setState = async (
state: ProjectState | undefined
): Promise<void> => {
appStateStore = state
}
export const getUser = async (
token: string,
hostname: string
): Promise<Models['User_type']> => {
// Use the host passed in if it's set.
// Otherwise, use the default host.
const host = !hostname ? DEFAULT_HOST : hostname
// Change the baseURL to the one we want.
let baseurl = host
if (!(host.indexOf('http://') === 0) && !(host.indexOf('https://') === 0)) {
baseurl = `https://${host}`
if (host.indexOf('localhost') === 0) {
baseurl = `http://${host}`
}
}
// Use kittycad library to fetch the user info from /user/me
if (baseurl !== DEFAULT_HOST) {
// The TypeScript generated library uses environment variables for this
// because it was intended for NodeJS.
// Needs to stay like this because window.electron.kittycad needs it
// internally.
window.electron.setBaseUrl(baseurl)
}
try {
const user = await window.electron.kittycad('users.get_user_self', {
client: { token },
})
return user
} catch (e) {
console.error(e)
}
return Promise.reject(new Error('unreachable'))
}