Source: extensions/Extensions.ts

/**
 * `extensions` is a global object that holds all the extensions registered with PixiJS.
 * PixiJS uses a this extensions architecture a lot to make the library more modular and
 * flexible.
 *
 * For example, if you want to add load a new type of asset, you can register a new
 * LoaderParser with the `extensions` object.
 *
 * ```js
 * import { extensions, ExtensionType } from 'pixi.js';
 *
 * // create a custom asset loader
 * const customAssetLoader = {
 *    extension: {
 *        type: ExtensionType.LoadParser,
 *        name: 'custom-asset-loader',
 *    },
 *    test(url) {
 *       // check if this new loader should be used...
 *    },
 *    load(url) {
 *        // load the asset...
 *    },
 * };
 *
 * // add the custom asset loader to pixi
 * extensions.add(customAssetLoader);
 * ```
 *
 * This would add the `customAssetLoader` to the list of available loaders that PixiJS can use.
 *
 * There are many different types of extensions, which are listed in ExtensionType.
 * @namespace extensions
 */

/**
 * Collection of valid extension types.
 * @memberof extensions
 */
enum ExtensionType
// eslint-disable-next-line @typescript-eslint/indent
{
    /** extensions that are registered as Application plugins */
    Application = 'application',

    /** extensions that are registered as WebGL render pipes */
    WebGLPipes = 'webgl-pipes',
    /** extensions that are registered as WebGL render pipes adaptors */
    WebGLPipesAdaptor = 'webgl-pipes-adaptor',
    /** extensions that are registered as WebGL render systems */
    WebGLSystem = 'webgl-system',

    /** extensions that are registered as WebGPU render pipes */
    WebGPUPipes = 'webgpu-pipes',
    /** extensions that are registered as WebGPU render pipes adaptors */
    WebGPUPipesAdaptor = 'webgpu-pipes-adaptor',
    /** extensions that are registered as WebGPU render systems */
    WebGPUSystem = 'webgpu-system',

    /** extensions that are registered as Canvas render pipes */
    CanvasSystem = 'canvas-system',
    /** extensions that are registered as Canvas render pipes adaptors */
    CanvasPipesAdaptor = 'canvas-pipes-adaptor',
    /** extensions that are registered as Canvas render systems */
    CanvasPipes = 'canvas-pipes',

    /** extensions that combine the other Asset extensions */
    Asset = 'asset',
    /** extensions that are used to load assets through Assets */
    LoadParser = 'load-parser',
    /** extensions that are used to resolve asset urls through Assets */
    ResolveParser = 'resolve-parser',
    /** extensions that are used to handle how urls are cached by Assets */
    CacheParser = 'cache-parser',
    /** extensions that are used to add/remove available resources from Assets */
    DetectionParser = 'detection-parser',

    /** extensions that are registered with the MaskEffectManager */
    MaskEffect = 'mask-effect',

    /** A type of extension for creating a new advanced blend mode */
    BlendMode = 'blend-mode',

    /** A type of extension that will be used to auto detect a resource type */
    TextureSource = 'texture-source',

    /** A type of extension that will be used to auto detect an environment */
    Environment = 'environment',

    /** A type of extension for building and triangulating custom shapes used in graphics. */
    ShapeBuilder = 'shape-builder',
}

/**
 * The metadata for an extension.
 * @memberof extensions
 * @ignore
 */
interface ExtensionMetadataDetails
{
    /** The extension type, can be multiple types */
    type: ExtensionType | ExtensionType[];
    /** Optional. Some plugins provide an API name/property, to make them more easily accessible */
    name?: string;
    /** Optional, used for sorting the plugins in a particular order */
    priority?: number;
}

/**
 * The metadata for an extension.
 * @memberof extensions
 */
type ExtensionMetadata = ExtensionType | ExtensionMetadataDetails;

