## Summary - Add `@typescript/native-preview` (tsgo) for dramatically faster type checking on frontend projects - Configure tsgo as default for frontend projects (twenty-front, twenty-ui, twenty-shared, etc.) - Keep tsc for twenty-server (faster for NestJS decorator-heavy code) - Fix type imports for tsgo compatibility (DOMPurify, AxiosInstance) - Remove deprecated `baseUrl` from tsconfigs where safe ## Performance Results | Project | tsgo | tsc -b | Speedup | |---------|------|--------|---------| | **twenty-front** | 1.4s | 60.7s | **43x faster** | | **twenty-server** | 2m42s | 1m10s | tsc is faster (decorators) | tsgo excels at modern React/JSX codebases but struggles with decorator-heavy NestJS backends, so we use the optimal checker for each. ## Usage ```bash # Default (tsgo for frontend, tsc for backend) nx typecheck twenty-front nx typecheck twenty-server # Force tsc fallback if needed nx typecheck twenty-front --configuration=tsc # Force tsgo on backend (slower, not recommended) nx typecheck twenty-server --configuration=tsgo ``` ## Test plan - [x] `nx typecheck twenty-front` passes with tsgo - [x] `nx typecheck twenty-server` passes with tsc - [x] `nx run-many -t typecheck --exclude=fireflies` passes - [ ] CI tests pass
94 lines
2.5 KiB
TypeScript
94 lines
2.5 KiB
TypeScript
import { type AxiosError, type AxiosInstance } from 'axios';
|
|
|
|
const cropRegex = /([w|h])([0-9]+)/;
|
|
|
|
export type ShortCropSize = `${'w' | 'h'}${number}` | 'original';
|
|
|
|
export interface CropSize {
|
|
type: 'width' | 'height';
|
|
value: number;
|
|
}
|
|
|
|
export const getCropSize = (value: ShortCropSize): CropSize | null => {
|
|
const match = value.match(cropRegex);
|
|
|
|
if (value === 'original' || match === null) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
type: match[1] === 'w' ? 'width' : 'height',
|
|
value: +match[2],
|
|
};
|
|
};
|
|
|
|
export const getImageBufferFromUrl = async (
|
|
url: string,
|
|
axiosInstance: AxiosInstance,
|
|
): Promise<Buffer> => {
|
|
if (!url || typeof url !== 'string' || url.trim().length === 0) {
|
|
throw new Error('Invalid URL provided: URL must be a non-empty string');
|
|
}
|
|
|
|
try {
|
|
const response = await axiosInstance.get(url, {
|
|
responseType: 'arraybuffer',
|
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
maxRedirects: 5,
|
|
timeout: 30000,
|
|
});
|
|
|
|
if (!response.data) {
|
|
throw new Error('Received empty response from image URL');
|
|
}
|
|
|
|
const bufferLength = Buffer.isBuffer(response.data)
|
|
? response.data.length
|
|
: response.data.byteLength;
|
|
|
|
if (bufferLength === 0) {
|
|
throw new Error('Received empty response from image URL');
|
|
}
|
|
|
|
const contentType = response.headers['content-type'];
|
|
|
|
if (contentType && !contentType.startsWith('image/')) {
|
|
throw new Error(
|
|
`Invalid content type: expected image/*, got ${contentType}`,
|
|
);
|
|
}
|
|
|
|
return Buffer.from(response.data, 'binary');
|
|
} catch (error) {
|
|
const axiosError = error as AxiosError;
|
|
const axiosResponse = axiosError.response;
|
|
|
|
if (axiosResponse) {
|
|
throw new Error(
|
|
`Failed to fetch image: HTTP ${axiosResponse.status} from ${url}`,
|
|
);
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
if (
|
|
axiosError.code === 'ECONNABORTED' ||
|
|
error.message.includes('timeout')
|
|
) {
|
|
throw new Error(
|
|
`Request timeout while fetching image from URL: ${url}`,
|
|
);
|
|
}
|
|
if (
|
|
axiosError.code === 'ENOTFOUND' ||
|
|
axiosError.code === 'ECONNREFUSED' ||
|
|
error.message.includes('ENOTFOUND') ||
|
|
error.message.includes('ECONNREFUSED')
|
|
) {
|
|
throw new Error(`Failed to connect to image URL: ${url}`);
|
|
}
|
|
throw new Error(`Failed to fetch image from URL: ${error.message}`);
|
|
}
|
|
throw new Error(`Failed to fetch image from URL: ${url}`);
|
|
}
|
|
};
|