@flexireact/core 2.2.0 → 2.3.0
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/README.md +203 -15
- package/core/font/index.ts +306 -0
- package/core/image/index.ts +413 -0
- package/core/index.ts +44 -1
- package/core/metadata/index.ts +622 -0
- package/core/server/index.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Image Optimization
|
|
3
|
+
*
|
|
4
|
+
* Optimized image component with:
|
|
5
|
+
* - Automatic WebP/AVIF conversion
|
|
6
|
+
* - Responsive srcset generation
|
|
7
|
+
* - Lazy loading with blur placeholder
|
|
8
|
+
* - Priority loading for LCP images
|
|
9
|
+
* - Automatic width/height to prevent CLS
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import crypto from 'crypto';
|
|
16
|
+
|
|
17
|
+
// Image optimization config
|
|
18
|
+
export interface ImageConfig {
|
|
19
|
+
domains: string[];
|
|
20
|
+
deviceSizes: number[];
|
|
21
|
+
imageSizes: number[];
|
|
22
|
+
formats: ('webp' | 'avif' | 'png' | 'jpeg')[];
|
|
23
|
+
minimumCacheTTL: number;
|
|
24
|
+
dangerouslyAllowSVG: boolean;
|
|
25
|
+
quality: number;
|
|
26
|
+
cacheDir: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const defaultImageConfig: ImageConfig = {
|
|
30
|
+
domains: [],
|
|
31
|
+
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
32
|
+
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
|
33
|
+
formats: ['webp', 'avif'],
|
|
34
|
+
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
|
|
35
|
+
dangerouslyAllowSVG: false,
|
|
36
|
+
quality: 75,
|
|
37
|
+
cacheDir: '.flexi/image-cache'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Image props
|
|
41
|
+
export interface ImageProps {
|
|
42
|
+
src: string;
|
|
43
|
+
alt: string;
|
|
44
|
+
width?: number;
|
|
45
|
+
height?: number;
|
|
46
|
+
fill?: boolean;
|
|
47
|
+
sizes?: string;
|
|
48
|
+
quality?: number;
|
|
49
|
+
priority?: boolean;
|
|
50
|
+
placeholder?: 'blur' | 'empty' | 'data:image/...';
|
|
51
|
+
blurDataURL?: string;
|
|
52
|
+
loading?: 'lazy' | 'eager';
|
|
53
|
+
className?: string;
|
|
54
|
+
style?: React.CSSProperties;
|
|
55
|
+
onLoad?: () => void;
|
|
56
|
+
onError?: () => void;
|
|
57
|
+
unoptimized?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Generate blur placeholder
|
|
61
|
+
export async function generateBlurPlaceholder(imagePath: string): Promise<string> {
|
|
62
|
+
try {
|
|
63
|
+
// For now, return a simple SVG blur placeholder
|
|
64
|
+
// In production, we'd use sharp to generate a tiny blurred version
|
|
65
|
+
return `data:image/svg+xml;base64,${Buffer.from(
|
|
66
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 5">
|
|
67
|
+
<filter id="b" color-interpolation-filters="sRGB">
|
|
68
|
+
<feGaussianBlur stdDeviation="1"/>
|
|
69
|
+
</filter>
|
|
70
|
+
<rect width="100%" height="100%" fill="#1a1a1a"/>
|
|
71
|
+
<rect width="100%" height="100%" filter="url(#b)" opacity="0.5" fill="#333"/>
|
|
72
|
+
</svg>`
|
|
73
|
+
).toString('base64')}`;
|
|
74
|
+
} catch {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get image dimensions
|
|
80
|
+
export async function getImageDimensions(src: string): Promise<{ width: number; height: number } | null> {
|
|
81
|
+
try {
|
|
82
|
+
// For local files
|
|
83
|
+
if (!src.startsWith('http')) {
|
|
84
|
+
const imagePath = path.join(process.cwd(), 'public', src);
|
|
85
|
+
if (fs.existsSync(imagePath)) {
|
|
86
|
+
// Read first bytes to detect dimensions
|
|
87
|
+
const buffer = fs.readFileSync(imagePath);
|
|
88
|
+
return detectDimensions(buffer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Detect image dimensions from buffer
|
|
98
|
+
function detectDimensions(buffer: Buffer): { width: number; height: number } | null {
|
|
99
|
+
// PNG
|
|
100
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50) {
|
|
101
|
+
return {
|
|
102
|
+
width: buffer.readUInt32BE(16),
|
|
103
|
+
height: buffer.readUInt32BE(20)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// JPEG
|
|
108
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
109
|
+
let offset = 2;
|
|
110
|
+
while (offset < buffer.length) {
|
|
111
|
+
if (buffer[offset] !== 0xff) break;
|
|
112
|
+
const marker = buffer[offset + 1];
|
|
113
|
+
if (marker === 0xc0 || marker === 0xc2) {
|
|
114
|
+
return {
|
|
115
|
+
height: buffer.readUInt16BE(offset + 5),
|
|
116
|
+
width: buffer.readUInt16BE(offset + 7)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
offset += 2 + buffer.readUInt16BE(offset + 2);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// GIF
|
|
124
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
|
125
|
+
return {
|
|
126
|
+
width: buffer.readUInt16LE(6),
|
|
127
|
+
height: buffer.readUInt16LE(8)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// WebP
|
|
132
|
+
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[8] === 0x57 && buffer[9] === 0x45) {
|
|
133
|
+
// VP8
|
|
134
|
+
if (buffer[12] === 0x56 && buffer[13] === 0x50 && buffer[14] === 0x38) {
|
|
135
|
+
if (buffer[15] === 0x20) { // VP8
|
|
136
|
+
return {
|
|
137
|
+
width: buffer.readUInt16LE(26) & 0x3fff,
|
|
138
|
+
height: buffer.readUInt16LE(28) & 0x3fff
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (buffer[15] === 0x4c) { // VP8L
|
|
142
|
+
const bits = buffer.readUInt32LE(21);
|
|
143
|
+
return {
|
|
144
|
+
width: (bits & 0x3fff) + 1,
|
|
145
|
+
height: ((bits >> 14) & 0x3fff) + 1
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Generate srcset for responsive images
|
|
155
|
+
export function generateSrcSet(
|
|
156
|
+
src: string,
|
|
157
|
+
widths: number[],
|
|
158
|
+
quality: number = 75
|
|
159
|
+
): string {
|
|
160
|
+
return widths
|
|
161
|
+
.map(w => `/_flexi/image?url=${encodeURIComponent(src)}&w=${w}&q=${quality} ${w}w`)
|
|
162
|
+
.join(', ');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Generate sizes attribute
|
|
166
|
+
export function generateSizes(sizes?: string): string {
|
|
167
|
+
if (sizes) return sizes;
|
|
168
|
+
return '100vw';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Image optimization endpoint handler
|
|
172
|
+
export async function handleImageOptimization(
|
|
173
|
+
req: any,
|
|
174
|
+
res: any,
|
|
175
|
+
config: Partial<ImageConfig> = {}
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const fullConfig = { ...defaultImageConfig, ...config };
|
|
178
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
179
|
+
|
|
180
|
+
const imageUrl = url.searchParams.get('url');
|
|
181
|
+
const width = parseInt(url.searchParams.get('w') || '0', 10);
|
|
182
|
+
const quality = parseInt(url.searchParams.get('q') || String(fullConfig.quality), 10);
|
|
183
|
+
const format = url.searchParams.get('f') as 'webp' | 'avif' | 'png' | 'jpeg' | null;
|
|
184
|
+
|
|
185
|
+
if (!imageUrl) {
|
|
186
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
187
|
+
res.end('Missing url parameter');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
let imageBuffer: Buffer;
|
|
193
|
+
let contentType: string;
|
|
194
|
+
|
|
195
|
+
// Fetch image
|
|
196
|
+
if (imageUrl.startsWith('http')) {
|
|
197
|
+
// Remote image
|
|
198
|
+
const response = await fetch(imageUrl);
|
|
199
|
+
if (!response.ok) throw new Error('Failed to fetch image');
|
|
200
|
+
imageBuffer = Buffer.from(await response.arrayBuffer());
|
|
201
|
+
contentType = response.headers.get('content-type') || 'image/jpeg';
|
|
202
|
+
} else {
|
|
203
|
+
// Local image
|
|
204
|
+
const imagePath = path.join(process.cwd(), 'public', imageUrl);
|
|
205
|
+
if (!fs.existsSync(imagePath)) {
|
|
206
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
207
|
+
res.end('Image not found');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
imageBuffer = fs.readFileSync(imagePath);
|
|
211
|
+
contentType = getContentType(imagePath);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Generate cache key
|
|
215
|
+
const cacheKey = crypto
|
|
216
|
+
.createHash('md5')
|
|
217
|
+
.update(`${imageUrl}-${width}-${quality}-${format}`)
|
|
218
|
+
.digest('hex');
|
|
219
|
+
|
|
220
|
+
const cacheDir = path.join(process.cwd(), fullConfig.cacheDir);
|
|
221
|
+
const cachePath = path.join(cacheDir, `${cacheKey}.${format || 'webp'}`);
|
|
222
|
+
|
|
223
|
+
// Check cache
|
|
224
|
+
if (fs.existsSync(cachePath)) {
|
|
225
|
+
const cachedImage = fs.readFileSync(cachePath);
|
|
226
|
+
res.writeHead(200, {
|
|
227
|
+
'Content-Type': `image/${format || 'webp'}`,
|
|
228
|
+
'Cache-Control': `public, max-age=${fullConfig.minimumCacheTTL}`,
|
|
229
|
+
'X-Flexi-Image-Cache': 'HIT'
|
|
230
|
+
});
|
|
231
|
+
res.end(cachedImage);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// For now, serve original image
|
|
236
|
+
// In production, we'd use sharp for resizing/conversion
|
|
237
|
+
// TODO: Integrate sharp for actual optimization
|
|
238
|
+
|
|
239
|
+
res.writeHead(200, {
|
|
240
|
+
'Content-Type': contentType,
|
|
241
|
+
'Cache-Control': `public, max-age=${fullConfig.minimumCacheTTL}`,
|
|
242
|
+
'X-Flexi-Image-Cache': 'MISS'
|
|
243
|
+
});
|
|
244
|
+
res.end(imageBuffer);
|
|
245
|
+
|
|
246
|
+
} catch (error: any) {
|
|
247
|
+
console.error('Image optimization error:', error);
|
|
248
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
249
|
+
res.end('Image optimization failed');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Get content type from file extension
|
|
254
|
+
function getContentType(filePath: string): string {
|
|
255
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
256
|
+
const types: Record<string, string> = {
|
|
257
|
+
'.jpg': 'image/jpeg',
|
|
258
|
+
'.jpeg': 'image/jpeg',
|
|
259
|
+
'.png': 'image/png',
|
|
260
|
+
'.gif': 'image/gif',
|
|
261
|
+
'.webp': 'image/webp',
|
|
262
|
+
'.avif': 'image/avif',
|
|
263
|
+
'.svg': 'image/svg+xml',
|
|
264
|
+
'.ico': 'image/x-icon'
|
|
265
|
+
};
|
|
266
|
+
return types[ext] || 'application/octet-stream';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Image component (server-side rendered)
|
|
270
|
+
export function createImageComponent(config: Partial<ImageConfig> = {}) {
|
|
271
|
+
const fullConfig = { ...defaultImageConfig, ...config };
|
|
272
|
+
|
|
273
|
+
return function Image(props: ImageProps): React.ReactElement {
|
|
274
|
+
const {
|
|
275
|
+
src,
|
|
276
|
+
alt,
|
|
277
|
+
width,
|
|
278
|
+
height,
|
|
279
|
+
fill = false,
|
|
280
|
+
sizes,
|
|
281
|
+
quality = fullConfig.quality,
|
|
282
|
+
priority = false,
|
|
283
|
+
placeholder = 'empty',
|
|
284
|
+
blurDataURL,
|
|
285
|
+
loading,
|
|
286
|
+
className = '',
|
|
287
|
+
style = {},
|
|
288
|
+
unoptimized = false,
|
|
289
|
+
...rest
|
|
290
|
+
} = props;
|
|
291
|
+
|
|
292
|
+
// Determine loading strategy
|
|
293
|
+
const loadingAttr = priority ? 'eager' : (loading || 'lazy');
|
|
294
|
+
|
|
295
|
+
// Generate optimized src
|
|
296
|
+
const optimizedSrc = unoptimized
|
|
297
|
+
? src
|
|
298
|
+
: `/_flexi/image?url=${encodeURIComponent(src)}&w=${width || 1920}&q=${quality}`;
|
|
299
|
+
|
|
300
|
+
// Generate srcset for responsive images
|
|
301
|
+
const allSizes = [...fullConfig.imageSizes, ...fullConfig.deviceSizes].sort((a, b) => a - b);
|
|
302
|
+
const relevantSizes = width
|
|
303
|
+
? allSizes.filter(s => s <= width * 2)
|
|
304
|
+
: allSizes;
|
|
305
|
+
|
|
306
|
+
const srcSet = unoptimized
|
|
307
|
+
? undefined
|
|
308
|
+
: generateSrcSet(src, relevantSizes, quality);
|
|
309
|
+
|
|
310
|
+
// Build styles
|
|
311
|
+
const imgStyle: React.CSSProperties = {
|
|
312
|
+
...style,
|
|
313
|
+
...(fill ? {
|
|
314
|
+
position: 'absolute',
|
|
315
|
+
top: 0,
|
|
316
|
+
left: 0,
|
|
317
|
+
width: '100%',
|
|
318
|
+
height: '100%',
|
|
319
|
+
objectFit: 'cover'
|
|
320
|
+
} : {})
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Placeholder styles
|
|
324
|
+
const wrapperStyle: React.CSSProperties = fill ? {
|
|
325
|
+
position: 'relative',
|
|
326
|
+
width: '100%',
|
|
327
|
+
height: '100%'
|
|
328
|
+
} : {};
|
|
329
|
+
|
|
330
|
+
const placeholderStyle: React.CSSProperties = placeholder === 'blur' ? {
|
|
331
|
+
backgroundImage: `url(${blurDataURL || generateBlurPlaceholderSync()})`,
|
|
332
|
+
backgroundSize: 'cover',
|
|
333
|
+
backgroundPosition: 'center',
|
|
334
|
+
filter: 'blur(20px)',
|
|
335
|
+
transform: 'scale(1.1)'
|
|
336
|
+
} : {};
|
|
337
|
+
|
|
338
|
+
// Create image element
|
|
339
|
+
const imgElement = React.createElement('img', {
|
|
340
|
+
src: optimizedSrc,
|
|
341
|
+
alt,
|
|
342
|
+
width: fill ? undefined : width,
|
|
343
|
+
height: fill ? undefined : height,
|
|
344
|
+
loading: loadingAttr,
|
|
345
|
+
decoding: 'async',
|
|
346
|
+
srcSet,
|
|
347
|
+
sizes: generateSizes(sizes),
|
|
348
|
+
className: `flexi-image ${className}`.trim(),
|
|
349
|
+
style: imgStyle,
|
|
350
|
+
fetchPriority: priority ? 'high' : undefined,
|
|
351
|
+
...rest
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Wrap with placeholder if needed
|
|
355
|
+
if (fill || placeholder === 'blur') {
|
|
356
|
+
return React.createElement('div', {
|
|
357
|
+
className: 'flexi-image-wrapper',
|
|
358
|
+
style: { ...wrapperStyle, ...placeholderStyle }
|
|
359
|
+
}, imgElement);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return imgElement;
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Sync blur placeholder (for SSR)
|
|
367
|
+
function generateBlurPlaceholderSync(): string {
|
|
368
|
+
return `data:image/svg+xml;base64,${Buffer.from(
|
|
369
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 5">
|
|
370
|
+
<filter id="b" color-interpolation-filters="sRGB">
|
|
371
|
+
<feGaussianBlur stdDeviation="1"/>
|
|
372
|
+
</filter>
|
|
373
|
+
<rect width="100%" height="100%" fill="#1a1a1a"/>
|
|
374
|
+
</svg>`
|
|
375
|
+
).toString('base64')}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Default Image component
|
|
379
|
+
export const Image = createImageComponent();
|
|
380
|
+
|
|
381
|
+
// Loader types for different image providers
|
|
382
|
+
export interface ImageLoader {
|
|
383
|
+
(props: { src: string; width: number; quality?: number }): string;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Built-in loaders
|
|
387
|
+
export const imageLoaders = {
|
|
388
|
+
default: ({ src, width, quality = 75 }: { src: string; width: number; quality?: number }) =>
|
|
389
|
+
`/_flexi/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,
|
|
390
|
+
|
|
391
|
+
cloudinary: ({ src, width, quality = 75 }: { src: string; width: number; quality?: number }) =>
|
|
392
|
+
`https://res.cloudinary.com/demo/image/fetch/w_${width},q_${quality}/${src}`,
|
|
393
|
+
|
|
394
|
+
imgix: ({ src, width, quality = 75 }: { src: string; width: number; quality?: number }) =>
|
|
395
|
+
`${src}?w=${width}&q=${quality}&auto=format`,
|
|
396
|
+
|
|
397
|
+
vercel: ({ src, width, quality = 75 }: { src: string; width: number; quality?: number }) =>
|
|
398
|
+
`/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,
|
|
399
|
+
|
|
400
|
+
cloudflare: ({ src, width, quality = 75 }: { src: string; width: number; quality?: number }) =>
|
|
401
|
+
`/cdn-cgi/image/width=${width},quality=${quality}/${src}`
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export default {
|
|
405
|
+
Image,
|
|
406
|
+
createImageComponent,
|
|
407
|
+
handleImageOptimization,
|
|
408
|
+
generateBlurPlaceholder,
|
|
409
|
+
getImageDimensions,
|
|
410
|
+
generateSrcSet,
|
|
411
|
+
imageLoaders,
|
|
412
|
+
defaultImageConfig
|
|
413
|
+
};
|
package/core/index.ts
CHANGED
|
@@ -70,6 +70,49 @@ export {
|
|
|
70
70
|
builtinPlugins
|
|
71
71
|
} from './plugins/index.js';
|
|
72
72
|
|
|
73
|
+
// Font Optimization
|
|
74
|
+
export {
|
|
75
|
+
createFont,
|
|
76
|
+
googleFont,
|
|
77
|
+
localFont,
|
|
78
|
+
generateFontCSS,
|
|
79
|
+
generateFontPreloadTags,
|
|
80
|
+
handleFontRequest,
|
|
81
|
+
fonts,
|
|
82
|
+
googleFonts
|
|
83
|
+
} from './font/index.js';
|
|
84
|
+
export type { FontConfig, FontResult } from './font/index.js';
|
|
85
|
+
|
|
86
|
+
// Metadata API
|
|
87
|
+
export {
|
|
88
|
+
generateMetadataTags,
|
|
89
|
+
mergeMetadata,
|
|
90
|
+
generateJsonLd,
|
|
91
|
+
jsonLd
|
|
92
|
+
} from './metadata/index.js';
|
|
93
|
+
export type {
|
|
94
|
+
Metadata,
|
|
95
|
+
OpenGraph,
|
|
96
|
+
Twitter,
|
|
97
|
+
Icons,
|
|
98
|
+
Robots,
|
|
99
|
+
Viewport,
|
|
100
|
+
Author
|
|
101
|
+
} from './metadata/index.js';
|
|
102
|
+
|
|
103
|
+
// Image Optimization
|
|
104
|
+
export {
|
|
105
|
+
Image,
|
|
106
|
+
createImageComponent,
|
|
107
|
+
handleImageOptimization,
|
|
108
|
+
generateBlurPlaceholder,
|
|
109
|
+
getImageDimensions,
|
|
110
|
+
generateSrcSet,
|
|
111
|
+
imageLoaders,
|
|
112
|
+
defaultImageConfig
|
|
113
|
+
} from './image/index.js';
|
|
114
|
+
export type { ImageProps, ImageConfig, ImageLoader } from './image/index.js';
|
|
115
|
+
|
|
73
116
|
// Server Actions
|
|
74
117
|
export {
|
|
75
118
|
serverAction,
|
|
@@ -110,7 +153,7 @@ export {
|
|
|
110
153
|
export type { CookieOptions } from './helpers.js';
|
|
111
154
|
|
|
112
155
|
// Version
|
|
113
|
-
export const VERSION = '2.
|
|
156
|
+
export const VERSION = '2.3.0';
|
|
114
157
|
|
|
115
158
|
// Default export
|
|
116
159
|
export default {
|