Merge branch 'main' into pierremtb/issue3528-Add-electron-updater

This commit is contained in:
Pierre Jacquier
2024-08-22 17:44:50 -04:00
committed by GitHub
25 changed files with 1027 additions and 556 deletions

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'
import { test, expect, Page } from '@playwright/test'
import {
doExport,
getUtils,
@ -6,6 +6,7 @@ import {
Paths,
setupElectron,
tearDown,
createProjectAndRenameIt,
} from './test-utils'
import fsp from 'fs/promises'
import fs from 'fs'
@ -45,17 +46,20 @@ test(
'click help/keybindings from project page',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
await fsp.mkdir(join(dir, 'bracket'), { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'focusrite_scarlett_mounting_braket.kcl'
),
join(dir, 'bracket', 'main.kcl')
)
},
})
@ -64,8 +68,6 @@ test(
page.on('console', console.log)
page.on('console', console.log)
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
@ -92,17 +94,20 @@ test(
'when code with error first loads you get errors in console',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/broken-code`, { recursive: true })
await fsp.mkdir(join(dir, 'broken-code'), { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/broken-code-test.kcl',
`${dir}/broken-code/main.kcl`
join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'broken-code-test.kcl'
),
join(dir, 'broken-code', 'main.kcl')
)
},
})
@ -232,10 +237,6 @@ test(
'Rename and delete projects, also spam arrow keys when renaming',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
@ -524,10 +525,6 @@ test(
'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
})
@ -535,33 +532,23 @@ test(
page.on('console', console.log)
const createProjectAndRenameIt = async (name: string) =>
test.step(`Create and rename project ${name}`, async () => {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText(`project-000`)).toBeVisible()
await page.getByText(`project-000`).hover()
await page.getByText(`project-000`).focus()
await page.getByLabel('sketch').first().click()
await page.waitForTimeout(100)
// type "updated project name"
await page.keyboard.press('Backspace')
await page.keyboard.type(name)
await page.getByLabel('checkmark').last().click()
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameIt('router-template-slate')
// await createProjectAndRenameIt('focusrite_scarlett_mounting_braket')
await createProjectAndRenameIt('bracket')
await createProjectAndRenameIt('lego')
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('delete the middle project, i.e. the bracket project', async () => {
const project = page.getByText('bracket')
@ -622,10 +609,6 @@ test(
'Can sort projects on home page',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
})
@ -635,33 +618,23 @@ test(
page.on('console', console.log)
const createProjectAndRenameIt = async (name: string) =>
test.step(`Create and rename project ${name}`, async () => {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText(`project-000`)).toBeVisible()
await page.getByText(`project-000`).hover()
await page.getByText(`project-000`).focus()
await page.getByLabel('sketch').first().click()
await page.waitForTimeout(100)
// type "updated project name"
await page.keyboard.press('Backspace')
await page.keyboard.type(name)
await page.getByLabel('checkmark').last().click()
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameIt('router-template-slate')
// await createProjectAndRenameIt('focusrite_scarlett_mounting_braket')
await createProjectAndRenameIt('bracket')
await createProjectAndRenameIt('lego')
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('should be shorted by modified initially', async () => {
const lastModifiedButton = page.getByRole('button', {
@ -748,10 +721,6 @@ test(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ testInfo })
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -836,25 +805,35 @@ test(
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
fsp.mkdir(`${dir}/bracket`, { recursive: true }),
fsp.mkdir(join(dir, 'router-template-slate'), { recursive: true }),
fsp.mkdir(join(dir, 'bracket'), { recursive: true }),
])
await Promise.all([
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'router-template-slate.kcl'
),
join(dir, 'router-template-slate', 'main.kcl')
),
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'focusrite_scarlett_mounting_braket.kcl'
),
join(dir, 'bracket', 'main.kcl')
),
])
},
@ -1302,10 +1281,6 @@ test(
'Settings persist across restarts',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
await test.step('We can change a user setting like theme', async () => {
const { electronApp, page } = await setupElectron({
testInfo,
@ -1682,6 +1657,7 @@ test.describe('Renaming in the file tree', () => {
})
await test.step('Rename the folder', async () => {
await page.waitForTimeout(60000)
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
@ -1710,3 +1686,47 @@ test.describe('Renaming in the file tree', () => {
}
)
})
test(
'Original project name persist after onboarding',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
const getAllProjects = () => page.getByTestId('project-link').all()
page.on('console', console.log)
await test.step('Should create and name a project called wrist brace', async () => {
await createProjectAndRenameIt({ name: 'wrist brace', page })
})
await test.step('Should go through onboarding', async () => {
await page.getByTestId('user-sidebar-toggle').click()
await page.getByTestId('user-settings').click()
await page.getByRole('button', { name: 'Replay Onboarding' }).click()
const numberOfOnboardingSteps = 12
for (let clicks = 0; clicks < numberOfOnboardingSteps; clicks++) {
await page.getByTestId('onboarding-next').click()
}
await page.getByTestId('project-sidebar-toggle').click()
})
await test.step('Should go home after onboarding is completed', async () => {
await page.getByRole('button', { name: 'Go to Home' }).click()
})
await test.step('Should show the original project called wrist brace', async () => {
const projectNames = ['Tutorial Project 00', 'wrist brace']
for (const [index, projectLink] of (await getAllProjects()).entries()) {
await expect(projectLink).toContainText(projectNames[index])
}
})
await electronApp.close()
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -2,10 +2,11 @@ import {
expect,
Page,
Download,
TestInfo,
BrowserContext,
TestInfo,
_electron as electron,
Locator,
test,
} from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactGraph'
import os from 'os'
@ -26,6 +27,7 @@ import {
import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { isArray } from 'lib/utils'
type TestColor = [number, number, number]
export const TEST_COLORS = {
@ -44,6 +46,9 @@ export const commonPoints = {
num2: 14.44,
}
export const editorSelector = '[role="textbox"][data-language="kcl"]'
type PaneId = 'variables' | 'code' | 'files' | 'logs'
async function waitForPageLoadWithRetry(page: Page) {
await expect(async () => {
await page.goto('/')
@ -205,7 +210,7 @@ export const wiggleMove = async (
}
export const circleMove = async (
page: any,
page: Page,
x: number,
y: number,
steps: number,
@ -311,13 +316,19 @@ export function normaliseKclNumbers(code: string, ignoreZero = true): string {
return replaceNumbers(code)
}
export async function getUtils(page: Page) {
export async function getUtils(page: Page, test_?: typeof test) {
if (!test) {
console.warn(
'Some methods in getUtils requires test object as second argument'
)
}
// Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name()
const cdpSession =
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
return {
const util = {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page),
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
@ -484,7 +495,74 @@ export async function getUtils(page: Page) {
networkOptions
)
},
toNormalizedCode: (text: string) => {
return text.replace(/\s+/g, '')
},
createAndSelectProject: async (hasText: string) => {
return test_?.step(
`Create and select project with text "${hasText}"`,
async () => {
await page.getByTestId('home-new-file').click()
const projectLinksPost = page.getByTestId('project-link')
await projectLinksPost.filter({ hasText }).click()
}
)
},
editorTextMatches: async (code: string) => {
const editor = page.locator(editorSelector)
const editorText = await editor.textContent()
return expect(util.toNormalizedCode(editorText || '')).toBe(
util.toNormalizedCode(code)
)
},
pasteCodeInEditor: async (code: string) => {
return test?.step('Paste in KCL code', async () => {
const editor = page.locator(editorSelector)
await editor.fill(code)
await util.editorTextMatches(code)
})
},
clickPane: async (paneId: PaneId) => {
return test?.step(`Open ${paneId} pane`, async () => {
await page.getByTestId(paneId + '-pane-button').click()
await expect(page.locator('#' + paneId + '-pane')).toBeVisible()
})
},
createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => {
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
await page
.getByTestId('file-pane-scroll-container')
.filter({ hasText: name })
.click()
})
},
panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(
({ PERSIST_MODELING_CONTEXT, paneIds }) => {
localStorage.setItem(
PERSIST_MODELING_CONTEXT,
JSON.stringify({ openPanes: paneIds })
)
},
{ PERSIST_MODELING_CONTEXT, paneIds }
)
await page.reload()
})
},
}
return util
}
type TemplateOptions = Array<number | Array<number>>
@ -505,7 +583,7 @@ const _makeTemplate = (
templateParts: TemplateStringsArray,
...options: TemplateOptions
) => {
const length = Math.max(...options.map((a) => (Array.isArray(a) ? a[0] : 0)))
const length = Math.max(...options.map((a) => (isArray(a) ? a[0] : 0)))
let reExpTemplate = ''
for (let i = 0; i < length; i++) {
const currentStr = templateParts.map((str, index) => {
@ -513,7 +591,7 @@ const _makeTemplate = (
return (
escapeRegExp(str) +
String(
Array.isArray(currentOptions)
isArray(currentOptions)
? currentOptions[i]
: typeof currentOptions === 'number'
? currentOptions
@ -733,6 +811,7 @@ export async function setup(context: BrowserContext, page: Page) {
// kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
// Trigger a navigation, since loading file:// doesn't.
await page.reload()
}
@ -814,3 +893,29 @@ export async function isOutOfViewInScrollContainer(
return isOutOfView
}
export async function createProjectAndRenameIt({
name,
page,
}: {
name: string
page: Page
}) {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText(`project-000`)).toBeVisible()
await page.getByText(`project-000`).hover()
await page.getByText(`project-000`).focus()
await page.getByLabel('sketch').first().click()
await page.waitForTimeout(100)
// type the name passed in
await page.keyboard.press('Backspace')
await page.keyboard.type(name)
await page.getByLabel('checkmark').last().click()
}

View File

@ -264,4 +264,69 @@ test.describe('Testing settings', () => {
await electronApp.close()
}
)
test(
`Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
clickPane,
createNewFileAndSelect,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen([])
await test.step('Precondition: No projects exist', async () => {
await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link')
await expect(projectLinksPre).toHaveCount(0)
})
await createAndSelectProject('project-000')
await clickPane('code')
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await clickPane('files')
await createNewFileAndSelect('2.kcl')
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCylinder)
const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings',
})
const settingsCloseButton = page.getByTestId('settings-close-button')
await test.step('Open and close settings', async () => {
await settingsOpenButton.click()
await settingsCloseButton.click()
})
await test.step('Postcondition: Same file content is in editor as before settings opened', async () => {
await editorTextMatches(kclCylinder)
})
await electronApp.close()
}
)
})

