From 76f1d09abb33873984b50eccbf1cb88a0f6883e8 Mon Sep 17 00:00:00 2001 From: 49fl Date: Wed, 15 Jan 2025 18:30:20 -0500 Subject: [PATCH] Fix broken golden standard tests caused by changes to kcl-samples (#5065) * Fix our golden standard tests (broken by new assemblies kcl-samples) * Finally use the right combination of env vars * Fix the manifest * Continue to fix multiple file kcl-samples * Fix loading in desktop app * Type narrow for tsc * fmt --------- Co-authored-by: Frank Noirot --- .../testing-samples-loading.spec.ts | 16 +- public/kcl-samples-manifest-fallback.json | 139 ++++++++++++------ src/components/FileMachineProvider.tsx | 6 +- src/lang/kclSamples.test.ts | 120 +++++++-------- src/lang/std/fileSystemManager.ts | 42 +++--- src/lib/getKclSamplesManifest.ts | 2 + src/lib/kclCommands.ts | 38 +++-- 7 files changed, 216 insertions(+), 147 deletions(-) diff --git a/e2e/playwright/testing-samples-loading.spec.ts b/e2e/playwright/testing-samples-loading.spec.ts index 96da7d5ba7..44f7fc95be 100644 --- a/e2e/playwright/testing-samples-loading.spec.ts +++ b/e2e/playwright/testing-samples-loading.spec.ts @@ -8,8 +8,8 @@ import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' test.describe('Testing in-app sample loading', () => { /** - * Note this test implicitly depends on the KCL sample "car-wheel.kcl", - * its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl + * Note this test implicitly depends on the KCL sample "a-parametric-bearing-pillow-block", + * its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl */ test('Web: should overwrite current code, cannot create new file', async ({ editor, @@ -29,8 +29,8 @@ test.describe('Testing in-app sample loading', () => { // Locators and constants const newSample = { - file: 'car-wheel' + FILE_EXT, - title: 'Car Wheel', + file: 'a-parametric-bearing-pillow-block' + FILE_EXT, + title: 'A Parametric Bearing Pillow Block', } const commandBarButton = page.getByRole('button', { name: 'Commands' }) const samplesCommandOption = page.getByRole('option', { @@ -75,8 +75,8 @@ test.describe('Testing in-app sample loading', () => { /** * Note this test implicitly depends on the KCL samples: - * "car-wheel.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl - * "gear-rack.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/gear-rack.kcl + * "a-parametric-bearing-pillow-block": https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl + * "gear-rack": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/main.kcl */ test( 'Desktop: should create new file by default, optionally overwrite', @@ -93,8 +93,8 @@ test.describe('Testing in-app sample loading', () => { // Locators and constants const sampleOne = { - file: 'car-wheel' + FILE_EXT, - title: 'Car Wheel', + file: 'a-parametric-bearing-pillow-block' + FILE_EXT, + title: 'A Parametric Bearing Pillow Block', } const sampleTwo = { file: 'gear-rack' + FILE_EXT, diff --git a/public/kcl-samples-manifest-fallback.json b/public/kcl-samples-manifest-fallback.json index aa3b14e747..d633a3c348 100644 --- a/public/kcl-samples-manifest-fallback.json +++ b/public/kcl-samples-manifest-fallback.json @@ -1,166 +1,211 @@ [ { - "file": "80-20-rail.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl", + "multipleFiles": false, "title": "80/20 Rail", "description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position" }, { - "file": "a-parametric-bearing-pillow-block.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl", + "multipleFiles": false, "title": "A Parametric Bearing Pillow Block", "description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads." }, { - "file": "ball-bearing.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl", + "multipleFiles": false, "title": "Ball Bearing", "description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads." }, { - "file": "bracket.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "bracket/main.kcl", + "multipleFiles": false, "title": "Shelf Bracket", "description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided." }, { - "file": "brake-caliper.kcl", - "title": "Brake Caliper", - "description": "Brake calipers are used to squeeze the brake pads against the rotor, causing larger and larger amounts of friction depending on how hard the brakes are pressed." - }, - { - "file": "car-wheel.kcl", - "title": "Car Wheel", - "description": "A sports car wheel with a circular lug pattern and spokes." - }, - { - "file": "car-wheel-assembly.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl", + "multipleFiles": true, "title": "Car Wheel Assembly", "description": "A car wheel assembly with a rotor, tire, and lug nuts." }, { - "file": "dodecahedron.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl", + "multipleFiles": false, "title": "Hollow Dodecahedron", "description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards." }, { - "file": "enclosure.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl", + "multipleFiles": false, "title": "Enclosure", "description": "An enclosure body and sealing lid for storing items" }, { - "file": "flange-with-patterns.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl", + "multipleFiles": false, "title": "Flange", "description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others." }, { - "file": "flange-xy.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl", + "multipleFiles": false, "title": "Flange with XY coordinates", "description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others." }, { - "file": "focusrite-scarlett-mounting-bracket.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl", + "multipleFiles": false, "title": "A mounting bracket for the Focusrite Scarlett Solo audio interface", "description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material" }, { - "file": "food-service-spatula.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl", + "multipleFiles": false, "title": "Food Service Spatula", "description": "Use these spatulas for mixing, flipping, and scraping." }, { - "file": "french-press.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "french-press/main.kcl", + "multipleFiles": false, "title": "French Press", "description": "A french press immersion coffee maker" }, { - "file": "gear.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "gear/main.kcl", + "multipleFiles": false, "title": "Spur Gear", "description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear." }, { - "file": "gear-rack.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl", + "multipleFiles": false, "title": "100mm Gear Rack", "description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate." }, { - "file": "hex-nut.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl", + "multipleFiles": false, "title": "Hex nut", "description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware." }, { - "file": "i-beam.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl", + "multipleFiles": false, "title": "I-beam", "description": "A structural metal beam with an I shaped cross section. Often used in construction" }, { - "file": "kitt.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "kitt/main.kcl", + "multipleFiles": false, "title": "Kitt", "description": "The beloved KittyCAD mascot in a voxelized style." }, { - "file": "lego.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "lego/main.kcl", + "multipleFiles": false, "title": "Lego Brick", "description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code." }, { - "file": "lug-nut.kcl", - "title": "Lug Nut", - "description": "lug Nuts are essential components used to create secure connections, whether for electrical purposes, like terminating wires or grounding, or for mechanical purposes, such as providing mounting points or reinforcing structural joints." - }, - { - "file": "mounting-plate.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl", + "multipleFiles": false, "title": "Mounting Plate", "description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components." }, { - "file": "multi-axis-robot.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl", + "multipleFiles": true, "title": "Robot Arm", "description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes" }, { - "file": "pipe.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "pipe/main.kcl", + "multipleFiles": false, "title": "Pipe", "description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow." }, { - "file": "pipe-flange-assembly.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl", + "multipleFiles": false, "title": "Pipe and Flange Assembly", "description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint." }, { - "file": "pipe-with-bend.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl", + "multipleFiles": false, "title": "Pipe with bend", "description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow." }, { - "file": "poopy-shoe.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl", + "multipleFiles": false, "title": "Poopy Shoe", "description": "poop shute for bambu labs printer - optimized for printing." }, { - "file": "router-template-cross-bar.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl", + "multipleFiles": false, "title": "Router template for a cross bar", "description": "A guide for routing a notch into a cross bar." }, { - "file": "router-template-slate.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl", + "multipleFiles": false, "title": "Router template for a slate", "description": "A guide for routing a slate for a cross bar." }, { - "file": "sheet-metal-bracket.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl", + "multipleFiles": false, "title": "Sheet Metal Bracket", "description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly." }, { - "file": "socket-head-cap-screw.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl", + "multipleFiles": false, "title": "Socket Head Cap Screw", "description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key." }, { - "file": "tire.kcl", - "title": "Tire", - "description": "A tire is a critical component of a vehicle that provides the necessary traction and grip between the car and the road. It supports the vehicle's weight and absorbs shocks from road irregularities." + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl", + "multipleFiles": true, + "title": "Walkie Talkie", + "description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings." }, { - "file": "washer.kcl", + "file": "main.kcl", + "pathFromProjectDirectoryToFirstFile": "washer/main.kcl", + "multipleFiles": false, "title": "Washer", "description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time." }, diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index 3f08619e59..ca0c1a4d45 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -57,7 +57,9 @@ export const FileMachineProvider = ({ useEffect(() => { markOnce('code/didLoadFile') async function fetchKclSamples() { - setKclSamples(await getKclSamplesManifest()) + const manifest = await getKclSamplesManifest() + const filteredFiles = manifest.filter((file) => !file.multipleFiles) + setKclSamples(filteredFiles) } fetchKclSamples().catch(reportError) }, []) @@ -324,7 +326,7 @@ export const FileMachineProvider = ({ } }, kclSamples.map((sample) => ({ - value: sample.file, + value: sample.pathFromProjectDirectoryToFirstFile, name: sample.title, })) ).filter( diff --git a/src/lang/kclSamples.test.ts b/src/lang/kclSamples.test.ts index c7ea9f9c55..d08c4fcc85 100644 --- a/src/lang/kclSamples.test.ts +++ b/src/lang/kclSamples.test.ts @@ -1,79 +1,81 @@ import { assertParse, initPromise, programMemoryInit } from './wasm' import { enginelessExecutor } from '../lib/testHelpers' -// These unit tests makes web requests to a public github repository. + +import path from 'node:path' +import fs from 'node:fs/promises' +import child_process from 'node:child_process' + +// The purpose of these tests is to act as a first line of defense +// if something gets real screwy with our KCL ecosystem. +// THESE TESTS ONLY RUN UNDER A NODEJS ENVIRONMENT. They DO NOT +// test under our application. + +const DIR_KCL_SAMPLES = 'kcl-samples' +const URL_GIT_KCL_SAMPLES = 'https://github.com/KittyCAD/kcl-samples.git' interface KclSampleFile { file: string + pathFromProjectDirectoryToFirstFile: string title: string filename: string description: string } -beforeAll(async () => { - await initPromise -}) - -// Only used to actually fetch an older version of KCL code that will break in the parser. -/* eslint-disable @typescript-eslint/no-unused-vars */ -async function getBrokenSampleCodeForLocalTesting() { - const result = await fetch( - 'https://raw.githubusercontent.com/KittyCAD/kcl-samples/5ccd04a1773ebdbfd02684057917ce5dbe0eaab3/80-20-rail.kcl' - ) - const text = await result.text() - return text +try { + // @ts-expect-error + await fs.rm(DIR_KCL_SAMPLES, { recursive: true }) +} catch (e) { + console.log(e) } -async function getKclSampleCodeFromGithub(file: string): Promise { - const result = await fetch( - `https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/${file}/${file}.kcl` - ) - const text = await result.text() - return text -} +child_process.spawnSync('git', ['clone', URL_GIT_KCL_SAMPLES, DIR_KCL_SAMPLES]) -async function getFileNamesFromManifestJSON(): Promise { - const result = await fetch( - 'https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/manifest.json' - ) - const json = await result.json() - json.forEach((file: KclSampleFile) => { - const filenameWithoutExtension = file.file.split('.')[0] - file.filename = filenameWithoutExtension - }) - return json -} +// @ts-expect-error +let files = await fs.readdir(DIR_KCL_SAMPLES) +// @ts-expect-error +const manifestJsonStr = await fs.readFile( + path.resolve(DIR_KCL_SAMPLES, 'manifest.json'), + 'utf-8' +) +const manifest = JSON.parse(manifestJsonStr) -// Value to use across all tests! -let files: KclSampleFile[] = [] +process.chdir(DIR_KCL_SAMPLES) -describe('Test KCL Samples from public Github repository', () => { - describe('When parsing source code', () => { - // THIS RUNS ACROSS OTHER TESTS! - it('should fetch files', async () => { - files = await getFileNamesFromManifestJSON() - }) - // Run through all of the files in the manifest json. This will allow us to be automatically updated - // with the latest changes in github. We won't be hard coding the filenames - files.forEach((file: KclSampleFile) => { - it(`should parse ${file.filename} without errors`, async () => { - const code = await getKclSampleCodeFromGithub(file.filename) - assertParse(code) - }, 1000) - }) - }) +beforeAll(async () => { + await initPromise +}) - describe('when performing enginelessExecutor', () => { - it( - 'should run through all the files', - async () => { - for (let i = 0; i < files.length; i++) { - const file: KclSampleFile = files[i] - const code = await getKclSampleCodeFromGithub(file.filename) +afterAll(async () => { + try { + process.chdir('..') + await fs.rm(DIR_KCL_SAMPLES, { recursive: true }) + } catch (e) {} +}) + +afterEach(() => { + process.chdir('..') +}) + +// The tests have to be sequential because we need to change directories +// to support `import` working properly. +// @ts-expect-error +describe.sequential('Test KCL Samples from public Github repository', () => { + // @ts-expect-error + describe.sequential('when performing enginelessExecutor', () => { + manifest.forEach((file: KclSampleFile) => { + // @ts-expect-error + it.sequential( + `should execute ${file.title} (${file.file}) successfully`, + async () => { + const [dirProject, fileKcl] = + file.pathFromProjectDirectoryToFirstFile.split('/') + process.chdir(dirProject) + const code = await fs.readFile(fileKcl, 'utf-8') const ast = assertParse(code) await enginelessExecutor(ast, programMemoryInit()) - } - }, - files.length * 1000 - ) + }, + files.length * 1000 + ) + }) }) }) diff --git a/src/lang/std/fileSystemManager.ts b/src/lang/std/fileSystemManager.ts index 02b6be43c1..7918741d1c 100644 --- a/src/lang/std/fileSystemManager.ts +++ b/src/lang/std/fileSystemManager.ts @@ -1,5 +1,21 @@ import { isDesktop } from 'lib/isDesktop' +// Polyfill window.electron fs functions as needed when in a nodejs context +// (INTENDED FOR VITEST SHINANGANS.) +if (process.env.NODE_ENV === 'test' && process.env.VITEST) { + const fs = require('node:fs/promises') + const path = require('node:path') + Object.assign(window, { + electron: { + readFile: fs.readFile, + stat: fs.stat, + readdir: fs.readdir, + path, + process: {}, + }, + }) +} + /// FileSystemManager is a class that provides a way to read files from the local file system. /// It assumes that you are in a project since it is solely used by the std lib /// when executing code. @@ -19,13 +35,9 @@ class FileSystemManager { } async readFile(path: string): Promise { - // Using local file system only works from desktop. - if (!isDesktop()) { - return Promise.reject( - new Error( - 'This function can only be called from the desktop application' - ) - ) + // Using local file system only works from desktop and nodejs + if (!window?.electron?.readFile) { + return Promise.reject(new Error('No polyfill found for this function')) } return this.join(this.dir, path).then((filePath) => { @@ -35,12 +47,8 @@ class FileSystemManager { async exists(path: string): Promise { // Using local file system only works from desktop. - if (!isDesktop()) { - return Promise.reject( - new Error( - 'This function can only be called from the desktop application' - ) - ) + if (!window?.electron?.stat) { + return Promise.reject(new Error('No polyfill found for this function')) } return this.join(this.dir, path).then(async (file) => { @@ -57,12 +65,8 @@ class FileSystemManager { async getAllFiles(path: string): Promise { // Using local file system only works from desktop. - if (!isDesktop()) { - return Promise.reject( - new Error( - 'This function can only be called from the desktop application' - ) - ) + if (!window?.electron?.readdir) { + return Promise.reject(new Error('No polyfill found for this function')) } return this.join(this.dir, path).then((filepath) => { diff --git a/src/lib/getKclSamplesManifest.ts b/src/lib/getKclSamplesManifest.ts index 83d1435cbf..d5b43930ed 100644 --- a/src/lib/getKclSamplesManifest.ts +++ b/src/lib/getKclSamplesManifest.ts @@ -3,6 +3,8 @@ import { isDesktop } from './isDesktop' export type KclSamplesManifestItem = { file: string + pathFromProjectDirectoryToFirstFile: string + multipleFiles: boolean title: string description: string } diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index 1e0ed9dd5d..ba0fb4db04 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -49,20 +49,30 @@ export function kclCommands( if (!data?.sample) { return } + const pathParts = data.sample.split('/') + const projectPathPart = pathParts[0] + const primaryKclFile = pathParts[1] const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent( - data.sample.replace(FILE_EXT, '') - )}/${encodeURIComponent(data.sample)}` + projectPathPart + )}/${encodeURIComponent(primaryKclFile)}` const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent( - data.sample.replace(FILE_EXT, '') + projectPathPart )}/${PROJECT_SETTINGS_FILE_NAME}` - Promise.all([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)]) + Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)]) + .then((results) => { + const a = + 'value' in results[0] ? results[0].value : results[0].reason + const b = + 'value' in results[1] ? results[1].value : results[1].reason + return [a, b] + }) .then( async ([ codeResponse, settingsResponse, ]): Promise => { - if (!(codeResponse.ok && settingsResponse.ok)) { + if (!codeResponse.ok) { console.error( 'Failed to fetch sample code:', codeResponse.statusText @@ -70,20 +80,24 @@ export function kclCommands( return Promise.reject(new Error('Failed to fetch sample code')) } const code = await codeResponse.text() - const parsedProjectSettings = parseProjectSettings( - await settingsResponse.text() - ) + + // It's possible that a sample doesn't have a project.toml + // associated with it. let projectSettingsPayload: ReturnType< typeof projectConfigurationToSettingsPayload > = {} - if (!err(parsedProjectSettings)) { - projectSettingsPayload = projectConfigurationToSettingsPayload( - parsedProjectSettings + if (settingsResponse.ok) { + const parsedProjectSettings = parseProjectSettings( + await settingsResponse.text() ) + if (!err(parsedProjectSettings)) { + projectSettingsPayload = + projectConfigurationToSettingsPayload(parsedProjectSettings) + } } return { - sampleName: data.sample, + sampleName: data.sample.split('/')[0] + FILE_EXT, code, method: data.method, sampleUnits: