diff --git a/.talismanrc b/.talismanrc index b54938c5..e446461c 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,4 @@ fileignoreconfig: - filename: pnpm-lock.yaml - checksum: 8b5a2f43585d3191cdc71ad611f50c94b6d13fb7442cf4218ee0851a068af178 + checksum: 7a2d08a029dd995917883504dd816fc7a579aca7d3e39fc5368959f0e766c7b2 version: '1.0' diff --git a/packages/contentstack-asset-management/src/export/asset-types.ts b/packages/contentstack-asset-management/src/export/asset-types.ts index 2a88e9d1..6223b38d 100644 --- a/packages/contentstack-asset-management/src/export/asset-types.ts +++ b/packages/contentstack-asset-management/src/export/asset-types.ts @@ -13,15 +13,17 @@ export default class ExportAssetTypes extends AssetManagementExportAdapter { async start(spaceUid: string): Promise { await this.init(); + + log.debug('Starting shared asset types export process...', this.exportContext.context); + const assetTypesData = await this.getWorkspaceAssetTypes(spaceUid); const items = getArrayFromResponse(assetTypesData, 'asset_types'); const dir = this.getAssetTypesDir(); - log.debug( - items.length === 0 - ? 'No asset types, wrote empty asset-types' - : `Writing ${items.length} shared asset types`, - this.exportContext.context, - ); + if (items.length === 0) { + log.info('No asset types to export, writing empty asset-types', this.exportContext.context); + } else { + log.debug(`Writing ${items.length} shared asset types`, this.exportContext.context); + } await this.writeItemsToChunkedJson(dir, 'asset-types.json', 'asset_types', ['uid', 'title', 'category', 'file_extension'], items); this.tick(true, PROCESS_NAMES.AM_ASSET_TYPES, null); } diff --git a/packages/contentstack-asset-management/src/export/assets.ts b/packages/contentstack-asset-management/src/export/assets.ts index 28016691..acd0f167 100644 --- a/packages/contentstack-asset-management/src/export/assets.ts +++ b/packages/contentstack-asset-management/src/export/assets.ts @@ -7,6 +7,7 @@ import type { AssetManagementAPIConfig, LinkedWorkspace } from '../types/asset-m import type { ExportContext } from '../types/export-types'; import { AssetManagementExportAdapter } from './base'; import { getAssetItems, writeStreamToFile } from '../utils/export-helpers'; +import { runInBatches } from '../utils/concurrent-batch'; import { PROCESS_NAMES, PROCESS_STATUS } from '../constants/index'; export default class ExportAssets extends AssetManagementExportAdapter { @@ -16,8 +17,14 @@ export default class ExportAssets extends AssetManagementExportAdapter { async start(workspace: LinkedWorkspace, spaceDir: string): Promise { await this.init(); + + log.debug(`Starting assets export for space ${workspace.space_uid}`, this.exportContext.context); + log.info(`Exporting asset folders, metadata, and files for space ${workspace.space_uid}`, this.exportContext.context); + const assetsDir = pResolve(spaceDir, 'assets'); await mkdir(assetsDir, { recursive: true }); + log.debug(`Assets directory ready: ${assetsDir}`, this.exportContext.context); + log.debug(`Fetching folders and assets for space ${workspace.space_uid}`, this.exportContext.context); const [folders, assetsData] = await Promise.all([ @@ -43,37 +50,61 @@ export default class ExportAssets extends AssetManagementExportAdapter { ['uid', 'url', 'filename', 'file_name', 'parent_uid'], assetItems, ); + log.debug( + `Finished writing chunked assets metadata (${assetItems.length} item(s)) under ${assetsDir}`, + this.exportContext.context, + ); + log.info( + assetItems.length === 0 + ? `Wrote empty asset metadata for space ${workspace.space_uid}` + : `Wrote ${assetItems.length} asset metadata record(s) for space ${workspace.space_uid}`, + this.exportContext.context, + ); this.tick(true, `assets: ${workspace.space_uid} (${assetItems.length})`, null); + log.debug(`Starting binary downloads for space ${workspace.space_uid}`, this.exportContext.context); await this.downloadWorkspaceAssets(assetsData, assetsDir, workspace.space_uid); } - private async downloadWorkspaceAssets( - assetsData: unknown, - assetsDir: string, - spaceUid: string, - ): Promise { + private async downloadWorkspaceAssets(assetsData: unknown, assetsDir: string, spaceUid: string): Promise { const items = getAssetItems(assetsData); if (items.length === 0) { + log.info(`No asset files to download for space ${spaceUid}`, this.exportContext.context); log.debug('No assets to download', this.exportContext.context); return; } this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].DOWNLOADING); + log.info(`Downloading asset files for space ${spaceUid} (${items.length} in metadata)`, this.exportContext.context); log.debug(`Downloading ${items.length} asset file(s) for space ${spaceUid}...`, this.exportContext.context); const filesDir = pResolve(assetsDir, 'files'); await mkdir(filesDir, { recursive: true }); + log.debug(`Asset files directory ready: ${filesDir}`, this.exportContext.context); const securedAssets = this.exportContext.securedAssets ?? false; const authtoken = securedAssets ? configHandler.get('authtoken') : null; + log.debug( + `Asset downloads: securedAssets=${securedAssets}, concurrency=${this.downloadAssetsBatchConcurrency}`, + this.exportContext.context, + ); let lastError: string | null = null; let allSuccess = true; + let downloadOk = 0; + let downloadFail = 0; - for (const asset of items) { + const validItems = items.filter((asset) => Boolean(asset.url && (asset.uid ?? asset._uid))); + const skipped = items.length - validItems.length; + if (skipped > 0) { + log.debug( + `Skipping ${skipped} asset row(s) without url or uid (${validItems.length} file download(s) scheduled)`, + this.exportContext.context, + ); + } + await runInBatches(validItems, this.downloadAssetsBatchConcurrency, async (asset) => { const uid = asset.uid ?? asset._uid; const url = asset.url; const filename = asset.filename ?? asset.file_name ?? 'asset'; - if (!url || !uid) continue; + if (!url || !uid) return; try { const separator = url.includes('?') ? '&' : '?'; const downloadUrl = securedAssets && authtoken ? `${url}${separator}authtoken=${authtoken}` : url; @@ -86,15 +117,26 @@ export default class ExportAssets extends AssetManagementExportAdapter { await mkdir(assetFolderPath, { recursive: true }); const filePath = pResolve(assetFolderPath, filename); await writeStreamToFile(nodeStream, filePath); - log.debug(`Downloaded asset ${uid}`, this.exportContext.context); + downloadOk += 1; + log.debug(`Downloaded asset ${uid} → ${filePath}`, this.exportContext.context); } catch (e) { allSuccess = false; + downloadFail += 1; lastError = (e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].FAILED; log.debug(`Failed to download asset ${uid}: ${e}`, this.exportContext.context); } - } + }); this.tick(allSuccess, `downloads: ${spaceUid}`, lastError); - log.debug('Asset downloads completed', this.exportContext.context); + log.info( + allSuccess + ? `Finished downloading ${downloadOk} asset file(s) for space ${spaceUid}` + : `Asset downloads for space ${spaceUid} completed with errors: ${downloadOk} succeeded, ${downloadFail} failed`, + this.exportContext.context, + ); + log.debug( + `Asset downloads finished for space ${spaceUid}: ok=${downloadOk}, failed=${downloadFail}, allSuccess=${allSuccess}`, + this.exportContext.context, + ); } } diff --git a/packages/contentstack-asset-management/src/export/base.ts b/packages/contentstack-asset-management/src/export/base.ts index 6fff78b4..055d2d3b 100644 --- a/packages/contentstack-asset-management/src/export/base.ts +++ b/packages/contentstack-asset-management/src/export/base.ts @@ -5,7 +5,7 @@ import { FsUtility, log, CLIProgressManager, configHandler } from '@contentstack import type { AssetManagementAPIConfig } from '../types/asset-management-api'; import type { ExportContext } from '../types/export-types'; import { AssetManagementAdapter } from '../utils/asset-management-api-adapter'; -import { AM_MAIN_PROCESS_NAME, FALLBACK_AM_CHUNK_FILE_SIZE_MB } from '../constants/index'; +import { AM_MAIN_PROCESS_NAME, FALLBACK_AM_API_CONCURRENCY, FALLBACK_AM_CHUNK_FILE_SIZE_MB } from '../constants/index'; export type { ExportContext }; @@ -63,6 +63,16 @@ export class AssetManagementExportAdapter extends AssetManagementAdapter { return this.exportContext.spacesRootPath; } + /** Parallel AM export limit for bootstrap and default batch operations. */ + protected get apiConcurrency(): number { + return this.exportContext.apiConcurrency ?? FALLBACK_AM_API_CONCURRENCY; + } + + /** Asset download batch size; falls back to {@link apiConcurrency}. */ + protected get downloadAssetsBatchConcurrency(): number { + return this.exportContext.downloadAssetsConcurrency ?? this.apiConcurrency; + } + protected getAssetTypesDir(): string { return pResolve(this.exportContext.spacesRootPath, 'asset_types'); } diff --git a/packages/contentstack-asset-management/src/export/fields.ts b/packages/contentstack-asset-management/src/export/fields.ts index 2960c024..fd997e5e 100644 --- a/packages/contentstack-asset-management/src/export/fields.ts +++ b/packages/contentstack-asset-management/src/export/fields.ts @@ -13,13 +13,17 @@ export default class ExportFields extends AssetManagementExportAdapter { async start(spaceUid: string): Promise { await this.init(); + + log.debug('Starting shared fields export process...', this.exportContext.context); + const fieldsData = await this.getWorkspaceFields(spaceUid); const items = getArrayFromResponse(fieldsData, 'fields'); const dir = this.getFieldsDir(); - log.debug( - items.length === 0 ? 'No field items, wrote empty fields' : `Writing ${items.length} shared fields`, - this.exportContext.context, - ); + if (items.length === 0) { + log.info('No field items to export, writing empty fields', this.exportContext.context); + } else { + log.debug(`Writing ${items.length} shared fields`, this.exportContext.context); + } await this.writeItemsToChunkedJson(dir, 'fields.json', 'fields', ['uid', 'title', 'display_type'], items); this.tick(true, PROCESS_NAMES.AM_FIELDS, null); } diff --git a/packages/contentstack-asset-management/src/export/spaces.ts b/packages/contentstack-asset-management/src/export/spaces.ts index 5bb6c747..5d639a41 100644 --- a/packages/contentstack-asset-management/src/export/spaces.ts +++ b/packages/contentstack-asset-management/src/export/spaces.ts @@ -1,6 +1,6 @@ import { resolve as pResolve } from 'node:path'; import { mkdir } from 'node:fs/promises'; -import { log, CLIProgressManager, configHandler } from '@contentstack/cli-utilities'; +import { log, CLIProgressManager, configHandler, handleAndLogError } from '@contentstack/cli-utilities'; import type { AssetManagementExportOptions, AssetManagementAPIConfig } from '../types/asset-management-api'; import type { ExportContext } from '../types/export-types'; @@ -46,6 +46,8 @@ export class ExportSpaces { return; } + log.debug('Starting Asset Management export process...', context); + log.info('Started Asset Management export', context); log.debug(`Exporting Asset Management 2.0 (${linkedWorkspaces.length} space(s))`, context); log.debug(`Spaces: ${linkedWorkspaces.map((ws) => ws.space_uid).join(', ')}`, context); @@ -70,6 +72,8 @@ export class ExportSpaces { context, securedAssets, chunkFileSizeMb, + apiConcurrency: this.options.apiConcurrency, + downloadAssetsConcurrency: this.options.downloadAssetsConcurrency, }; const sharedFieldsDir = pResolve(spacesRootPath, 'fields'); @@ -81,11 +85,9 @@ export class ExportSpaces { try { const exportAssetTypes = new ExportAssetTypes(apiConfig, exportContext); exportAssetTypes.setParentProgressManager(progress); - await exportAssetTypes.start(firstSpaceUid); - const exportFields = new ExportFields(apiConfig, exportContext); exportFields.setParentProgressManager(progress); - await exportFields.start(firstSpaceUid); + await Promise.all([exportAssetTypes.start(firstSpaceUid), exportFields.start(firstSpaceUid)]); for (const ws of linkedWorkspaces) { progress.updateStatus(`Exporting space: ${ws.space_uid}...`, AM_MAIN_PROCESS_NAME); @@ -109,9 +111,11 @@ export class ExportSpaces { } progress.completeProcess(AM_MAIN_PROCESS_NAME, true); + log.info('Asset Management export completed successfully', context); log.debug('Asset Management 2.0 export completed', context); } catch (err) { progress.completeProcess(AM_MAIN_PROCESS_NAME, false); + handleAndLogError(err, { ...(context as Record) }, 'Asset Management export failed'); throw err; } } diff --git a/packages/contentstack-asset-management/src/export/workspaces.ts b/packages/contentstack-asset-management/src/export/workspaces.ts index 88cfd397..c2f5bb4f 100644 --- a/packages/contentstack-asset-management/src/export/workspaces.ts +++ b/packages/contentstack-asset-management/src/export/workspaces.ts @@ -15,6 +15,9 @@ export default class ExportWorkspace extends AssetManagementExportAdapter { async start(workspace: LinkedWorkspace, spaceDir: string, branchName: string): Promise { await this.init(); + + log.debug(`Starting export for AM space ${workspace.space_uid}`, this.exportContext.context); + const spaceResponse = await this.getSpace(workspace.space_uid); const space = spaceResponse.space; await mkdir(spaceDir, { recursive: true }); @@ -25,7 +28,13 @@ export default class ExportWorkspace extends AssetManagementExportAdapter { is_default: workspace.is_default, branch: branchName || 'main', }; - await writeFile(pResolve(spaceDir, 'metadata.json'), JSON.stringify(metadata, null, 2)); + const metadataPath = pResolve(spaceDir, 'metadata.json'); + try { + await writeFile(metadataPath, JSON.stringify(metadata, null, 2)); + } catch (e) { + log.warn(`Could not write ${metadataPath}: ${e}`, this.exportContext.context); + throw e; + } this.tick(true, `space: ${workspace.space_uid}`, null); log.debug(`Space metadata written for ${workspace.space_uid}`, this.exportContext.context); diff --git a/packages/contentstack-asset-management/src/import/asset-types.ts b/packages/contentstack-asset-management/src/import/asset-types.ts index e5cc4f15..71f5fbba 100644 --- a/packages/contentstack-asset-management/src/import/asset-types.ts +++ b/packages/contentstack-asset-management/src/import/asset-types.ts @@ -31,19 +31,24 @@ export default class ImportAssetTypes extends AssetManagementImportAdapter { async start(): Promise { await this.init(); + log.debug('Starting shared asset types import process...', this.importContext.context); + const stripKeys = this.importContext.assetTypesImportInvalidKeys ?? [...FALLBACK_ASSET_TYPES_IMPORT_INVALID_KEYS]; const dir = this.getAssetTypesDir(); const indexName = this.importContext.assetTypesFileName ?? 'asset-types.json'; const indexPath = join(dir, indexName); if (!existsSync(indexPath)) { - log.debug('No shared asset types to import (index missing)', this.importContext.context); + log.info('No shared asset types to import (index missing)', this.importContext.context); return; } const existingByUid = await this.loadExistingAssetTypesMap(); - this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_ASSET_TYPES].IMPORTING, PROCESS_NAMES.AM_IMPORT_ASSET_TYPES); + this.updateStatus( + PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_ASSET_TYPES].IMPORTING, + PROCESS_NAMES.AM_IMPORT_ASSET_TYPES, + ); await forEachChunkedJsonStore>( dir, @@ -51,10 +56,8 @@ export default class ImportAssetTypes extends AssetManagementImportAdapter { { context: this.importContext.context, chunkReadLogLabel: 'asset-types', - onOpenError: (e) => - log.debug(`Could not open chunked asset-types index: ${e}`, this.importContext.context), - onEmptyIndexer: () => - log.debug('No shared asset types to import (empty indexer)', this.importContext.context), + onOpenError: (e) => log.warn(`Could not open chunked asset-types index: ${e}`, this.importContext.context), + onEmptyIndexer: () => log.debug('No shared asset types to import (empty indexer)', this.importContext.context), }, async (records) => { const toCreate = this.buildAssetTypesToCreate(records, existingByUid, stripKeys); @@ -103,7 +106,10 @@ export default class ImportAssetTypes extends AssetManagementImportAdapter { this.importContext.context, ); } else { - log.debug(`Asset type "${uid}" already exists with matching definition, skipping`, this.importContext.context); + log.debug( + `Asset type "${uid}" already exists with matching definition, skipping`, + this.importContext.context, + ); } this.tick(true, `asset-type: ${uid} (skipped, already exists)`, null, PROCESS_NAMES.AM_IMPORT_ASSET_TYPES); continue; diff --git a/packages/contentstack-asset-management/src/import/assets.ts b/packages/contentstack-asset-management/src/import/assets.ts index 14c5baed..b6972124 100644 --- a/packages/contentstack-asset-management/src/import/assets.ts +++ b/packages/contentstack-asset-management/src/import/assets.ts @@ -64,6 +64,11 @@ export default class ImportAssets extends AssetManagementImportAdapter { const uidMap: Record = {}; const urlMap: Record = {}; + log.debug( + `Building identity mappers from export (reuse path, spaceDir=${spaceDir})`, + this.importContext.context, + ); + const loc = this.resolveAssetsChunkedLocation(spaceDir); if (!loc) { log.debug( @@ -73,6 +78,11 @@ export default class ImportAssets extends AssetManagementImportAdapter { return { uidMap, urlMap }; } + log.debug( + `Reading chunked assets for identity map: ${loc.assetsDir} (index: ${loc.indexName})`, + this.importContext.context, + ); + const fs = new FsUtility({ basePath: loc.assetsDir, indexFileName: loc.indexName }); let totalRows = 0; @@ -93,7 +103,11 @@ export default class ImportAssets extends AssetManagementImportAdapter { ); log.debug( - `Built identity mappers for ${totalRows} exported asset row(s) (reuse path, chunked read)`, + `Built identity mappers for ${totalRows} exported asset row(s): ${Object.keys(uidMap).length} uid entries, ${Object.keys(urlMap).length} url entries`, + this.importContext.context, + ); + log.info( + `Prepared identity uid/url mappers from ${totalRows} exported asset row(s) (reuse existing space)`, this.importContext.context, ); @@ -108,6 +122,10 @@ export default class ImportAssets extends AssetManagementImportAdapter { const uidMap: Record = {}; const urlMap: Record = {}; + log.debug(`Starting assets and folders import for space ${newSpaceUid}`, this.importContext.context); + log.info(`Importing folders and assets into space ${newSpaceUid}`, this.importContext.context); + log.debug(`Assets directory: ${assetsDir}`, this.importContext.context); + // ----------------------------------------------------------------------- // 1. Import folders // ----------------------------------------------------------------------- @@ -115,19 +133,35 @@ export default class ImportAssets extends AssetManagementImportAdapter { const foldersFileName = this.importContext.foldersFileName ?? 'folders.json'; const foldersFilePath = join(assetsDir, foldersFileName); + if (!existsSync(foldersFilePath)) { + log.debug(`No ${foldersFileName} at ${foldersFilePath}, skipping folder import`, this.importContext.context); + } + if (existsSync(foldersFilePath)) { let foldersData: unknown; try { foldersData = JSON.parse(readFileSync(foldersFilePath, 'utf8')); } catch (e) { - log.debug(`Could not read ${foldersFileName}: ${e}`, this.importContext.context); + log.warn(`Could not read ${foldersFileName}: ${e}`, this.importContext.context); } if (foldersData) { + log.debug(`Reading folders from ${foldersFilePath}`, this.importContext.context); const folders = getArrayFromResponse(foldersData, 'folders') as FolderRecord[]; this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_FOLDERS].IMPORTING, PROCESS_NAMES.AM_IMPORT_FOLDERS); - log.debug(`Importing ${folders.length} folder(s) for space ${newSpaceUid}`, this.importContext.context); + log.debug( + `Importing ${folders.length} folder(s) for space ${newSpaceUid} (concurrency=${this.importFoldersBatchConcurrency})`, + this.importContext.context, + ); await this.importFolders(newSpaceUid, folders, folderUidMap); + log.debug( + `Folder import phase complete: ${Object.keys(folderUidMap).length} exported folder uid(s) mapped to target`, + this.importContext.context, + ); + log.info( + `Finished importing ${Object.keys(folderUidMap).length} folder(s) for space ${newSpaceUid}`, + this.importContext.context, + ); } } @@ -136,18 +170,25 @@ export default class ImportAssets extends AssetManagementImportAdapter { // ----------------------------------------------------------------------- const loc = this.resolveAssetsChunkedLocation(spaceDir); if (!loc) { + log.info( + `No asset metadata index in ${assetsDir}; skipping file uploads for space ${newSpaceUid}`, + this.importContext.context, + ); log.debug(`No assets.json index found in ${assetsDir}, skipping asset upload`, this.importContext.context); return { uidMap, urlMap }; } this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_ASSETS].IMPORTING, PROCESS_NAMES.AM_IMPORT_ASSETS); log.debug( - `Uploading assets for space ${newSpaceUid} from chunked export (incremental chunks)`, + `Uploading assets for space ${newSpaceUid} from ${loc.assetsDir} (index: ${loc.indexName}, concurrency=${this.uploadAssetsBatchConcurrency})`, this.importContext.context, ); const assetFs = new FsUtility({ basePath: loc.assetsDir, indexFileName: loc.indexName }); let exportRowCount = 0; + let uploadOk = 0; + let uploadFail = 0; + let missingFiles = 0; await forEachChunkRecordsFromFs( assetFs, @@ -162,7 +203,8 @@ export default class ImportAssets extends AssetManagementImportAdapter { const filePath = pResolve(assetsDir, 'files', oldUid, filename); if (!existsSync(filePath)) { - log.debug(`Asset file not found: ${filePath}, skipping`, this.importContext.context); + missingFiles += 1; + log.warn(`Asset file not found: ${filePath}, skipping`, this.importContext.context); this.tick(false, `asset: ${oldUid}`, 'File not found on disk', PROCESS_NAMES.AM_IMPORT_ASSETS); continue; } @@ -173,6 +215,12 @@ export default class ImportAssets extends AssetManagementImportAdapter { uploadJobs.push({ asset, filePath, mappedParentUid, oldUid }); } + const skippedInChunk = assetChunk.length - uploadJobs.length; + log.debug( + `Asset chunk: ${assetChunk.length} row(s), ${uploadJobs.length} upload job(s)${skippedInChunk ? `, ${skippedInChunk} missing on disk` : ''}`, + this.importContext.context, + ); + await runInBatches( uploadJobs, this.uploadAssetsBatchConcurrency, @@ -192,8 +240,10 @@ export default class ImportAssets extends AssetManagementImportAdapter { } this.tick(true, `asset: ${oldUid}`, null, PROCESS_NAMES.AM_IMPORT_ASSETS); - log.debug(`Uploaded asset ${oldUid} → ${created.uid}`, this.importContext.context); + uploadOk += 1; + log.debug(`Uploaded asset ${oldUid} → ${created.uid} (${filePath})`, this.importContext.context); } catch (e) { + uploadFail += 1; this.tick( false, `asset: ${oldUid}`, @@ -208,7 +258,13 @@ export default class ImportAssets extends AssetManagementImportAdapter { ); log.debug( - `Finished asset uploads for space ${newSpaceUid} (${exportRowCount} row(s) read from export chunks)`, + `Finished asset uploads for space ${newSpaceUid}: rows=${exportRowCount}, ok=${uploadOk}, failed=${uploadFail}, missingFile=${missingFiles}`, + this.importContext.context, + ); + log.info( + uploadFail === 0 && missingFiles === 0 + ? `Finished importing ${uploadOk} asset file(s) for space ${newSpaceUid}` + : `Finished importing assets for space ${newSpaceUid}: ${uploadOk} uploaded, ${uploadFail} failed, ${missingFiles} missing on disk`, this.importContext.context, ); @@ -226,8 +282,10 @@ export default class ImportAssets extends AssetManagementImportAdapter { ): Promise { let remaining = [...folders]; let prevLength = -1; + let pass = 0; while (remaining.length > 0 && remaining.length !== prevLength) { + pass += 1; prevLength = remaining.length; const ready: FolderRecord[] = []; const nextPass: FolderRecord[] = []; @@ -244,6 +302,11 @@ export default class ImportAssets extends AssetManagementImportAdapter { } } + log.debug( + `Folder import pass ${pass}: creating ${ready.length} folder(s), ${nextPass.length} blocked on parent (${remaining.length} total remaining before this pass)`, + this.importContext.context, + ); + await runInBatches(ready, this.importFoldersBatchConcurrency, async (folder) => { const { parent_uid: parentUid } = folder; const isRootParent = !parentUid || parentUid === 'root'; @@ -270,8 +333,13 @@ export default class ImportAssets extends AssetManagementImportAdapter { remaining = nextPass; } + log.debug( + `Folder import passes finished for space ${newSpaceUid} after ${pass} pass(es); ${Object.keys(folderUidMap).length} folder uid(s) mapped`, + this.importContext.context, + ); + if (remaining.length > 0) { - log.debug( + log.warn( `${remaining.length} folder(s) could not be imported (unresolved parent UIDs)`, this.importContext.context, ); diff --git a/packages/contentstack-asset-management/src/import/fields.ts b/packages/contentstack-asset-management/src/import/fields.ts index 1e431b14..9785906c 100644 --- a/packages/contentstack-asset-management/src/import/fields.ts +++ b/packages/contentstack-asset-management/src/import/fields.ts @@ -31,13 +31,15 @@ export default class ImportFields extends AssetManagementImportAdapter { async start(): Promise { await this.init(); + log.debug('Starting shared fields import process...', this.importContext.context); + const stripKeys = this.importContext.fieldsImportInvalidKeys ?? [...FALLBACK_FIELDS_IMPORT_INVALID_KEYS]; const dir = this.getFieldsDir(); const indexName = this.importContext.fieldsFileName ?? 'fields.json'; const indexPath = join(dir, indexName); if (!existsSync(indexPath)) { - log.debug('No shared fields to import (index missing)', this.importContext.context); + log.info('No shared fields to import (index missing)', this.importContext.context); return; } @@ -51,7 +53,7 @@ export default class ImportFields extends AssetManagementImportAdapter { { context: this.importContext.context, chunkReadLogLabel: 'fields', - onOpenError: (e) => log.debug(`Could not open chunked fields index: ${e}`, this.importContext.context), + onOpenError: (e) => log.warn(`Could not open chunked fields index: ${e}`, this.importContext.context), onEmptyIndexer: () => log.debug('No shared fields to import (empty indexer)', this.importContext.context), }, async (records) => { diff --git a/packages/contentstack-asset-management/src/import/spaces.ts b/packages/contentstack-asset-management/src/import/spaces.ts index b5a7165e..6f66d24b 100644 --- a/packages/contentstack-asset-management/src/import/spaces.ts +++ b/packages/contentstack-asset-management/src/import/spaces.ts @@ -1,7 +1,7 @@ import { join, resolve as pResolve } from 'node:path'; import { mkdirSync, readdirSync, statSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; -import { log, CLIProgressManager, configHandler } from '@contentstack/cli-utilities'; +import { log, CLIProgressManager, configHandler, handleAndLogError } from '@contentstack/cli-utilities'; import type { AssetManagementAPIConfig, @@ -70,6 +70,8 @@ export class ImportSpaces { context, }; + log.debug('Starting Asset Management import process...', context); + // Discover space directories let spaceDirs: string[] = []; try { @@ -81,7 +83,7 @@ export class ImportSpaces { } }); } catch (e) { - log.debug(`Could not read spaces root path ${spacesRootPath}: ${e}`, context); + log.warn(`Could not read spaces root path ${spacesRootPath}: ${e}`, context); } const totalSteps = 2 + spaceDirs.length * 2; @@ -94,6 +96,8 @@ export class ImportSpaces { const allSpaceUidMap: Record = {}; const spaceMappings: SpaceMapping[] = []; let hasFailures = false; + let spacesSucceeded = 0; + let spacesFailed = 0; // Space UIDs already present in the target org — reuse when export dir name matches a uid here. const existingSpaceUids = new Set(); @@ -110,6 +114,8 @@ export class ImportSpaces { } try { + log.info('Started Asset Management import', context); + // 1. Import shared fields progress.updateStatus(`Importing shared fields...`, AM_MAIN_PROCESS_NAME); const fieldsImporter = new ImportFields(apiConfig, importContext); @@ -147,15 +153,17 @@ export class ImportSpaces { }); log.debug(`Imported space ${spaceUid} → ${result.newSpaceUid}`, context); + spacesSucceeded += 1; } catch (err) { hasFailures = true; + spacesFailed += 1; progress.tick( false, `space: ${spaceUid}`, (err as Error)?.message ?? 'Failed to import space', AM_MAIN_PROCESS_NAME, ); - log.debug(`Failed to import space ${spaceUid}: ${err}`, context); + log.warn(`Failed to import space ${spaceUid}: ${err}`, context); } } @@ -174,9 +182,14 @@ export class ImportSpaces { } progress.completeProcess(AM_MAIN_PROCESS_NAME, !hasFailures); + log.info( + `Asset Management import finished: ${spacesSucceeded} space(s) succeeded, ${spacesFailed} failed, ${spaceDirs.length} attempted.`, + context, + ); log.debug('Asset Management 2.0 import completed', context); } catch (err) { progress.completeProcess(AM_MAIN_PROCESS_NAME, false); + handleAndLogError(err, { ...(context as Record) }, 'Asset Management import failed'); throw err; } diff --git a/packages/contentstack-asset-management/src/import/workspaces.ts b/packages/contentstack-asset-management/src/import/workspaces.ts index 5d13450d..e042b1f3 100644 --- a/packages/contentstack-asset-management/src/import/workspaces.ts +++ b/packages/contentstack-asset-management/src/import/workspaces.ts @@ -30,13 +30,15 @@ export default class ImportWorkspace extends AssetManagementImportAdapter { ): Promise { await this.init(); + log.debug(`Starting import for AM space directory ${oldSpaceUid}`, this.importContext.context); + // Read exported metadata const metadataPath = join(spaceDir, 'metadata.json'); let metadata: Record = {}; try { metadata = JSON.parse(readFileSync(metadataPath, 'utf8')) as Record; } catch (e) { - log.debug(`Could not read metadata.json for space ${oldSpaceUid}: ${e}`, this.importContext.context); + log.warn(`Could not read ${metadataPath} for space ${oldSpaceUid}: ${e}`, this.importContext.context); } const exportedTitle = (metadata.title as string) ?? oldSpaceUid; diff --git a/packages/contentstack-asset-management/src/types/asset-management-api.ts b/packages/contentstack-asset-management/src/types/asset-management-api.ts index 4292821f..40423da8 100644 --- a/packages/contentstack-asset-management/src/types/asset-management-api.ts +++ b/packages/contentstack-asset-management/src/types/asset-management-api.ts @@ -151,6 +151,14 @@ export type AssetManagementExportOptions = { * FsUtility `chunkFileSize` in MB for AM export chunked writes. */ chunkFileSizeMb?: number; + /** + * Max parallel AM API/export tasks for export (shared module bootstrap default). + */ + apiConcurrency?: number; + /** + * Max parallel asset file downloads per workspace. + */ + downloadAssetsConcurrency?: number; }; // --------------------------------------------------------------------------- diff --git a/packages/contentstack-asset-management/src/types/export-types.ts b/packages/contentstack-asset-management/src/types/export-types.ts index 7cefa319..865302a6 100644 --- a/packages/contentstack-asset-management/src/types/export-types.ts +++ b/packages/contentstack-asset-management/src/types/export-types.ts @@ -3,6 +3,8 @@ export type ExportContext = { context?: Record; securedAssets?: boolean; chunkFileSizeMb?: number; + apiConcurrency?: number; + downloadAssetsConcurrency?: number; }; /** diff --git a/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts b/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts index b26b2664..b5398c81 100644 --- a/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts +++ b/packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs'; import { basename } from 'node:path'; -import { HttpClient, log, authenticationHandler } from '@contentstack/cli-utilities'; +import { HttpClient, log, authenticationHandler, handleAndLogError } from '@contentstack/cli-utilities'; import type { AssetManagementAPIConfig, @@ -93,7 +93,11 @@ export class AssetManagementAdapter implements IAssetManagementAdapter { this.apiClient.headers(this.config.headers ? { ...authHeader, ...this.config.headers } : authHeader); log.debug('Asset Management adapter initialization completed', this.config.context); } catch (error: unknown) { - log.debug(`Asset Management adapter initialization failed: ${error}`, this.config.context); + handleAndLogError( + error as Error, + this.config.context ? { ...(this.config.context as Record) } : {}, + 'Asset Management adapter initialization failed', + ); throw error; } } diff --git a/packages/contentstack-asset-management/test/unit/export/assets.test.ts b/packages/contentstack-asset-management/test/unit/export/assets.test.ts index 763285ca..ab6b831d 100644 --- a/packages/contentstack-asset-management/test/unit/export/assets.test.ts +++ b/packages/contentstack-asset-management/test/unit/export/assets.test.ts @@ -60,6 +60,16 @@ describe('ExportAssets', () => { }); describe('start method', () => { + it('should use fallback download concurrency when not configured', () => { + const exporter = new ExportAssets(apiConfig, exportContext); + expect((exporter as any).downloadAssetsBatchConcurrency).to.equal(5); + }); + + it('should use configured download concurrency when provided', () => { + const exporter = new ExportAssets(apiConfig, { ...exportContext, downloadAssetsConcurrency: 2 }); + expect((exporter as any).downloadAssetsBatchConcurrency).to.equal(2); + }); + it('should fetch folders and assets using the workspace space_uid', async () => { const foldersStub = sinon.stub(ExportAssets.prototype, 'getWorkspaceFolders').resolves(foldersData); const assetsStub = sinon.stub(ExportAssets.prototype, 'getWorkspaceAssets').resolves(emptyAssetsResponse); diff --git a/packages/contentstack-asset-management/test/unit/export/spaces.test.ts b/packages/contentstack-asset-management/test/unit/export/spaces.test.ts index 935b7d26..72e0910c 100644 --- a/packages/contentstack-asset-management/test/unit/export/spaces.test.ts +++ b/packages/contentstack-asset-management/test/unit/export/spaces.test.ts @@ -75,6 +75,26 @@ describe('ExportSpaces', () => { expect(fieldsStub.firstCall.args[0]).to.equal('space-1'); }); + it('should run shared asset types and fields exports in parallel', async () => { + const atStub = ExportAssetTypes.prototype.start as sinon.SinonStub; + const fieldsStub = ExportFields.prototype.start as sinon.SinonStub; + let resolveAssetTypes!: () => void; + const assetTypesGate = new Promise((resolve) => { + resolveAssetTypes = resolve; + }); + atStub.callsFake(async () => assetTypesGate); + + const exporter = new ExportSpaces(baseOptions); + const startPromise = exporter.start(); + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(atStub.calledOnce).to.be.true; + expect(fieldsStub.calledOnce).to.be.true; + + resolveAssetTypes(); + await startPromise; + }); + it('should iterate over all workspaces in order', async () => { const exporter = new ExportSpaces(baseOptions); await exporter.start(); @@ -109,6 +129,20 @@ describe('ExportSpaces', () => { expect(fakeProgress.completeProcess.firstCall.args).to.deep.equal([AM_MAIN_PROCESS_NAME, false]); }); + it('should mark progress as failed and re-throw when shared bootstrap export errors', async () => { + (ExportFields.prototype.start as sinon.SinonStub).rejects(new Error('shared-bootstrap-error')); + + const exporter = new ExportSpaces(baseOptions); + try { + await exporter.start(); + expect.fail('should have thrown'); + } catch (err: any) { + expect(err.message).to.equal('shared-bootstrap-error'); + } + + expect(fakeProgress.completeProcess.firstCall.args).to.deep.equal([AM_MAIN_PROCESS_NAME, false]); + }); + it('should use the provided parentProgressManager instead of creating a new one', async () => { const fakeParent = { addProcess: sinon.stub().returnsThis(), diff --git a/packages/contentstack-export/src/config/index.ts b/packages/contentstack-export/src/config/index.ts index 3b6c8ea4..da39dc24 100644 --- a/packages/contentstack-export/src/config/index.ts +++ b/packages/contentstack-export/src/config/index.ts @@ -114,6 +114,8 @@ const config: DefaultConfig = { }, 'asset-management': { chunkFileSizeMb: 1, + apiConcurrency: 5, + downloadAssetsConcurrency: 5, }, content_types: { dirName: 'content_types', diff --git a/packages/contentstack-export/src/export/modules/assets.ts b/packages/contentstack-export/src/export/modules/assets.ts index f25e2f31..c7dcd871 100644 --- a/packages/contentstack-export/src/export/modules/assets.ts +++ b/packages/contentstack-export/src/export/modules/assets.ts @@ -80,6 +80,8 @@ export default class ExportAssets extends BaseClass { context: this.exportConfig.context as unknown as Record, securedAssets: this.exportConfig.securedAssets, chunkFileSizeMb: assetManagementModuleConfig?.chunkFileSizeMb, + apiConcurrency: assetManagementModuleConfig?.apiConcurrency, + downloadAssetsConcurrency: assetManagementModuleConfig?.downloadAssetsConcurrency, }); exporter.setParentProgressManager(progress); await exporter.start(); diff --git a/packages/contentstack-export/src/types/default-config.ts b/packages/contentstack-export/src/types/default-config.ts index 3a07b46c..7fedadd6 100644 --- a/packages/contentstack-export/src/types/default-config.ts +++ b/packages/contentstack-export/src/types/default-config.ts @@ -99,6 +99,10 @@ export default interface DefaultConfig { 'asset-management': { /** Passed to FsUtility chunkFileSize (MB) when writing chunked export JSON. */ chunkFileSizeMb: number; + /** Shared export concurrency fallback used by AM 2.0 export. */ + apiConcurrency: number; + /** Parallel downloads per AM workspace export. */ + downloadAssetsConcurrency: number; dependencies?: Modules[]; }; content_types: { diff --git a/packages/contentstack-export/test/unit/export/modules/assets.test.ts b/packages/contentstack-export/test/unit/export/modules/assets.test.ts index 8d7f0b0c..de7dd4d7 100644 --- a/packages/contentstack-export/test/unit/export/modules/assets.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/assets.test.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { FsUtility, getDirectories } from '@contentstack/cli-utilities'; +import { ExportSpaces } from '@contentstack/cli-asset-management'; import ExportAssets from '../../../../src/export/modules/assets'; import { ExportConfig } from '../../../../src/types'; import { mockData, assetsMetaData } from '../../mock/assets'; @@ -138,6 +139,8 @@ describe('ExportAssets', () => { }, 'asset-management': { chunkFileSizeMb: 1, + apiConcurrency: 5, + downloadAssetsConcurrency: 5, }, content_types: { dirName: 'content_types', @@ -220,7 +223,7 @@ describe('ExportAssets', () => { dirName: 'composable_studio', fileName: 'composable_studio.json', apiBaseUrl: 'https://api.contentstack.io', - apiVersion: 'v3' + apiVersion: 'v3', }, }, } as ExportConfig; @@ -304,7 +307,8 @@ describe('ExportAssets', () => { completeProcess: sinon.stub(), tick: sinon.stub(), } as any); - sinon.stub(exportAssets as any, 'withLoadingSpinner').callsFake(async (msg: string, fn: () => Promise) => { + sinon.stub(exportAssets as any, 'withLoadingSpinner').callsFake(async (...args: unknown[]) => { + const fn = args[1] as () => Promise; return await fn(); }); sinon.stub(exportAssets as any, 'completeProgress'); @@ -329,6 +333,31 @@ describe('ExportAssets', () => { expect(downloadAssetsStub.calledOnce).to.be.true; }); + it('should forward AM export concurrency options to ExportSpaces', async () => { + mockExportConfig.linkedWorkspaces = [{ uid: 'ws-1', space_uid: 'am-space-1', is_default: true }]; + mockExportConfig.region.assetManagementUrl = 'https://am.example.com'; + mockExportConfig.org_uid = 'org-from-config'; + mockExportConfig.modules['asset-management'].chunkFileSizeMb = 2; + mockExportConfig.modules['asset-management'].apiConcurrency = 7; + mockExportConfig.modules['asset-management'].downloadAssetsConcurrency = 3; + + const progressManager = { addProcess: sinon.stub(), startProcess: sinon.stub(), updateStatus: sinon.stub() }; + ((exportAssets as any).createNestedProgress as sinon.SinonStub).returns(progressManager as any); + sinon.stub(exportAssets as any, 'completeProgressWithMessage'); + const setParentStub = sinon.stub(ExportSpaces.prototype, 'setParentProgressManager'); + const startStub = sinon.stub(ExportSpaces.prototype, 'start').resolves(); + + await exportAssets.start(); + + expect(setParentStub.calledOnceWith(progressManager as any)).to.be.true; + expect(startStub.calledOnce).to.be.true; + const forwardedOptions = (startStub.thisValues[0] as any).options; + expect(forwardedOptions.chunkFileSizeMb).to.equal(2); + expect(forwardedOptions.apiConcurrency).to.equal(7); + expect(forwardedOptions.downloadAssetsConcurrency).to.equal(3); + expect(forwardedOptions.org_uid).to.equal('org-from-config'); + }); + it('should export versioned assets when enabled', async () => { mockExportConfig.modules.assets.includeVersionedAssets = true; exportAssets.versionedAssets = [{ 'asset-1': 2 }]; diff --git a/packages/contentstack-export/test/unit/export/modules/base-class.test.ts b/packages/contentstack-export/test/unit/export/modules/base-class.test.ts index 3d0f67e0..52eefd92 100644 --- a/packages/contentstack-export/test/unit/export/modules/base-class.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/base-class.test.ts @@ -156,6 +156,8 @@ describe('BaseClass', () => { }, 'asset-management': { chunkFileSizeMb: 1, + apiConcurrency: 5, + downloadAssetsConcurrency: 5, }, content_types: { dirName: 'content_types', diff --git a/packages/contentstack-import/test/unit/import/modules/locales.test.ts b/packages/contentstack-import/test/unit/import/modules/locales.test.ts index 8839b84f..5bc5e8f7 100644 --- a/packages/contentstack-import/test/unit/import/modules/locales.test.ts +++ b/packages/contentstack-import/test/unit/import/modules/locales.test.ts @@ -62,6 +62,24 @@ describe('ImportLocales', () => { folderValidKeys: ['uid', 'name'], validKeys: ['uid', 'title'], }, + 'asset-management': { + dirName: 'spaces', + fieldsDir: 'fields', + assetTypesDir: 'asset_types', + fieldsFileName: 'fields.json', + assetTypesFileName: 'asset-types.json', + foldersFileName: 'folders.json', + assetsFileName: 'assets.json', + fieldsImportInvalidKeys: [], + assetTypesImportInvalidKeys: [], + mapperRootDir: 'mapper', + mapperAssetsModuleDir: 'assets', + mapperUidFileName: 'uid-mapping.json', + mapperUrlFileName: 'url-mapping.json', + mapperSpaceUidFileName: 'space-uid-mapping.json', + uploadAssetsConcurrency: 1, + importFoldersConcurrency: 1, + }, 'assets-old': { dirName: 'assets', fileName: 'assets.json', @@ -112,24 +130,6 @@ describe('ImportLocales', () => { apiBaseUrl: 'https://composable-studio-api.contentstack.com/v1', apiVersion: 'v1', }, - 'asset-management': { - dirName: 'spaces', - fieldsDir: 'fields', - assetTypesDir: 'asset_types', - fieldsFileName: 'fields.json', - assetTypesFileName: 'asset-types.json', - foldersFileName: 'folders.json', - assetsFileName: 'assets.json', - fieldsImportInvalidKeys: [], - assetTypesImportInvalidKeys: [], - mapperRootDir: 'mapper', - mapperAssetsModuleDir: 'assets', - mapperUidFileName: 'uid-mapping.json', - mapperUrlFileName: 'url-mapping.json', - mapperSpaceUidFileName: 'space-uid-mapping.json', - uploadAssetsConcurrency: 2, - importFoldersConcurrency: 1, - }, personalize: { baseURL: {}, dirName: 'personalize', diff --git a/packages/contentstack-import/test/unit/utils/extension-helper.test.ts b/packages/contentstack-import/test/unit/utils/extension-helper.test.ts index 2f77a330..f99a8fd9 100644 --- a/packages/contentstack-import/test/unit/utils/extension-helper.test.ts +++ b/packages/contentstack-import/test/unit/utils/extension-helper.test.ts @@ -41,24 +41,6 @@ describe('Extension Helper', () => { apiBaseUrl: 'https://composable-studio-api.contentstack.com', apiVersion: 'v1', }, - 'asset-management': { - dirName: 'spaces', - fieldsDir: 'fields', - assetTypesDir: 'asset_types', - fieldsFileName: 'fields.json', - assetTypesFileName: 'asset-types.json', - foldersFileName: 'folders.json', - assetsFileName: 'assets.json', - fieldsImportInvalidKeys: [], - assetTypesImportInvalidKeys: [], - mapperRootDir: 'mapper', - mapperAssetsModuleDir: 'assets', - mapperUidFileName: 'uid-mapping.json', - mapperUrlFileName: 'url-mapping.json', - mapperSpaceUidFileName: 'space-uid-mapping.json', - uploadAssetsConcurrency: 2, - importFoldersConcurrency: 1, - }, types: [], locales: { dirName: 'locales', fileName: 'locales.json', requiredKeys: ['code', 'name'] }, customRoles: { @@ -85,6 +67,24 @@ describe('Extension Helper', () => { folderValidKeys: ['uid', 'name'], validKeys: ['uid', 'title'], }, + 'asset-management': { + dirName: 'spaces', + fieldsDir: 'fields', + assetTypesDir: 'asset_types', + fieldsFileName: 'fields.json', + assetTypesFileName: 'asset-types.json', + foldersFileName: 'folders.json', + assetsFileName: 'assets.json', + fieldsImportInvalidKeys: [], + assetTypesImportInvalidKeys: [], + mapperRootDir: 'mapper', + mapperAssetsModuleDir: 'assets', + mapperUidFileName: 'uid-mapping.json', + mapperUrlFileName: 'url-mapping.json', + mapperSpaceUidFileName: 'space-uid-mapping.json', + uploadAssetsConcurrency: 1, + importFoldersConcurrency: 1, + }, 'assets-old': { dirName: 'assets', fileName: 'assets.json', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 305076a6..d7e7793b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1348,8 +1348,8 @@ packages: resolution: {integrity: sha512-ZTlxhUTlMIX0t3orbh4bJ73KOyC0553CC/1I12GavnOcVEbtJ26YLj7IG20lO4vDo3KjgSs604X+e2yX/0g1aA==} engines: {node: '>=8.0.0'} - '@contentstack/marketplace-sdk@1.5.0': - resolution: {integrity: sha512-n2USMwswXBDtmVOg0t5FUks8X0d49u0UDFSrwxti09X/SONeP0P8wSqIDCjoB2gGRQc6fg/Fg2YPRvejUWeR4A==} + '@contentstack/marketplace-sdk@1.5.1': + resolution: {integrity: sha512-XoQODTWZ4cQeo7iIAcYcYLX9bSHvgeF1J230GTM2dVhN3w9aTylZ35zZttvsa76fDZWgRmZBO5AE99dVVq7xyA==} '@contentstack/utils@1.7.1': resolution: {integrity: sha512-b/0t1malpJeFCNd9+1uN3BuO8mRn2b5+aNtrYEZ6YlSNjYNRu9IjqSxZ5Clhs5267950UV1ayhgFE8z3qre2eQ==} @@ -1364,11 +1364,11 @@ packages: '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -2961,8 +2961,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.18: - resolution: {integrity: sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==} + baseline-browser-mapping@2.10.19: + resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} engines: {node: '>=6.0.0'} hasBin: true @@ -3515,8 +3515,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.336: - resolution: {integrity: sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==} + electron-to-chromium@1.5.339: + resolution: {integrity: sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==} elegant-spinner@1.0.1: resolution: {integrity: sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==} @@ -4123,8 +4123,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} git-hooks-list@3.2.0: resolution: {integrity: sha512-ZHG9a1gEhUMX1TvGrLdyWb9kDopCBbTnI8z4JgRMYxsijWipgjSEYoPWqBuIB0DnRnvqlQSEeVmzpeuPm7NdFQ==} @@ -7354,7 +7354,7 @@ snapshots: '@contentstack/cli-utilities@2.0.0-beta.6(@types/node@14.18.63)': dependencies: '@contentstack/management': 1.29.2(debug@4.4.3) - '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) + '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 @@ -7389,7 +7389,7 @@ snapshots: '@contentstack/cli-utilities@2.0.0-beta.6(@types/node@14.18.63)(debug@4.4.3)': dependencies: '@contentstack/management': 1.29.2(debug@4.4.3) - '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) + '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 @@ -7424,7 +7424,7 @@ snapshots: '@contentstack/cli-utilities@2.0.0-beta.6(@types/node@18.19.130)': dependencies: '@contentstack/management': 1.29.2(debug@4.4.3) - '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) + '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 @@ -7459,7 +7459,7 @@ snapshots: '@contentstack/cli-utilities@2.0.0-beta.6(@types/node@20.19.39)': dependencies: '@contentstack/management': 1.29.2(debug@4.4.3) - '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) + '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 @@ -7494,7 +7494,7 @@ snapshots: '@contentstack/cli-utilities@2.0.0-beta.6(@types/node@22.19.17)': dependencies: '@contentstack/management': 1.29.2(debug@4.4.3) - '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) + '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 @@ -7541,7 +7541,7 @@ snapshots: transitivePeerDependencies: - debug - '@contentstack/marketplace-sdk@1.5.0(debug@4.4.3)': + '@contentstack/marketplace-sdk@1.5.1(debug@4.4.3)': dependencies: '@contentstack/utils': 1.9.1 axios: 1.15.0(debug@4.4.3) @@ -7562,13 +7562,13 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@emnapi/core@1.9.2': + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.2': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true @@ -8553,8 +8553,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -8656,7 +8656,7 @@ snapshots: '@oclif/plugin-warn-if-update-available@3.1.60': dependencies: - '@oclif/core': 4.9.0 + '@oclif/core': 4.10.5 ansis: 3.17.0 debug: 4.4.3(supports-color@8.1.1) http-call: 5.3.0 @@ -10050,7 +10050,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.18: {} + baseline-browser-mapping@2.10.19: {} big-json@3.2.0: dependencies: @@ -10089,9 +10089,9 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.18 + baseline-browser-mapping: 2.10.19 caniuse-lite: 1.0.30001788 - electron-to-chromium: 1.5.336 + electron-to-chromium: 1.5.339 node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -10648,7 +10648,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.336: {} + electron-to-chromium@1.5.339: {} elegant-spinner@1.0.1: {} @@ -10809,7 +10809,7 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1) eslint-plugin-perfectionist: 2.11.0(eslint@8.57.1)(typescript@5.9.3) @@ -10870,8 +10870,8 @@ snapshots: eslint-config-oclif: 5.2.2(eslint@8.57.1) eslint-config-xo: 0.49.0(eslint@8.57.1) eslint-config-xo-space: 0.35.0(eslint@8.57.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsdoc: 50.8.0(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 17.24.0(eslint@8.57.1)(typescript@5.9.3) @@ -10917,28 +10917,13 @@ snapshots: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 - get-tsconfig: 4.13.7 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.16 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3(supports-color@8.1.1) - eslint: 8.57.1 - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -10947,7 +10932,7 @@ snapshots: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.16 @@ -10979,14 +10964,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.2(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -11003,7 +10988,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11061,7 +11046,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11072,7 +11057,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11131,7 +11116,7 @@ snapshots: enhanced-resolve: 5.20.1 eslint: 8.57.1 eslint-plugin-es-x: 7.8.0(eslint@8.57.1) - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 @@ -11146,7 +11131,7 @@ snapshots: enhanced-resolve: 5.20.1 eslint: 8.57.1 eslint-plugin-es-x: 7.8.0(eslint@8.57.1) - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 @@ -11659,7 +11644,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.7: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -14266,7 +14251,7 @@ snapshots: tsx@4.21.0: dependencies: esbuild: 0.27.7 - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3