{
- return new Promise((resolve) => {
- htmlPluginState.cleanupCallbacks.push(() => {
- resolve('canceled');
- });
- }).then((res) => {
- expect(res).toBe('canceled');
- done();
- });
- }]}
- />);
-
- component.unmount();
- });
-
- it('componentDidUpdate should trigger plugin rerun', function() {
- const mock = jest.fn();
- const component = mount( );
-
- // plugins called once
- expect(mock).toHaveBeenCalledTimes(1);
-
- // trigger componentDidUpdate
- component.setProps('');
-
- expect(mock).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/packages/react/__tests__/AdvancedVideo.test.tsx b/packages/react/__tests__/AdvancedVideo.test.tsx
deleted file mode 100644
index c51730aa..00000000
--- a/packages/react/__tests__/AdvancedVideo.test.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import { AdvancedVideo } from '../src';
-import { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';
-import { mount } from 'enzyme';
-import React from 'react';
-import { auto, theora, vp9 } from '@cloudinary/url-gen/qualifiers/videoCodec';
-import { videoCodec } from '@cloudinary/url-gen/actions/transcode';
-
-const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
-const cloudinaryVideo = new CloudinaryVideo('sample', { cloudName: 'demo' }, { analytics: false });
-const cloudinaryVideoWithExtension = new CloudinaryVideo('sample.mp4', { cloudName: 'demo' }, { analytics: false });
-const cloudinaryVideoWithAnalytics = new CloudinaryVideo('sample', { cloudName: 'demo' }, { analytics: true });
-
-describe('AdvancedVideo', () => {
- it('should render video with default sources', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(
- '' +
- '' +
- '' +
- ' ');
- done();
- }, 0);// one tick
- });
-
- it('should generate url sources with correct placement of extension and url analytics', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(
- 'https://res.cloudinary.com/demo/video/upload/sample.webm?_a=');
- expect(component.html()).toContain(
- 'https://res.cloudinary.com/demo/video/upload/sample.ogv?_a=');
- expect(component.html()).toContain(
- 'https://res.cloudinary.com/demo/video/upload/sample.mp4?_a=');
- done();
- }, 0);// one tick
- });
-
- it('should render video with input sources', function (done) {
- const sources = [
- {
- type: 'mp4',
- codecs: ['vp8', 'vorbis'],
- transcode: videoCodec(auto())
- },
- {
- type: 'webm',
- codecs: ['avc1.4D401E', 'mp4a.40.2'],
- transcode: videoCodec(vp9())
- }];
-
- const component = mount( );
-
- setTimeout(() => {
- expect(component.html()).toContain(
- '' +
- '' +
- ' ');
- done();
- }, 0);// one tick
- });
-
- it('should render video with input sources when using useFetchFormat', function (done) {
- const sources = [
- {
- type: 'mp4',
- codecs: ['vp8', 'vorbis'],
- transcode: videoCodec(auto())
- },
- {
- type: 'webm',
- codecs: ['avc1.4D401E', 'mp4a.40.2'],
- transcode: videoCodec(vp9())
- },
- {
- type: 'ogv',
- codecs: ['theora'],
- transcode: videoCodec(theora())
- }];
-
- const component = mount( );
-
- setTimeout(() => {
- expect(component.html()).toContain(
- '' +
- '' +
- '' +
- ' ');
- done();
- }, 0);// one tick
- });
-
- it('should pass video attributes', function (done) {
- const component = mount( );
-
- setTimeout(() => {
- expect(component.html()).toContain('loop=""');
- expect(component.html()).not.toContain('playsinline');
- expect(component.html()).toContain('muted=""');
- expect(component.html()).toContain('autoplay=""');
- expect(component.html()).toContain('controls=""');
- done();
- }, 1000);// one tick
- });
-
- it('should contain poster', function (done) {
- const component = mount( );
-
- setTimeout(() => {
- expect(component.html()).toContain('poster="www.example.com"');
- done();
- }, 0);// one tick
- });
-
- it('should contain poster when "auto" is passed as cldPoster', function (done) {
- const component = mount( );
-
- setTimeout(() => {
- expect(component.html()).toContain('poster="https://res.cloudinary.com/demo/video/upload/q_auto/f_jpg/so_auto/sample"');
- done();
- }, 0);// one tick
- });
-
- it('should contain poster when cloudinary image is passed as cldPoster', function (done) {
- const component = mount( );
-
- setTimeout(() => {
- expect(component.html()).toContain('poster="https://res.cloudinary.com/demo/image/upload/sample"');
- done();
- }, 0);// one tick
- });
-
- it('should simulate onPlay event', function (done) {
- const mockCallBack = jest.fn();
-
- const component = mount( );
- setTimeout(() => {
- component.find('video').simulate('play');
- expect(mockCallBack.mock.calls.length).toEqual(1);
- done();
- }, 0);// one tick
- });
-
- it('should simulate onLoadStart event', function (done) {
- const mockCallBack = jest.fn();
-
- const component = mount( );
- setTimeout(() => {
- component.find('video').simulate('loadstart');
- expect(mockCallBack.mock.calls.length).toEqual(1);
- done();
- }, 0);// one tick
- });
-
- it('should simulate onEnded event', function (done) {
- const mockCallBack = jest.fn();
-
- const component = mount( );
- setTimeout(() => {
- component.find('video').simulate('ended');
- expect(mockCallBack.mock.calls.length).toEqual(1);
- done();
- }, 0);// one tick
- });
-
- it('should simulate onError event', function (done) {
- const mockCallBack = jest.fn();
-
- const component = mount( );
- setTimeout(() => {
- component.find('video').simulate('error');
- expect(mockCallBack.mock.calls.length).toEqual(1);
- done();
- }, 0);// one tick
- });
-
- it('should simulate onPlaying event', function (done) {
- const mockCallBack = jest.fn();
-
- const component = mount( );
- setTimeout(() => {
- component.find('video').simulate('playing');
- expect(mockCallBack.mock.calls.length).toEqual(1);
- done();
- }, 0);// one tick
- });
-
- it('Should support forwarding innerRef to underlying video element', () => {
- const myRef = React.createRef();
- mount( );
- const video: any = myRef.current;
-
- ['play', 'pause', 'canPlayType', 'addTextTrack'].forEach((func) => {
- expect(typeof video[func]).toBe('function');
- });
- });
-});
diff --git a/packages/react/__tests__/accessibility.test.tsx b/packages/react/__tests__/accessibility.test.tsx
deleted file mode 100644
index 059c286a..00000000
--- a/packages/react/__tests__/accessibility.test.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { AdvancedImage, accessibility } from '../src';
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
-import { mount } from 'enzyme';
-import React from 'react';
-
-const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
-
-describe('accessibility', () => {
- it('should apply default', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/co_black,e_colorize:70/sample"');
- done();
- }, 0);// one tick
- });
-
- it('should apply darkmode', function () {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/co_black,e_colorize:70/sample"');
- }, 0);// one tick
- });
-
- it('should apply brightmode', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/co_white,e_colorize:40/sample"');
- done();
- }, 0);// one tick
- });
-
- it('should apply monochrome', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/e_grayscale/sample"');
- done();
- }, 0);// one tick
- });
-
- it('should apply colorblind', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/e_assist_colorblind/sample"');
- done();
- }, 0);// one tick
- });
-
- it('should default if supplied with incorrect mode', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html())
- .toBe(' ');
- done();
- }, 0);// one tick
- });
-});
diff --git a/packages/react/__tests__/analytics-ssr.test.tsx b/packages/react/__tests__/analytics-ssr.test.tsx
deleted file mode 100644
index 1d200454..00000000
--- a/packages/react/__tests__/analytics-ssr.test.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @jest-environment node
- */
-import { AdvancedImage } from '../src'
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage'
-import React from 'react'
-import { SDKAnalyticsConstants } from '../src/internal/SDKAnalyticsConstants'
-// @ts-ignore
-import { version } from '../package.json'
-import { renderToString } from 'react-dom/server'
-
-const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' })
-
-describe('ssr analytics', () => {
- beforeEach(() => {
- SDKAnalyticsConstants.sdkSemver = '1.0.0'
- SDKAnalyticsConstants.techVersion = '10.2.5'
- })
-
- afterEach(() => {
- SDKAnalyticsConstants.sdkSemver = version
- SDKAnalyticsConstants.techVersion = React.version
- })
- it('creates an img with analytics', function (done) {
- const ElementImageHtml = renderToString(
-
- )
- setTimeout(() => {
- expect(ElementImageHtml).toContain(
- 'https://res.cloudinary.com/demo/image/upload/sample?_a=DAJAABDSZAA0'
- )
- done()
- }, 0) // one tick
- })
-})
diff --git a/packages/react/__tests__/analytics.test.tsx b/packages/react/__tests__/analytics.test.tsx
deleted file mode 100644
index 96f468d6..00000000
--- a/packages/react/__tests__/analytics.test.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { AdvancedImage } from '../src';
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
-import { mount } from 'enzyme';
-import React from 'react';
-import { SDKAnalyticsConstants } from '../src/internal/SDKAnalyticsConstants';
-// @ts-ignore
-import { version } from '../package.json';
-
-const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' });
-
-describe('analytics', () => {
- beforeEach(() => {
- SDKAnalyticsConstants.sdkSemver = '1.0.0';
- SDKAnalyticsConstants.techVersion = '10.2.5';
- });
-
- afterEach(() => {
- SDKAnalyticsConstants.sdkSemver = version;
- SDKAnalyticsConstants.techVersion = React.version;
- });
- it('creates an img with analytics', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toEqual(' ');
- done();
- }, 0);// one tick
- });
-});
diff --git a/packages/react/__tests__/lazyload.test.tsx b/packages/react/__tests__/lazyload.test.tsx
deleted file mode 100644
index a56582ab..00000000
--- a/packages/react/__tests__/lazyload.test.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { AdvancedImage, lazyload } from '../src'
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
-import { mount } from 'enzyme';
-import React from 'react';
-import { testWithMockedIntersectionObserver } from '../../../testUtils/testWithMockedIntersectionObserver'
-
-const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
-
-describe('lazy-load', () => {
- it('should not have src pre-scroll', async function() {
- const component = await mount(
-
- );
- // no src pre scroll
- expect(component.html()).toBe('');
- });
-
- it('should have src when in view', function(done) {
- const elm = document.createElement('img');
- testWithMockedIntersectionObserver((mockIntersectionEvent: ({}) => void) => {
- const component = mount( );
- mockIntersectionEvent([{ isIntersecting: true, target: component.getDOMNode() }]);
- setTimeout(() => {
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/sample"');
- done();
- }, 0);// one tick
- });
- });
-
- it('should set lazyload root margin and threshold', function(done) {
- const elm = document.createElement('img');
- testWithMockedIntersectionObserver((mockIntersectionEvent: ({}) => void) => {
- // @ts-ignore
- const component = mount( );
- mockIntersectionEvent([{ isIntersecting: true, target: component.getDOMNode() }]);
- setTimeout(() => {
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/sample"');
- done();
- }, 0);// one tick
- });
- });
-});
diff --git a/packages/react/__tests__/package.tests.tsx b/packages/react/__tests__/package.tests.tsx
deleted file mode 100644
index 7a176391..00000000
--- a/packages/react/__tests__/package.tests.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import {execSync} from "child_process";
-import fs from 'fs';
-import * as path from "path";
-
-describe('tests for the package output', () => {
- it('ensures that the pacakge contains the right files', () => {
- execSync('npm run build');
- const dirContents = fs.readdirSync(path.resolve(__dirname, '../dist'));
-
- expect(dirContents.includes('index.js')).toBe(true);
- expect(dirContents.includes('index.umd.js')).toBe(true);
- expect(dirContents.includes('index.cjs.js')).toBe(true);
- expect(dirContents.includes('index.d.ts')).toBe(true);
- expect(dirContents.includes('package.json')).toBe(true);
- });
-});
diff --git a/packages/react/__tests__/placeholder.test.tsx b/packages/react/__tests__/placeholder.test.tsx
deleted file mode 100644
index e9a90c84..00000000
--- a/packages/react/__tests__/placeholder.test.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { AdvancedImage, placeholder } from '../src'
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
-import { PLACEHOLDER_IMAGE_OPTIONS } from '../../html/src/utils/internalConstants';
-import { mount } from 'enzyme';
-import React from 'react';
-import { sepia } from '@cloudinary/url-gen/actions/effect';
-
-describe('placeholder', () => {
- let cloudinaryImage: CloudinaryImage;
-
- const mockImage = {
- src: null,
- onload: () => {},
- onerror: () => {}
- };
- beforeEach(() => {
- // @ts-ignore
- window.Image = function() { return mockImage };
- cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
- });
- it('should apply default', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`);
- done();
- }, 0);// one tick
- });
-
- it("should apply 'vectorize'", function () {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`);
- }, 0);// one tick
- });
-
- it('should apply pixelate', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.pixelate}/sample"`);
- done();
- }, 0);// one tick
- });
-
- it('should apply blur', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.blur}/sample"`);
- done();
- }, 0);// one tick
- });
-
- it('should apply predominant-color', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS['predominant-color']}/sample"`);
- done();
- }, 0);// one tick
- });
-
- it('should default if supplied with incorrect mode', function (done) {
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`);
- done();
- }, 0);// one tick
- });
-
- it('should append placeholder transformation', function (done) {
- cloudinaryImage.effect(sepia());
- const component = mount( );
- setTimeout(() => {
- expect(component.html()).toContain(`src="https://res.cloudinary.com/demo/image/upload/e_sepia/${PLACEHOLDER_IMAGE_OPTIONS.vectorize}/sample"`);
- done();
- }, 0);// one tick
- });
-
- /*
- This test is built with two setTimouts since the placeholder plugin makes use of two promises.
- The placeholder image loads first. Once it loads, the promise is resolved and the
- larger image will load. Once the larger image loads, promised and plugin is resolved.
- */
- it('should not fail error', function (done) {
- const component = mount( );
- setTimeout(() => {
- // @ts-ignore
- component.getDOMNode().onload(); // simulate element onload
- setTimeout(() => {
- mockImage.onerror(); // simulate image onerror
- expect(mockImage.src).toBe('https://res.cloudinary.com/demo/image/upload/sample');
- done();
- })
- }, 5);// one tick
- });
-});
diff --git a/packages/react/__tests__/responsive.test.tsx b/packages/react/__tests__/responsive.test.tsx
deleted file mode 100644
index 9396c6b0..00000000
--- a/packages/react/__tests__/responsive.test.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { AdvancedImage, responsive } from '../src'
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
-import { mount } from 'enzyme';
-import React from 'react';
-import { ResponsiveHelper } from './testUtils/responsiveHelperWrapper';
-import { crop } from '@cloudinary/url-gen/actions/resize';
-import { dispatchResize } from './testUtils/dispatchResize';
-import FakeTimers from '@sinonjs/fake-timers'
-
-const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
-
-describe('responsive', () => {
- let clock:any;
- beforeEach(() => {
- clock = FakeTimers.install()
- });
- afterEach(() => {
- clock.uninstall()
- });
-
- it('should apply initial container width (default 250)', async function () {
- const component = mount(
-
-
- );
-
- window.dispatchEvent(new Event('resize'));
- clock.tick(100); // timeout for debounce
-
- const el = component.find('#wrapper').getDOMNode();
- expect(el.clientWidth).toBe(250);
- });
-
-
- it('Should respect single step and ignore default width of 250 (When Step < Width)', async function () {
- const component = mount(
-
-
- );
-
- clock.tick(100); // timeout for debounce
-
- // Output is exactly 300 due to internal rounding: ROUND_UP(CONTAINER / STEP) * STEP
- // When STEP < CONTAINER, output is always a multiplication of STEP
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_300/sample"');
- });
-
-
- it('Should respect single step and ignore default width of 250 (When Step > Width)', async function () {
- const component = mount(
-
-
- );
-
- clock.tick(100); // timeout for debounce
-
- // Output is exactly 251 due to internal rounding: ROUND_UP(CONTAINER / STEP) * STEP
- // When STEP > CONTAINER, output is always STEP.
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_251/sample"');
- });
-
- it('Should respect steps and ignore default width of 250', async function () {
- const component = mount(
-
-
- );
-
- clock.tick(100); // timeout for debounce
-
- // Output is closest number to parentElement, never exceeding the width of the max step )
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_30/sample"');
- });
-
- it('should update container width on window resize', function () {
- const component = mount(
-
-
- );
-
- const el = dispatchResize(component, 100);
- clock.tick(100); // timeout for debounce
- expect(el.clientWidth).toBe(100);
- });
-
- it('should step by the 100th', async function () {
- const component = mount(
-
-
- );
-
- await clock.tickAsync(0); // one tick
- window.dispatchEvent(new Event('resize'));
- await clock.tickAsync(100); // timeout for debounce
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_300/sample"');
- });
-
- it('should step by breakpoints', async function () {
- const component = mount(
-
-
- );
-
- await clock.tickAsync(0); // one tick
- window.dispatchEvent(new Event('resize'));
- await clock.tickAsync(100); // timeout for debounce
-
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_800/sample"');
-
- // simulate resize to 975
- await clock.tickAsync(0); // one tick
- dispatchResize(component, 975);
- await clock.tickAsync(100); // timeout for debounce
-
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_1000/sample"');
- });
-
- it('should not resize to larger than provided breakpoints', async function () {
- const component = mount(
-
-
- );
-
- await clock.tickAsync(0); // one tick
- dispatchResize(component, 4000);
- await clock.tickAsync(100); // timeout for debounce
-
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_3000/sample"');
- });
-
- it('should handle unordered breakpoints', async function () {
- const component = mount(
-
-
- );
-
- await clock.tickAsync(0); // one tick
- dispatchResize(component, 5000);
- await clock.tickAsync(100); // timeout for debounce
-
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_scale,w_3000/sample"');
- });
-
- it('should append to existing transformation', async function () {
- cloudinaryImage.resize(crop('500'));
-
- const component = mount(
-
-
- );
-
- await clock.tickAsync(0); // one tick
- window.dispatchEvent(new Event('resize'));
- await clock.tickAsync(100); // timeout for debounce
-
- expect(component.html()).toContain('src="https://res.cloudinary.com/demo/image/upload/c_crop,w_500/c_scale,w_250/sample"');
- });
-});
diff --git a/packages/react/__tests__/ssr.test.tsx b/packages/react/__tests__/ssr.test.tsx
deleted file mode 100644
index e954b408..00000000
--- a/packages/react/__tests__/ssr.test.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * @jest-environment node
- */
-import { AdvancedImage, placeholder, responsive, accessibility, lazyload } from '../src'
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
-import React from 'react';
-import { renderToString } from 'react-dom/server'
-
-const cloudinaryImage = new CloudinaryImage('sample', { cloudName: 'demo' }, { analytics: false });
-
-describe('ssr', () => {
- it('should render accessibility transformation with accessibility', function (done) {
- const ElementImageHtml = renderToString( );
- setTimeout(() => {
- expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/co_black,e_colorize:70/sample');
- done();
- }, 0);// one tick
- });
-
- it('should render the placeholder image in SSR', function (done) {
- const ElementImageHtml = renderToString( );
- setTimeout(() => {
- expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/e_vectorize/q_auto/f_svg/sample');
- done();
- }, 0);// one tick
- });
-
- it('should render original image when responsive', function (done) {
- const ElementImageHtml = renderToString( );
- setTimeout(() => {
- expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/sample');
- done();
- }, 0);// one tick
- });
-
- it('should render original image when lazy loaded', function (done) {
- const ElementImageHtml = renderToString( );
- setTimeout(() => {
- expect(ElementImageHtml).toContain('https://res.cloudinary.com/demo/image/upload/sample');
- done();
- }, 0);// one tick
- });
-});
diff --git a/packages/react/__tests__/testUtils/dispatchResize.tsx b/packages/react/__tests__/testUtils/dispatchResize.tsx
deleted file mode 100644
index 04406f24..00000000
--- a/packages/react/__tests__/testUtils/dispatchResize.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * Util function used to dispatch a resize event used in the responsive tests
- * @param component
- * @param resizeValue
- */
-export function dispatchResize(component: any, resizeValue:number){
- const el = component.find('#wrapper').getDOMNode();
- Object.defineProperty(el, 'clientWidth', {value: resizeValue, configurable: true});
- window.dispatchEvent(new Event('resize'));
-
- return el;
-}
diff --git a/packages/react/__tests__/testUtils/responsiveHelperWrapper.tsx b/packages/react/__tests__/testUtils/responsiveHelperWrapper.tsx
deleted file mode 100644
index e86ad38c..00000000
--- a/packages/react/__tests__/testUtils/responsiveHelperWrapper.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React, {useEffect, useRef, useState} from 'react';
-
-/**
- * Simulates clientWidth
- * Initializes clientWidth with 250
- */
-export function ResponsiveHelper(props: { children: React.ReactNode }) {
- const ref = useRef(null);
- const [refElement, setRefElement] = useState(false)
- useEffect(() => {
- const parentElement = ref.current as unknown as HTMLElement;
- Object.defineProperty(parentElement, 'clientWidth', {value: 250, configurable: true});
- // @ts-ignore
- setRefElement(ref.current);
- }, []);
- return
- {refElement && props.children}
-
-}
diff --git a/packages/react/babel.config.js b/packages/react/babel.config.js
deleted file mode 100644
index 597a9815..00000000
--- a/packages/react/babel.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
- presets: [
- '@babel/preset-env',
- '@babel/preset-react',
- '@babel/preset-typescript'
- ]
-};
diff --git a/packages/react/esbuild.mjs b/packages/react/esbuild.mjs
new file mode 100644
index 00000000..3021173b
--- /dev/null
+++ b/packages/react/esbuild.mjs
@@ -0,0 +1,11 @@
+import esbuild from 'esbuild'
+
+await esbuild.build({
+ outfile: `./dist/index.js`,
+ entryPoints: ['src/index.ts'],
+ tsconfig: './tsconfig.json',
+ bundle: true,
+ platform: 'neutral',
+ format: 'esm',
+ packages: 'external',
+})
diff --git a/packages/react/index.html b/packages/react/index.html
new file mode 100644
index 00000000..ec421f7f
--- /dev/null
+++ b/packages/react/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+Welcome to Cloudinary React SDK
+
+
+
diff --git a/packages/react/jest.config.json b/packages/react/jest.config.json
index 700a234c..e69de29b 100644
--- a/packages/react/jest.config.json
+++ b/packages/react/jest.config.json
@@ -1,50 +0,0 @@
-{
- "preset": "ts-jest",
- "setupFiles": ["./setup.jest.ts"],
- "bail": true,
- "collectCoverageFrom": [
- "/src/**/*.ts"
- ],
- "transform": {
- "^.+\\.(js|ts|tsx)$": "ts-jest"
- },
- "testEnvironment": "jsdom",
- "moduleFileExtensions": [
- "ts",
- "tsx",
- "js",
- "jsx",
- "json",
- "node"
- ],
- "modulePaths": [
- "node_modules",
- "/src"
- ],
- "testPathIgnorePatterns": [
- "./__tests__/testUtils"
- ],
- "moduleNameMapper": {
- "^@cloudinary/html": "/../html/src"
- },
- "snapshotSerializers": ["enzyme-to-json/serializer"],
- "coverageThreshold": {
- "global": {
- "branches": 95,
- "functions": 95,
- "lines": 95,
- "statements": 95
- }
- },
- "globals": {
- "ts-jest": {
- "diagnostics": false
- }
- },
- "transformIgnorePatterns": [
- "/node_modules\/(?!@cloudinary/(html|url-gen|transformation-builder-sdk))(.*)"
- ],
- "setupFilesAfterEnv": [
- "./src/setupTests.js"
- ]
-}
diff --git a/packages/react/package.json b/packages/react/package.json
index 2045b622..c2dcf8e1 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,57 +1,52 @@
{
"name": "@cloudinary/react",
- "version": "1.13.1",
+ "version": "2.0.0-alpha-1",
"description": "Cloudinary React SDK",
"author": "cloudinary",
"license": "MIT",
"repository": "https://github.com/cloudinary/frontend-frameworks",
- "main": "dist/index.umd.js",
- "module": "dist/index.js",
- "types": "dist/index.d.ts",
+ "main": "dist/index.js",
"files": [
"package.json",
"dist"
],
"sideEffects": false,
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"scripts": {
- "build": "npm run build --prefix ../html && tsc && rollup -c && cp package.json ./dist",
- "test": "jest --config jest.config.json",
- "test-coverage": "jest --coverage"
+ "build": "node ./esbuild.mjs && tsc",
+ "lint": "eslint ./src",
+ "typecheck": "tsc --noEmit --skipLibCheck --project ./tsconfig.json",
+ "test": "mocha src/**/*.test.*",
+ "test-coverage": "exit 1",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build"
},
"peerDependencies": {
+ "@cloudinary/url-gen": "^1.16.0",
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
- "@babel/preset-env": "^7.12.10",
- "@babel/preset-typescript": "^7.12.7",
- "@cloudinary/html": "^1.0.1",
- "@cloudinary/url-gen": "^1.21.0",
- "@rollup/plugin-commonjs": "^21.0.1",
- "@rollup/plugin-json": "^4.1.0",
- "@rollup/plugin-node-resolve": "^13.1.3",
- "@rollup/plugin-replace": "^3.0.1",
- "@rollup/plugin-typescript": "^8.3.0",
- "@sinonjs/fake-timers": "^6.0.1",
- "@testing-library/jest-dom": "^4.2.4",
- "@types/enzyme": "^3.10.8",
- "@types/enzyme-adapter-react-16": "^1.0.6",
- "@types/jest": "^27.5.2",
+ "@chromatic-com/storybook": "^3.2.2",
+ "@storybook/addon-essentials": "^8.4.7",
+ "@storybook/addon-interactions": "^8.4.7",
+ "@storybook/addon-onboarding": "^8.4.7",
+ "@storybook/addon-webpack5-compiler-swc": "^1.0.5",
+ "@storybook/blocks": "^8.4.7",
+ "@storybook/core-server": "^8.4.7",
+ "@storybook/react": "^8.4.7",
+ "@storybook/react-webpack5": "^8.4.7",
+ "@storybook/test": "^8.4.7",
+ "@types/mocha": "^10.0.10",
"@types/node": "^12.12.38",
"@types/react": "^16.9.27",
"@types/react-dom": "^16.9.7",
- "@types/sinonjs__fake-timers": "^6.0.2",
- "@typescript-eslint/eslint-plugin": "^2.26.0",
- "@typescript-eslint/parser": "^2.26.0",
- "babel-eslint": "^10.0.3",
- "cheerio": "1.0.0-rc.12",
- "cross-env": "^7.0.2",
- "enzyme": "^3.11.0",
- "enzyme-adapter-react-16": "^1.15.5",
- "enzyme-to-json": "^3.6.1",
- "eslint": "^6.8.0",
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
+ "@typescript-eslint/parser": "^8.18.0",
+ "assert": "^2.1.0",
+ "esbuild": "^0.24.0",
+ "eslint": "^8.57.1",
"eslint-config-prettier": "^6.7.0",
"eslint-config-standard": "^14.1.0",
"eslint-config-standard-react": "^9.2.0",
@@ -61,18 +56,16 @@
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-standard": "^4.0.1",
+ "eslint-plugin-storybook": "^0.11.1",
"gh-pages": "^2.2.0",
- "jest": "27.5.1",
- "npm-run-all": "^4.1.5",
- "prettier": "^2.0.4",
+ "jsdom": "^21.1.0",
+ "jsdom-global": "^3.0.2",
+ "mocha": "^10.8.2",
+ "prettier": "^3.4.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-scripts": "^3.4.1",
- "rollup": "^2.66.1",
- "ts-jest": "^27.1.5",
- "typescript": "^3.7.5"
- },
- "dependencies": {
- "@cloudinary/html": "^1.13.1"
+ "storybook": "^8.4.7",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.7.2"
}
}
diff --git a/packages/react/setup.jest.ts b/packages/react/setup.jest.ts
index bcc44a6d..e69de29b 100644
--- a/packages/react/setup.jest.ts
+++ b/packages/react/setup.jest.ts
@@ -1,7 +0,0 @@
-// const { TextDecoder, TextEncoder, ReadableStream } = require('node:util');
-//
-// globalThis.TextDecoder = TextDecoder;
-// globalThis.TextEncoder = TextEncoder;
-// globalThis.ReadableStream = ReadableStream;
-
-export {};
diff --git a/packages/react/src/AdvancedImage.tsx b/packages/react/src/AdvancedImage.tsx
deleted file mode 100644
index 434745ec..00000000
--- a/packages/react/src/AdvancedImage.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import React from 'react';
-import { CloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
-
-import {
- HtmlImageLayer,
- Plugins,
- isBrowser,
- serverSideSrc,
- cancelCurrentlyRunningPlugins
-} from '@cloudinary/html'
-import { SDKAnalyticsConstants } from './internal/SDKAnalyticsConstants';
-
-interface ImgProps {
- cldImg: CloudinaryImage;
- plugins?: Plugins;
- [x: string]: any;
-}
-
-/**
- * @mixin ReactSDK
- * @description The Cloudinary React SDK contains components like \ to easily render your media assets from Cloudinary.
- * The SDK also comes with support for optional JS plugins that make the components smart, with features like lazy loading, placeholder, accessibility & responsiveness.
- *
- * @example
- *
- * Please note that the order of the plugins is important. See {@link https://cloudinary.com/documentation/sdks/js/frontend-frameworks/index.html#plugin-order|Plugin Order} for more details.
- *
- * // Example
- * import {CloudinaryImage} from "@cloudinary/url-gen/assets/CloudinaryImage";
- * import {
- * AdvancedImage,
- * accessibility,
- * responsive,
- * lazyload,
- * placeholder
- * } from '@cloudinary/react';
- *
- * const App = () => {
- *
- * const myCld = new Cloudinary({ cloudName: 'demo'});
- * let img = myCld().image('sample');
- *
- * return (
- *
- * )
- * };
- *
- *
- *
- *
- *
- */
-
-/**
- * @memberOf ReactSDK
- * @type {Component}
- * @description The Cloudinary image component.
- * @prop {CloudinaryImage} cldImg Generated by @cloudinary/url-gen
- * @prop {Plugins} plugins Advanced image component plugins accessibility(), responsive(), lazyload(), placeholder()
- */
-class AdvancedImage extends React.Component {
- imageRef: React.RefObject;
- htmlLayerInstance: HtmlImageLayer;
-
- constructor(props: ImgProps) {
- super(props);
- this.imageRef = React.createRef();
- }
-
- /**
- * On mount, creates a new HTMLLayer instance and initializes with ref to img element,
- * user generated cloudinaryImage and the plugins to be used.
- */
- componentDidMount() {
- this.htmlLayerInstance = new HtmlImageLayer(
- this.imageRef.current,
- this.props.cldImg,
- this.props.plugins,
- SDKAnalyticsConstants
- )
- }
-
- /**
- * On update, we cancel running plugins and update image instance with the state of user
- * cloudinaryImage and the state of plugins.
- */
- componentDidUpdate() {
- cancelCurrentlyRunningPlugins(this.htmlLayerInstance.htmlPluginState);
- // call html layer to update the dom again with plugins and reset toBeCanceled
- this.htmlLayerInstance.update(this.props.cldImg, this.props.plugins, SDKAnalyticsConstants)
- }
-
- /**
- * On unmount, we cancel the currently running plugins, and destroy the html layer instance
- */
- componentWillUnmount() {
- // Safely cancel running events on unmount.
- cancelCurrentlyRunningPlugins(this.htmlLayerInstance.htmlPluginState);
- this.htmlLayerInstance.unmount();
- }
-
- render() {
- const {
- cldImg,
- plugins,
- ...otherProps // Assume any other props are for the base element
- } = this.props;
- if (isBrowser()) { // On client side render
- return
- } else { // on server side render
- const src = serverSideSrc(
- this.props.plugins,
- this.props.cldImg,
- SDKAnalyticsConstants
- );
- return
- }
- }
-}
-
-export { AdvancedImage };
diff --git a/packages/react/src/AdvancedVideo.tsx b/packages/react/src/AdvancedVideo.tsx
deleted file mode 100644
index c8ef4ddf..00000000
--- a/packages/react/src/AdvancedVideo.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import React, { Component, createRef, EventHandler, HTMLAttributes, MutableRefObject, SyntheticEvent } from 'react';
-import { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';
-
-import {
- HtmlVideoLayer,
- Plugins,
- VideoPoster,
- VideoSources,
- cancelCurrentlyRunningPlugins
-} from '@cloudinary/html';
-
-type ReactEventHandler = EventHandler>;
-
-interface VideoProps extends HTMLAttributes{
- cldVid: CloudinaryVideo,
- cldPoster?: VideoPoster,
- plugins?: Plugins,
- sources?: VideoSources,
- innerRef?: ((instance: any) => void) | MutableRefObject | null
- useFetchFormat?: boolean,
-
- // supported video attributes
- controls?: boolean
- loop?: boolean,
- muted?: boolean,
- poster?: string,
- preload?: string,
- autoPlay?: boolean,
- playsInline?: boolean
-
- // supported video events
- onPlay?: ReactEventHandler,
- onLoadStart?: ReactEventHandler,
- onPlaying?: ReactEventHandler,
- onError?: ReactEventHandler,
- onEnded?: ReactEventHandler
-}
-
-const VIDEO_ATTRIBUTES_KEYS: string[] = ['controls', 'loop', 'muted', 'poster', 'preload', 'autoplay', 'playsinline'];
-
-/**
- * @memberOf ReactSDK
- * @type {Component}
- * @description The Cloudinary video component.
- * @prop {CloudinaryVideo} transformation Generated by @cloudinary/url-gen
- * @prop {Plugins} plugins Advanced image component plugins lazyload()
- * @prop videoAttributes Optional attributes include controls, loop, muted, poster, preload, autoplay
- * @prop videoEvents Optional video events include play, loadstart, playing, error, ended
- * @prop {VideoSources} sources Optional sources to generate
- * @example
- *
- * Using custom defined resources.
- *
- * const vid = new CloudinaryVideo('dog', {cloudName: 'demo'});
- * const videoEl = useRef();
- * const sources = [
- * {
- * type: 'mp4',
- * codecs: ['vp8', 'vorbis'],
- * transcode: videoCodec(auto())
- * },
- * {
- * type: 'webm',
- * codecs: ['avc1.4D401E', 'mp4a.40.2'],
- * videoCodec: videoCodec(auto())
- * }];
- *
- * return
- */
-class AdvancedVideo extends Component {
- videoRef: MutableRefObject
- htmlVideoLayerInstance: HtmlVideoLayer;
-
- constructor(props: VideoProps) {
- super(props);
- this.videoRef = createRef();
- this.attachRef = this.attachRef.bind(this);
- }
-
- /**
- * On mount, creates a new HTMLVideoLayer instance and initializes with ref to video element,
- * user generated cloudinaryVideo and the plugins to be used.
- */
- componentDidMount() {
- this.htmlVideoLayerInstance = new HtmlVideoLayer(
- this.videoRef && this.videoRef.current,
- this.props.cldVid,
- this.props.sources,
- this.props.plugins,
- this.getVideoAttributes(),
- this.props.cldPoster,
- {
- useFetchFormat: this.props.useFetchFormat
- }
- )
- }
-
- /**
- * On update, we cancel running plugins and update the video instance if the src
- * was changed.
- */
- componentDidUpdate() {
- cancelCurrentlyRunningPlugins(this.htmlVideoLayerInstance.htmlPluginState);
- // call html layer to update the dom again with plugins and reset toBeCanceled
- this.htmlVideoLayerInstance.update(
- this.props.cldVid,
- this.props.sources,
- this.props.plugins,
- this.getVideoAttributes(),
- this.props.cldPoster
- )
- }
-
- /**
- * On unmount, we cancel the currently running plugins.
- */
- componentWillUnmount() {
- // safely cancel running events on unmount
- cancelCurrentlyRunningPlugins(this.htmlVideoLayerInstance.htmlPluginState)
- }
-
- /**
- * Returns video attributes.
- */
- getVideoAttributes() {
- const result = {};
- VIDEO_ATTRIBUTES_KEYS.forEach((key: string) => {
- if (key in this.props) {
- result[key] = this.props[key];
- }
- });
-
- return result;
- }
-
- /**
- * Attach both this.videoRef and props.innerRef as ref to the given element.
- * @param element - the element to attach a ref to
- */
- attachRef(element: HTMLVideoElement) {
- this.videoRef.current = element;
- const { innerRef } = this.props;
-
- if (innerRef) {
- if (innerRef instanceof Function) {
- innerRef(element);
- } else {
- innerRef.current = element;
- }
- }
- };
-
- render() {
- const {
- cldVid,
- cldPoster,
- plugins,
- sources,
- innerRef,
- useFetchFormat,
- ...videoAttrs // Assume any other props are for the base element
- } = this.props;
-
- return
- }
-}
-
-export { AdvancedVideo };
diff --git a/packages/react/src/CloudinaryImage.stories.tsx b/packages/react/src/CloudinaryImage.stories.tsx
new file mode 100644
index 00000000..61a46189
--- /dev/null
+++ b/packages/react/src/CloudinaryImage.stories.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { CloudinaryImage } from './CloudinaryImage';
+import { Cloudinary } from '@cloudinary/url-gen';
+import { backgroundRemoval, sepia } from '@cloudinary/url-gen/actions/effect';
+import { scale } from '@cloudinary/url-gen/actions/resize';
+import { format, quality } from '@cloudinary/url-gen/actions/delivery';
+
+const meta: Meta = {
+ component: CloudinaryImage,
+ tags: ['autodocs']
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const VersionV3: Story = {
+ render: () => {
+ return (
+
+ );
+ }
+};
+
+export const VersionV2: Story = {
+ render: () => {
+ const cloudinary = new Cloudinary({
+ cloud: {
+ cloudName: 'demo'
+ }
+ });
+
+ const cloudinaryImageObject = cloudinary
+ .image('front_face')
+ .effect(backgroundRemoval())
+ .effect(sepia())
+ .resize(scale().height(333))
+ .delivery(format('auto'))
+ .delivery(quality('auto'));
+
+ return
+ }
+}
diff --git a/packages/react/src/CloudinaryImage.tsx b/packages/react/src/CloudinaryImage.tsx
new file mode 100644
index 00000000..c1b0d16b
--- /dev/null
+++ b/packages/react/src/CloudinaryImage.tsx
@@ -0,0 +1,98 @@
+import React, { forwardRef } from 'react';
+import { type CloudinaryImage as UrlGenCloudinaryImage } from '@cloudinary/url-gen/assets/CloudinaryImage';
+import {
+ TransformationMap
+} from './types';
+import { parseCloudinaryUrlToParts } from './parseCloudinaryUrlToParts';
+import {
+ parsePropsToTransformationString
+} from './parsePropsToTransformationString';
+import { parseFormat } from './transformationParsers/parseFormat';
+import { parseQuality } from './transformationParsers/parseQuality';
+import { parseWidth } from './transformationParsers/parseWidth';
+import { parseHeight } from './transformationParsers/parseHeight';
+import { parseEffects } from './transformationParsers/parseEffects';
+import { parseBackground } from './transformationParsers/parseBackground';
+import { createParseResize } from './transformationParsers/parseResize';
+import { parseRotate } from './transformationParsers/parseRotate';
+import { parseGravity } from './transformationParsers/parseGravity';
+import { parseAspectRatio } from './transformationParsers/parseAspectRatio';
+import { parseIgnoreAspectRatio } from './transformationParsers/parseIgnoreAspectRatio';
+import { parseZoom } from './transformationParsers/parseZoom';
+import { parseRoundCorners } from './transformationParsers/parseRoundCorners';
+import { parseOpacity } from './transformationParsers/parseOpacity';
+import { Quality } from './transformationTypes/quality';
+import { ImageFormat } from './transformationTypes/format';
+import { RemoveBackground } from './transformationTypes/removeBackground';
+import { ImageEffect } from './transformationTypes/effect';
+import { Background } from './transformationTypes/background';
+import { Rotate } from './transformationTypes/rotate';
+import { RoundCorners } from './transformationTypes/roundCorners';
+import { Opacity } from './transformationTypes/opacity';
+import { parseRemoveBackground } from './transformationParsers/parseRemoveBackground';
+import { Resize } from './transformationTypes/resize';
+
+type ImageTransformationProps = {
+ quality?: Quality;
+ format?: ImageFormat;
+ removeBackground?: RemoveBackground;
+ effects?: ImageEffect[];
+ background?: Background;
+ rotate?: Rotate;
+ roundCorners?: RoundCorners;
+ opacity?: Opacity;
+ resize?: Resize;
+};
+
+type ImageV3Props = {
+ cldImg?: never
+ src: string;
+ alt: string;
+ children?: never;
+ imgProps?: React.HTMLProps
+} & ImageTransformationProps;
+
+interface ImageV2Props {
+ src?: never;
+ alt: string;
+ cldImg: UrlGenCloudinaryImage;
+ children?: never;
+ imgProps?: React.HTMLProps
+}
+
+export type CloudinaryImageProps = ImageV3Props | ImageV2Props;
+
+export const CloudinaryImage = forwardRef(
+ (props, ref) => {
+ if (props.cldImg) {
+ const { cldImg, alt, imgProps, ...rest } = props;
+ return ;
+ }
+ const transformationMap = {
+ format: parseFormat,
+ quality: parseQuality,
+ background: parseBackground,
+ removeBackground: parseRemoveBackground,
+ effects: parseEffects(parseRemoveBackground),
+ resize: createParseResize({
+ parseHeight,
+ parseAspectRatio,
+ parseGravity,
+ parseWidth,
+ parseBackground,
+ parseIgnoreAspectRatio,
+ parseZoom
+ }),
+ rotate: parseRotate,
+ roundCorners: parseRoundCorners,
+ opacity: parseOpacity
+ } satisfies TransformationMap;
+ const { src, alt, children, cldImg, imgProps, ...rest } = props;
+ const { baseCloudUrl, assetPath } = parseCloudinaryUrlToParts(src);
+ const transformationString = parsePropsToTransformationString({
+ transformationProps: rest,
+ transformationMap
+ });
+
+ return ;
+ });
diff --git a/packages/react/src/CloudinaryVideo.stories.tsx b/packages/react/src/CloudinaryVideo.stories.tsx
new file mode 100644
index 00000000..8eb1a237
--- /dev/null
+++ b/packages/react/src/CloudinaryVideo.stories.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { CloudinaryVideo } from './CloudinaryVideo';
+import { Cloudinary } from '@cloudinary/url-gen';
+import { byRadius } from '@cloudinary/url-gen/actions/roundCorners';
+import { trim } from '@cloudinary/url-gen/actions/videoEdit';
+import { blackwhite } from '@cloudinary/url-gen/actions/effect';
+import { videoCodec } from '@cloudinary/url-gen/actions/transcode';
+import { h264 } from '@cloudinary/url-gen/qualifiers/videoCodec';
+import { baseline } from '@cloudinary/url-gen/qualifiers/videoCodecProfile';
+import { vcl31 } from '@cloudinary/url-gen/qualifiers/videoCodecLevel';
+import { scale } from '@cloudinary/url-gen/actions/resize';
+
+const meta: Meta = {
+ component: CloudinaryVideo,
+ tags: ['autodocs']
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Version_v3: Story = {
+ render: () => {
+ return (
+
+ )
+ }
+};
+
+const cloudinary = new Cloudinary({
+ cloud: {
+ cloudName: 'demo'
+ }
+});
+
+export const Version_v2: Story = {
+ render: () => {
+ const cloudinaryVideoObject = cloudinary.video('dog.mp4')
+ .effect(blackwhite())
+ .resize(scale().height(333))
+ .videoEdit(trim().duration('1p'))
+ .roundCorners(byRadius(20))
+ .transcode(
+ videoCodec(
+ h264()
+ .profile(baseline())
+ .level(vcl31())
+ )
+ );
+
+ return (
+
+ )
+ }
+}
diff --git a/packages/react/src/CloudinaryVideo.tsx b/packages/react/src/CloudinaryVideo.tsx
new file mode 100644
index 00000000..abbca60c
--- /dev/null
+++ b/packages/react/src/CloudinaryVideo.tsx
@@ -0,0 +1,106 @@
+import React, { forwardRef } from 'react';
+import { type CloudinaryVideo as UrlGenCloudinaryVideo } from '@cloudinary/url-gen/assets/CloudinaryVideo';
+import { parseCloudinaryUrlToParts } from './parseCloudinaryUrlToParts';
+import { parseFormat } from './transformationParsers/parseFormat';
+import { parseQuality } from './transformationParsers/parseQuality';
+import { parseWidth } from './transformationParsers/parseWidth';
+import { parseHeight } from './transformationParsers/parseHeight';
+import { parseOpacity } from './transformationParsers/parseOpacity';
+import {
+ parsePropsToTransformationString
+} from './parsePropsToTransformationString';
+import { parseRotate } from './transformationParsers/parseRotate';
+import { parseRoundCorners } from './transformationParsers/parseRoundCorners';
+import { parseBackground } from './transformationParsers/parseBackground';
+import { Quality } from './transformationTypes/quality';
+import { VideoFormat } from './transformationTypes/format';
+import { Background } from './transformationTypes/background';
+import { Rotate } from './transformationTypes/rotate';
+import { RoundCorners } from './transformationTypes/roundCorners';
+import { Opacity } from './transformationTypes/opacity';
+import { TransformationMap } from './types';
+import { Duration } from './transformationTypes/duration';
+import { parseDuration } from './transformationParsers/parseDuration';
+import { StartOffset } from './transformationTypes/startOffset';
+import { parseStartOffset } from './transformationParsers/parseStartOffset';
+import { VideoCodec } from './transformationTypes/videoCodec';
+import { parseVideoCodec } from './transformationParsers/parseVideoCodec';
+import { createParseResize } from './transformationParsers/parseResize';
+import { parseAspectRatio } from './transformationParsers/parseAspectRatio';
+import { parseGravity } from './transformationParsers/parseGravity';
+import { parseIgnoreAspectRatio } from './transformationParsers/parseIgnoreAspectRatio';
+import { parseZoom } from './transformationParsers/parseZoom';
+import { RemoveBackground } from './transformationTypes/removeBackground';
+import { parseEffects } from './transformationParsers/parseEffects';
+import { VideoEffect } from './transformationTypes/effect';
+import { parseRemoveBackground } from './transformationParsers/parseRemoveBackground';
+import { Resize } from './transformationTypes/resize';
+
+export type VideoTransformationProps = {
+ quality?: Quality;
+ format?: VideoFormat;
+ removeBackground?: RemoveBackground;
+ effects?: VideoEffect[];
+ background?: Background;
+ rotate?: Rotate;
+ roundCorners?: RoundCorners;
+ opacity?: Opacity;
+ duration?: Duration;
+ startOffset?: StartOffset;
+ videoCodec?: VideoCodec;
+ resize?: Exclude;
+};
+
+type VideoV3Props = {
+ cldVid?: never;
+ src: string;
+ videoProps?: React.HTMLProps
+ children?: never;
+} & VideoTransformationProps;
+
+interface VideoV2Props {
+ src?: never;
+ cldVid: UrlGenCloudinaryVideo;
+ videoProps?: React.HTMLProps
+ children?: never;
+}
+
+export type CloudinaryVideoProps = VideoV3Props | VideoV2Props;
+
+export const CloudinaryVideo = forwardRef((props, ref) => {
+ if (props.cldVid) {
+ const { cldVid, ...rest } = props;
+ return ;
+ }
+
+ const transformationMap = {
+ format: parseFormat,
+ quality: parseQuality,
+ background: parseBackground,
+ removeBackground: parseRemoveBackground,
+ effects: parseEffects(parseRemoveBackground),
+ resize: createParseResize({
+ parseHeight,
+ parseAspectRatio,
+ parseGravity,
+ parseWidth,
+ parseBackground,
+ parseIgnoreAspectRatio,
+ parseZoom
+ }),
+ rotate: parseRotate,
+ roundCorners: parseRoundCorners,
+ opacity: parseOpacity,
+ duration: parseDuration,
+ startOffset: parseStartOffset,
+ videoCodec: parseVideoCodec
+ } satisfies TransformationMap;
+ const { src, children, cldVid, videoProps, ...rest } = props
+ const { baseCloudUrl, assetPath } = parseCloudinaryUrlToParts(props.src);
+ const transformationString = parsePropsToTransformationString({
+ transformationProps: rest,
+ transformationMap
+ });
+
+ return ;
+});
diff --git a/packages/react/src/internal/SDKAnalyticsConstants.ts b/packages/react/src/SDKAnalyticsConstants.ts
similarity index 100%
rename from packages/react/src/internal/SDKAnalyticsConstants.ts
rename to packages/react/src/SDKAnalyticsConstants.ts
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
new file mode 100644
index 00000000..256b0d63
--- /dev/null
+++ b/packages/react/src/index.ts
@@ -0,0 +1,2 @@
+export { CloudinaryImage, type CloudinaryImageProps } from './CloudinaryImage';
+export { CloudinaryVideo, type CloudinaryVideoProps } from './CloudinaryVideo';
diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx
deleted file mode 100644
index 10c310b2..00000000
--- a/packages/react/src/index.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Import and export all needed types
- */
-import {
- placeholder,
- accessibility,
- lazyload,
- responsive
-} from '@cloudinary/html'
-import { AdvancedImage } from './AdvancedImage';
-import { AdvancedVideo } from './AdvancedVideo';
-
-export { placeholder, accessibility, lazyload, responsive, AdvancedImage, AdvancedVideo };
diff --git a/packages/react/src/parseCloudinaryUrlToParts.ts b/packages/react/src/parseCloudinaryUrlToParts.ts
new file mode 100644
index 00000000..a9d9d72d
--- /dev/null
+++ b/packages/react/src/parseCloudinaryUrlToParts.ts
@@ -0,0 +1,9 @@
+export interface CloudinaryUrlParts {
+ baseCloudUrl: string;
+ assetPath: string;
+}
+
+export const parseCloudinaryUrlToParts = (url: string): CloudinaryUrlParts => {
+ const [domainWithCloud, assetType, assetPath] = url.split(/\/(image|video)\/upload\//);
+ return { baseCloudUrl: `${domainWithCloud}/${assetType}/upload`, assetPath };
+};
diff --git a/packages/react/src/parsePropsToTransformationString.ts b/packages/react/src/parsePropsToTransformationString.ts
new file mode 100644
index 00000000..54c2810d
--- /dev/null
+++ b/packages/react/src/parsePropsToTransformationString.ts
@@ -0,0 +1,19 @@
+export const parsePropsToTransformationString = >
+ ({ transformationProps, transformationMap }: {
+ transformationProps: Props,
+ transformationMap: { [K in keyof Required]: (value: Exclude) => string}
+}): string => {
+ const transformationPropKeys = Object.keys(transformationProps) as (keyof typeof transformationProps)[];
+
+ const transformationStringList: Array = transformationPropKeys
+ .map((transformationName): undefined | string => {
+ // FIXME this type guard seems to be too much for TS
+ if (transformationProps[transformationName] === undefined) {
+ return undefined;
+ }
+
+ return transformationMap[transformationName](transformationProps[transformationName]);
+ });
+
+ return transformationStringList.filter(Boolean).join('/');
+};
diff --git a/packages/react/src/setupTests.js b/packages/react/src/setupTests.js
deleted file mode 100644
index edda72c3..00000000
--- a/packages/react/src/setupTests.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import 'regenerator-runtime/runtime'
-import Enzyme from 'enzyme'
-import Adapter from 'enzyme-adapter-react-16'
-Enzyme.configure({ adapter: new Adapter() });
diff --git a/packages/react/src/transformationParsers/parseAspectRatio.ts b/packages/react/src/transformationParsers/parseAspectRatio.ts
new file mode 100644
index 00000000..10d4a61d
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseAspectRatio.ts
@@ -0,0 +1,3 @@
+import { AspectRatio } from '../transformationTypes/aspectRatio';
+
+export const parseAspectRatio = (aspectRatio: AspectRatio) => `ar_${aspectRatio}`;
diff --git a/packages/react/src/transformationParsers/parseBackground.ts b/packages/react/src/transformationParsers/parseBackground.ts
new file mode 100644
index 00000000..face2864
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseBackground.ts
@@ -0,0 +1,24 @@
+import { Background } from '../transformationTypes/background';
+
+export const parseBackground = (background: Background): `b_${string}` => {
+ if (background === 'auto') {
+ return 'b_auto';
+ }
+
+ switch (background.type) {
+ case 'auto':
+ return 'b_auto';
+ case 'color':
+ return `b_${background.color}`;
+ case 'blurred':
+ if ('intensity' in background) {
+ return `b_blurred:${background.intensity}${background.brightness ? `:${background.brightness}` : ''}`;
+ }
+ return 'b_blurred';
+ case 'generativeAiFill': {
+ return `b_gen_fill${background.prompt ? `:prompt_${background.prompt}` : ''}${
+ background.seed ? `:seed_${background.seed}` : ''
+ }`;
+ }
+ }
+};
diff --git a/packages/react/src/transformationParsers/parseDuration.ts b/packages/react/src/transformationParsers/parseDuration.ts
new file mode 100644
index 00000000..d5945ec3
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseDuration.ts
@@ -0,0 +1,3 @@
+import { Duration } from '../transformationTypes/duration';
+
+export const parseDuration = (duration: Duration) => `du_${String(duration).replace('%', 'p')}`;
diff --git a/packages/react/src/transformationParsers/parseEffects.ts b/packages/react/src/transformationParsers/parseEffects.ts
new file mode 100644
index 00000000..09570c2d
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseEffects.ts
@@ -0,0 +1,39 @@
+import { AnyEffect } from '../transformationTypes/effect';
+import { parseRemoveBackground as ParseRemoveBackground } from './parseRemoveBackground';
+
+export const parseEffects = (parseRemoveBackground: typeof ParseRemoveBackground) => (effects: AnyEffect[]): string => {
+ return effects
+ .map((effect):`e_${string}` => {
+ switch (effect.type) {
+ case 'sepia':
+ return `e_sepia${effect.level ? `:${effect.level}` : ''}`;
+ case 'removeBackground':
+ return parseRemoveBackground(effect.mode ?? true);
+ case 'fade':
+ return `e_fade:${effect.duration}`;
+ case 'gamma':
+ return `e_gamma:${effect.level}`;
+ case 'grayscale':
+ return 'e_grayscale';
+ case 'light':
+ return `e_light${effect.intensity ? `:shadowintensity_${effect.intensity}` : ''}`;
+ case 'negate':
+ return 'e_negate';
+ case 'noise':
+ return `e_noise${effect.level ? `:${effect.level}` : ''}`;
+ case 'pixelate':
+ return `e_pixelate${effect.squareSize ? `:${effect.squareSize}` : ''}`;
+ case 'blur':
+ return `e_blur${effect.strength ? `:${effect.strength}` : ''}`
+ case 'blurFaces':
+ return `e_blur_faces${effect.strength ? `:${effect.strength}` : ''}`;
+ case 'autoBrightness':
+ return `e_auto_brightness${effect.blendPercentage ? `:${effect.blendPercentage}` : ''}`;
+ case 'autoColor':
+ return `e_auto_color${effect.blendPercentage ? `:${effect.blendPercentage}` : ''}`;
+ case 'autoContrast':
+ return `e_auto_contrast${effect.blendPercentage ? `:${effect.blendPercentage}` : ''}`;
+ }
+ })
+ .join('/');
+};
diff --git a/packages/react/src/transformationParsers/parseFormat.test.ts b/packages/react/src/transformationParsers/parseFormat.test.ts
new file mode 100644
index 00000000..f9dd8f0d
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseFormat.test.ts
@@ -0,0 +1,9 @@
+import 'mocha'
+import assert from 'assert'
+import { parseFormat } from './parseFormat'
+
+describe('parseFormat', () => {
+ it('returns URL part for any format', () => {
+ assert.equal(parseFormat('auto'), 'f_auto')
+ })
+})
diff --git a/packages/react/src/transformationParsers/parseFormat.ts b/packages/react/src/transformationParsers/parseFormat.ts
new file mode 100644
index 00000000..05c4008d
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseFormat.ts
@@ -0,0 +1,3 @@
+import { ImageFormat, VideoFormat } from '../transformationTypes/format';
+
+export const parseFormat = (format: ImageFormat | VideoFormat) => `f_${format}`;
diff --git a/packages/react/src/transformationParsers/parseGravity.ts b/packages/react/src/transformationParsers/parseGravity.ts
new file mode 100644
index 00000000..0cd454c1
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseGravity.ts
@@ -0,0 +1,46 @@
+import { Gravity } from '../transformationTypes/gravity';
+
+type SimpleGravity = Gravity extends infer GravityValue ? GravityValue extends string ? GravityValue : never : never;
+
+// FIXME fill in missing gravity parsing
+export const parseGravity = (gravity: Gravity): `g_${string}` | '' => {
+ const camelCaseToSnakeCase = (str: Str) => str.replace(/([A-Z]{1})/g, (value) => `_${value.toLowerCase()}`);
+ const simpleGravityParser = (gravity: G): `g_${string}` => `g_${camelCaseToSnakeCase(gravity)}`;
+
+ const gravityToUrlComponent = {
+ auto: simpleGravityParser,
+ center: simpleGravityParser,
+ north: simpleGravityParser,
+ northWest: simpleGravityParser,
+ northEast: simpleGravityParser,
+ east: simpleGravityParser,
+ south: simpleGravityParser,
+ southWest: simpleGravityParser,
+ southEast: simpleGravityParser,
+ west: simpleGravityParser
+ } satisfies Record `g_${string}`>;
+
+ if (typeof gravity === 'string') {
+ return gravityToUrlComponent[gravity](gravity);
+ }
+
+ switch (gravity.mode) {
+ case 'special':
+ if (gravity.position === 'liquid') {
+ return 'g_liquid';
+ }
+ return '';
+ case 'object':
+ return `g_${gravity.focus}` as const;
+ case 'auto':
+ return '';
+ case 'clippingPath':
+ return '';
+ case 'region':
+ return '';
+ case 'trackPerson':
+ return '';
+ }
+
+ return 'g_auto';
+};
diff --git a/packages/react/src/transformationParsers/parseHeight.ts b/packages/react/src/transformationParsers/parseHeight.ts
new file mode 100644
index 00000000..d5c1b4b0
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseHeight.ts
@@ -0,0 +1,3 @@
+import { HeightOption } from '../transformationTypes/resize';
+
+export const parseHeight = (height: HeightOption) => `h_${height}`;
diff --git a/packages/react/src/transformationParsers/parseIgnoreAspectRatio.ts b/packages/react/src/transformationParsers/parseIgnoreAspectRatio.ts
new file mode 100644
index 00000000..4f597a9b
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseIgnoreAspectRatio.ts
@@ -0,0 +1 @@
+export const parseIgnoreAspectRatio = (ignoreAspectRatio: boolean) => ignoreAspectRatio ? 'fl_ignore_aspect_ratio' : '';
diff --git a/packages/react/src/transformationParsers/parseOpacity.ts b/packages/react/src/transformationParsers/parseOpacity.ts
new file mode 100644
index 00000000..c4b4b7c1
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseOpacity.ts
@@ -0,0 +1,3 @@
+import { Opacity } from '../transformationTypes/opacity';
+
+export const parseOpacity = (opacity: Opacity): `o_${string}` => `o_${opacity}`;
diff --git a/packages/react/src/transformationParsers/parseQuality.test.ts b/packages/react/src/transformationParsers/parseQuality.test.ts
new file mode 100644
index 00000000..f5ff9b7e
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseQuality.test.ts
@@ -0,0 +1,65 @@
+import 'mocha'
+import assert from 'assert'
+import { parseQuality } from './parseQuality'
+
+describe('parseQuality: returns correct transformation component for', () => {
+ it('"auto" option', () => {
+ assert.equal(parseQuality('auto'), 'q_auto')
+ })
+
+ it('"auto:good" option', () => {
+ assert.equal(parseQuality('auto:good'), 'q_auto:good')
+ })
+
+ it('"auto:eco" option', () => {
+ assert.equal(parseQuality('auto:eco'), 'q_auto:eco')
+ })
+
+ it('"auto:low" option', () => {
+ assert.equal(parseQuality('auto:low'), 'q_auto:low')
+ })
+
+ it('"auto:best" option', () => {
+ assert.equal(parseQuality('auto:best'), 'q_auto:best')
+ })
+
+ it('"jpegmini" option', () => {
+ assert.equal(parseQuality('jpegmini'), 'q_jpegmini')
+ })
+
+ it('"jpegmini:0" option', () => {
+ assert.equal(parseQuality('jpegmini:0'), 'q_jpegmini:0')
+ })
+
+ it('"jpegmini:1" option', () => {
+ assert.equal(parseQuality('jpegmini:1'), 'q_jpegmini:1')
+ })
+
+ it('"jpegmini:2" option', () => {
+ assert.equal(parseQuality('jpegmini:2'), 'q_jpegmini:2')
+ })
+
+ it('"low" option', () => {
+ assert.equal(parseQuality('low'), 'q_low')
+ })
+
+ it('"eco" option', () => {
+ assert.equal(parseQuality('eco'), 'q_eco')
+ })
+
+ it('"medium" option', () => {
+ assert.equal(parseQuality('medium'), 'q_medium')
+ })
+
+ it('"good" option', () => {
+ assert.equal(parseQuality('good'), 'q_good')
+ })
+
+ it('"high" option', () => {
+ assert.equal(parseQuality('high'), 'q_high')
+ })
+
+ it('"best" option', () => {
+ assert.equal(parseQuality('best'), 'q_best')
+ })
+})
diff --git a/packages/react/src/transformationParsers/parseQuality.ts b/packages/react/src/transformationParsers/parseQuality.ts
new file mode 100644
index 00000000..610bdac6
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseQuality.ts
@@ -0,0 +1,3 @@
+import { Quality } from '../transformationTypes/quality';
+
+export const parseQuality = (quality: Quality): `q_${string}` => `q_${quality}`;
diff --git a/packages/react/src/transformationParsers/parseRemoveBackground.ts b/packages/react/src/transformationParsers/parseRemoveBackground.ts
new file mode 100644
index 00000000..61899ad0
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseRemoveBackground.ts
@@ -0,0 +1,9 @@
+import { RemoveBackground } from '../transformationTypes/removeBackground';
+
+export const parseRemoveBackground = (removeBackground: Value): Value extends false ? '' : `e_${string}` => {
+ if (!removeBackground) {
+ return '' as Value extends false ? '' : `e_${string}`;
+ }
+
+ return (removeBackground === 'fineEdges' ? 'e_background_removal:fineedges_y' : 'e_background_removal') as Value extends false ? '' : `e_${string}`;
+}
diff --git a/packages/react/src/transformationParsers/parseResize.ts b/packages/react/src/transformationParsers/parseResize.ts
new file mode 100644
index 00000000..d25f6c6b
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseResize.ts
@@ -0,0 +1,132 @@
+import { HeightOption, Resize, WidthOption } from '../transformationTypes/resize';
+import { AspectRatio } from '../transformationTypes/aspectRatio';
+import { Gravity } from '../transformationTypes/gravity';
+import { Background } from '../transformationTypes/background';
+
+export interface CreateParseResizeParams {
+ parseHeight: (height: HeightOption) => string;
+ parseWidth: (width: WidthOption) => string;
+ parseGravity: (gravity: Gravity) => string;
+ parseAspectRatio: (aspectRatio: AspectRatio) => string;
+ parseBackground: (background: Background) => string;
+ parseIgnoreAspectRatio: (ignoreAspectRatio: boolean) => string;
+ parseZoom: (zoom: number) => string;
+}
+
+export const createParseResize =
+ ({ parseHeight, parseWidth, parseGravity, parseAspectRatio, parseBackground, parseIgnoreAspectRatio, parseZoom }: CreateParseResizeParams) =>
+ (resize: Resize): `c_${string}` => {
+ const parseWhenDefined =
+ (value: MaybeValue, parser: (value: Value) => string): string => {
+ if (typeof value === 'undefined') {
+ return '';
+ }
+
+ return `,${parser(value)}`;
+ };
+
+ if (!resize.mode) {
+ return `c_scale${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ }
+
+ switch (resize.mode) {
+ case 'auto':
+ return `c_auto${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'autoPadding':
+ return `c_auto_pad${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)}${
+ parseWhenDefined(resize.background, parseBackground)
+ }`;
+ case 'scale':
+ return `c_scale${
+ parseWhenDefined(resize.ignoreAspectRatio, parseIgnoreAspectRatio)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.gravity, () => parseGravity({ mode: 'special', position: 'liquid' }))}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'limited':
+ return `c_limit${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'padding':
+ case 'limitedPadding':
+ case 'minimumPadding':
+ return `${
+ resize.mode === 'padding' ? 'c_pad' : resize.mode === 'limitedPadding' ? 'c_lpad' : 'c_mpad'}${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.background, parseBackground)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'fit':
+ case 'minimumFit':
+ return `${
+ resize.mode === 'fit' ? 'c_fit' : 'c_mfit'}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'fill':
+ return `c_fill${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'limitedFill':
+ return `c_lfill${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'fillPadding':
+ return `c_fill_pad${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'crop':
+ return `c_crop${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'thumbnail':
+ return `c_thumb${
+ parseWhenDefined(resize.gravity, parseGravity)}${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)}${
+ parseWhenDefined(resize.zoom, parseZoom)
+ }`;
+ case 'imaggaCrop':
+ return `c_imagga_crop${
+ parseWhenDefined(resize.aspectRatio, parseAspectRatio)}${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ case 'imaggaScale':
+ return `c_imagga_scale${
+ parseWhenDefined(resize.width, parseWidth)}${
+ parseWhenDefined(resize.height, parseHeight)
+ }`;
+ }
+ };
diff --git a/packages/react/src/transformationParsers/parseRotate.ts b/packages/react/src/transformationParsers/parseRotate.ts
new file mode 100644
index 00000000..ba8cddf8
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseRotate.ts
@@ -0,0 +1,20 @@
+import { Rotate } from '../transformationTypes/rotate';
+
+export const parseRotate = (rotate: Rotate): `a_${string}` => {
+ if (typeof rotate === 'number') {
+ return `a_${rotate}`;
+ }
+
+ switch (rotate.mode) {
+ case 'auto-left':
+ return `a_auto_left.${rotate.angle}`;
+ case 'auto-right':
+ return `a_auto_left.${rotate.angle}`;
+ case 'horizontal-flip':
+ return `a_hflip.${rotate.angle}`;
+ case 'vertical-flip':
+ return `a_vflip.${rotate.angle}`;
+ case 'ignore-exif-data':
+ return `a_ignore.${rotate.angle}`;
+ }
+};
diff --git a/packages/react/src/transformationParsers/parseRoundCorners.ts b/packages/react/src/transformationParsers/parseRoundCorners.ts
new file mode 100644
index 00000000..ff85644d
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseRoundCorners.ts
@@ -0,0 +1,13 @@
+import { RoundCorners } from '../transformationTypes/roundCorners';
+
+export const parseRoundCorners = (roundCorners: RoundCorners): `r_${string}` => {
+ if (typeof roundCorners === 'number') {
+ return `r_${roundCorners}`;
+ }
+
+ if (roundCorners === 'maximum') {
+ return 'r_max';
+ }
+
+ return `r_:${roundCorners.join(':')}`
+}
diff --git a/packages/react/src/transformationParsers/parseStartOffset.ts b/packages/react/src/transformationParsers/parseStartOffset.ts
new file mode 100644
index 00000000..24e3348c
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseStartOffset.ts
@@ -0,0 +1,3 @@
+import { StartOffset } from '../transformationTypes/startOffset';
+
+export const parseStartOffset = (startOffset: StartOffset) => `so_${startOffset}`;
diff --git a/packages/react/src/transformationParsers/parseVideoCodec.ts b/packages/react/src/transformationParsers/parseVideoCodec.ts
new file mode 100644
index 00000000..dc01be93
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseVideoCodec.ts
@@ -0,0 +1,17 @@
+import { VideoCodec } from '../transformationTypes/videoCodec';
+
+export const parseVideoCodec = (videoCodec: VideoCodec): `vc_${string}` => {
+ if (typeof videoCodec === 'string') {
+ return `vc_${videoCodec}`;
+ }
+
+ if (videoCodec.includeBFrames === false) {
+ return `vc_${videoCodec.use}:${videoCodec.profile}:${videoCodec.level}:bframes_no:`
+ }
+
+ return `vc_${videoCodec.use}${
+ Object.is(videoCodec.profile, undefined) ? '' : `:${videoCodec.profile}`
+ }${
+ Object.is(videoCodec.level, undefined) ? '' : `:${videoCodec.level}`
+ }`;
+}
diff --git a/packages/react/src/transformationParsers/parseWidth.ts b/packages/react/src/transformationParsers/parseWidth.ts
new file mode 100644
index 00000000..b39a306a
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseWidth.ts
@@ -0,0 +1,3 @@
+import { WidthOption } from '../transformationTypes/resize';
+
+export const parseWidth = (width: WidthOption) => `w_${width}`;
diff --git a/packages/react/src/transformationParsers/parseZoom.ts b/packages/react/src/transformationParsers/parseZoom.ts
new file mode 100644
index 00000000..c43c4ef7
--- /dev/null
+++ b/packages/react/src/transformationParsers/parseZoom.ts
@@ -0,0 +1 @@
+export const parseZoom = (zoom: number) => `z_${zoom}`;
diff --git a/packages/react/src/transformationTypes/aspectRatio.ts b/packages/react/src/transformationTypes/aspectRatio.ts
new file mode 100644
index 00000000..514ed2db
--- /dev/null
+++ b/packages/react/src/transformationTypes/aspectRatio.ts
@@ -0,0 +1 @@
+export type AspectRatio = `${number}:${number}` | number;
diff --git a/packages/react/src/transformationTypes/background.ts b/packages/react/src/transformationTypes/background.ts
new file mode 100644
index 00000000..42bcf8b8
--- /dev/null
+++ b/packages/react/src/transformationTypes/background.ts
@@ -0,0 +1,30 @@
+export type Background =
+ | 'auto'
+ | {
+ type: 'color';
+ color: string;
+ }
+ | {
+ type: 'auto';
+ mode?: 'border' | 'predominant' | 'border-contrast' | 'predominant-contrast';
+ }
+ | {
+ type: 'auto';
+ mode: 'predominant-gradient' | 'predominant-gradient-contrast' | 'border-gradient' | 'border-gradient-contrast';
+ amountOfPredominantColorsToUse?: 2 | 4;
+ direction?: 'horizontal' | 'vertical' | 'diagonal-descending' | 'diagonal-ascending';
+ borderPalette?: string[];
+ }
+ | {
+ type: 'blurred';
+ }
+ | {
+ type: 'blurred';
+ intensity: number;
+ brightness?: number;
+ }
+ | {
+ type: 'generativeAiFill';
+ prompt?: string;
+ seed?: number;
+ };
diff --git a/packages/react/src/transformationTypes/duration.ts b/packages/react/src/transformationTypes/duration.ts
new file mode 100644
index 00000000..d7fa80e4
--- /dev/null
+++ b/packages/react/src/transformationTypes/duration.ts
@@ -0,0 +1 @@
+export type Duration = number | `${number}%`
diff --git a/packages/react/src/transformationTypes/effect.ts b/packages/react/src/transformationTypes/effect.ts
new file mode 100644
index 00000000..3454416e
--- /dev/null
+++ b/packages/react/src/transformationTypes/effect.ts
@@ -0,0 +1,92 @@
+// FIXME image only
+type Sepia = {
+ type: 'sepia';
+ level?: number;
+};
+// FIXME image only
+type BackgroundRemoval = {
+ type: 'removeBackground';
+ mode?: 'fineEdges';
+};
+
+type Fade = {
+ type: 'fade';
+ duration: number;
+}
+
+type Gamma = {
+ type: 'gamma';
+ /**
+ * @description -50 to 150
+ */
+ level: number;
+}
+// FIXME image only
+type Grayscale = {
+ type: 'grayscale'
+}
+
+type Light = {
+ type: 'light';
+ /**
+ * @default 30
+ */
+ intensity?: number;
+}
+
+type Negate = {
+ type: 'negate';
+}
+
+type Noise = {
+ type: 'noise';
+ /**
+ * @description 0 - 100
+ */
+ level: number;
+}
+
+type Pixelate = {
+ type: 'pixelate';
+ /**
+ * @description 1 - 200
+ * @default algorithm based
+ */
+ squareSize?: number;
+}
+
+type Blur = {
+ type: 'blur' | 'blurFaces';
+ /**
+ * @description 1 to 2000
+ */
+ strength?: number
+}
+
+type Auto = {
+ type: 'autoBrightness' | 'autoColor' | 'autoContrast';
+ /**
+ * @description 0 to 100
+ * @default 100
+ */
+ blendPercentage?: number;
+}
+
+export type ImageEffect =
+ | Sepia
+ | BackgroundRemoval
+ | Gamma
+ | Grayscale
+ | Light
+ | Negate
+ | Pixelate
+ | Blur
+ | Auto;
+
+export type VideoEffect =
+ | Fade
+ | Gamma
+ | Noise
+ | Blur;
+
+export type AnyEffect = ImageEffect | VideoEffect;
diff --git a/packages/react/src/transformationTypes/format.ts b/packages/react/src/transformationTypes/format.ts
new file mode 100644
index 00000000..f41a1cd9
--- /dev/null
+++ b/packages/react/src/transformationTypes/format.ts
@@ -0,0 +1,20 @@
+export type ImageFormat =
+ | 'auto'
+ | 'jpg'
+ | 'png'
+ | 'gif'
+ | 'webp'
+ | 'bmp'
+ | 'ico'
+ | 'pdf'
+ | 'tiff'
+ | 'eps'
+ | 'jpc'
+ | 'jp2'
+ | 'psd'
+ | 'svg'
+ | 'avif'
+ | 'heic'
+ | 'heif';
+
+export type VideoFormat = 'auto' | 'webm' | 'mp4' | 'mkv' | 'flv' | 'mov' | '3gp' | 'avi' | 'wmv';
diff --git a/packages/react/src/transformationTypes/gravity.ts b/packages/react/src/transformationTypes/gravity.ts
new file mode 100644
index 00000000..1fa2c5e6
--- /dev/null
+++ b/packages/react/src/transformationTypes/gravity.ts
@@ -0,0 +1,78 @@
+type CompassPosition =
+ | 'north'
+ | 'northWest'
+ | 'northEast'
+ | 'south'
+ | 'southWest'
+ | 'southEast'
+ | 'west'
+ | 'east'
+ | 'center';
+
+type DetectionAlgorithm = 'face' | 'faces' | 'advancedFace' | 'advancedFaces' | 'advancedEyes';
+
+type SpecialPosition = { mode: 'special'; } & ({
+ position: 'decomposeTile' | 'liquid' | 'ocrText';
+} | {
+ position: 'custom';
+ positionFallback?: DetectionAlgorithm;
+} | {
+ position: DetectionAlgorithm;
+ positionFallback?: CompassPosition;
+} | {
+ position: 'xy';
+ x: number;
+ y: number;
+});
+
+type Object = {
+ mode: 'object';
+ focus: string;
+}
+
+type Auto = 'auto' | {
+ mode: 'auto';
+ algorithm?: 'subject' | 'classic';
+ /**
+ * @default faces
+ */
+ priority?: string | string[];
+ /**
+ * @description 0 - 100
+ */
+ thumbAggressiveness?: number;
+ /**
+ *@default on
+ */
+ ruleOfThirds?: 'on' | 'off';
+};
+
+type ClippingPath = {
+ mode: 'clippingPath',
+ focus: string;
+};
+
+type Region = {
+ mode: 'region';
+ focus: string;
+ height: number;
+ width: number;
+};
+
+type TrackPerson = {
+ mode: 'trackPerson';
+ focus: string;
+ /**
+ * @default east
+ */
+ position?: CompassPosition;
+};
+
+export type Gravity =
+ | CompassPosition
+ | ClippingPath
+ | Region
+ | TrackPerson
+ | Auto
+ | SpecialPosition
+ | Object;
diff --git a/packages/react/src/transformationTypes/helpers.ts b/packages/react/src/transformationTypes/helpers.ts
new file mode 100644
index 00000000..7bf5a3e7
--- /dev/null
+++ b/packages/react/src/transformationTypes/helpers.ts
@@ -0,0 +1,23 @@
+// FIXME currently this type allows for passing
+// { height: number } | { height: undefined } instead of { height: number } | { height?: never }
+export type RequireAtLeastOneProperty<
+ Obj extends Record,
+ Keys extends keyof Obj = keyof Obj
+> = Keys extends infer A extends string
+ ? {
+ [K in Exclude]?: Obj[K];
+ } & { [K in A]: Obj[A] }
+ : never;
+
+export type RequireAtLeastTwoProperties<
+ Obj extends Record,
+ Keys extends keyof Obj = keyof Obj
+> = Keys extends infer FirstKey extends string
+ ? Exclude extends infer LimitedKeys extends string
+ ? LimitedKeys extends infer SecondKey extends string
+ ? {
+ [K in FirstKey | SecondKey]: Obj[K];
+ } & Partial
+ : never
+ : never
+ : never;
diff --git a/packages/react/src/transformationTypes/opacity.ts b/packages/react/src/transformationTypes/opacity.ts
new file mode 100644
index 00000000..cc4b3c28
--- /dev/null
+++ b/packages/react/src/transformationTypes/opacity.ts
@@ -0,0 +1 @@
+export type Opacity = number;
diff --git a/packages/react/src/transformationTypes/quality.ts b/packages/react/src/transformationTypes/quality.ts
new file mode 100644
index 00000000..a8007488
--- /dev/null
+++ b/packages/react/src/transformationTypes/quality.ts
@@ -0,0 +1,16 @@
+export type Quality =
+ | 'auto'
+ | 'auto:good'
+ | 'auto:eco'
+ | 'auto:low'
+ | 'auto:best'
+ | 'jpegmini'
+ | 'jpegmini:0'
+ | 'jpegmini:1'
+ | 'jpegmini:2'
+ | 'low'
+ | 'eco'
+ | 'medium'
+ | 'good'
+ | 'high'
+ | 'best';
diff --git a/packages/react/src/transformationTypes/removeBackground.ts b/packages/react/src/transformationTypes/removeBackground.ts
new file mode 100644
index 00000000..3c2848c5
--- /dev/null
+++ b/packages/react/src/transformationTypes/removeBackground.ts
@@ -0,0 +1 @@
+export type RemoveBackground = boolean | 'fineEdges';
diff --git a/packages/react/src/transformationTypes/resize.ts b/packages/react/src/transformationTypes/resize.ts
new file mode 100644
index 00000000..9acbf994
--- /dev/null
+++ b/packages/react/src/transformationTypes/resize.ts
@@ -0,0 +1,94 @@
+import { AspectRatio } from './aspectRatio';
+import { Gravity } from './gravity';
+import { RequireAtLeastOneProperty, RequireAtLeastTwoProperties } from './helpers';
+import { Background } from './background';
+
+export type WidthOption = 'auto' | 'initial-width' | number;
+
+export type HeightOption = 'auto' | 'initial-height' | number;
+
+interface SizeOptions {
+ width: WidthOption;
+ height: HeightOption;
+}
+
+// FIXME look through `mode` values and align to pattern
+type AutoResize = {
+ mode: 'auto';
+ gravity: Gravity;
+} & RequireAtLeastTwoProperties;
+
+type AutoPaddingResize = {
+ mode: 'autoPadding';
+ gravity: Gravity;
+ background?: Background;
+} & RequireAtLeastTwoProperties;
+
+export type ScaleResize = {
+ mode: 'scale';
+ gravity?: 'liquid';
+ ignoreAspectRatio?: boolean;
+} & RequireAtLeastOneProperty;
+
+type LimitedResize = {
+ mode: 'limited';
+} & RequireAtLeastTwoProperties;
+
+type PaddingResize = {
+ mode: 'padding' | 'limitedPadding' | 'minimumPadding';
+ gravity?: Gravity;
+ background?: Background;
+} & RequireAtLeastTwoProperties;
+
+type FitResize = {
+ mode: 'fit' | 'minimumFit';
+} & RequireAtLeastTwoProperties;
+
+type FillResize = {
+ mode: 'fill' | 'limitedFill';
+ gravity?: Gravity;
+} & RequireAtLeastTwoProperties;
+
+type FillPaddingResize = {
+ mode: 'fillPadding';
+ gravity: 'auto';
+ background?: Background;
+} & RequireAtLeastTwoProperties;
+
+type CropResize = {
+ mode: 'crop';
+ gravity?: Gravity;
+ x?: number;
+ y?: number;
+} & RequireAtLeastTwoProperties;
+
+type ThumbResize = {
+ mode: 'thumbnail';
+ gravity: Gravity;
+ zoom?: number;
+} & RequireAtLeastTwoProperties;
+
+type ImaggaCropResize = {
+ mode: 'imaggaCrop';
+ aspectRatio?: AspectRatio;
+} & RequireAtLeastOneProperty;
+
+type ImaggaScaleResize = {
+ mode: 'imaggaScale';
+} & RequireAtLeastTwoProperties;
+
+// FIXME improve error reporting (misinferring in tsc)
+export type Resize =
+ | (RequireAtLeastOneProperty & { mode?: never })
+ | ImaggaScaleResize
+ | ImaggaCropResize
+ | CropResize
+ | AutoResize
+ | ThumbResize
+ | ScaleResize
+ | FitResize
+ | LimitedResize
+ | PaddingResize
+ | FillResize
+ | AutoPaddingResize
+ | FillPaddingResize;
diff --git a/packages/react/src/transformationTypes/rotate.ts b/packages/react/src/transformationTypes/rotate.ts
new file mode 100644
index 00000000..7f6c9585
--- /dev/null
+++ b/packages/react/src/transformationTypes/rotate.ts
@@ -0,0 +1,7 @@
+export type Rotate =
+ | number
+ | {
+ // FIXME supposedly auto modes only work with crop, but there is no error when using it without
+ mode: 'vertical-flip' | 'horizontal-flip' | 'auto-left' | 'auto-right' | 'ignore-exif-data';
+ angle: number;
+ };
diff --git a/packages/react/src/transformationTypes/roundCorners.ts b/packages/react/src/transformationTypes/roundCorners.ts
new file mode 100644
index 00000000..490690eb
--- /dev/null
+++ b/packages/react/src/transformationTypes/roundCorners.ts
@@ -0,0 +1 @@
+export type RoundCorners = number | [number, number] | [number, number, number] | [number, number, number, number] | 'maximum';
diff --git a/packages/react/src/transformationTypes/startOffset.ts b/packages/react/src/transformationTypes/startOffset.ts
new file mode 100644
index 00000000..fb1f18e7
--- /dev/null
+++ b/packages/react/src/transformationTypes/startOffset.ts
@@ -0,0 +1 @@
+export type StartOffset = number | 'auto' | `${number}%`;
diff --git a/packages/react/src/transformationTypes/videoCodec.ts b/packages/react/src/transformationTypes/videoCodec.ts
new file mode 100644
index 00000000..22f9dd8b
--- /dev/null
+++ b/packages/react/src/transformationTypes/videoCodec.ts
@@ -0,0 +1,18 @@
+type Codecs = 'h264' | 'h265' | 'av1' | 'prores' | 'theora' | 'vp8' | 'vp9' | 'none';
+type H264Profile ='baseline' | 'main' | 'high' | 'high444' | 'auto';
+type H264Level = 3.0 | 3.1 | 4.0 | 4.1 | 4.2 | 5.0 | 5.1 | 5.2 | 'auto';
+
+export type VideoCodec = Codecs | {
+ use: 'h264';
+ profile?: H264Profile;
+ level?: H264Level;
+ /**
+ * @default true
+ */
+ includeBFrames?: true;
+} | {
+ use: 'h264';
+ includeBFrames: false;
+ profile: H264Profile;
+ level: H264Level;
+};
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
new file mode 100644
index 00000000..31851702
--- /dev/null
+++ b/packages/react/src/types.ts
@@ -0,0 +1,9 @@
+export type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
+
+export type TransformationNameToParser = {
+ [Key in Exclude]-?: (
+ value: Required[Key]
+ ) => string;
+};
+
+export type TransformationMap = never> = TransformationNameToParser>, ExcludedTransformations>
diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json
index 424f7e36..e86c018d 100644
--- a/packages/react/tsconfig.json
+++ b/packages/react/tsconfig.json
@@ -2,6 +2,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext",
+ "target": "ES2022",
"declarationDir" : "./dist",
"lib": [
"dom",
@@ -17,11 +18,9 @@
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
- "suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
- "target": "es5",
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -29,14 +28,21 @@
"resolveJsonModule": true,
"isolatedModules": false,
"emitDeclarationOnly": true,
- "types": ["jest", "node"],
- "baseUrl": "src"
+ "types": ["node"]
},
- "include": ["src/index.tsx"],
+ "include": ["./src/**/*.*"],
"exclude": [
"node_modules",
"dist",
"playground",
- "__tests__"
- ]
+ ".eslintrc.js"
+ ],
+ "ts-node": {
+ "compilerOptions": {
+ "module": "CommonJS",
+ "target": "es5",
+ "esModuleInterop": true,
+ "moduleResolution": "node"
+ }
+ }
}
diff --git a/packages/react/tsconfig.test.json b/packages/react/tsconfig.test.json
deleted file mode 100644
index 9c57932d..00000000
--- a/packages/react/tsconfig.test.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "module": "ES2015"
- }
-}
diff --git a/tsconfig.json b/tsconfig.json
index 0d7d47cc..0a38116f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,15 +6,15 @@
"moduleResolution": "node",
"baseUrl": "src",
"esModuleInterop": true,
- "resolveJsonModule": true
+ "resolveJsonModule": true,
+ "paths": {
+ "@cloudinary/html": ["html/src"],
+ "@cloudinary/ng": ["angular/projects/cloudinary-library/src"],
+ "@cloudinary/react": ["react/src"],
+ "@cloudinary/vue": ["vue/src"],
+ "@cloudinary/*": ["*/src"],
+ "*": ["node_modules"]
+ }
},
- "paths": {
- "@cloudinary/html": ["html/src"],
- "@cloudinary/ng": ["angular/projects/cloudinary-library/src"],
- "@cloudinary/react": ["react/src"],
- "@cloudinary/vue": ["vue/src"],
- "@cloudinary/*": ["*/src"],
- "*": ["node_modules"]
-},
"exclude": ["node_modules", "**/node_modules", "**/e2e", "**/dist", "**/build", "**/coverage"]
}