@muhgholy/next-drive 4.6.0 → 4.8.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 CHANGED
@@ -269,18 +269,28 @@ Upload files programmatically from server-side code:
269
269
  ```typescript
270
270
  import { driveUpload } from "@muhgholy/next-drive/server";
271
271
 
272
- // Upload from file path
272
+ // Upload to specific folder by ID
273
273
  const file = await driveUpload(
274
274
  "/tmp/photo.jpg",
275
275
  { userId: "123" },
276
276
  {
277
277
  name: "photo.jpg",
278
- parentId: "folderId", // Optional: folder ID or null for root
278
+ folder: { id: "folderId" }, // Optional: folder ID
279
279
  accountId: "LOCAL", // Optional: storage account ID
280
280
  enforce: false, // Optional: bypass quota check
281
281
  }
282
282
  );
283
283
 
284
+ // Upload to folder by path (creates folders if not exist)
285
+ const file = await driveUpload(
286
+ "/tmp/photo.jpg",
287
+ { userId: "123" },
288
+ {
289
+ name: "photo.jpg",
290
+ folder: { path: "images/2024/january" }, // Creates folders recursively
291
+ }
292
+ );
293
+
284
294
  // Upload from stream
285
295
  import fs from "fs";
286
296
  const stream = fs.createReadStream("/tmp/video.mp4");
@@ -307,13 +317,14 @@ const file = await driveUpload(
307
317
 
308
318
  **Options:**
309
319
 
310
- | Option | Type | Required | Description |
311
- | ----------- | ---------------- | -------- | -------------------------------------------------------- |
312
- | `name` | `string` | Yes | File name with extension |
313
- | `parentId` | `string \| null` | No | Parent folder ID (null or 'root' for root) |
314
- | `accountId` | `string` | No | Storage account ID ('LOCAL' for local storage) |
315
- | `mime` | `string` | No | MIME type (auto-detected from extension if not provided) |
316
- | `enforce` | `boolean` | No | Bypass quota check (default: false) |
320
+ | Option | Type | Required | Description |
321
+ | ------------ | --------------------------------- | -------- | -------------------------------------------------------- |
322
+ | `name` | `string` | Yes | File name with extension |
323
+ | `folder.id` | `string` | No | Parent folder ID |
324
+ | `folder.path`| `string` | No | Folder path (e.g., `images/2024`) - creates if not exist |
325
+ | `accountId` | `string` | No | Storage account ID ('LOCAL' for local storage) |
326
+ | `mime` | `string` | No | MIME type (auto-detected from extension if not provided) |
327
+ | `enforce` | `boolean` | No | Bypass quota check (default: false) |
317
328
 
318
329
  ### Get Signed URL
319
330
 
@@ -579,7 +590,7 @@ Serve optimized images with dynamic compression, resizing, and format conversion
579
590
  ### URL Format
580
591
 
581
592
  ```
