diff --git a/packages/data-prefetch/src/cli/index.ts b/packages/data-prefetch/src/cli/index.ts index 134b405a7b5..d24c5a79e26 100644 --- a/packages/data-prefetch/src/cli/index.ts +++ b/packages/data-prefetch/src/cli/index.ts @@ -2,6 +2,9 @@ import path from 'path'; import fs from 'fs-extra'; import { + bindLoggerToCompiler, + createInfrastructureLogger, + createLogger, encodeName, moduleFederationPlugin, MFPrefetchCommon, @@ -18,6 +21,15 @@ const { RuntimeGlobals, Template } = require( normalizeWebpackPath('webpack'), ) as typeof import('webpack'); +const createBundlerLogger: typeof createLogger = + typeof createInfrastructureLogger === 'function' + ? (createInfrastructureLogger as unknown as typeof createLogger) + : createLogger; + +const logger = createBundlerLogger( + '[ Module Federation Data Prefetch Plugin ]', +); + export function getFederationGlobalScope( runtimeGlobals: typeof RuntimeGlobals, ): string { @@ -35,6 +47,7 @@ export class PrefetchPlugin implements WebpackPluginInstance { // eslint-disable-next-line max-lines-per-function apply(compiler: Compiler) { + bindLoggerToCompiler(logger, compiler, 'PrefetchPlugin'); const { name, exposes } = this.options; if (!exposes) { return; @@ -54,7 +67,7 @@ export class PrefetchPlugin implements WebpackPluginInstance { } if (this.options.shareStrategy !== SHARED_STRATEGY) { this.options.shareStrategy = SHARED_STRATEGY; - console.warn( + logger.warn( `[Module Federation Data Prefetch]: Your shared strategy is set to '${SHARED_STRATEGY}', this is a necessary condition for data prefetch`, ); } diff --git a/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts b/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts index ae109779452..37bae9d69e7 100644 --- a/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts +++ b/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts @@ -1,4 +1,4 @@ -import { logger } from '@module-federation/sdk'; +import { infrastructureLogger as logger } from '@module-federation/sdk'; import { normalizeOptions, type moduleFederationPlugin, diff --git a/packages/enhanced/src/lib/container/ContainerEntryModule.ts b/packages/enhanced/src/lib/container/ContainerEntryModule.ts index fc248c4f255..2329afbde53 100644 --- a/packages/enhanced/src/lib/container/ContainerEntryModule.ts +++ b/packages/enhanced/src/lib/container/ContainerEntryModule.ts @@ -5,7 +5,7 @@ 'use strict'; import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; -import { logger } from '@module-federation/sdk'; +import { infrastructureLogger as logger } from '@module-federation/sdk'; import { getShortErrorMsg, buildDescMap, diff --git a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts index 3f195bfc1b0..cb0b23e39b5 100644 --- a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts @@ -8,9 +8,10 @@ import { DtsPlugin } from '@module-federation/dts-plugin'; import { ContainerManager, utils } from '@module-federation/managers'; import { StatsPlugin } from '@module-federation/manifest'; import { + bindLoggerToCompiler, composeKeyWithSeparator, type moduleFederationPlugin, - logger, + infrastructureLogger, } from '@module-federation/sdk'; import { PrefetchPlugin } from '@module-federation/data-prefetch/cli'; import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; @@ -102,6 +103,11 @@ class ModuleFederationPlugin implements WebpackPluginInstance { * @returns {void} */ apply(compiler: Compiler): void { + bindLoggerToCompiler( + infrastructureLogger, + compiler, + 'EnhancedModuleFederationPlugin', + ); const { _options: options } = this; // must before ModuleFederationPlugin (new RemoteEntryPlugin(options) as unknown as WebpackPluginInstance).apply( @@ -175,7 +181,7 @@ class ModuleFederationPlugin implements WebpackPluginInstance { if (err instanceof Error) { err.message = `[ ModuleFederationPlugin ]: Manifest will not generate, because: ${err.message}`; } - logger.warn(err); + infrastructureLogger.warn(err); disableManifest = true; } } diff --git a/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts index 52a6514660f..af5bf1c471c 100644 --- a/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts +++ b/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts @@ -13,7 +13,7 @@ import fs from 'fs'; import path from 'path'; import { ConcatSource } from 'webpack-sources'; import { transformSync } from '@swc/core'; -import { logger } from '@module-federation/sdk'; +import { infrastructureLogger as logger } from '@module-federation/sdk'; const { RuntimeModule, Template, RuntimeGlobals } = require( normalizeWebpackPath('webpack'), diff --git a/packages/enhanced/test/ConfigTestCases.template.js b/packages/enhanced/test/ConfigTestCases.template.js index 30c6dd68946..fca3dca415a 100644 --- a/packages/enhanced/test/ConfigTestCases.template.js +++ b/packages/enhanced/test/ConfigTestCases.template.js @@ -18,6 +18,40 @@ const captureStdio = require('./helpers/captureStdio'); const asModule = require('./helpers/asModule'); const filterInfraStructureErrors = require('./helpers/infrastructureLogErrors'); +const stripAllowedInfrastructureLogs = (stderrOutput) => { + if (!stderrOutput) { + return ''; + } + const remaining = []; + const lines = stderrOutput.split(/\r?\n/); + let skippingStack = false; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) { + skippingStack = false; + continue; + } + if (filterInfraStructureErrors.isAllowedLog(trimmed)) { + skippingStack = true; + continue; + } + if ( + skippingStack && + (/^\s*(at\s|\()/i.test(line) || /^<[^>]+>\s*(at\s|\()/i.test(line)) + ) { + continue; + } + skippingStack = false; + remaining.push(line); + } + const result = remaining.join('\n'); + if (process.env.DEBUG_INFRA_LOG === '1' && result) { + // eslint-disable-next-line no-console + console.warn('[infra stderr]', result); + } + return result; +}; + const casesPath = path.join(__dirname, 'configCases'); const categories = fs.readdirSync(casesPath).map((cat) => { return { @@ -216,7 +250,9 @@ const describeCases = (config) => { fs.mkdirSync(outputDirectory, { recursive: true }); infraStructureLog.length = 0; require('webpack')(options, (err) => { - const infrastructureLogging = stderr.toString(); + const infrastructureLogging = stripAllowedInfrastructureLogs( + stderr.toString(), + ); if (infrastructureLogging) { return done( new Error( @@ -261,7 +297,8 @@ const describeCases = (config) => { errorsCount: true, }); if (errorsCount === 0) { - const infrastructureLogging = stderr.toString(); + const infrastructureLogging = + stripAllowedInfrastructureLogs(stderr.toString()); if (infrastructureLogging) { return done( new Error( @@ -363,7 +400,9 @@ const describeCases = (config) => { ) { return; } - const infrastructureLogging = stderr.toString(); + const infrastructureLogging = stripAllowedInfrastructureLogs( + stderr.toString(), + ); if (infrastructureLogging) { return done( new Error( diff --git a/packages/enhanced/test/configCases/sharing/consume-module-ignore-warnings/index.js b/packages/enhanced/test/configCases/sharing/consume-module-ignore-warnings/index.js index a30c8a9f86e..7ce0cc840f0 100644 --- a/packages/enhanced/test/configCases/sharing/consume-module-ignore-warnings/index.js +++ b/packages/enhanced/test/configCases/sharing/consume-module-ignore-warnings/index.js @@ -1,9 +1,20 @@ let warnings = []; let oldWarn; +const shouldIgnoreWarning = (warning) => + typeof warning === 'string' && warning.startsWith('[ Federation Runtime ]'); + +const captureWarning = (...args) => { + const [message] = args; + if (shouldIgnoreWarning(message)) { + return; + } + warnings.push(message); +}; + beforeEach((done) => { oldWarn = console.warn; - console.warn = (m) => warnings.push(m); + console.warn = captureWarning; done(); }); diff --git a/packages/enhanced/test/configCases/sharing/consume-module/index.js b/packages/enhanced/test/configCases/sharing/consume-module/index.js index 6720ca3bc87..589cab76151 100644 --- a/packages/enhanced/test/configCases/sharing/consume-module/index.js +++ b/packages/enhanced/test/configCases/sharing/consume-module/index.js @@ -1,9 +1,20 @@ let warnings = []; let oldWarn; +const shouldIgnoreWarning = (warning) => + typeof warning === 'string' && warning.startsWith('[ Federation Runtime ]'); + +const captureWarning = (...args) => { + const [message] = args; + if (shouldIgnoreWarning(message)) { + return; + } + warnings.push(message); +}; + beforeEach((done) => { oldWarn = console.warn; - console.warn = (m) => warnings.push(m); + console.warn = captureWarning; done(); }); diff --git a/packages/enhanced/test/helpers/infrastructureLogErrors.js b/packages/enhanced/test/helpers/infrastructureLogErrors.js index d74755b4f22..259d10e7507 100644 --- a/packages/enhanced/test/helpers/infrastructureLogErrors.js +++ b/packages/enhanced/test/helpers/infrastructureLogErrors.js @@ -10,6 +10,38 @@ const PERSISTENCE_CACHE_INVALIDATE_ERROR = (log, config) => { return `Pack got invalid because of write to: ${match[1].trim()}`; } }; +const ALLOWED_PREFIXES = [ + '[ Module Federation ]', + '[ Federation Runtime ]', + '[ Module Federation Manifest Plugin ]', + '[ Module Federation Bridge React ]', + '[ Module Federation Bridge Vue3 ]', + '[ Module Federation Bridge ]', + '[Module Federation Manifest Plugin]', + '[Module Federation Bridge React]', + '[Module Federation Bridge Vue3]', + '[Module Federation Bridge]', +]; + +const normalizeLogEntry = (log) => { + let normalized; + if (typeof log === 'string') normalized = log; + else if (Array.isArray(log)) normalized = log.join(' '); + else if (log && typeof log.message === 'string') normalized = log.message; + else normalized = String(log ?? ''); + + return normalized.replace(/\u001b\[[0-9;]*m/g, ''); +}; + +const isAllowedLog = (log) => { + const normalized = normalizeLogEntry(log).trim(); + if (!normalized) { + return true; + } + const sanitized = normalized.replace(/^<[^>]+>\s*/, ''); + return ALLOWED_PREFIXES.some((prefix) => sanitized.startsWith(prefix)); +}; + const errorsFilter = [PERSISTENCE_CACHE_INVALIDATE_ERROR]; /** @@ -20,6 +52,9 @@ const errorsFilter = [PERSISTENCE_CACHE_INVALIDATE_ERROR]; module.exports = function filterInfraStructureErrors(logs, config) { const results = []; for (const log of logs) { + if (isAllowedLog(log)) { + continue; + } for (const filter of errorsFilter) { const result = filter(log, config); if (result) results.push({ message: result }); @@ -27,3 +62,5 @@ module.exports = function filterInfraStructureErrors(logs, config) { } return results; }; + +module.exports.isAllowedLog = (log) => isAllowedLog(log); diff --git a/packages/manifest/src/StatsPlugin.ts b/packages/manifest/src/StatsPlugin.ts index 3a2db60843f..befb284e97e 100644 --- a/packages/manifest/src/StatsPlugin.ts +++ b/packages/manifest/src/StatsPlugin.ts @@ -1,5 +1,8 @@ import { Compiler, WebpackPluginInstance } from 'webpack'; -import { moduleFederationPlugin } from '@module-federation/sdk'; +import { + bindLoggerToCompiler, + moduleFederationPlugin, +} from '@module-federation/sdk'; import { ManifestManager } from './ManifestManager'; import { StatsManager } from './StatsManager'; import { PLUGIN_IDENTIFIER } from './constants'; @@ -40,6 +43,7 @@ export class StatsPlugin implements WebpackPluginInstance { } apply(compiler: Compiler): void { + bindLoggerToCompiler(logger, compiler, PLUGIN_IDENTIFIER); if (!this._enable) { return; } diff --git a/packages/manifest/src/logger.ts b/packages/manifest/src/logger.ts index 0fe103e36e0..193371c8d41 100644 --- a/packages/manifest/src/logger.ts +++ b/packages/manifest/src/logger.ts @@ -1,7 +1,15 @@ import chalk from 'chalk'; -import { createLogger } from '@module-federation/sdk'; +import { + createInfrastructureLogger, + createLogger, +} from '@module-federation/sdk'; import { PLUGIN_IDENTIFIER } from './constants'; -const logger = createLogger(chalk.cyan(`[ ${PLUGIN_IDENTIFIER} ]`)); +const createBundlerLogger: typeof createLogger = + typeof createInfrastructureLogger === 'function' + ? (createInfrastructureLogger as unknown as typeof createLogger) + : createLogger; + +const logger = createBundlerLogger(chalk.cyan(`[ ${PLUGIN_IDENTIFIER} ]`)); export default logger; diff --git a/packages/nextjs-mf/src/logger.ts b/packages/nextjs-mf/src/logger.ts new file mode 100644 index 00000000000..e9ae35d0ae9 --- /dev/null +++ b/packages/nextjs-mf/src/logger.ts @@ -0,0 +1,13 @@ +import { + createInfrastructureLogger, + createLogger, +} from '@module-federation/sdk'; + +const createBundlerLogger: typeof createLogger = + typeof createInfrastructureLogger === 'function' + ? (createInfrastructureLogger as unknown as typeof createLogger) + : createLogger; + +const logger = createBundlerLogger('[ nextjs-mf ]'); + +export default logger; diff --git a/packages/nextjs-mf/src/plugins/CopyFederationPlugin.ts b/packages/nextjs-mf/src/plugins/CopyFederationPlugin.ts index b053282f650..6388ea524d6 100644 --- a/packages/nextjs-mf/src/plugins/CopyFederationPlugin.ts +++ b/packages/nextjs-mf/src/plugins/CopyFederationPlugin.ts @@ -1,6 +1,8 @@ import { promises as fs } from 'fs'; import path from 'path'; import type { Compilation, Compiler, WebpackPluginInstance } from 'webpack'; +import { bindLoggerToCompiler } from '@module-federation/sdk'; +import logger from '../logger'; /** * Plugin to copy build output files. @@ -23,6 +25,7 @@ class CopyBuildOutputPlugin implements WebpackPluginInstance { * @method */ apply(compiler: Compiler): void { + bindLoggerToCompiler(logger, compiler, 'CopyBuildOutputPlugin'); /** * Copies files from source to destination. * @param {string} source - The source directory. @@ -78,7 +81,7 @@ class CopyBuildOutputPlugin implements WebpackPluginInstance { await copyFiles(sourcePath, serverLoc); } catch (error) { // If the promise rejects, the file does not exist. - console.error(`File at ${sourcePath} does not exist.`); + logger.error(`File at ${sourcePath} does not exist.`); } }, ); diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts index 486f9e64c0e..1a3a53e76a6 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts @@ -3,6 +3,7 @@ import { ChunkCorrelationPlugin } from '@module-federation/node'; import InvertedContainerPlugin from '../container/InvertedContainerPlugin'; import type { moduleFederationPlugin } from '@module-federation/sdk'; import type { NextFederationPluginExtraOptions } from './next-fragments'; +import logger from '../../logger'; /** * Applies client-specific plugins. @@ -37,12 +38,12 @@ export function applyClientPlugins( // Log a warning if automatic page stitching is enabled, as it is disabled in v7 if (extraOptions.automaticPageStitching) { - console.warn('[nextjs-mf]', 'automatic page stitching is disabled in v7'); + logger.warn('automatic page stitching is disabled in v7'); } // Log an error if a custom library is set, as it is not allowed if (options.library) { - console.error('[nextjs-mf] you cannot set custom library'); + logger.error('you cannot set custom library'); } // Set the library option to be a window object with the name of the module federation plugin diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts index 416476409b3..80534b3798c 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts @@ -27,7 +27,9 @@ import { } from './apply-server-plugins'; import { applyClientPlugins } from './apply-client-plugins'; import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack'; +import { bindLoggerToCompiler } from '@module-federation/sdk'; import type { moduleFederationPlugin } from '@module-federation/sdk'; +import logger from '../../logger'; import path from 'path'; /** @@ -54,6 +56,7 @@ export class NextFederationPlugin { * @param compiler The webpack compiler object. */ apply(compiler: Compiler) { + bindLoggerToCompiler(logger, compiler, 'NextFederationPlugin'); process.env['FEDERATION_WEBPACK_PATH'] = process.env['FEDERATION_WEBPACK_PATH'] || getWebpackPath(compiler, { framework: 'nextjs' }); @@ -120,9 +123,8 @@ export class NextFederationPlugin { const compilerValid = validateCompilerOptions(compiler); const pluginValid = validatePluginOptions(this._options); const envValid = process.env['NEXT_PRIVATE_LOCAL_WEBPACK']; - if (compilerValid === undefined) - console.error('Compiler validation failed'); - if (pluginValid === undefined) console.error('Plugin validation failed'); + if (compilerValid === undefined) logger.error('Compiler validation failed'); + if (pluginValid === undefined) logger.error('Plugin validation failed'); const validCompilerTarget = compiler.options.name === 'server' || compiler.options.name === 'client'; if (!envValid) diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts index cf398032679..6f9bd3348bb 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.test.ts @@ -1,8 +1,9 @@ +import logger from '../../logger'; import { removeUnnecessarySharedKeys } from './remove-unnecessary-shared-keys'; describe('removeUnnecessarySharedKeys', () => { beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(jest.fn()); + jest.spyOn(logger, 'warn').mockImplementation(jest.fn()); }); afterEach(() => { @@ -19,7 +20,7 @@ describe('removeUnnecessarySharedKeys', () => { removeUnnecessarySharedKeys(shared); expect(shared).toEqual({ lodash: '4.17.21' }); - expect(console.warn).toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalled(); }); it('should not remove keys that are not in the default share scope', () => { @@ -31,7 +32,7 @@ describe('removeUnnecessarySharedKeys', () => { removeUnnecessarySharedKeys(shared); expect(shared).toEqual({ lodash: '4.17.21', axios: '0.21.1' }); - expect(console.warn).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); }); it('should not remove keys from an empty object', () => { @@ -40,6 +41,6 @@ describe('removeUnnecessarySharedKeys', () => { removeUnnecessarySharedKeys(shared); expect(shared).toEqual({}); - expect(console.warn).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); }); }); diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts index bb5f5522ee2..c479eb52822 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/remove-unnecessary-shared-keys.ts @@ -6,6 +6,7 @@ * @param {Record} shared - The shared object to be checked. */ import { DEFAULT_SHARE_SCOPE } from '../../internal'; +import logger from '../../logger'; /** * Function to remove unnecessary shared keys from the default share scope. @@ -22,9 +23,8 @@ export function removeUnnecessarySharedKeys( * If the key is found in the default share scope, log a warning and remove the key from the shared object. */ if (DEFAULT_SHARE_SCOPE[key]) { - console.warn( - `%c[nextjs-mf] You are sharing ${key} from the default share scope. This is not necessary and can be removed.`, - 'color: red', + logger.warn( + `You are sharing ${key} from the default share scope. This is not necessary and can be removed.`, ); delete (shared as { [key: string]: unknown })[key]; } diff --git a/packages/nextjs-mf/src/plugins/container/RemoveEagerModulesFromRuntimePlugin.ts b/packages/nextjs-mf/src/plugins/container/RemoveEagerModulesFromRuntimePlugin.ts index cbada7fdf78..b5e3740833e 100644 --- a/packages/nextjs-mf/src/plugins/container/RemoveEagerModulesFromRuntimePlugin.ts +++ b/packages/nextjs-mf/src/plugins/container/RemoveEagerModulesFromRuntimePlugin.ts @@ -1,4 +1,6 @@ import type { Compiler, Compilation, Chunk, Module } from 'webpack'; +import { bindLoggerToCompiler } from '@module-federation/sdk'; +import logger from '../../logger'; /** * This plugin removes eager modules from the runtime. @@ -27,14 +29,18 @@ class RemoveEagerModulesFromRuntimePlugin { */ apply(compiler: Compiler) { if (!this.container) { - console.warn( - '[nextjs-mf]:', - 'RemoveEagerModulesFromRuntimePlugin container is not defined:', - this.container, + logger.warn( + `RemoveEagerModulesFromRuntimePlugin container is not defined: ${this.container}`, ); return; } + bindLoggerToCompiler( + logger, + compiler, + 'RemoveEagerModulesFromRuntimePlugin', + ); + compiler.hooks.thisCompilation.tap( 'RemoveEagerModulesFromRuntimePlugin', (compilation: Compilation) => { @@ -84,7 +90,7 @@ class RemoveEagerModulesFromRuntimePlugin { private removeModules(compilation: Compilation, chunk: Chunk) { for (const moduleToRemove of this.modulesToProcess) { if (this.debug) { - console.log('removing', moduleToRemove.constructor.name); + logger.info(`removing ${moduleToRemove.constructor.name}`); } if (compilation.chunkGraph.isModuleInChunk(moduleToRemove, chunk)) { diff --git a/packages/node/src/plugins/DynamicFilesystemChunkLoadingRuntimeModule.ts b/packages/node/src/plugins/DynamicFilesystemChunkLoadingRuntimeModule.ts index 2a7a76ba027..5a5335ccab9 100644 --- a/packages/node/src/plugins/DynamicFilesystemChunkLoadingRuntimeModule.ts +++ b/packages/node/src/plugins/DynamicFilesystemChunkLoadingRuntimeModule.ts @@ -23,6 +23,10 @@ import { generateInstallChunk, generateExternalInstallChunkCode, } from './webpackChunkUtilities'; +import { + createInfrastructureLogger, + createLogger, +} from '@module-federation/sdk'; import { fileSystemRunInContextStrategy, httpEvalStrategy, @@ -46,6 +50,11 @@ interface ChunkLoadingContext { webpack: Compiler['webpack']; } +const createBundlerLogger: typeof createLogger = + typeof createInfrastructureLogger === 'function' + ? (createInfrastructureLogger as unknown as typeof createLogger) + : createLogger; + class DynamicFilesystemChunkLoadingRuntimeModule extends RuntimeModule { private runtimeRequirements: Set; private options: DynamicFilesystemChunkLoadingRuntimeModuleOptions; @@ -53,6 +62,9 @@ class DynamicFilesystemChunkLoadingRuntimeModule extends RuntimeModule { hooks = { strategyCase: new SyncWaterfallHook(['source']), }; + private logger = createBundlerLogger( + '[ DynamicFilesystemChunkLoadingRuntimeModule ]', + ); constructor( runtimeRequirements: Set, @@ -106,10 +118,17 @@ class DynamicFilesystemChunkLoadingRuntimeModule extends RuntimeModule { const { chunkGraph, chunk, compilation } = this; const { Template } = webpack; if (!chunkGraph || !chunk || !compilation) { - console.warn('Missing required properties. Returning empty string.'); + this.logger.warn('Missing required properties. Returning empty string.'); return ''; } + const infrastructureLogger = compilation.getLogger?.( + 'DynamicFilesystemChunkLoadingRuntimeModule', + ); + if (infrastructureLogger) { + this.logger.setDelegate(infrastructureLogger as any); + } + const { runtimeTemplate } = compilation; const jsModulePlugin = webpack?.javascript?.JavascriptModulesPlugin || diff --git a/packages/node/src/plugins/NodeFederationPlugin.ts b/packages/node/src/plugins/NodeFederationPlugin.ts index ddc78522a96..da56874871d 100644 --- a/packages/node/src/plugins/NodeFederationPlugin.ts +++ b/packages/node/src/plugins/NodeFederationPlugin.ts @@ -3,6 +3,11 @@ import type { Compiler, container } from 'webpack'; import type { ModuleFederationPluginOptions } from '../types'; import EntryChunkTrackerPlugin from './EntryChunkTrackerPlugin'; +import { + bindLoggerToCompiler, + createInfrastructureLogger, + createLogger, +} from '@module-federation/sdk'; /** * Interface for NodeFederationOptions which extends ModuleFederationPluginOptions * @interface @@ -22,6 +27,11 @@ interface Context { ModuleFederationPlugin?: typeof container.ModuleFederationPlugin; } +const createBundlerLogger: typeof createLogger = + typeof createInfrastructureLogger === 'function' + ? (createInfrastructureLogger as unknown as typeof createLogger) + : createLogger; + /** * Class representing a NodeFederationPlugin. * @class @@ -30,6 +40,7 @@ class NodeFederationPlugin { private _options: ModuleFederationPluginOptions; private context: Context; private useRuntimePlugin?: boolean; + private logger = createBundlerLogger('[ Node Federation Plugin ]'); /** * Create a NodeFederationPlugin. @@ -52,6 +63,7 @@ class NodeFederationPlugin { * @param {Compiler} compiler - The webpack compiler. */ apply(compiler: Compiler) { + bindLoggerToCompiler(this.logger, compiler, 'NodeFederationPlugin'); const { webpack } = compiler; const pluginOptions = this.preparePluginOptions(); this.updateCompilerOptions(compiler); @@ -100,7 +112,7 @@ class NodeFederationPlugin { try { return require('@module-federation/enhanced').ModuleFederationPlugin; } catch (e) { - console.error( + this.logger.error( "Can't find @module-federation/enhanced, falling back to webpack ModuleFederationPlugin, this may not work", ); if (this.context.ModuleFederationPlugin) { diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts index 575b5342e6c..bd847172ccb 100644 --- a/packages/rspack/src/ModuleFederationPlugin.ts +++ b/packages/rspack/src/ModuleFederationPlugin.ts @@ -6,6 +6,7 @@ import type { RspackPluginInstance, } from '@rspack/core'; import { + bindLoggerToCompiler, composeKeyWithSeparator, moduleFederationPlugin, } from '@module-federation/sdk'; @@ -16,6 +17,7 @@ import ReactBridgePlugin from '@module-federation/bridge-react-webpack-plugin'; import path from 'node:path'; import fs from 'node:fs'; import { RemoteEntryPlugin } from './RemoteEntryPlugin'; +import logger from './logger'; type ExcludeFalse = T extends undefined | false ? never : T; type SplitChunks = Compiler['options']['optimization']['splitChunks']; @@ -93,6 +95,7 @@ export class ModuleFederationPlugin implements RspackPluginInstance { } apply(compiler: Compiler): void { + bindLoggerToCompiler(logger, compiler, PLUGIN_NAME); const { _options: options } = this; if (!options.name) { @@ -149,7 +152,7 @@ export class ModuleFederationPlugin implements RspackPluginInstance { if (err instanceof Error) { err.message = `[ ModuleFederationPlugin ]: Manifest will not generate, because: ${err.message}`; } - console.warn(err); + logger.warn(err); disableManifest = true; } } diff --git a/packages/rspack/src/logger.ts b/packages/rspack/src/logger.ts index c0e7fa3a866..cc769415205 100644 --- a/packages/rspack/src/logger.ts +++ b/packages/rspack/src/logger.ts @@ -1,5 +1,13 @@ -import { createLogger } from '@module-federation/sdk'; +import { + createInfrastructureLogger, + createLogger, +} from '@module-federation/sdk'; -const logger = createLogger('[ Module Federation Rspack Plugin ]'); +const createBundlerLogger: typeof createLogger = + typeof createInfrastructureLogger === 'function' + ? (createInfrastructureLogger as unknown as typeof createLogger) + : createLogger; + +const logger = createBundlerLogger('[ Module Federation Rspack Plugin ]'); export default logger; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 137746e9888..d840e767eae 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -7,8 +7,14 @@ export { simpleJoinRemoteEntry, inferAutoPublicPath, } from './generateSnapshotFromManifest'; -export { logger, createLogger } from './logger'; -export type { Logger } from './logger'; +export { + logger, + infrastructureLogger, + createLogger, + createInfrastructureLogger, + bindLoggerToCompiler, +} from './logger'; +export type { Logger, InfrastructureLogger } from './logger'; export * from './env'; export * from './dom'; export * from './node'; diff --git a/packages/sdk/src/logger.ts b/packages/sdk/src/logger.ts index 5368eb0f3d1..79164586755 100644 --- a/packages/sdk/src/logger.ts +++ b/packages/sdk/src/logger.ts @@ -2,39 +2,81 @@ import { isDebugMode } from './env'; const PREFIX = '[ Module Federation ]'; +type LogMethod = 'log' | 'info' | 'warn' | 'error' | 'debug'; + +type LoggerDelegate = Partial void>> & { + [key: string]: ((...args: any[]) => void) | undefined; +}; + +const DEFAULT_DELEGATE: LoggerDelegate = console as unknown as LoggerDelegate; + +const methodFallbackMap: Record = { + log: ['log', 'info'], + info: ['info', 'log'], + warn: ['warn', 'info', 'log'], + error: ['error', 'warn', 'log'], + debug: ['debug', 'log'], +}; + class Logger { prefix: string; - constructor(prefix: string) { + private delegate: LoggerDelegate; + + constructor(prefix: string, delegate: LoggerDelegate = DEFAULT_DELEGATE) { this.prefix = prefix; + this.delegate = delegate ?? DEFAULT_DELEGATE; } setPrefix(prefix: string) { this.prefix = prefix; } + setDelegate(delegate?: LoggerDelegate | null) { + this.delegate = delegate ?? DEFAULT_DELEGATE; + } + + private emit(method: LogMethod, args: any[]) { + const delegate = this.delegate ?? DEFAULT_DELEGATE; + const candidates = methodFallbackMap[method]; + const handlerName = candidates.find( + (candidate) => typeof delegate[candidate] === 'function', + ); + if (handlerName) { + const handler = delegate[handlerName]; + if (typeof handler === 'function') { + handler.call(delegate, this.prefix, ...args); + return; + } + } + + if (typeof DEFAULT_DELEGATE.log === 'function') { + DEFAULT_DELEGATE.log.call(DEFAULT_DELEGATE, this.prefix, ...args); + } + } + log(...args: any[]) { - console.log(this.prefix, ...args); + this.emit('log', args); } warn(...args: any[]) { - console.log(this.prefix, ...args); + this.emit('warn', args); } error(...args: any[]) { - console.log(this.prefix, ...args); + this.emit('error', args); } success(...args: any[]) { - console.log(this.prefix, ...args); + this.emit('info', args); } info(...args: any[]) { - console.log(this.prefix, ...args); + this.emit('info', args); } ready(...args: any[]) { - console.log(this.prefix, ...args); + this.emit('info', args); } debug(...args: any[]) { if (isDebugMode()) { - console.log(this.prefix, ...args); + this.emit('debug', args); } } } @@ -43,7 +85,66 @@ function createLogger(prefix: string) { return new Logger(prefix); } +type InfrastructureLogger = Logger & { + __mf_infrastructure_logger__: true; +}; + +function createInfrastructureLogger(prefix: string): InfrastructureLogger { + const infrastructureLogger = new Logger(prefix) as InfrastructureLogger; + Object.defineProperty(infrastructureLogger, '__mf_infrastructure_logger__', { + value: true, + enumerable: false, + configurable: false, + }); + return infrastructureLogger; +} + +type InfrastructureLoggerCapableCompiler = { + getInfrastructureLogger?: (name: string) => unknown; +}; + +function bindLoggerToCompiler( + loggerInstance: Logger, + compiler: InfrastructureLoggerCapableCompiler, + name: string, +) { + if ( + !(loggerInstance as Partial) + .__mf_infrastructure_logger__ + ) { + return; + } + if (!compiler?.getInfrastructureLogger) { + return; + } + try { + const infrastructureLogger = compiler.getInfrastructureLogger(name); + if ( + infrastructureLogger && + typeof infrastructureLogger === 'object' && + (typeof (infrastructureLogger as LoggerDelegate).log === 'function' || + typeof (infrastructureLogger as LoggerDelegate).info === 'function' || + typeof (infrastructureLogger as LoggerDelegate).warn === 'function' || + typeof (infrastructureLogger as LoggerDelegate).error === 'function') + ) { + loggerInstance.setDelegate( + infrastructureLogger as unknown as LoggerDelegate, + ); + } + } catch { + // If the bundler throws (older versions), fall back to default console logger. + loggerInstance.setDelegate(undefined); + } +} + const logger = createLogger(PREFIX); +const infrastructureLogger = createInfrastructureLogger(PREFIX); -export { logger, createLogger }; -export type { Logger }; +export { + logger, + infrastructureLogger, + createLogger, + createInfrastructureLogger, + bindLoggerToCompiler, +}; +export type { Logger, InfrastructureLogger };