alchemy-media 0.7.6 → 0.9.0-alpha

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.9.0-alpha.1 (2024-02-15)
2
+
3
+ * Upgrade to Alchemy v1.4.0
4
+
5
+ ## 0.8.0 (2023-10-17)
6
+
7
+ * Upgrade `@11ways/exiv2` to v0.7.0
8
+
1
9
  ## 0.7.6 (2023-10-05)
2
10
 
3
11
  * Let `Media#imageUrl()` helper handle image instructions that start with a slash
package/bootstrap.js CHANGED
@@ -1,150 +1,7 @@
1
- var path = alchemy.use('path'),
2
- Veronica = alchemy.use('veronica'),
3
- fs = alchemy.use('fs');
4
-
5
- // Define the default options
6
- var options = {
7
-
8
- // The path to use in the url
9
- url: '/media/image',
10
-
11
- // Where to store the files
12
- path: path.resolve(PATH_ROOT, 'files'),
13
-
14
- // Which hash function to use
15
- hash: 'sha1',
16
-
17
- // Enable webp
18
- webp: true,
19
-
20
- // Temporary map for intermediate file changes
21
- scratch: path.resolve(PATH_TEMP, 'scratch'),
22
-
23
- // The cache map for resized images & thumbnails
24
- cache: path.resolve(PATH_TEMP, 'imagecache'),
25
-
26
- // Path to fontawesome pro
27
- fontawesome_pro: null,
28
- };
29
-
30
- // Inject the user-overridden options
31
- alchemy.plugins.media = Object.assign(options, alchemy.plugins.media);
32
-
33
- // Find the paths to these binaries
34
- options.convert = alchemy.findPathToBinarySync('convert', options.convert);
35
- options.exiv2 = alchemy.findPathToBinarySync('exiv2', options.exiv2);
36
- options.cwebp = alchemy.findPathToBinarySync('cwebp', options.cwebp);
37
-
38
- // Make sure these folders exist
39
- alchemy.createDir(options.scratch);
40
- alchemy.createDir(options.cache);
41
-
42
- // Create routes
43
- Router.add({
44
- name : 'MediaFile#serveStatic',
45
- methods : ['get'],
46
- can_be_postponed : false,
47
- paths : /\/media\/static\/(.*)*/,
48
- });
49
-
50
- Router.add({
51
- name : 'MediaFile#image',
52
- methods : ['get'],
53
- can_be_postponed : false,
54
- paths : options.url + '/{id}',
55
- });
56
-
57
- // The prefix is added at the end of the route so it does not
58
- // change the user's active_prefix
59
- Router.add({
60
- name : 'MediaFile#data',
61
- methods : ['get'],
62
- can_be_postponed : false,
63
- paths : '/media/data/{prefix}/{id}',
64
- });
65
-
66
- Router.add({
67
- name : 'MediaFile#info',
68
- methods : ['get'],
69
- can_be_postponed : false,
70
- paths : '/media/info',
71
- });
72
-
73
- Router.add({
74
- name : 'MediaFile#recordsource',
75
- methods : ['get'],
76
- can_be_postponed : false,
77
- paths : '/media/recordsource',
78
- permission : 'media.recordsource',
79
- });
80
-
81
- // Allow dummy extensions
82
- Router.add({
83
- name : 'Media#fileextension',
84
- methods : ['get'],
85
- can_be_postponed : false,
86
- paths : '/media/file/{id}.{extension}',
87
- handler : 'MediaFile#file'
88
- });
89
-
90
- // Allow direct file downloads
91
- Router.add({
92
- name : 'MediaFile#file',
93
- methods : ['get'],
94
- can_be_postponed : false,
95
- paths : '/media/file/{id}',
96
- });
97
-
98
- Router.add({
99
- name : 'MediaFile#downloadFile',
100
- methods : ['get'],
101
- can_be_postponed : false,
102
- paths : '/media/download/{id}',
103
- });
104
-
105
- Router.add({
106
- name : 'MediaFile#thumbnail',
107
- methods : ['get'],
108
- can_be_postponed : false,
109
- paths : '/media/thumbnail/{id}',
110
- });
111
-
112
- Router.add({
113
- name : 'MediaFile#placeholder',
114
- methods : ['get'],
115
- can_be_postponed : false,
116
- paths : '/media/placeholder',
117
- });
118
-
119
- Router.add({
120
- name : 'MediaFile#upload',
121
- methods : ['post'],
122
- can_be_postponed : false,
123
- paths : '/media/upload',
124
- });
125
-
126
- Router.add({
127
- name : 'MediaFile#uploadsingle',
128
- methods : ['post'],
129
- can_be_postponed : false,
130
- paths : '/media/uploadsingle',
131
- });
132
-
133
- var profiles = alchemy.shared('Media.profiles');
134
-
135
- // Create a new veronica instance
136
- options.veronica = new Veronica({
137
- cwebp: options.cwebp,
138
- temp: options.scratch,
139
- cache: options.cache
140
- });
1
+ const profiles = alchemy.shared('Media.profiles');
141
2
 