/**
 * Format when registering an extension. Generally, the extension
 * should have these values as `extension` static property,
 * but you can override name or type by providing an object.
 * @memberof extensions
 */
interface ExtensionFormat
{
    /** The extension type, can be multiple types */
    type: ExtensionType | ExtensionType[];
    /** Optional. Some plugins provide an API name/property, such as Renderer plugins */
    name?: string;
    /** Optional, used for sorting the plugins in a particular order */
    priority?: number;
    /** Reference to the plugin object/class */
    ref: any;
}

/**
 * Extension format that is used internally for registrations.
 * @memberof extensions
 * @ignore
 */
interface StrictExtensionFormat extends ExtensionFormat
{
    /** The extension type, always expressed as multiple, even if a single */
    type: ExtensionType[];
}

type ExtensionHandler = (extension: StrictExtensionFormat) => void;

/**
 * Convert input into extension format data.
 * @ignore
 */
const normalizeExtension = (ext: ExtensionFormat | any): StrictExtensionFormat =>
{
    // Class/Object submission, use extension object
    if (typeof ext === 'function' || (typeof ext === 'object' && ext.extension))
    {
        // #if _DEBUG
        if (!ext.extension)
        {
            throw new Error('Extension class must have an extension object');
        }
        // #endif
        const metadata: ExtensionMetadataDetails = (typeof ext.extension !== 'object')
            ? { type: ext.extension }
            : ext.extension;

        ext = { ...metadata, ref: ext };
    }
    if (typeof ext === 'object')
    {
        ext = { ...ext };
    }
    else
    {
        throw new Error('Invalid extension type');
    }

    if (typeof ext.type === 'string')
    {
        ext.type = [ext.type];
    }

    return ext;
};

/**
 * Get the priority for an extension.
 * @ignore
 * @param ext - Any extension
 * @param defaultPriority - Fallback priority if none is defined.
 * @returns The priority for the extension.
 * @memberof extensions
 */
export const normalizeExtensionPriority = (ext: ExtensionFormat | any, defaultPriority: number): number =>
    normalizeExtension(ext).priority ?? defaultPriority;

/**
 * Global registration of all PixiJS extensions. One-stop-shop for extensibility.
 *
 * Import the `extensions` object and use it to register new functionality via the described methods below.
 * ```js
 * import { extensions } from 'pixi.js';
 *
 * // register a new extension
 * extensions.add(myExtension);
 * ```
 * @property {Function} remove - Remove extensions from PixiJS.
 * @property {Function} add - Register new extensions with PixiJS.
 * @property {Function} handle - Internal method to handle extensions by name.
 * @property {Function} handleByMap - Handle a type, but using a map by `name` property.
 * @property {Function} handleByNamedList - Handle a type, but using a list of extensions with a `name` property.
 * @property {Function} handleByList - Handle a type, but using a list of extensions.
 * @memberof extensions
 */