582
- /api/drive?action=serve&id={fileId}&quality={preset}&display={context}&size={preset}&format={format}
593
+ /api/drive?action=serve&id={fileId}&quality={preset}&display={context}&size={scale}&format={format}
583
594
  ```
584
595
 
585
596
  ### Parameters
@@ -587,10 +598,22 @@ Serve optimized images with dynamic compression, resizing, and format conversion
587
598
  | Parameter | Type | Description |
588
599
  |-----------|------|-------------|
589
600
  | `quality` | `low` / `medium` / `high` / `1-100` | Compression level |
590
- | `display` | string | Context-based quality adjustment |
591
- | `size` | string | Predefined dimensions preset |
601
+ | `display` | string | Sets aspect ratio, base dimensions, and quality factor |
602
+ | `size` | string | Scale factor (xs/sm/md/lg/xl) or standalone dimension preset |
592
603
  | `format` | `jpeg` / `webp` / `avif` / `png` | Output format |
593
604
 
605
+ ### How Display + Size Work Together
606
+
607
+ When **display** is specified, it defines the aspect ratio and base dimensions. The **size** parameter then scales those dimensions:
608
+
609
+ ```
610
+ display=article-image + size=sm → 400×225 (16:9, half size)
611
+ display=article-image + size=md → 800×450 (16:9, default)
612
+ display=article-image + size=lg → 1200×675 (16:9, 1.5x)
613
+ ```
614
+
615
+ When **no display** is specified, size uses standalone presets (fixed dimensions).
616
+
594
617
  ### Quality Presets
595
618
 
596
619
  | Preset | Base Quality | Use Case |
@@ -602,88 +625,74 @@ Serve optimized images with dynamic compression, resizing, and format conversion
602
625
 
603
626
  > Quality is dynamically adjusted based on file size. Larger files get more aggressive compression.
604
627
 
605
- ### Display Presets (Quality Context)
606
-
607
- | Display | Quality Factor | Use Case |
608
- |---------|----------------|----------|
609
- | `article-header` | 0.9 | Hero/banner images |
610
- | `article-image` | 0.85 | In-content images |
611
- | `thumbnail` | 0.7 | Small previews |
612
- | `avatar` | 0.8 | Profile pictures |
613
- | `logo` | 0.95 | Branding/logos |
614
- | `card` | 0.8 | Card components |
615
- | `gallery` | 0.85 | Gallery/grid |
616
- | `og` | 0.9 | Open Graph/social |
617
- | `icon` | 0.75 | Small icons |
618
- | `cover` | 0.9 | Full-width covers |
619
- | `story` | 0.85 | Story/vertical |
620
-
621
- ### Size Presets (Dimensions)
622
-
623
- **Square Sizes:**
624
- | Size | Dimensions |
625
- |------|------------|
626
- | `xs` | 64×64 |
627
- | `sm` | 128×128 |
628
- | `md` | 256×256 |
629
- | `lg` | 512×512 |
630
- | `xl` | 1024×1024 |
631
- | `2xl` | 1600×1600 |
632
- | `icon` | 48×48 |
633
- | `thumb` | 150×150 |
634
- | `square` | 600×600 |
635
- | `avatar-sm` | 64×64 |
636
- | `avatar-md` | 128×128 |
637
- | `avatar-lg` | 256×256 |
638
-
639
- **Landscape (16:9):**
640
- | Size | Dimensions |
641
- |------|------------|
642
- | `landscape-sm` | 480×270 |
643
- | `landscape` | 800×450 |
644
- | `landscape-lg` | 1280×720 |
645
- | `landscape-xl` | 1920×1080 |
646
-
647
- **Portrait (9:16):**
648
- | Size | Dimensions |
649
- |------|------------|
650
- | `portrait-sm` | 270×480 |
651
- | `portrait` | 450×800 |
652
- | `portrait-lg` | 720×1280 |
653
-
654
- **Wide/Banner:**
655
- | Size | Dimensions | Ratio |
656
- |------|------------|-------|
657
- | `wide` | 1200×630 | OG standard |
658
- | `banner` | 1200×400 | 3:1 |
659
- | `banner-sm` | 800×200 | 4:1 |
660
-
661
- **Other:**
662
- | Size | Dimensions | Ratio |
663
- |------|------------|-------|
664
- | `photo-4x3` | 800×600 | 4:3 |
665
- | `photo-3x2` | 900×600 | 3:2 |
666
- | `story` | 1080×1920 | 9:16 |
667
- | `video` | 1280×720 | 16:9 |
668
- | `video-sm` | 640×360 | 16:9 |
669
- | `card-sm` | 300×200 | 3:2 |
670
- | `card` | 400×300 | 4:3 |
671
- | `card-lg` | 600×400 | 3:2 |
628
+ ### Display Presets (Aspect Ratio + Dimensions)
629
+
630
+ | Display | Aspect Ratio | Base Size | Quality Factor |
631
+ |---------|--------------|-----------|----------------|
632
+ | `article-header` | 16:9 | 1200×675 | 0.9 |
633
+ | `article-image` | 16:9 | 800×450 | 0.85 |
634
+ | `thumbnail` | 1:1 | 150×150 | 0.7 |
635
+ | `avatar` | 1:1 | 128×128 | 0.8 |
636
+ | `logo` | 2:1 | 200×100 | 0.95 |
637
+ | `card` | 4:3 | 400×300 | 0.8 |
638
+ | `gallery` | 1:1 | 600×600 | 0.85 |
639
+ | `og` | ~1.9:1 | 1200×630 | 0.9 |
640
+ | `icon` | 1:1 | 48×48 | 0.75 |
641
+ | `cover` | 16:9 | 1920×1080 | 0.9 |
642
+ | `story` | 9:16 | 1080×1920 | 0.85 |
643
+ | `video` | 16:9 | 1280×720 | 0.85 |
644
+ | `banner` | 3:1 | 1200×400 | 0.9 |
645
+ | `portrait` | 3:4 | 600×800 | 0.85 |
646
+ | `landscape` | 4:3 | 800×600 | 0.85 |
647
+
648
+ ### Size Scale (with Display)
649
+
650
+ When used with a display preset, size scales the dimensions:
651
+
652
+ | Size | Scale | Example with `article-image` (800×450) |
653
+ |------|-------|----------------------------------------|
654
+ | `xs` | 0.25× | 200×113 |
655
+ | `sm` | 0.5× | 400×225 |
656
+ | `md` | 1.0× | 800×450 |
657
+ | `lg` | 1.5× | 1200×675 |
658
+ | `xl` | 2.0× | 1600×900 |
659
+ | `2xl` | 2.5× | 2000×1125 |
660
+
661
+ ### Standalone Size Presets (without Display)
662
+
663
+ When no display is specified, use these fixed dimension presets:
664
+
665
+ | Size | Dimensions | Size | Dimensions |
666
+ |------|------------|------|------------|
667
+ | `xs` | 64×64 | `landscape-sm` | 480×270 |
668
+ | `sm` | 128×128 | `landscape` | 800×450 |
669
+ | `md` | 256×256 | `landscape-lg` | 1280×720 |
670
+ | `lg` | 512×512 | `portrait-sm` | 270×480 |
671
+ | `xl` | 1024×1024 | `portrait` | 450×800 |
672
+ | `icon` | 48×48 | `wide` | 1200×630 |
673
+ | `thumb` | 150×150 | `banner` | 1200×400 |
674
+ | `video` | 1280×720 | `card` | 400×300 |
672
675
 
673
676
  ### Examples
674
677
 
675
678
  ```html
