Skip to content
19 changes: 12 additions & 7 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@

fileignoreconfig:
- filename: package-lock.json
checksum: fa2c81c72305bc19fa4f64457af1000aa2f0c179ecb7e46a5fc9d8b760c705a7
- filename: pnpm-lock.yaml
checksum: faaef9e003c5ba45e56ab1ca24a4fc285f53a2b9ecc6d3dbd11e7130a31db4a2
- filename: packages/contentstack-clone/src/core/util/clone-handler.ts
checksum: 0a7ed55e96aa5a94084538b4a480608593b04c316bf02cf54cdd3a647e9b714c
- filename: package-lock.json
checksum: d42b12a21c38a527fb850cbe8500160120ccb58e57aa0b4f8098b835e7ca0b4d
- filename: pnpm-lock.yaml
checksum: c442a57252ba95771003f170786afed30888f14f67dc59766352be4a57d5ad55
- filename: packages/contentstack-clone/src/core/util/clone-handler.ts
checksum: 0a7ed55e96aa5a94084538b4a480608593b04c316bf02cf54cdd3a647e9b714c
- filename: packages/contentstack-config/src/interfaces/index.ts
checksum: 0c8e8a6e478151f6f6a1c50a5ce66c64e93d5d1d1bcd22344a9496cdc19f63b8
- filename: packages/contentstack-config/src/utils/region-handler.ts
checksum: 0ce1a0f3e4228f5429661a32611fda32f04da038337b74bad93bc717406481f4
- filename: packages/contentstack/README.md
checksum: 2f6da4ed14592fcb5c5fb8bc70bf57dd9fed512ed580b3edfad23055d8f05f38
version: '1.0'
7 changes: 3 additions & 4 deletions packages/contentstack-audit/src/audit-base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid';
import isEmpty from 'lodash/isEmpty';
import { join, resolve } from 'path';
import cloneDeep from 'lodash/cloneDeep';
import { cliux, sanitizePath, TableFlags, TableHeader, log, configHandler, CLIProgressManager, clearProgressModuleSetting } from '@contentstack/cli-utilities';
import { cliux, sanitizePath, TableFlags, TableHeader, log, configHandler, CLIProgressManager, clearProgressModuleSetting, readContentTypeSchemas } from '@contentstack/cli-utilities';
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
import config from './config';
import { print } from './util/log';
Expand Down Expand Up @@ -480,10 +480,9 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
* `gfSchema`. The values of these properties are the parsed JSON data from two different files.
*/
getCtAndGfSchema() {
const ctPath = join(
const ctDirPath = join(
this.sharedConfig.basePath,
this.sharedConfig.moduleConfig['content-types'].dirName,
this.sharedConfig.moduleConfig['content-types'].fileName,
);
const gfPath = join(
this.sharedConfig.basePath,
Expand All @@ -492,7 +491,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
);

const gfSchema = existsSync(gfPath) ? (JSON.parse(readFileSync(gfPath, 'utf8')) as ContentTypeStruct[]) : [];
const ctSchema = existsSync(ctPath) ? (JSON.parse(readFileSync(ctPath, 'utf8')) as ContentTypeStruct[]) : [];
const ctSchema = (readContentTypeSchemas(ctDirPath) || []) as ContentTypeStruct[];

return { ctSchema, gfSchema };
}
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack-audit/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const config = {
moduleConfig: {
'content-types': {
name: 'content type',
fileName: 'schema.json',
fileName: 'schema.json', // Not used - reads from individual files
dirName: 'content_types',
},
'global-fields': {
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack-bootstrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ $ npm install -g @contentstack/cli-cm-bootstrap
$ csdx COMMAND
running command...
$ csdx (--version)
@contentstack/cli-cm-bootstrap/2.0.0-beta.6 darwin-arm64 node-v24.12.0
@contentstack/cli-cm-bootstrap/2.0.0-beta.6 darwin-arm64 node-v23.11.0
$ csdx --help [COMMAND]
USAGE
$ csdx COMMAND
Expand Down
1,797 changes: 1,797 additions & 0 deletions packages/contentstack-bulk-publish/README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/contentstack-export/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ $ npm install -g @contentstack/cli-cm-export
$ csdx COMMAND
running command...
$ csdx (--version)
@contentstack/cli-cm-export/2.0.0-beta.7 darwin-arm64 node-v24.12.0
@contentstack/cli-cm-export/2.0.0-beta.7 darwin-arm64 node-v23.11.0
$ csdx --help [COMMAND]
USAGE
$ csdx COMMAND
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,5 @@ export default class ContentTypesExport extends BaseClass {
await executeTask(contentTypes, writeWithProgress.bind(this), {
concurrency: this.exportConfig.writeConcurrency,
});

const schemaFilePath = path.join(this.contentTypesDirPath, 'schema.json');
log.debug(`Writing aggregate schema to: ${schemaFilePath}`, this.exportConfig.context);

return fsUtil.writeFile(schemaFilePath, contentTypes);
}
}
11 changes: 5 additions & 6 deletions packages/contentstack-export/src/export/modules/entries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from 'path';
import { ContentstackClient, FsUtility, handleAndLogError, messageHandler, log } from '@contentstack/cli-utilities';
import { ContentstackClient, FsUtility, handleAndLogError, messageHandler, log, readContentTypeSchemas } from '@contentstack/cli-utilities';
import { Export, ExportProjects } from '@contentstack/cli-variants';
import { sanitizePath } from '@contentstack/cli-utilities';

Expand Down Expand Up @@ -30,7 +30,7 @@ export default class EntriesExport extends BaseClass {
private variantEntries!: any;
private entriesDirPath: string;
private localesFilePath: string;
private schemaFilePath: string;
private contentTypesDirPath: string;
private entriesFileHelper: FsUtility;
private projectInstance: ExportProjects;
public exportVariantEntry: boolean = false;
Expand All @@ -51,11 +51,10 @@ export default class EntriesExport extends BaseClass {
sanitizePath(exportConfig.modules.locales.dirName),
sanitizePath(exportConfig.modules.locales.fileName),
);
this.schemaFilePath = path.resolve(
this.contentTypesDirPath = path.resolve(
sanitizePath(exportConfig.exportDir),
sanitizePath(exportConfig.branchName || ''),
sanitizePath(exportConfig.modules.content_types.dirName),
'schema.json',
);
this.projectInstance = new ExportProjects(this.exportConfig);
this.exportConfig.context.module = MODULE_CONTEXTS.ENTRIES;
Expand All @@ -70,7 +69,7 @@ export default class EntriesExport extends BaseClass {
const [locales, contentTypes, entryRequestOptions, totalEntriesCount, variantInfo] =
await this.withLoadingSpinner('ENTRIES: Analyzing content structure and entries...', async () => {
const locales = fsUtil.readFile(this.localesFilePath) as Array<Record<string, unknown>>;
const contentTypes = fsUtil.readFile(this.schemaFilePath) as Array<Record<string, unknown>>;
const contentTypes = readContentTypeSchemas(this.contentTypesDirPath);

if (!Array.isArray(locales) || locales?.length === 0) {
log.debug(`No locales found in ${this.localesFilePath}`, this.exportConfig.context);
Expand All @@ -83,7 +82,7 @@ export default class EntriesExport extends BaseClass {
return [locales, contentTypes, [], 0, null];
}
log.debug(
`Loaded ${contentTypes?.length} content types from ${this.schemaFilePath}`,
`Loaded ${contentTypes?.length} content types from individual files in ${this.contentTypesDirPath}`,
this.exportConfig.context,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ describe('ExportContentTypes', () => {
// Stub FsUtility methods
sinon.stub(FsUtility.prototype, 'writeFile').resolves();
sinon.stub(FsUtility.prototype, 'makeDirectory').resolves();
// Stub FsUtility.prototype.readdir and readFile for readContentTypeSchemas support
sinon.stub(FsUtility.prototype, 'readdir').returns([]);
sinon.stub(FsUtility.prototype, 'readFile').returns(undefined);
});

afterEach(() => {
Expand Down Expand Up @@ -314,6 +317,7 @@ describe('ExportContentTypes', () => {

it('should handle empty content types', async () => {
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
const completeProgressStub = sinon.stub(exportContentTypes as any, 'completeProgress');

mockStackClient.contentType.returns({
query: sinon.stub().returns({
Expand All @@ -327,8 +331,10 @@ describe('ExportContentTypes', () => {
exportContentTypes.contentTypes = [];
await exportContentTypes.start();

// Verify writeFile was called even with empty array
expect(writeFileStub.called).to.be.true;
// With empty content types, writeFile is not called (no files to write)
// But completeProgress should be called to mark the process as complete
expect(completeProgressStub.called).to.be.true;
expect(completeProgressStub.calledWith(true)).to.be.true;
});

it('should handle errors during export without throwing', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ describe('EntriesExport', () => {
createFolderStub.callsFake(() => {
// Do nothing - prevent actual directory creation
});

// Stub FsUtility.prototype.readdir and readFile for readContentTypeSchemas support
// readContentTypeSchemas creates its own FsUtility instance, so we need to stub the prototype
sandbox.stub(FsUtility.prototype, 'readdir').returns([]);
sandbox.stub(FsUtility.prototype, 'readFile').returns(undefined);

entriesExport = new EntriesExport({
exportConfig: mockExportConfig,
Expand Down Expand Up @@ -173,16 +178,15 @@ describe('EntriesExport', () => {
mockExportConfig.modules.locales.dirName,
mockExportConfig.modules.locales.fileName,
);
const expectedSchemaPath = path.resolve(
const expectedContentTypesDirPath = path.resolve(
mockExportConfig.exportDir,
mockExportConfig.branchName || '',
mockExportConfig.modules.content_types.dirName,
'schema.json',
);

expect(entriesExport.entriesDirPath).to.equal(expectedEntriesPath);
expect(entriesExport.localesFilePath).to.equal(expectedLocalesPath);
expect(entriesExport.schemaFilePath).to.equal(expectedSchemaPath);
expect(entriesExport.contentTypesDirPath).to.equal(expectedContentTypesDirPath);
});

it('should initialize ExportProjects instance', () => {
Expand All @@ -197,27 +201,29 @@ describe('EntriesExport', () => {

describe('start() method - Early Returns', () => {
it('should return early when no content types are found', async () => {
mockFsUtil.readFile
.onFirstCall()
.returns([{ code: 'en-us' }]) // locales
.onSecondCall()
.returns([]); // content types
// Stub mockFsUtil.readFile for locales
mockFsUtil.readFile.returns([{ code: 'en-us' }]);

// Stub FsUtility.prototype for readContentTypeSchemas to return empty
(FsUtility.prototype.readdir as sinon.SinonStub).returns([]); // No content type files

await entriesExport.start();

// Should not attempt to fetch entries
expect(mockStackAPIClient.contentType.called).to.be.false;
// Should read both locales and content types files
expect(mockFsUtil.readFile.calledTwice).to.be.true;
// Should read locales file
expect(mockFsUtil.readFile.called).to.be.true;
});

it('should handle empty locales array gracefully', async () => {
const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }];
mockFsUtil.readFile
.onFirstCall()
.returns([]) // empty locales
.onSecondCall()
.returns(contentTypes);
const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1', schema: [] as any }];

// Stub mockFsUtil.readFile for locales
mockFsUtil.readFile.returns([]); // empty locales

// Stub FsUtility.prototype for readContentTypeSchemas to return content types
(FsUtility.prototype.readdir as sinon.SinonStub).returns(['ct-1.json']);
(FsUtility.prototype.readFile as sinon.SinonStub).returns(contentTypes[0]);

await entriesExport.start();

Expand All @@ -226,14 +232,14 @@ describe('EntriesExport', () => {
});

it('should handle non-array locales gracefully', async () => {
const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }];
// Use empty array instead of null to avoid Object.keys error
// The code checks !Array.isArray first, so empty array will work
mockFsUtil.readFile
.onFirstCall()
.returns([]) // empty locales array
.onSecondCall()
.returns(contentTypes);
const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1', schema: [] as any }];

// Stub mockFsUtil.readFile for locales
mockFsUtil.readFile.returns([]); // empty locales array

// Stub FsUtility.prototype for readContentTypeSchemas to return content types
(FsUtility.prototype.readdir as sinon.SinonStub).returns(['ct-1.json']);
(FsUtility.prototype.readFile as sinon.SinonStub).returns(contentTypes[0]);

// Mock entry query for when entries are processed
const mockEntryQuery = {
Expand Down Expand Up @@ -1055,11 +1061,19 @@ describe('EntriesExport', () => {
it('should process all request objects and complete file writing', async () => {
const locales = [{ code: 'en-us' }];
const contentTypes = [
{ uid: 'ct-1', title: 'Content Type 1' },
{ uid: 'ct-2', title: 'Content Type 2' },
{ uid: 'ct-1', title: 'Content Type 1', schema: [] as any },
{ uid: 'ct-2', title: 'Content Type 2', schema: [] as any },
];

mockFsUtil.readFile.onFirstCall().returns(locales).onSecondCall().returns(contentTypes);
mockFsUtil.readFile.returns(locales); // For locales file

// Stub FsUtility.prototype for readContentTypeSchemas
(FsUtility.prototype.readdir as sinon.SinonStub).returns(['ct-1.json', 'ct-2.json']);
(FsUtility.prototype.readFile as sinon.SinonStub).callsFake((filePath: string) => {
if (filePath.includes('ct-1.json')) return contentTypes[0];
if (filePath.includes('ct-2.json')) return contentTypes[1];
return undefined;
});

const mockEntryQuery = {
query: sandbox.stub().returns({
Expand Down Expand Up @@ -1098,9 +1112,13 @@ describe('EntriesExport', () => {

it('should handle errors during entry processing gracefully', async () => {
const locales = [{ code: 'en-us' }];
const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }];
const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1', schema: [] as any }];

mockFsUtil.readFile.onFirstCall().returns(locales).onSecondCall().returns(contentTypes);
mockFsUtil.readFile.returns(locales); // For locales file

// Stub FsUtility.prototype for readContentTypeSchemas
(FsUtility.prototype.readdir as sinon.SinonStub).returns(['ct-1.json']);
(FsUtility.prototype.readFile as sinon.SinonStub).returns(contentTypes[0]);

const processingError = new Error('Entry processing failed');
const getEntriesStub = sandbox.stub(entriesExport, 'getEntries').rejects(processingError);
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack-import-setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ $ npm install -g @contentstack/cli-cm-import-setup
$ csdx COMMAND
running command...
$ csdx (--version)
@contentstack/cli-cm-import-setup/2.0.0-beta.3 darwin-arm64 node-v24.12.0
@contentstack/cli-cm-import-setup/2.0.0-beta.3 darwin-arm64 node-v23.11.0
$ csdx --help [COMMAND]
USAGE
$ csdx COMMAND
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack-import/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ $ npm install -g @contentstack/cli-cm-import
$ csdx COMMAND
running command...
$ csdx (--version)
@contentstack/cli-cm-import/2.0.0-beta.6 darwin-arm64 node-v24.12.0
@contentstack/cli-cm-import/2.0.0-beta.6 darwin-arm64 node-v23.11.0
$ csdx --help [COMMAND]
USAGE
$ csdx COMMAND
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import * as path from 'path';
import { find, cloneDeep, map } from 'lodash';
import { sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities';
import { sanitizePath, log, handleAndLogError, readContentTypeSchemas } from '@contentstack/cli-utilities';
import { ImportConfig, ModuleClassParams } from '../../types';
import BaseClass, { ApiOptions } from './base-class';
import { updateFieldRules } from '../../utils/content-type-helper';
Expand Down Expand Up @@ -474,7 +474,7 @@ export default class ContentTypesImport extends BaseClass {
const [cts, gfs, pendingGfs, pendingExt] = await this.withLoadingSpinner(
'CONTENT TYPES: Analyzing import data...',
async () => {
const cts = fsUtil.readFile(path.join(this.cTsFolderPath, 'schema.json'));
const cts = readContentTypeSchemas(this.cTsFolderPath);
const gfs = fsUtil.readFile(path.resolve(this.gFsFolderPath, this.gFsConfig.fileName));
const pendingGfs = fsUtil.readFile(this.gFsPendingPath);
const pendingExt = fsUtil.readFile(this.extPendingPath);
Expand Down
17 changes: 11 additions & 6 deletions packages/contentstack-import/src/import/modules/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import * as path from 'path';
import { writeFileSync } from 'fs';
import { isEmpty, values, cloneDeep, find, indexOf, forEach, remove } from 'lodash';
import { FsUtility, sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities';
import { FsUtility, sanitizePath, log, handleAndLogError, readContentTypeSchemas } from '@contentstack/cli-utilities';
import {
fsUtil,
lookupExtension,
Expand Down Expand Up @@ -230,7 +230,8 @@ export default class EntriesImport extends BaseClass {
return this.withLoadingSpinner('ENTRIES: Analyzing import data...', async () => {
log.debug('Loading content types for entry analysis', this.importConfig.context);

this.cTs = fsUtil.readFile(path.join(this.cTsPath, 'schema.json')) as Record<string, unknown>[];
this.cTs = readContentTypeSchemas(this.cTsPath);

if (!this.cTs || isEmpty(this.cTs)) {
return [0, 0, 0, 0, 0];
}
Expand Down Expand Up @@ -1201,15 +1202,19 @@ export default class EntriesImport extends BaseClass {
log.debug(`Found ${cTsWithFieldRules.length} content types with field rules to update`, this.importConfig.context);

try {
// Read content types from individual files
const cTs = readContentTypeSchemas(this.cTsPath) || [];
for (let cTUid of cTsWithFieldRules) {
log.debug(`Processing field rules for content type: ${cTUid}`, this.importConfig.context);

const cTs: Record<string, any>[] = fsUtil.readFile(path.join(this.cTsPath, 'schema.json')) as Record<
string,
unknown
>[];

const contentType: any = find(cTs, { uid: cTUid });

if (!contentType) {
log.debug(`Content type ${cTUid} not found in schemas`, this.importConfig.context);
continue;
}

if (contentType.field_rules) {
log.debug(
`Found ${contentType.field_rules.length} field rules for content type: ${cTUid}`,
Expand Down
Loading
Loading