Compare commits

...

34 Commits

Author SHA1 Message Date
ac938e1f23 fix: removing console logs 2025-07-03 15:19:43 -05:00
bf9dd893f1 fix: removed debugging commment 2025-07-03 15:13:33 -05:00
03a13fd741 fix: detection logic we spoof too much :( 2025-07-03 15:10:17 -05:00
d567de1e5f fix: known urls 2025-07-03 14:40:39 -05:00
66970d674d fix: love when the results are based on runtime :thonking: 2025-07-03 14:39:32 -05:00
07d899e884 fix: restructured the test names to be more accurate 2025-07-03 13:52:34 -05:00
86e6590f4d fix: damn AI at it again catching my typtoes :( 2025-07-03 13:47:15 -05:00
340c503633 fix: updated with Jace's new envs 2025-07-03 13:39:42 -05:00
7abc119993 chore: env(). support 2025-07-03 13:24:18 -05:00
34494f3bba Consolidate KittyCAD API token environment variables (#7665)
* Consolidate KittyCAD API token environment variables

* Remove duplicate variable in type definition

* Remove unnecessary intermediate steps

* Keep base label for concatenation functions
2025-07-03 13:15:21 -04:00
4102249bd8 fix: merging main 2025-07-03 09:07:12 -05:00
df6256266c [Chore] All api urls are now using the helper function (#7672)
* fix: logging information about the login

* chore: improving the withBaseURL workflow

* chore: moving VITE_KC_API_BASE_URL to the helper function

* fix: env to helper function api base url

* chore: fixing another api base url

* chore: shortlinks with base api helper function

* chore: prompt edit with base helper function

* fix: auto fmt

* fix: withAPIBaseURL for all urls

* fix: AI caught my typo, RIP

* fix: expected

* fix: renaming this so it is less specific to environment

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-07-03 13:54:03 +00:00
e5d082f441 [Chore] Removing old confusing BASE_URL environment variable. (#7678)
chore: this is deprecated, we should be using the VITE_KC_*_BASE_URL workflows
2025-07-02 22:51:01 +00:00
d4d3e179b1 Update test data (#7674) 2025-07-02 15:04:03 -07:00
3929f2e9fb chore: unit tests 2025-07-02 16:34:33 -05:00
bab98ab5c9 chore: updated all site base urls 2025-07-02 16:25:58 -05:00
8dc1b156ff chore: updated hard coded site urls in home.tsx 2025-07-02 14:21:02 -05:00
693fd8eb31 fix: updating the signin page hard coded urls 2025-07-02 14:18:05 -05:00
14a72344e2 chore: updated all VITE_KC_SITE_BASE_URL locations, need to do raw urls next 2025-07-02 14:09:39 -05:00
e5d1cd847d chore: migrating many urls to the helper function withSiteBaseURL 2025-07-02 14:05:37 -05:00
01d294a8bb chore: helper function and unit tests for a withSiteBaseURL 2025-07-02 13:52:41 -05:00
aa460d631d fix: expected 2025-07-02 12:55:14 -05:00
23e609443b fix: AI caught my typo, RIP 2025-07-02 12:36:38 -05:00
d3aa09a20b fix: withAPIBaseURL for all urls 2025-07-02 12:31:17 -05:00
eba91e85ea fix: auto fmt 2025-07-02 11:50:30 -05:00
74f6e338f7 chore: prompt edit with base helper function 2025-07-02 11:50:17 -05:00
529c619fb5 chore: shortlinks with base api helper function 2025-07-02 11:45:35 -05:00
195000b50c chore: fixing another api base url 2025-07-02 11:43:57 -05:00
7993b5f4e8 fix: env to helper function api base url 2025-07-02 11:41:29 -05:00
fb8acbefe7 chore: moving VITE_KC_API_BASE_URL to the helper function 2025-07-02 11:35:33 -05:00
8f2a2391d1 chore: improving the withBaseURL workflow 2025-07-02 11:33:23 -05:00
4f4c44e7c7 KCL: Getter for axes of planes (#7662)
## Goal

Currently, there's no way in KCL to get fields of a plane, e.g. the underlying X axis, Y axis or origin.

This would be useful for geometry calculations in KCL. It would help KCL users write transformations between planes for rotating geometry.

For example, this enables

```kcl
export fn crossProduct(@vectors) {
  a = vectors[0]
  b = vectors[1]
  x = a[1] * b[2] - (a[2] * b[1])
  y = a[2] * b[0] - (a[0] * b[2])
  z = a[0] * b[1] - (a[1] * b[0])
  return [x, y, z]
}

export fn normalOf(@plane) {
  return crossProduct([plane.xAxis, plane.yAxis])
}
```

## Implementation

My goal was just to enable a simple getter for planes, like `myPlane.xAxis` and yAxis and origins. That's nearly what happened, except I discovered that there's two ways to represent a plane: either `KclValue::Plane` or `KclValue::Object` with the right fields.

No matter which format your plane is represented as, it should behave consistently when you get its properties. Those properties should be returned as `[number; 3]` because that is how KCL represents points.

Unfortunately we actually require planes-as-objects to be defined with axes like `myPlane = { xAxis = { x = 1, y = 0, z = 0 }, ...}`, but that's a mistake in my opinion. So if you do use that representation of a plane, it should still return a [number; 3]. This required some futzing around so that we let you access object fields .x and .y as [0] and [1], which is weird, but whatever, I think it's good.

This PR is tested via planestuff.kcl which has a Rust unit test.

Part of the hole efforts, see https://github.com/KittyCAD/modeling-app/discussions/7543
2025-07-02 16:24:26 +00:00
e2247669f0 fix: logging information about the login 2025-07-02 10:22:42 -05:00
1b75020686 Remove unused code to skip auth (#7280) 2025-07-02 14:49:34 +00:00
53 changed files with 703 additions and 303 deletions

View File

@ -3,18 +3,17 @@
NODE_ENV=development NODE_ENV=development
DEV=true DEV=true
# App
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev VITE_KITTYCAD_API_BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
#VITE_WASM_URL="optional way of overriding the wasm url, particular for unit tests which need this if you running not on the default 3000 port" #VITE_WASM_URL="optional override of Wasm URL if not on default port 3000"
#VITE_KC_DEV_TOKEN="optional token to skip auth in the app" #VITE_KITTYCAD_API_TOKEN="required for testing, optional to skip auth in the app"
#token="required token for playwright. TODO: clean up env vars in #3973" FAIL_ON_CONSOLE_ERRORS=true
# KCL
RUST_BACKTRACE=1 RUST_BACKTRACE=1
PYO3_PYTHON=/usr/local/bin/python3 PYO3_PYTHON=/usr/local/bin/python3
#KITTYCAD_API_TOKEN="required token for engine testing" #KITTYCAD_API_TOKEN=$VITE_KITTYCAD_API_TOKEN
FAIL_ON_CONSOLE_ERRORS=true

View File

@ -1,7 +1,8 @@
NODE_ENV=production NODE_ENV=production
# App
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev VITE_KITTYCAD_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SITE_APP_URL=https://app.zoo.dev VITE_KC_SITE_APP_URL=https://app.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000 VITE_KC_CONNECTION_TIMEOUT_MS=15000

View File

@ -157,7 +157,7 @@ jobs:
timeout_minutes: 5 timeout_minutes: 5
max_attempts: 5 max_attempts: 5
env: env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -169,7 +169,7 @@ jobs:
if: always() if: always()
run: npm run test:snapshots -- --last-failed --update-snapshots run: npm run test:snapshots -- --last-failed --update-snapshots
env: env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -284,7 +284,7 @@ jobs:
timeout_minutes: 5 timeout_minutes: 5
max_attempts: 5 max_attempts: 5
env: env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -410,7 +410,7 @@ jobs:
max_attempts: 9 max_attempts: 9
env: env:
FAIL_ON_CONSOLE_ERRORS: true FAIL_ON_CONSOLE_ERRORS: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}

View File

@ -62,7 +62,7 @@ jobs:
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: xvfb-run -a npm run test:unit run: xvfb-run -a npm run test:unit
env: env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Check for changes - name: Check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}

View File

@ -65,7 +65,7 @@ If you're not a Zoo employee you won't be able to access the dev environment, yo
### Development environment variables ### Development environment variables
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service. The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `zoo.dev`). There is an optional environment variable called `VITE_KITTYCAD_API_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
### Developing in Chrome ### Developing in Chrome
@ -96,7 +96,7 @@ To package the app for your platform with electron-builder, run `npm run tronb:p
Prepare these system dependencies: Prepare these system dependencies:
- Set $token from https://zoo.dev/account/api-tokens - Set `$VITE_KITTYCAD_API_TOKEN` from https://zoo.dev/account/api-tokens
#### Snapshot tests (Google Chrome on Ubuntu only) #### Snapshot tests (Google Chrome on Ubuntu only)
@ -259,7 +259,7 @@ If the application needs to overwrite the known file on disk use this pattern. T
- `npm run circular-deps:overwrite` - `npm run circular-deps:overwrite`
- `npm run url-checker:overwrite` - `npm run url-checker:overwrite`
#### Diff baseline and current #### Diff baseline and current
These commands will write a /tmp/ file on disk and compare it to the known file in the repository. This command will also be used in the CI CD pipeline for automated checks These commands will write a /tmp/ file on disk and compare it to the known file in the repository. This command will also be used in the CI CD pipeline for automated checks

View File

@ -1,6 +1,5 @@
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing auth functionality
test.describe('Authentication tests', () => { test.describe('Authentication tests', () => {
test( test(
`The user can sign out and back in`, `The user can sign out and back in`,
@ -13,22 +12,12 @@ test.describe('Authentication tests', () => {
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.projectSection.waitFor() await homePage.projectSection.waitFor()
// This is only needed as an override to test-utils' setup() for this test
await page.addInitScript(() => {
localStorage.setItem('TOKEN_PERSIST_KEY', '')
})
await test.step('Click on sign out and expect sign in page', async () => { await test.step('Click on sign out and expect sign in page', async () => {
await toolbar.userSidebarButton.click() await toolbar.userSidebarButton.click()
await toolbar.signOutButton.click() await toolbar.signOutButton.click()
await expect(signInPage.signInButton).toBeVisible() await expect(signInPage.signInButton).toBeVisible()
}) })
await test.step("Refresh doesn't log the user back in", async () => {
await page.reload()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Click on sign in and cancel, click again and expect different code', async () => { await test.step('Click on sign in and cancel, click again and expect different code', async () => {
await signInPage.signInButton.click() await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible() await expect(signInPage.userCode).toBeVisible()

View File

@ -17,7 +17,7 @@ import dotenv from 'dotenv'
const NODE_ENV = process.env.NODE_ENV || 'development' const NODE_ENV = process.env.NODE_ENV || 'development'
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] }) dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
export const token = process.env.token || '' export const token = process.env.VITE_KITTYCAD_API_TOKEN || ''
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration' import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'

7
interface.d.ts vendored
View File

@ -72,16 +72,13 @@ export interface IElectronAPI {
} }
process: { process: {
env: { env: {
BASE_URL: string
IS_PLAYWRIGHT: string IS_PLAYWRIGHT: string
VITE_KC_DEV_TOKEN: string VITE_KITTYCAD_API_TOKEN: string
VITE_KC_API_WS_MODELING_URL: string VITE_KC_API_WS_MODELING_URL: string
VITE_KC_API_BASE_URL: string VITE_KITTYCAD_API_BASE_URL: string
VITE_KC_SITE_BASE_URL: string VITE_KC_SITE_BASE_URL: string
VITE_KC_SITE_APP_URL: string VITE_KC_SITE_APP_URL: string
VITE_KC_SKIP_AUTH: string
VITE_KC_CONNECTION_TIMEOUT_MS: string VITE_KC_CONNECTION_TIMEOUT_MS: string
VITE_KC_DEV_TOKEN: string
NODE_ENV: string NODE_ENV: string
PROD: string PROD: string
DEV: string DEV: string

View File

@ -10,71 +10,76 @@ DATA;
NAMED_UNIT(*) NAMED_UNIT(*)
SI_UNIT($, .METRE.) SI_UNIT($, .METRE.)
); );
#2 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $); #2 = (
#3 = ( NAMED_UNIT(*)
PLANE_ANGLE_UNIT()
SI_UNIT($, .RADIAN.)
);
#3 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $);
#4 = (
GEOMETRIC_REPRESENTATION_CONTEXT(3) GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2)) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3))
GLOBAL_UNIT_ASSIGNED_CONTEXT((#1)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1, #2))
REPRESENTATION_CONTEXT('', '3D') REPRESENTATION_CONTEXT('', '3D')
); );
#4 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); #5 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#5 = VERTEX_POINT('NONE', #4); #6 = VERTEX_POINT('NONE', #5);
#6 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005)); #7 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005));
#7 = VERTEX_POINT('NONE', #6); #8 = VERTEX_POINT('NONE', #7);
#8 = DIRECTION('NONE', (1, 0, -0)); #9 = DIRECTION('NONE', (1, 0, -0));
#9 = DIRECTION('NONE', (0, 1, 0)); #10 = DIRECTION('NONE', (0, 1, 0));
#10 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005)); #11 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005));
#11 = AXIS2_PLACEMENT_3D('NONE', #10, #9, #8); #12 = AXIS2_PLACEMENT_3D('NONE', #11, #10, #9);
#12 = CIRCLE('NONE', #11, 0.01); #13 = CIRCLE('NONE', #12, 0.01);
#13 = DIRECTION('NONE', (0, 1, 0)); #14 = DIRECTION('NONE', (0, 1, 0));
#14 = VECTOR('NONE', #13, 1); #15 = VECTOR('NONE', #14, 1);
#15 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); #16 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#16 = LINE('NONE', #15, #14); #17 = LINE('NONE', #16, #15);
#17 = DIRECTION('NONE', (1, 0, -0)); #18 = DIRECTION('NONE', (1, 0, -0));
#18 = DIRECTION('NONE', (0, 1, 0)); #19 = DIRECTION('NONE', (0, 1, 0));
#19 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005)); #20 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005));
#20 = AXIS2_PLACEMENT_3D('NONE', #19, #18, #17); #21 = AXIS2_PLACEMENT_3D('NONE', #20, #19, #18);
#21 = CIRCLE('NONE', #20, 0.01); #22 = CIRCLE('NONE', #21, 0.01);
#22 = EDGE_CURVE('NONE', #5, #5, #12, .T.); #23 = EDGE_CURVE('NONE', #6, #6, #13, .T.);
#23 = EDGE_CURVE('NONE', #5, #7, #16, .T.); #24 = EDGE_CURVE('NONE', #6, #8, #17, .T.);
#24 = EDGE_CURVE('NONE', #7, #7, #21, .T.); #25 = EDGE_CURVE('NONE', #8, #8, #22, .T.);
#25 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005)); #26 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005));
#26 = DIRECTION('NONE', (0, 1, 0)); #27 = DIRECTION('NONE', (0, 1, 0));
#27 = DIRECTION('NONE', (1, 0, -0)); #28 = DIRECTION('NONE', (1, 0, -0));
#28 = AXIS2_PLACEMENT_3D('NONE', #25, #26, #27); #29 = AXIS2_PLACEMENT_3D('NONE', #26, #27, #28);
#29 = CYLINDRICAL_SURFACE('NONE', #28, 0.01); #30 = CYLINDRICAL_SURFACE('NONE', #29, 0.01);
#30 = CARTESIAN_POINT('NONE', (0, -0.01, -0)); #31 = CARTESIAN_POINT('NONE', (0, -0.01, -0));
#31 = DIRECTION('NONE', (0, 1, 0)); #32 = DIRECTION('NONE', (0, 1, 0));
#32 = AXIS2_PLACEMENT_3D('NONE', #30, #31, $); #33 = AXIS2_PLACEMENT_3D('NONE', #31, #32, $);
#33 = PLANE('NONE', #32); #34 = PLANE('NONE', #33);
#34 = CARTESIAN_POINT('NONE', (0, 0, -0)); #35 = CARTESIAN_POINT('NONE', (0, 0, -0));
#35 = DIRECTION('NONE', (0, 1, 0)); #36 = DIRECTION('NONE', (0, 1, 0));
#36 = AXIS2_PLACEMENT_3D('NONE', #34, #35, $); #37 = AXIS2_PLACEMENT_3D('NONE', #35, #36, $);
#37 = PLANE('NONE', #36); #38 = PLANE('NONE', #37);
#38 = ORIENTED_EDGE('NONE', *, *, #22, .T.); #39 = ORIENTED_EDGE('NONE', *, *, #23, .T.);
#39 = ORIENTED_EDGE('NONE', *, *, #24, .F.); #40 = ORIENTED_EDGE('NONE', *, *, #25, .F.);
#40 = EDGE_LOOP('NONE', (#38)); #41 = EDGE_LOOP('NONE', (#39));
#41 = FACE_BOUND('NONE', #40, .T.); #42 = FACE_BOUND('NONE', #41, .T.);
#42 = EDGE_LOOP('NONE', (#39)); #43 = EDGE_LOOP('NONE', (#40));
#43 = FACE_BOUND('NONE', #42, .T.); #44 = FACE_BOUND('NONE', #43, .T.);
#44 = ADVANCED_FACE('NONE', (#41, #43), #29, .T.); #45 = ADVANCED_FACE('NONE', (#42, #44), #30, .T.);
#45 = ORIENTED_EDGE('NONE', *, *, #22, .F.); #46 = ORIENTED_EDGE('NONE', *, *, #23, .F.);
#46 = EDGE_LOOP('NONE', (#45)); #47 = EDGE_LOOP('NONE', (#46));
#47 = FACE_BOUND('NONE', #46, .T.); #48 = FACE_BOUND('NONE', #47, .T.);
#48 = ADVANCED_FACE('NONE', (#47), #33, .F.); #49 = ADVANCED_FACE('NONE', (#48), #34, .F.);
#49 = ORIENTED_EDGE('NONE', *, *, #24, .T.); #50 = ORIENTED_EDGE('NONE', *, *, #25, .T.);
#50 = EDGE_LOOP('NONE', (#49)); #51 = EDGE_LOOP('NONE', (#50));
#51 = FACE_BOUND('NONE', #50, .T.); #52 = FACE_BOUND('NONE', #51, .T.);
#52 = ADVANCED_FACE('NONE', (#51), #37, .T.); #53 = ADVANCED_FACE('NONE', (#52), #38, .T.);
#53 = CLOSED_SHELL('NONE', (#44, #48, #52)); #54 = CLOSED_SHELL('NONE', (#45, #49, #53));
#54 = MANIFOLD_SOLID_BREP('NONE', #53); #55 = MANIFOLD_SOLID_BREP('NONE', #54);
#55 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies'); #56 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies');
#56 = PRODUCT_DEFINITION_CONTEXT('part definition', #55, 'design'); #57 = PRODUCT_DEFINITION_CONTEXT('part definition', #56, 'design');
#57 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ()); #58 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ());
#58 = PRODUCT_DEFINITION_FORMATION('', $, #57); #59 = PRODUCT_DEFINITION_FORMATION('', $, #58);
#59 = PRODUCT_DEFINITION('design', $, #58, #56); #60 = PRODUCT_DEFINITION('design', $, #59, #57);
#60 = PRODUCT_DEFINITION_SHAPE('NONE', $, #59); #61 = PRODUCT_DEFINITION_SHAPE('NONE', $, #60);
#61 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#54), #3); #62 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#55), #4);
#62 = SHAPE_DEFINITION_REPRESENTATION(#60, #61); #63 = SHAPE_DEFINITION_REPRESENTATION(#61, #62);
ENDSEC; ENDSEC;
END-ISO-10303-21; END-ISO-10303-21;

View File

@ -994,6 +994,39 @@ impl Node<MemberExpression> {
// Check the property and object match -- e.g. ints for arrays, strs for objects. // Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object, property, self.computed) { match (object, property, self.computed) {
(KclValue::Plane { value: plane }, Property::String(property), false) => match property.as_str() {
"yAxis" => {
let (p, u) = plane.info.y_axis.as_3_dims();
Ok(KclValue::array_from_point3d(
p,
NumericType::Known(crate::exec::UnitType::Length(u)),
vec![meta],
))
}
"xAxis" => {
let (p, u) = plane.info.x_axis.as_3_dims();
Ok(KclValue::array_from_point3d(
p,
NumericType::Known(crate::exec::UnitType::Length(u)),
vec![meta],
))
}
"origin" => {
let (p, u) = plane.info.origin.as_3_dims();
Ok(KclValue::array_from_point3d(
p,
NumericType::Known(crate::exec::UnitType::Length(u)),
vec![meta],
))
}
other => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{other}' not found in plane"),
vec![self.clone().into()],
),
None,
)),
},
(KclValue::Object { value: map, meta: _ }, Property::String(property), false) => { (KclValue::Object { value: map, meta: _ }, Property::String(property), false) => {
if let Some(value) = map.get(&property) { if let Some(value) = map.get(&property) {
Ok(value.to_owned()) Ok(value.to_owned())
@ -1013,7 +1046,22 @@ impl Node<MemberExpression> {
vec![self.clone().into()], vec![self.clone().into()],
))) )))
} }
(KclValue::Object { .. }, p, _) => { (KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => {
if i == 0
&& let Some(value) = map.get("x")
{
return Ok(value.to_owned());
}
if i == 1
&& let Some(value) = map.get("y")
{
return Ok(value.to_owned());
}
if i == 2
&& let Some(value) = map.get("z")
{
return Ok(value.to_owned());
}
let t = p.type_name(); let t = p.type_name();
let article = article_for(t); let article = article_for(t);
Err(KclError::new_semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
@ -2205,4 +2253,12 @@ y = x[0mm + 1]
"#; "#;
parse_execute(ast).await.unwrap_err(); parse_execute(ast).await.unwrap_err();
} }
#[tokio::test(flavor = "multi_thread")]
async fn getting_property_of_plane() {
// let ast = include_str!("../../tests/inputs/planestuff.kcl");
let ast = std::fs::read_to_string("tests/inputs/planestuff.kcl").unwrap();
parse_execute(&ast).await.unwrap();
}
} }

View File

@ -921,6 +921,12 @@ impl Point3d {
units: UnitLen::Unknown, units: UnitLen::Unknown,
} }
} }
pub fn as_3_dims(&self) -> ([f64; 3], UnitLen) {
let p = [self.x, self.y, self.z];
let u = self.units;
(p, u)
}
} }
impl From<[TyF64; 3]> for Point3d { impl From<[TyF64; 3]> for Point3d {

View File

@ -458,6 +458,31 @@ impl KclValue {
} }
} }
/// Put the point into a KCL point.
pub fn array_from_point3d(p: [f64; 3], ty: NumericType, meta: Vec<Metadata>) -> Self {
let [x, y, z] = p;
Self::HomArray {
value: vec![
Self::Number {
value: x,
meta: meta.clone(),
ty,
},
Self::Number {
value: y,
meta: meta.clone(),
ty,
},
Self::Number {
value: z,
meta: meta.clone(),
ty,
},
],
ty: ty.into(),
}
}
pub(crate) fn as_usize(&self) -> Option<usize> { pub(crate) fn as_usize(&self) -> Option<usize> {
match self { match self {
KclValue::Number { value, .. } => crate::try_f64_to_usize(*value), KclValue::Number { value, .. } => crate::try_f64_to_usize(*value),

View File

@ -0,0 +1,60 @@
// There are 3 ways to define a plane in KCL, according to https://zoo.dev/docs/kcl-std/types/std-types-Plane
// - A default plane
// - Modifying a default plane e.g. via offsetPlane
// - Defining your own struct
// This file tests they all work equivalently.
// Define a plane using struct representation.
myPlane = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
}
// Prove we can get its axes and origin.
ax = myPlane.xAxis
assert(ax[0], isEqualTo = 1)
assert(ax[1], isEqualTo = 0)
assert(ax[2], isEqualTo = 0)
ay = myPlane.yAxis
assert(ay[0], isEqualTo = 0)
assert(ay[1], isEqualTo = 1)
assert(ay[2], isEqualTo = 0)
aorigin = myPlane.origin
assert(aorigin[0], isEqualTo = 0)
assert(aorigin[1], isEqualTo = 0)
assert(aorigin[2], isEqualTo = 0)
// Define a plane using standard planes.
myOtherPlane = XY
// Prove we can get its axes and origin.
axOther = myOtherPlane.xAxis
assert(axOther[0], isEqualTo = 1)
assert(axOther[1], isEqualTo = 0)
assert(axOther[2], isEqualTo = 0)
ayOther = myOtherPlane.yAxis
assert(ayOther[0], isEqualTo = 0)
assert(ayOther[1], isEqualTo = 1)
assert(ayOther[2], isEqualTo = 0)
aoriginOther = myOtherPlane.origin
assert(aoriginOther[0], isEqualTo = 0)
assert(aoriginOther[1], isEqualTo = 0)
assert(aoriginOther[2], isEqualTo = 0)
// Define a plane using a plane-modifying function like offsetPlane.
myAlternatePlane = offsetPlane(XY, offset = 0)
// Prove we can get its axes and origin.
axAlternate = myAlternatePlane.xAxis
assert(axAlternate[0], isEqualTo = 1)
assert(axAlternate[1], isEqualTo = 0)
assert(axAlternate[2], isEqualTo = 0)
ayAlternate = myAlternatePlane.yAxis
assert(ayAlternate[0], isEqualTo = 0)
assert(ayAlternate[1], isEqualTo = 1)
assert(ayAlternate[2], isEqualTo = 0)
aoriginAlternate = myAlternatePlane.origin
assert(aoriginAlternate[0], isEqualTo = 0)
assert(aoriginAlternate[1], isEqualTo = 0)
assert(aoriginAlternate[2], isEqualTo = 0)

View File

@ -4,6 +4,11 @@
URL STATUS URL STATUS
000 https://${BASE_URL} 000 https://${BASE_URL}
405 https://api.dev.zoo.dev/oauth2/token/revoke
401 https://api.dev.zoo.dev/users
401 https://dev.zoo.dev
401 https://dev.zoo.dev/docs
401 https://dev.zoo.dev/docs/kcl-samples/car-wheel-assembly
301 https://discord.gg/JQEpHR7Nt2 301 https://discord.gg/JQEpHR7Nt2
404 https://github.com/KittyCAD/engine/issues/3528 404 https://github.com/KittyCAD/engine/issues/3528
404 https://github.com/KittyCAD/modeling-app/commit/${ref} 404 https://github.com/KittyCAD/modeling-app/commit/${ref}
@ -17,5 +22,4 @@ URL STATUS
302 https://stackoverflow.com/a/58436959/22753272 302 https://stackoverflow.com/a/58436959/22753272
303 https://text-to-cad.zoo.dev/dashboard 303 https://text-to-cad.zoo.dev/dashboard
307 https://zoo.dev/ 307 https://zoo.dev/
308 https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text
308 https://zoo.dev/docs/kcl 308 https://zoo.dev/docs/kcl

View File

@ -53,7 +53,6 @@ import {
WASM_INIT_FAILED_TOAST_ID, WASM_INIT_FAILED_TOAST_ID,
} from '@src/lib/constants' } from '@src/lib/constants'
import { isPlaywright } from '@src/lib/isPlaywright' import { isPlaywright } from '@src/lib/isPlaywright'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { useNetworkHealthStatus } from '@src/components/NetworkHealthIndicator' import { useNetworkHealthStatus } from '@src/components/NetworkHealthIndicator'
import { useNetworkMachineStatus } from '@src/components/NetworkMachineIndicator' import { useNetworkMachineStatus } from '@src/components/NetworkMachineIndicator'
import { import {
@ -65,6 +64,7 @@ import { useModelingContext } from '@src/hooks/useModelingContext'
import { xStateValueToString } from '@src/lib/xStateValueToString' import { xStateValueToString } from '@src/lib/xStateValueToString'
import { getSelectionTypeDisplayText } from '@src/lib/selections' import { getSelectionTypeDisplayText } from '@src/lib/selections'
import type { StatusBarItemType } from '@src/components/StatusBar/statusBarTypes' import type { StatusBarItemType } from '@src/components/StatusBar/statusBarTypes'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
// CYCLIC REF // CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor sceneInfra.camControls.engineStreamActor = engineStreamActor
@ -189,7 +189,8 @@ export function App() {
() => () =>
DownloadAppToast({ DownloadAppToast({
onAccept: () => { onAccept: () => {
openWindow(`${VITE_KC_SITE_BASE_URL}/${APP_DOWNLOAD_PATH}`) const url = withSiteBaseURL(`/${APP_DOWNLOAD_PATH}`)
openWindow(url)
.then(() => { .then(() => {
toast.dismiss(DOWNLOAD_APP_TOAST_ID) toast.dismiss(DOWNLOAD_APP_TOAST_ID)
}) })

View File

@ -5,8 +5,8 @@ import {
BillingRemaining, BillingRemaining,
BillingRemainingMode, BillingRemainingMode,
} from '@src/components/BillingRemaining' } from '@src/components/BillingRemaining'
import { type BillingActor } from '@src/machines/billingMachine' import { type BillingActor } from '@src/machines/billingMachine'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
export const BillingDialog = (props: { billingActor: BillingActor }) => { export const BillingDialog = (props: { billingActor: BillingActor }) => {
const billingContext = useSelector( const billingContext = useSelector(
@ -42,7 +42,7 @@ export const BillingDialog = (props: { billingActor: BillingActor }) => {
{!hasUnlimited && ( {!hasUnlimited && (
<a <a
className="bg-ml-black text-ml-white rounded-lg text-center p-1 cursor-pointer" className="bg-ml-black text-ml-white rounded-lg text-center p-1 cursor-pointer"
href="https://zoo.dev/design-studio-pricing" href={withSiteBaseURL('/design-studio-pricing')}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
data-testid="billing-upgrade-button" data-testid="billing-upgrade-button"

View File

@ -15,6 +15,7 @@ import {
import { onboardingStartPath } from '@src/lib/onboardingPaths' import { onboardingStartPath } from '@src/lib/onboardingPaths'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
const HelpMenuDivider = () => ( const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" /> <div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -89,7 +90,7 @@ export function HelpMenu() {
<HelpMenuDivider /> <HelpMenuDivider />
<HelpMenuItem <HelpMenuItem
as="a" as="a"
href="https://zoo.dev/docs/kcl-samples" href={withSiteBaseURL('/docs/kcl-samples')}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -97,7 +98,7 @@ export function HelpMenu() {
</HelpMenuItem> </HelpMenuItem>
<HelpMenuItem <HelpMenuItem
as="a" as="a"
href="https://zoo.dev/docs/kcl-lang" href={withSiteBaseURL('/docs/kcl-lang')}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@ -7,7 +7,7 @@ import {
LanguageServerClient, LanguageServerClient,
LspWorkerEventType, LspWorkerEventType,
} from '@kittycad/codemirror-lsp-client' } from '@kittycad/codemirror-lsp-client'
import { TEST, VITE_KC_API_BASE_URL } from '@src/env' import env from '@src/env'
import React, { createContext, useContext, useMemo, useState } from 'react' import React, { createContext, useContext, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
@ -28,6 +28,7 @@ import type { FileEntry } from '@src/lib/project'
import { codeManager } from '@src/lib/singletons' import { codeManager } from '@src/lib/singletons'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { useToken } from '@src/lib/singletons' import { useToken } from '@src/lib/singletons'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -77,7 +78,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// But the server happens async so we break this into two parts. // But the server happens async so we break this into two parts.
// Below is the client and server promise. // Below is the client and server promise.
const { lspClient: kclLspClient } = useMemo(() => { const { lspClient: kclLspClient } = useMemo(() => {
if (!token || token === '' || TEST) { if (!token || token === '' || env().TEST) {
return { lspClient: null } return { lspClient: null }
} }
@ -85,7 +86,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = { const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
apiBaseUrl: VITE_KC_API_BASE_URL, apiBaseUrl: withAPIBaseURL(''),
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Kcl, worker: LspWorker.Kcl,
@ -136,7 +137,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => { const kclLSP = useMemo(() => {
let plugin = null let plugin = null
if (isKclLspReady && !TEST && kclLspClient) { if (isKclLspReady && !env().TEST && kclLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = kcl({ const lsp = kcl({
documentUri: `file:///${PROJECT_ENTRYPOINT}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,
@ -170,7 +171,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
}, [kclLspClient, isKclLspReady]) }, [kclLspClient, isKclLspReady])
const { lspClient: copilotLspClient } = useMemo(() => { const { lspClient: copilotLspClient } = useMemo(() => {
if (!token || token === '' || TEST) { if (!token || token === '' || env().TEST) {
return { lspClient: null } return { lspClient: null }
} }
@ -178,7 +179,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = { const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
apiBaseUrl: VITE_KC_API_BASE_URL, apiBaseUrl: withAPIBaseURL(''),
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Copilot, worker: LspWorker.Copilot,
@ -212,7 +213,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
const copilotLSP = useMemo(() => { const copilotLSP = useMemo(() => {
let plugin = null let plugin = null
if (isCopilotLspReady && !TEST && copilotLspClient) { if (isCopilotLspReady && !env().TEST && copilotLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = copilotPlugin({ const lsp = copilotPlugin({
documentUri: `file:///${PROJECT_ENTRYPOINT}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,

View File

@ -12,6 +12,7 @@ import { reportRejection } from '@src/lib/trap'
import { commandBarActor, settingsActor } from '@src/lib/singletons' import { commandBarActor, settingsActor } from '@src/lib/singletons'
import styles from './KclEditorMenu.module.css' import styles from './KclEditorMenu.module.css'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
export const KclEditorMenu = ({ children }: PropsWithChildren) => { export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
@ -67,7 +68,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Item> <Menu.Item>
<a <a
className={styles.button} className={styles.button}
href="https://zoo.dev/docs/kcl-lang" href={withSiteBaseURL('/docs/kcl-lang')}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()} onClick={openExternalBrowserIfDesktop()}
@ -108,7 +109,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Item> <Menu.Item>
<a <a
className={styles.button} className={styles.button}
href="https://zoo.dev/docs/kcl-samples" href={withSiteBaseURL('/docs/kcl-samples')}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()} onClick={openExternalBrowserIfDesktop()}

View File

@ -35,7 +35,7 @@ import {
rectangularSelection, rectangularSelection,
} from '@codemirror/view' } from '@codemirror/view'
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { TEST } from '@src/env' import env from '@src/env'
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef } from 'react'
@ -149,7 +149,7 @@ export const KclEditorPane = () => {
if (copilotLSP) extensions.push(copilotLSP) if (copilotLSP) extensions.push(copilotLSP)
// These extensions have proven to mess with vitest // These extensions have proven to mess with vitest
if (!TEST) { if (!env().TEST) {
extensions.push( extensions.push(
lintGutter(), lintGutter(),
lineNumbers(), lineNumbers(),

View File

@ -1,5 +1,4 @@
import { Transition } from '@headlessui/react' import { Transition } from '@headlessui/react'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { base64ToString } from '@src/lib/base64' import { base64ToString } from '@src/lib/base64'
@ -16,6 +15,7 @@ import { platform } from '@src/lib/utils'
import { codeManager } from '@src/lib/singletons' import { codeManager } from '@src/lib/singletons'
import { Logo } from '@src/components/Logo' import { Logo } from '@src/components/Logo'
import { useEffect } from 'react' import { useEffect } from 'react'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
/** /**
* This component is a handler that checks if a certain query parameter * This component is a handler that checks if a certain query parameter
@ -98,7 +98,7 @@ export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
> >
<Transition.Child <Transition.Child
as="div" as="div"
className={`max-w-3xl py-6 px-10 flex flex-col items-center gap-12 className={`max-w-3xl py-6 px-10 flex flex-col items-center gap-12
mx-auto border rounded-lg shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100`} mx-auto border rounded-lg shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100`}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0 scale-95"
@ -133,7 +133,7 @@ export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
buttonClasses + buttonClasses +
' text-sm border-transparent justify-center dark:bg-transparent' ' text-sm border-transparent justify-center dark:bg-transparent'
} }
to={`${VITE_KC_SITE_BASE_URL}/${APP_DOWNLOAD_PATH}`} to={withSiteBaseURL(`/${APP_DOWNLOAD_PATH}`)}
iconEnd={{ icon: 'link', bgClassName: '!bg-transparent' }} iconEnd={{ icon: 'link', bgClassName: '!bg-transparent' }}
> >
Download desktop app Download desktop app

View File

@ -12,9 +12,9 @@ import { Popover } from '@headlessui/react'
import Tooltip from '@src/components/Tooltip' import Tooltip from '@src/components/Tooltip'
import { HelpMenu } from '@src/components/HelpMenu' import { HelpMenu } from '@src/components/HelpMenu'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { APP_DOWNLOAD_PATH } from '@src/lib/constants' import { APP_DOWNLOAD_PATH } from '@src/lib/constants'
import { desktopAppPitchMessage } from '@src/components/DownloadAppToast' import { desktopAppPitchMessage } from '@src/components/DownloadAppToast'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
export const defaultGlobalStatusBarItems = ({ export const defaultGlobalStatusBarItems = ({
location, location,
@ -37,7 +37,7 @@ export const defaultGlobalStatusBarItems = ({
id: 'download-desktop-app', id: 'download-desktop-app',
element: 'externalLink', element: 'externalLink',
label: 'Download the app', label: 'Download the app',
href: `${VITE_KC_SITE_BASE_URL}/${APP_DOWNLOAD_PATH}`, href: withSiteBaseURL(`/${APP_DOWNLOAD_PATH}`),
icon: 'download', icon: 'download',
toolTip: { toolTip: {
children: desktopAppPitchMessage, children: desktopAppPitchMessage,

View File

@ -13,6 +13,7 @@ import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths' import { PATHS } from '@src/lib/paths'
import { authActor } from '@src/lib/singletons' import { authActor } from '@src/lib/singletons'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
type User = Models['User_type'] type User = Models['User_type']
@ -62,7 +63,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
{ {
id: 'account', id: 'account',
Element: 'externalLink', Element: 'externalLink',
to: 'https://zoo.dev/account', to: withSiteBaseURL('/account'),
children: ( children: (
<> <>
<span className="flex-1">Manage account</span> <span className="flex-1">Manage account</span>

126
src/env.test.ts Normal file
View File

@ -0,0 +1,126 @@
import env from '@src/env'
import { vi } from 'vitest'
import { viteEnv, windowElectronProcessEnv, processEnv } from '@src/env'
describe('@src/env', () => {
describe('default export', () => {
it('should run the process.env workflow', () => {
// vite > node.js
const expected = {
NODE_ENV: 'test',
VITE_KC_API_WS_MODELING_URL:
'wss://api.dev.zoo.dev/ws/modeling/commands',
VITE_KITTYCAD_API_BASE_URL: 'https://api.dev.zoo.dev',
VITE_KC_SITE_BASE_URL: 'https://dev.zoo.dev',
VITE_KC_SITE_APP_URL: 'https://app.dev.zoo.dev',
VITE_KC_CONNECTION_TIMEOUT_MS: '5000',
VITE_KITTYCAD_API_TOKEN: 'redacted',
PROD: undefined,
TEST: 'true',
DEV: '1',
CI: 'true',
}
const actual = env()
expect(typeof actual.VITE_KITTYCAD_API_TOKEN).toBe('string')
//@ts-ignore I do not want this token in our logs for any reason.
actual.VITE_KITTYCAD_API_TOKEN = 'redacted'
//@ts-ignore need to hard code this for localhost and CI
actual.CI = 'true'
expect(actual).toStrictEqual(expected)
})
})
describe('viteEnv', () => {
it('should match the EnvironmentVariables key types*', () => {
// Do not print entire object or compare, it contains a ton of ENV vars.
// We only need to match against EnvironmentVariables
const actual = viteEnv()
expect(typeof actual.NODE_ENV).toBe('string')
// Not passed in during tests?
expect(typeof actual.VITE_KC_WS_MODELING_URL).toBe('undefined')
expect(typeof actual.VITE_KITTYCAD_API_BASE_URL).toBe('string')
expect(typeof actual.VITE_KC_SITE_BASE_URL).toBe('string')
// Not passed in during tests?
expect(typeof actual.VITE_KC_SITE_API_URL).toBe('undefined')
expect(typeof actual.VITE_KC_CONNECTION_TIMEOUT_MS).toBe('string')
expect(typeof actual.VITE_KITTYCAD_API_TOKEN).toBe('string')
expect(typeof actual.PROD).toBe('boolean')
expect(typeof actual.TEST).toBe('string')
expect(typeof actual.DEV).toBe('boolean')
// Don't check CI...
})
})
describe('windowElectronProcessEnv', () => {
it('should return undefined in vitest runtime', () => {
const expected = undefined
const actual = windowElectronProcessEnv()
expect(actual).toBe(expected)
})
describe('When mocking window', () => {
it('should match the EnvironmentVariable key types*', () => {
vi.stubGlobal('electron', {
process: {
env: {
NODE_ENV: 'test',
VITE_KC_API_WS_MODELING_URL:
'wss://api.dev.zoo.dev/ws/modeling/commands',
VITE_KITTYCAD_API_BASE_URL: 'https://api.dev.zoo.dev',
VITE_KC_SITE_BASE_URL: 'https://dev.zoo.dev',
VITE_KC_SITE_APP_URL: 'https://app.dev.zoo.dev',
VITE_KC_CONNECTION_TIMEOUT_MS: '5000',
VITE_KITTYCAD_API_TOKEN: 'redacted',
PROD: undefined,
TEST: 'true',
DEV: '1',
CI: undefined,
},
},
})
const expected = {
NODE_ENV: 'test',
VITE_KC_API_WS_MODELING_URL:
'wss://api.dev.zoo.dev/ws/modeling/commands',
VITE_KITTYCAD_API_BASE_URL: 'https://api.dev.zoo.dev',
VITE_KC_SITE_BASE_URL: 'https://dev.zoo.dev',
VITE_KC_SITE_APP_URL: 'https://app.dev.zoo.dev',
VITE_KC_CONNECTION_TIMEOUT_MS: '5000',
VITE_KITTYCAD_API_TOKEN: 'redacted',
PROD: undefined,
TEST: 'true',
DEV: '1',
CI: undefined,
}
const actual = windowElectronProcessEnv()
expect(actual).toStrictEqual(expected)
vi.unstubAllGlobals()
})
})
it('should fail on missing window.electron', () => {
// someone didn't clean up their test if this fails!
const expected = undefined
const actual = windowElectronProcessEnv()
expect(actual).toBe(expected)
expect(window.electron).toBe(expected)
})
})
describe('processEnv', () => {
it('should match the EnvironmentVariables key types*', () => {
// Do not print entire object or compare, it contains a ton of ENV vars.
// We only need to match against EnvironmentVariables
const actual = processEnv()
expect(!!actual).toBe(true)
expect(typeof actual?.NODE_ENV).toBe('string')
// Not passed in during tests?
expect(typeof actual?.VITE_KC_WS_MODELING_URL).toBe('undefined')
expect(typeof actual?.VITE_KITTYCAD_API_BASE_URL).toBe('string')
expect(typeof actual?.VITE_KC_SITE_BASE_URL).toBe('string')
// Not passed in during tests?
expect(typeof actual?.VITE_KC_SITE_API_URL).toBe('undefined')
expect(typeof actual?.VITE_KC_CONNECTION_TIMEOUT_MS).toBe('string')
expect(typeof actual?.VITE_KITTYCAD_API_TOKEN).toBe('string')
expect(typeof actual?.PROD).toBe('string')
expect(typeof actual?.TEST).toBe('string')
expect(typeof actual?.DEV).toBe('string')
// Don't check CI...
})
})
})

View File

@ -1,21 +1,88 @@
// It turns out import.meta.env is a really fucky env var passing method. type EnvironmentVariables = {
// It's purely generated by Vite and nothing else. readonly NODE_ENV: string | undefined
// For Jest tests, we use babel to deal with it (it's a Syntax error otherwise) readonly VITE_KC_API_WS_MODELING_URL: string | undefined
// @ts-ignore: TS1343 readonly VITE_KITTYCAD_API_BASE_URL: string | undefined
const env = window.electron?.process.env ?? import.meta.env readonly VITE_KC_SITE_BASE_URL: string | undefined
readonly VITE_KC_SITE_APP_URL: string | undefined
readonly VITE_KC_CONNECTION_TIMEOUT_MS: string | undefined
readonly VITE_KITTYCAD_API_TOKEN: string | undefined
readonly PROD: string | undefined
readonly TEST: string | undefined
readonly DEV: string | undefined
readonly CI: string | undefined
}
export const NODE_ENV = env.NODE_ENV as string | undefined export const viteEnv = () => {
export const VITE_KC_API_WS_MODELING_URL = env.VITE_KC_API_WS_MODELING_URL as // It turns out import.meta.env is a really fucky env var passing method.
| string // It's purely generated by Vite and nothing else.
| undefined // For Jest tests, we use babel to deal with it (it's a Syntax error otherwise)
export const VITE_KC_API_BASE_URL = env.VITE_KC_API_BASE_URL // @ts-ignore: TS1343
export const VITE_KC_SITE_BASE_URL = env.VITE_KC_SITE_BASE_URL return import.meta.env
export const VITE_KC_SITE_APP_URL = env.VITE_KC_SITE_APP_URL }
export const VITE_KC_SKIP_AUTH = env.VITE_KC_SKIP_AUTH as string | undefined
export const VITE_KC_CONNECTION_TIMEOUT_MS = export const windowElectronProcessEnv = () => {
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined return typeof window !== 'undefined' && typeof window.electron !== 'undefined'
export const VITE_KC_DEV_TOKEN = env.VITE_KC_DEV_TOKEN as string | undefined ? window?.electron?.process?.env
export const PROD = env.PROD as string | undefined : undefined
export const TEST = env.TEST as string | undefined }
export const DEV = env.DEV as string | undefined
export const CI = env.CI as string | undefined export const processEnv = () => {
if (typeof process === 'undefined') {
// Web, no window.process or process
return undefined
} else if (
typeof process !== 'undefined' &&
typeof window !== 'undefined' &&
process.env.TEST === 'false'
) {
// Web, you made window.process, why :(, need process.env.TEST to make sure the frontend gets evaluated.
// The frontend can spoof this too :(
return undefined
}
return process.env
}
/**
* This function will work in any runtime. Note that we shouldn't be using this for any values outside of the
* EnvironmentVariables type. This is not going to replace process.env.
*
* Vite -> node.js -> bridge -> javascript
* We want to have the node.js and javascript runtime share the same code for getting these important configurations.
*/
export default (): EnvironmentVariables => {
// Compute the possible environment variables, order operation is important
// runtime (TODO) > process.env > window.electron.process.env > import.meta.env
const viteOnly = viteEnv()
const windowElectronProcessEnvOnly = windowElectronProcessEnv()
const processEnvOnly = processEnv()
const env = processEnvOnly || windowElectronProcessEnvOnly || viteOnly
// Vite uses Booleans and process.env uses strings
let PROD = env.PROD
if (typeof PROD === 'boolean') {
PROD = Number(PROD).toString()
}
let DEV = env.DEV
if (typeof DEV === 'boolean') {
DEV = Number(DEV).toString()
}
const environmentVariables: EnvironmentVariables = {
NODE_ENV: (env.NODE_ENV as string) || undefined,
VITE_KC_API_WS_MODELING_URL:
(env.VITE_KC_API_WS_MODELING_URL as string) || undefined,
VITE_KITTYCAD_API_BASE_URL:
(env.VITE_KITTYCAD_API_BASE_URL as string) || undefined,
VITE_KC_SITE_BASE_URL: (env.VITE_KC_SITE_BASE_URL as string) || undefined,
VITE_KC_SITE_APP_URL: (env.VITE_KC_SITE_APP_URL as string) || undefined,
VITE_KC_CONNECTION_TIMEOUT_MS:
(env.VITE_KC_CONNECTION_TIMEOUT_MS as string) || undefined,
VITE_KITTYCAD_API_TOKEN:
(env.VITE_KITTYCAD_API_TOKEN as string) || undefined,
PROD: PROD || undefined,
TEST: (env.TEST as string) || undefined,
DEV: DEV || undefined,
CI: (env.CI as string) || undefined,
}
return environmentVariables
}

View File

@ -1,4 +1,4 @@
import { VITE_KC_DEV_TOKEN } from '@src/env' import env from '@src/env'
import { createLiteral } from '@src/lang/create' import { createLiteral } from '@src/lang/create'
import type { import type {
@ -40,10 +40,9 @@ import { isOverlap } from '@src/lib/utils'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: env().VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -4,16 +4,15 @@ import { initPromise } from '@src/lang/wasmUtils'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import type { Selection } from '@src/lib/selections' import type { Selection } from '@src/lib/selections'
import { engineCommandManager, kclManager } from '@src/lib/singletons' import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { VITE_KC_DEV_TOKEN } from '@src/env' import env from '@src/env'
import { modifyAstWithTagsForSelection } from '@src/lang/modifyAst/tagManagement' import { modifyAstWithTagsForSelection } from '@src/lang/modifyAst/tagManagement'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: env().VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -1,5 +1,5 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from '@src/env' import env from '@src/env'
import { jsAppSettings } from '@src/lib/settings/settingsUtils' import { jsAppSettings } from '@src/lib/settings/settingsUtils'
import { BSON } from 'bson' import { BSON } from 'bson'
@ -387,7 +387,7 @@ class EngineConnection extends EventTarget {
// SHOULD ONLY BE USED FOR VITESTS // SHOULD ONLY BE USED FOR VITESTS
connectLite(callback: () => void) { connectLite(callback: () => void) {
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${256}&video_res_height=${256}` const url = `${env().VITE_KC_API_WS_MODELING_URL}?video_res_width=${256}&video_res_height=${256}`
this.websocket = new WebSocket(url, []) this.websocket = new WebSocket(url, [])
this.websocket.binaryType = 'arraybuffer' this.websocket.binaryType = 'arraybuffer'
@ -400,7 +400,7 @@ class EngineConnection extends EventTarget {
this.send({ this.send({
type: 'headers', type: 'headers',
headers: { headers: {
Authorization: `Bearer ${VITE_KC_DEV_TOKEN}`, Authorization: `Bearer ${env().VITE_KITTYCAD_API_TOKEN}`,
}, },
}) })
} }
@ -1531,7 +1531,7 @@ export class EngineCommandManager extends EventTarget {
additionalSettings += additionalSettings +=
'&show_grid=' + (this.settings.showScaleGrid ? 'true' : 'false') '&show_grid=' + (this.settings.showScaleGrid ? 'true' : 'false')
const pool = !this.settings.pool ? '' : `&pool=${this.settings.pool}` const pool = !this.settings.pool ? '' : `&pool=${this.settings.pool}`
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}` const url = `${env().VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
this.engineConnection = new EngineConnection({ this.engineConnection = new EngineConnection({
engineCommandManager: this, engineCommandManager: this,
url, url,

View File

@ -1,4 +1,3 @@
import { VITE_KC_API_BASE_URL } from '@src/env'
import { UAParser } from 'ua-parser-js' import { UAParser } from 'ua-parser-js'
import type { OsInfo } from '@rust/kcl-lib/bindings/OsInfo' import type { OsInfo } from '@rust/kcl-lib/bindings/OsInfo'
@ -11,6 +10,7 @@ import { isDesktop } from '@src/lib/isDesktop'
import type RustContext from '@src/lib/rustContext' import type RustContext from '@src/lib/rustContext'
import screenshot from '@src/lib/screenshot' import screenshot from '@src/lib/screenshot'
import { APP_VERSION } from '@src/routes/utils' import { APP_VERSION } from '@src/routes/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
/* eslint-disable suggest-no-throw/suggest-no-throw -- /* eslint-disable suggest-no-throw/suggest-no-throw --
* All the throws in CoreDumpManager are intentional and should be caught and handled properly * All the throws in CoreDumpManager are intentional and should be caught and handled properly
@ -35,7 +35,7 @@ export class CoreDumpManager {
codeManager: CodeManager codeManager: CodeManager
rustContext: RustContext rustContext: RustContext
token: string | undefined token: string | undefined
baseUrl: string = VITE_KC_API_BASE_URL baseUrl: string = withAPIBaseURL('')
constructor( constructor(
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,

View File

@ -1,4 +1,4 @@
import { DEV } from '@src/env' import env from '@src/env'
import type { import type {
Actor, Actor,
AnyStateMachine, AnyStateMachine,
@ -89,7 +89,8 @@ export function createMachineCommand<
} else if ('status' in commandConfig) { } else if ('status' in commandConfig) {
const { status } = commandConfig const { status } = commandConfig
if (status === 'inactive') return null if (status === 'inactive') return null
if (status === 'development' && !(DEV || IS_STAGING_OR_DEBUG)) return null if (status === 'development' && !(env().DEV || IS_STAGING_OR_DEBUG))
return null
} }
const icon = ('icon' in commandConfig && commandConfig.icon) || undefined const icon = ('icon' in commandConfig && commandConfig.icon) || undefined

View File

@ -1,4 +1,4 @@
import { DEV } from '@src/env' import env from '@src/env'
import isomorphicFetch from 'isomorphic-fetch' import isomorphicFetch from 'isomorphic-fetch'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
@ -28,7 +28,7 @@ export default async function crossPlatformFetch<T>(
// Add credentials: 'include' to options // Add credentials: 'include' to options
// We send the token with the headers only in development mode, DO NOT // We send the token with the headers only in development mode, DO NOT
// DO THIS IN PRODUCTION, as it is a security risk. // DO THIS IN PRODUCTION, as it is a security risk.
opts.headers = headers(DEV ? token : undefined) opts.headers = headers(env().DEV ? token : undefined)
opts.credentials = 'include' opts.credentials = 'include'
response = await fetch(url, opts) response = await fetch(url, opts)
} }

View File

@ -26,6 +26,7 @@ import { err } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types' import type { DeepPartial } from '@src/lib/types'
import { getInVariableCase } from '@src/lib/utils' import { getInVariableCase } from '@src/lib/utils'
import { IS_STAGING } from '@src/routes/utils' import { IS_STAGING } from '@src/routes/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function renameProjectDirectory( export async function renameProjectDirectory(
projectPath: string, projectPath: string,
@ -697,7 +698,9 @@ export const readTokenFile = async () => {
export const writeTokenFile = async (token: string) => { export const writeTokenFile = async (token: string) => {
const tokenFilePath = await getTokenFilePath() const tokenFilePath = await getTokenFilePath()
if (err(token)) return Promise.reject(token) if (err(token)) return Promise.reject(token)
return window.electron.writeFile(tokenFilePath, token) const result = window.electron.writeFile(tokenFilePath, token)
console.log('token written to disk')
return result
} }
export const writeTelemetryFile = async (content: string) => { export const writeTelemetryFile = async (content: string) => {
@ -722,12 +725,9 @@ export const setState = async (state: Project | undefined): Promise<void> => {
appStateStore = state appStateStore = state
} }
export const getUser = async ( export const getUser = async (token: string): Promise<Models['User_type']> => {
token: string,
hostname: string
): Promise<Models['User_type']> => {
try { try {
const user = await fetch(`${hostname}/users/me`, { const user = await fetch(withAPIBaseURL('/users/me'), {
headers: new Headers({ headers: new Headers({
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}), }),

View File

@ -1,4 +1,4 @@
import { VITE_KC_SITE_APP_URL } from '@src/env' import env from '@src/env'
import { createCreateFileUrl } from '@src/lib/links' import { createCreateFileUrl } from '@src/lib/links'
@ -9,7 +9,7 @@ describe(`link creation tests`, () => {
// Converted with external online tools // Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D` const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&code=${expectedEncodedCode}&ask-open-desktop=true` const expectedLink = `${env().VITE_KC_SITE_APP_URL}/?create-file=true&name=test&code=${expectedEncodedCode}&ask-open-desktop=true`
const result = createCreateFileUrl({ code, name, isRestrictedToOrg: false }) const result = createCreateFileUrl({ code, name, isRestrictedToOrg: false })
expect(result.toString()).toBe(expectedLink) expect(result.toString()).toBe(expectedLink)

View File

@ -1,4 +1,4 @@
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from '@src/env' import env from '@src/env'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { stringToBase64 } from '@src/lib/base64' import { stringToBase64 } from '@src/lib/base64'
@ -7,6 +7,7 @@ import {
CREATE_FILE_URL_PARAM, CREATE_FILE_URL_PARAM,
} from '@src/lib/constants' } from '@src/lib/constants'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export interface FileLinkParams { export interface FileLinkParams {
code: string code: string
@ -57,7 +58,7 @@ export async function copyFileShareLink(
* open the URL in the desktop app. * open the URL in the desktop app.
*/ */
export function createCreateFileUrl({ code, name }: FileLinkParams) { export function createCreateFileUrl({ code, name }: FileLinkParams) {
let origin = VITE_KC_SITE_APP_URL let origin = env().VITE_KC_SITE_APP_URL
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: String(true), [CREATE_FILE_URL_PARAM]: String(true),
name, name,
@ -96,7 +97,7 @@ export async function createShortlink(
if (password) { if (password) {
body.password = password body.password = password
} }
const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, { const response = await fetch(withAPIBaseURL('/user/shortlinks'), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-type': 'application/json', 'Content-type': 'application/json',

View File

@ -1,7 +1,6 @@
import type { SelectionRange } from '@codemirror/state' import type { SelectionRange } from '@codemirror/state'
import { EditorSelection, Transaction } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state'
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_BASE_URL } from '@src/env'
import { diffLines } from 'diff' import { diffLines } from 'diff'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import type { TextToCadMultiFileIteration_type } from '@kittycad/lib/dist/types/src/models' import type { TextToCadMultiFileIteration_type } from '@kittycad/lib/dist/types/src/models'
@ -28,6 +27,8 @@ import { uuidv4 } from '@src/lib/utils'
import type { File as KittyCadLibFile } from '@kittycad/lib/dist/types/src/models' import type { File as KittyCadLibFile } from '@kittycad/lib/dist/types/src/models'
import type { FileMeta } from '@src/lib/types' import type { FileMeta } from '@src/lib/types'
import type { RequestedKCLFile } from '@src/machines/systemIO/utils' import type { RequestedKCLFile } from '@src/machines/systemIO/utils'
import { withAPIBaseURL, withSiteBaseURL } from '@src/lib/withBaseURL'
import env from '@src/env'
type KclFileMetaMap = { type KclFileMetaMap = {
[execStateFileNamesIndex: number]: Extract<FileMeta, { type: 'kcl' }> [execStateFileNamesIndex: number]: Extract<FileMeta, { type: 'kcl' }>
@ -77,7 +78,7 @@ async function submitTextToCadRequest(
}) })
const response = await fetch( const response = await fetch(
`${VITE_KC_API_BASE_URL}/ml/text-to-cad/multi-file/iteration`, withAPIBaseURL('/ml/text-to-cad/multi-file/iteration'),
{ {
method: 'POST', method: 'POST',
headers: { headers: {
@ -304,7 +305,7 @@ export async function getPromptToEditResult(
id: string, id: string,
token?: string token?: string
): Promise<Models['TextToCadMultiFileIteration_type'] | Error> { ): Promise<Models['TextToCadMultiFileIteration_type'] | Error> {
const url = VITE_KC_API_BASE_URL + '/async/operations/' + id const url = withAPIBaseURL(`/async/operations/${id}`)
const data: Models['TextToCadMultiFileIteration_type'] | Error = const data: Models['TextToCadMultiFileIteration_type'] | Error =
await crossPlatformFetch( await crossPlatformFetch(
url, url,
@ -340,7 +341,7 @@ export async function doPromptEdit({
;(window as any).process = { ;(window as any).process = {
env: { env: {
ZOO_API_TOKEN: token, ZOO_API_TOKEN: token,
ZOO_HOST: VITE_KC_API_BASE_URL, ZOO_HOST: withAPIBaseURL(''),
}, },
} }
try { try {
@ -438,7 +439,7 @@ export async function promptToEditFlow({
return Promise.reject(result) return Promise.reject(result)
} }
const oldCodeWebAppOnly = codeManager.code const oldCodeWebAppOnly = codeManager.code
const downloadLink = `${VITE_KC_SITE_BASE_URL}/${APP_DOWNLOAD_PATH}` const downloadLink = withSiteBaseURL(`/${APP_DOWNLOAD_PATH}`)
if (!isDesktop() && Object.values(result.outputs).length > 1) { if (!isDesktop() && Object.values(result.outputs).length > 1) {
const toastId = uuidv4() const toastId = uuidv4()

View File

@ -2,7 +2,7 @@ import type { Configuration } from '@rust/kcl-lib/bindings/Configuration'
import type { NamedView } from '@rust/kcl-lib/bindings/NamedView' import type { NamedView } from '@rust/kcl-lib/bindings/NamedView'
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration' import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
import { default_app_settings } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib' import { default_app_settings } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
import { TEST } from '@src/env' import env from '@src/env'
import { import {
defaultAppSettings, defaultAppSettings,
@ -545,7 +545,7 @@ export function getSettingInputType(setting: Setting) {
export const jsAppSettings = async (): Promise<DeepPartial<Configuration>> => { export const jsAppSettings = async (): Promise<DeepPartial<Configuration>> => {
let jsAppSettings = default_app_settings() let jsAppSettings = default_app_settings()
if (!TEST) { if (!env().TEST) {
// TODO: https://github.com/KittyCAD/modeling-app/issues/6445 // TODO: https://github.com/KittyCAD/modeling-app/issues/6445
const settings = await import('@src/lib/singletons').then((module) => const settings = await import('@src/lib/singletons').then((module) =>
module.getSettings() module.getSettings()

View File

@ -1,4 +1,4 @@
import { VITE_KC_API_BASE_URL } from '@src/env' import { withAPIBaseURL } from '@src/lib/withBaseURL'
import EditorManager from '@src/editor/manager' import EditorManager from '@src/editor/manager'
import { KclManager } from '@src/lang/KclSingleton' import { KclManager } from '@src/lang/KclSingleton'
@ -171,7 +171,7 @@ const appMachine = setup({
systemId: BILLING, systemId: BILLING,
input: { input: {
...BILLING_CONTEXT_DEFAULTS, ...BILLING_CONTEXT_DEFAULTS,
urlUserService: VITE_KC_API_BASE_URL, urlUserService: withAPIBaseURL(''),
}, },
}), }),
], ],

View File

@ -1,5 +1,4 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_API_BASE_URL } from '@src/env'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import type { NavigateFunction } from 'react-router-dom' import type { NavigateFunction } from 'react-router-dom'
import { import {
@ -19,6 +18,7 @@ import { err, reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils' import { toSync } from '@src/lib/utils'
import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext' import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext'
import { joinOSPaths } from '@src/lib/paths' import { joinOSPaths } from '@src/lib/paths'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function submitTextToCadPrompt( export async function submitTextToCadPrompt(
prompt: string, prompt: string,
@ -32,7 +32,7 @@ export async function submitTextToCadPrompt(
kcl_version: kclManager.kclVersion, kcl_version: kclManager.kclVersion,
} }
// Glb has a smaller footprint than gltf, should we want to render it. // Glb has a smaller footprint than gltf, should we want to render it.
const url = VITE_KC_API_BASE_URL + '/ai/text-to-cad/glb?kcl=true' const url = withAPIBaseURL('/ai/text-to-cad/glb?kcl=true')
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch( const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
url, url,
{ {
@ -58,7 +58,7 @@ export async function getTextToCadResult(
id: string, id: string,
token?: string token?: string
): Promise<Models['TextToCad_type'] | Error> { ): Promise<Models['TextToCad_type'] | Error> {
const url = VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id const url = withAPIBaseURL(`/user/text-to-cad/${id}`)
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch( const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
url, url,
{ {

View File

@ -1,14 +1,13 @@
import type { Models } from '@kittycad/lib/dist/types/src' import type { Models } from '@kittycad/lib/dist/types/src'
import { VITE_KC_API_BASE_URL } from '@src/env'
import crossPlatformFetch from '@src/lib/crossPlatformFetch' import crossPlatformFetch from '@src/lib/crossPlatformFetch'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function sendTelemetry( export async function sendTelemetry(
id: string, id: string,
feedback: Models['MlFeedback_type'], feedback: Models['MlFeedback_type'],
token?: string token?: string
): Promise<void> { ): Promise<void> {
const url = const url = withAPIBaseURL(`/user/text-to-cad/${id}?feedback=${feedback}`)
VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id + '?feedback=' + feedback
await crossPlatformFetch( await crossPlatformFetch(
url, url,
{ {

View File

@ -11,6 +11,7 @@ import {
pipeHasCircle, pipeHasCircle,
} from '@src/machines/modelingMachine' } from '@src/machines/modelingMachine'
import { IS_ML_EXPERIMENTAL } from '@src/lib/constants' import { IS_ML_EXPERIMENTAL } from '@src/lib/constants'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
export type ToolbarModeName = 'modeling' | 'sketching' export type ToolbarModeName = 'modeling' | 'sketching'
@ -105,7 +106,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-sketch-startSketchOn', url: withSiteBaseURL(
'/docs/kcl-std/functions/std-sketch-startSketchOn'
),
}, },
], ],
}, },
@ -125,7 +128,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-sketch-extrude', url: withSiteBaseURL('/docs/kcl-std/functions/std-sketch-extrude'),
}, },
], ],
}, },
@ -145,7 +148,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-sketch-sweep', url: withSiteBaseURL('/docs/kcl-std/functions/std-sketch-sweep'),
}, },
], ],
}, },
@ -165,7 +168,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-sketch-loft', url: withSiteBaseURL('/docs/kcl-std/functions/std-sketch-loft'),
}, },
], ],
}, },
@ -185,11 +188,11 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-sketch-revolve', url: withSiteBaseURL('/docs/kcl-std/functions/std-sketch-revolve'),
}, },
{ {
label: 'KCL example', label: 'KCL example',
url: 'https://zoo.dev/docs/kcl-samples/ball-bearing', url: withSiteBaseURL('/docs/kcl-samples/ball-bearing'),
}, },
], ],
}, },
@ -209,7 +212,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-solid-fillet', url: withSiteBaseURL('/docs/kcl-std/functions/std-solid-fillet'),
}, },
], ],
}, },
@ -234,7 +237,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}, },
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-solid-chamfer', url: withSiteBaseURL('/docs/kcl-std/functions/std-solid-chamfer'),
}, },
], ],
}, },
@ -253,7 +256,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-solid-shell', url: withSiteBaseURL('/docs/kcl-std/functions/std-solid-shell'),
}, },
], ],
}, },
@ -275,7 +278,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-solid-union', url: withSiteBaseURL('/docs/kcl-std/functions/std-solid-union'),
}, },
], ],
}, },
@ -293,7 +296,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-solid-subtract', url: withSiteBaseURL(
'/docs/kcl-std/functions/std-solid-subtract'
),
}, },
], ],
}, },
@ -311,7 +316,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-solid-intersect', url: withSiteBaseURL(
'/docs/kcl-std/functions/std-solid-intersect'
),
}, },
], ],
}, },
@ -337,7 +344,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-offsetPlane', url: withSiteBaseURL('/docs/kcl-std/functions/std-offsetPlane'),
}, },
], ],
}, },
@ -368,7 +375,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-helix', url: withSiteBaseURL('/docs/kcl-std/functions/std-helix'),
}, },
], ],
}, },
@ -389,7 +396,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'API docs', label: 'API docs',
url: 'https://zoo.dev/docs/kcl-lang/modules', url: withSiteBaseURL('/docs/kcl-lang/modules'),
}, },
], ],
}, },
@ -410,7 +417,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'API docs', label: 'API docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-transform-translate', url: withSiteBaseURL(
'/docs/kcl-std/functions/std-transform-translate'
),
}, },
], ],
}, },
@ -428,7 +437,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'API docs', label: 'API docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-transform-rotate', url: withSiteBaseURL(
'/docs/kcl-std/functions/std-transform-rotate'
),
}, },
], ],
}, },
@ -446,7 +457,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'API docs', label: 'API docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-clone', url: withSiteBaseURL('/docs/kcl-std/functions/std-clone'),
}, },
], ],
}, },
@ -482,7 +493,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'API docs', label: 'API docs',
url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text', url: withSiteBaseURL(
'/docs/api/ml/generate-a-cad-model-from-text'
),
}, },
], ],
}, },
@ -739,7 +752,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-sketch-polygon', url: withSiteBaseURL('/docs/kcl-std/functions/std-sketch-polygon'),
}, },
], ],
}, },
@ -755,7 +768,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
links: [ links: [
{ {
label: 'KCL docs', label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-transform-mirror2d', url: withSiteBaseURL(
'/docs/kcl-std/functions/std-transform-mirror2d'
),
}, },
], ],
}, },

View File

@ -0,0 +1,61 @@
import { withAPIBaseURL, withSiteBaseURL } from '@src/lib/withBaseURL'
describe('withBaseURL', () => {
/**
* running in the development environment
* the .env.development should load
*/
describe('withAPIBaseURL', () => {
it('should return base url', () => {
const expected = 'https://api.dev.zoo.dev'
const actual = withAPIBaseURL('')
expect(actual).toBe(expected)
})
it('should return base url with /users', () => {
const expected = 'https://api.dev.zoo.dev/users'
const actual = withAPIBaseURL('/users')
expect(actual).toBe(expected)
})
it('should return a longer base url with /oauth2/token/revoke', () => {
const expected = 'https://api.dev.zoo.dev/oauth2/token/revoke'
const actual = withAPIBaseURL('/oauth2/token/revoke')
expect(actual).toBe(expected)
})
it('should ensure base url does not have ending slash', () => {
const expected = 'https://api.dev.zoo.dev'
const actual = withAPIBaseURL('')
expect(actual).toBe(expected)
const expectedEndsWith = expected[expected.length - 1]
const actualEndsWith = actual[actual.length - 1]
expect(actual).toBe(expected)
expect(actualEndsWith).toBe(expectedEndsWith)
})
})
describe('withSiteBaseURL', () => {
it('should return base url', () => {
const expected = 'https://dev.zoo.dev'
const actual = withSiteBaseURL('')
expect(actual).toBe(expected)
})
it('should return base url with /docs', () => {
const expected = 'https://dev.zoo.dev/docs'
const actual = withSiteBaseURL('/docs')
expect(actual).toBe(expected)
})
it('should return a longer base base url with /docs/kcl-samples/car-wheel-assembly', () => {
const expected = 'https://dev.zoo.dev/docs/kcl-samples/car-wheel-assembly'
const actual = withSiteBaseURL('/docs/kcl-samples/car-wheel-assembly')
expect(actual).toBe(expected)
})
it('should ensure base url does not have ending slash', () => {
const expected = 'https://dev.zoo.dev'
const actual = withSiteBaseURL('')
expect(actual).toBe(expected)
const expectedEndsWith = expected[expected.length - 1]
const actualEndsWith = actual[actual.length - 1]
expect(actual).toBe(expected)
expect(actualEndsWith).toBe(expectedEndsWith)
})
})
})

View File

@ -1,5 +1,9 @@
import { VITE_KC_API_BASE_URL } from '@src/env' import env from '@src/env'
export default function withBaseUrl(path: string): string { export function withAPIBaseURL(path: string): string {
return VITE_KC_API_BASE_URL + path return env().VITE_KITTYCAD_API_BASE_URL + path
}
export function withSiteBaseURL(path: string): string {
return env().VITE_KC_SITE_BASE_URL + path
} }

View File

@ -1,10 +1,5 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { import env from '@src/env'
DEV,
VITE_KC_API_BASE_URL,
VITE_KC_DEV_TOKEN,
VITE_KC_SKIP_AUTH,
} from '@src/env'
import { assign, fromPromise, setup } from 'xstate' import { assign, fromPromise, setup } from 'xstate'
import { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants' import { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants'
@ -15,32 +10,9 @@ import {
} from '@src/lib/desktop' } from '@src/lib/desktop'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { markOnce } from '@src/lib/performance' import { markOnce } from '@src/lib/performance'
import { import { withAPIBaseURL } from '@src/lib/withBaseURL'
default as withBaseURL,
default as withBaseUrl,
} from '@src/lib/withBaseURL'
import { ACTOR_IDS } from '@src/machines/machineConstants' import { ACTOR_IDS } from '@src/machines/machineConstants'
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
const LOCAL_USER: Models['User_type'] = {
id: '8675309',
name: 'Test User',
email: 'kittycad.sidebar.test@example.com',
image: 'https://placekitten.com/200/200',
created_at: 'yesteryear',
updated_at: 'today',
company: 'Test Company',
discord: 'Test User#1234',
github: 'testuser',
phone: '555-555-5555',
first_name: 'Test',
last_name: 'User',
can_train_on_data: false,
is_service_account: false,
deletion_scheduled: false,
}
export interface UserContext { export interface UserContext {
user?: Models['User_type'] user?: Models['User_type']
token: string token: string
@ -56,11 +28,21 @@ export type Events =
} }
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
/**
* Determine which token do we have persisted to initialize the auth machine
*/
const persistedCookie = getCookie(COOKIE_NAME)
const persistedLocalStorage = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
const persistedDevToken = env().VITE_KITTYCAD_API_TOKEN
export const persistedToken = export const persistedToken =
VITE_KC_DEV_TOKEN || persistedDevToken || persistedCookie || persistedLocalStorage
getCookie(COOKIE_NAME) || console.log('Initial persisted token')
localStorage?.getItem(TOKEN_PERSIST_KEY) || console.table([
'' ['cookie', !!persistedCookie],
['local storage', !!persistedLocalStorage],
['api token', !!persistedDevToken],
])
export const authMachine = setup({ export const authMachine = setup({
types: {} as { types: {} as {
@ -157,7 +139,7 @@ export const authMachine = setup({
async function getUser(input: { token?: string }) { async function getUser(input: { token?: string }) {
const token = await getAndSyncStoredToken(input) const token = await getAndSyncStoredToken(input)
const url = withBaseURL('/user') const url = withAPIBaseURL('/user')
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
@ -165,21 +147,8 @@ async function getUser(input: { token?: string }) {
if (!token && isDesktop()) return Promise.reject(new Error('No token found')) if (!token && isDesktop()) return Promise.reject(new Error('No token found'))
if (token) headers['Authorization'] = `Bearer ${token}` if (token) headers['Authorization'] = `Bearer ${token}`
if (SKIP_AUTH) {
// For local tests
if (localStorage.getItem('FORCE_NO_IMAGE')) {
LOCAL_USER.image = ''
}
markOnce('code/didAuth')
return {
user: LOCAL_USER,
token,
}
}
const userPromise = isDesktop() const userPromise = isDesktop()
? getUserDesktop(token, VITE_KC_API_BASE_URL) ? getUserDesktop(token)
: fetch(url, { : fetch(url, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
@ -228,12 +197,25 @@ async function getAndSyncStoredToken(input: {
token?: string token?: string
}): Promise<string> { }): Promise<string> {
// dev mode // dev mode
if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN const VITE_KITTYCAD_API_TOKEN = env().VITE_KITTYCAD_API_TOKEN
if (VITE_KITTYCAD_API_TOKEN) {
console.log('Token used for authentication')
console.table([['api token', !!VITE_KITTYCAD_API_TOKEN]])
return VITE_KITTYCAD_API_TOKEN
}
const token = const inputToken = input.token && input.token !== '' ? input.token : ''
input.token && input.token !== '' const cookieToken = getCookie(COOKIE_NAME)
? input.token const localStorageToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || '' const token = inputToken || cookieToken || localStorageToken
console.log('Token used for authentication')
console.table([
['persisted token', !!inputToken],
['cookie', !!cookieToken],
['local storage', !!localStorageToken],
['api token', !!VITE_KITTYCAD_API_TOKEN],
])
if (token) { if (token) {
// has just logged in, update storage // has just logged in, update storage
localStorage.setItem(TOKEN_PERSIST_KEY, token) localStorage.setItem(TOKEN_PERSIST_KEY, token)
@ -259,7 +241,7 @@ async function logout() {
if (token) { if (token) {
try { try {
await fetch(withBaseUrl('/oauth2/token/revoke'), { await fetch(withAPIBaseURL('/oauth2/token/revoke'), {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
@ -282,7 +264,7 @@ async function logout() {
} }
} }
return fetch(withBaseUrl('/logout'), { return fetch(withAPIBaseURL('/logout'), {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}) })

View File

@ -11,7 +11,7 @@ import {
engineCommandManager, engineCommandManager,
kclManager, kclManager,
} from '@src/lib/singletons' } from '@src/lib/singletons'
import { VITE_KC_DEV_TOKEN } from '@src/env' import env from '@src/env'
import { getConstraintInfoKw } from '@src/lang/std/sketch' import { getConstraintInfoKw } from '@src/lang/std/sketch'
import { getNodeFromPath } from '@src/lang/queryAst' import { getNodeFromPath } from '@src/lang/queryAst'
import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Node } from '@rust/kcl-lib/bindings/Node'
@ -29,10 +29,9 @@ import { removeSingleConstraintInfo } from '@src/lang/modifyAst'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: env().VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -1,5 +1,5 @@
import { engineCommandManager, kclManager } from '@src/lib/singletons' import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { VITE_KC_DEV_TOKEN } from '@src/env' import env from '@src/env'
import { getModuleIdByFileName, isArray } from '@src/lib/utils' import { getModuleIdByFileName, isArray } from '@src/lib/utils'
import { vi, inject } from 'vitest' import { vi, inject } from 'vitest'
import { assertParse } from '@src/lang/wasm' import { assertParse } from '@src/lang/wasm'
@ -355,10 +355,9 @@ cases.push(
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: env().VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -70,12 +70,10 @@ dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
// default vite values based on mode // default vite values based on mode
process.env.NODE_ENV ??= viteEnv.MODE process.env.NODE_ENV ??= viteEnv.MODE
process.env.BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL
process.env.VITE_KC_API_BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL process.env.VITE_KITTYCAD_API_BASE_URL ??= viteEnv.VITE_KITTYCAD_API_BASE_URL
process.env.VITE_KC_SITE_BASE_URL ??= viteEnv.VITE_KC_SITE_BASE_URL process.env.VITE_KC_SITE_BASE_URL ??= viteEnv.VITE_KC_SITE_BASE_URL
process.env.VITE_KC_SITE_APP_URL ??= viteEnv.VITE_KC_SITE_APP_URL process.env.VITE_KC_SITE_APP_URL ??= viteEnv.VITE_KC_SITE_APP_URL
process.env.VITE_KC_SKIP_AUTH ??= viteEnv.VITE_KC_SKIP_AUTH
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??=
viteEnv.VITE_KC_CONNECTION_TIMEOUT_MS viteEnv.VITE_KC_CONNECTION_TIMEOUT_MS

View File

@ -5,6 +5,7 @@ import { reportRejection } from '@src/lib/trap'
import { typeSafeWebContentsSend } from '@src/menu/channels' import { typeSafeWebContentsSend } from '@src/menu/channels'
import type { ZooMenuItemConstructorOptions } from '@src/menu/roles' import type { ZooMenuItemConstructorOptions } from '@src/menu/roles'
import { getAutoUpdater } from '@src/updater' import { getAutoUpdater } from '@src/updater'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
export const helpRole = ( export const helpRole = (
mainWindow: BrowserWindow mainWindow: BrowserWindow
@ -26,14 +27,16 @@ export const helpRole = (
id: 'Help.KCL code samples', id: 'Help.KCL code samples',
click: () => { click: () => {
shell shell
.openExternal('https://zoo.dev/docs/kcl-samples') .openExternal(withSiteBaseURL('/docs/kcl-samples'))
.catch(reportRejection) .catch(reportRejection)
}, },
}, },
{ {
label: 'KCL Docs', label: 'KCL Docs',
click: () => { click: () => {
shell.openExternal('https://zoo.dev/docs/kcl').catch(reportRejection) shell
.openExternal(withSiteBaseURL('/docs/kcl'))
.catch(reportRejection)
}, },
}, },
{ {
@ -116,7 +119,7 @@ export const helpRole = (
{ {
label: 'Manage Account', label: 'Manage Account',
click: () => { click: () => {
shell.openExternal('https://zoo.dev/account').catch(reportRejection) shell.openExternal(withSiteBaseURL('/account')).catch(reportRejection)
}, },
}, },
], ],

View File

@ -289,12 +289,11 @@ contextBridge.exposeInMainWorld('electron', {
exposeProcessEnvs([ exposeProcessEnvs([
'NODE_ENV', 'NODE_ENV',
'VITE_KC_API_WS_MODELING_URL', 'VITE_KC_API_WS_MODELING_URL',
'VITE_KC_API_BASE_URL', 'VITE_KITTYCAD_API_BASE_URL',
'VITE_KC_SITE_BASE_URL', 'VITE_KC_SITE_BASE_URL',
'VITE_KC_SITE_APP_URL', 'VITE_KC_SITE_APP_URL',
'VITE_KC_SKIP_AUTH',
'VITE_KC_CONNECTION_TIMEOUT_MS', 'VITE_KC_CONNECTION_TIMEOUT_MS',
'VITE_KC_DEV_TOKEN', 'VITE_KITTYCAD_API_TOKEN',
'IS_PLAYWRIGHT', 'IS_PLAYWRIGHT',

View File

@ -67,6 +67,7 @@ import {
defaultGlobalStatusBarItems, defaultGlobalStatusBarItems,
} from '@src/components/StatusBar/defaultStatusBarItems' } from '@src/components/StatusBar/defaultStatusBarItems'
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
type ReadWriteProjectState = { type ReadWriteProjectState = {
value: boolean value: boolean
@ -367,9 +368,9 @@ const Home = () => {
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/docs" to={withSiteBaseURL('/account')}
onClick={openExternalBrowserIfDesktop( onClick={openExternalBrowserIfDesktop(
'https://zoo.dev/account' withSiteBaseURL('/account')
)} )}
className={sidebarButtonClasses} className={sidebarButtonClasses}
iconStart={{ iconStart={{
@ -384,8 +385,8 @@ const Home = () => {
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/blog" to={withSiteBaseURL('/blog')}
onClick={openExternalBrowserIfDesktop('https://zoo.dev/blog')} onClick={openExternalBrowserIfDesktop(withSiteBaseURL('/blog'))}
className={sidebarButtonClasses} className={sidebarButtonClasses}
iconStart={{ iconStart={{
icon: 'glasses', icon: 'glasses',

View File

@ -25,12 +25,12 @@ import { systemIOActor, commandBarActor } from '@src/lib/singletons'
import type { IndexLoaderData } from '@src/lib/types' import type { IndexLoaderData } from '@src/lib/types'
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils' import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow' import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { import {
browserAxialFan, browserAxialFan,
browserAxialFanAfterTextToCad, browserAxialFanAfterTextToCad,
} from '@src/lib/exampleKcl' } from '@src/lib/exampleKcl'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
type BrowserOnboaringRoute = RouteObject & { type BrowserOnboaringRoute = RouteObject & {
path: keyof typeof browserOnboardingPaths path: keyof typeof browserOnboardingPaths
@ -461,7 +461,7 @@ function PromptToEditResult() {
function OnboardingConclusion() { function OnboardingConclusion() {
// Close the panes on mount, close on unmount // Close the panes on mount, close on unmount
useOnboardingPanes() useOnboardingPanes()
const downloadLink = `${VITE_KC_SITE_BASE_URL}/${APP_DOWNLOAD_PATH}` const downloadLink = withSiteBaseURL(`/${APP_DOWNLOAD_PATH}`)
return ( return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-16 grid justify-center items-center"> <div className="cursor-not-allowed fixed inset-0 z-50 p-16 grid justify-center items-center">

View File

@ -27,8 +27,8 @@ import {
modifiedFanHousingBrowser, modifiedFanHousingBrowser,
modifiedParametersDesktop, modifiedParametersDesktop,
} from '@src/lib/exampleKcl' } from '@src/lib/exampleKcl'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow' import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
type DesktopOnboardingRoute = RouteObject & { type DesktopOnboardingRoute = RouteObject & {
path: keyof typeof desktopOnboardingPaths path: keyof typeof desktopOnboardingPaths
@ -642,10 +642,8 @@ function OnboardingConclusion() {
project, click the Zoo button in the top left. To learn more detailed project, click the Zoo button in the top left. To learn more detailed
and advanced techniques,{' '} and advanced techniques,{' '}
<a <a
onClick={openExternalBrowserIfDesktop( onClick={openExternalBrowserIfDesktop(withSiteBaseURL('/docs'))}
`${VITE_KC_SITE_BASE_URL}/docs` href={`${withSiteBaseURL('/docs')}`}
)}
href={`${VITE_KC_SITE_BASE_URL}/docs`}
> >
check out our docs check out our docs
</a> </a>

View File

@ -6,7 +6,6 @@ import { Link } from 'react-router-dom'
import { ActionButton } from '@src/components/ActionButton' import { ActionButton } from '@src/components/ActionButton'
import { CustomIcon } from '@src/components/CustomIcon' import { CustomIcon } from '@src/components/CustomIcon'
import { Logo } from '@src/components/Logo' import { Logo } from '@src/components/Logo'
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_BASE_URL } from '@src/env'
import { APP_NAME } from '@src/lib/constants' import { APP_NAME } from '@src/lib/constants'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow' import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
@ -15,6 +14,7 @@ import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils' import { toSync } from '@src/lib/utils'
import { authActor, useSettings } from '@src/lib/singletons' import { authActor, useSettings } from '@src/lib/singletons'
import { APP_VERSION, generateSignInUrl } from '@src/routes/utils' import { APP_VERSION, generateSignInUrl } from '@src/routes/utils'
import { withAPIBaseURL, withSiteBaseURL } from '@src/lib/withBaseURL'
const subtleBorder = const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80' 'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
@ -36,7 +36,7 @@ const SignIn = () => {
app: { theme }, app: { theme },
} = useSettings() } = useSettings()
const signInUrl = generateSignInUrl() const signInUrl = generateSignInUrl()
const kclSampleUrl = `${VITE_KC_SITE_BASE_URL}/docs/kcl-samples/car-wheel-assembly` const kclSampleUrl = withSiteBaseURL('/docs/kcl-samples/car-wheel-assembly')
const getThemeText = useCallback( const getThemeText = useCallback(
(shouldContrast = true) => (shouldContrast = true) =>
@ -54,7 +54,7 @@ const SignIn = () => {
const signInDesktop = async () => { const signInDesktop = async () => {
// We want to invoke our command to login via device auth. // We want to invoke our command to login via device auth.
const userCodeToDisplay = await window.electron const userCodeToDisplay = await window.electron
.startDeviceFlow(VITE_KC_API_BASE_URL + location.search) .startDeviceFlow(withAPIBaseURL(location.search))
.catch(reportError) .catch(reportError)
if (!userCodeToDisplay) { if (!userCodeToDisplay) {
console.error('No user code received while trying to log in') console.error('No user code received while trying to log in')
@ -260,7 +260,7 @@ const SignIn = () => {
<div className="flex gap-4 flex-wrap items-center"> <div className="flex gap-4 flex-wrap items-center">
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/docs/kcl-samples/pillow-block-bearing" to={withSiteBaseURL('/docs/kcl-samples/pillow-block-bearing')}
iconStart={{ iconStart={{
icon: 'settings', icon: 'settings',
bgClassName: '!bg-transparent', bgClassName: '!bg-transparent',
@ -273,7 +273,7 @@ const SignIn = () => {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/docs/zoo-design-studio/text-to-cad" to={withSiteBaseURL('/docs/zoo-design-studio/text-to-cad')}
iconStart={{ iconStart={{
icon: 'sparkles', icon: 'sparkles',
bgClassName: '!bg-transparent', bgClassName: '!bg-transparent',
@ -296,7 +296,7 @@ const SignIn = () => {
<div className="flex gap-4 flex-wrap items-center"> <div className="flex gap-4 flex-wrap items-center">
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/design-api" to={withSiteBaseURL('/design-api')}
iconStart={{ icon: 'sketch', bgClassName: '!bg-transparent' }} iconStart={{ icon: 'sketch', bgClassName: '!bg-transparent' }}
className="!bg-primary !text-chalkboard-10 !border-transarent" className="!bg-primary !text-chalkboard-10 !border-transarent"
> >
@ -304,7 +304,7 @@ const SignIn = () => {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/machine-learning-api" to={withSiteBaseURL('/machine-learning-api')}
iconStart={{ iconStart={{
icon: 'elephant', icon: 'elephant',
bgClassName: '!bg-transparent', bgClassName: '!bg-transparent',

View File

@ -1,15 +1,16 @@
import { NODE_ENV, VITE_KC_SITE_BASE_URL } from '@src/env' import env from '@src/env'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { import {
IS_PLAYWRIGHT_KEY, IS_PLAYWRIGHT_KEY,
IMMEDIATE_SIGN_IN_IF_NECESSARY_QUERY_PARAM, IMMEDIATE_SIGN_IN_IF_NECESSARY_QUERY_PARAM,
} from '@src/lib/constants' } from '@src/lib/constants'
import { PATHS } from '@src/lib/paths' import { PATHS } from '@src/lib/paths'
import { withSiteBaseURL } from '@src/lib/withBaseURL'
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true' const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'
export const APP_VERSION = export const APP_VERSION =
isTestEnv && NODE_ENV === 'development' isTestEnv && env().NODE_ENV === 'development'
? '11.22.33' ? '11.22.33'
: isDesktop() : isDesktop()
? // @ts-ignore ? // @ts-ignore
@ -54,7 +55,7 @@ export function generateSignInUrl() {
'?' '?'
) )
return `${VITE_KC_SITE_BASE_URL}${ return withSiteBaseURL(
PATHS.SIGN_IN `${PATHS.SIGN_IN}?callbackUrl=${encodeURIComponent(finalURL)}`
}?callbackUrl=${encodeURIComponent(finalURL)}` )
} }