1 | /**
|
2 | * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
3 | * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
4 | */
|
5 |
|
6 | /**
|
7 | * @module core/plugincollection
|
8 | */
|
9 |
|
10 | import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
11 | import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
|
12 | import mix from '@ckeditor/ckeditor5-utils/src/mix';
|
13 |
|
14 | /**
|
15 | * Manages a list of CKEditor plugins, including loading, resolving dependencies and initialization.
|
16 | *
|
17 | * @mixes module:utils/emittermixin~EmitterMixin
|
18 | */
|
19 | export default class PluginCollection {
|
20 | /**
|
21 | * Creates an instance of the plugin collection class.
|
22 | * Allows loading and initializing plugins and their dependencies.
|
23 | * Allows providing a list of already loaded plugins. These plugins will not be destroyed along with this collection.
|
24 | *
|
25 | * @param {module:core/editor/editor~Editor|module:core/context~Context} context
|
26 | * @param {Array.<Function>} [availablePlugins] Plugins (constructors) which the collection will be able to use
|
27 | * when {@link module:core/plugincollection~PluginCollection#init} is used with the plugin names (strings, instead of constructors).
|
28 | * Usually, the editor will pass its built-in plugins to the collection so they can later be
|
29 | * used in `config.plugins` or `config.removePlugins` by names.
|
30 | * @param {Iterable.<Array>} contextPlugins A list of already initialized plugins represented by a
|
31 | * `[ PluginConstructor, pluginInstance ]` pair.
|
32 | */
|
33 | constructor( context, availablePlugins = [], contextPlugins = [] ) {
|
34 | /**
|
35 | * @protected
|
36 | * @type {module:core/editor/editor~Editor|module:core/context~Context}
|
37 | */
|
38 | this._context = context;
|
39 |
|
40 | /**
|
41 | * @protected
|
42 | * @type {Map}
|
43 | */
|
44 | this._plugins = new Map();
|
45 |
|
46 | /**
|
47 | * A map of plugin constructors that can be retrieved by their names.
|
48 | *
|
49 | * @protected
|
50 | * @type {Map.<String|Function,Function>}
|
51 | */
|
52 | this._availablePlugins = new Map();
|
53 |
|
54 | for ( const PluginConstructor of availablePlugins ) {
|
55 | if ( PluginConstructor.pluginName ) {
|
56 | this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
|
57 | }
|
58 | }
|
59 |
|
60 | /**
|
61 | * Map of {@link module:core/contextplugin~ContextPlugin context plugins} which can be retrieved by their constructors or instances.
|
62 | *
|
63 | * @protected
|
64 | * @type {Map<Function,Function>}
|
65 | */
|
66 | this._contextPlugins = new Map();
|
67 |
|
68 | for ( const [ PluginConstructor, pluginInstance ] of contextPlugins ) {
|
69 | this._contextPlugins.set( PluginConstructor, pluginInstance );
|
70 | this._contextPlugins.set( pluginInstance, PluginConstructor );
|
71 |
|
72 | // To make it possible to require a plugin by its name.
|
73 | if ( PluginConstructor.pluginName ) {
|
74 | this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
|
75 | }
|
76 | }
|
77 | }
|
78 |
|
79 | /**
|
80 | * Iterable interface.
|
81 | *
|
82 | * Returns `[ PluginConstructor, pluginInstance ]` pairs.
|
83 | *
|
84 | * @returns {Iterable.<Array>}
|
85 | */
|
86 | * [ Symbol.iterator ]() {
|
87 | for ( const entry of this._plugins ) {
|
88 | if ( typeof entry[ 0 ] == 'function' ) {
|
89 | yield entry;
|
90 | }
|
91 | }
|
92 | }
|
93 |
|
94 | /**
|
95 | * Gets the plugin instance by its constructor or name.
|
96 | *
|
97 | * // Check if 'Clipboard' plugin was loaded.
|
98 | * if ( editor.plugins.has( 'ClipboardPipeline' ) ) {
|
99 | * // Get clipboard plugin instance
|
100 | * const clipboard = editor.plugins.get( 'ClipboardPipeline' );
|
101 | *
|
102 | * this.listenTo( clipboard, 'inputTransformation', ( evt, data ) => {
|
103 | * // Do something on clipboard input.
|
104 | * } );
|
105 | * }
|
106 | *
|
107 | * **Note**: This method will throw an error if a plugin is not loaded. Use `{@link #has editor.plugins.has()}`
|
108 | * to check if a plugin is available.
|
109 | *
|
110 | * @param {Function|String} key The plugin constructor or {@link module:core/plugin~PluginInterface.pluginName name}.
|
111 | * @returns {module:core/plugin~PluginInterface}
|
112 | */
|
113 | get( key ) {
|
114 | const plugin = this._plugins.get( key );
|
115 |
|
116 | if ( !plugin ) {
|
117 | let pluginName = key;
|
118 |
|
119 | if ( typeof key == 'function' ) {
|
120 | pluginName = key.pluginName || key.name;
|
121 | }
|
122 |
|
123 | /**
|
124 | * The plugin is not loaded and could not be obtained.
|
125 | *
|
126 | * Plugin classes (constructors) need to be provided to the editor and must be loaded before they can be obtained from
|
127 | * the plugin collection.
|
128 | * This is usually done in CKEditor 5 builds by setting the {@link module:core/editor/editor~Editor.builtinPlugins}
|
129 | * property.
|
130 | *
|
131 | * **Note**: You can use `{@link module:core/plugincollection~PluginCollection#has editor.plugins.has()}`
|
132 | * to check if a plugin was loaded.
|
133 | *
|
134 | * @error plugincollection-plugin-not-loaded
|
135 | * @param {String} plugin The name of the plugin which is not loaded.
|
136 | */
|
137 | throw new CKEditorError( 'plugincollection-plugin-not-loaded', this._context, { plugin: pluginName } );
|
138 | }
|
139 |
|
140 | return plugin;
|
141 | }
|
142 |
|
143 | /**
|
144 | * Checks if a plugin is loaded.
|
145 | *
|
146 | * // Check if the 'Clipboard' plugin was loaded.
|
147 | * if ( editor.plugins.has( 'ClipboardPipeline' ) ) {
|
148 | * // Now use the clipboard plugin instance:
|
149 | * const clipboard = editor.plugins.get( 'ClipboardPipeline' );
|
150 | *
|
151 | * // ...
|
152 | * }
|
153 | *
|
154 | * @param {Function|String} key The plugin constructor or {@link module:core/plugin~PluginInterface.pluginName name}.
|
155 | * @returns {Boolean}
|
156 | */
|
157 | has( key ) {
|
158 | return this._plugins.has( key );
|
159 | }
|
160 |
|
161 | /**
|
162 | * Initializes a set of plugins and adds them to the collection.
|
163 | *
|
164 | * @param {Array.<Function|String>} plugins An array of {@link module:core/plugin~PluginInterface plugin constructors}
|
165 | * or {@link module:core/plugin~PluginInterface.pluginName plugin names}.
|
166 | * @param {Array.<String|Function>} [pluginsToRemove] Names of the plugins or plugin constructors
|
167 | * that should not be loaded (despite being specified in the `plugins` array).
|
168 | * @param {Array.<Function>} [pluginsSubstitutions] An array of {@link module:core/plugin~PluginInterface plugin constructors}
|
169 | * that will be used to replace plugins of the same names that were passed in `plugins` or that are in their dependency tree.
|
170 | * A useful option for replacing built-in plugins while creating tests (for mocking their APIs). Plugins that will be replaced
|
171 | * must follow these rules:
|
172 | * * The new plugin must be a class.
|
173 | * * The new plugin must be named.
|
174 | * * Both plugins must not depend on other plugins.
|
175 | * @returns {Promise.<module:core/plugin~LoadedPlugins>} A promise which gets resolved once all plugins are loaded
|
176 | * and available in the collection.
|
177 | */
|
178 | init( plugins, pluginsToRemove = [], pluginsSubstitutions = [] ) {
|
179 | // Plugin initialization procedure consists of 2 main steps:
|
180 | // 1) collecting all available plugin constructors,
|
181 | // 2) verification whether all required plugins can be instantiated.
|
182 | //
|
183 | // In the first step, all plugin constructors, available in the provided `plugins` array and inside
|
184 | // plugin's dependencies (from the `Plugin.requires` array), are recursively collected and added to the existing
|
185 | // `this._availablePlugins` map, but without any verification at the given moment. Performing the verification
|
186 | // at this point (during the plugin constructor searching) would cause false errors to occur, that some plugin
|
187 | // is missing but in fact it may be defined further in the array as the dependency of other plugin. After
|
188 | // traversing the entire dependency tree, it will be checked if all required "top level" plugins are available.
|
189 | //
|
190 | // In the second step, the list of plugins that have not been explicitly removed is traversed to get all the
|
191 | // plugin constructors to be instantiated in the correct order and to validate against some rules. Finally, if
|
192 | // no plugin is missing and no other error has been found, they all will be instantiated.
|
193 | const that = this;
|
194 | const context = this._context;
|
195 |
|
196 | findAvailablePluginConstructors( plugins );
|
197 |
|
198 | validatePlugins( plugins );
|
199 |
|
200 | const pluginsToLoad = plugins.filter( plugin => !isPluginRemoved( plugin, pluginsToRemove ) );
|
201 |
|
202 | const pluginConstructors = [ ...getPluginConstructors( pluginsToLoad ) ];
|
203 |
|
204 | substitutePlugins( pluginConstructors, pluginsSubstitutions );
|
205 |
|
206 | const pluginInstances = loadPlugins( pluginConstructors );
|
207 |
|
208 | return initPlugins( pluginInstances, 'init' )
|
209 | .then( () => initPlugins( pluginInstances, 'afterInit' ) )
|
210 | .then( () => pluginInstances );
|
211 |
|
212 | function isPluginConstructor( plugin ) {
|
213 | return typeof plugin === 'function';
|
214 | }
|
215 |
|
216 | function isContextPlugin( plugin ) {
|
217 | return isPluginConstructor( plugin ) && plugin.isContextPlugin;
|
218 | }
|
219 |
|
220 | function isPluginRemoved( plugin, pluginsToRemove ) {
|
221 | return pluginsToRemove.some( removedPlugin => {
|
222 | if ( removedPlugin === plugin ) {
|
223 | return true;
|
224 | }
|
225 |
|
226 | if ( getPluginName( plugin ) === removedPlugin ) {
|
227 | return true;
|
228 | }
|
229 |
|
230 | if ( getPluginName( removedPlugin ) === plugin ) {
|
231 | return true;
|
232 | }
|
233 |
|
234 | return false;
|
235 | } );
|
236 | }
|
237 |
|
238 | function getPluginName( plugin ) {
|
239 | return isPluginConstructor( plugin ) ?
|
240 | plugin.pluginName || plugin.name :
|
241 | plugin;
|
242 | }
|
243 |
|
244 | function findAvailablePluginConstructors( plugins, processed = new Set() ) {
|
245 | plugins.forEach( plugin => {
|
246 | if ( !isPluginConstructor( plugin ) ) {
|
247 | return;
|
248 | }
|
249 |
|
250 | if ( processed.has( plugin ) ) {
|
251 | return;
|
252 | }
|
253 |
|
254 | processed.add( plugin );
|
255 |
|
256 | if ( plugin.pluginName && !that._availablePlugins.has( plugin.pluginName ) ) {
|
257 | that._availablePlugins.set( plugin.pluginName, plugin );
|
258 | }
|
259 |
|
260 | if ( plugin.requires ) {
|
261 | findAvailablePluginConstructors( plugin.requires, processed );
|
262 | }
|
263 | } );
|
264 | }
|
265 |
|
266 | function getPluginConstructors( plugins, processed = new Set() ) {
|
267 | return plugins
|
268 | .map( plugin => {
|
269 | return isPluginConstructor( plugin ) ?
|
270 | plugin :
|
271 | that._availablePlugins.get( plugin );
|
272 | } )
|
273 | .reduce( ( result, plugin ) => {
|
274 | if ( processed.has( plugin ) ) {
|
275 | return result;
|
276 | }
|
277 |
|
278 | processed.add( plugin );
|
279 |
|
280 | if ( plugin.requires ) {
|
281 | validatePlugins( plugin.requires, plugin );
|
282 |
|
283 | getPluginConstructors( plugin.requires, processed ).forEach( plugin => result.add( plugin ) );
|
284 | }
|
285 |
|
286 | return result.add( plugin );
|
287 | }, new Set() );
|
288 | }
|
289 |
|
290 | function validatePlugins( plugins, parentPluginConstructor = null ) {
|
291 | plugins
|
292 | .map( plugin => {
|
293 | return isPluginConstructor( plugin ) ?
|
294 | plugin :
|
295 | that._availablePlugins.get( plugin ) || plugin;
|
296 | } )
|
297 | .forEach( plugin => {
|
298 | checkMissingPlugin( plugin, parentPluginConstructor );
|
299 | checkContextPlugin( plugin, parentPluginConstructor );
|
300 | checkRemovedPlugin( plugin, parentPluginConstructor );
|
301 | } );
|
302 | }
|
303 |
|
304 | function checkMissingPlugin( plugin, parentPluginConstructor ) {
|
305 | if ( isPluginConstructor( plugin ) ) {
|
306 | return;
|
307 | }
|
308 |
|
309 | if ( parentPluginConstructor ) {
|
310 | /**
|
311 | * A required "soft" dependency was not found on the plugin list.
|
312 | *
|
313 | * When configuring the editor, either prior to building (via
|
314 | * {@link module:core/editor/editor~Editor.builtinPlugins `Editor.builtinPlugins`}) or when
|
315 | * creating a new instance of the editor (e.g. via
|
316 | * {@link module:core/editor/editorconfig~EditorConfig#plugins `config.plugins`}), you need to provide
|
317 | * some of the dependencies for other plugins that you used.
|
318 | *
|
319 | * This error is thrown when one of these dependencies was not provided. The name of the missing plugin
|
320 | * can be found in `missingPlugin` and the plugin that required it in `requiredBy`.
|
321 | *
|
322 | * In order to resolve it, you need to import the missing plugin and add it to the
|
323 | * current list of plugins (`Editor.builtinPlugins` or `config.plugins`/`config.extraPlugins`).
|
324 | *
|
325 | * Soft requirements were introduced in version 26.0.0. If you happen to stumble upon this error
|
326 | * when upgrading to version 26.0.0, read also the
|
327 | * {@glink updating/migration-to-26 Migration to 26.0.0} guide.
|
328 | *
|
329 | * @error plugincollection-soft-required
|
330 | * @param {String} missingPlugin The name of the required plugin.
|
331 | * @param {String} requiredBy The name of the plugin that requires the other plugin.
|
332 | */
|
333 | throw new CKEditorError(
|
334 | 'plugincollection-soft-required',
|
335 | context,
|
336 | { missingPlugin: plugin, requiredBy: getPluginName( parentPluginConstructor ) }
|
337 | );
|
338 | }
|
339 |
|
340 | /**
|
341 | * A plugin is not available and could not be loaded.
|
342 | *
|
343 | * Plugin classes (constructors) need to be provided to the editor before they can be loaded by name.
|
344 | * This is usually done in CKEditor 5 builds by setting the {@link module:core/editor/editor~Editor.builtinPlugins}
|
345 | * property.
|
346 | *
|
347 | * **If you see this warning when using one of the {@glink installation/getting-started/predefined-builds
|
348 | * CKEditor 5 Builds}**,
|
349 | * it means that you try to enable a plugin which was not included in that build. This may be due to a typo
|
350 | * in the plugin name or simply because that plugin is not a part of this build. In the latter scenario,
|
351 | * read more about {@glink installation/getting-started/quick-start custom builds}.
|
352 | *
|
353 | * **If you see this warning when using one of the editor creators directly** (not a build), then it means
|
354 | * that you tried loading plugins by name. However, unlike CKEditor 4, CKEditor 5 does not implement a "plugin loader".
|
355 | * This means that CKEditor 5 does not know where to load the plugin modules from. Therefore, you need to
|
356 | * provide each plugin through a reference (as a constructor function). Check out the examples in
|
357 | * {@glink installation/advanced/alternative-setups/integrating-from-source "Building from source"}.
|
358 | *
|
359 | * @error plugincollection-plugin-not-found
|
360 | * @param {String} plugin The name of the plugin which could not be loaded.
|
361 | */
|
362 | throw new CKEditorError(
|
363 | 'plugincollection-plugin-not-found',
|
364 | context,
|
365 | { plugin }
|
366 | );
|
367 | }
|
368 |
|
369 | function checkContextPlugin( plugin, parentPluginConstructor ) {
|
370 | if ( !isContextPlugin( parentPluginConstructor ) ) {
|
371 | return;
|
372 | }
|
373 |
|
374 | if ( isContextPlugin( plugin ) ) {
|
375 | return;
|
376 | }
|
377 |
|
378 | /**
|
379 | * If a plugin is a context plugin, all plugins it requires should also be context plugins
|
380 | * instead of plugins. In other words, if one plugin can be used in the context,
|
381 | * all its requirements should also be ready to be used in the context. Note that the context
|
382 | * provides only a part of the API provided by the editor. If one plugin needs a full
|
383 | * editor API, all plugins which require it are considered as plugins that need a full
|
384 | * editor API.
|
385 | *
|
386 | * @error plugincollection-context-required
|
387 | * @param {String} plugin The name of the required plugin.
|
388 | * @param {String} requiredBy The name of the parent plugin.
|
389 | */
|
390 | throw new CKEditorError(
|
391 | 'plugincollection-context-required',
|
392 | context,
|
393 | { plugin: getPluginName( plugin ), requiredBy: getPluginName( parentPluginConstructor ) }
|
394 | );
|
395 | }
|
396 |
|
397 | function checkRemovedPlugin( plugin, parentPluginConstructor ) {
|
398 | if ( !parentPluginConstructor ) {
|
399 | return;
|
400 | }
|
401 |
|
402 | if ( !isPluginRemoved( plugin, pluginsToRemove ) ) {
|
403 | return;
|
404 | }
|
405 |
|
406 | /**
|
407 | * Cannot load a plugin because one of its dependencies is listed in the `removePlugins` option.
|
408 | *
|
409 | * @error plugincollection-required
|
410 | * @param {String} plugin The name of the required plugin.
|
411 | * @param {String} requiredBy The name of the parent plugin.
|
412 | */
|
413 | throw new CKEditorError(
|
414 | 'plugincollection-required',
|
415 | context,
|
416 | { plugin: getPluginName( plugin ), requiredBy: getPluginName( parentPluginConstructor ) }
|
417 | );
|
418 | }
|
419 |
|
420 | function loadPlugins( pluginConstructors ) {
|
421 | return pluginConstructors.map( PluginConstructor => {
|
422 | const pluginInstance = that._contextPlugins.get( PluginConstructor ) || new PluginConstructor( context );
|
423 |
|
424 | that._add( PluginConstructor, pluginInstance );
|
425 |
|
426 | return pluginInstance;
|
427 | } );
|
428 | }
|
429 |
|
430 | function initPlugins( pluginInstances, method ) {
|
431 | return pluginInstances.reduce( ( promise, plugin ) => {
|
432 | if ( !plugin[ method ] ) {
|
433 | return promise;
|
434 | }
|
435 |
|
436 | if ( that._contextPlugins.has( plugin ) ) {
|
437 | return promise;
|
438 | }
|
439 |
|
440 | return promise.then( plugin[ method ].bind( plugin ) );
|
441 | }, Promise.resolve() );
|
442 | }
|
443 |
|
444 | // Replaces plugin constructors with the specified set of plugins.
|
445 | //
|
446 | // @param {Array.<Function>} pluginConstructors
|
447 | // @param {Array.<Function>} pluginsSubstitutions
|
448 | function substitutePlugins( pluginConstructors, pluginsSubstitutions ) {
|
449 | for ( const pluginItem of pluginsSubstitutions ) {
|
450 | if ( typeof pluginItem != 'function' ) {
|
451 | /**
|
452 | * The plugin replacing an existing plugin must be a function.
|
453 | *
|
454 | * @error plugincollection-replace-plugin-invalid-type
|
455 | */
|
456 | throw new CKEditorError( 'plugincollection-replace-plugin-invalid-type', null, { pluginItem } );
|
457 | }
|
458 | const pluginName = pluginItem.pluginName;
|
459 |
|
460 | if ( !pluginName ) {
|
461 | /**
|
462 | * The plugin replacing an existing plugin must have a name.
|
463 | *
|
464 | * @error plugincollection-replace-plugin-missing-name
|
465 | */
|
466 | throw new CKEditorError( 'plugincollection-replace-plugin-missing-name', null, { pluginItem } );
|
467 | }
|
468 |
|
469 | if ( pluginItem.requires && pluginItem.requires.length ) {
|
470 | /**
|
471 | * The plugin replacing an existing plugin cannot depend on other plugins.
|
472 | *
|
473 | * @error plugincollection-plugin-for-replacing-cannot-have-dependencies
|
474 | */
|
475 | throw new CKEditorError( 'plugincollection-plugin-for-replacing-cannot-have-dependencies', null, { pluginName } );
|
476 | }
|
477 |
|
478 | const pluginToReplace = that._availablePlugins.get( pluginName );
|
479 |
|
480 | if ( !pluginToReplace ) {
|
481 | /**
|
482 | * The replaced plugin does not exist in the
|
483 | * {@link module:core/plugincollection~PluginCollection available plugins} collection.
|
484 | *
|
485 | * @error plugincollection-plugin-for-replacing-not-exist
|
486 | */
|
487 | throw new CKEditorError( 'plugincollection-plugin-for-replacing-not-exist', null, { pluginName } );
|
488 | }
|
489 |
|
490 | const indexInPluginConstructors = pluginConstructors.indexOf( pluginToReplace );
|
491 |
|
492 | if ( indexInPluginConstructors === -1 ) {
|
493 | // The Context feature can substitute plugins as well.
|
494 | // It may happen that the editor will be created with the given context, where the plugin for substitute
|
495 | // was already replaced. In such a case, we don't want to do it again.
|
496 | if ( that._contextPlugins.has( pluginToReplace ) ) {
|
497 | return;
|
498 | }
|
499 |
|
500 | /**
|
501 | * The replaced plugin will not be loaded so it cannot be replaced.
|
502 | *
|
503 | * @error plugincollection-plugin-for-replacing-not-loaded
|
504 | */
|
505 | throw new CKEditorError( 'plugincollection-plugin-for-replacing-not-loaded', null, { pluginName } );
|
506 | }
|
507 |
|
508 | if ( pluginToReplace.requires && pluginToReplace.requires.length ) {
|
509 | /**
|
510 | * The replaced plugin cannot depend on other plugins.
|
511 | *
|
512 | * @error plugincollection-replaced-plugin-cannot-have-dependencies
|
513 | */
|
514 | throw new CKEditorError( 'plugincollection-replaced-plugin-cannot-have-dependencies', null, { pluginName } );
|
515 | }
|
516 |
|
517 | pluginConstructors.splice( indexInPluginConstructors, 1, pluginItem );
|
518 | that._availablePlugins.set( pluginName, pluginItem );
|
519 | }
|
520 | }
|
521 | }
|
522 |
|
523 | /**
|
524 | * Destroys all loaded plugins.
|
525 | *
|
526 | * @returns {Promise}
|
527 | */
|
528 | destroy() {
|
529 | const promises = [];
|
530 |
|
531 | for ( const [ , pluginInstance ] of this ) {
|
532 | if ( typeof pluginInstance.destroy == 'function' && !this._contextPlugins.has( pluginInstance ) ) {
|
533 | promises.push( pluginInstance.destroy() );
|
534 | }
|
535 | }
|
536 |
|
537 | return Promise.all( promises );
|
538 | }
|
539 |
|
540 | /**
|
541 | * Adds the plugin to the collection. Exposed mainly for testing purposes.
|
542 | *
|
543 | * @protected
|
544 | * @param {Function} PluginConstructor The plugin constructor.
|
545 | * @param {module:core/plugin~PluginInterface} plugin The instance of the plugin.
|
546 | */
|
547 | _add( PluginConstructor, plugin ) {
|
548 | this._plugins.set( PluginConstructor, plugin );
|
549 |
|
550 | const pluginName = PluginConstructor.pluginName;
|
551 |
|
552 | if ( !pluginName ) {
|
553 | return;
|
554 | }
|
555 |
|
556 | if ( this._plugins.has( pluginName ) ) {
|
557 | /**
|
558 | * Two plugins with the same {@link module:core/plugin~PluginInterface.pluginName} were loaded.
|
559 | * This will lead to runtime conflicts between these plugins.
|
560 | *
|
561 | * In practice, this warning usually means that new plugins were added to an existing CKEditor 5 build.
|
562 | * Plugins should always be added to a source version of the editor (`@ckeditor/ckeditor5-editor-*`),
|
563 | * not to an editor imported from one of the `@ckeditor/ckeditor5-build-*` packages.
|
564 | *
|
565 | * Check your import paths and the list of plugins passed to
|
566 | * {@link module:core/editor/editor~Editor.create `Editor.create()`}
|
567 | * or specified in {@link module:core/editor/editor~Editor.builtinPlugins `Editor.builtinPlugins`}.
|
568 | *
|
569 | * The second option is that your `node_modules/` directory contains duplicated versions of the same
|
570 | * CKEditor 5 packages. Normally, on clean installations, npm deduplicates packages in `node_modules/`, so
|
571 | * it may be enough to call `rm -rf node_modules && npm i`. However, if you installed conflicting versions
|
572 | * of some packages, their dependencies may need to be installed in more than one version which may lead to this
|
573 | * warning.
|
574 | *
|
575 | * Technically speaking, this error occurs because after adding a plugin to an existing editor build
|
576 | * the dependencies of this plugin are being duplicated.
|
577 | * They are already built into that editor build and now get added for the second time as dependencies
|
578 | * of the plugin you are installing.
|
579 | *
|
580 | * Read more about {@glink installation/getting-started/installing-plugins installing plugins}.
|
581 | *
|
582 | * @error plugincollection-plugin-name-conflict
|
583 | * @param {String} pluginName The duplicated plugin name.
|
584 | * @param {Function} plugin1 The first plugin constructor.
|
585 | * @param {Function} plugin2 The second plugin constructor.
|
586 | */
|
587 | throw new CKEditorError(
|
588 | 'plugincollection-plugin-name-conflict',
|
589 | null,
|
590 | { pluginName, plugin1: this._plugins.get( pluginName ).constructor, plugin2: PluginConstructor }
|
591 | );
|
592 | }
|
593 |
|
594 | this._plugins.set( pluginName, plugin );
|
595 | }
|
596 | }
|
597 |
|
598 | mix( PluginCollection, EmitterMixin );
|