diff --git a/src/compileTypes/compileTypes.ts b/src/compileTypes/compileTypes.ts index c3812e9..bef2f03 100644 --- a/src/compileTypes/compileTypes.ts +++ b/src/compileTypes/compileTypes.ts @@ -4,7 +4,7 @@ import ts from 'typescript'; import { getAllFilePaths, getLogger } from '../helpers'; -import type { FederationConfig } from '../models'; +import type { CommonLogger } from '../models/CommonLogger'; import { getTSConfigCompilerOptions, reportCompileDiagnostic } from './helpers'; export type CompileTypesParams = { @@ -12,7 +12,6 @@ export type CompileTypesParams = { exposedModules: string[]; outFile: string; dirGlobalTypes: string; - federationConfig: FederationConfig; }; export type CompileTypesResult = { @@ -20,16 +19,12 @@ export type CompileTypesResult = { typeDefinitions: string; }; -export function compileTypes({ - tsconfigPath, - exposedModules, - outFile, - dirGlobalTypes, -}: CompileTypesParams): CompileTypesResult { - const logger = getLogger(); - +export function compileTypes( + { tsconfigPath, exposedModules, outFile, dirGlobalTypes }: CompileTypesParams, + logger: CommonLogger = getLogger(), +): CompileTypesResult { const exposedFileNames = Object.values(exposedModules); - const { moduleResolution, ...compilerOptions } = getTSConfigCompilerOptions(tsconfigPath); + const { moduleResolution, ...compilerOptions } = getTSConfigCompilerOptions(tsconfigPath, logger); Object.assign(compilerOptions, { declaration: true, @@ -59,11 +54,12 @@ export function compileTypes({ ...getAllFilePaths(`./${dirGlobalTypes}`).filter(filePath => filePath.endsWith('.d.ts')), ); } - logger.log('Including a set of root files in compilation', exposedFileNames); + logger.log('[compileTypes]: Including a set of root files in compilation'); + logger.log(JSON.stringify(exposedFileNames, null, 2)); const program = ts.createProgram(exposedFileNames, compilerOptions, host); const { diagnostics, emitSkipped } = program.emit(); - diagnostics.forEach(reportCompileDiagnostic); + diagnostics.forEach(item => reportCompileDiagnostic(item, logger)); if (emitSkipped) { logger.log('[compileTypes]: TypeScript program emit skipped'); diff --git a/src/compileTypes/compileTypesAsync.ts b/src/compileTypes/compileTypesAsync.ts index 4c5671e..ff305d8 100644 --- a/src/compileTypes/compileTypesAsync.ts +++ b/src/compileTypes/compileTypesAsync.ts @@ -1,13 +1,18 @@ import path from 'node:path'; -import { Worker, parentPort } from 'node:worker_threads'; +import { Worker } from 'node:worker_threads'; import { getLogger } from '../helpers'; -import type { CompileTypesParams } from './compileTypes'; -import type { CompileTypesWorkerMessage } from './compileTypesWorker'; +import type { + CompileTypesWorkerMessage, + CompileTypesWorkerResultMessage, +} from './compileTypesWorker'; let worker: Worker | null = null; -export function compileTypesAsync(params: CompileTypesParams, loggerHint = ''): Promise { +export function compileTypesAsync( + params: CompileTypesWorkerMessage, + loggerHint = '', +): Promise { const logger = getLogger(); return new Promise((resolve, reject) => { @@ -16,11 +21,14 @@ export function compileTypesAsync(params: CompileTypesParams, loggerHint = ''): worker.terminate(); } - const workerPath = path.join(__dirname, 'compileWorker.js'); + const workerPath = path.join(__dirname, 'compileTypesWorker.js'); worker = new Worker(workerPath); - worker.on('message', (result: CompileTypesWorkerMessage) => { + worker.on('message', (result: CompileTypesWorkerResultMessage) => { switch (result.status) { + case 'log': + logger[result.level]('[Worker]:', result.message); + return; case 'success': resolve(); break; @@ -52,6 +60,6 @@ export function compileTypesAsync(params: CompileTypesParams, loggerHint = ''): worker = null; }); - parentPort?.postMessage({ ...params, logger }); + worker.postMessage(params); }); } diff --git a/src/compileTypes/compileTypesWorker.ts b/src/compileTypes/compileTypesWorker.ts index a53065f..a428816 100644 --- a/src/compileTypes/compileTypesWorker.ts +++ b/src/compileTypes/compileTypesWorker.ts @@ -1,48 +1,64 @@ import { parentPort } from 'node:worker_threads'; import type { Compilation } from 'webpack'; +import type { FederationConfig } from '../models'; import { type CompileTypesParams, compileTypes } from './compileTypes'; import { rewritePathsWithExposedFederatedModules } from './rewritePathsWithExposedFederatedModules'; +import { sendLog, workerLogger } from './workerLogger'; -type CompileTypesWorkerMessageError = { +export type LogLevel = keyof Pick< + Compilation['logger'], + 'log' | 'info' | 'warn' | 'error' | 'debug' +>; + +type CompileTypesWorkerResultMessageError = { status: 'error'; error: Error; }; -export type CompileTypesWorkerMessage = +export type CompileTypesWorkerMessage = CompileTypesParams & { + federationConfig: FederationConfig; +}; + +export type CompileTypesWorkerResultMessage = | { status: 'success' } | { status: 'failure' } - | CompileTypesWorkerMessageError; - -parentPort?.on('message', (message: CompileTypesParams & { logger: Compilation['logger'] }) => { - const { logger, ...params } = message; + | CompileTypesWorkerResultMessageError + | { status: 'log'; level: LogLevel; message: string }; +parentPort?.on('message', ({ federationConfig, ...params }: CompileTypesWorkerMessage) => { try { - const startTime = performance.now(); - const { isSuccess, typeDefinitions } = compileTypes(params); + let startTime = performance.now(); + const { isSuccess, typeDefinitions } = compileTypes(params, workerLogger); if (isSuccess) { - const endTime = performance.now(); - const timeTakenInSeconds = (endTime - startTime) / 1000; - logger.log(`Types compilation completed in ${timeTakenInSeconds.toFixed(2)} seconds`); + let endTime = performance.now(); + let timeTakenInSeconds = (endTime - startTime) / 1000; + sendLog('log', `Types compilation completed in ${timeTakenInSeconds.toFixed(2)} seconds`); - logger.log( + sendLog( + 'log', `Replacing paths with names of exposed federate modules in typings file: ${params.outFile}`, ); + startTime = performance.now(); rewritePathsWithExposedFederatedModules( - params.federationConfig, + federationConfig, params.outFile, typeDefinitions, + workerLogger, ); + endTime = performance.now(); + timeTakenInSeconds = (endTime - startTime) / 1000; + sendLog('log', `Typings file rewritten in ${timeTakenInSeconds.toFixed(2)} seconds`); - parentPort?.postMessage({ status: 'success' } satisfies CompileTypesWorkerMessage); + parentPort?.postMessage({ status: 'success' } satisfies CompileTypesWorkerResultMessage); } else { - parentPort?.postMessage({ status: 'failure' } satisfies CompileTypesWorkerMessage); + parentPort?.postMessage({ status: 'failure' } satisfies CompileTypesWorkerResultMessage); } } catch (error) { parentPort?.postMessage({ status: 'error', error: error as Error, - } satisfies CompileTypesWorkerMessageError); + } satisfies CompileTypesWorkerResultMessageError); } }); diff --git a/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts b/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts index 7ef96fe..2e28c5c 100644 --- a/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts +++ b/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts @@ -36,10 +36,17 @@ describe('includeTypesFromNodeModules', () => { ].join('\n'); expect(result).toBe([initialTypings, moduleADeclaration, moduleBDeclaration].join('\n')); - expect(mockLogger.log).toHaveBeenCalledWith('Including typings for npm packages:', [ - ['ModuleA', 'libraryA'], - ['ModuleB', 'libraryB'], - ]); + expect(mockLogger.log).toHaveBeenCalledWith('Including typings for npm packages:'); + expect(mockLogger.log).toHaveBeenCalledWith( + JSON.stringify( + [ + ['ModuleA', 'libraryA'], + ['ModuleB', 'libraryB'], + ], + null, + 2, + ), + ); }); test('does not modify typings when there are no NPM package paths', () => { diff --git a/src/compileTypes/helpers/__tests__/substituteAliasedModules.test.ts b/src/compileTypes/helpers/__tests__/substituteAliasedModules.test.ts index 913aa5c..ea1c0f8 100644 --- a/src/compileTypes/helpers/__tests__/substituteAliasedModules.test.ts +++ b/src/compileTypes/helpers/__tests__/substituteAliasedModules.test.ts @@ -14,7 +14,7 @@ describe('substituteAliasedModules', () => { const federatedModuleName = 'myCommon'; const logger = getLogger(); - test('substitutes import path when #not-for-import version exists', () => { + test("substitutes import function's path argument when #not-for-import declaration exists", () => { const modulePath = 'libs/currency'; const typings = ` Some import("${modulePath}") more content @@ -30,12 +30,13 @@ describe('substituteAliasedModules', () => { expect(logger.log).toHaveBeenCalledWith(`Substituting import path: ${modulePath}`); }); - test('does not modify typings when a #not-for-import version does not exist', () => { + test('does not modify typings when a #not-for-import declaration does not exist', () => { const originalTypings = 'Some content import("another/module") more content'; const result = substituteAliasedModules(federatedModuleName, originalTypings); expect(result).toBe(originalTypings); - expect(logger.log).not.toHaveBeenCalled(); + expect(logger.log).toHaveBeenCalledWith('Unique import paths in myCommon:'); + expect(logger.log).toHaveBeenCalledWith(JSON.stringify(['another/module'], null, 2)); }); }); diff --git a/src/compileTypes/helpers/getTSConfigCompilerOptions.ts b/src/compileTypes/helpers/getTSConfigCompilerOptions.ts index 2b35248..b5a9d0b 100644 --- a/src/compileTypes/helpers/getTSConfigCompilerOptions.ts +++ b/src/compileTypes/helpers/getTSConfigCompilerOptions.ts @@ -3,10 +3,12 @@ import path from 'node:path'; import type ts from 'typescript'; import { getLogger } from '../../helpers'; +import type { CommonLogger } from '../../models'; -export function getTSConfigCompilerOptions(tsconfigFileNameOrPath: string): ts.CompilerOptions { - const logger = getLogger(); - +export function getTSConfigCompilerOptions( + tsconfigFileNameOrPath: string, + logger: CommonLogger = getLogger(), +): ts.CompilerOptions { const tsconfigPath = path.resolve(tsconfigFileNameOrPath); if (!tsconfigPath) { logger.error('ERROR: Could not find a valid tsconfig.json'); diff --git a/src/compileTypes/helpers/includeTypesFromNodeModules.ts b/src/compileTypes/helpers/includeTypesFromNodeModules.ts index be7aec8..5f2c913 100644 --- a/src/compileTypes/helpers/includeTypesFromNodeModules.ts +++ b/src/compileTypes/helpers/includeTypesFromNodeModules.ts @@ -1,11 +1,11 @@ import { getLogger } from '../../helpers'; -import type { FederationConfig } from '../../models'; +import type { CommonLogger, FederationConfig } from '../../models'; export function includeTypesFromNodeModules( federationConfig: FederationConfig, typings: string, + logger: CommonLogger = getLogger(), ): string { - const logger = getLogger(); let typingsWithNpmPackages = typings; const exposedNpmPackages = Object.entries(federationConfig.exposes) @@ -26,7 +26,8 @@ export function includeTypesFromNodeModules( ].join('\n'); if (exposedNpmPackages.length) { - logger.log('Including typings for npm packages:', exposedNpmPackages); + logger.log('Including typings for npm packages:'); + logger.log(JSON.stringify(exposedNpmPackages, null, 2)); } try { @@ -34,8 +35,8 @@ export function includeTypesFromNodeModules( typingsWithNpmPackages += `\n${createNpmModule(exposedModuleKey, packageName)}`; }); } catch (err) { - logger.warn('Typings was not included for npm package:', (err as Dict)?.url); - logger.log(err); + logger.warn(`Typings was not included for npm package: ${(err as Dict)?.url}`); + logger.log(JSON.stringify(err, null, 2)); } return typingsWithNpmPackages; diff --git a/src/compileTypes/helpers/reportCompileDiagnostic.ts b/src/compileTypes/helpers/reportCompileDiagnostic.ts index dc19a62..b678088 100644 --- a/src/compileTypes/helpers/reportCompileDiagnostic.ts +++ b/src/compileTypes/helpers/reportCompileDiagnostic.ts @@ -1,15 +1,15 @@ import ts from 'typescript'; import { getLogger } from '../../helpers'; +import type { CommonLogger } from '../../models'; -export function reportCompileDiagnostic(diagnostic: ts.Diagnostic): void { - const logger = getLogger(); +export function reportCompileDiagnostic( + diagnostic: ts.Diagnostic, + logger: CommonLogger = getLogger(), +): void { const { line } = diagnostic.file!.getLineAndCharacterOfPosition(diagnostic.start!); logger.log( - 'TS Error', - diagnostic.code, - ':', - ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine), + `TS Error ${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine)}`, ); - logger.log(' at', `${diagnostic.file!.fileName}:${line + 1}`, '\n'); + logger.log(` at ${diagnostic.file!.fileName}:${line + 1}`); } diff --git a/src/compileTypes/helpers/substituteAliasedModules.ts b/src/compileTypes/helpers/substituteAliasedModules.ts index 32d116e..fa6d26c 100644 --- a/src/compileTypes/helpers/substituteAliasedModules.ts +++ b/src/compileTypes/helpers/substituteAliasedModules.ts @@ -1,9 +1,12 @@ import { PREFIX_NOT_FOR_IMPORT } from '../../constants'; import { getLogger } from '../../helpers'; +import type { CommonLogger } from '../../models'; -export function substituteAliasedModules(federatedModuleName: string, typings: string): string { - const logger = getLogger(); - +export function substituteAliasedModules( + federatedModuleName: string, + typings: string, + logger: CommonLogger = getLogger(), +): string { // Collect all instances of `import("...")` const regexImportPaths = /import\("([^"]*)"\)/g; const uniqueImportPaths = new Set(); @@ -16,7 +19,16 @@ export function substituteAliasedModules(federatedModuleName: string, typings: s let modifiedTypings = typings; - uniqueImportPaths.forEach(importPath => { + const filteredImportPaths = Array.from(uniqueImportPaths).filter( + path => !path.startsWith(PREFIX_NOT_FOR_IMPORT), + ); + + if (filteredImportPaths.length) { + logger.log(`Unique import paths in ${federatedModuleName}:`); + logger.log(JSON.stringify(filteredImportPaths, null, 2)); + } + + filteredImportPaths.forEach(importPath => { const notForImportPath = `${PREFIX_NOT_FOR_IMPORT}/${federatedModuleName}/${importPath}`; if (modifiedTypings.includes(`declare module "${notForImportPath}"`)) { diff --git a/src/compileTypes/rewritePathsWithExposedFederatedModules.ts b/src/compileTypes/rewritePathsWithExposedFederatedModules.ts index 703a618..28d8004 100644 --- a/src/compileTypes/rewritePathsWithExposedFederatedModules.ts +++ b/src/compileTypes/rewritePathsWithExposedFederatedModules.ts @@ -4,26 +4,22 @@ import path from 'node:path'; import mkdirp from 'mkdirp'; import { PREFIX_NOT_FOR_IMPORT } from '../constants'; -import type { FederationConfig } from '../models'; +import type { CommonLogger, FederationConfig } from '../models'; +import { getLogger } from '../helpers'; import { includeTypesFromNodeModules, substituteAliasedModules } from './helpers'; export function rewritePathsWithExposedFederatedModules( federationConfig: FederationConfig, outFile: string, typings: string, + logger: CommonLogger = getLogger(), ): void { const regexDeclareModule = /declare module "(.*)"/g; - const declaredModulePaths: string[] = []; + const declaredModulePaths = + Array.from(typings.matchAll(regexDeclareModule), match => match[1]) || []; - // Collect all instances of `declare module "..."` - for ( - let execResults: null | string[] = regexDeclareModule.exec(typings); - execResults !== null; - execResults = regexDeclareModule.exec(typings) - ) { - declaredModulePaths.push(execResults[1]); - } + logger.debug(`Declared module paths: ${JSON.stringify(declaredModulePaths, null, 2)}`); let typingsUpdated: string = typings; @@ -57,8 +53,8 @@ export function rewritePathsWithExposedFederatedModules( ].join('\n'); }); - typingsUpdated = substituteAliasedModules(federationConfig.name, typingsUpdated); - typingsUpdated = includeTypesFromNodeModules(federationConfig, typingsUpdated); + typingsUpdated = substituteAliasedModules(federationConfig.name, typingsUpdated, logger); + typingsUpdated = includeTypesFromNodeModules(federationConfig, typingsUpdated, logger); mkdirp.sync(path.dirname(outFile)); fs.writeFileSync(outFile, typingsUpdated.replace(/\r\n/g, '\n')); diff --git a/src/compileTypes/workerLogger.ts b/src/compileTypes/workerLogger.ts new file mode 100644 index 0000000..4207c3d --- /dev/null +++ b/src/compileTypes/workerLogger.ts @@ -0,0 +1,16 @@ +import { parentPort } from 'node:worker_threads'; + +import type { CommonLogger } from '../models'; +import type { LogLevel } from './compileTypesWorker'; + +export function sendLog(level: LogLevel, message: string) { + parentPort?.postMessage({ status: 'log', level, message }); +} + +export const workerLogger: CommonLogger = { + error: (message: string) => sendLog('error', message), + warn: (message: string) => sendLog('warn', message), + info: (message: string) => sendLog('info', message), + log: (message: string) => sendLog('log', message), + debug: (message: string) => sendLog('debug', message), +}; diff --git a/src/models/CommonLogger.ts b/src/models/CommonLogger.ts new file mode 100644 index 0000000..488b027 --- /dev/null +++ b/src/models/CommonLogger.ts @@ -0,0 +1,7 @@ +export type CommonLogger = { + error: (message: string) => void; + warn: (message: string) => void; + info: (message: string) => void; + log: (message: string) => void; + debug: (message: string) => void; +}; diff --git a/src/models/index.ts b/src/models/index.ts index 8eae8d2..9902203 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,3 +1,4 @@ +export * from './CommonLogger'; export * from './FederationConfig'; export * from './ModuleFederationPluginOptions'; export * from './ModuleFederationTypesPluginOptions'; diff --git a/src/plugin.ts b/src/plugin.ts index 159465b..2b52d80 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -95,16 +95,20 @@ export class ModuleFederationTypesPlugin implements WebpackPluginInstance { // Create types for exposed modules const compileTypesAfterEmit = async () => { - compileTypesAsync( - { - tsconfigPath: TS_CONFIG_FILE, - exposedModules: exposes as string[], - outFile, - dirGlobalTypes, - federationConfig: federationPluginOptions as FederationConfig, - }, - getLoggerHint(compiler), - ); + try { + await compileTypesAsync( + { + tsconfigPath: TS_CONFIG_FILE, + exposedModules: exposes as string[], + outFile, + dirGlobalTypes, + federationConfig: federationPluginOptions as FederationConfig, + }, + getLoggerHint(compiler), + ); + } catch (error) { + logger.error('Error compiling types', error); + } }; // Import types from remote modules