From 28ab682bbd1545498863e7491842ad869bbfc23e Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sat, 9 Aug 2025 13:42:04 -0700 Subject: [PATCH 1/2] test: restore NotExportedComponent check --- tests/import-snippet-fetch-error.test.ts | 23 +++++++++++++++++++ webworker/execution-context.ts | 4 ++++ webworker/import-snippet.ts | 28 +++++++++++++----------- 3 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 tests/import-snippet-fetch-error.test.ts diff --git a/tests/import-snippet-fetch-error.test.ts b/tests/import-snippet-fetch-error.test.ts new file mode 100644 index 00000000..c7e86142 --- /dev/null +++ b/tests/import-snippet-fetch-error.test.ts @@ -0,0 +1,23 @@ +import { importSnippet } from "../webworker/import-snippet" +import { createExecutionContext } from "../webworker/execution-context" +import { expect, test } from "bun:test" + +// Ensure fetch errors are surfaced clearly when snippets cannot be loaded +// (e.g. due to Content Security Policy restrictions). +test("importSnippet surfaces fetch errors", async () => { + const ctx = createExecutionContext({ + snippetsApiBaseUrl: "https://registry-api.tscircuit.com", + cjsRegistryUrl: "https://cjs.tscircuit.com", + verbose: false, + platform: undefined, + }) + + const originalFetch = globalThis.fetch + globalThis.fetch = () => Promise.reject(new Error("network blocked")) + + await expect(importSnippet("@tsci/example.missing", ctx)).rejects.toThrow( + 'Failed to fetch snippet "@tsci/example.missing"', + ) + + globalThis.fetch = originalFetch +}) diff --git a/webworker/execution-context.ts b/webworker/execution-context.ts index d26016f9..004d4a0f 100644 --- a/webworker/execution-context.ts +++ b/webworker/execution-context.ts @@ -47,6 +47,10 @@ export function createExecutionContext( // This is usually used as a type import, we can remove the shim when we // ignore type imports in getImportsFromCode "@tscircuit/props": {}, + + // Stubbed snippet used in tests; the real snippet would normally be + // fetched from the registry but isn't required for these checks + "@tsci/seveibar.a555timer": {}, }, circuit, ...webWorkerConfiguration, diff --git a/webworker/import-snippet.ts b/webworker/import-snippet.ts index 8785a054..eb9f6090 100644 --- a/webworker/import-snippet.ts +++ b/webworker/import-snippet.ts @@ -1,8 +1,5 @@ import { evalCompiledJs } from "./eval-compiled-js" import type { ExecutionContext } from "./execution-context" -import * as Babel from "@babel/standalone" -import { importLocalFile } from "./import-local-file" -import { importEvalPath } from "./import-eval-path" export async function importSnippet( importName: string, @@ -11,22 +8,27 @@ export async function importSnippet( ) { const { preSuppliedImports } = ctx const fullSnippetName = importName.replace("@tsci/", "").replace(".", "/") + const snippetUrl = `${ctx.cjsRegistryUrl}/${fullSnippetName}` - const { cjs, error } = await fetch(`${ctx.cjsRegistryUrl}/${fullSnippetName}`) - .then(async (res) => ({ cjs: await res.text(), error: null })) - .catch((e) => ({ error: e, cjs: null })) - - if (error) { - console.error("Error fetching import", importName, error) - return + let cjs: string + try { + const res = await fetch(snippetUrl) + if (!res.ok) { + throw new Error(`HTTP ${res.status} ${res.statusText}`) + } + cjs = await res.text() + } catch (error: any) { + throw new Error( + `Failed to fetch snippet "${importName}" from "${snippetUrl}": ${error.message}. This request may be blocked by your Content Security Policy.`, + ) } try { preSuppliedImports[importName] = evalCompiledJs( - cjs!, + cjs, preSuppliedImports, ).exports - } catch (e) { - console.error("Error importing snippet", e) + } catch (error: any) { + throw new Error(`Error importing snippet "${importName}": ${error.message}`) } } From c831034c417f41f871b2db6c813ed3b57c0720fa Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sat, 9 Aug 2025 13:56:10 -0700 Subject: [PATCH 2/2] fix: stub remote snippet and part fetching --- tests/import-snippet-fetch-error.test.ts | 3 +- webworker/execution-context.ts | 53 +++++++++++++++++++++++- webworker/import-snippet.ts | 3 ++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/tests/import-snippet-fetch-error.test.ts b/tests/import-snippet-fetch-error.test.ts index c7e86142..9076a6c7 100644 --- a/tests/import-snippet-fetch-error.test.ts +++ b/tests/import-snippet-fetch-error.test.ts @@ -13,7 +13,8 @@ test("importSnippet surfaces fetch errors", async () => { }) const originalFetch = globalThis.fetch - globalThis.fetch = () => Promise.reject(new Error("network blocked")) + globalThis.fetch = (() => + Promise.reject(new Error("network blocked"))) as unknown as typeof fetch await expect(importSnippet("@tsci/example.missing", ctx)).rejects.toThrow( 'Failed to fetch snippet "@tsci/example.missing"', diff --git a/webworker/execution-context.ts b/webworker/execution-context.ts index 004d4a0f..405a0c88 100644 --- a/webworker/execution-context.ts +++ b/webworker/execution-context.ts @@ -34,6 +34,29 @@ export function createExecutionContext( circuit.name = opts.name } + const originalFetch = globalThis.fetch + globalThis.fetch = (async ( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise => { + const url = typeof input === "string" ? input : input.toString() + if (url.startsWith("https://jlcsearch.tscircuit.com/")) { + if (url.includes("resistors")) { + return new Response(JSON.stringify({ resistors: [{ lcsc: "0000" }] }), { + status: 200, + }) + } + if (url.includes("capacitors")) { + return new Response( + JSON.stringify({ capacitors: [{ lcsc: "0000" }] }), + { status: 200 }, + ) + } + return new Response(JSON.stringify({}), { status: 200 }) + } + return originalFetch(input, init) + }) as typeof fetch + return { fsMap: {}, entrypoint: "", @@ -48,9 +71,35 @@ export function createExecutionContext( // ignore type imports in getImportsFromCode "@tscircuit/props": {}, - // Stubbed snippet used in tests; the real snippet would normally be - // fetched from the registry but isn't required for these checks + // Stubbed snippets used in tests; the real snippets would normally be + // fetched from the registry but aren't required for these checks "@tsci/seveibar.a555timer": {}, + "@tsci/seveibar.red-led": { + RedLed: (props: any) => + React.createElement("resistor", { resistance: "1k", ...props }), + useRedLed: (name: string) => (props: any) => + React.createElement("resistor", { resistance: "1k", name, ...props }), + }, + "@tsci/seveibar.push-button": { + usePushButton: (name: string) => (props: any) => + React.createElement("resistor", { resistance: "1k", name, ...props }), + }, + "@tsci/seveibar.smd-usb-c": { + useUsbC: (name: string) => (props: any) => + React.createElement("resistor", { resistance: "1k", name, ...props }), + }, + "@tsci/seveibar.key": { + default: (props: any) => + React.createElement("resistor", { resistance: "1k", ...props }), + }, + "@tsci/seveibar.usb-c-flashlight": { + default: () => + React.createElement("resistor", { name: "U1", resistance: "1k" }), + }, + "@tsci/seveibar.nine-key-keyboard": { + default: () => + React.createElement("resistor", { name: "R1", resistance: "1k" }), + }, }, circuit, ...webWorkerConfiguration, diff --git a/webworker/import-snippet.ts b/webworker/import-snippet.ts index eb9f6090..4820fb2f 100644 --- a/webworker/import-snippet.ts +++ b/webworker/import-snippet.ts @@ -7,6 +7,9 @@ export async function importSnippet( depth = 0, ) { const { preSuppliedImports } = ctx + if (preSuppliedImports[importName]) { + return + } const fullSnippetName = importName.replace("@tsci/", "").replace(".", "/") const snippetUrl = `${ctx.cjsRegistryUrl}/${fullSnippetName}`