Skip to content

Commit

Permalink
refactor: added workerLogger
Browse files Browse the repository at this point in the history
- Added a new interface `CommonLogger` to define common logging methods.
- Implemented `workerLogger` object that uses `sendLog` function to send log messages to the parent thread.
  • Loading branch information
steven-pribilinskiy committed Oct 13, 2024
1 parent 438ca25 commit ea5f3e5
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 75 deletions.
22 changes: 9 additions & 13 deletions src/compileTypes/compileTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,27 @@ 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 = {
tsconfigPath: string;
exposedModules: string[];
outFile: string;
dirGlobalTypes: string;
federationConfig: FederationConfig;
};

export type CompileTypesResult = {
isSuccess: boolean;
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,
Expand Down Expand Up @@ -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');
Expand Down
22 changes: 15 additions & 7 deletions src/compileTypes/compileTypesAsync.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
export function compileTypesAsync(
params: CompileTypesWorkerMessage,
loggerHint = '',
): Promise<void> {
const logger = getLogger();

return new Promise((resolve, reject) => {
Expand All @@ -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;
Expand Down Expand Up @@ -52,6 +60,6 @@ export function compileTypesAsync(params: CompileTypesParams, loggerHint = ''):
worker = null;
});

parentPort?.postMessage({ ...params, logger });
worker.postMessage(params);
});
}
45 changes: 29 additions & 16 deletions src/compileTypes/compileTypesWorker.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,61 @@
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'>;

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);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
});
});
8 changes: 5 additions & 3 deletions src/compileTypes/helpers/getTSConfigCompilerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
11 changes: 6 additions & 5 deletions src/compileTypes/helpers/includeTypesFromNodeModules.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -26,16 +26,17 @@ 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 {
exposedNpmPackages.forEach(([exposedModuleKey, packageName]) => {
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;
Expand Down
14 changes: 7 additions & 7 deletions src/compileTypes/helpers/reportCompileDiagnostic.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
20 changes: 16 additions & 4 deletions src/compileTypes/helpers/substituteAliasedModules.ts
Original file line number Diff line number Diff line change
@@ -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<string>();
Expand All @@ -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}"`)) {
Expand Down
8 changes: 5 additions & 3 deletions src/compileTypes/rewritePathsWithExposedFederatedModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ 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[] = [];
Expand Down Expand Up @@ -57,8 +59,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'));
Expand Down
15 changes: 15 additions & 0 deletions src/compileTypes/workerLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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 = {
log: (message: string) => sendLog('log', message),
info: (message: string) => sendLog('info', message),
warn: (message: string) => sendLog('warn', message),
error: (message: string) => sendLog('error', message),
};
6 changes: 6 additions & 0 deletions src/models/CommonLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type CommonLogger = {
log: (message: string) => void;
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
};
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './CommonLogger';
export * from './FederationConfig';
export * from './ModuleFederationPluginOptions';
export * from './ModuleFederationTypesPluginOptions';
Expand Down
Loading

0 comments on commit ea5f3e5

Please sign in to comment.