676
- <!-- Article header with OG dimensions -->
677
- <img src="/api/drive?action=serve&id=123&display=article-header&size=wide&format=webp">
679
+ <!-- Article image, smaller variant (400×225) -->
680
+ <img src="/api/drive?action=serve&id=123&display=article-image&size=sm&format=webp">
681
+
682
+ <!-- Article image, default size (800×450) -->
683
+ <img src="/api/drive?action=serve&id=123&display=article-image&format=webp">
684
+
685
+ <!-- Article image, larger variant (1200×675) -->
686
+ <img src="/api/drive?action=serve&id=123&display=article-image&size=lg&format=webp">
678
687
 
679
- <!-- Thumbnail with aggressive compression -->
680
- <img src="/api/drive?action=serve&id=123&display=thumbnail&size=thumb&format=webp">
688
+ <!-- Thumbnail (150×150 square) -->
689
+ <img src="/api/drive?action=serve&id=123&display=thumbnail&format=webp">
681
690
 
682
- <!-- Avatar -->
683
- <img src="/api/drive?action=serve&id=123&display=avatar&size=avatar-md&format=webp">
691
+ <!-- Avatar, smaller (64×64) -->
692
+ <img src="/api/drive?action=serve&id=123&display=avatar&size=sm&format=webp">
684
693
 
685
- <!-- Gallery image -->
686
- <img src="/api/drive?action=serve&id=123&display=gallery&size=landscape&format=webp">
694
+ <!-- Standalone size, no display -->
695
+ <img src="/api/drive?action=serve&id=123&size=landscape&format=webp">
687
696
 
688
697
  <!-- Just quality, no resize -->
689
698
  <img src="/api/drive?action=serve&id=123&quality=medium&format=webp">
@@ -357,70 +357,58 @@ var extractImageMetadata = async (filePath) => {
357
357
  }
358
358
  };