142
3
  alchemy.hawkejs.addRawHtml(`<script>document.cookie = 'mediaResolution=' + encodeURIComponent(JSON.stringify({width:Math.max(screen.availWidth||0, window.outerWidth||0) || 1024,height:Math.max(screen.availHeight||0, window.outerHeight||0) || 768,dpr:window.devicePixelRatio}))</script>`);
143
4
 
144
- if (options.fontawesome_pro) {
145
- alchemy.exposeStatic('fontawesome_pro', options.fontawesome_pro);
146
- }
147
-
148
5
  /**
149
6
  * Register a profile under the given name
150
7
  *
@@ -155,11 +12,11 @@ if (options.fontawesome_pro) {
155
12
  * @param {String} name
156
13
  * @param {Object} settings
157
14
  */
158
- options.addProfile = function addProfile(name, settings) {
15
+ Plugin.addProfile = function addProfile(name, settings) {
159
16
  profiles[name] = settings;
160
17
  };
161
18
 
162
19
  // Add the thumbnail profile
163
- options.addProfile('thumbnail', {width: 100, height: 100});
164
- options.addProfile('pickerThumb', {width: 250, height: 250});
165
- options.addProfile('chimera-gallery', {width: 400, height: 400});
20
+ Plugin.addProfile('thumbnail', {width: 100, height: 100});
21
+ Plugin.addProfile('pickerThumb', {width: 250, height: 250});
22
+ Plugin.addProfile('chimera-gallery', {width: 400, height: 400});
@@ -0,0 +1,92 @@
1
+ let url_prefix = '/media/image';
2
+
3
+ // Create routes
4
+ Plugin.addRoute({
5
+ name : 'MediaFile#serveStatic',
6
+ methods : ['get'],
7
+ can_be_postponed : false,
8
+ paths : /\/media\/static\/(.*)*/,
9
+ });
10
+
11
+ Plugin.addRoute({
12
+ name : 'MediaFile#image',
13
+ methods : ['get'],
14
+ can_be_postponed : false,
15
+ paths : url_prefix + '/{id}',
16
+ });
17
+
18
+ // The prefix is added at the end of the route so it does not
19
+ // change the user's active_prefix
20
+ Plugin.addRoute({
21
+ name : 'MediaFile#data',
22
+ methods : ['get'],
23
+ can_be_postponed : false,
24
+ paths : '/media/data/{prefix}/{id}',
25
+ });
26
+
27
+ Plugin.addRoute({
28
+ name : 'MediaFile#info',
29
+ methods : ['get'],
30
+ can_be_postponed : false,
31
+ paths : '/media/info',
32
+ });
33
+
34
+ Plugin.addRoute({
35
+ name : 'MediaFile#recordsource',
36
+ methods : ['get'],
37
+ can_be_postponed : false,
38
+ paths : '/media/recordsource',
39
+ permission : 'media.recordsource',
40
+ });
41
+
42
+ // Allow dummy extensions
43
+ Plugin.addRoute({
44
+ name : 'Media#fileextension',
45
+ methods : ['get'],
46
+ can_be_postponed : false,
47
+ paths : '/media/file/{id}.{extension}',
48
+ handler : 'MediaFile#file'
49
+ });
50
+
51
+ // Allow direct file downloads
52
+ Plugin.addRoute({
53
+ name : 'MediaFile#file',
54
+ methods : ['get'],
55
+ can_be_postponed : false,
56
+ paths : '/media/file/{id}',
57
+ });
58
+
59
+ Plugin.addRoute({
60
+ name : 'MediaFile#downloadFile',
61
+ methods : ['get'],
62
+ can_be_postponed : false,
63
+ paths : '/media/download/{id}',
64
+ });
65
+
66
+ Plugin.addRoute({
67
+ name : 'MediaFile#thumbnail',
68
+ methods : ['get'],
69
+ can_be_postponed : false,
70
+ paths : '/media/thumbnail/{id}',
71
+ });
72
+
73
+ Plugin.addRoute({
74
+ name : 'MediaFile#placeholder',
75
+ methods : ['get'],
76
+ can_be_postponed : false,
77
+ paths : '/media/placeholder',
78
+ });
79
+
80
+ Plugin.addRoute({
81
+ name : 'MediaFile#upload',
82
+ methods : ['post'],
83
+ can_be_postponed : false,
84
+ paths : '/media/upload',
85
+ });
86
+
87
+ Plugin.addRoute({
88
+ name : 'MediaFile#uploadsingle',
89
+ methods : ['post'],
90
+ can_be_postponed : false,
91
+ paths : '/media/uploadsingle',
92
+ });
@@ -0,0 +1,111 @@
1
+ const libpath = require('path');
2
+
3
+ // system.plugins.media
4
+ const MEDIA_PLUGIN_GROUP = Plugin.getSettingsGroup();
5
+
6
+ MEDIA_PLUGIN_GROUP.addSetting('translatable', {
7
+ type : 'boolean',
8
+ default : false,
9
+ description : 'Should file title & alt fields be translatable?',
10
+ });
11
+
12
+ MEDIA_PLUGIN_GROUP.addSetting('file_hash_algorithm', {
13
+ type : 'string',
14
+ default : 'sha1',
15
+ description : 'The default file hash method',
16
+ });
17
+
18
+ MEDIA_PLUGIN_GROUP.addSetting('enable_webp', {
19
+ type : 'boolean',
20
+ default : true,
21
+ description : 'Serve webp images when possible',
22
+ });
23
+
24
+ MEDIA_PLUGIN_GROUP.addSetting('file_storage_path', {
25
+ type : 'string',
26
+ default : libpath.resolve(PATH_ROOT, 'files'),
27
+ description : 'Where to store uploaded files',
28
+ });
29
+
30
+ MEDIA_PLUGIN_GROUP.addSetting('scratch_path', {
31
+ type : 'string',
32
+ default : libpath.resolve(PATH_TEMP, 'scratch'),
33
+ description : 'Temporary map for intermediate file changes',
34
+ action : (value, value_instance) => {
35
+
36
+ if (value) {
37
+ alchemy.createDir(value);
38
+ }
39
+
40
+ createVeronicaInstance();
41
+ },
42
+ });
43
+
44
+ MEDIA_PLUGIN_GROUP.addSetting('cache_path', {
45
+ type : 'string',
46
+ default : libpath.resolve(PATH_TEMP, 'imagecache'),
47
+ description : 'The cache map for resized images & thumbnails',
48
+ action : (value, value_instance) => {
49
+
50
+ if (value) {
51
+ alchemy.createDir(value);
52
+ }
53
+
54
+ createVeronicaInstance();
55
+ },
56
+ });
57
+
58
+ MEDIA_PLUGIN_GROUP.addSetting('fontawesome_pro', {
59
+ type : 'string',
60
+ default : null,
61
+ description : 'The URL to fontawesome pro',
62
+ action : (value, value_instance) => {
63
+ alchemy.exposeStatic('fontawesome_pro', value);
64
+ },
65
+ });
66
+
67
+ MEDIA_PLUGIN_GROUP.addSetting('max_page_width', {
68
+ type : 'integer',
69
+ default : null,
70
+ description : 'Limit the maximum allowed page width, used for image resizing',
71
+ });
72
+
73
+ const BINARIES = MEDIA_PLUGIN_GROUP.createGroup('binaries');
74
+
75
+ BINARIES.addSetting('convert', {
76
+ type : 'string',
77
+ default : alchemy.findPathToBinarySync('convert'),
78
+ description : 'The path to the `convert` binary',
79
+ });
80
+
81
+ BINARIES.addSetting('exiv2', {
82
+ type : 'string',
83
+ default : alchemy.findPathToBinarySync('exiv2'),
84
+ description : 'The path to the `exiv2` binary',
85
+ });
86
+
87
+ BINARIES.addSetting('cwebp', {
88
+ type : 'string',
89
+ default : alchemy.findPathToBinarySync('cwebp'),
90
+ description : 'The path to the `cwebp` binary',
91
+ action : createVeronicaInstance,
92
+ });
93
+
94
+ /**
95
+ * Recreate the Veronica instance
96
+ *
97
+ * @author Jelle De Loecker <jelle@elevenways.be>
98
+ * @since 0.9.0
99
+ * @version 0.9.0
100
+ */
101
+ function createVeronicaInstance() {
102
+
103
+ let Veronica = alchemy.use('veronica');
104
+ const media = alchemy.settings.plugins.media;
105
+
106
+ alchemy.plugins.media.veronica = new Veronica({
107
+ cwebp : media.binaries.cwebp,
108
+ temp : media.scratch_path,
109
+ cache : media.cache_path,
110
+ });
111
+ }
@@ -19,10 +19,6 @@ var exiv2 = alchemy.use('@11ways/exiv2'),
19
19
  */
20
20
  const ImageMedia = Function.inherits('Alchemy.MediaType', 'ImageMediaType');
21
21
 
22
- ImageMedia.setProperty('exivPath', alchemy.plugins.media.exiv2);
23
- ImageMedia.setProperty('hashType', alchemy.plugins.media.hash);
24
- ImageMedia.setProperty('veronica', alchemy.plugins.media.veronica);
25
-
26
22
  ImageMedia.setProperty('typeMap', {
27
23
  images: {
28
24
  regex: /^image\//,
@@ -30,6 +26,45 @@ ImageMedia.setProperty('typeMap', {
30
26
  }
31
27
  });
32
28
 
29
+ /**
30
+ * Add a property getter to the exiv binary
31
+ *
32
+ * @author Jelle De Loecker <jelle@elevenways.be>
33
+ * @since 0.9.0
34
+ * @version 0.9.0
35
+ *
36
+ * @type {string}
37
+ */
38
+ ImageMedia.setProperty(function exiv_path() {
39
+ return alchemy.settings.plugins.media.binaries.exiv2;
40
+ });
41
+
42
+ /**
43
+ * Add a property getter to the cwebp binary
44
+ *
45
+ * @author Jelle De Loecker <jelle@elevenways.be>
46
+ * @since 0.9.0
47
+ * @version 0.9.0
48
+ *
49
+ * @type {string}
50
+ */
51
+ ImageMedia.setProperty(function cwebp_path() {
52
+ return alchemy.settings.plugins.media.binaries.cwebp;
53
+ });
54
+
55
+ /**
56
+ * Get the hash algorithm
57
+ *
58
+ * @author Jelle De Loecker <jelle@elevenways.be>
59
+ * @since 0.9.0
60
+ * @version 0.9.0
61
+ *
62
+ * @type {string}
63
+ */
64
+ ImageMedia.setProperty(function hashType() {
65
+ return alchemy.settings.plugins.media.file_hash_algorithm;
66
+ });
67
+
33
68
  /**
34
69
  * Generate a thumbnail of this type
35
70
  *
@@ -163,8 +198,8 @@ function getResizedDimension(query, type, resolution, dpr) {
163
198
  }
164
199
 
165
200
  // It's called "max_page_width", but height can also be checked against this
166
- if (base_size > alchemy.plugins.media.max_page_width) {
167
- base_size = alchemy.plugins.media.max_page_width;
201
+ if (base_size > alchemy.settings.plugins.media.max_page_width) {
202
+ base_size = alchemy.settings.plugins.media.max_page_width;
168
203
  }
169
204
 
170
205
  size = (base_size * percentage) / 100;
@@ -307,7 +342,9 @@ ImageMedia.setMethod(function serve(conduit, record, options) {
307
342
 
308
343
  if (resizeOptions) {
309
344
 
310
- if (this.supportsWebp(conduit) && alchemy.plugins.media.cwebp) {
345
+ const cwebp_path = this.cwebp_path;
346
+
347
+ if (this.supportsWebp(conduit) && cwebp_path) {
311
348
  resizeOptions.type = 'webp';
312
349
  }
313
350
 
@@ -440,11 +477,13 @@ ImageMedia.setMethod(function getSize(conduit, filePath, options, callback) {
440
477
 
441
478
  tempPath = PATH_TEMP + '/' + alchemy.ObjectId() + '.webp';
442
479
 
443
- if (!alchemy.plugins.media.cwebp) {
480
+ const cwebp_path = this.cwebp_path;
481
+
482
+ if (!cwebp_path) {
444
483
  return callback(new Error('Unable to get size: cwebp not found'));
445
484
  }
446
485
 
447
- child.exec(alchemy.plugins.media.cwebp + ' ' + [resizeOptions, '-q 80', filePath, '-o', tempPath].join(' '), function(err, out) {
486
+ child.exec(cwebp_path + ' ' + [resizeOptions, '-q 80', filePath, '-o', tempPath].join(' '), function(err, out) {
448
487
 
449
488
  if (err) {
450
489
  return callback(err);
@@ -560,7 +599,7 @@ ImageMedia.setMethod(function getMetadata(filePath, base, info, extra, callback)
560
599
  return getInfo();
561
600
  }
562
601
 
563
- child.execFile(that.exivPath, ['rm', tempFile], function afterStrip(err) {
602
+ child.execFile(that.exiv_path, ['rm', tempFile], function afterStrip(err) {
564
603
 
565
604
  if (err) {
566
605
  log.error('Error stripping metadata ', {err: err});
@@ -600,11 +639,11 @@ ImageMedia.setMethod(function getMetadata(filePath, base, info, extra, callback)
600
639
  *
601
640
  * @author Jelle De Loecker <jelle@develry.be>
602
641
  * @since 0.0.1
603
- * @version 0.4.2
642
+ * @version 0.9.0
604
643
  */
605
644
  ImageMedia.setMethod(function resize(source, target, width, height, callback) {
606
645
 
607
- var convertBin = alchemy.plugins.media.convert;
646
+ let convertBin = alchemy.settings.plugins.media.convert;
608
647
 
609
648
  child.execFile(convertBin, [source, '-resize', width + 'x' + height, target], function(err) {
610
649
  callback(err);
@@ -616,7 +655,7 @@ ImageMedia.setMethod(function resize(source, target, width, height, callback) {
616
655
  *
617
656
  * @author Jelle De Loecker <jelle@develry.be>
618
657
  * @since 0.0.1
619
- * @version 0.2.0
658
+ * @version 0.9.0
620
659
  *
621
660
  * @param {String} filePath The path of the file to inspect
622
661
  * @param {Function} callback
@@ -624,7 +663,7 @@ ImageMedia.setMethod(function resize(source, target, width, height, callback) {
624
663
  ImageMedia.setMethod(function getImageInfo(filePath, callback) {
625
664
 
626
665
  // Run exiv on the given file
627
- child.execFile(this.exivPath, ['-pt', filePath], function afterStrip(err, out) {
666
+ child.execFile(this.exiv_path, ['-pt', filePath], function afterStrip(err, out) {
628
667
 
629
668
  var output,
630
669
  lines,
@@ -22,8 +22,6 @@ MediaType = Function.inherits('Alchemy.Base', function MediaType(options) {
22
22
 
23
23
  typeName = this.constructor.name.replace(/MediaType$/, '');
24
24
 
25
- this.veronica = alchemy.plugins.media.veronica;
26
-
27
25
  this.title = typeName || 'Media';
28
26
  this.typeName = this.title.underscore();
29
27
  this.options = options || {};
@@ -55,25 +53,38 @@ MediaType.makeAbstractClass();
55
53
  */
56
54
  MediaType.startNewGroup();
57
55
 
56
+ /**
57
+ * Get the veronica instance
58
+ *
59
+ * @author Jelle De Loecker <jelle@elevenways.be>
60
+ * @since 0.9.0
61
+ * @version 0.9.0
62
+ *
63
+ * @type {Veronica}
64
+ */
65
+ MediaType.setProperty(function veronica() {
66
+ return alchemy.plugins.media.veronica;
67
+ });
68
+
58
69
  /**
59
70
  * See if we can use webp
60
71
  *
61
72
  * @author Jelle De Loecker <jelle@develry.be>
62
73
  * @since 0.0.1
63
- * @version 0.4.0
74
+ * @version 0.9.0
64
75
  *
65
76
  * @param {Conduit} conduit
66
77
  */
67
78
  MediaType.setMethod(function supportsWebp(conduit) {
68
79
 
69
- var result = false,
80
+ let result = false,
70
81
  has_webp_support,
71
82
  uaString,
72
83
  agent,
73
84
  is;
74
85
 
75
86
  // If webp has been disabled, return false
76
- if (!alchemy.plugins.media.webp) {
87
+ if (!alchemy.settings.plugins.media.enable_webp) {
77
88
  return false;
78
89
  }
79
90
 
@@ -53,12 +53,12 @@ MediaFile.constitute(function addFields() {
53
53
  this.addField('extra', 'Object');
54
54
 
55
55
  this.addField('title', 'String', {
56
- translatable : alchemy.plugins.media.translatable,
56
+ translatable : alchemy.settings.plugins.media.translatable,
57
57
  description : 'The title of the file (will be used in the title attribute)',
58
58
  });
59
59
 
60
60
  this.addField('alt', 'String', {
61
- translatable : alchemy.plugins.media.translatable,
61
+ translatable : alchemy.settings.plugins.media.translatable,
62
62
  description : 'The alternative information of the file (will be used in the alt attribute)',
63
63
  });
64
64
 
@@ -41,10 +41,34 @@ MediaRaw.constitute(function addFields() {
41
41
  this.addField('extra', 'Object');
42
42
  });
43
43
 
44
- MediaRaw.setProperty('basePath', alchemy.plugins.media.path);
45
- MediaRaw.setProperty('hash', alchemy.plugins.media.hash);
46
44
  MediaRaw.setProperty('types', alchemy.getClassGroup('media_type'));
47
45
 
46
+ /**
47
+ * Get the hash algorithm
48
+ *
49
+ * @author Jelle De Loecker <jelle@elevenways.be>
50
+ * @since 0.9.0
51
+ * @version 0.9.0
52
+ *
53
+ * @type {string}
54
+ */
55
+ MediaRaw.setProperty(function hashType() {
56
+ return alchemy.settings.plugins.media.file_hash_algorithm;
57
+ });
58
+
59
+ /**
60
+ * Get the base path
61
+ *
62
+ * @author Jelle De Loecker <jelle@elevenways.be>
63
+ * @since 0.9.0
64
+ * @version 0.9.0
65
+ *
66
+ * @type {string}
67
+ */
68
+ MediaRaw.setProperty(function basePath() {
69
+ return alchemy.settings.plugins.media.file_storage_path;
70
+ });
71
+
48
72
  /**
49
73
  * Path to this file
50
74
  *
@@ -239,13 +263,11 @@ MediaRaw.setMethod(function addFile(file, options, callback) {
239
263
 
240
264
  alchemy.getFileInfo(file, {hash: this.hashType}, function gotFileInfo(err, info) {
241
265
 
242
- var type;
243
-
244
266
  if (err) {
245
267
  return callback(err);
246
268
  }
247
269
 
248
- type = that.MediaType.determineType(info.mimetype, options);
270
+ let type = that.MediaType.determineType(info.mimetype, options);
249
271
 
250
272
  type.normalize(file, info, function afterNormalize(err, rawPath, rawInfo, rawExtra, extra) {
251
273
 
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "alchemy-media",
3
3
  "description": "The media plugin for Alchemy",
4
- "version": "0.7.6",
4
+ "version": "0.9.0-alpha",
5
5
  "repository": {
6
6
  "type" : "git",
7
7
  "url" : "https://github.com/11ways/alchemy-media.git"
8
8
  },
9
9
  "dependencies": {
10
- "@11ways/exiv2" : "~0.6.4",
10
+ "@11ways/exiv2" : "~0.7.0",
11
11
  "gm" : "~1.23.1",
12
12
  "veronica" : "~0.2.2"
13
13
  },
14
14
  "peerDependencies": {
15
- "alchemymvc" : ">=1.3.0"
15
+ "alchemymvc" : ">=1.4.0||>=1.4.0-alpha"
16
16
  },
17
17
  "license": "MIT",
18
18
  "engines": {
19
- "node" : ">=14.0.0"
19
+ "node" : ">=16.20.1"
20
20
  }
21
21
  }