UNPKG

9.04 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md.
4 */
5
6'use strict';
7
8const chalk = require( 'chalk' );
9const rimraf = require( 'rimraf' );
10const fs = require( 'fs' );
11const path = require( 'path' );
12const semver = require( 'semver' );
13const { NormalModule } = require( 'webpack' );
14const { version: webpackVersion } = require( 'webpack/package.json' );
15const { RawSource, ConcatSource } = require( 'webpack-sources' );
16
17/**
18 * Serve translations depending on the used translation service and passed options.
19 * It takes care about whole Webpack compilation process and doesn't contain much logic that should be tested.
20 *
21 * See https://webpack.js.org/api/compiler/#event-hooks and https://webpack.js.org/api/compilation/ for details about specific hooks.
22 *
23 * @param {Object} compiler The webpack compiler.
24 * @param {Object} options Translation options.
25 * @param {String} options.outputDirectory The output directory for the emitted translation files, relative to the webpack context.
26 * @param {Boolean} [options.strict] An option that make this function throw when the error is found during the compilation.
27 * @param {Boolean} [options.verbose] An option that make this function log everything into the console.
28 * @param {String} [options.sourceFilesPattern] The source files pattern
29 * @param {String} [options.packageNamesPattern] The package names pattern.
30 * @param {String} [options.corePackagePattern] The core package pattern.
31 * @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets.
32 * ckeditor5 - independent without hard-to-test logic.
33 */
34module.exports = function serveTranslations( compiler, options, translationService ) {
35 const cwd = process.cwd();
36
37 // Provides translateSource function for the `translatesourceloader` loader.
38 const translateSource = ( source, sourceFile ) => translationService.translateSource( source, sourceFile );
39
40 // Watch for warnings and errors during translation process.
41 translationService.on( 'error', emitError );
42 translationService.on( 'warning', emitWarning );
43
44 // Remove old translation files.
45 // Assert whether the translation output directory exists inside the cwd.
46 const pathToLanguages = path.join( compiler.options.output.path, options.outputDirectory );
47
48 if ( fs.existsSync( pathToLanguages ) ) {
49 if ( pathToLanguages.includes( cwd ) && cwd !== pathToLanguages ) {
50 rimraf.sync( pathToLanguages );
51 } else {
52 emitError(
53 `Can't remove path to translation files directory (${ pathToLanguages }). Assert whether you specified a correct path.`
54 );
55 }
56 }
57
58 // Add core translations before `translateSourceLoader` starts translating.
59 compiler.hooks.normalModuleFactory.tap( 'CKEditor5Plugin', normalModuleFactory => {
60 const resolver = normalModuleFactory.getResolver( 'normal' );
61
62 resolver.resolve( { cwd }, cwd, options.corePackageSampleResourcePath, {}, ( err, pathToResource ) => {
63 if ( err ) {
64 console.warn( 'Cannot find the CKEditor 5 core translation package (which defaults to `@ckeditor/ckeditor5-core`).' );
65
66 return;
67 }
68
69 const corePackage = pathToResource.match( options.corePackagePattern )[ 0 ];
70
71 translationService.loadPackage( corePackage );
72 } );
73
74 // Translations from the core package may not be used in the source code (in *.js files).
75 // However, in the case of the DLL integration, core translations should be inserted in the bundle file,
76 // because features that use common identifiers do not provide translation ids by themselves.
77 if ( options.includeCorePackageTranslations ) {
78 resolver.resolve( { cwd }, cwd, options.corePackageContextsResourcePath, {}, ( err, pathToResource ) => {
79 if ( err ) {
80 console.warn( 'Cannot find the CKEditor 5 core translation context (which defaults to `@ckeditor/ckeditor5-core`).' );
81
82 return;
83 }
84
85 // Add all context messages found in the core package.
86 const contexts = require( pathToResource );
87
88 for ( const item of Object.keys( contexts ) ) {
89 translationService.addIdMessage( item );
90 }
91 } );
92 }
93 } );
94
95 // Load translation files and add a loader if the package match requirements.
96 compiler.hooks.compilation.tap( 'CKEditor5Plugin', compilation => {
97 getCompilationHooks( compilation ).tap( 'CKEditor5Plugin', ( context, module ) => {
98 const relativePathToResource = path.relative( cwd, module.resource );
99
100 if ( relativePathToResource.match( options.sourceFilesPattern ) ) {
101 module.loaders.push( {
102 loader: path.join( __dirname, 'translatesourceloader.js' ),
103 options: { translateSource }
104 } );
105
106 const pathToPackage = getPathToPackage( cwd, module.resource, options.packageNamesPattern );
107
108 translationService.loadPackage( pathToPackage );
109 }
110 } );
111
112 // At the end of the compilation add assets generated from the PO files.
113 // Use `optimize-chunk-assets` instead of `emit` to emit assets before the `webpack.BannerPlugin`.
114 getChunkAssets( compilation ).tap( 'CKEditor5Plugin', chunks => {
115 const generatedAssets = translationService.getAssets( {
116 outputDirectory: options.outputDirectory,
117 compilationAssetNames: Object.keys( compilation.assets )
118 .filter( name => name.endsWith( '.js' ) )
119 } );
120
121 const allFiles = getFilesFromChunks( chunks );
122
123 for ( const asset of generatedAssets ) {
124 if ( asset.shouldConcat ) {
125 // Concatenate sources to not break the file's sourcemap.
126 const originalAsset = compilation.assets[ asset.outputPath ];
127
128 compilation.assets[ asset.outputPath ] = new ConcatSource( asset.outputBody, '\n', originalAsset );
129 } else {
130 const chunkExists = allFiles.includes( asset.outputPath );
131
132 if ( !chunkExists ) {
133 // Assign `RawSource` when the corresponding chunk does not exist.
134 compilation.assets[ asset.outputPath ] = new RawSource( asset.outputBody );
135 } else {
136 // Assign a string when the corresponding chunk exists and maintains the proper sourcemap.
137 // Changing it to RawSource would break sourcemaps.
138 compilation.assets[ asset.outputPath ] = asset.outputBody;
139 }
140 }
141 }
142 } );
143 } );
144
145 // A set of unique messages that prevents message duplications.
146 const uniqueMessages = new Set();
147
148 function emitError( error ) {
149 if ( uniqueMessages.has( error ) ) {
150 return;
151 }
152
153 uniqueMessages.add( error );
154
155 if ( options.strict ) {
156 throw new Error( chalk.red( error ) );
157 }
158
159 console.error( chalk.red( `[CKEditorWebpackPlugin] Error: ${ error }` ) );
160 }
161
162 function emitWarning( warning ) {
163 if ( uniqueMessages.has( warning ) ) {
164 return;
165 }
166
167 uniqueMessages.add( warning );
168
169 if ( options.verbose ) {
170 console.warn( chalk.yellow( `[CKEditorWebpackPlugin] Warning: ${ warning }` ) );
171 }
172 }
173};
174
175/**
176 * Return path to the package if the resource comes from `ckeditor5-*` package.
177 *
178 * @param {String} cwd Current working directory.
179 * @param {String} resource Absolute path to the resource.
180 * @returns {String|null}
181 */
182function getPathToPackage( cwd, resource, packageNamePattern ) {
183 const relativePathToResource = path.relative( cwd, resource );
184
185 const match = relativePathToResource.match( packageNamePattern );
186
187 if ( !match ) {
188 return null;
189 }
190
191 const index = relativePathToResource.search( packageNamePattern ) + match[ 0 ].length;
192
193 return relativePathToResource.slice( 0, index );
194}
195
196/**
197 * Returns an object with the compilation hooks depending on the Webpack version.
198 *
199 * @param {Object} compilation
200 * @returns {Object}
201 */
202function getCompilationHooks( compilation ) {
203 if ( semver.major( webpackVersion ) === 4 ) {
204 return compilation.hooks.normalModuleLoader;
205 }
206
207 return NormalModule.getCompilationHooks( compilation ).loader;
208}
209
210/**
211 * Returns an object with the chunk assets depending on the Webpack version.
212 *
213 * @param {Object} compilation
214 * @returns {Object}
215 */
216function getChunkAssets( compilation ) {
217 // Webpack 5 vs Webpack 4.
218 return compilation.hooks.processAssets || compilation.hooks.optimizeChunkAssets;
219}
220
221/**
222 * Returns an array with list of loaded files depending on the Webpack version.
223 *
224 * @param {Object|Array} chunks
225 * @returns {Array}
226 */
227function getFilesFromChunks( chunks ) {
228 // Webpack 4.
229 if ( Array.isArray( chunks ) ) {
230 return chunks.reduce( ( acc, chunk ) => [ ...acc, ...chunk.files ], [] );
231 }
232
233 // Webpack 5.
234 return Object.keys( chunks );
235}
236
237/**
238 * TranslationService interface.
239 *
240 * It should extend or mix NodeJS' EventEmitter to provide `on()` method.
241 *
242 * @interface TranslationService
243 */
244
245/**
246 * Load package translations.
247 *
248 * @method #loadPackage
249 * @param {String} pathToPackage Path to the package.
250 */
251
252/**
253 * Translate file's source to the target language.
254 *
255 * @method #translateSource
256 * @param {String} source File's source.
257 * @returns {String}
258 */
259
260/**
261 * Get assets at the end of compilation.
262 *
263 * @method #getAssets
264 * @returns {Array.<Object>}
265 */
266
267/**
268 * Error found during the translation process.
269 *
270 * @fires error
271 */