diff --git a/node-src/index.ts b/node-src/index.ts index 507c09ca1..393c78108 100644 --- a/node-src/index.ts +++ b/node-src/index.ts @@ -27,10 +27,11 @@ import LoggingRenderer from './lib/loggingRenderer'; import NonTTYRenderer from './lib/nonTTYRenderer'; import parseArguments from './lib/parseArguments'; import { exitCodes, setExitCode } from './lib/setExitCode'; +import { uploadShare } from './lib/share'; import { uploadMetadataFiles } from './lib/uploadMetadataFiles'; import { rewriteErrorMessage } from './lib/utilities'; import { writeChromaticDiagnostics } from './lib/writeChromaticDiagnostics'; -import getTasks from './tasks'; +import getTasks, { runShareBuild } from './tasks'; import { Context, Flags, Options } from './types'; import { endActivity } from './ui/components/activity'; import buildCanceled from './ui/messages/errors/buildCanceled'; @@ -81,7 +82,24 @@ export type InitialContext = Omit< 'options' >; -const isContext = (ctx: InitialContext): ctx is Context => 'options' in ctx; +async function setupContext(ctx: InitialContext, configFile?: string): Promise { + ctx.http = new HTTPClient(ctx); + ctx.client = new GraphQLClient(ctx, `${ctx.env.CHROMATIC_INDEX_URL}/graphql`, { + headers: { + 'x-chromatic-session-id': ctx.sessionId, + 'x-chromatic-cli-version': ctx.pkg.version, + 'apollographql-client-name': 'chromatic-cli', + 'apollographql-client-version': ctx.pkg.version, + }, + retries: 3, + }); + ctx.configuration = await getConfiguration(configFile); + const options = getOptions(ctx); + (ctx as Context).options = options; + ctx.log.setLogFile(options.logFile); + + return ctx as Context; +} /** * Entry point for the CLI, GitHub Action, and Node API @@ -159,52 +177,36 @@ export async function run({ /** * Entry point for testing only (typically invoked via `run` above) * - * @param ctx The context set when executing the CLI. + * @param initialContext The context set when executing the CLI. * * @returns A promise that resolves when all steps are completed. */ -export async function runAll(ctx: InitialContext) { - ctx.log.info(''); - ctx.log.info(intro(ctx)); - ctx.log.info(''); +export async function runAll(initialContext: InitialContext) { + initialContext.log.info(''); + initialContext.log.info(intro(initialContext)); + initialContext.log.info(''); const onError = (err: Error | Error[]) => { - ctx.log.info(''); - ctx.log.error(fatalError(ctx, [err].flat())); - ctx.extraOptions?.experimental_onTaskError?.(ctx, { - formattedError: fatalError(ctx, [err].flat()), + initialContext.log.info(''); + initialContext.log.error(fatalError(initialContext, [err].flat())); + initialContext.extraOptions?.experimental_onTaskError?.(initialContext, { + formattedError: fatalError(initialContext, [err].flat()), originalError: err, }); - setExitCode(ctx, exitCodes.INVALID_OPTIONS, true); + setExitCode(initialContext, exitCodes.INVALID_OPTIONS, true); }; + let ctx: Context; try { - ctx.http = new HTTPClient(ctx); - ctx.client = new GraphQLClient(ctx, `${ctx.env.CHROMATIC_INDEX_URL}/graphql`, { - headers: { - 'x-chromatic-session-id': ctx.sessionId, - 'x-chromatic-cli-version': ctx.pkg.version, - 'apollographql-client-name': 'chromatic-cli', - 'apollographql-client-version': ctx.pkg.version, - }, - retries: 3, - }); - ctx.configuration = await getConfiguration( - ctx.extraOptions?.configFile || ctx.flags.configFile + ctx = await setupContext( + initialContext, + initialContext.extraOptions?.configFile || initialContext.flags.configFile ); - const options = getOptions(ctx); - (ctx as Context).options = options; - ctx.log.setLogFile(options.logFile); - setExitCode(ctx, exitCodes.OK); } catch (err) { return onError(err); } - if (!isContext(ctx)) { - return onError(new Error('Invalid context')); - } - // Run these in parallel; neither should ever reject await Promise.all([runBuild(ctx), checkForUpdates(ctx)]).catch((error) => { Sentry.captureException(error); @@ -297,6 +299,126 @@ async function runBuild(ctx: Context) { } } +export interface ShareOptions { + userToken: string; + onUrl?: (url: string) => void; + onProgress?: (progress: number, total: number) => void; + onError?: (error: Error) => void; + abortSignal?: AbortSignal; +} + +export interface ShareOutput { + shareUrl: string; +} + +/** + * Share a Storybook without creating a full Chromatic build. + * Reserves a share URL, runs the upload pipeline, and resolves when the upload is complete. + * + * @param shareOptions Options for the share operation. + * @param shareOptions.userToken The user token for authentication. + * @param shareOptions.onUrl Callback fired as soon as the share URL is reserved. + * @param shareOptions.onProgress Callback reporting upload progress as (bytesUploaded, totalBytes). + * @param shareOptions.onError Callback for errors. When provided, share() resolves instead of rejecting. + * @param shareOptions.abortSignal An AbortSignal to cancel the share operation. + * + * @returns An object with the share URL. + */ +export async function share(shareOptions: ShareOptions): Promise { + const { onUrl, onError } = shareOptions; + + let ctx: Context; + try { + ctx = await setupShareContext(shareOptions); + } catch (error) { + if (onError) { + onError(error); + return { shareUrl: '' }; + } + throw error; + } + + let shareUrl = ''; + try { + const { shareUrl: shareUrlFromIndex, target } = await uploadShare(ctx); + shareUrl = shareUrlFromIndex; + ctx.share = { shareUrl, target }; + ctx.git = { branch: '', commit: '', committedAt: 0, fromCI: false }; + + onUrl?.(shareUrl); + + await runShareTasks(ctx); + } catch (error) { + // If a callback was provided, use that then resolve + if (onError) { + onError(error); + return { shareUrl }; + } + throw error; + } + + return { shareUrl }; +} + +async function setupShareContext(shareOptions: ShareOptions): Promise { + const { userToken, onProgress, abortSignal } = shareOptions; + + const extraOptions: Partial = { + userToken, + ...(abortSignal && { experimental_abortSignal: abortSignal }), + ...(onProgress && { + experimental_onTaskProgress: (_ctx: Context, status: { progress: number; total: number }) => { + onProgress(status.progress, status.total); + }, + }), + }; + const config = { + ...parseArguments([]), + extraOptions, + }; + + const log = createLogger(config.flags, extraOptions); + + const packageInfo = await readPackageUp({ cwd: process.cwd(), normalize: false }); + if (!packageInfo) { + throw new Error('No package.json found'); + } + + const { path: packagePath, packageJson } = packageInfo; + const initialContext: InitialContext = { + ...config, + flags: { + ...config.flags, + interactive: false, + }, + packagePath, + packageJson, + env: getEnvironment(), + log, + sessionId: uuid(), + }; + + return setupContext(initialContext); +} + +async function runShareTasks(ctx: Context): Promise { + const listrOptions: any = { + log: ctx.log, + renderer: NonTTYRenderer, + }; + + try { + await new Listr( + runShareBuild.map((task) => task(ctx)), + listrOptions + ).run(ctx); + ctx.log.debug('Tasks completed'); + } finally { + endActivity(ctx); + ctx.log.flush(); + } +} + export interface GitInfo { slug: string; branch: string; diff --git a/node-src/lib/getOptions.ts b/node-src/lib/getOptions.ts index 8d004dfc9..b53669cd6 100644 --- a/node-src/lib/getOptions.ts +++ b/node-src/lib/getOptions.ts @@ -211,10 +211,7 @@ export default function getOptions(ctx: InitialContext): Options { potentialOptions.diagnosticsFile = potentialOptions.diagnosticsFile ?? DEFAULT_DIAGNOSTICS_FILE; } - if ( - !potentialOptions.projectToken && - !(potentialOptions.projectId && potentialOptions.userToken) - ) { + if (!potentialOptions.projectToken && !potentialOptions.userToken) { throw new Error(missingProjectToken()); } diff --git a/node-src/lib/share/index.ts b/node-src/lib/share/index.ts new file mode 100644 index 000000000..ba004e6ab --- /dev/null +++ b/node-src/lib/share/index.ts @@ -0,0 +1,48 @@ +import { Context } from '../../types'; + +const UploadShareMutation = ` + mutation UploadShareMutation { + uploadShare { + shareUrl + target { + formAction + formFields + keyPrefix + } + } + } +`; + +interface UploadShareResult { + uploadShare: { + shareUrl: string; + target: { + formAction: string; + formFields: Record; + keyPrefix: string; + }; + }; +} + +/** + * Reserve a share upload slot and obtain S3 presigned POST credentials. + * + * Request: mutation UploadShareMutation + * Headers: Authorization: Bearer + * Response: { shareUrl, target: { formAction, formFields, keyPrefix } } + * + * @param ctx The task context, used for the GraphQL client and user token. + * + * @returns The share URL and S3 presigned POST target for uploading files. + */ +export async function uploadShare(ctx: Context) { + const { uploadShare: result } = await ctx.client.runQuery( + UploadShareMutation, + {}, + { + endpoint: `${ctx.env.CHROMATIC_INDEX_URL}/graphql`, + headers: { Authorization: `Bearer ${ctx.options.userToken}` }, + } + ); + return result; +} diff --git a/node-src/share.test.ts b/node-src/share.test.ts new file mode 100644 index 000000000..60e06dfbf --- /dev/null +++ b/node-src/share.test.ts @@ -0,0 +1,205 @@ +import { Readable } from 'node:stream'; + +import { execa as execaDefault } from 'execa'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { share } from '.'; +import { uploadShare } from './lib/share'; +import { uploadFiles } from './lib/uploadFiles'; + +vi.mock('execa', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execa: vi.fn(() => Promise.resolve()), + }; +}); + +vi.mock('node-fetch', () => ({ + default: vi.fn(async (_url, { body } = {} as any) => ({ + ok: true, + json: async () => { + let query = ''; + try { + const data = JSON.parse(body); + query = data.query; + } catch { + // Do nothing + } + + if (query?.match('CreateAppTokenMutation')) { + return { data: { appToken: 'app-token' } }; + } + if (query?.match('CreateCLITokenMutation')) { + return { data: { cliToken: 'cli-token' } }; + } + + throw new Error(query ? `Unknown query: ${query}` : `Unmocked request`); + }, + })), +})); + +vi.mock('./lib/share', () => ({ + uploadShare: vi.fn(async () => ({ + shareUrl: 'https://share.chromatic.com/test-share-id', + target: { + formAction: 'https://s3.amazonaws.com', + formFields: {}, + keyPrefix: 'shares/user-123-upload-456', + }, + })), +})); + +vi.mock('./lib/uploadFiles'); + +vi.mock('./lib/getFileHashes', () => ({ + getFileHashes: (files: string[]) => + Promise.resolve(Object.fromEntries(files.map((f) => [f, 'hash']))), +})); + +vi.mock('./lib/getPackageManager', () => ({ + getPackageManagerName: () => Promise.resolve('pnpm'), + getPackageManagerRunCommand: (args: string[]) => Promise.resolve(`pnpm run ${args.join(' ')}`), +})); + +vi.mock('fs', async (importOriginal) => { + const originalModule = (await importOriginal()) as any; + return { + ...originalModule, + pathExists: async () => true, + mkdirSync: vi.fn(), + readFileSync: originalModule.readFileSync, + writeFileSync: vi.fn(), + createReadStream: vi.fn(() => Readable.from([])), + createWriteStream: originalModule.createWriteStream, + readdirSync: vi.fn(() => ['iframe.html', 'index.html']), + stat: originalModule.stat, + statSync: vi.fn((path: string) => { + const fsStatSync = originalModule.statSync; + if (path.endsWith('package.json')) return fsStatSync(path); + return { isDirectory: () => false, size: 42 }; + }), + existsSync: vi.fn(() => true), + access: vi.fn((_path: string, callback: (err: null) => void) => + Promise.resolve(callback(null)) + ), + }; +}); + +const execa = vi.mocked(execaDefault); +const upload = vi.mocked(uploadFiles); +const mockUploadShare = vi.mocked(uploadShare); + +let processEnvironment: NodeJS.ProcessEnv; + +beforeEach(() => { + processEnvironment = process.env; + process.env = { + DISABLE_LOGGING: 'true', + CHROMATIC_PROJECT_TOKEN: undefined, + }; + execa.mockReset(); + execa.mockResolvedValue({ all: '1.2.3' } as any); +}); + +afterEach(() => { + process.env = processEnvironment; + vi.clearAllMocks(); +}); + +describe('share()', () => { + it('resolves with shareUrl after upload completes', async () => { + const result = await share({ userToken: 'user-token' }); + + expect(result.shareUrl).toBe('https://share.chromatic.com/test-share-id'); + expect(mockUploadShare).toHaveBeenCalled(); + }); + + it('calls onUrl as soon as the URL is reserved', async () => { + const onUrl = vi.fn(); + + // Make uploadFiles pend so we can verify onUrl fires before upload completes + let resolveUpload: (() => void) | undefined; + upload.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveUpload = resolve; + }) + ); + + const resultPromise = share({ userToken: 'user-token', onUrl }); + + // Wait for the pipeline to reach the upload step + await vi.waitFor(() => expect(upload).toHaveBeenCalled()); + + // onUrl should have been called before upload completes + expect(onUrl).toHaveBeenCalledWith('https://share.chromatic.com/test-share-id'); + + resolveUpload?.(); + await resultPromise; + }); + + it('calls onProgress with bytes uploaded and total', async () => { + const onProgress = vi.fn(); + + upload.mockImplementationOnce(async (_ctx, _targets, progressCallback) => { + progressCallback?.(42); + }); + + await share({ userToken: 'user-token', onProgress }); + + expect(onProgress).toHaveBeenCalledWith(42, expect.any(Number)); + }); + + it('passes abortSignal through to the upload context', async () => { + const controller = new AbortController(); + + upload.mockImplementationOnce(async (ctx) => { + expect(ctx.options.experimental_abortSignal).toBe(controller.signal); + }); + + await share({ userToken: 'user-token', abortSignal: controller.signal }); + }); + + it('rejects when uploadShare fails and no onError provided', async () => { + mockUploadShare.mockRejectedValueOnce(new Error('Reserve failed')); + + await expect(share({ userToken: 'user-token' })).rejects.toThrow('Reserve failed'); + }); + + it('calls onError and resolves when uploadShare fails', async () => { + const onError = vi.fn(); + mockUploadShare.mockRejectedValueOnce(new Error('Reserve failed')); + + const result = await share({ userToken: 'user-token', onError }); + + expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'Reserve failed' })); + expect(result.shareUrl).toBe(''); + }); + + it('calls onError and resolves when upload fails', async () => { + const onError = vi.fn(); + upload.mockRejectedValueOnce(new Error('S3 upload failed')); + + const result = await share({ userToken: 'user-token', onError }); + + expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'S3 upload failed' })); + expect(result.shareUrl).toBe('https://share.chromatic.com/test-share-id'); + }); + + it('rejects when upload fails and no onError provided', async () => { + upload.mockRejectedValueOnce(new Error('S3 upload failed')); + + await expect(share({ userToken: 'user-token' })).rejects.toThrow(); + }); + + it('calls uploadShare with ctx containing the user token', async () => { + await share({ userToken: 'my-user-token' }); + + expect(mockUploadShare).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ userToken: 'my-user-token' }), + }) + ); + }); +}); diff --git a/node-src/tasks/index.ts b/node-src/tasks/index.ts index 3e76d8eee..4f0a630e8 100644 --- a/node-src/tasks/index.ts +++ b/node-src/tasks/index.ts @@ -12,8 +12,11 @@ import restoreWorkspace from './restoreWorkspace'; import snapshot from './snapshot'; import storybookInfo from './storybookInfo'; import upload from './upload'; +import uploadShare from './uploadShare'; import verify from './verify'; +export const runShareBuild = [build, prepare, uploadShare]; + export const runUploadBuild = [ auth, gitInfo, diff --git a/node-src/tasks/uploadShare.test.ts b/node-src/tasks/uploadShare.test.ts new file mode 100644 index 000000000..81eb4678f --- /dev/null +++ b/node-src/tasks/uploadShare.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { uploadShareFiles } from './uploadShare'; + +vi.mock('../lib/uploadFiles'); + +import { uploadFiles } from '../lib/uploadFiles'; + +const uploadFilesMock = vi.mocked(uploadFiles); + +const environment = { CHROMATIC_RETRIES: 2, CHROMATIC_OUTPUT_INTERVAL: 0 }; + +const shareTarget = { + formAction: 'https://s3.amazonaws.com/presigned', + formFields: { bucket: 'chromatic-builds', 'X-Amz-Signature': 'sig' }, + keyPrefix: 'shares/user-123-upload-456', +}; + +describe('uploadShareFiles', () => { + it('uploads index.html in a separate phase after all other files', async () => { + uploadFilesMock.mockResolvedValue(undefined); + + const ctx = { + share: { shareUrl: 'https://chromatic.com/share/abc', target: shareTarget }, + env: environment, + options: {}, + sourceDir: '/static/', + fileInfo: { + paths: ['iframe.html', 'main.js', 'index.html'], + lengths: [ + { knownAs: 'iframe.html', contentLength: 42 }, + { knownAs: 'main.js', contentLength: 100 }, + { knownAs: 'index.html', contentLength: 42 }, + ], + total: 184, + }, + } as any; + + await uploadShareFiles(ctx, {} as any); + + expect(uploadFilesMock).toHaveBeenCalledTimes(2); + + // First call: non-index files + const firstCallTargets = uploadFilesMock.mock.calls[0][1]; + expect(firstCallTargets.map((t) => t.filePath)).toEqual( + expect.arrayContaining(['iframe.html', 'main.js']) + ); + expect(firstCallTargets.map((t) => t.filePath)).not.toContain('index.html'); + + // Second call: index.html only + const secondCallTargets = uploadFilesMock.mock.calls[1][1]; + expect(secondCallTargets).toHaveLength(1); + expect(secondCallTargets[0].filePath).toBe('index.html'); + }); + + it('uses the shared formAction and sets per-file key in formFields', async () => { + uploadFilesMock.mockResolvedValue(undefined); + + const ctx = { + share: { shareUrl: 'https://chromatic.com/share/abc', target: shareTarget }, + env: environment, + options: {}, + sourceDir: '/static/', + fileInfo: { + paths: ['main.js', 'index.html'], + lengths: [ + { knownAs: 'main.js', contentLength: 100 }, + { knownAs: 'index.html', contentLength: 42 }, + ], + total: 142, + }, + } as any; + + await uploadShareFiles(ctx, {} as any); + + const allTargets = uploadFilesMock.mock.calls.flatMap(([, targets]) => targets); + for (const t of allTargets) { + expect(t.formAction).toBe(shareTarget.formAction); + expect(t.formFields.key).toBe(`${shareTarget.keyPrefix}/${t.filePath}`); + } + }); + + it('rejects if a file upload fails', async () => { + uploadFilesMock.mockRejectedValue(new Error('Upload failed')); + + const ctx = { + share: { shareUrl: 'https://chromatic.com/share/abc', target: shareTarget }, + env: environment, + options: {}, + sourceDir: '/static/', + fileInfo: { + paths: ['iframe.html', 'index.html'], + lengths: [ + { knownAs: 'iframe.html', contentLength: 42 }, + { knownAs: 'index.html', contentLength: 42 }, + ], + total: 84, + }, + } as any; + + await expect(uploadShareFiles(ctx, {} as any)).rejects.toThrow('Upload failed'); + }); +}); diff --git a/node-src/tasks/uploadShare.ts b/node-src/tasks/uploadShare.ts new file mode 100644 index 000000000..203986d57 --- /dev/null +++ b/node-src/tasks/uploadShare.ts @@ -0,0 +1,84 @@ +import path from 'path'; + +import { createTask, transitionTo } from '../lib/tasks'; +import { uploadFiles } from '../lib/uploadFiles'; +import { Context, Task } from '../types'; +import { initial, starting, success } from '../ui/tasks/uploadShare'; + +export const uploadShareFiles = async (ctx: Context, _task: Task) => { + const { paths = [], lengths = [] } = ctx.fileInfo ?? {}; + + if (!ctx.share) { + throw new Error('Missing share context'); + } + + if (!paths.includes('index.html')) { + throw new Error('Missing index.html — cannot publish without an entry point'); + } + + const { formAction, formFields, keyPrefix } = ctx.share.target; + + const lengthsByPath = new Map( + lengths.map(({ knownAs, contentLength }) => [knownAs, contentLength]) + ); + + const toTarget = (filePath: string) => ({ + filePath, + formAction, + formFields: { ...formFields, key: `${keyPrefix}/${filePath}` }, + contentType: '', + fileKey: '', + targetPath: filePath, + localPath: path.join(ctx.sourceDir, filePath), + contentLength: lengthsByPath.get(filePath) ?? 0, + }); + + type Target = ReturnType; + const nonIndexTargets: Target[] = []; + let indexTarget: Target | undefined; + let totalBytes = 0; + + for (const path of paths) { + const target = toTarget(path); + totalBytes += target.contentLength; + + if (path === 'index.html') { + indexTarget = target; + } else { + nonIndexTargets.push(target); + } + } + + // Since we're doing multi-phase uploads, we need to track progress across all phases and report + // it as a single progress value + let uploadedBytes = 0; + let lastProgress = 0; + const onProgress = (progress: number) => { + uploadedBytes += progress - lastProgress; + lastProgress = progress; + ctx.options.experimental_onTaskProgress?.( + { ...ctx }, + { progress: uploadedBytes, total: totalBytes, unit: 'bytes' } + ); + }; + + // Upload all non-index.html files in parallel, then index.html last — signals CDN readiness + await uploadFiles(ctx, nonIndexTargets, onProgress); + lastProgress = 0; // reset progress for next phase + if (indexTarget) await uploadFiles(ctx, [indexTarget], onProgress); +}; + +/** + * Sets up the Listr task for uploading the Storybook to a share URL. + * + * @param ctx The context set when executing the CLI. + * + * @returns A Listr task. + */ +export default function main(ctx: Context) { + return createTask({ + name: 'share', + title: initial(ctx).title, + steps: [transitionTo(starting), uploadShareFiles, transitionTo(success, true)], + }); +} diff --git a/node-src/types.ts b/node-src/types.ts index 09a9dd8cc..d10e67e4f 100644 --- a/node-src/types.ts +++ b/node-src/types.ts @@ -156,6 +156,7 @@ export type TaskName = | 'build' | 'prepare' | 'upload' + | 'share' | 'verify' | 'snapshot' | 'report' @@ -358,6 +359,14 @@ export interface Context { }[]; total: number; }; + share?: { + shareUrl: string; + target: { + formAction: string; + formFields: Record; + keyPrefix: string; + }; + }; sentinelUrls?: string[]; uploadedBytes?: number; uploadedFiles?: number; diff --git a/node-src/ui/tasks/uploadShare.ts b/node-src/ui/tasks/uploadShare.ts new file mode 100644 index 000000000..f3bb9aff7 --- /dev/null +++ b/node-src/ui/tasks/uploadShare.ts @@ -0,0 +1,17 @@ +import { Context } from '../../types'; + +export const initial = (_ctx: Context) => ({ + status: 'initial', + title: 'Upload your Storybook for sharing', +}); + +export const starting = (_ctx: Context) => ({ + status: 'pending', + title: 'Uploading your Storybook for sharing', + output: 'Starting upload', +}); + +export const success = (_ctx: Context) => ({ + status: 'success', + title: 'Storybook uploaded for sharing', +}); diff --git a/package.json b/package.json index 73666739e..15f9ab7b5 100644 --- a/package.json +++ b/package.json @@ -208,7 +208,6 @@ "util-deprecate": "^1.0.2", "uuid": "^8.3.2", "vite": "^8.0.3", - "vite-tsconfig-paths": "^5.1.4", "vitest": "^4.1.2", "why-is-node-running": "^2.1.2", "ws": "^8.18.2", diff --git a/vitest.config.mts b/vitest.config.mts index 6be46b51f..2de548001 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,4 +1,3 @@ -import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig, Plugin } from 'vitest/config'; export default defineConfig({ @@ -9,5 +8,7 @@ export default defineConfig({ exclude: ['**/*.stories.{t,j}s', '**/lib/testLogger.ts', '**/__mocks__/**'], }, }, - plugins: [tsconfigPaths() as Plugin], + resolve: { + tsconfigPaths: true, + }, }); diff --git a/yarn.lock b/yarn.lock index 7a38544ae..7d60cc483 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8035,7 +8035,6 @@ __metadata: util-deprecate: "npm:^1.0.2" uuid: "npm:^8.3.2" vite: "npm:^8.0.3" - vite-tsconfig-paths: "npm:^5.1.4" vitest: "npm:^4.1.2" why-is-node-running: "npm:^2.1.2" ws: "npm:^8.18.2" @@ -11819,13 +11818,6 @@ __metadata: languageName: node linkType: hard -"globrex@npm:^0.1.2": - version: 0.1.2 - resolution: "globrex@npm:0.1.2" - checksum: 10c0/a54c029520cf58bda1d8884f72bd49b4cd74e977883268d931fd83bcbd1a9eb96d57c7dbd4ad80148fb9247467ebfb9b215630b2ed7563b2a8de02e1ff7f89d1 - languageName: node - linkType: hard - "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -20498,20 +20490,6 @@ __metadata: languageName: node linkType: hard -"tsconfck@npm:^3.0.3": - version: 3.1.5 - resolution: "tsconfck@npm:3.1.5" - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - bin: - tsconfck: bin/tsconfck.js - checksum: 10c0/9b62cd85d5702aa23ea50ea578d7124f3d59cc4518fcc7eacc04f4f9c9c481f720738ff8351bd4472247c0723a17dfd01af95a5b60ad623cdb8727fbe4881847 - languageName: node - linkType: hard - "tsconfig-paths@npm:^3.15.0": version: 3.15.0 resolution: "tsconfig-paths@npm:3.15.0" @@ -21563,22 +21541,6 @@ __metadata: languageName: node linkType: hard -"vite-tsconfig-paths@npm:^5.1.4": - version: 5.1.4 - resolution: "vite-tsconfig-paths@npm:5.1.4" - dependencies: - debug: "npm:^4.1.1" - globrex: "npm:^0.1.2" - tsconfck: "npm:^3.0.3" - peerDependencies: - vite: "*" - peerDependenciesMeta: - vite: - optional: true - checksum: 10c0/6228f23155ea25d92b1e1702284cf8dc52ad3c683c5ca691edd5a4c82d2913e7326d00708cef1cbfde9bb226261df0e0a12e03ef1d43b6a92d8f02b483ef37e3 - languageName: node - linkType: hard - "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.3": version: 8.0.3 resolution: "vite@npm:8.0.3"