Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better expose of errors that happen during injecting of cloud assets. #53

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ module.exports = {
rules: {
'@typescript-eslint/no-unused-expressions': 'off'
}
},
{
files: [ '**/*.ts' ],
rules: {
// In some cases, this particular rule causes crashes of whole eslint. It may be not needed after upgrade eslint.
'@typescript-eslint/no-useless-constructor': 'off'
}
}
]
};
96 changes: 96 additions & 0 deletions src/cdn/CKEditorCloudLoaderError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import type { BundleInstallationInfo } from '../installation-info/types.js';

/**
* Base class for all CKEditor cloud loader errors.
*/
export class CKEditorCloudLoaderError<T extends CKEditorCloudLoaderErrorTag> extends Error {
/**
* Error tag used for identifying the error type.
*/
public readonly tag: T;

/**
* Additional context information about the error.
*/
public readonly context: CKEditorCloudLoaderErrorContext[T];

/**
* Creates a new CKEditorCloudLoaderError instance.
*
* @param message The error message.
* @param tag Error tag.
* @param context Additional context information about the error.
*/
constructor( message: string, tag: T, context: CKEditorCloudLoaderErrorContext[T] ) {
super( message );

this.name = 'CKEditorCloudLoaderError';
this.tag = tag;
this.context = context;

/**
* When extending built-in classes like Error in TypeScript/JavaScript, we need to manually fix the prototype chain.
* This is because the Error constructor resets the prototype chain when it's called, breaking inheritance.
* Without this line, instanceof checks would fail and custom properties would not be preserved in some environments.
*
* @see {@link https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work}
*/
Object.setPrototypeOf( this, CKEditorCloudLoaderError.prototype );
}

/**
* Prints a formatted error message to the console for debugging purposes.
* The output includes the error message, tag, and context information in a grouped format.
*/
public dump(): void {
console.group(
'%c🚨 CKEditor Cloud Loader Error',
'background-color: #d93025; color: white; padding: 2px 5px; border-radius: 3px; font-size: 14px; font-weight: bold;'
);

console.error(
'%cMessage: %c%s\n%cTag: %c%s\n%cContext: %c%s',
'font-weight: bold', '', this.message,
'font-weight: bold', '', this.tag,
'font-weight: bold', '', JSON.stringify( this.context )
);

console.groupEnd();
}
}

/**
* Checks if the given error is an instance of CKEditorCloudLoaderError.
*
* @param error The error to check.
* @returns True if the error is an instance of CKEditorCloudLoaderError, false otherwise.
*/
export function isCKEditorCloudLoaderError( error: unknown ): error is CKEditorCloudLoaderError<CKEditorCloudLoaderErrorTag> {
return error instanceof CKEditorCloudLoaderError;
}

/**
* Maps error tags to their context types.
*/
export type CKEditorCloudLoaderErrorContext = {
'resource-load-error': {
url: string;
type: 'script' | 'stylesheet';
};
'version-not-supported': {
currentVersion: string;
minimumVersion: string;
};
'editor-already-loaded': BundleInstallationInfo<string>;
'ckbox-already-loaded': BundleInstallationInfo<string>;
};

/**
* Available error tags for CKEditor cloud loader errors.
*/
export type CKEditorCloudLoaderErrorTag = keyof CKEditorCloudLoaderErrorContext;
14 changes: 10 additions & 4 deletions src/cdn/ck/createCKCdnBaseBundlePack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { without } from '../../utils/without.js';
import { getCKBaseBundleInstallationInfo } from '../../installation-info/getCKBaseBundleInstallationInfo.js';
import { createCKDocsUrl } from '../../docs/createCKDocsUrl.js';

import { CKEditorCloudLoaderError } from '../CKEditorCloudLoaderError.js';
import { createCKCdnUrl, type CKCdnUrlCreator } from './createCKCdnUrl.js';

import type { CKCdnVersion } from './isCKCdnVersion.js';

