Advanced editor bundling in Node.js

This article presents how to create a CKEditor 5 editor bundle in Node.js. It is a step-by-step guide on creating an editor bundle from the CKEditor 5 package downloaded from CKEditor 5 Builder, but can be easily adapted for a variety of setups. An editor bundle is required by CKEditor Cloud Services to enable document storage, import and export as well as connection optimization features.

# Bundle requirements

The editor bundle that can be used by CKEditor Cloud Services must fulfill certain requirements:

  • It needs to be built into a single .js file.
  • The plugins should be added in the builtinPlugins property to be included in the editor bundle. You cannot use the config.plugins option for adding the plugins to the editor.
  • The output.library property in the webpack configuration should be set toCKEditorCS value.
  • Plugins used in the editor bundle cannot execute external HTTP requests.
  • The editor instance needs to be a default export in the editor bundle.

The officially supported CKEditor 5 plugins already meet the requirement of not executing external requests in case of using them in the editor bundle by CKEditor Cloud Services. If your custom plugins send any external requests, then you have two options:

  • If this plugin does not modify the editor’s model or the editor’s output then you can remove it in the editor configuration during the editor bundle upload.
  • If this plugin modifies the editor’s model you can refactor it into 2 plugins. One of them will take care of the editing part, and only the other will make the HTTP requests. Thanks to this you will be able to remove one of the plugins during the editor bundle upload without affecting the output data.

Refer to the Uploading the editor bundle section for more information on removing the plugins from the editor configuration.

# Creating a bundle

The following example acts as a template for how to prepare an editor bundle configuration. It assumes that you have basic CKEditor 5 files - the package.json, main.js, index.html and style.css files which are the result of using CKEditor 5 Builder with “Self-hosted (npm)” integration method.

# Dependencies

This example uses the following dependencies:

  • ckeditor5
  • ckeditor5-premium-features
  • webpack
  • webpack-cli
  • mini-css-extract-plugin

If you followed the Builder setup, ckeditor5* packages should be already there. For the rest run:

npm install -D webpack webpack-cli mini-css-extract-plugin

After that, your package.json dependencies sections should look as below:

{
  "dependencies": {
    "ckeditor5": "^42.0.0",
    "ckeditor5-premium-features": "^42.0.0"
  },
  "devDependencies": {
    "mini-css-extract-plugin": "^2.9.0",
    "webpack": "^5.92.1",
    "webpack-cli": "^5.1.4"
  }
}

# Editor setup

It is highly recommended to use the same editor setup for both creating a bundle and integrating CKEditor 5 with the frontend layer. This way, it is easier to keep everything synchronized and consistent between the front end and back end. This requires slight adjustments to the above setup in how parts of the editor are defined and used.

The common parts for both setups are the editor class, list of plugins and editor config. We will split the main.js file into 3 files:

  • The main.js file that will define shared parts.
  • The main-fe.js file, that will be responsible for editor setup in the frontend application.
  • The main-be.js file, from which the editor bundle will be generated. It will be bundler entry file.

The first step is adjusting the main.js file.

// main.js

import {
    ClassicEditor,
    Essentials,
    Paragraph,
    Bold,
    Italic
} from 'ckeditor5';

import {
    RealTimeCollaborativeEditing
} from 'ckeditor5-premium-features';

// 1. Remove style imports. Those will be used in main-fe.js.
// import 'ckeditor5/ckeditor5.css';
// import 'ckeditor5-premium-features/ckeditor5-premium-features.css';

// import './style.css';

// 2. Create a list of plugins which will be exported.
const pluginList = [
    Essentials,
    Paragraph,
    Bold,
    Italic,
    RealTimeCollaborativeEditing
];

// 3. Adjust 'editorConfig' to utilize 'pluginList'.
const editorConfig = {
    plugins: pluginList,
    ...
}

// 4. Export shared parts, instead of initializing editor.
// ClassicEditor.create(document.querySelector('#editor'), editorConfig);

export {
    ClassicEditor,
    pluginList,
    editorConfig
}

Next, we will create main-fe.js which will be used on the front end.

// main-fe.js

import { ClassicEditor, editorConfig } from './main.js';

// 1. Move style imports from main.js here.
import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';

import './style.css';

// 2. Initialize editor.
ClassicEditor.create(document.querySelector('#editor'), editorConfig);

After that, main-be.js file, used for bundling, should be created.

// main-be.js

import { ClassicEditor, pluginList } from './main.js';

class CKEditorCS extends ClassicEditor {}

// 1. Assign plugins to be used in bundle.
CKEditorCS.builtinPlugins = pluginList;

// 2. Export editor class.
export default CKEditorCS;

# Frontend building

With the above changes, your application can be build and bundled the same way as before. The only adjustment needed is changing the file which is imported.

