Skip to content

Commit

Permalink
Better expose of errors that happen during injecting of cloud assets.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Jan 13, 2025
1 parent 716d005 commit df8360e
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 23 deletions.
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

0 comments on commit df8360e

Please sign in to comment.