359
359
  var DISPLAY_PRESETS = {
360
- "article-header": 0.9,
361
- // Hero/banner images - high quality
362
- "article-image": 0.85,
363
- // In-content images
364
- "thumbnail": 0.7,
365
- // Small previews - lower quality ok
366
- "avatar": 0.8,
367
- // Profile pictures
368
- "logo": 0.95,
369
- // Branding - needs clarity
370
- "card": 0.8,
371
- // Card components
372
- "gallery": 0.85,
373
- // Gallery/grid images
374
- "og": 0.9,
375
- // Open Graph/social sharing
376
- "icon": 0.75,
377
- // Small icons
378
- "cover": 0.9,
379
- // Full-width covers
380
- "story": 0.85
381
- // Story/vertical format
360
+ "article-header": { ratio: [16, 9], baseWidth: 1200, qualityFactor: 0.9 },
361
+ "article-image": { ratio: [16, 9], baseWidth: 800, qualityFactor: 0.85 },
362
+ "thumbnail": { ratio: [1, 1], baseWidth: 150, qualityFactor: 0.7 },
363
+ "avatar": { ratio: [1, 1], baseWidth: 128, qualityFactor: 0.8 },
364
+ "logo": { ratio: [2, 1], baseWidth: 200, qualityFactor: 0.95 },
365
+ "card": { ratio: [4, 3], baseWidth: 400, qualityFactor: 0.8 },
366
+ "gallery": { ratio: [1, 1], baseWidth: 600, qualityFactor: 0.85 },
367
+ "og": { ratio: [1200, 630], baseWidth: 1200, qualityFactor: 0.9 },
368
+ "icon": { ratio: [1, 1], baseWidth: 48, qualityFactor: 0.75 },
369
+ "cover": { ratio: [16, 9], baseWidth: 1920, qualityFactor: 0.9 },
370
+ "story": { ratio: [9, 16], baseWidth: 1080, qualityFactor: 0.85 },
371
+ "video": { ratio: [16, 9], baseWidth: 1280, qualityFactor: 0.85 },
372
+ "banner": { ratio: [3, 1], baseWidth: 1200, qualityFactor: 0.9 },
373
+ "portrait": { ratio: [3, 4], baseWidth: 600, qualityFactor: 0.85 },
374
+ "landscape": { ratio: [4, 3], baseWidth: 800, qualityFactor: 0.85 }
382
375
  };