View File

@ -84,7 +84,7 @@
"fetch:wasm": "./get-latest-wasm-bundle.sh",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
@ -140,7 +140,7 @@
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
"@types/mocha": "^10.0.6",
"@types/node": "^22.4.2",
"@types/node": "^22.5.0",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4",
@ -167,7 +167,7 @@
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10",
"http-server": "^14.1.1",
"husky": "^9.0.11",
"husky": "^9.1.5",
"node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
@ -178,7 +178,7 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.12",
"vite": "^5.4.2",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -17,6 +17,7 @@ export default defineConfig({
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['dot'],
['list'],
['json', { outputFile: './test-results/report.json' }],
['html'],
],

View File

@ -1,4 +1,4 @@
import { defineConfig } from '@playwright/test'
import { defineConfig, devices } from '@playwright/test'
/**
* See https://playwright.dev/docs/test-configuration.
@ -30,4 +30,24 @@ export default defineConfig({
actionTimeout: 15_000,
screenshot: 'only-on-failure',
},
projects: [
{
name: 'Google Chrome',
use: {
...devices['Desktop Chrome'],
channel: 'chrome',
contextOptions: {
/* Chromium is the only one with these permission types */
permissions: ['clipboard-write', 'clipboard-read'],
},
launchOptions: {
...(process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
? {
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
}
: {}),
},
}, // or 'chrome-beta'
},
],
})

View File

