next-cloudinary 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,31 +9,26 @@ function _extends() {
9
9
  _extends = Object.assign ? Object.assign.bind() : function (target) {
10
10
  for (var i = 1; i < arguments.length; i++) {
11
11
  var source = arguments[i];
12
-
13
12
  for (var key in source) {
14
13
  if (Object.prototype.hasOwnProperty.call(source, key)) {
15
14
  target[key] = source[key];
16
15
  }
17
16
  }
18
17
  }
19
-
20
18
  return target;
21
19
  };
22
20
  return _extends.apply(this, arguments);
23
21
  }
24
-
25
22
  function _objectWithoutPropertiesLoose(source, excluded) {
26
23
  if (source == null) return {};
27
24
  var target = {};
28
25
  var sourceKeys = Object.keys(source);
29
26
  var key, i;
30
-
31
27
  for (i = 0; i < sourceKeys.length; i++) {
32
28
  key = sourceKeys[i];
33
29
  if (excluded.indexOf(key) >= 0) continue;
34
30
  target[key] = source[key];
35
31
  }
36
-
37
32
  return target;
38
33
  }
39
34
 
@@ -49,15 +44,12 @@ function plugin$6({
49
44
  crop = 'limit'
50
45
  } = options;
51
46
  let transformationString = `c_${crop},w_${width}`;
52
-
53
47
  if (!options.gravity && cropsGravityAuto.includes(crop)) {
54
48
  options.gravity = 'auto';
55
49
  }
56
-
57
50
  if (!['limit'].includes(crop)) {
58
51
  transformationString = `${transformationString},h_${height}`;
59
52
  }
60
-
61
53
  if (options.gravity) {
62
54
  if (options.gravity === 'auto' && !cropsGravityAuto.includes(crop)) {
63
55
  console.warn('Auto gravity can only be used with crop, fill, lfill, fill_pad or thumb. Not applying gravity.');
@@ -65,7 +57,6 @@ function plugin$6({
65
57
  transformationString = `${transformationString},g_${options.gravity}`;
66
58
  }
67
59
  }
68
-
69
60
  cldImage.resize(transformationString);
70
61
  }
71
62
 
@@ -129,7 +120,6 @@ function plugin$5({
129
120
  params.forEach(key => {
130
121
  const prop = key.prop || key;
131
122
  const effect = key.effect || key;
132
-
133
123
  if (options[prop] === true) {
134
124
  cldImage.effect(`e_${effect}`);
135
125
  } else if (typeof options[prop] === 'string') {
@@ -145,6 +135,7 @@ var effectsPlugin = {
145
135
  };
146
136
 
147
137
  // aspectRatio
138
+
148
139
  const primary = {
149
140
  aspectRatio: {
150
141
  qualifier: 'ar'
@@ -216,11 +207,9 @@ function plugin$4({
216
207
  } = options;
217
208
  const type = 'overlay';
218
209
  const typeQualifier = 'l';
219
-
220
210
  if (Array.isArray(overlays)) {
221
211
  overlays.forEach(applyOverlay);
222
212
  }
223
-
224
213
  if (typeof text$1 === 'string') {
225
214
  applyOverlay({
226
215
  text: _extends({}, DEFAULT_TEXT_OPTIONS, {
@@ -232,43 +221,44 @@ function plugin$4({
232
221
  text: _extends({}, DEFAULT_TEXT_OPTIONS, text$1)
233
222
  });
234
223
  }
224
+
235
225
  /**
236
226
  * applyOverlay
237
227
  */
238
228
 
239
-
240
229
  function applyOverlay(_ref) {
241
230
  let {
242
- publicId,
243
- position: position$1,
244
- text: text$1,
245
- effects: layerEffects = []
246
- } = _ref,
247
- options = _objectWithoutPropertiesLoose(_ref, _excluded$3);
248
-
231
+ publicId,
232
+ position: position$1,
233
+ text: text$1,
234
+ effects: layerEffects = []
235
+ } = _ref,
236
+ options = _objectWithoutPropertiesLoose(_ref, _excluded$3);
249
237
  const hasPublicId = typeof publicId === 'string';
250
238
  const hasText = typeof text$1 === 'object' || typeof text$1 === 'string';
251
239
  const hasPosition = typeof position$1 === 'object';
252
-
253
240
  if (!hasPublicId && !hasText) {
254
241
  console.warn(`An ${type} is missing Public ID or Text`);
255
242
  return;
256
- } // Start to construct the transformation string using text or the public ID
257
- // if it's image-based
243
+ }
258
244
 
245
+ // Start to construct the transformation string using text or the public ID
246
+ // if it's image-based
259
247
 
260
248
  let layerTransformation;
261
-
262
249
  if (hasText) {
263
250
  layerTransformation = `${typeQualifier}_text`;
264
251
  } else {
265
252
  layerTransformation = `${typeQualifier}_${publicId.replace(/\//g, ':')}`;
266
- } // Begin organizing transformations based on what it is and the location
267
- // it needs to be placed in the URL
253
+ }
268
254
 
255
+ // Begin organizing transformations based on what it is and the location
256
+ // it needs to be placed in the URL
269
257
 
270
258
  const primary$1 = [];
271
- const applied = []; // Gemeral options
259
+ const applied = [];
260
+
261
+ // Gemeral options
272
262
 
273
263
  Object.keys(options).forEach(key => {
274
264
  if (!primary[key]) return;
@@ -276,7 +266,9 @@ function plugin$4({
276
266
  qualifier
277
267
  } = primary[key];
278
268
  primary$1.push(`${qualifier}_${options[key]}`);
279
- }); // Layer effects
269
+ });
270
+
271
+ // Layer effects
280
272
 
281
273
  layerEffects.forEach(effect => {
282
274
  Object.keys(effect).forEach(key => {
@@ -286,7 +278,9 @@ function plugin$4({
286
278
  } = primary[key];
287
279
  primary$1.push(`${qualifier}_${effect[key]}`);
288
280
  });
289
- }); // Text styling
281
+ });
282
+
283
+ // Text styling
290
284
 
291
285
  if (hasText) {
292
286
  if (typeof text$1 === 'string') {
@@ -294,7 +288,6 @@ function plugin$4({
294
288
  text: text$1
295
289
  });
296
290
  }
297
-
298
291
  const textTransformations = [];
299
292
  Object.keys(text$1).forEach(key => {
300
293
  if (!text[key]) return;
@@ -302,7 +295,6 @@ function plugin$4({
302
295
  qualifier,
303
296
  location
304
297
  } = text[key];
305
-
306
298
  if (location === 'primary') {
307
299
  primary$1.push(`${qualifier}_${text$1[key]}`);
308
300
  } else {
@@ -310,8 +302,9 @@ function plugin$4({
310
302
  }
311
303
  });
312
304
  layerTransformation = `${layerTransformation}:${textTransformations.join('_')}:${text$1.text}`;
313
- } // Positioning
305
+ }
314
306
 
307
+ // Positioning
315
308
 
316
309
  if (hasPosition) {
317
310
  Object.keys(position$1).forEach(key => {
@@ -321,17 +314,20 @@ function plugin$4({
321
314
  } = position[key];
322
315
  applied.push(`${qualifier}_${position$1[key]}`);
323
316
  });
324
- } // Add all primary transformations
317
+ }
325
318
 
319
+ // Add all primary transformations
326
320
 
327
- layerTransformation = `${layerTransformation},${primary$1.join(',')}`; // Add all applied transformations
321
+ layerTransformation = `${layerTransformation},${primary$1.join(',')}`;
328
322
 
329
- layerTransformation = `${layerTransformation}/fl_layer_apply,fl_no_overflow`;
323
+ // Add all applied transformations
330
324
 
325
+ layerTransformation = `${layerTransformation}/fl_layer_apply,fl_no_overflow`;
331
326
  if (applied.length > 0) {
332
327
  layerTransformation = `${layerTransformation},${applied.join(',')}`;
333
- } // Finally add it to the image
328
+ }
334
329
 
330
+ // Finally add it to the image
335
331
 
336
332
  cldImage.addTransformation(layerTransformation);
337
333
  }
@@ -371,7 +367,6 @@ function plugin$2({
371
367
  const {
372
368
  removeBackground = false
373
369
  } = options;
374
-
375
370
  if (removeBackground) {
376
371
  cldImage.effect('e_background_removal');
377
372
  }
@@ -394,56 +389,54 @@ function plugin$1({
394
389
  underlays = []
395
390
  } = options;
396
391
  const typeQualifier = 'u';
397
-
398
392
  if (Array.isArray(underlays)) {
399
393
  underlays.forEach(applyUnderlay);
400
394
  }
401
-
402
395
  if (typeof underlay === 'string') {
403
396
  const underlayOptions = {
404
397
  publicId: underlay,
405
398
  crop: 'fill'
406
399
  };
407
-
408
400
  if (options.width) {
409
401
  underlayOptions.width = options.width;
410
402
  }
411
-
412
403
  if (options.height) {
413
404
  underlayOptions.height = options.height;
414
405
  }
415
-
416
406
  applyUnderlay(underlayOptions);
417
407
  }
408
+
418
409
  /**
419
410
  * applyUnderlay
420
411
  */
421
412
 
422
-
423
413
  function applyUnderlay(_ref) {
424
414
  let {
425
- publicId,
426
- type,
427
- position: position$1,
428
- effects: layerEffects = []
429
- } = _ref,
430
- options = _objectWithoutPropertiesLoose(_ref, _excluded$2);
431
-
415
+ publicId,
416
+ type,
417
+ position: position$1,
418
+ effects: layerEffects = []
419
+ } = _ref,
420
+ options = _objectWithoutPropertiesLoose(_ref, _excluded$2);
432
421
  const hasPublicId = typeof publicId === 'string';
433
422
  const hasPosition = typeof position$1 === 'object';
434
-
435
423
  if (!hasPublicId) {
436
424
  console.warn(`An ${type} is missing a Public ID`);
437
425
  return;
438
- } // Start to construct the transformation string using text or the public ID
426
+ }
427
+
428
+ // Start to construct the transformation string using text or the public ID
439
429
  // if it's image-based
440
430
 
431
+ let layerTransformation = `${typeQualifier}_${publicId.replace(/\//g, ':')}`;
441
432
 
442
- let layerTransformation = `${typeQualifier}_${publicId.replace(/\//g, ':')}`; // Begin organizing transformations based on what it is and the location
433
+ // Begin organizing transformations based on what it is and the location
443
434
  // it needs to be placed in the URL
444
435
 
445
436
  const primary$1 = [];
446
- const applied = []; // Gemeral options
437
+ const applied = [];
438
+
439
+ // Gemeral options
447
440
 
448
441
  Object.keys(options).forEach(key => {
449
442
  if (!primary[key]) return;
@@ -451,7 +444,9 @@ function plugin$1({
451
444
  qualifier
452
445
  } = primary[key];
453
446
  primary$1.push(`${qualifier}_${options[key]}`);
454
- }); // Layer effects
447
+ });
448
+
449
+ // Layer effects
455
450
 
456
451
  layerEffects.forEach(effect => {
457
452
  Object.keys(effect).forEach(key => {
@@ -461,7 +456,9 @@ function plugin$1({
461
456
  } = primary[key];
462
457
  primary$1.push(`${qualifier}_${effect[key]}`);
463
458
  });
464
- }); // Positioning
459
+ });
460
+
461
+ // Positioning
465
462
 
466
463
  if (hasPosition) {
467
464
  Object.keys(position$1).forEach(key => {
@@ -471,17 +468,20 @@ function plugin$1({
471
468
  } = position[key];
472
469
  applied.push(`${qualifier}_${position$1[key]}`);
473
470
  });
474
- } // Add all primary transformations
471
+ }
475
472
 
473
+ // Add all primary transformations
476
474
 
477
- layerTransformation = `${layerTransformation},${primary$1.join(',')}`; // Add all applied transformations
475
+ layerTransformation = `${layerTransformation},${primary$1.join(',')}`;
478
476
 
479
- layerTransformation = `${layerTransformation}/fl_layer_apply,fl_no_overflow`;
477
+ // Add all applied transformations
480
478
 
479
+ layerTransformation = `${layerTransformation}/fl_layer_apply,fl_no_overflow`;
481
480
  if (applied.length > 0) {
482
481
  layerTransformation = `${layerTransformation},${applied.join(',')}`;
483
- } // Finally add it to the image
482
+ }
484
483
 
484
+ // Finally add it to the image
485
485
 
486
486
  cldImage.addTransformation(layerTransformation);
487
487
  }
@@ -502,7 +502,6 @@ function plugin({
502
502
  zoompan = false
503
503
  } = options;
504
504
  const overrides = {};
505
-
506
505
  if (zoompan === true) {
507
506
  cldImage.effect('e_zoompan');
508
507
  } else if (typeof zoompan === 'string') {
@@ -514,29 +513,23 @@ function plugin({
514
513
  }
515
514
  } else if (typeof zoompan === 'object') {
516
515
  let zoompanEffect = 'e_zoompan';
517
-
518
516
  if (typeof zoompan.options === 'string') {
519
517
  zoompanEffect = `${zoompanEffect}${zoompan.options}`;
520
518
  }
521
-
522
519
  cldImage.effect(zoompanEffect);
523
520
  let loopEffect;
524
-
525
521
  if (zoompan.loop === true) {
526
522
  loopEffect = 'e_loop';
527
523
  } else if (typeof zoompan.loop === 'string') {
528
524
  loopEffect = `e_loop${zoompan.loop}`;
529
525
  }
530
-
531
526
  if (loopEffect) {
532
527
  cldImage.effect(loopEffect);
533
528
  }
534
529
  }
535
-
536
530
  if (zoompan !== false) {
537
531
  overrides.format = 'gif';
538
532
  }
539
-
540
533
  return {
541
534
  options: overrides
542
535
  };
@@ -548,11 +541,15 @@ var zoompanPlugin = {
548
541
  plugin: plugin
549
542
  };
550
543
 
551
- const transformationPlugins = [// Background Removal must always come first
552
- removeBackgroundPlugin, croppingPlugin, effectsPlugin, overlaysPlugin, underlaysPlugin, zoompanPlugin, // Raw transformations needs to be last simply to make sure
544
+ const transformationPlugins = [
545
+ // Background Removal must always come first
546
+ removeBackgroundPlugin, croppingPlugin, effectsPlugin, overlaysPlugin, underlaysPlugin, zoompanPlugin,
547
+ // Raw transformations needs to be last simply to make sure
553
548
  // it's always expected to applied the same way
549
+
554
550
  rawTransformationsPlugin];
555
551
  let cld;
552
+
556
553
  /**
557
554
  * constructCloudinaryUrl
558
555
  */
@@ -572,7 +569,6 @@ function constructCloudinaryUrl({
572
569
  }
573
570
  }, config));
574
571
  }
575
-
576
572
  const publicId = getPublicId(options.src);
577
573
  const cldImage = cld.image(publicId);
578
574
  transformationPlugins.forEach(({
@@ -584,13 +580,13 @@ function constructCloudinaryUrl({
584
580
  cldImage,
585
581
  options
586
582
  }) || {};
587
-
588
583
  if (pluginOptions != null && pluginOptions.format) {
589
584
  options.format = pluginOptions.format;
590
585
  }
591
586
  });
592
587
  return cldImage.setDeliveryType(options.deliveryType || 'upload').format(options.format || 'auto').delivery(`q_${options.quality || 'auto'}`).toURL();
593
588
  }
589
+
594
590
  /**
595
591
  * Retrieves the public id of a cloudiary image url. If no url is recognized it returns the parameter it self.
596
592
  * If it's recognized that is a url and it's not possible to get the public id, it warns the user.
@@ -604,13 +600,11 @@ function getPublicId(src) {
604
600
  if (typeof src !== 'string') {
605
601
  throw new Error(`Invalid src of type ${typeof src}`);
606
602
  }
607
-
608
603
  if (src.includes('res.cloudinary.com')) {
609
604
  const regexWithTransformations = /(https?)\:\/\/(res.cloudinary.com)\/([^\/]+)\/(image|video|raw)\/(upload|authenticated)\/(.*)\/(v[0-9]+)\/(.+)(?:\.[a-z]{3})?/;
610
605
  const regexWithoutTransformations = /(https?)\:\/\/(res.cloudinary.com)\/([^\/]+)\/(image|video|raw)\/(upload|authenticated)\/(v[0-9]+)\/(.+)(?:\.[a-z]{3})?/;
611
606
  const withTransformations = src.match(regexWithTransformations);
612
607
  const withoutTransformations = src.match(regexWithoutTransformations);
613
-
614
608
  if (withTransformations) {
615
609
  return withTransformations[withTransformations.length - 1];
616
610
  } else if (withoutTransformations) {
@@ -619,9 +613,9 @@ function getPublicId(src) {
619
613
  console.warn(`Not possible to retrieve the publicUrl from ${src}, make sure it's a valid cloudinary image url.`);
620
614
  }
621
615
  }
622
-
623
616
  return src;
624
617
  }
618
+
625
619
  /**
626
620
  * createPlaceholderUrl
627
621
  */
@@ -632,17 +626,14 @@ function createPlaceholderUrl({
632
626
  config
633
627
  }) {
634
628
  const rawTransformations = [];
635
-
636
629
  if (placeholder === 'grayscale') {
637
630
  rawTransformations.push('e_grayscale');
638
631
  }
639
-
640
632
  if (typeof placeholder === 'string' && placeholder.includes('color:')) {
641
633
  const color = placeholder.split(':').splice(1).join(':');
642
634
  rawTransformations.push('e_grayscale');
643
635
  rawTransformations.push(`e_colorize:60,co_${color}`);
644
636
  }
645
-
646
637
  return constructCloudinaryUrl({
647
638
  options: {
648
639
  src,
@@ -670,20 +661,25 @@ const CldImage = props => {
670
661
  if (CLD_OPTIONS.includes(prop)) {
671
662
  throw new Error(`Option ${prop} already exists!`);
672
663
  }
673
-
674
664
  CLD_OPTIONS.push(prop);
675
665
  });
676
- }); // Construct the base Image component props by filtering out Cloudinary-specific props
666
+ });
667
+
668
+ // Construct the base Image component props by filtering out Cloudinary-specific props
677
669
 
678
670
  const imageProps = {};
679
- Object.keys(props).filter(key => !CLD_OPTIONS.includes(key)).forEach(key => imageProps[key] = props[key]); // Construct Cloudinary-specific props by looking for values for any of the supported prop keys
671
+ Object.keys(props).filter(key => !CLD_OPTIONS.includes(key)).forEach(key => imageProps[key] = props[key]);
672
+
673
+ // Construct Cloudinary-specific props by looking for values for any of the supported prop keys
680
674
 
681
675
  const cldOptions = {};
682
676
  CLD_OPTIONS.forEach(key => {
683
677
  if (props[key]) {
684
678
  cldOptions[key] = props[key];
685
679
  }
686
- }); // If we see a placeholder option, configure a Cloudinary-based URL.
680
+ });
681
+
682
+ // If we see a placeholder option, configure a Cloudinary-based URL.
687
683
  // The resulting image will always be blurred per Next.js, so we're
688
684
  // limited on options for placeholders.
689
685
  //
@@ -698,12 +694,10 @@ const CldImage = props => {
698
694
  src: publicId,
699
695
  placeholder: props.placeholder
700
696
  });
701
-
702
697
  if (imageProps.placeholder !== 'blur') {
703
698
  imageProps.placeholder = 'blur';
704
699
  }
705
700
  }
706
-
707
701
  return /*#__PURE__*/jsx(Image, _extends({}, imageProps, {
708
702
  loader: options => cloudinaryLoader(_extends({}, imageProps, {
709
703
  options
@@ -715,24 +709,23 @@ const _excluded$1 = ["excludeTags", "twitterTitle"];
715
709
  const IMAGE_WIDTH = 2400;
716
710
  const IMAGE_HEIGHT = 1200;
717
711
  const TWITTER_CARD = 'summary_large_image';
718
-
719
712
  const CldOgImage = _ref => {
720
713
  let {
721
- excludeTags = [],
722
- twitterTitle
723
- } = _ref,
724
- props = _objectWithoutPropertiesLoose(_ref, _excluded$1);
725
-
714
+ excludeTags = [],
715
+ twitterTitle
716
+ } = _ref,
717
+ props = _objectWithoutPropertiesLoose(_ref, _excluded$1);
726
718
  const options = _extends({}, props, {
727
719
  width: props.width || IMAGE_WIDTH,
728
720
  height: props.height || IMAGE_HEIGHT,
729
721
  crop: props.crop || 'fill',
730
722
  gravity: props.gravity || 'center'
731
723
  });
732
-
733
724
  const ogImageUrl = constructCloudinaryUrl({
734
725
  options
735
- }); // We need to include the tags within the Next.js Head component rather than
726
+ });
727
+
728
+ // We need to include the tags within the Next.js Head component rather than
736
729
  // direcly adding them inside of the Head otherwise we get unexpected results
737
730
 
738
731
  return /*#__PURE__*/jsxs(Head, {
@@ -773,6 +766,7 @@ const CldUploadWidget = ({
773
766
  }) => {
774
767
  const cloudinary = useRef();
775
768
  const widget = useRef();
769
+
776
770
  /**
777
771
  * generateSignature
778
772
  * @description Makes a request to an endpoint to sign Cloudinary parameters as part of widget creation
@@ -790,28 +784,27 @@ const CldUploadWidget = ({
790
784
  callback(signature);
791
785
  });
792
786
  }
787
+
793
788
  /**
794
789
  * createWidget
795
790
  * @description Creates a new instance of the Cloudinary widget and stores in a ref
796
791
  */
797
792
 
798
-
799
793
  function createWidget() {
800
794
  var _cloudinary$current;
801
-
802
795
  // When creating a signed upload, you need to provide both your Cloudinary API Key
803
796
  // as well as a signature generator function that will sign any paramters
804
797
  // either on page load or during the upload process. Read more about signed uploads at:
805
798
  // https://cloudinary.com/documentation/upload_widget#signed_uploads
799
+
806
800
  const totalOptions = _extends({}, options, !!signed && {
807
801
  uploadSignature: generateSignature
808
- }); // no need for apiSecret because of api/sign-cloudinary-params
809
-
802
+ });
810
803
 
804
+ // no need for apiSecret because of api/sign-cloudinary-params
811
805
  if (signed && !totalOptions.apiKey) {
812
806
  return new Error("Signed Upload needs apiKey!");
813
807
  }
814
-
815
808
  return (_cloudinary$current = cloudinary.current) == null ? void 0 : _cloudinary$current.createUploadWidget(totalOptions, function (error, result) {
816
809
  // The callback is a bit more chatty than failed or success so
817
810
  // only trigger when one of those are the case. You can additionally
@@ -822,29 +815,27 @@ const CldUploadWidget = ({
822
815
  }
823
816
  });
824
817
  }
818
+
825
819
  /**
826
820
  * open
827
821
  * @description When triggered, uses the current widget instance to open the upload modal
828
822
  */
829
823
 
830
-
831
824
  function open() {
832
825
  if (!(widget != null && widget.current)) {
833
826
  widget.current = createWidget();
834
827
  }
835
-
836
828
  (widget == null ? void 0 : widget.current) && widget.current.open();
837
829
  }
830
+
838
831
  /**
839
832
  * handleOnLoad
840
833
  * @description Stores the Cloudinary window instance to a ref when the widget script loads
841
834
  */
842
835
 
843
-
844
836
  function handleOnLoad() {
845
837
  cloudinary.current = window.cloudinary;
846
838
  }
847
-
848
839
  return /*#__PURE__*/jsxs(Fragment, {
849
840
  children: [children({
850
841
  cloudinary: cloudinary.current,
@@ -862,16 +853,14 @@ const CldUploadWidget = ({
862
853
  };
863
854
 
864
855
  const _excluded = ["onUpload", "options", "signed", "label", "children"];
865
-
866
856
  const CldUploadButton = _ref => {
867
857
  let {
868
- onUpload,
869
- options,
870
- signed,
871
- children
872
- } = _ref,
873
- props = _objectWithoutPropertiesLoose(_ref, _excluded);
874
-
858
+ onUpload,
859
+ options,
860
+ signed,
861
+ children
862
+ } = _ref,
863
+ props = _objectWithoutPropertiesLoose(_ref, _excluded);
875
864
  return /*#__PURE__*/jsx(Fragment, {
876
865
  children: /*#__PURE__*/jsx(CldUploadWidget, {
877
866
  signed: signed,
@@ -885,7 +874,6 @@ const CldUploadButton = _ref => {
885
874
  e.preventDefault();
886
875
  open();
887
876
  }
888
-
889
877
  return /*#__PURE__*/jsx("button", _extends({
890
878
  onClick: handleOnClick
891
879
  }, props, {