Merge branch 'main' into pierremtb/issue3528-Add-electron-updater
This commit is contained in:
		@ -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  | 
@ -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()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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'],
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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 }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										79
									
								
								src/lib/paths.test.ts
									
									
									
									
									
										Normal 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,
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@ -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,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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]]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/main.ts
									
									
									
									
									
								
							@ -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.
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
@ -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());
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								src/wasm-lib/rust-toolchain.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/wasm-lib/rust-toolchain.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
[toolchain]
 | 
			
		||||
channel = "1.80.1"
 | 
			
		||||
components = ["clippy", "rustfmt"]
 | 
			
		||||
@ -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
									
									
									
									
									
								
							
							
						
						
									
										204
									
								
								yarn.lock
									
									
									
									
									
								
							@ -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"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user