diff --git a/packages/components/src/components/Loader/executions.tsx b/packages/components/src/components/Loader/executions.tsx index 86825e24f..d7d8a271d 100644 --- a/packages/components/src/components/Loader/executions.tsx +++ b/packages/components/src/components/Loader/executions.tsx @@ -12,14 +12,15 @@ import { Timeline, Tooltip, Typography, + Spin, } from 'antd'; import dayjs from 'dayjs'; -import { PropsWithChildren, useCallback, useState } from 'react'; +import { PropsWithChildren, useCallback, useState, useEffect } from 'react'; import InputIcon from 'src/common/svg/loader/input.svg'; import OutputIcon from 'src/common/svg/loader/output.svg'; import StepIcon from 'src/common/svg/loader/step.svg'; import { Size } from '../../constants'; -import { beautifyPath, formatCosts, useTheme } from '../../utils'; +import { beautifyPath, formatCosts, useTheme, useDataLoader } from '../../utils'; import { CodeViewer, DiffViewer } from '../base'; import { Card } from '../Card'; import { CodeOpener } from '../Opener'; @@ -37,7 +38,7 @@ const LoaderPropsItem = ({ resource, cwd, }: { - loader: SDK.LoaderTransformData & { + loader: Omit & { costs: number; }; resource: SDK.ResourceData; @@ -118,13 +119,47 @@ export const LoaderExecutions = ({ const { theme } = useTheme(); const isLight = theme === 'light'; const loader = loaders[currentIndex]; - const before = loader.input || ''; const leftSpan = 5; const hasError = loader.errors && loader.errors.length; const [activeKey, setActiveKey] = useState('loaderDetails'); const onChange = useCallback((key: string) => { setActiveKey(key); }, []); + + // State for lazy-loaded code + const [loaderCode, setLoaderCode] = useState<{ + input: string; + output: string; + } | null>(null); + const [isLoadingCode, setIsLoadingCode] = useState(false); + const { loader: dataLoader } = useDataLoader(); + + // Fetch code when user switches to loader details tab or changes loader + useEffect(() => { + if (activeKey === 'loaderDetails' && dataLoader) { + setIsLoadingCode(true); + setLoaderCode(null); + + dataLoader + .loadAPI(SDK.ServerAPI.API.GetLoaderFileInputAndOutput, { + file: resource.path, + loader: loader.loader, + loaderIndex: loader.loaderIndex, + }) + .then((res) => { + setLoaderCode(res); + setIsLoadingCode(false); + }) + .catch((err) => { + console.error('Failed to load loader code:', err); + setLoaderCode({ input: '', output: '' }); + setIsLoadingCode(false); + }); + } + }, [currentIndex, activeKey, resource.path, dataLoader]); + + const before = loaderCode?.input || ''; + const loaderResult = loaderCode?.output || ''; return ( @@ -278,12 +313,16 @@ export const LoaderExecutions = ({ />
- {loader.isPitch ? ( - loader.result ? ( + {isLoadingCode ? ( +
+ +
+ ) : loader.isPitch ? ( + loaderResult ? (
@@ -297,7 +336,7 @@ export const LoaderExecutions = ({ ) : (
- {!loader.result && !before ? ( + {!loaderResult && !before ? ( diff --git a/packages/types/src/sdk/server/apis/loader.ts b/packages/types/src/sdk/server/apis/loader.ts index b3b377c0c..276fabd12 100644 --- a/packages/types/src/sdk/server/apis/loader.ts +++ b/packages/types/src/sdk/server/apis/loader.ts @@ -29,7 +29,7 @@ export interface LoaderAPIResponse { >; [API.GetLoaderFileDetails]: { resource: ResourceData; - loaders: (LoaderTransformData & { + loaders: (Omit & { costs: number; })[]; }; diff --git a/packages/utils/src/common/data/index.ts b/packages/utils/src/common/data/index.ts index 4de24c4d3..b23f53190 100644 --- a/packages/utils/src/common/data/index.ts +++ b/packages/utils/src/common/data/index.ts @@ -127,10 +127,12 @@ export class APIDataLoader { case SDK.ServerAPI.API.GetLoaderFileInputAndOutput: return this.loader.loadData('loader').then((res) => { - return Loader.getLoaderFileFirstInput( - ( - body as SDK.ServerAPI.InferRequestBodyType - ).file, + const params = + body as SDK.ServerAPI.InferRequestBodyType; + return Loader.getLoaderFileInputAndOutput( + params.file, + params.loader, + params.loaderIndex, res || [], ) as R; }); diff --git a/packages/utils/src/common/loader.ts b/packages/utils/src/common/loader.ts index 0c75e6074..f22078cf5 100644 --- a/packages/utils/src/common/loader.ts +++ b/packages/utils/src/common/loader.ts @@ -210,8 +210,11 @@ export function getLoaderFileDetails( return { ...data, loaders: data.loaders.map((el) => { + // Strip large input/result fields to reduce data volume + // These can be fetched on-demand via GetLoaderFileInputAndOutput API + const { input, result, ...loaderWithoutCode } = el; return { - ...el, + ...loaderWithoutCode, loader: getLoadrName(el.loader), costs: getLoaderCosts(el, list), }; diff --git a/packages/utils/tests/common/loader.test.ts b/packages/utils/tests/common/loader.test.ts new file mode 100644 index 000000000..40710506f --- /dev/null +++ b/packages/utils/tests/common/loader.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect } from '@rstest/core'; +import { SDK } from '@rsdoctor/types'; +import { getLoaderFileDetails, getLoaderFileInputAndOutput } from '../../src/common/loader'; + +describe('test src/common/loader.ts', () => { + const mockLoaderData: SDK.LoaderData = [ + { + resource: { + path: '/test/file.js', + queryRaw: '', + query: {}, + ext: 'js', + }, + loaders: [ + { + loader: 'babel-loader', + loaderIndex: 0, + path: '/node_modules/babel-loader/lib/index.js', + input: 'const foo = 1;', + result: 'var foo = 1;', + startAt: 1000, + endAt: 1100, + options: {}, + isPitch: false, + sync: true, + errors: [], + pid: 1, + ppid: 0, + }, + { + loader: 'ts-loader', + loaderIndex: 1, + path: '/node_modules/ts-loader/index.js', + input: 'const bar: number = 2;', + result: 'const bar = 2;', + startAt: 900, + endAt: 950, + options: {}, + isPitch: false, + sync: true, + errors: [], + pid: 1, + ppid: 0, + }, + ], + }, + ]; + + it('getLoaderFileDetails should strip input and result fields', () => { + const result = getLoaderFileDetails('/test/file.js', mockLoaderData); + + expect(result).toBeDefined(); + expect(result.resource.path).toBe('/test/file.js'); + expect(result.loaders).toHaveLength(2); + + // Verify that input and result are NOT included + result.loaders.forEach((loader) => { + expect(loader).not.toHaveProperty('input'); + expect(loader).not.toHaveProperty('result'); + // But other properties should be present + expect(loader.loader).toBeDefined(); + expect(loader.loaderIndex).toBeDefined(); + expect(loader.costs).toBeDefined(); + }); + }); + + it('getLoaderFileInputAndOutput should return input and output', () => { + const result = getLoaderFileInputAndOutput( + '/test/file.js', + 'babel-loader', + 0, + mockLoaderData, + ); + + expect(result).toBeDefined(); + expect(result.input).toBe('const foo = 1;'); + expect(result.output).toBe('var foo = 1;'); + }); + + it('getLoaderFileInputAndOutput should return empty strings for non-existent loader', () => { + const result = getLoaderFileInputAndOutput( + '/test/file.js', + 'non-existent-loader', + 0, + mockLoaderData, + ); + + expect(result.input).toBe(''); + expect(result.output).toBe(''); + }); + + it('getLoaderFileInputAndOutput should return empty strings for non-existent file', () => { + const result = getLoaderFileInputAndOutput( + '/non-existent/file.js', + 'babel-loader', + 0, + mockLoaderData, + ); + + expect(result.input).toBe(''); + expect(result.output).toBe(''); + }); + + it('getLoaderFileDetails should throw error for non-existent file', () => { + expect(() => { + getLoaderFileDetails('/non-existent/file.js', mockLoaderData); + }).toThrow('"/non-existent/file.js" not match any loader data'); + }); +});