import './globals.js';
Expand Down Expand Up @@ -82,16 +84,20 @@ export function createCKCdnBaseBundlePack(

switch ( installationInfo?.source ) {
case 'npm':
throw new Error(
throw new CKEditorCloudLoaderError(
'CKEditor 5 is already loaded from npm. Check the migration guide for more details: ' +
createCKDocsUrl( 'updating/migration-to-cdn/vanilla-js.html' )
createCKDocsUrl( 'updating/migration-to-cdn/vanilla-js.html' ),
'editor-already-loaded',
installationInfo
);

case 'cdn':
if ( installationInfo.version !== version ) {
throw new Error(
throw new CKEditorCloudLoaderError(
`CKEditor 5 is already loaded from CDN in version ${ installationInfo.version }. ` +
`Remove the old <script> and <link> tags loading CKEditor 5 to allow loading the ${ version } version.`
`Remove the old <script> and <link> tags loading CKEditor 5 to allow loading the ${ version } version.`,
'editor-already-loaded',
installationInfo
);
}

Expand Down
8 changes: 6 additions & 2 deletions src/cdn/ckbox/createCKBoxCdnBundlePack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { without } from '../../utils/without.js';
import { getCKBoxInstallationInfo } from '../../installation-info/getCKBoxInstallationInfo.js';

import type { CKCdnResourcesAdvancedPack } from '../../cdn/utils/loadCKCdnResourcesPack.js';

import { createCKBoxCdnUrl, type CKBoxCdnVersion } from './createCKBoxCdnUrl.js';
import { CKEditorCloudLoaderError } from '../CKEditorCloudLoaderError.js';

import './globals.js';

Expand Down Expand Up @@ -62,9 +64,11 @@ export function createCKBoxBundlePack(
const installationInfo = getCKBoxInstallationInfo();

if ( installationInfo && installationInfo.version !== version ) {
throw new Error(
throw new CKEditorCloudLoaderError(
`CKBox is already loaded from CDN in version ${ installationInfo.version }. ` +
`Remove the old <script> and <link> tags loading CKBox to allow loading the ${ version } version.`
`Remove the old <script> and <link> tags loading CKBox to allow loading the ${ version } version.`,
'ckbox-already-loaded',
installationInfo
);
}
}
Expand Down
11 changes: 9 additions & 2 deletions src/cdn/loadCKEditorCloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
type CdnPluginsPacks
} from './plugins/combineCdnPluginsPacks.js';

import { CKEditorCloudLoaderError } from './CKEditorCloudLoaderError.js';

/**
* A composable function that loads CKEditor Cloud Services bundles.
* It returns the exports of the loaded bundles.
Expand Down Expand Up @@ -131,9 +133,14 @@ function validateCKEditorVersion( version: CKCdnVersion ) {
}

if ( !isCKCdnSupportedByEditorVersion( version ) ) {
throw new Error(
throw new CKEditorCloudLoaderError(
`The CKEditor 5 CDN can't be used with the given editor version: ${ version }. ` +
'Please make sure you are using at least the CKEditor 5 version 44.'
'Please make sure you are using at least the CKEditor 5 version 44.',
'version-not-supported',
{
currentVersion: version,
minimumVersion: '44.0.0'
}
);
}
}
Expand Down
49 changes: 35 additions & 14 deletions src/cdn/utils/loadCKCdnResourcesPack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { injectStylesheet } from '../../utils/injectStylesheet.js';
import { preloadResource } from '../../utils/preloadResource.js';
import { uniq } from '../../utils/uniq.js';

import { CKEditorCloudLoaderError } from '../CKEditorCloudLoaderError.js';

/**
* Loads pack of resources (scripts and stylesheets) and returns the exported global variables (if any).
*
Expand Down Expand Up @@ -56,21 +58,45 @@ export async function loadCKCdnResourcesPack<P extends CKCdnResourcesPack<any>>(

// Load stylesheet tags before scripts to avoid a flash of unstyled content.
await Promise.all(
uniq( stylesheets ).map( href => injectStylesheet( {
href,
attributes: htmlAttributes,
placementInHead: 'start'
} ) )
uniq( stylesheets ).map( async href => {
try {
await injectStylesheet( {
href,
attributes: htmlAttributes,
placementInHead: 'start'
} );
} catch ( _: unknown ) {
throw new CKEditorCloudLoaderError(
`The stylesheet "${ href }" could not be loaded. Please check if the URL is correct and the resource is available.`,
'resource-load-error',
{
type: 'stylesheet',
url: href
}
);
}
} )
);

// Load script tags.
for ( const script of uniq( scripts ) ) {
for await ( const script of uniq( scripts ) ) {
const injectorProps: InjectScriptProps = {
attributes: htmlAttributes
};

if ( typeof script === 'string' ) {
await injectScript( script, injectorProps );
try {
await injectScript( script, injectorProps );
} catch ( _: unknown ) {
throw new CKEditorCloudLoaderError(
`The script "${ script }" could not be loaded. Please check if the URL is correct and the resource is available.`,
'resource-load-error',
{
type: 'stylesheet',
url: script
}
);
}
} else {
await script( injectorProps );
}
Expand All @@ -90,13 +116,8 @@ export function normalizeCKCdnResourcesPack<R = any>( pack: CKCdnResourcesPack<R
// Check if it is array of URLs, if so, convert it to the advanced format.
if ( Array.isArray( pack ) ) {
return {
scripts: pack.filter(
item => typeof item === 'function' || item.endsWith( '.js' )
),

stylesheets: pack.filter(
item => item.endsWith( '.css' )
)
scripts: pack.filter( item => !item.endsWith( '.css' ) ),
stylesheets: pack.filter( item => item.endsWith( '.css' ) )
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export { filterObjectValues } from './utils/filterObjectValues.js';
export { filterBlankObjectValues } from './utils/filterBlankObjectValues.js';
export { mapObjectValues } from './utils/mapObjectValues.js';
export { without } from './utils/without.js';

export { appendExtraPluginsToEditorConfig } from './plugins/appendExtraPluginsToEditorConfig.js';
export {
createIntegrationUsageDataPlugin,
Expand All @@ -35,6 +34,7 @@ export {

export { isCKEditorFreeLicense } from './license/isCKEditorFreeLicense.js';

export { CKEditorCloudLoaderError, isCKEditorCloudLoaderError } from './cdn/CKEditorCloudLoaderError.js';
export {
CK_CDN_URL,
createCKCdnUrl,
Expand Down
Loading