const extensions = {

    /** @ignore */
    _addHandlers: {} as Partial<Record<ExtensionType, ExtensionHandler>>,

    /** @ignore */
    _removeHandlers: {} as Partial<Record<ExtensionType, ExtensionHandler>>,

    /** @ignore */
    _queue: {} as Partial<Record<ExtensionType, StrictExtensionFormat[]>>,

    /**
     * Remove extensions from PixiJS.
     * @param extensions - Extensions to be removed.
     * @returns {extensions} For chaining.
     */
    remove(...extensions: Array<ExtensionFormat | any>)
    {
        extensions.map(normalizeExtension).forEach((ext) =>
        {
            ext.type.forEach((type) => this._removeHandlers[type]?.(ext));
        });

        return this;
    },

    /**
     * Register new extensions with PixiJS.
     * @param extensions - The spread of extensions to add to PixiJS.
     * @returns {extensions} For chaining.
     */
    add(...extensions: Array<ExtensionFormat | any>)
    {
        // Handle any extensions either passed as class w/ data or as data
        extensions.map(normalizeExtension).forEach((ext) =>
        {
            ext.type.forEach((type) =>
            {
                const handlers = this._addHandlers;
                const queue = this._queue;

                if (!handlers[type])
                {
                    queue[type] = queue[type] || [];
                    queue[type]?.push(ext);
                }
                else
                {
                    handlers[type]?.(ext);
                }
            });
        });

        return this;
    },

    /**
     * Internal method to handle extensions by name.
     * @param type - The extension type.
     * @param onAdd  - Function handler when extensions are added/registered StrictExtensionFormat.
     * @param onRemove  - Function handler when extensions are removed/unregistered StrictExtensionFormat.
     * @returns {extensions} For chaining.
     */
    handle(type: ExtensionType, onAdd: ExtensionHandler, onRemove: ExtensionHandler)
    {
        const addHandlers = this._addHandlers;
        const removeHandlers = this._removeHandlers;

        // #if _DEBUG
        if (addHandlers[type] || removeHandlers[type])
        {
            throw new Error(`Extension type ${type} already has a handler`);
        }
        // #endif

        addHandlers[type] = onAdd;
        removeHandlers[type] = onRemove;

        // Process the queue
        const queue = this._queue;

        // Process any plugins that have been registered before the handler
        if (queue[type])
        {
            queue[type]?.forEach((ext) => onAdd(ext));
            delete queue[type];
        }

        return this;
    },

    /**
     * Handle a type, but using a map by `name` property.
     * @param type - Type of extension to handle.
     * @param map - The object map of named extensions.
     * @returns {extensions} For chaining.
     */
    handleByMap(type: ExtensionType, map: Record<string, any>)
    {
        return this.handle(type,
            (extension) =>
            {
                if (extension.name)
                {
                    map[extension.name] = extension.ref;
                }
            },
            (extension) =>
            {
                if (extension.name)
                {
                    delete map[extension.name];
                }
            }
        );
    },

    /**
     * Handle a type, but using a list of extensions with a `name` property.
     * @param type - Type of extension to handle.
     * @param map - The array of named extensions.
     * @param defaultPriority - Fallback priority if none is defined.
     * @returns {extensions} For chaining.
     */
    handleByNamedList(type: ExtensionType, map: {name: string, value: any}[], defaultPriority = -1)
    {
        return this.handle(
            type,
            (extension) =>
            {
                const index = map.findIndex((item) => item.name === extension.name);

                if (index >= 0) return;

                map.push({ name: extension.name, value: extension.ref });
                map.sort((a, b) =>
                    normalizeExtensionPriority(b.value, defaultPriority)
                    - normalizeExtensionPriority(a.value, defaultPriority));
            },
            (extension) =>
            {
                const index = map.findIndex((item) => item.name === extension.name);

                if (index !== -1)
                {
                    map.splice(index, 1);
                }
            }
        );
    },

    /**
     * Handle a type, but using a list of extensions.
     * @param type - Type of extension to handle.
     * @param list - The list of extensions.
     * @param defaultPriority - The default priority to use if none is specified.
     * @returns {extensions} For chaining.
     */
    handleByList(type: ExtensionType, list: any[], defaultPriority = -1)
    {
        return this.handle(
            type,
            (extension) =>
            {
                if (list.includes(extension.ref))
                {
                    return;
                }

                list.push(extension.ref);
                list.sort((a, b) =>
                    normalizeExtensionPriority(b, defaultPriority) - normalizeExtensionPriority(a, defaultPriority));
            },
            (extension) =>
            {
                const index = list.indexOf(extension.ref);

                if (index !== -1)
                {
                    list.splice(index, 1);
                }
            }
        );
    },
};

export {
    extensions,
    ExtensionType,
};
export type {
    StrictExtensionFormat as ExtensionFormat,
    ExtensionFormat as ExtensionFormatLoose,
    ExtensionHandler,
    ExtensionMetadata,
    ExtensionMetadataDetails
};