# Editor bundling with Vite

This guide shows how to build a CKEditor 5 [editor bundle](../../guides/collaboration/editor-bundle.md) with [Vite](https://vite.dev/) and upload it to CKEditor Cloud Services. The guide uses React as an example, but the same logic applies to other frameworks.

> **Note**
>
> If you use webpack, refer to the [Editor bundling with webpack](editor-bundle-webpack.md) guide.

<a id="how-the-setup-works"></a>

## How the setup works

In a framework application, a component usually defines the editor configuration together with DOM elements and callbacks that exist only in the browser. The bundle cannot use such a component. This guide moves the editor definition into plain modules that the application and the bundle share:

* A shared editor module defines the editor class with its plugins, and the part of the configuration that Cloud Services needs.
* The application adds the runtime configuration: credentials, the document ID, DOM elements, functions, and classes. It also imports the editor styles.
* A second Vite configuration builds the bundle from a small entry file.
* An upload script sends the bundle, the shared configuration, and the test data to Cloud Services.

The project has this structure:

```
├── src
│   ├── editor
│   │   ├── editor.js           # Shared: the editor class and its plugins.
│   │   ├── config.js           # Shared: the configuration and the bundle version.
│   │   ├── bundle-entry.js     # Bundle only: the entry file of the editor bundle.
│   │   └── plugins/            # Your custom plugins.
│   ├── App.jsx                 # Application only: the runtime configuration.
│   └── main.jsx                # Application only: the editor styles.
├── scripts
│   └── upload-bundle.js        # Uploads the editor bundle.
├── test-data
│   └── document.html           # The test data for the upload.
├── .gitignore                  # Also ignores dist-bundle/ and .env.upload.local.
├── package.json
├── vite.config.js              # The application build.
└── vite.bundle.config.js       # The editor bundle build.
```

<a id="dependencies"></a>

## Dependencies

This example uses the following dependencies:

* `ckeditor5` and `ckeditor5-premium-features` – The editor and the premium features, for example real-time collaboration.
* `@ckeditor/ckeditor5-react` – The React integration. In other frameworks, use their integration instead.
* `react` and `react-dom` – The application framework.
* `vite` and `@vitejs/plugin-react` – The build tool of the application. The bundle build needs only Vite, without other plugins or loaders.

If your Vite application does not have the CKEditor 5 packages yet, add them:

```bash
npm install ckeditor5 ckeditor5-premium-features @ckeditor/ckeditor5-react
```

After that, your `package.json` file should contain these packages:

```json
{
	// ...
	"dependencies": {
		"@ckeditor/ckeditor5-react": "^12.0.0",
		"ckeditor5": "^49.0.0",
		"ckeditor5-premium-features": "^49.0.0",
		"react": "^19.2.8",
		"react-dom": "^19.2.8"
	},
	"devDependencies": {
		// ...
		"@vitejs/plugin-react": "^6.1.1",
		"vite": "^8.3.0"
	}
}
```

<a id="shared-editor-module"></a>

## Shared editor module

<a id="editor-class"></a>

### Editor class

The `editor.js` file defines the `AppEditor` class and lists all its plugins in [`builtinPlugins`](../../../../ckeditor5/latest/api/module_core_editor_editor-Editor.md#static-member-builtinPlugins). Cloud Services creates the editor from this class, so the plugins must be in the bundle. You cannot add them with the `config.plugins` option. The name `AppEditor` keeps the class apart from the `Editor` base class of `ckeditor5`.

```js
// src/editor/editor.js

import {
	ClassicEditor,
	Autosave,
	BlockQuote,
	Bold,
	Essentials,
	GeneralHtmlSupport,
	Heading,
	Image,
	ImageCaption,
	ImageInsertViaUrl,
	ImageToolbar,
	Italic,
	Link,
	List,
	Paragraph,
	Table,
	TableToolbar,
	Underline,
	WordCount
} from 'ckeditor5';

import {
	Comments,
	PresenceList,
	RealTimeCollaborativeComments,
	RealTimeCollaborativeEditing,
	RealTimeCollaborativeRevisionHistory,
	RealTimeCollaborativeTrackChanges,
	RevisionHistory,
	TrackChanges
} from 'ckeditor5-premium-features';

import { PlaceholderEditing } from './plugins/placeholder/placeholderediting.js';
import { PlaceholderUI } from './plugins/placeholder/placeholderui.js';

// The editor class that the application and the editor bundle share.
export class AppEditor extends ClassicEditor {}

AppEditor.builtinPlugins = [
	Autosave,
	BlockQuote,
	Bold,
	Essentials,
	GeneralHtmlSupport,
	Heading,
	Image,
	ImageCaption,
	ImageInsertViaUrl,
	ImageToolbar,
	Italic,
	Link,
	List,
	Paragraph,
	Table,
	TableToolbar,
	Underline,
	WordCount,

	Comments,
	PresenceList,
	RealTimeCollaborativeComments,
	RealTimeCollaborativeEditing,
	RealTimeCollaborativeRevisionHistory,
	RealTimeCollaborativeTrackChanges,
	RevisionHistory,
	TrackChanges,

	// List the parts of a custom feature, not only its glue plugin. The upload removes `PlaceholderUI`,
	// and a glue plugin that requires it would stop the upload with the `plugincollection-required` error.
	PlaceholderEditing,
	PlaceholderUI
];
```

`PlaceholderEditing` and `PlaceholderUI` are the two parts of a custom feature, based on the [inline widget tutorial](https://ckeditor.com/docs/ckeditor5/latest/tutorials/widgets/implementing-an-inline-widget.html). The [Custom plugins](#custom-plugins) section explains why the bundle needs the first one and Cloud Services removes the second one.

<a id="shared-configuration"></a>

### Shared configuration

The `config.js` file has the part of the editor configuration that the application and Cloud Services share:

```js
// src/editor/config.js

// The part of the editor configuration that the application and Cloud Services share.
// The upload script sends it as JSON, so it must not contain DOM elements, functions or classes.
// Regular expressions in `htmlSupport` are allowed: the upload script converts them to strings.

// Change this value every time you change the editor bundle or this configuration.
export const BUNDLE_VERSION = 'react-vite-1.0.0';

export const editorConfig = {
	cloudServices: {
		bundleVersion: BUNDLE_VERSION
	},
	toolbar: {
		items: [
			'undo', 'redo',
			'|', 'revisionHistory', 'trackChanges', 'comment',
			'|', 'heading',
			'|', 'bold', 'italic', 'underline',
			'|', 'link', 'insertImage', 'insertTable', 'blockQuote',
			'|', 'bulletedList', 'numberedList',
			'|', 'placeholder'
		]
	},
	image: {
		toolbar: [ 'toggleImageCaption', 'imageTextAlternative', '|', 'comment' ]
	},
	table: {
		contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
	},
	htmlSupport: {
		allow: [
			{
				name: /^(div|section)$/,
				classes: true
			}
		]
	},
	placeholderConfig: {
		typesUrl: '/placeholder-types.json'
	}
};

// Plugins that only the browser needs. The upload script lists them in `removePlugins`,
// so Cloud Services removes them before it uses the bundle.
export const pluginsRemovedOnServer = [ 'PlaceholderUI' ];
```

Keep these rules for the shared configuration:

* The upload script sends the configuration as JSON. DOM elements, functions, and classes do not survive `JSON.stringify()`, so they go in the runtime configuration of the application. The upload script converts the regular expressions of the [General HTML Support configuration](../../guides/collaboration/editor-bundle.md#editor-configuration) to strings, because Cloud Services expects them in this format.
* The upload script imports this module in Node.js. Thus the module must not import `ckeditor5` or other browser code. Node.js cannot load `ckeditor5` and stops with `TypeError: domDocument.createElement is not a function`.
* `BUNDLE_VERSION` is the only place for the bundle version. The application and the upload script read it from this module, so they always use the same value.
* `pluginsRemovedOnServer` lists the plugins that Cloud Services must not run.

<a id="using-the-shared-module-in-the-application"></a>

## Using the shared module in the application

The application imports the editor class and the shared configuration, and adds the runtime configuration. In React, give both to the `<CKEditor>` component of the [React integration](https://ckeditor.com/docs/ckeditor5/latest/getting-started/integrations/react-default-npm.html):

```jsx
// src/App.jsx

import { useState } from 'react';
import { CKEditor } from '@ckeditor/ckeditor5-react';
import { Bold, Italic, List } from 'ckeditor5';

import { AppEditor } from './editor/editor.js';
import { editorConfig } from './editor/config.js';

// ...

// The URL hash holds the document ID, so that two windows with the same URL share one session.
const documentId = getDocumentId();

export default function App() {
	// React gives the layout element to `setLayout` after it renders the containers of the editor UI.
	const [ layout, setLayout ] = useState( null );

	return (
		<div className="main-container" ref={ setLayout }>
			{ /* ... */ }
			{ layout && <CKEditor editor={ AppEditor } config={ getRuntimeConfig( layout ) } /> }
			{ /* ... */ }
		</div>
	);
}

// The shared configuration plus the values that exist only in the browser:
// credentials, the document ID, DOM elements, functions and classes.
function getRuntimeConfig( layout ) {
	const element = selector => layout.querySelector( selector );

	return {
		...editorConfig,
		licenseKey: import.meta.env.VITE_CKEDITOR_LICENSE_KEY,
		cloudServices: {
			...editorConfig.cloudServices,
			tokenUrl: import.meta.env.VITE_CKEDITOR_TOKEN_URL,
			webSocketUrl: import.meta.env.VITE_CKEDITOR_WEBSOCKET_URL
		},
		collaboration: {
			channelId: documentId
		},
		// ...
		sidebar: {
			container: element( '.editor-container__sidebar' )
		},
		// ...
		comments: {
			editorConfig: {
				extraPlugins: [ Bold, Italic, List ]
			}
		},
		autosave: {
			save: editor => saveDocument( editor.getData() )
		}
	};
}

// ...
```

The editor renders only after React gives the layout element to `setLayout`, so the containers of the editor UI exist at that moment. The component reads no refs during render, so the React rules of the Vite template report no warnings. The configuration does not contain the `plugins` option, so the editor uses `builtinPlugins` of the shared class.

Vite gives the browser only the environment variables with the `VITE_` prefix. Put the license key and the Cloud Services URLs in `.env.local`, but never the API secret.

Import the editor styles in the application, for example in `main.jsx`. Keep them out of the shared modules, because the bundle does not need them:

```jsx
// src/main.jsx

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';

import App from './App.jsx';

createRoot( document.getElementById( 'root' ) ).render(
	<StrictMode>
		<App />
	</StrictMode>
);
```

In other frameworks, give the same class and configuration to the integration component. In plain JavaScript, give them to `AppEditor.create()`:

```js
// src/plain-javascript.js

import { Bold, Italic, List } from 'ckeditor5';

import { AppEditor } from './editor/editor.js';
import { editorConfig } from './editor/config.js';

import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';
// ...

// The same runtime configuration as in the React application, with the elements of this page.
AppEditor.create( {
	...editorConfig,
	attachTo: document.querySelector( '#editor' ),
	licenseKey: import.meta.env.VITE_CKEDITOR_LICENSE_KEY,
	cloudServices: {
		...editorConfig.cloudServices,
		tokenUrl: import.meta.env.VITE_CKEDITOR_TOKEN_URL,
		webSocketUrl: import.meta.env.VITE_CKEDITOR_WEBSOCKET_URL
	},
	collaboration: {
		channelId: window.location.hash.slice( 1 )
	},
	// ...
	sidebar: {
		container: document.querySelector( '#sidebar' )
	},
	// ...
} );
```

<a id="bundle-entry-file"></a>

## Bundle entry file

The bundle has its own entry file, so that it contains only the editor:

```js
// src/editor/bundle-entry.js

// The entry file of the editor bundle. Cloud Services uses its default export as the `CKEditorCS` global.
import { AppEditor } from './editor.js';

export default AppEditor;
```

The default export must be the only export. In Vite library mode, an entry file with named exports makes the `CKEditorCS` global an object with the class in its `default` property. Then the upload fails. The Vite configuration below stops such a build.

<a id="vite-configuration-for-the-bundle"></a>

## Vite configuration for the bundle

A second Vite configuration builds the bundle in [library mode](https://vite.dev/guide/build.html#library-mode):

```js
// vite.bundle.config.js

import { defineConfig } from 'vite';

// Builds the editor bundle for Cloud Services: one file that sets the `CKEditorCS` global.
export default defineConfig( {
	// Resolve the paths below from this folder, not from the folder that runs the build.
	root: import.meta.dirname,

	// Vite does not replace `process.env` in library mode, and the Cloud Services page has no `process` global.
	define: {
		'process.env.NODE_ENV': JSON.stringify( 'production' )
	},

	build: {
		outDir: 'dist-bundle',
		emptyOutDir: true,
		copyPublicDir: false,
		lib: {
			entry: 'src/editor/bundle-entry.js',
			name: 'CKEditorCS',
			formats: [ 'umd' ],
			fileName: () => 'editor.bundle.js'
		},
		rolldownOptions: {
			output: {
				// Stop the build if the entry file exports anything other than the editor class.
				exports: 'default'
			}
		}
	}
} );
```

The configuration uses these options:

* `root` – Vite resolves `lib.entry` and `outDir` from the folder of this file. This matters when you start the build from another folder, for example from the root of a monorepo.
* `define` – Replaces `process.env.NODE_ENV`. Cloud Services runs the bundle in a page that has no `process` global. Without this option, a dependency that reads `process.env`, for example React, stops the bundle.
* `build.lib` – Builds one UMD file that sets the `CKEditorCS` global to the default export of the entry file. Cloud Services cannot load an ES module, so do not use the `es` format.
* `build.rolldownOptions.output.exports` – Stops the build if the entry file has other exports than the default export.
* `outDir` and `copyPublicDir` – Keep the bundle apart from the application build in `dist/`.

> **Warning**
>
> This configuration is for Vite 8. Vite 7 ignores `build.rolldownOptions` without an error, so use `build.rollupOptions` there.

Vite puts the code of dynamic imports into the UMD file, so you do not need an equivalent of the `dynamicImportMode: 'eager'` webpack option. If your plugins import CSS files, Vite also writes a CSS file to `dist-bundle/`. The upload does not need it.

<a id="test-data"></a>

## Test data

Cloud Services checks the uploaded bundle with the `test_data` parameter. It creates the editor with this data and compares the editor output with `test_data` character by character. The editor changes hand-written HTML. For example, it adds a `data-list-item-id` attribute to each list item. It also writes some spaces next to inline widgets as `&nbsp;`, and it changes `<h1>` to `<h2>`. Thus, take the test data from the editor itself:

1. In your application, open a document that uses all features and custom plugins of your editor.

2. In the developer tools of the browser, copy the editor data with this command:

   ```js
   copy( document.querySelector( '.ck-editor__editable' ).ckeditorInstance.getData( { trim: 'none' } ) );
   ```

   The command gets the editor instance from the editable element with the [`ckeditorInstance`](../../../../ckeditor5/latest/framework/how-tos.md#how-to-get-the-editor-instance-object-from-the-dom-element) property. CKEditor 5 has no global list of editor instances.

3. Compare the data with the original document, as the [Making the test data](testing-editor-bundle.md#making-the-test-data) section describes. The upload does not find the content that the editor loses when it first loads the document.

4. Paste the data into `test-data/document.html`. Do not format it.

Make new test data each time you change the plugins or update CKEditor 5. For a checklist of the content to test, the changes that the editor makes, and a local check, refer to the [Testing the editor bundle with test data](testing-editor-bundle.md) guide.

<a id="upload-script"></a>

## Upload script

The upload script sends the bundle with the [`POST /editors`](https://help.cke-cs.com/api/v5/docs#tag/Editor-bundles/paths/~1editors/post) request:

```js
// scripts/upload-bundle.js

// Uploads the editor bundle to Cloud Services with the `POST /editors` request.
// It reads the credentials from environment variables. Never put them in the frontend code.

import { createHmac } from 'node:crypto';
import { readFile } from 'node:fs/promises';

import { editorConfig, pluginsRemovedOnServer } from '../src/editor/config.js';

const { CKEDITOR_CS_API_BASE_URL: apiBaseUrl, CKEDITOR_CS_API_SECRET: apiSecret } = process.env;

if ( !apiBaseUrl || !apiSecret ) {
	console.error( 'Set the CKEDITOR_CS_API_BASE_URL and CKEDITOR_CS_API_SECRET environment variables.' );
	process.exit( 1 );
}

const bundle = await readFile( new URL( '../dist-bundle/editor.bundle.js', import.meta.url ), 'utf8' );

// Cloud Services compares the test data with the editor output exactly, so remove the final new line of the file.
const testData = ( await readFile( new URL( '../test-data/document.html', import.meta.url ), 'utf8' ) ).trimEnd();

const config = {
	...editorConfig,
	removePlugins: pluginsRemovedOnServer
};

// Cloud Services expects the regular expressions of `htmlSupport` as strings, for example "/^(div|section)$/".
const body = JSON.stringify( { bundle, config, test_data: testData }, ( _key, value ) => {
	return value instanceof RegExp ? value.toString() : value;
} );

const url = new URL( `${ apiBaseUrl }/editors` );
const timestamp = Date.now();
const signature = createHmac( 'sha256', apiSecret )
	.update( `POST${ url.pathname }${ url.search }${ timestamp }${ body }` )
	.digest( 'hex' );

const response = await fetch( url, {
	method: 'POST',
	headers: {
		'Content-Type': 'application/json',
		'X-CS-Timestamp': String( timestamp ),
		'X-CS-Signature': signature
	},
	body
} );

if ( response.status === 201 ) {
	console.log( `Uploaded the editor bundle "${ editorConfig.cloudServices.bundleVersion }".` );
} else {
	console.error( `The upload failed with HTTP ${ response.status }:` );
	console.error( await response.text() );
	process.exitCode = 1;
}
```

The script needs two environment variables:

* `CKEDITOR_CS_API_BASE_URL` – The API base URL of your environment with the environment ID, for example `https://ORGANIZATION.cke-cs.com/api/v5/ENVIRONMENT_ID`.
* `CKEDITOR_CS_API_SECRET` – An [API secret](../../developer-resources/security/api-secret.md) of the environment.

Do not give these variables the `VITE_` prefix, because Vite would put them in the application code. In CI, set them as secret variables. On your computer, you can put them in a `.env.upload.local` file, which the npm script below reads if it exists. Do not commit this file. The `.gitignore` file of the Vite templates ignores all files that end with `.local`.

Some details of the script:

* It signs the exact body that it sends. Refer to the [request signature](../../developer-resources/security/request-signature.md) guide for the algorithm.
* It sends the shared configuration with the `removePlugins` option.
* It removes the white space at the end of the test data file, for example the final new line that code editors add, because Cloud Services compares the data exactly.
* The request body, with the bundle, the configuration, and the test data, must be smaller than 10 MB.

<a id="npm-scripts"></a>

## npm scripts

Add the scripts for the bundle to the `scripts` section of `package.json`:

```json
{
	// ...
	"scripts": {
		// ...
		"build:bundle": "vite build --config vite.bundle.config.js",
		"upload:bundle": "node --env-file-if-exists=.env.upload.local scripts/upload-bundle.js",
		"release:bundle": "npm run build:bundle && npm run upload:bundle"
	}
}
```

Also add the bundle folder to `.gitignore`. Then git does not track the bundle, and the linter of the Vite template does not check it:

```
# The editor bundle for Cloud Services
dist-bundle
```

To build and upload the bundle, run:

```bash
npm run release:bundle
```

After a successful upload, the script prints `Uploaded the editor bundle "react-vite-1.0.0".` If you run it again with the same `BUNDLE_VERSION`, the upload fails with HTTP 409 and the `Editor react-vite-1.0.0 already exists.` message.

To see what Cloud Services stores, use the [`GET /editors/{bundle_version}/config`](https://help.cke-cs.com/api/v5/docs#tag/Editor-bundles/paths/~1editors~1%7Bbundle_version%7D~1config/get) request. The response shows the uploaded configuration and the plugins that Cloud Services adds to `removePlugins`.

<a id="custom-plugins"></a>

## Custom plugins

Cloud Services runs the bundle without your application and does not use the editor UI. The uploaded configuration has no DOM elements, and all network requests from the editor during document conversions are blocked. Make your custom plugins ready for this:

* **Plugins that define data** – A plugin that adds schema items, converters, or commands defines how the editor stores content. It must be in the bundle, and you must not remove it. In the example, this is `PlaceholderEditing`.
* **Plugins that only the browser needs** – A plugin that sends requests, or shows UI that loads data from your server, must not run on the server. Add its name to `pluginsRemovedOnServer`. In the example, this is `PlaceholderUI`, which loads the placeholder types from the application server. Cloud Services removes some official plugins itself, for example the real-time collaboration plugins, `Autosave`, and the export and import plugins. The `GET /editors/{bundle_version}/config` request shows the full list.
* **Plugins that do both** – Split them into an editing part and a UI part, as in the [inline widget tutorial](https://ckeditor.com/docs/ckeditor5/latest/tutorials/widgets/implementing-an-inline-widget.html). List both parts in `builtinPlugins`. If a glue plugin requires a plugin that the upload removes, the upload fails with the `plugincollection-required` error.
* **Plugin names** – The `removePlugins` option finds plugins by their `pluginName`. Give each custom plugin a static `pluginName`.
* **Plugins that need DOM elements** – Put the DOM elements in the runtime configuration only, and make the plugin start without them. If the uploaded configuration must have such an option, set it to `null`. Do not send the element itself, because `JSON.stringify()` turns it into an empty object. For example, an empty object in `sidebar.container` stops the upload with the `sidebar-setcontainer-invalid-element` error.
* **Plugins that use React or other UI libraries** – Keep the UI part out of the bundle if you can. If a library must be in the bundle, the `define` option of the bundle configuration handles `process.env.NODE_ENV`.

This is how the UI part of the placeholder feature gets its name and loads its data:

```js
// src/editor/plugins/placeholder/placeholderui.js

// ...

// The UI part of the placeholder feature. It loads the placeholder types from the application server.
// Cloud Services does not need it and cannot reach that server, so the upload script removes it.
export class PlaceholderUI extends Plugin {
	static get pluginName() {
		return 'PlaceholderUI';
	}

	// ...

	async _loadTypes( items ) {
		const response = await fetch( this.editor.config.get( 'placeholderConfig.typesUrl' ) );

		// ...
	}
}
```

> **Note**
>
> The [Server-side Editor API](../../guides/collaboration/server-side-editor-api.md) does not use the `removePlugins` option of the upload. To remove plugins for a script, use the `plugins_to_remove` parameter of the request. Refer to the [Editor scripts](../../developer-resources/server-side-editor-api/editor-scripts.md) guide for details.

<a id="updating-the-bundle"></a>

## Updating the bundle

When you change the editor, for example its plugins, its configuration, or the CKEditor 5 version:

1. Change `BUNDLE_VERSION` in `src/editor/config.js`.
2. Run `npm run release:bundle`.
3. Deploy the application with the new version.

Then follow the steps in the [Updating the editor bundle](../../guides/collaboration/editor-bundle.md#updating-the-editor-bundle) section of the main guide.

A collaboration session keeps the bundle version that created it. Editors with the old version can continue to work in it. An editor with the new version cannot connect to the session and fails with the `realtimecollaborationclient-init-connection-failed` error. The error details say: `Incompatible bundle version or engine version detected.` When the session ends or you remove it, the editor with the new version starts a new session. If document storage is on, the document data stays after you remove the session.

<a id="typescript"></a>

## TypeScript

The TypeScript variant of the project has the same files with the `.ts` and `.tsx` extensions. Make these changes:

* Type the shared configuration with `satisfies EditorConfig`. Declare the types of custom options in a module augmentation, and import it in `config.ts`:

  ```ts
  // src/editor/config.ts

  import type { EditorConfig } from 'ckeditor5';

  // The types of the custom options, such as `placeholderConfig`. The file is empty at runtime.
  import './plugins/placeholder/augmentation.ts';

  // ...

  export const editorConfig = {
  	cloudServices: {
  		bundleVersion: BUNDLE_VERSION
  	},
  	// ...
  } satisfies EditorConfig;
  ```

  ```ts
  // src/editor/plugins/placeholder/augmentation.ts

  import type { PlaceholderCommand } from './placeholdercommand.ts';

  declare module 'ckeditor5' {
  	interface EditorConfig {

  		// The URL of a JSON array with the names of the placeholder types.
  		placeholderConfig?: {
  			typesUrl: string;
  		};
  	}

  	interface CommandsMap {
  		placeholder: PlaceholderCommand;
  	}
  }
  ```

* Node.js 22.18 and later runs `scripts/upload-bundle.ts` directly, without a build step. Thus `config.ts` and the modules that it imports must use only syntax that Node.js can remove. They must also import from `ckeditor5` only with `import type`. The `react-ts` template of Vite turns on the `erasableSyntaxOnly` compiler option, which checks this. Keep it on.

* In `scripts/upload-bundle.ts`, import the configuration from `../src/editor/config.ts`. The rest of the script is the same as in JavaScript.

* In `App.tsx`, type the layout element and the containers. The element helper returns `HTMLElement`, because the configuration options do not accept `null`:

  ```tsx
  // src/App.tsx

  // ...

  export default function App() {
  	// React gives the layout element to `setLayout` after it renders the containers of the editor UI.
  	const [ layout, setLayout ] = useState<HTMLDivElement | null>( null );

  	// ...
  }

  // ...
  function getRuntimeConfig( layout: HTMLElement ): EditorConfig {
  	// The containers exist, because React renders them before it gives the layout element to `setLayout`.
  	const element = ( selector: string ) => layout.querySelector<HTMLElement>( selector )!;

  	// ...
  }
  ```

* Add the bundle configuration and the scripts to `tsconfig.node.json`:

  ```json
  // tsconfig.node.json

  {
  	"compilerOptions": {
  		// ...
  	},
  	"include": ["vite.config.ts", "vite.bundle.config.ts", "scripts"]
  }
  ```

* Change the npm scripts:

  ```json
  {
  	// ...
  	"scripts": {
  		// ...
  		"build:bundle": "tsc -b && vite build --config vite.bundle.config.ts",
  		"upload:bundle": "node --env-file-if-exists=.env.upload.local scripts/upload-bundle.ts"
  	}
  }
  ```

<a id="troubleshooting"></a>

## Troubleshooting

These are the most common errors of the upload.

Cloud Services returns HTTP 409 when the bundle, its configuration, or the test data has a problem:

* `Broken editor build. Details: Failed to execute 'appendChild' on 'Node': Unexpected token 'export'.` – The bundle is an ES module. Build it with `formats: [ 'umd' ]`.
* ``Editor should be accessible under the `CKEditorCS` name.`` – The `build.lib.name` option is not `CKEditorCS`. Set `name: 'CKEditorCS'`.
* `Broken editor build. Details: CKEditorCS.create is not a function.` – The entry file has other exports than the editor class. Export only the editor class. The `exports: 'default'` option stops such builds.
* `Broken editor build. Details: process is not defined.` – A dependency in the bundle reads `process.env`. Add the `define` option to the bundle configuration.
* `Converted data result is not the same as provided in "test_data".` – The editor output is different from `test_data`. The `data.convertedData` field of the response shows the output. Take the test data from `editor.getData( { trim: 'none' } )` and do not change it. Refer to the [Changes that the editor makes](testing-editor-bundle.md#changes-that-the-editor-makes) section of the test data guide.
* `Broken editor build. Details: sidebar-setcontainer-invalid-element` – The uploaded configuration contains a DOM element, which `JSON.stringify()` turned into an empty object. Keep DOM elements in the runtime configuration, or set the option to `null`.
* `plugincollection-required {"plugin":"…","requiredBy":"…"}` – A plugin in the bundle requires a plugin from `removePlugins`. List the parts of the feature in `builtinPlugins` and remove only the UI part.
* `Editor <version> already exists.` – A bundle with this version exists. You cannot replace or delete it. Change `BUNDLE_VERSION`.

When the request itself has a problem, Cloud Services returns other status codes:

* `Validation failed.` with `CKEditor5 Editor Version must be at least 3 characters` (HTTP 400) – The configuration has no `cloudServices.bundleVersion`. Set the bundle version. It must have at least 3 characters.
* `Invalid signature.` (HTTP 401) – The API secret is wrong, or the signed data is not the same as the sent body. Sign the exact body string that you send.
* `Invalid request timestamp` (HTTP 401) – The `X-CS-Timestamp` header differs by more than 15 minutes from the server time. Create the timestamp with `Date.now()` immediately before you send the request.
* `Payload too large. The maximum size is: 10 MB.` (HTTP 413) – The request body is larger than 10 MB. Remove the plugins that your editor does not use, and use smaller test data.

If the upload is successful but the editor in the browser cannot connect with the `realtimecollaborationclient-init-connection-failed` error, refer to the [Updating the bundle](#updating-the-bundle) section.

---

Full index of the Cloud Services documentation: [llms.txt](../../../llms.txt)
