From 611a1ef5dc1d056877623ce1864718dc274a6849 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 1 Apr 2026 22:06:42 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=96=EF=B8=8F=20fix:=20Sandpack=20Exter?= =?UTF-8?q?nalResources=20for=20Static=20HTML=20Artifact=20Previews=20(#12?= =?UTF-8?q?509)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: omit externalResources for static Sandpack previews The Tailwind CDN URL lacks a file extension, causing Sandpack's static template to throw a runtime injection error. Static previews already load Tailwind via a script tag in the shared index.html, so externalResources is unnecessary for them. Closes #12507 * refactor: extract buildSandpackOptions and add tests - Surgically omit only externalResources for static templates instead of discarding all sharedOptions, preventing future regression if new template-agnostic options are added. - Extract options logic into a pure, testable helper function. - Add unit tests covering all template/config combinations. * chore: fix import order and pin test assertions * fix: use URL fragment hint instead of omitting externalResources Sandpack's static template regex detects resource type from the URL's last file extension. The versioned CDN path (/3.4.17) matched ".17" instead of ".js", throwing "Unable to determine file type". Rather than omitting externalResources for static templates (which would remove the only Tailwind injection path for HTML artifacts that don't embed their own script tag), append a #tailwind.js fragment hint so the regex matches ".js". Fragments are not sent to the server, so the CDN response is unchanged. --- .../components/Artifacts/ArtifactPreview.tsx | 15 +++----- client/src/utils/__tests__/artifacts.test.ts | 38 +++++++++++++++++++ client/src/utils/artifacts.ts | 22 ++++++++++- 3 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 client/src/utils/__tests__/artifacts.test.ts diff --git a/client/src/components/Artifacts/ArtifactPreview.tsx b/client/src/components/Artifacts/ArtifactPreview.tsx index c125889c88..8257f76887 100644 --- a/client/src/components/Artifacts/ArtifactPreview.tsx +++ b/client/src/components/Artifacts/ArtifactPreview.tsx @@ -6,7 +6,7 @@ import type { } from '@codesandbox/sandpack-react/unstyled'; import type { TStartupConfig } from 'librechat-data-provider'; import type { ArtifactFiles } from '~/common'; -import { sharedFiles, sharedOptions } from '~/utils/artifacts'; +import { sharedFiles, buildSandpackOptions } from '~/utils/artifacts'; export const ArtifactPreview = memo(function ({ files, @@ -39,15 +39,10 @@ export const ArtifactPreview = memo(function ({ }; }, [currentCode, files, fileKey]); - const options: typeof sharedOptions = useMemo(() => { - if (!startupConfig) { - return sharedOptions; - } - return { - ...sharedOptions, - bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL, - }; - }, [startupConfig, template]); + const options: SandpackProviderProps['options'] = useMemo( + () => buildSandpackOptions(template, startupConfig), + [startupConfig, template], + ); if (Object.keys(artifactFiles).length === 0) { return null; diff --git a/client/src/utils/__tests__/artifacts.test.ts b/client/src/utils/__tests__/artifacts.test.ts new file mode 100644 index 0000000000..bf5c1919c7 --- /dev/null +++ b/client/src/utils/__tests__/artifacts.test.ts @@ -0,0 +1,38 @@ +import { buildSandpackOptions } from '../artifacts'; + +const TAILWIND_CDN = 'https://cdn.tailwindcss.com/3.4.17#tailwind.js'; + +describe('buildSandpackOptions', () => { + it('includes externalResources with .js fragment hint for static template', () => { + const options = buildSandpackOptions('static'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('includes externalResources for react-ts template', () => { + const options = buildSandpackOptions('react-ts'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('uses staticBundlerURL when template is static and config is provided', () => { + const config = { staticBundlerURL: 'https://static.example.com' } as Parameters< + typeof buildSandpackOptions + >[1]; + const options = buildSandpackOptions('static', config); + expect(options?.bundlerURL).toBe('https://static.example.com'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('uses bundlerURL when template is react-ts and config is provided', () => { + const config = { bundlerURL: 'https://bundler.example.com' } as Parameters< + typeof buildSandpackOptions + >[1]; + const options = buildSandpackOptions('react-ts', config); + expect(options?.bundlerURL).toBe('https://bundler.example.com'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('returns base options without bundlerURL when no config is provided', () => { + const options = buildSandpackOptions('react-ts'); + expect(options?.bundlerURL).toBeUndefined(); + }); +}); diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index 793bc484bc..2ca02422a2 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -4,6 +4,7 @@ import type { SandpackProviderProps, SandpackPredefinedTemplate, } from '@codesandbox/sandpack-react'; +import type { TStartupConfig } from 'librechat-data-provider'; const artifactFilename = { 'application/vnd.react': 'App.tsx', @@ -138,10 +139,29 @@ export function getProps(type: string): Partial { }; } +/** Fragment hint lets Sandpack's static-template regex detect `.js` from the URL; + * without it, the versioned CDN path (`/3.4.17`) has no recognised extension and + * `injectExternalResources` throws "Unable to determine file type". */ +const TAILWIND_CDN = 'https://cdn.tailwindcss.com/3.4.17#tailwind.js'; + export const sharedOptions: SandpackProviderProps['options'] = { - externalResources: ['https://cdn.tailwindcss.com/3.4.17'], + externalResources: [TAILWIND_CDN], }; +export function buildSandpackOptions( + template: SandpackProviderProps['template'], + startupConfig?: TStartupConfig, +): SandpackProviderProps['options'] { + if (!startupConfig) { + return sharedOptions; + } + + return { + ...sharedOptions, + bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL, + }; +} + export const sharedFiles = { '/lib/utils.ts': shadcnComponents.utils, '/components/ui/accordion.tsx': shadcnComponents.accordian,