<!doctype html>
<html lang="en">
    <head>...</head>
    <body>
        ...
        <!-- <script type="module" src="./main.js"></script> -->
        <script type="module" src="./main-fe.js"></script>
    </body>
</html>

# Bundler configuration

Next, you need to prepare webpack configuration to generate a valid bundle.

const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );

module.exports = {
    entry: './main-be.js', // Your editor build configuration.
    output: {
        filename: 'editor.bundle.js', // Content of this file is required to upload to CKEditor Cloud Services.
        library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name!

        libraryTarget: 'umd',
        libraryExport: 'default',
        clean: true
    },
    plugins: [
        new MiniCssExtractPlugin()
    ],
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ]
    },
    performance: { hints: false }
};

# Generating the bundle

Then, run the following commands inside the CKEditor 5 package folder:

npx webpack --mode production

This command will result in the generation of the ./dist/editor.bundle.js file. This is the bundle file which then should be uploaded to CKEditor Cloud Services server.

# Building the editor bundle with TypeScript

The examples presented above are using JavaScript in the editor source files. Building the editor bundle with TypeScript is also possible. To start using TypeScript, follow these steps:

  1. Integrate TypeScript with webpack. Refer to this guide for more details.
  2. Change the extension of the editor source file to .ts. Adjust the entry path in webpack.config.js.
  3. Refer to the Working with TypeScript guide and adjust the imports and configuration settings if needed.

# Creating a bundle from legacy configuration

This section relates to bundling editor from legacy configuration. Legacy configuration is the one using editor presets, DLLs, or @ckeditor/ckeditor5-* imports.

Refer to the above Creating a bundle section for up-to-date instructions.

The following example acts as a template for how to prepare an editor build and bundler configuration. It assumes that you have an existing CKEditor 5 package with ckeditor.js, package.json and webpack.config.js files.

# Dependencies

This example uses the following dependencies:

  • @ckeditor/ckeditor5-editor-classic
  • @ckeditor/ckeditor5-basic-styles
  • @ckeditor/ckeditor5-essentials
  • @ckeditor/ckeditor5-paragraph
  • @ckeditor/ckeditor5-real-time-collaboration
  • @ckeditor/ckeditor5-dev-utils
  • @ckeditor/ckeditor5-theme-lark
  • webpack
  • webpack-cli
  • postcss-loader
  • raw-loader
  • style-loader

The package.json file dependencies sections should look similar to one below.

The list of packages and their versions may vary, depending on the editor preset/build and the time when it was created.

{
  "dependencies": {
    "@ckeditor/ckeditor5-basic-styles": "41.4.2",
    "@ckeditor/ckeditor5-editor-classic": "41.4.2",
    "@ckeditor/ckeditor5-essentials": "41.4.2",
    "@ckeditor/ckeditor5-paragraph": "41.4.2",
    "@ckeditor/ckeditor5-real-time-collaboration": "41.4.2"
  },
  "devDependencies": {
    "@ckeditor/ckeditor5-dev-utils": "^32.1.2",
    "@ckeditor/ckeditor5-theme-lark": "41.4.2",
    "postcss-loader": "^4.3.0",
    "raw-loader": "^4.0.2",
    "style-loader": "^2.0.0",
    "webpack": "^5.91.0",
    "webpack-cli": "^4.10.0"
  }
}

# Editor build configuration

This file presents an example of an editor build configuration. It is used by the bundler as an entry file. This ckeditorcs.js file should be created based on your ckeditor.js file.

Remember to adapt the configurations below to your needs because the examples are kept to a minimum. Refer to the Advanced setup documentation of CKEditor 5 builds to adjust the bundle-creating process to your requirements.

// ckeditorcs.js

// The editor base creator to use.
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';

// All plugins that you would like to use in your editor.
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import RealTimeCollaborativeEditing from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativeediting';

class CKEditorCS extends ClassicEditor {}

// Load all plugins you would like to use in your editor this way.
// This is the only way to load plugins into the editor which will then be used in CKEditor Cloud Services.
CKEditorCS.builtinPlugins = [
    Essentials,
    Paragraph,
    Bold,
    Italic,
    RealTimeCollaborativeEditing
];

// Export your editor.
export default CKEditorCS;

# Bundler configuration

This file presents an example of the bundler configuration. Here webpack is used as the bundler.

// webpack.config.js

const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );

module.exports = {
    entry: './ckeditorcs.js', // Your editor build configuration.
    output: {
        filename: 'editor.bundle.js', // Content of this file is required to upload to CKEditor Cloud Services.
        library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name!

        libraryTarget: 'umd',
        libraryExport: 'default'
    },
    module: {
        rules: [
            {
                test: /\.svg$/,
                use: [ 'raw-loader' ]
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: 'style-loader',
                        options: {
                            injectType: 'singletonStyleTag'
                        }
                    },
                    {
                        loader: 'postcss-loader',
                        options: styles.getPostCssConfig( {
                            themeImporter: {
                                themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
                            },
                            minify: true
                        } )
                    }
                ]
            }
        ]
    },
    performance: { hints: false }
};

