11import { join } from 'node:path' ;
22import values from 'lodash/values' ;
33import isEmpty from 'lodash/isEmpty' ;
4- import { log , handleAndLogError } from '@contentstack/cli-utilities' ;
4+ import { log , handleAndLogError , CLIProgressManager } from '@contentstack/cli-utilities' ;
55import { PATH_CONSTANTS } from '../../constants' ;
66
77import BaseClass , { ApiOptions } from './base-class' ;
@@ -19,6 +19,8 @@ export default class ImportTaxonomies extends BaseClass {
1919 private termsSuccessPath : string ;
2020 private termsFailsPath : string ;
2121 private localesFilePath : string ;
22+ private envUidMapperPath : string ;
23+ private envUidMapper : Record < string , string > = { } ;
2224 private isLocaleBasedStructure : boolean = false ;
2325 public createdTaxonomies : Record < string , unknown > = { } ;
2426 public failedTaxonomies : Record < string , unknown > = { } ;
@@ -46,8 +48,16 @@ export default class ImportTaxonomies extends BaseClass {
4648 importConfig . modules . locales . dirName ,
4749 importConfig . modules . locales . fileName ,
4850 ) ;
51+ this . envUidMapperPath = join (
52+ importConfig . backupDir ,
53+ PATH_CONSTANTS . MAPPER ,
54+ PATH_CONSTANTS . MAPPER_MODULES . ENVIRONMENTS ,
55+ PATH_CONSTANTS . FILES . UID_MAPPING ,
56+ ) ;
4957 }
5058
59+ // --- Lifecycle ---
60+
5161 /**
5262 * @method start
5363 * @returns {Promise<void> } Promise<void>
@@ -56,7 +66,7 @@ export default class ImportTaxonomies extends BaseClass {
5666 try {
5767 log . debug ( 'Starting taxonomies import process...' , this . importConfig . context ) ;
5868
59- const [ taxonomiesCount ] = await this . analyzeTaxonomies ( ) ;
69+ const [ taxonomiesCount , publishJobCount ] = await this . analyzeTaxonomies ( ) ;
6070 if ( taxonomiesCount === 0 ) {
6171 log . info ( 'No taxonomies found to import' , this . importConfig . context ) ;
6272 return ;
@@ -67,8 +77,12 @@ export default class ImportTaxonomies extends BaseClass {
6777 // Check if locale-based structure exists before import
6878 this . isLocaleBasedStructure = this . detectAndScanLocaleStructure ( ) ;
6979
70- const progress = this . createSimpleProgress ( this . currentModuleName , taxonomiesCount ) ;
71- progress . updateStatus ( PROCESS_STATUS [ PROCESS_NAMES . TAXONOMIES_IMPORT ] . IMPORTING ) ;
80+ const progress = this . createNestedProgress ( this . currentModuleName ) ;
81+ this . initializeTaxonomiesProgress ( progress , taxonomiesCount , publishJobCount ) ;
82+
83+ progress
84+ . startProcess ( PROCESS_NAMES . TAXONOMIES_IMPORT )
85+ . updateStatus ( PROCESS_STATUS [ PROCESS_NAMES . TAXONOMIES_IMPORT ] . IMPORTING , PROCESS_NAMES . TAXONOMIES_IMPORT ) ;
7286 log . debug ( 'Starting taxonomies import' , this . importConfig . context ) ;
7387
7488 if ( this . isLocaleBasedStructure ) {
@@ -79,6 +93,19 @@ export default class ImportTaxonomies extends BaseClass {
7993 await this . importTaxonomiesLegacy ( ) ;
8094 }
8195
96+ progress . completeProcess ( PROCESS_NAMES . TAXONOMIES_IMPORT , true ) ;
97+
98+ if ( publishJobCount > 0 ) {
99+ progress
100+ . startProcess ( PROCESS_NAMES . TAXONOMIES_PUBLISH )
101+ . updateStatus (
102+ PROCESS_STATUS [ PROCESS_NAMES . TAXONOMIES_PUBLISH ] . PUBLISHING ,
103+ PROCESS_NAMES . TAXONOMIES_PUBLISH ,
104+ ) ;
105+ await this . processTaxonomyPublishing ( ) ;
106+ progress . completeProcess ( PROCESS_NAMES . TAXONOMIES_PUBLISH , true ) ;
107+ }
108+
82109 this . createSuccessAndFailedFile ( ) ;
83110 this . completeProgressWithMessage ( ) ;
84111 } catch ( error ) {
@@ -87,6 +114,8 @@ export default class ImportTaxonomies extends BaseClass {
87114 }
88115 }
89116
117+ // --- Import ---
118+
90119 /**
91120 * create taxonomy and enter success & failure related data into taxonomies mapper file
92121 * @method importTaxonomies
@@ -344,6 +373,191 @@ export default class ImportTaxonomies extends BaseClass {
344373 return true ;
345374 }
346375
376+ // --- Progress ---
377+
378+ /**
379+ * Registers nested progress for taxonomy import and optional taxonomy publish when publish jobs exist.
380+ */
381+ initializeTaxonomiesProgress ( progress : CLIProgressManager , taxonomyCount : number , publishJobCount : number ) : void {
382+ progress . addProcess ( PROCESS_NAMES . TAXONOMIES_IMPORT , taxonomyCount ) ;
383+ if ( publishJobCount > 0 ) {
384+ progress . addProcess ( PROCESS_NAMES . TAXONOMIES_PUBLISH , publishJobCount ) ;
385+ }
386+ }
387+
388+ // --- Publish ---
389+
390+ /**
391+ * Reads source env UID → destination stack env UID map produced during environments import.
392+ */
393+ private readEnvUidMapperSync ( ) : Record < string , string > {
394+ if ( ! fileHelper . fileExistsSync ( this . envUidMapperPath ) ) {
395+ log . debug ( `Environment UID mapper not found at ${ this . envUidMapperPath } ` , this . importConfig . context ) ;
396+ return { } ;
397+ }
398+
399+ try {
400+ const raw = fsUtil . readFile ( this . envUidMapperPath , true ) as Record < string , unknown > ;
401+ const out : Record < string , string > = { } ;
402+ for ( const [ k , v ] of Object . entries ( raw || { } ) ) {
403+ if ( v !== undefined && v !== null && String ( v ) . trim ( ) !== '' ) {
404+ out [ k ] = String ( v ) ;
405+ }
406+ }
407+ return out ;
408+ } catch {
409+ log . debug ( 'Failed to read environment UID mapper' , this . importConfig . context ) ;
410+ return { } ;
411+ }
412+ }
413+
414+ private countPublishEligibleTaxonomies ( envMapper : Record < string , string > ) : number {
415+ let count = 0 ;
416+ for ( const key of Object . keys ( this . taxonomies || { } ) ) {
417+ const meta = this . taxonomies [ key ] as Record < string , any > ;
418+ const taxonomyUid = meta ?. uid || key ;
419+ const filePath = this . findTaxonomyFilePath ( taxonomyUid ) ;
420+ if ( ! filePath ) continue ;
421+
422+ const details = this . loadTaxonomyFile ( filePath ) ;
423+ const tax = details ?. taxonomy as Record < string , any > | undefined ;
424+ if ( ! tax ?. publish_details ?. length || ! tax ?. locale ) continue ;
425+
426+ const hasMapped = ( tax . publish_details as any [ ] ) . some (
427+ ( p : any ) => p ?. environment && envMapper [ String ( p . environment ) ] ,
428+ ) ;
429+ if ( hasMapped ) count ++ ;
430+ }
431+ return count ;
432+ }
433+
434+ private collectTaxonomyPublishJobs ( ) : Array < { taxonomy : Record < string , any > } > {
435+ const jobs : Array < { taxonomy : Record < string , any > } > = [ ] ;
436+ const seen = new Set < string > ( ) ;
437+
438+ for ( const key of Object . keys ( this . taxonomies || { } ) ) {
439+ const meta = this . taxonomies [ key ] as Record < string , any > ;
440+ const taxonomyUid = meta ?. uid || key ;
441+ if ( seen . has ( taxonomyUid ) ) continue ;
442+
443+ const filePath = this . findTaxonomyFilePath ( taxonomyUid ) ;
444+ if ( ! filePath ) continue ;
445+
446+ const details = this . loadTaxonomyFile ( filePath ) ;
447+ const tax = details ?. taxonomy as Record < string , any > | undefined ;
448+ if ( ! tax ?. publish_details ?. length || ! tax ?. locale ) continue ;
449+
450+ seen . add ( taxonomyUid ) ;
451+ jobs . push ( { taxonomy : tax } ) ;
452+ }
453+
454+ return jobs ;
455+ }
456+
457+ private loadEnvUidMapper ( ) : void {
458+ this . envUidMapper = this . readEnvUidMapperSync ( ) ;
459+ if ( isEmpty ( this . envUidMapper ) ) {
460+ log . warn (
461+ 'Environment UID mapper is empty; taxonomy publishing is skipped. Import environments first or ensure mapper/environments/uid-mapping.json exists.' ,
462+ this . importConfig . context ,
463+ ) ;
464+ }
465+ }
466+
467+ async processTaxonomyPublishing ( ) : Promise < void > {
468+ this . loadEnvUidMapper ( ) ;
469+ const jobs = this . collectTaxonomyPublishJobs ( ) ;
470+
471+ if ( jobs . length === 0 ) {
472+ log . debug ( 'No taxonomies with publish_details to publish' , this . importConfig . context ) ;
473+ return ;
474+ }
475+
476+ log . info ( 'Starting taxonomy publishing process' , this . importConfig . context ) ;
477+
478+ const onSuccess = ( { apiData } : any ) => {
479+ const taxonomyUid = apiData ?. items ?. [ 0 ] ?. uid ;
480+ this . progressManager ?. tick (
481+ true ,
482+ `taxonomy published: ${ taxonomyUid } ` ,
483+ null ,
484+ PROCESS_NAMES . TAXONOMIES_PUBLISH ,
485+ ) ;
486+ log . success ( `Published taxonomy '${ taxonomyUid } '` , this . importConfig . context ) ;
487+ } ;
488+
489+ const onReject = ( { error, apiData } : any ) => {
490+ const taxonomyUid = apiData ?. items ?. [ 0 ] ?. uid ;
491+ handleAndLogError (
492+ error ,
493+ { ...this . importConfig . context , taxonomyUid } ,
494+ `Failed to publish taxonomy '${ taxonomyUid } '` ,
495+ ) ;
496+ this . progressManager ?. tick (
497+ false ,
498+ `taxonomy publish: ${ taxonomyUid } ` ,
499+ ( error as Error ) ?. message || `Failed to publish taxonomy '${ taxonomyUid } '` ,
500+ PROCESS_NAMES . TAXONOMIES_PUBLISH ,
501+ ) ;
502+ } ;
503+
504+ await this . makeConcurrentCall (
505+ {
506+ apiContent : jobs as unknown as Record < string , any > [ ] ,
507+ processName : 'publish taxonomies' ,
508+ apiParams : {
509+ serializeData : this . serializePublishTaxonomies . bind ( this ) ,
510+ reject : onReject ,
511+ resolve : onSuccess ,
512+ entity : 'publish-taxonomies' ,
513+ includeParamOnCompletion : true ,
514+ } ,
515+ concurrencyLimit : this . importConfig . concurrency || this . importConfig . fetchConcurrency || 1 ,
516+ } ,
517+ undefined ,
518+ false ,
519+ ) ;
520+ }
521+
522+ /**
523+ * Builds taxonomy publish payload: destination env UIDs from mapper, locales from taxonomy.locale, items: [{ uid }].
524+ */
525+ serializePublishTaxonomies ( apiOptions : ApiOptions ) : ApiOptions {
526+ const job = apiOptions . apiData as { taxonomy ?: Record < string , any > } ;
527+ const taxonomy = job ?. taxonomy ;
528+
529+ if ( ! taxonomy ?. publish_details ?. length || ! taxonomy ?. locale ) {
530+ apiOptions . apiData = undefined ;
531+ return apiOptions ;
532+ }
533+
534+ const environments : string [ ] = [ ] ;
535+ for ( const pub of taxonomy . publish_details as any [ ] ) {
536+ const sourceEnvUid = pub ?. environment ;
537+ if ( ! sourceEnvUid ) continue ;
538+ const destUid = this . envUidMapper [ String ( sourceEnvUid ) ] ;
539+ if ( destUid && ! environments . includes ( destUid ) ) {
540+ environments . push ( destUid ) ;
541+ }
542+ }
543+
544+ if ( environments . length === 0 ) {
545+ apiOptions . apiData = undefined ;
546+ return apiOptions ;
547+ }
548+
549+ const locales = [ String ( taxonomy . locale ) ] ;
550+ apiOptions . apiData = {
551+ environments,
552+ locales,
553+ items : [ { uid : taxonomy . uid } ] ,
554+ } ;
555+
556+ return apiOptions ;
557+ }
558+
559+ // --- Mapper output ---
560+
347561 /**
348562 * create taxonomies success and fail in (mapper/taxonomies)
349563 * create terms success and fail in (mapper/taxonomies/terms)
@@ -396,25 +610,36 @@ export default class ImportTaxonomies extends BaseClass {
396610 }
397611 }
398612
399- private async analyzeTaxonomies ( ) : Promise < [ number ] > {
613+ // --- Analyze & prepare ---
614+
615+ private async analyzeTaxonomies ( ) : Promise < [ number , number ] > {
400616 return this . withLoadingSpinner ( 'TAXONOMIES: Analyzing import data...' , async ( ) => {
401617 log . debug ( 'Checking for taxonomies folder existence' , this . importConfig . context ) ;
402618
403- if ( fileHelper . fileExistsSync ( this . taxonomiesFolderPath ) ) {
404- log . debug ( `Found taxonomies folder: ${ this . taxonomiesFolderPath } ` , this . importConfig . context ) ;
405-
406- this . taxonomies = fsUtil . readFile ( join ( this . taxonomiesFolderPath , 'taxonomies.json' ) , true ) as Record <
407- string ,
408- unknown
409- > ;
410-
411- const taxonomyCount = Object . keys ( this . taxonomies || { } ) . length ;
412- log . debug ( `Loaded ${ taxonomyCount } taxonomy items from file` , this . importConfig . context ) ;
413- return [ taxonomyCount ] ;
414- } else {
619+ if ( ! fileHelper . fileExistsSync ( this . taxonomiesFolderPath ) ) {
415620 log . info ( `No Taxonomies Found! - '${ this . taxonomiesFolderPath } '` , this . importConfig . context ) ;
416- return [ 0 ] ;
621+ return [ 0 , 0 ] ;
417622 }
623+
624+ log . debug ( `Found taxonomies folder: ${ this . taxonomiesFolderPath } ` , this . importConfig . context ) ;
625+
626+ this . taxonomies = fsUtil . readFile ( join ( this . taxonomiesFolderPath , 'taxonomies.json' ) , true ) as Record <
627+ string ,
628+ unknown
629+ > ;
630+
631+ this . isLocaleBasedStructure = this . detectAndScanLocaleStructure ( ) ;
632+
633+ const taxonomyCount = Object . keys ( this . taxonomies || { } ) . length ;
634+ const envMapper = this . readEnvUidMapperSync ( ) ;
635+ const publishJobCount = this . countPublishEligibleTaxonomies ( envMapper ) ;
636+
637+ log . debug (
638+ `Loaded ${ taxonomyCount } taxonomy items; ${ publishJobCount } eligible for publish (mapped environments).` ,
639+ this . importConfig . context ,
640+ ) ;
641+
642+ return [ taxonomyCount , publishJobCount ] ;
418643 } ) ;
419644 }
420645
0 commit comments