@ -75,7 +75,13 @@ import {
changeSketchArguments,
updateStartProfileAtArgs,
} from 'lang/std/sketch'
import { isOverlap, normaliseAngle, roundOff, throttle } from 'lib/utils'
import {
isArray,
isOverlap,
normaliseAngle,
roundOff,
throttle,
} from 'lib/utils'
import {
addStartProfileAt,
createArrayExpression,
@ -99,6 +105,7 @@ import {
import { getThemeColorForThreeJs } from 'lib/theme'
import { err, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
type DraftSegment = 'line' | 'tangentialArcTo'
@ -116,6 +123,8 @@ export const SEGMENT_WIDTH_PX = 1.6
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
type Vec3Array = [number, number, number]
// This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements.
// Cameras, controls, raycasters, etc are handled by sceneInfra
@ -384,7 +393,7 @@ export class SceneEntities {
if (err(sketchGroup)) return Promise.reject(sketchGroup)
if (!sketchGroup) return Promise.reject('sketchGroup not found')
if (!Array.isArray(sketchGroup?.value))
if (!isArray(sketchGroup?.value))
return {
truncatedAst,
programMemoryOverride,
@ -1838,6 +1847,7 @@ export function getSketchQuaternion(
})
if (err(sketchGroup)) return sketchGroup
const zAxis = sketchGroup?.on.zAxis || sketchNormalBackUp
if (!zAxis) return Error('SketchGroup zAxis not found')
return getQuaternionFromZAxis(massageFormats(zAxis))
}
@ -1962,8 +1972,6 @@ export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
return quaternion
}
function massageFormats(a: any): Vector3 {
return Array.isArray(a)
? new Vector3(a[0], a[1], a[2])
: new Vector3(a.x, a.y, a.z)
function massageFormats(a: Vec3Array | Point3d): Vector3 {
return isArray(a) ? new Vector3(a[0], a[1], a[2]) : new Vector3(a.x, a.y, a.z)
}

View File

@ -61,6 +61,7 @@ function RenameForm({
<label>
<span className="sr-only">Rename file</span>
<input
data-testid="file-rename-field"
ref={inputRef}
type="text"
autoFocus
@ -402,6 +403,7 @@ export const FileTreeMenu = () => {
<>
<ActionButton
Element="button"
data-testid="create-file-button"
iconStart={{
icon: 'filePlus',
iconClassName: '!text-current',
@ -417,6 +419,7 @@ export const FileTreeMenu = () => {
<ActionButton
Element="button"
data-testid="create-folder-button"
iconStart={{
icon: 'folderPlus',
iconClassName: '!text-current',

View File

@ -563,7 +563,7 @@ export function createArrayExpression(
start: 0,
end: 0,
digest: null,
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
nonCodeMeta: nonCodeMetaEmpty(),
elements,
}
}
@ -577,7 +577,7 @@ export function createPipeExpression(
end: 0,
digest: null,
body,
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
nonCodeMeta: nonCodeMetaEmpty(),
}
}
@ -613,6 +613,7 @@ export function createObjectExpression(properties: {
start: 0,
end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(),
properties: Object.entries(properties).map(([key, value]) => ({
type: 'ObjectProperty',
start: 0,
@ -1065,3 +1066,7 @@ export async function deleteFromSelection(
return new Error('Selection not recognised, could not delete')
}
const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, start: [], digest: null }
}

View File

@ -16,7 +16,6 @@ import init, {
parse_app_settings,
parse_project_settings,
default_project_settings,
parse_project_route,
base64_decode,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
@ -33,7 +32,6 @@ import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { err } from 'lib/trap'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types'
@ -611,13 +609,6 @@ export function parseProjectSettings(
return parse_project_settings(toml)
}
export function parseProjectRoute(
configuration: DeepPartial<Configuration>,
route_str: string
): ProjectRoute | Error {
return parse_project_route(JSON.stringify(configuration), route_str)
}
export function base64Decode(base64: string): ArrayBuffer | Error {
try {
const decoded = base64_decode(base64)

View File

@ -19,7 +19,6 @@ import {
import { DeepPartial } from './types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export { parseProjectRoute } from 'lang/wasm'
export async function renameProjectDirectory(
projectPath: string,
@ -39,7 +38,7 @@ export async function renameProjectDirectory(
// Make sure the new name does not exist.
const newPath = window.electron.path.join(
projectPath.split('/').slice(0, -1).join('/'),
window.electron.path.dirname(projectPath),
newName
)
try {
@ -186,9 +185,9 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
return Promise.reject(new Error(`Path ${path} is not a directory`))
}
const pathParts = path.split(window.electron.path.sep)
const name = window.electron.path.basename(path)
let entry: FileEntry = {
name: pathParts.slice(-1)[0],
name: name,
path,
children: [],
}
@ -330,7 +329,6 @@ export async function getProjectInfo(projectPath: string): Promise<Project> {
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)

79
src/lib/paths.test.ts Normal file
View File

@ -0,0 +1,79 @@
import { parseProjectRoute } from './paths'
import * as path from 'path'
describe('testing parseProjectRoute', () => {
it('should parse a project as a subpath of project dir', async () => {
let config = {
settings: {
project: {
directory: '/home/somebody/projects',
},
},
}
const route = '/home/somebody/projects/project'
expect(await parseProjectRoute(config, route, path)).toEqual({
projectName: 'project',
projectPath: route,
currentFileName: null,
currentFilePath: null,
})
})
it('should parse a project as the project dir', async () => {
let config = {
settings: {
project: {
directory: '/home/somebody/projects',
},
},
}
const route = '/home/somebody/projects'
expect(await parseProjectRoute(config, route, path)).toEqual({
projectName: null,
projectPath: route,
currentFileName: null,
currentFilePath: null,
})
})
it('should parse a project with file in the project dir', async () => {
let config = {
settings: {
project: {
directory: '/home/somebody/projects',
},
},
}
const route = '/home/somebody/projects/assembly/main.kcl'
expect(await parseProjectRoute(config, route, path)).toEqual({
projectName: 'assembly',
projectPath: '/home/somebody/projects/assembly',
currentFileName: 'main.kcl',
currentFilePath: route,
})
})
it('should parse a project with file in a subdir in the project dir', async () => {
let config = {
settings: {
project: {
directory: '/home/somebody/projects',
},
},
}
const route = '/home/somebody/projects/assembly/subdir/main.kcl'
expect(await parseProjectRoute(config, route, path)).toEqual({
projectName: 'assembly',
projectPath: '/home/somebody/projects/assembly',
currentFileName: 'main.kcl',
currentFilePath: route,
})
})
it('should work in the browser context', async () => {
let config = {}
const route = '/browser/main.kcl'
expect(await parseProjectRoute(config, route, undefined)).toEqual({
projectName: 'browser',
projectPath: '/browser',
currentFileName: 'main.kcl',
currentFilePath: route,
})
})
})

View File

@ -1,13 +1,13 @@
import { onboardingPaths } from 'routes/Onboarding/paths'
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
import { isDesktop } from './isDesktop'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { parseProjectRoute, readAppSettingsFile } from './desktop'
import { readAppSettingsFile } from './desktop'
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
import { err } from 'lib/trap'
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
import { DeepPartial } from './types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { PlatformPath } from 'path'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -25,6 +25,13 @@ type OnboardingPaths = {
const SETTINGS = '/settings' as const
export type ProjectRoute = {
projectName: string | null
projectPath: string
currentFileName: string | null
currentFilePath: string | null
}
export const PATHS = {
INDEX: '/',
HOME: '/home',
@ -60,9 +67,64 @@ export async function getProjectMetaByRouteId(
return Promise.reject(new Error('No configuration found'))
}
const route = parseProjectRoute(configuration, id)
const route = parseProjectRoute(configuration, id, window?.electron?.path)
if (err(route)) return Promise.reject(route)
return route
}
export async function parseProjectRoute(
configuration: DeepPartial<Configuration>,
id: string,
pathlib: PlatformPath | undefined
): Promise<ProjectRoute> {
let projectName = null
let projectPath = ''
let currentFileName = null
let currentFilePath = null
if (
pathlib &&
configuration.settings?.project?.directory &&
id.startsWith(configuration.settings.project.directory)
) {
const relativeToRoot = pathlib.relative(
configuration.settings.project.directory,
id
)
projectName = relativeToRoot.split(pathlib.sep)[0]
projectPath = pathlib.join(
configuration.settings.project.directory,
projectName
)
projectName = projectName === '' ? null : projectName
} else {
projectPath = id
if (pathlib) {
if (pathlib.extname(id) === '.kcl') {
projectPath = pathlib.dirname(id)
}
projectName = pathlib.basename(projectPath)
} else {
if (id.endsWith('.kcl')) {
projectPath = '/browser'
projectName = 'browser'
}
}
}
if (pathlib) {
if (projectPath !== id) {
currentFileName = pathlib.basename(id)
currentFilePath = id
}
} else {
currentFileName = 'main.kcl'
currentFilePath = id
}
return {
projectName: projectName,
projectPath: projectPath,
currentFileName: currentFileName,
currentFilePath: currentFilePath,
}
}

View File

@ -36,9 +36,9 @@ export const settingsLoader: LoaderFunction = async ({
configuration
)
if (projectPathData) {
const { project_path } = projectPathData
const { projectPath } = projectPathData
const { settings: s } = await loadAndValidateSettings(
project_path || undefined
projectPath || undefined
)
return s
}
@ -83,48 +83,49 @@ export const fileLoader: LoaderFunction = async (
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
if (!isBrowserProject && projectPathData) {
const { project_name, project_path, current_file_name, current_file_path } =
const { projectName, projectPath, currentFileName, currentFilePath } =
projectPathData
const urlObj = new URL(routerData.request.url)
let code = ''
if (!urlObj.pathname.endsWith('/settings')) {
if (!current_file_name || !current_file_path || !project_name) {
if (!currentFileName || !currentFilePath || !projectName) {
return redirect(
`${PATHS.FILE}/${encodeURIComponent(
isDesktop()
? (await getProjectInfo(project_path)).default_file
? (await getProjectInfo(projectPath)).default_file
: params.id + '/' + PROJECT_ENTRYPOINT
)}`
)
}
code = await window.electron.readFile(current_file_path)
code = await window.electron.readFile(currentFilePath)
code = normalizeLineEndings(code)
// Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from
// the file system and not the editor.
codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCurrentFilePath(currentFilePath)
codeManager.updateCodeStateEditor(code)
}
// Set the file system manager to the project path
// So that WASM gets an updated path for operations
fileSystemManager.dir = project_path
fileSystemManager.dir = projectPath
const defaultProjectData = {
name: project_name || 'unnamed',
path: project_path,
name: projectName || 'unnamed',
path: projectPath,
children: [],
kcl_file_count: 0,
directory_count: 0,
metadata: null,
default_file: project_path,
default_file: projectPath,
}
const maybeProjectInfo = isDesktop()
? await getProjectInfo(project_path)
? await getProjectInfo(projectPath)
: null
console.log('maybeProjectInfo', {
@ -137,8 +138,8 @@ export const fileLoader: LoaderFunction = async (
code,
project: maybeProjectInfo ?? defaultProjectData,
file: {
name: current_file_name || '',
path: current_file_path || '',
name: currentFileName || '',
path: currentFilePath || '',
children: [],
},
}
@ -187,3 +188,7 @@ export const homeLoader: LoaderFunction = async (): Promise<
}
}
}
const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -4,6 +4,13 @@ import { v4 } from 'uuid'
export const uuidv4 = v4
/**
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
*/
export function isArray(val: any): val is unknown[] {
return Array.isArray(val)
}
export function isOverlap(a: SourceRange, b: SourceRange) {
const [startingRange, secondRange] = a[0] < b[0] ? [a, b] : [b, a]
const [lastOfFirst, firstOfSecond] = [startingRange[1], secondRange[0]]

View File

@ -11,11 +11,11 @@ import * as kittycad from '@kittycad/lib/import'
import { updateElectronApp, UpdateSourceType } from 'update-electron-app'
// If it's not set, scream.
const NODE_ENV = process.env.NODE_ENV
if (!NODE_ENV) {
console.error('*FOX SCREAM* process.env.NODE_ENV is not explicitly set!')
process.exit(1)
}
const NODE_ENV = process.env.NODE_ENV || 'production'
if (!process.env.NODE_ENV)
console.warn(
'*FOX SCREAM* process.env.NODE_ENV is not explicitly set!, defaulting to production'
)
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
// Handle creating/removing shortcuts on Windows when installing/uninstalling.

View File

@ -2466,11 +2466,13 @@ impl ArrayExpression {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase", tag = "type")]
pub struct ObjectExpression {
pub start: usize,
pub end: usize,
pub properties: Vec<ObjectProperty>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
pub digest: Option<Digest>,
}
@ -2481,6 +2483,7 @@ impl ObjectExpression {
start: 0,
end: 0,
properties,
non_code_meta: Default::default(),
digest: None,
}
}
@ -2514,6 +2517,14 @@ impl ObjectExpression {
}
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
if self
.non_code_meta
.non_code_nodes
.values()
.any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
{
return self.recast_multi_line(options, indentation_level, is_in_pipe);
}
let flat_recast = format!(
"{{ {} }}",
self.properties
@ -2529,35 +2540,49 @@ impl ObjectExpression {
.join(", ")
);
let max_array_length = 40;
if flat_recast.len() > max_array_length {
let inner_indentation = if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level + 1)
} else {
options.get_indentation(indentation_level + 1)
};
format!(
"{{\n{}{}\n{}}}",
inner_indentation,
self.properties
.iter()
.map(|prop| {
format!(
"{}: {}",
prop.key.name,
prop.value.recast(options, indentation_level + 1, is_in_pipe)
)
})
.collect::<Vec<String>>()
.join(format!(",\n{}", inner_indentation).as_str()),
if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level)
} else {
options.get_indentation(indentation_level)
},
)
} else {
flat_recast
let needs_multiple_lines = flat_recast.len() > max_array_length;
if !needs_multiple_lines {
return flat_recast;
}
self.recast_multi_line(options, indentation_level, is_in_pipe)
}
/// Recast, but always outputs the object with newlines between each property.
fn recast_multi_line(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
let inner_indentation = if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level + 1)
} else {
options.get_indentation(indentation_level + 1)
};
let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
let mut props = self.properties.iter();
let format_items: Vec<_> = (0..num_items)
.flat_map(|i| {
if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
noncode.iter().map(|nc| nc.format("")).collect::<Vec<_>>()
} else {
let prop = props.next().unwrap();
// Use a comma unless it's the last item
let comma = if i == num_items - 1 { "" } else { ",\n" };
let s = format!(
"{}: {}{comma}",
prop.key.name,
prop.value.recast(options, indentation_level + 1, is_in_pipe).trim()
);
vec![s]
}
})
.collect();
dbg!(&format_items);
let end_indent = if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level)
} else {
options.get_indentation(indentation_level)
};
format!(
"{{\n{inner_indentation}{}\n{end_indent}}}",
format_items.join(&inner_indentation),
)
}
/// Returns a hover value that includes the given character position.
@ -5897,6 +5922,66 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
}
}
#[test]
fn recast_objects_no_comments() {
let input = r#"
const sketch002 = startSketchOn({
plane: {
origin: { x: 1, y: 2, z: 3 },
x_axis: { x: 4, y: 5, z: 6 },
y_axis: { x: 7, y: 8, z: 9 },
z_axis: { x: 10, y: 11, z: 12 }
}
})
"#;
let expected = r#"const sketch002 = startSketchOn({
plane: {
origin: { x: 1, y: 2, z: 3 },
x_axis: { x: 4, y: 5, z: 6 },
y_axis: { x: 7, y: 8, z: 9 },
z_axis: { x: 10, y: 11, z: 12 }
}
})
"#;
let tokens = crate::token::lexer(input).unwrap();
let p = crate::parser::Parser::new(tokens);
let ast = p.ast().unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, expected);
}
#[test]
fn recast_objects_with_comments() {
use winnow::Parser;
for (i, (input, expected, reason)) in [(
"\
{
a: 1,
// b: 2,
c: 3
}",
"\
{
a: 1,
// b: 2,
c: 3
}",
"preserves comments",
)]
.into_iter()
.enumerate()
{
let tokens = crate::token::lexer(input).unwrap();
crate::parser::parser_impl::print_tokens(&tokens);
let expr = crate::parser::parser_impl::object.parse(&tokens).unwrap();
assert_eq!(
expr.recast(&FormatOptions::new(), 0, false),
expected,
"failed test {i}, which is testing that recasting {reason}"
);
}
}
#[test]
fn recast_array_with_comments() {
use winnow::Parser;

View File

@ -586,22 +586,60 @@ fn object_property(i: TokenSlice) -> PResult<ObjectProperty> {
})
}
/// Match something that separates properties of an object.
fn property_separator(i: TokenSlice) -> PResult<()> {
alt((
// Normally you need a comma.
comma_sep,
// But, if the array is ending, no need for a comma.
peek(preceded(opt(whitespace), close_brace)).void(),
))
.parse_next(i)
}
/// Parse a KCL object value.
fn object(i: TokenSlice) -> PResult<ObjectExpression> {
pub(crate) fn object(i: TokenSlice) -> PResult<ObjectExpression> {
let start = open_brace(i)?.start;
ignore_whitespace(i);
let properties = separated(0.., object_property, comma_sep)
.context(expected(
"a comma-separated list of key-value pairs, e.g. 'height: 4, width: 3'",
))
.parse_next(i)?;
let properties: Vec<_> = repeat(
0..,
alt((
terminated(non_code_node.map(NonCodeOr::NonCode), whitespace),
terminated(object_property, property_separator).map(NonCodeOr::Code),
)),
)
.context(expected(
"a comma-separated list of key-value pairs, e.g. 'height: 4, width: 3'",
))
.parse_next(i)?;
// Sort the object's properties from the noncode nodes.
let (properties, non_code_nodes): (Vec<_>, HashMap<usize, _>) = properties.into_iter().enumerate().fold(
(Vec::new(), HashMap::new()),
|(mut properties, mut non_code_nodes), (i, e)| {
match e {
NonCodeOr::NonCode(x) => {
non_code_nodes.insert(i, vec![x]);
}
NonCodeOr::Code(x) => {
properties.push(x);
}
}
(properties, non_code_nodes)
},
);
ignore_trailing_comma(i);
ignore_whitespace(i);
let end = close_brace(i)?.end;
let non_code_meta = NonCodeMeta {
non_code_nodes,
..Default::default()
};
Ok(ObjectExpression {
start,
end,
properties,
non_code_meta,
digest: None,
})
}
@ -3056,12 +3094,6 @@ e
}
#[allow(unused)]
fn print_tokens(tokens: &[Token]) {
for (i, tok) in tokens.iter().enumerate() {
println!("{i:.2}: ({:?}):) '{}'", tok.token_type, tok.value.replace("\n", "\\n"));
}
}
#[test]
fn array_linesep_no_trailing_comma() {
let program = r#"[
@ -3259,6 +3291,7 @@ mod snapshot_tests {
#[test]
fn $func_name() {
let tokens = crate::token::lexer($test_kcl_program).unwrap();
print_tokens(&tokens);
let actual = match program.parse(&tokens) {
Ok(x) => x,
Err(e) => panic!("could not parse test: {e:?}"),
@ -3404,4 +3437,28 @@ mod snapshot_tests {
// B,
]"
);
snapshot_test!(
ay,
"let props = {
a: 1,
// b: 2,
c: 3,
}"
);
snapshot_test!(
az,
"let props = {
a: 1,
// b: 2,
c: 3
}"
);
}
#[allow(unused)]
#[cfg(test)]
pub(crate) fn print_tokens(tokens: &[Token]) {
for (i, tok) in tokens.iter().enumerate() {
println!("{i:.2}: ({:?}):) '{}'", tok.token_type, tok.value.replace("\n", "\\n"));
}
}

View File

@ -0,0 +1,111 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 80,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 80,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 80,
"id": {
"type": "Identifier",
"start": 4,
"end": 9,
"name": "props",
"digest": null
},
"init": {
"type": "ObjectExpression",
"type": "ObjectExpression",
"start": 12,
"end": 80,
"properties": [
{
"type": "ObjectProperty",
"start": 26,
"end": 30,
"key": {
"type": "Identifier",
"start": 26,
"end": 27,
"name": "a",
"digest": null
},
"value": {
"type": "Literal",
"type": "Literal",
"start": 29,
"end": 30,
"value": 1,
"raw": "1",
"digest": null
},
"digest": null
},
{
"type": "ObjectProperty",
"start": 65,
"end": 69,
"key": {
"type": "Identifier",
"start": 65,
"end": 66,
"name": "c",
"digest": null
},
"value": {
"type": "Literal",
"type": "Literal",
"start": 68,
"end": 69,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {
"1": [
{
"type": "NonCodeNode",
"start": 44,
"end": 52,
"value": {
"type": "blockComment",
"value": "b: 2,",
"style": "line"
},
"digest": null
}
]
},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "let",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -0,0 +1,111 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 79,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 79,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 79,
"id": {
"type": "Identifier",
"start": 4,
"end": 9,
"name": "props",
"digest": null
},
"init": {
"type": "ObjectExpression",
"type": "ObjectExpression",
"start": 12,
"end": 79,
"properties": [
{
"type": "ObjectProperty",
"start": 26,
"end": 30,
"key": {
"type": "Identifier",
"start": 26,
"end": 27,
"name": "a",
"digest": null
},
"value": {
"type": "Literal",
"type": "Literal",
"start": 29,
"end": 30,
"value": 1,
"raw": "1",
"digest": null
},
"digest": null
},
{
"type": "ObjectProperty",
"start": 65,
"end": 69,
"key": {
"type": "Identifier",
"start": 65,
"end": 66,
"name": "c",
"digest": null
},
"value": {
"type": "Literal",
"type": "Literal",
"start": 68,
"end": 69,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {
"1": [
{
"type": "NonCodeNode",
"start": 44,
"end": 52,
"value": {
"type": "blockComment",
"value": "b: 2,",
"style": "line"
},
"digest": null
}
]
},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "let",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -8,8 +8,6 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::settings::types::Configuration;
/// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
@ -148,99 +146,6 @@ const model = import("{}")"#,
}
}
/// Project route information.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectRoute {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_name: Option<String>,
pub project_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_file_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_file_path: Option<String>,
}
impl ProjectRoute {
/// Get the project state from the url in the route.
pub fn from_route(configuration: &Configuration, route: &str) -> Result<Self> {
let path = std::path::Path::new(route);
// Check if the default project path is in the route.
let (project_path, project_name) = if path.starts_with(&configuration.settings.project.directory)
&& configuration.settings.project.directory != std::path::PathBuf::default()
{
// Get the project name.
if let Some(project_name) = path
.strip_prefix(&configuration.settings.project.directory)?
.iter()
.next()
{
(
configuration
.settings
.project
.directory
.join(project_name)
.display()
.to_string(),
Some(project_name.to_string_lossy().to_string()),
)
} else {
(configuration.settings.project.directory.display().to_string(), None)
}
} else {
// Assume the project path is the parent directory of the file.
let project_dir = if path.display().to_string().ends_with(".kcl") {
path.parent()
.ok_or_else(|| anyhow::anyhow!("Parent directory not found: {}", path.display()))?
} else {
path
};
if project_dir == std::path::Path::new("/") {
(
path.display().to_string(),
Some(
path.file_name()
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
.to_string_lossy()
.to_string(),
),
)
} else if let Some(project_name) = project_dir.file_name() {
(
project_dir.display().to_string(),
Some(project_name.to_string_lossy().to_string()),
)
} else {
(project_dir.display().to_string(), None)
}
};
let (current_file_name, current_file_path) = if path.display().to_string() == project_path {
(None, None)
} else {
(
Some(
path.file_name()
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
.to_string_lossy()
.to_string(),
),
Some(path.display().to_string()),
)
};
Ok(Self {
project_name,
project_path,
current_file_name,
current_file_path,
})
}
}
/// Information about project.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
@ -535,160 +440,6 @@ impl From<std::fs::Metadata> for FileMetadata {
mod tests {
use pretty_assertions::assert_eq;
#[test]
fn test_project_route_from_route_std_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some(
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl".to_string()
),
}
);
}
#[test]
fn test_project_route_from_route_std_path_dir() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: None,
current_file_path: None
}
);
}
#[test]
fn test_project_route_from_route_std_path_dir_empty() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: None,
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects".to_string(),
current_file_name: None,
current_file_path: None
}
);
}
#[test]
fn test_project_route_from_route_outside_std_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/kittycad/modeling-app/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("modeling-app".to_string()),
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some("/Users/macinatormax/kittycad/modeling-app/main.kcl".to_string()),
}
);
}
#[test]
fn test_project_route_from_route_outside_std_path_dir() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/kittycad/modeling-app";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("modeling-app".to_string()),
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
current_file_name: None,
current_file_path: None
}
);
}
#[test]
fn test_project_route_from_route_browser() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory = std::path::PathBuf::default();
let route = "/browser/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("browser".to_string()),
project_path: "/browser".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some("/browser/main.kcl".to_string())
}
);
}
#[test]
fn test_project_route_from_route_browser_no_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory = std::path::PathBuf::default();
let route = "/browser";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("browser".to_string()),
project_path: "/browser".to_string(),
current_file_name: None,
current_file_path: None
}
);
}
#[test]
fn test_project_route_from_route_non_main_file() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: Some("thing.kcl".to_string()),
current_file_path: Some(
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl".to_string()
),
}
);
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_non_exist() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());

