From d2b044986fbee857f04f4deeb75e225b2bc23ae2 Mon Sep 17 00:00:00 2001 From: Steven Prybylynskyi Date: Mon, 14 Oct 2024 17:49:26 +0200 Subject: [PATCH] refactor: update logger interface and add log grouping - Update the `CommonLogger` interface to include the `group`, `groupEnd`, and `groupCollapsed` methods. - Add the `LogLevel` type to represent different log levels. - Modify the `compileTypes` function to use the new logger methods for logging and grouping log messages. --- package.json | 4 +- .../__tests__/workerLogger.test.ts | 97 +++++++++++++++++++ src/compileTypes/compileTypes.ts | 9 +- src/compileTypes/compileTypesAsync.ts | 25 +++-- src/compileTypes/compileTypesWorker.ts | 44 +++++---- .../includeTypesFromNodeModules.test.ts | 19 ++-- .../substituteAliasedModules.test.ts | 5 +- .../helpers/includeTypesFromNodeModules.ts | 19 +++- .../helpers/substituteAliasedModules.ts | 12 ++- ...rewritePathsWithExposedFederatedModules.ts | 10 +- src/compileTypes/workerLogger.ts | 32 ++++-- .../downloadRemoteEntryURLsFromManifests.ts | 9 +- src/models/CommonLogger.ts | 12 ++- src/models/LogLevel.ts | 6 ++ src/models/index.ts | 1 + 15 files changed, 236 insertions(+), 68 deletions(-) create mode 100644 src/compileTypes/__tests__/workerLogger.test.ts create mode 100644 src/models/LogLevel.ts diff --git a/package.json b/package.json index 4973498..5d6253d 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,7 @@ "url": "https://github.com/cloudbeds/webpack-module-federation-types-plugin.git" }, "main": "dist/plugin.js", - "files": [ - "dist" - ], + "files": ["dist"], "bin": { "make-federated-types": "dist/bin/make-federated-types.js", "download-federated-types": "dist/bin/download-federated-types.js" diff --git a/src/compileTypes/__tests__/workerLogger.test.ts b/src/compileTypes/__tests__/workerLogger.test.ts new file mode 100644 index 0000000..c579f9a --- /dev/null +++ b/src/compileTypes/__tests__/workerLogger.test.ts @@ -0,0 +1,97 @@ +import { parentPort } from 'node:worker_threads'; +import { describe, expect, test, vi } from 'vitest'; + +import { sendLog, workerLogger } from '../workerLogger'; + +vi.mock('node:worker_threads', () => ({ + parentPort: { + postMessage: vi.fn(), + }, +})); + +describe('workerLogger', () => { + const mockPostMessage = vi.mocked(parentPort?.postMessage); + + test('sendLog handles various data types correctly', () => { + const testData = [ + { a: 'b', c: { d: 'e' } }, + null, + 'c', + 1, + ['a', 'b'], + undefined, + 0, + true, + false, + () => 'anonymous function', + /test/, + ]; + + sendLog('info', testData); + + expect(mockPostMessage).toHaveBeenCalledWith({ + status: 'log', + level: 'info', + message: [ + '{\n "a": "b",\n "c": {\n "d": "e"\n }\n}', + 'null', + 'c', + '1', + '[\n "a",\n "b"\n]', + '', + '0', + 'true', + 'false', + '() => "anonymous function"', + '/test/', + ].join(' '), + }); + }); + + test('workerLogger methods call sendLog with correct level and data', () => { + const logMethods = ['error', 'warn', 'info', 'log', 'group', 'groupCollapsed'] as const; + const testData = ['Test message', { key: 'value' }, 42, true]; + + logMethods.forEach(method => { + workerLogger[method](...testData); + expect(mockPostMessage).toHaveBeenCalledWith({ + status: 'log', + level: method, + message: 'Test message {\n "key": "value"\n} 42 true', + }); + }); + }); + + test('workerLogger.groupEnd calls sendLog with empty array', () => { + workerLogger.groupEnd(); + expect(mockPostMessage).toHaveBeenCalledWith({ + status: 'log', + level: 'groupEnd', + message: '', + }); + }); + + test('sendLog throws error on circular references', () => { + const circular: Dict = { a: 'circular' }; + circular.self = circular; + + expect(() => sendLog('info', [circular])).toThrow('Converting circular structure to JSON'); + + expect(mockPostMessage).not.toHaveBeenCalled(); + }); + + test('sendLog handles deep nested objects and arrays', () => { + const deepNested = { + a: [1, { b: { c: [2, 3, { d: 4 }] } }], + e: { f: { g: { h: 5 } } }, + }; + + sendLog('info', [deepNested]); + + expect(mockPostMessage).toHaveBeenCalledWith({ + status: 'log', + level: 'info', + message: JSON.stringify(deepNested, null, 2), + }); + }); +}); diff --git a/src/compileTypes/compileTypes.ts b/src/compileTypes/compileTypes.ts index bef2f03..cf2fe72 100644 --- a/src/compileTypes/compileTypes.ts +++ b/src/compileTypes/compileTypes.ts @@ -54,8 +54,13 @@ export function compileTypes( ...getAllFilePaths(`./${dirGlobalTypes}`).filter(filePath => filePath.endsWith('.d.ts')), ); } - logger.log('[compileTypes]: Including a set of root files in compilation'); - logger.log(JSON.stringify(exposedFileNames, null, 2)); + + logger.groupCollapsed( + '[compileTypes]: Including a set of exposed modules in compilation', + `(${exposedFileNames.length} npm packages and root paths)`, + ); + logger.log(exposedFileNames); + logger.groupEnd(); const program = ts.createProgram(exposedFileNames, compilerOptions, host); const { diagnostics, emitSkipped } = program.emit(); diff --git a/src/compileTypes/compileTypesAsync.ts b/src/compileTypes/compileTypesAsync.ts index 5399cb4..0eb94e8 100644 --- a/src/compileTypes/compileTypesAsync.ts +++ b/src/compileTypes/compileTypesAsync.ts @@ -8,17 +8,19 @@ import type { } from './compileTypesWorker'; let worker: Worker | null = null; +let workerIndex = 0; export function compileTypesAsync( params: CompileTypesWorkerMessage, loggerHint = '', ): Promise { const logger = getLogger(); + workerIndex++; return new Promise((resolve, reject) => { if (worker) { - logger.log('Terminating existing worker process'); - worker.terminate(); + logger.log(`Terminating existing worker process #${workerIndex}`); + worker.postMessage({ type: 'exit' }); } const workerPath = path.join(__dirname, 'compileTypesWorker.js'); @@ -27,17 +29,23 @@ export function compileTypesAsync( worker.on('message', (result: CompileTypesWorkerResultMessage) => { switch (result.status) { case 'log': - logger[result.level]('[Worker]:', result.message); + logger[result.level](`[Worker #${workerIndex}]:`, result.message); return; case 'success': resolve(); break; case 'failure': - logger.warn('[Worker]: Failed to compile types for exposed modules.', loggerHint); + logger.warn( + `[Worker #${workerIndex}]: Failed to compile types for exposed modules.`, + loggerHint, + ); reject(new Error('Failed to compile types for exposed modules.')); break; case 'error': - logger.warn('[Worker]: Error compiling types for exposed modules.', loggerHint); + logger.warn( + `[Worker #${workerIndex}]: Error compiling types for exposed modules.`, + loggerHint, + ); reject(result.error); break; } @@ -46,7 +54,7 @@ export function compileTypesAsync( }); worker.on('error', error => { - logger.warn('[Worker]: Unexpected error.', loggerHint); + logger.warn(`[Worker #${workerIndex}]: Unexpected error.`, loggerHint); logger.log(error); reject(error); worker?.terminate(); @@ -54,11 +62,10 @@ export function compileTypesAsync( }); worker.on('exit', code => { - if (code === null || code === 0 || code === 1) { - logger.log(`[Worker]: Process exited with code ${code}`); + if (code === null || code === 0) { resolve(); } else { - reject(new Error(`[Worker]: Process exited with code ${code}`)); + reject(new Error(`[Worker #${workerIndex}]: Process exited with code ${code}`)); } worker = null; }); diff --git a/src/compileTypes/compileTypesWorker.ts b/src/compileTypes/compileTypesWorker.ts index a428816..10dca93 100644 --- a/src/compileTypes/compileTypesWorker.ts +++ b/src/compileTypes/compileTypesWorker.ts @@ -1,15 +1,9 @@ import { parentPort } from 'node:worker_threads'; -import type { Compilation } from 'webpack'; -import type { FederationConfig } from '../models'; +import type { FederationConfig, LogLevel } from '../models'; import { type CompileTypesParams, compileTypes } from './compileTypes'; import { rewritePathsWithExposedFederatedModules } from './rewritePathsWithExposedFederatedModules'; -import { sendLog, workerLogger } from './workerLogger'; - -export type LogLevel = keyof Pick< - Compilation['logger'], - 'log' | 'info' | 'warn' | 'error' | 'debug' ->; +import { workerLogger } from './workerLogger'; type CompileTypesWorkerResultMessageError = { status: 'error'; @@ -20,36 +14,48 @@ export type CompileTypesWorkerMessage = CompileTypesParams & { federationConfig: FederationConfig; }; +export type ExitMessage = { + type: 'exit'; +}; + export type CompileTypesWorkerResultMessage = | { status: 'success' } | { status: 'failure' } | CompileTypesWorkerResultMessageError | { status: 'log'; level: LogLevel; message: string }; -parentPort?.on('message', ({ federationConfig, ...params }: CompileTypesWorkerMessage) => { +parentPort?.on('message', (message: CompileTypesWorkerMessage | ExitMessage) => { + if ((message as ExitMessage).type === 'exit') { + workerLogger.log('Exiting by request'); + process.exit(0); + } + + const { federationConfig, ...params } = message as CompileTypesWorkerMessage; + try { - let startTime = performance.now(); + const startTime = performance.now(); const { isSuccess, typeDefinitions } = compileTypes(params, workerLogger); if (isSuccess) { - let endTime = performance.now(); - let timeTakenInSeconds = (endTime - startTime) / 1000; - sendLog('log', `Types compilation completed in ${timeTakenInSeconds.toFixed(2)} seconds`); + const timeTakenInSeconds = ((performance.now() - startTime) / 1000).toFixed(2); + workerLogger.log(`Types compilation completed in ${timeTakenInSeconds} seconds`); - sendLog( - 'log', + workerLogger.log( `Replacing paths with names of exposed federate modules in typings file: ${params.outFile}`, ); - startTime = performance.now(); + const rewriteStartTime = performance.now(); rewritePathsWithExposedFederatedModules( federationConfig, params.outFile, typeDefinitions, workerLogger, ); - endTime = performance.now(); - timeTakenInSeconds = (endTime - startTime) / 1000; - sendLog('log', `Typings file rewritten in ${timeTakenInSeconds.toFixed(2)} seconds`); + const rewriteTimeTakenInSeconds = ((performance.now() - rewriteStartTime) / 1000).toFixed(2); + workerLogger.log(`Typings file rewritten in ${rewriteTimeTakenInSeconds} seconds`); + + workerLogger.info( + `Types compilation and modification completed in ${timeTakenInSeconds} + ${rewriteTimeTakenInSeconds} seconds`, + ); parentPort?.postMessage({ status: 'success' } satisfies CompileTypesWorkerResultMessage); } else { diff --git a/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts b/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts index 2e28c5c..72a6979 100644 --- a/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts +++ b/src/compileTypes/helpers/__tests__/includeTypesFromNodeModules.test.ts @@ -7,6 +7,8 @@ import { includeTypesFromNodeModules } from '../includeTypesFromNodeModules'; const mockLogger = { log: vi.fn(), warn: vi.fn(), + groupCollapsed: vi.fn(), + groupEnd: vi.fn(), }; setLogger(mockLogger); @@ -36,17 +38,14 @@ describe('includeTypesFromNodeModules', () => { ].join('\n'); expect(result).toBe([initialTypings, moduleADeclaration, moduleBDeclaration].join('\n')); - expect(mockLogger.log).toHaveBeenCalledWith('Including typings for npm packages:'); - expect(mockLogger.log).toHaveBeenCalledWith( - JSON.stringify( - [ - ['ModuleA', 'libraryA'], - ['ModuleB', 'libraryB'], - ], - null, - 2, - ), + expect(mockLogger.groupCollapsed).toHaveBeenCalledWith( + 'Including typings for npm packages', + '(2 packages)', ); + expect(mockLogger.log).toHaveBeenCalledWith([ + ['ModuleA', 'libraryA'], + ['ModuleB', 'libraryB'], + ]); }); 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 ea1c0f8..dbc55e2 100644 --- a/src/compileTypes/helpers/__tests__/substituteAliasedModules.test.ts +++ b/src/compileTypes/helpers/__tests__/substituteAliasedModules.test.ts @@ -6,6 +6,8 @@ import { substituteAliasedModules } from '../substituteAliasedModules'; const mockLogger = { log: vi.fn(), + groupCollapsed: vi.fn(), + groupEnd: vi.fn(), }; setLogger(mockLogger); @@ -36,7 +38,6 @@ describe('substituteAliasedModules', () => { const result = substituteAliasedModules(federatedModuleName, originalTypings); expect(result).toBe(originalTypings); - expect(logger.log).toHaveBeenCalledWith('Unique import paths in myCommon:'); - expect(logger.log).toHaveBeenCalledWith(JSON.stringify(['another/module'], null, 2)); + expect(logger.log).toHaveBeenCalledWith('Found 1 import path in myCommon: another/module'); }); }); diff --git a/src/compileTypes/helpers/includeTypesFromNodeModules.ts b/src/compileTypes/helpers/includeTypesFromNodeModules.ts index 5f2c913..38cdc57 100644 --- a/src/compileTypes/helpers/includeTypesFromNodeModules.ts +++ b/src/compileTypes/helpers/includeTypesFromNodeModules.ts @@ -26,8 +26,16 @@ export function includeTypesFromNodeModules( ].join('\n'); if (exposedNpmPackages.length) { - logger.log('Including typings for npm packages:'); - logger.log(JSON.stringify(exposedNpmPackages, null, 2)); + if (exposedNpmPackages.length === 1) { + logger.log('Including typings for npm package', exposedNpmPackages[0]); + } else { + logger.groupCollapsed( + 'Including typings for npm packages', + `(${exposedNpmPackages.length} packages)`, + ); + logger.log(exposedNpmPackages); + logger.groupEnd(); + } } try { @@ -35,8 +43,11 @@ 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(JSON.stringify(err, null, 2)); + const url = (err as Dict)?.url; + if (url) { + logger.warn(`Typings were not included for npm package: ${url}`); + } + logger.log(err); } return typingsWithNpmPackages; diff --git a/src/compileTypes/helpers/substituteAliasedModules.ts b/src/compileTypes/helpers/substituteAliasedModules.ts index fa6d26c..f4d3152 100644 --- a/src/compileTypes/helpers/substituteAliasedModules.ts +++ b/src/compileTypes/helpers/substituteAliasedModules.ts @@ -24,8 +24,16 @@ export function substituteAliasedModules( ); if (filteredImportPaths.length) { - logger.log(`Unique import paths in ${federatedModuleName}:`); - logger.log(JSON.stringify(filteredImportPaths, null, 2)); + if (filteredImportPaths.length === 1) { + logger.log(`Found 1 import path in ${federatedModuleName}: ${filteredImportPaths[0]}`); + } else { + logger.groupCollapsed( + `Collected unique import paths in ${federatedModuleName}`, + `(${filteredImportPaths.length} paths)`, + ); + logger.log(filteredImportPaths); + logger.groupEnd(); + } } filteredImportPaths.forEach(importPath => { diff --git a/src/compileTypes/rewritePathsWithExposedFederatedModules.ts b/src/compileTypes/rewritePathsWithExposedFederatedModules.ts index 15fc405..ff36c37 100644 --- a/src/compileTypes/rewritePathsWithExposedFederatedModules.ts +++ b/src/compileTypes/rewritePathsWithExposedFederatedModules.ts @@ -18,7 +18,15 @@ export function rewritePathsWithExposedFederatedModules( const regexDeclareModule = /declare module "(.*)"/g; const declaredModulePaths = Array.from(typings.matchAll(regexDeclareModule), match => match[1]); - logger.debug(`Declared module paths: ${JSON.stringify(declaredModulePaths, null, 2)}`); + logger.groupCollapsed( + '[rewritePathsWithExposedFederatedModules] Collected module declaration paths', + `(${declaredModulePaths.length} declarations)`, + ); + logger.log( + declaredModulePaths.length > 20 ? 'first 20 items:' : '', + declaredModulePaths.slice(0, 20), + ); + logger.groupEnd(); let typingsUpdated: string = typings; diff --git a/src/compileTypes/workerLogger.ts b/src/compileTypes/workerLogger.ts index 4207c3d..c59783e 100644 --- a/src/compileTypes/workerLogger.ts +++ b/src/compileTypes/workerLogger.ts @@ -1,16 +1,32 @@ import { parentPort } from 'node:worker_threads'; import type { CommonLogger } from '../models'; -import type { LogLevel } from './compileTypesWorker'; +import type { LogLevel } from '../models'; -export function sendLog(level: LogLevel, message: string) { - parentPort?.postMessage({ status: 'log', level, message }); +export function sendLog(level: LogLevel, items: unknown[]) { + parentPort?.postMessage({ + status: 'log', + level, + message: items + .map(item => + typeof item === 'object' && + !(item instanceof RegExp) && + !(item instanceof Date) && + !(item instanceof Function) && + !(item instanceof Error) + ? JSON.stringify(item, null, 2) + : item?.toString(), + ) + .join(' '), + }); } 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), + error: (...data: unknown[]) => sendLog('error', data), + warn: (...data: unknown[]) => sendLog('warn', data), + info: (...data: unknown[]) => sendLog('info', data), + log: (...data: unknown[]) => sendLog('log', data), + group: (...data: unknown[]) => sendLog('group', data), + groupEnd: () => sendLog('groupEnd', []), + groupCollapsed: (...data: unknown[]) => sendLog('groupCollapsed', data), }; diff --git a/src/downloadTypes/helpers/downloadRemoteEntryURLsFromManifests.ts b/src/downloadTypes/helpers/downloadRemoteEntryURLsFromManifests.ts index 379c56b..9f0825a 100644 --- a/src/downloadTypes/helpers/downloadRemoteEntryURLsFromManifests.ts +++ b/src/downloadTypes/helpers/downloadRemoteEntryURLsFromManifests.ts @@ -1,5 +1,6 @@ import { getLogger, isValidUrl, toCamelCase } from '../../helpers'; import type { + CommonLogger, RemoteEntryUrls, RemoteManifest, RemoteManifestUrls, @@ -14,15 +15,15 @@ import { downloadRemoteEntryManifest } from './downloadRemoteEntryManifest'; */ export async function downloadRemoteEntryURLsFromManifests( remoteManifestUrls?: RemoteManifestUrls, + logger: CommonLogger = getLogger(), ): Promise { if (!remoteManifestUrls) { return {}; } - const logger = getLogger(); const remoteEntryURLs: RemoteEntryUrls = {}; - logger.log('Remote manifest URLs', remoteManifestUrls); + logger.log('Remote manifest URLs:', remoteManifestUrls); const { artifactsBaseUrl, ...manifestUrls } = remoteManifestUrls; @@ -50,7 +51,9 @@ export async function downloadRemoteEntryURLsFromManifests( } }); - logger.log('Remote entry URLs', remoteEntryURLs); + logger.groupCollapsed('Remote entry URLs', `(${Object.keys(remoteEntryURLs).length} microapps)`); + logger.log(remoteEntryURLs); + logger.groupEnd(); return remoteEntryURLs; } diff --git a/src/models/CommonLogger.ts b/src/models/CommonLogger.ts index 488b027..9aed03a 100644 --- a/src/models/CommonLogger.ts +++ b/src/models/CommonLogger.ts @@ -1,7 +1,9 @@ export type CommonLogger = { - error: (message: string) => void; - warn: (message: string) => void; - info: (message: string) => void; - log: (message: string) => void; - debug: (message: string) => void; + error: (...data: unknown[]) => void; + warn: (...data: unknown[]) => void; + info: (...data: unknown[]) => void; + log: (...data: unknown[]) => void; + group: (...data: unknown[]) => void; + groupEnd: () => void; + groupCollapsed: (...data: unknown[]) => void; }; diff --git a/src/models/LogLevel.ts b/src/models/LogLevel.ts new file mode 100644 index 0000000..8d66cb2 --- /dev/null +++ b/src/models/LogLevel.ts @@ -0,0 +1,6 @@ +import type { Compilation } from 'webpack'; + +export type LogLevel = keyof Pick< + Compilation['logger'], + 'log' | 'info' | 'warn' | 'error' | 'group' | 'groupEnd' | 'groupCollapsed' +>; diff --git a/src/models/index.ts b/src/models/index.ts index 9902203..45d9624 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,5 +1,6 @@ export * from './CommonLogger'; export * from './FederationConfig'; +export * from './LogLevel'; export * from './ModuleFederationPluginOptions'; export * from './ModuleFederationTypesPluginOptions'; export * from './RemoteEntryUrls';