@ckeditor/ckeditor5-dev-webpack-plugin
Version:
CKEditor 5 plugin for webpack.
277 lines (227 loc) • 9.37 kB
JavaScript
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
;
const chalk = require( 'chalk' );
const rimraf = require( 'rimraf' );
const fs = require( 'fs' );
const path = require( 'path' );
const { RawSource, ConcatSource } = require( 'webpack-sources' );
/**
* Serve translations depending on the used translation service and passed options.
* It takes care about whole Webpack compilation process and doesn't contain much logic that should be tested.
*
* See https://webpack.js.org/api/compiler/#event-hooks and https://webpack.js.org/api/compilation/ for details about specific hooks.
*
* @param {Object} compiler The webpack compiler.
* @param {Object} options Translation options.
* @param {String} options.outputDirectory The output directory for the emitted translation files, relative to the webpack context.
* @param {Boolean} [options.strict] An option that make this function throw when the error is found during the compilation.
* @param {Boolean} [options.verbose] An option that make this function log everything into the console.
* @param {String} [options.sourceFilesPattern] The source files pattern
* @param {String} [options.packageNamesPattern] The package names pattern.
* @param {String} [options.corePackagePattern] The core package pattern.
* @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets.
* ckeditor5 - independent without hard-to-test logic.
*/
module.exports = function serveTranslations( compiler, options, translationService ) {
const cwd = process.cwd();
// Provides translateSource function for the `translatesourceloader` loader.
const translateSource = ( source, sourceFile ) => translationService.translateSource( source, sourceFile );
// Watch for warnings and errors during translation process.
translationService.on( 'error', emitError );
translationService.on( 'warning', emitWarning );
// Remove old translation files.
// Assert whether the translation output directory exists inside the cwd.
const pathToLanguages = path.join( compiler.options.output.path, options.outputDirectory );
if ( fs.existsSync( pathToLanguages ) ) {
if ( pathToLanguages.includes( cwd ) && cwd !== pathToLanguages ) {
rimraf.sync( pathToLanguages );
} else {
emitError(
`Can't remove path to translation files directory (${ pathToLanguages }). Assert whether you specified a correct path.`
);
}
}
// Add core translations before `translateSourceLoader` starts translating.
compiler.hooks.normalModuleFactory.tap( 'CKEditor5Plugin', normalModuleFactory => {
const resolver = normalModuleFactory.getResolver( 'normal' );
resolver.resolve( { cwd }, cwd, options.corePackageSampleResourcePath, {}, ( err, pathToResource ) => {
if ( err ) {
console.warn( 'Cannot find the CKEditor 5 core translation package (which defaults to `@ckeditor/ckeditor5-core`).' );
return;
}
const corePackage = pathToResource.match( options.corePackagePattern )[ 0 ];
translationService.loadPackage( corePackage );
} );
// Translations from the core package may not be used in the source code (in *.js files).
// However, in the case of the DLL integration, core translations should be inserted in the bundle file,
// because features that use common identifiers do not provide translation ids by themselves.
if ( options.includeCorePackageTranslations ) {
resolver.resolve( { cwd }, cwd, options.corePackageContextsResourcePath, {}, ( err, pathToResource ) => {
if ( err ) {
console.warn( 'Cannot find the CKEditor 5 core translation context (which defaults to `@ckeditor/ckeditor5-core`).' );
return;
}
// Add all context messages found in the core package.
const contexts = require( pathToResource );
for ( const item of Object.keys( contexts ) ) {
translationService.addIdMessage( item );
}
} );
}
} );
// Load translation files and add a loader if the package match requirements.
compiler.hooks.compilation.tap( 'CKEditor5Plugin', compilation => {
getCompilationHooks( compiler, compilation ).tap( 'CKEditor5Plugin', ( context, module ) => {
const relativePathToResource = path.relative( cwd, module.resource );
if ( relativePathToResource.match( options.sourceFilesPattern ) ) {
// The `TranslateSource` loader must be added as the last one in the loader's chain,
// after any potential TypeScript file has already been compiled.
module.loaders.unshift( {
loader: path.join( __dirname, 'translatesourceloader.js' ),
options: { translateSource }
} );
const pathToPackage = getPathToPackage( cwd, module.resource, options.packageNamesPattern );
translationService.loadPackage( pathToPackage );
}
} );
// At the end of the compilation add assets generated from the PO files.
// Use `optimize-chunk-assets` instead of `emit` to emit assets before the `webpack.BannerPlugin`.
getChunkAssets( compilation ).tap( 'CKEditor5Plugin', chunks => {
const generatedAssets = translationService.getAssets( {
outputDirectory: options.outputDirectory,
compilationAssetNames: Object.keys( compilation.assets )
.filter( name => name.endsWith( '.js' ) )
} );
const allFiles = getFilesFromChunks( chunks );
for ( const asset of generatedAssets ) {
if ( asset.shouldConcat ) {
// Concatenate sources to not break the file's sourcemap.
const originalAsset = compilation.assets[ asset.outputPath ];
compilation.assets[ asset.outputPath ] = new ConcatSource( asset.outputBody, '\n', originalAsset );
} else {
const chunkExists = allFiles.includes( asset.outputPath );
if ( !chunkExists ) {
// Assign `RawSource` when the corresponding chunk does not exist.
compilation.assets[ asset.outputPath ] = new RawSource( asset.outputBody );
} else {
// Assign a string when the corresponding chunk exists and maintains the proper sourcemap.
// Changing it to RawSource would break sourcemaps.
compilation.assets[ asset.outputPath ] = asset.outputBody;
}
}
}
} );
} );
// A set of unique messages that prevents message duplications.
const uniqueMessages = new Set();
function emitError( error ) {
if ( uniqueMessages.has( error ) ) {
return;
}
uniqueMessages.add( error );
if ( options.strict ) {
throw new Error( chalk.red( error ) );
}
console.error( chalk.red( `[CKEditorWebpackPlugin] Error: ${ error }` ) );
}
function emitWarning( warning ) {
if ( uniqueMessages.has( warning ) ) {
return;
}
uniqueMessages.add( warning );
if ( options.verbose ) {
console.warn( chalk.yellow( `[CKEditorWebpackPlugin] Warning: ${ warning }` ) );
}
}
};
/**
* Return path to the package if the resource comes from `ckeditor5-*` package.
*
* @param {String} cwd Current working directory.
* @param {String} resource Absolute path to the resource.
* @returns {String|null}
*/
function getPathToPackage( cwd, resource, packageNamePattern ) {
const relativePathToResource = path.relative( cwd, resource );
const match = relativePathToResource.match( packageNamePattern );
if ( !match ) {
return null;
}
const index = relativePathToResource.search( packageNamePattern ) + match[ 0 ].length;
return relativePathToResource.slice( 0, index );
}
/**
* Returns an object with the compilation hooks depending on the webpack version.
*
* @param {webpack.Compiler} compiler
* @param {webpack.Compilation} compilation
* @returns {Object}
*/
function getCompilationHooks( compiler, compilation ) {
const { webpack } = compiler;
// Webpack is not available in a compiler instance (webpack 4).
if ( !webpack ) {
return compilation.hooks.normalModuleLoader;
}
// Do not import the `NormalModule` class directly. Find it in the current instance of webpack process.
// See: https://github.com/ckeditor/ckeditor5/issues/12887.
return webpack.NormalModule.getCompilationHooks( compilation ).loader;
}
/**
* Returns an object with the chunk assets depending on the Webpack version.
*
* @param {Object} compilation
* @returns {Object}
*/
function getChunkAssets( compilation ) {
// Webpack 5 vs Webpack 4.
return compilation.hooks.processAssets || compilation.hooks.optimizeChunkAssets;
}
/**
* Returns an array with list of loaded files depending on the Webpack version.
*
* @param {Object|Array} chunks
* @returns {Array}
*/
function getFilesFromChunks( chunks ) {
// Webpack 4.
if ( Array.isArray( chunks ) ) {
return chunks.reduce( ( acc, chunk ) => [ ...acc, ...chunk.files ], [] );
}
// Webpack 5.
return Object.keys( chunks );
}
/**
* TranslationService interface.
*
* It should extend or mix NodeJS' EventEmitter to provide `on()` method.
*
* @interface TranslationService
*/
/**
* Load package translations.
*
* @method #loadPackage
* @param {String} pathToPackage Path to the package.
*/
/**
* Translate file's source to the target language.
*
* @method #translateSource
* @param {String} source File's source.
* @returns {String}
*/
/**
* Get assets at the end of compilation.
*
* @method #getAssets
* @returns {Array.<Object>}
*/
/**
* Error found during the translation process.
*
* @fires error
*/