383
- var SIZE_PRESETS = {
384
- // Square sizes
376
+ var SIZE_SCALES = {
377
+ "xs": 0.25,
378
+ "sm": 0.5,
379
+ "md": 1,
380
+ "lg": 1.5,
381
+ "xl": 2,
382
+ "2xl": 2.5
383
+ };
384
+ var STANDALONE_SIZES = {
385
385
  "xs": { width: 64, height: 64 },
386
386
  "sm": { width: 128, height: 128 },
387
387
  "md": { width: 256, height: 256 },
388
388
  "lg": { width: 512, height: 512 },
389
389
  "xl": { width: 1024, height: 1024 },
390
390
  "2xl": { width: 1600, height: 1600 },
391
- // Named squares
392
391
  "icon": { width: 48, height: 48 },
393
392
  "thumb": { width: 150, height: 150 },
394
393
  "square": { width: 600, height: 600 },
395
394
  "avatar-sm": { width: 64, height: 64 },
396
395
  "avatar-md": { width: 128, height: 128 },
397
396
  "avatar-lg": { width: 256, height: 256 },
398
- // Landscape (16:9)
399
397
  "landscape-sm": { width: 480, height: 270 },
400
398
  "landscape": { width: 800, height: 450 },
401
399
  "landscape-lg": { width: 1280, height: 720 },
402
400
  "landscape-xl": { width: 1920, height: 1080 },
403
- // Portrait (9:16)
404
401
  "portrait-sm": { width: 270, height: 480 },
405
402
  "portrait": { width: 450, height: 800 },
406
403
  "portrait-lg": { width: 720, height: 1280 },
407
- // Wide/Banner (OG, social)
408
404
  "wide": { width: 1200, height: 630 },
409
- // Open Graph standard
410
405
  "banner": { width: 1200, height: 400 },
411
- // Banner/header
412
406
  "banner-sm": { width: 800, height: 200 },
413
- // Classic photo ratios
414
407
  "photo-4x3": { width: 800, height: 600 },
415
- // 4:3
416
408
  "photo-3x2": { width: 900, height: 600 },
417
- // 3:2
418
- // Story/vertical (9:16)
419
409
  "story": { width: 1080, height: 1920 },
420
- // Video thumbnails
421
410
  "video": { width: 1280, height: 720 },
422
411
  "video-sm": { width: 640, height: 360 },
423
- // Card sizes
424
412
  "card-sm": { width: 300, height: 200 },
425
413
  "card": { width: 400, height: 300 },
426
414
  "card-lg": { width: 600, height: 400 }
@@ -434,8 +422,24 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
434
422
  const n = parseInt(qualityPreset, 10);
435
423
  if (!isNaN(n)) baseQuality = Math.min(100, Math.max(1, n));
436
424
  }
437
- const displayFactor = display && DISPLAY_PRESETS[display] ? DISPLAY_PRESETS[display] : 1;
438
- baseQuality = Math.round(baseQuality * displayFactor);
425
+ let width;
426
+ let height;
427
+ let qualityFactor = 1;
428
+ const displayPreset = display ? DISPLAY_PRESETS[display] : void 0;
429
+ if (displayPreset) {
430
+ qualityFactor = displayPreset.qualityFactor;
431
+ const [ratioW, ratioH] = displayPreset.ratio;
432
+ const scale = size && SIZE_SCALES[size] ? SIZE_SCALES[size] : 1;
433
+ width = Math.round(displayPreset.baseWidth * scale);
434
+ height = Math.round(width * ratioH / ratioW);
435
+ } else if (size) {
436
+ const standalone = STANDALONE_SIZES[size];
437
+ if (standalone) {
438
+ width = standalone.width;
439
+ height = standalone.height;
440
+ }
441
+ }
442
+ baseQuality = Math.round(baseQuality * qualityFactor);
439
443
  let quality = baseQuality;
440
444
  let effort = 4;
441
445
  let pngCompression = 6;
@@ -463,12 +467,11 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
463
467
  pngCompression = 7;
464
468
  }
465
469
  }
466
- const dimensions = size && SIZE_PRESETS[size] ? SIZE_PRESETS[size] : void 0;
467
470
  return {
468
471
  quality: Math.max(1, Math.min(100, quality)),
469
472
  effort,
470
473
  pngCompression,
471
- ...dimensions && { width: dimensions.width, height: dimensions.height }
474
+ ...width && height && { width, height }
472
475
  };
473
476
  };
