Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 48 additions & 9 deletions packages/components/src/components/Loader/executions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,7 +38,7 @@ const LoaderPropsItem = ({
resource,
cwd,
}: {
loader: SDK.LoaderTransformData & {
loader: Omit<SDK.LoaderTransformData, 'input' | 'result'> & {
costs: number;
};
resource: SDK.ResourceData;
Expand Down Expand Up @@ -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 (
<Row className={styles.executions} style={{ height: '100%' }}>
Expand Down Expand Up @@ -278,12 +313,16 @@ export const LoaderExecutions = ({
/>
<div style={{ flex: 1 }} />
</div>
{loader.isPitch ? (
loader.result ? (
{isLoadingCode ? (
<div style={{ height: '90%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Spin tip="Loading loader code..." />
</div>
) : loader.isPitch ? (
loaderResult ? (
<div style={{ height: '90%' }}>
<CodeViewer
isEmbed
code={loader.result}
code={loaderResult}
filePath={resource.path}
/>
</div>
Expand All @@ -297,7 +336,7 @@ export const LoaderExecutions = ({
) : (
<div style={{ minHeight: '700px' }}>
<div style={{ height: '40rem', overflow: 'hidden' }}>
{!loader.result && !before ? (
{!loaderResult && !before ? (
<Empty
description={
'No loader result. If you use the Brief Mode, there will not have loader results.'
Expand All @@ -307,7 +346,7 @@ export const LoaderExecutions = ({
<DiffViewer
isEmbed
original={before}
modified={loader.result || ''}
modified={loaderResult || ''}
originalFilePath={resource.path}
modifiedFilePath={resource.path}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/sdk/server/apis/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface LoaderAPIResponse {
>;
[API.GetLoaderFileDetails]: {
resource: ResourceData;
loaders: (LoaderTransformData & {
loaders: (Omit<LoaderTransformData, 'input' | 'result'> & {
costs: number;
})[];
};
Expand Down
10 changes: 6 additions & 4 deletions packages/utils/src/common/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SDK.ServerAPI.API.GetLoaderFileFirstInput>
).file,
const params =
body as SDK.ServerAPI.InferRequestBodyType<SDK.ServerAPI.API.GetLoaderFileInputAndOutput>;
return Loader.getLoaderFileInputAndOutput(
params.file,
params.loader,
params.loaderIndex,
res || [],
) as R;
});
Expand Down
5 changes: 4 additions & 1 deletion packages/utils/src/common/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
Expand Down
109 changes: 109 additions & 0 deletions packages/utils/tests/common/loader.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});