View File

@ -0,0 +1,3 @@
[toolchain]
channel = "1.80.1"
components = ["clippy", "rustfmt"]

View File

@ -566,22 +566,6 @@ pub fn serialize_project_settings(val: JsValue) -> Result<JsValue, String> {
Ok(JsValue::from_str(&toml_str))
}
/// Parse the project route.
#[wasm_bindgen]
pub fn parse_project_route(configuration: &str, route: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let configuration: kcl_lib::settings::types::Configuration =
serde_json::from_str(configuration).map_err(|e| e.to_string())?;
let route =
kcl_lib::settings::types::file::ProjectRoute::from_route(&configuration, route).map_err(|e| e.to_string())?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&route).map_err(|e| e.to_string())
}
static ALLOWED_DECODING_FORMATS: &[data_encoding::Encoding] = &[
data_encoding::BASE64,
data_encoding::BASE64URL,

204
yarn.lock
View File

@ -2177,85 +2177,85 @@
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@rollup/rollup-android-arm-eabi@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.2.tgz#6b991cb44bf69e50163528ea85bed545330ba821"
integrity sha512-OHflWINKtoCFSpm/WmuQaWW4jeX+3Qt3XQDepkkiFTsoxFc5BpF3Z5aDxFZgBqRjO6ATP5+b1iilp4kGIZVWlA==
"@rollup/rollup-android-arm-eabi@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz#d941173f82f9b041c61b0dc1a2a91dcd06e4b31e"
integrity sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==
"@rollup/rollup-android-arm64@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.2.tgz#5d3c8c2f9742d62ba258cc378bd2d4720f0c431c"
integrity sha512-k0OC/b14rNzMLDOE6QMBCjDRm3fQOHAL8Ldc9bxEWvMo4Ty9RY6rWmGetNTWhPo+/+FNd1lsQYRd0/1OSix36A==
"@rollup/rollup-android-arm64@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz#7e7157c8543215245ceffc445134d9e843ba51c0"
integrity sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==
"@rollup/rollup-darwin-arm64@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.2.tgz#8eac8682a34a705bb6a57eb3e739fd6bbedfabed"
integrity sha512-IIARRgWCNWMTeQH+kr/gFTHJccKzwEaI0YSvtqkEBPj7AshElFq89TyreKNFAGh5frLfDCbodnq+Ye3dqGKPBw==
"@rollup/rollup-darwin-arm64@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz#f0a18a4fc8dc6eb1e94a51fa2adb22876f477947"
integrity sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==
"@rollup/rollup-darwin-x64@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.2.tgz#70a9953fc624bd7f645901f4250f6b5807ac7e92"
integrity sha512-52udDMFDv54BTAdnw+KXNF45QCvcJOcYGl3vQkp4vARyrcdI/cXH8VXTEv/8QWfd6Fru8QQuw1b2uNersXOL0g==
"@rollup/rollup-darwin-x64@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz#34b7867613e5cc42d2b85ddc0424228cc33b43f0"
integrity sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==
"@rollup/rollup-linux-arm-gnueabihf@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.2.tgz#8f6c4ff4c4972413ff94345080380d4e3caa3c69"
integrity sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==
"@rollup/rollup-linux-arm-gnueabihf@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz#422b19ff9ae02b05d3395183d1d43b38c7c8be0b"
integrity sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==
"@rollup/rollup-linux-arm-musleabihf@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.2.tgz#5d3c0fe5ea5ddf2feb511b3cb031df17eaa7e33d"
integrity sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==
"@rollup/rollup-linux-arm-musleabihf@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz#568aa29195ef6fc57ec6ed3f518923764406a8ee"
integrity sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==
"@rollup/rollup-linux-arm64-gnu@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.2.tgz#b7f104388b2f5624d9f8adfff10ba59af8ab8ed1"
integrity sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==
"@rollup/rollup-linux-arm64-gnu@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz#22309c8bcba9a73114f69165c72bc94b2fbec085"
integrity sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==
"@rollup/rollup-linux-arm64-musl@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.2.tgz#6d5ca6d3904309bec285ea5202d589cebb93dee4"
integrity sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==
"@rollup/rollup-linux-arm64-musl@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz#c93c388af6d33f082894b8a60839d7265b2b9bc5"
integrity sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==
"@rollup/rollup-linux-powerpc64le-gnu@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.2.tgz#4df9be1396ea9eb0ca99fd0f2e858008d7f063e3"
integrity sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==
"@rollup/rollup-linux-powerpc64le-gnu@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz#493c5e19e395cf3c6bd860c7139c8a903dea72b4"
integrity sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==
"@rollup/rollup-linux-riscv64-gnu@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.2.tgz#80d63c5562915a2f8616a04251fcaee0218112b0"
integrity sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==
"@rollup/rollup-linux-riscv64-gnu@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz#a2eab4346fbe5909165ce99adb935ba30c9fb444"
integrity sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==
"@rollup/rollup-linux-s390x-gnu@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.2.tgz#ef62e9bc5cc3b84fcfe96ec0a42d1989691217b3"
integrity sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==
"@rollup/rollup-linux-s390x-gnu@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz#0bc49a79db4345d78d757bb1b05e73a1b42fa5c3"
integrity sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==
"@rollup/rollup-linux-x64-gnu@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.2.tgz#6a275282a0080fee98ddd9fda0de23c4c6bafd48"
integrity sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==
"@rollup/rollup-linux-x64-gnu@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz#4fd36a6a41f3406d8693321b13d4f9b7658dd4b9"
integrity sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==
"@rollup/rollup-linux-x64-musl@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.2.tgz#64f0c704107e6b45b26dd8c2e1ff64246e4a1251"
integrity sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==
"@rollup/rollup-linux-x64-musl@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz#10ebb13bd4469cbad1a5d9b073bd27ec8a886200"
integrity sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==
"@rollup/rollup-win32-arm64-msvc@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.2.tgz#bada17b0c5017ff58d0feba401c43ff5a646c693"
integrity sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==
"@rollup/rollup-win32-arm64-msvc@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz#2fef1a90f1402258ef915ae5a94cc91a5a1d5bfc"
integrity sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==
"@rollup/rollup-win32-ia32-msvc@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.2.tgz#a716d862f6ac39d88bdb825e27f63aeb0387cd66"
integrity sha512-Mda7iG4fOLHNsPqjWSjANvNZYoW034yxgrndof0DwCy0D3FvTjeNo+HGE6oGWgvcLZNLlcp0hLEFcRs+UGsMLg==
"@rollup/rollup-win32-ia32-msvc@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz#a18ad47a95c5f264defb60acdd8c27569f816fc1"
integrity sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==
"@rollup/rollup-win32-x64-msvc@4.19.2":
version "4.19.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.2.tgz#d67206c5f2e4b2832ce360bbbde194e96d16dc51"
integrity sha512-DPi0ubYhSow/00YqmG1jWm3qt1F8aXziHc/UNy8bo9cpCacqhuWu+iSq/fp2SyEQK7iYTZ60fBU9cat3MXTjIQ==
"@rollup/rollup-win32-x64-msvc@4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz#20c09cf44dcb082140cc7f439dd679fe4bba3375"
integrity sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==
"@rushstack/eslint-patch@^1.1.0":
version "1.10.4"
@ -2540,10 +2540,10 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.7.tgz#4c620090f28ca7f905a94b706f74dc5b57b44f2f"
integrity sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==
"@types/node@*", "@types/node@^22.4.2":
version "22.4.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.4.2.tgz#55fefb1c3dba2ecd7eb76738c6b80da75760523f"
integrity sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==
"@types/node@*", "@types/node@^22.5.0":
version "22.5.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958"
integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==
dependencies:
undici-types "~6.19.2"
@ -5822,10 +5822,10 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
husky@^9.0.11:
version "9.1.4"
resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.4.tgz#926fd19c18d345add5eab0a42b2b6d9a80259b34"
integrity sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==
husky@^9.1.5:
version "9.1.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.5.tgz#2b6edede53ee1adbbd3a3da490628a23f5243b83"
integrity sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==
iconv-lite@0.4.24:
version "0.4.24"
@ -7564,10 +7564,10 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.23, postcss@^8.4.31, postcss@^8.4.39:
version "8.4.40"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8"
integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==
postcss@^8.4.23, postcss@^8.4.31, postcss@^8.4.41:
version "8.4.41"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681"
integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.1"
@ -8135,29 +8135,29 @@ rollup@^2.77.2:
optionalDependencies:
fsevents "~2.3.2"
rollup@^4.13.0:
version "4.19.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.19.2.tgz#4985cd2028965157e8d674a70e49f33aca9038eb"
integrity sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==
rollup@^4.20.0:
version "4.21.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.0.tgz#28db5f5c556a5180361d35009979ccc749560b9d"
integrity sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==
dependencies:
"@types/estree" "1.0.5"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.19.2"
"@rollup/rollup-android-arm64" "4.19.2"
"@rollup/rollup-darwin-arm64" "4.19.2"
"@rollup/rollup-darwin-x64" "4.19.2"
"@rollup/rollup-linux-arm-gnueabihf" "4.19.2"
"@rollup/rollup-linux-arm-musleabihf" "4.19.2"
"@rollup/rollup-linux-arm64-gnu" "4.19.2"
"@rollup/rollup-linux-arm64-musl" "4.19.2"
"@rollup/rollup-linux-powerpc64le-gnu" "4.19.2"
"@rollup/rollup-linux-riscv64-gnu" "4.19.2"
"@rollup/rollup-linux-s390x-gnu" "4.19.2"
"@rollup/rollup-linux-x64-gnu" "4.19.2"
"@rollup/rollup-linux-x64-musl" "4.19.2"
"@rollup/rollup-win32-arm64-msvc" "4.19.2"
"@rollup/rollup-win32-ia32-msvc" "4.19.2"
"@rollup/rollup-win32-x64-msvc" "4.19.2"
"@rollup/rollup-android-arm-eabi" "4.21.0"
"@rollup/rollup-android-arm64" "4.21.0"
"@rollup/rollup-darwin-arm64" "4.21.0"
"@rollup/rollup-darwin-x64" "4.21.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.21.0"
"@rollup/rollup-linux-arm-musleabihf" "4.21.0"
"@rollup/rollup-linux-arm64-gnu" "4.21.0"
"@rollup/rollup-linux-arm64-musl" "4.21.0"
"@rollup/rollup-linux-powerpc64le-gnu" "4.21.0"
"@rollup/rollup-linux-riscv64-gnu" "4.21.0"
"@rollup/rollup-linux-s390x-gnu" "4.21.0"
"@rollup/rollup-linux-x64-gnu" "4.21.0"
"@rollup/rollup-linux-x64-musl" "4.21.0"
"@rollup/rollup-win32-arm64-msvc" "4.21.0"
"@rollup/rollup-win32-ia32-msvc" "4.21.0"
"@rollup/rollup-win32-x64-msvc" "4.21.0"
fsevents "~2.3.2"
run-parallel@^1.1.9:
@ -9245,14 +9245,14 @@ vite-tsconfig-paths@^4.3.2:
globrex "^0.1.2"
tsconfck "^3.0.3"
vite@^5.0.0, vite@^5.0.12:
version "5.3.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.5.tgz#b847f846fb2b6cb6f6f4ed50a830186138cb83d8"
integrity sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==
vite@^5.0.0, vite@^5.4.2:
version "5.4.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.2.tgz#8acb6ec4bfab823cdfc1cb2d6c53ed311bc4e47e"
integrity sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.39"
rollup "^4.13.0"
postcss "^8.4.41"
rollup "^4.20.0"
optionalDependencies:
fsevents "~2.3.3"