474
477
  var objectIdSchema = z.string().refine((val) => isValidObjectId(val), {
@@ -1417,6 +1420,46 @@ var driveDelete = async (source, options) => {
1417
1420
  const owner = drive.owner;
1418
1421
  await provider.delete([driveId], owner, accountId);
1419
1422
  };
1423
+ var resolveFolderByPath = async (folderPath, owner, accountId) => {
1424
+ const normalizedPath = folderPath.replace(/^\/+|\/+$/g, "");
1425
+ if (!normalizedPath) {
1426
+ throw new Error("Folder path cannot be empty");
1427
+ }
1428
+ const segments = normalizedPath.split("/").filter((s) => s.length > 0);
1429
+ if (segments.length === 0) {
1430
+ throw new Error("Invalid folder path");
1431
+ }
1432
+ let providerName = "LOCAL";
1433
+ if (accountId && accountId !== "LOCAL") {
1434
+ const account = await drive_default.db.model("StorageAccount").findOne({ _id: accountId, owner });
1435
+ if (!account) {
1436
+ throw new Error("Invalid Storage Account");
1437
+ }
1438
+ if (account.metadata.provider === "GOOGLE") {
1439
+ providerName = "GOOGLE";
1440
+ }
1441
+ }
1442
+ let currentParentId = null;
1443
+ for (const segmentName of segments) {
1444
+ const existingFolder = await drive_default.findOne({
1445
+ owner,
1446
+ "provider.type": providerName,
1447
+ storageAccountId: accountId || null,
1448
+ parentId: currentParentId,
1449
+ name: segmentName,
1450
+ "information.type": "FOLDER",
1451
+ trashedAt: null
1452
+ });
1453
+ if (existingFolder) {
1454
+ currentParentId = String(existingFolder._id);
1455
+ } else {
1456
+ const provider = providerName === "GOOGLE" ? GoogleDriveProvider : LocalStorageProvider;
1457
+ const newFolder = await provider.createFolder(segmentName, currentParentId, owner, accountId);
1458
+ currentParentId = newFolder.id;
1459
+ }
1460
+ }
1461
+ return currentParentId;
1462
+ };
1420
1463
  var driveUpload = async (source, key, options) => {
1421
1464
  const config = getDriveConfig();
1422
1465
  let provider = LocalStorageProvider;
@@ -1506,12 +1549,20 @@ var driveUpload = async (source, key, options) => {
1506
1549
  throw new Error("Storage quota exceeded");
1507
1550
  }
1508
1551
  }
1552
+ let resolvedParentId = null;
1553
+ if (options.folder && "path" in options.folder) {
1554
+ resolvedParentId = await resolveFolderByPath(options.folder.path, key, accountId);
1555
+ } else if (options.folder && "id" in options.folder && options.folder.id !== "root") {
1556
+ resolvedParentId = options.folder.id;
1557
+ } else if (options.parentId && options.parentId !== "root") {
1558
+ resolvedParentId = options.parentId;
1559
+ }
1509
1560
  const drive = new drive_default({
1510
1561
  owner: key,
1511
1562
  storageAccountId: accountId || null,
1512
1563
  provider: { type: provider.name },
1513
1564
  name: options.name,
1514
- parentId: options.parentId === "root" || !options.parentId ? null : options.parentId,
1565
+ parentId: resolvedParentId,
1515
1566
  order: await getNextOrderValue(key),
1516
1567
  information: { type: "FILE", sizeInBytes: fileSize, mime: mimeType, path: "" },
1517
1568
  status: "UPLOADING"
@@ -1672,6 +1723,9 @@ var driveAPIHandler = async (req, res) => {
1672
1723
  const settings = getImageSettings(fileSize, quality, display, sizePreset);
1673
1724
  let targetFormat = format || mime.split("/")[1];
1674
1725
  if (targetFormat === "jpg") targetFormat = "jpeg";
1726
+ if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
1727
+ targetFormat = format || "webp";
1728
+ }
1675
1729
  const cacheDir = path.join(config.storage.path, "file", drive._id.toString(), "cache");
1676
1730
  const cacheKey = [
1677
1731
  "opt",
@@ -1708,13 +1762,17 @@ var driveAPIHandler = async (req, res) => {
1708
1762
  pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
1709
1763
  res.setHeader("Content-Type", "image/png");
1710
1764
  } else if (targetFormat === "webp") {
1711
- pipeline = pipeline.webp({ quality: settings.quality, effort: settings.effort });
1765
+ const webpEffort = Math.min(settings.effort, 6);
1766
+ pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
1712
1767
  res.setHeader("Content-Type", "image/webp");
1713
1768
  } else if (targetFormat === "avif") {
1714
1769
  pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
1715
1770
  res.setHeader("Content-Type", "image/avif");
1716
1771
  }
1717
1772
  res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1773
+ pipeline.on("error", (err) => {
1774
+ console.error("[next-drive] Pipeline error:", err);
1775
+ });
1718
1776
  stream.pipe(pipeline);
1719
1777
  pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
1720
1778
  pipeline.clone().pipe(res);
@@ -2258,5 +2316,5 @@ var driveAPIHandler = async (req, res) => {
2258
2316
  };
2259
2317
 
2260
2318
  export { driveAPIHandler, driveConfiguration, driveDelete, driveFilePath, driveFileSchemaZod, driveGetUrl, driveInfo, driveList, driveReadFile, driveUpload, getDriveConfig, getDriveInformation };
2261
- //# sourceMappingURL=chunk-UT2XCOS7.js.map
2262
- //# sourceMappingURL=chunk-UT2XCOS7.js.map
2319
+ //# sourceMappingURL=chunk-AYHO6FSR.js.map
2320
+ //# sourceMappingURL=chunk-AYHO6FSR.js.map