The most important part here is output.library field. Without this, the bundle will not work with the CKEditor Cloud Services server.

module.exports = {
    // ...
    output: {
        library: 'CKEditorCS', // It is required to expose you editor class under the "CKEditorCS" name!
        // ...
    },
    // ...
};

# Generating the bundle

Run the following commands inside the CKEditor 5 package folder:

npm install
npx webpack --mode production

This command will result in the generation of the ./dist/editor.bundle.js file. This is the bundle file which then should be uploaded to the CKEditor Cloud Services server.

Your webpack output.* configuration may slightly vary. For CKEditor 5, it is also common to store build artifacts in the ./build/ folder. In such a case, the bundle would be the ./build/editor.bundle.js file.

# Editor bundle with watchdog, context or “super builds”

Cloud Services expects the Editor class as the default export in the bundled file. If you are using an editor bundle with Watchdog and context or you are building multiple editors as “super build”, you need to take extra steps to be able to upload these editors.

You can build multiple editor bundles from multiple source files for different purposes. A single webpack build can be then used to bundle all of them. Thanks to this approach, you will have an editor bundle that is compatible with the Cloud Services and you can still use the other editor bundle and take advantage of the “super builds”, watchdog, or context.

# Creating a legacy editor bundle with Watchdog

Assuming that you have a ckeditor.js source file that is exporting the editor with watchdog:

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog';
// Other plugins imports.

class Editor extends ClassicEditor {}

Editor.builtinPlugins = [
    // Imported plugins.
];

const watchdog = new EditorWatchdog( Editor );
export default watchdog;

You can now create the ckeditorcs.js source file with the same content as the above file. The only difference is the export in this file. Instead of exporting the watchdog, you should export the Editor instance: export default Editor;.

With the two source files, you can tweak the webpack config to bundle both editors in a single build step:

'use strict';

const path = require( 'path' );
const webpack = require( 'webpack' );
const { bundler, styles } = require( '@ckeditor/ckeditor5-dev-utils' );
const { CKEditorTranslationsPlugin } = require( '@ckeditor/ckeditor5-dev-translations' );
const TerserWebpackPlugin = require( 'terser-webpack-plugin' );

const config = {
    devtool: 'source-map',
    performance: { hints: false },
    optimization: {
        minimizer: [
            new TerserWebpackPlugin( {
                sourceMap: true,
                terserOptions: {
                    output: {
                        comments: /^!/
                    }
                },
                extractComments: false
            } )
        ]
    },
    plugins: [
        new CKEditorTranslationsPlugin( {
            language: 'en',
            additionalLanguages: 'all'
        } ),
        new webpack.BannerPlugin( {
            banner: bundler.getLicenseBanner(),
            raw: true
        } )
    ],
    module: {
        rules: [
            {
                test: /\.svg$/,
                use: [ 'raw-loader' ]
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: 'style-loader',
                        options: {
                            injectType: 'singletonStyleTag',
                            attributes: {
                                'data-cke': true
                            }
                        }
                    },
                    {
                        loader: 'css-loader'
                    },
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: styles.getPostCssConfig( {
                                themeImporter: {
                                    themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
                                },
                                minify: true
                            } )
                        }
                    }
                ]
            }
        ]
    }
};

module.exports = [
    // The first bundle will have the editor with watchdog and can be used in your application.
    {
        ...config,
        entry: path.resolve( __dirname, 'src', 'ckeditor.js' ),
        output: {
            library: 'Watchdog',
            path: path.resolve( __dirname, 'build' ),
            filename: 'ckeditor.js',
            libraryTarget: 'umd',
            libraryExport: 'default'
        }
    },
    // The second bundle will be ready to be uploaded to the Cloud Services server.
    {
        ...config,
        entry: path.resolve( __dirname, 'src', 'ckeditorcs.js' ),
        output: {
            library: 'CKEditorCS',
            path: path.resolve( __dirname, 'build' ),
            filename: 'ckeditorcs.js',
            libraryTarget: 'umd',
            libraryExport: 'default'
        }
    }
];

Similarly, you can build multiple bundles if you are using context or “super builds”. In the case of a bundle with context make sure that the bundle for Cloud Services includes all the plugins that the context editor has. For super builds, you can simply create a copy of a source file and export a single editor instance that you would like to upload to Cloud Services.

# Building the editor bundle with TypeScript

The examples presented above are using JavaScript in the editor source files. Building the editor bundle with TypeScript is also possible. To start using TypeScript, follow these steps:

  1. Integrate TypeScript with webpack. Refer to this guide for more details.
  2. Change the extension of the editor source file to .ts. Adjust the entry path in webpack.config.js.
  3. Refer to the Working with TypeScript guide and adjust the imports and configuration settings if needed.