From 41306ae7d11fa998770e7da25af67ed13f789320 Mon Sep 17 00:00:00 2001 From: Aymeric PINEAU Date: Mon, 24 Mar 2025 13:14:28 +0100 Subject: [PATCH] feat(config): add TypeScript error logging during external file loading --- .../@intlayer/config/src/loadExternalFile.ts | 40 +++++++++- .../config/src/logTypeScriptErrors.ts | 80 +++++++++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 packages/@intlayer/config/src/logTypeScriptErrors.ts diff --git a/packages/@intlayer/config/src/loadExternalFile.ts b/packages/@intlayer/config/src/loadExternalFile.ts index 5ccd7fa47..fd10cfcec 100644 --- a/packages/@intlayer/config/src/loadExternalFile.ts +++ b/packages/@intlayer/config/src/loadExternalFile.ts @@ -1,10 +1,16 @@ +import { + buildSync, + type BuildFailure, + type BuildOptions, + type BuildResult, +} from 'esbuild'; +import { dirname } from 'path'; import { runInNewContext } from 'vm'; -import { type BuildOptions, buildSync, type BuildResult } from 'esbuild'; +import { LoadEnvFileOptions } from './envVariables/loadEnvFile'; import { getSandBoxContext } from './getSandboxContext'; -import { ESMxCJSRequire } from './utils/ESMxCJSRequire'; import { logger } from './logger'; -import { LoadEnvFileOptions } from './envVariables/loadEnvFile'; -import { dirname } from 'path'; +import { logTypeScriptErrors } from './logTypeScriptErrors'; +import { ESMxCJSRequire } from './utils/ESMxCJSRequire'; const getTransformationOptions = (filePath: string): BuildOptions => ({ loader: { @@ -55,11 +61,26 @@ export const loadExternalFile = ( // Rest is JS, MJS or TS + if (fileExtension === 'ts' || fileExtension === 'tsx') { + logTypeScriptErrors(filePath); + } + const moduleResult: BuildResult = buildSync({ entryPoints: [filePath], ...getTransformationOptions(filePath), + logLevel: 'silent', // prevent auto printing }); + // Log Warnings from esbuild + if (moduleResult.warnings?.length > 0) { + for (const warn of moduleResult.warnings) { + logger( + `[ESBUILD WARNING] ${warn.text} at ${warn.location?.file ?? filePath}:${warn.location?.line ?? '?'}:${warn.location?.column ?? '?'}`, + { level: 'warn' } + ); + } + } + const moduleResultString = moduleResult.outputFiles?.[0].text; if (!moduleResultString) { @@ -104,6 +125,17 @@ export const loadExternalFile = ( return fileContent; } catch (error) { + // Specific handling for esbuild errors (i.e. build-time TypeScript errors) + if ((error as BuildFailure).errors) { + const esbuildErrors = (error as BuildFailure).errors; + for (const err of esbuildErrors) { + logger( + `${err.text} at ${err.location?.file}:${err.location?.line}:${err.location?.column}`, + { level: 'error' } + ); + } + } + logger( `Error: ${error} ${JSON.stringify((error as Error).stack, null, 2)}`, { diff --git a/packages/@intlayer/config/src/logTypeScriptErrors.ts b/packages/@intlayer/config/src/logTypeScriptErrors.ts new file mode 100644 index 000000000..6954a6dfd --- /dev/null +++ b/packages/@intlayer/config/src/logTypeScriptErrors.ts @@ -0,0 +1,80 @@ +import { appLogger } from '@intlayer/config'; +import { + createProgram, + findConfigFile, + flattenDiagnosticMessageText, + getPreEmitDiagnostics, + parseConfigFileTextToJson, + parseJsonConfigFileContent, + sys, +} from 'typescript'; + +export const logTypeScriptErrors = (filePath: string) => { + const configFileName = findConfigFile( + process.cwd(), + sys.fileExists, + 'tsconfig.json' + ); + if (!configFileName) { + appLogger("Could not find a 'tsconfig.json' file.", { level: 'error' }); + return; + } + + const configFileText = sys.readFile(configFileName); + if (!configFileText) { + appLogger(`Could not read the 'tsconfig.json' file at ${configFileName}.`, { + level: 'error', + }); + return; + } + + const configJson = parseConfigFileTextToJson(configFileName, configFileText); + if (configJson.error) { + appLogger( + `Error parsing 'tsconfig.json': ${flattenDiagnosticMessageText(configJson.error.messageText, '\n')}`, + { level: 'error' } + ); + return; + } + + const parsedConfig = parseJsonConfigFileContent( + configJson.config, + sys, + process.cwd() + ); + if (parsedConfig.errors.length > 0) { + parsedConfig.errors.forEach((error) => { + appLogger( + `Error in tsconfig.json: ${flattenDiagnosticMessageText(error.messageText, '\n')}`, + { level: 'error' } + ); + }); + return; + } + + // Remove incremental options to avoid the error + const { incremental, tsBuildInfoFile, ...compilerOptions } = + parsedConfig.options; + + // Create a program with the modified compiler options + const program = createProgram([filePath], { + ...compilerOptions, + noEmit: true, + }); + const diagnostics = getPreEmitDiagnostics(program); + + diagnostics.forEach((diagnostic) => { + const message = flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + if (diagnostic.file && diagnostic.start !== undefined) { + const { line, character } = diagnostic.file.getLineAndCharacterOfPosition( + diagnostic.start + ); + appLogger( + `TS Error in ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`, + { level: 'error' } + ); + } else { + appLogger(`TS Error: ${message}`, { level: 'error' }); + } + }); +};