Skip to content

Commit 07e0d47

Browse files
authored
Merge branch 'v2-dev' into fix/DX-9739
2 parents a65f5b7 + 45c1904 commit 07e0d47

15 files changed

Lines changed: 165 additions & 183 deletions

File tree

‎.talismanrc‎

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
fileignoreconfig:
22
- filename: pnpm-lock.yaml
3-
checksum: a90a8f0dbfa98da53ecb9e63f021ae8a1ac44c03cfe5dccd52a539c948d1625a
3+
checksum: 8566e316dd7026214e8863568c72dcf946ba10648ac5c076a7c55215b5ab3422
44
- filename: packages/contentstack-export/src/utils/export-config-handler.ts
55
checksum: 3ff8e8ea60f92311f8224bce457f7aaab026194de61e13eebdc6ca5a39f66bc9
66
- filename: packages/contentstack-asset-management/src/utils/export-helpers.ts
@@ -9,16 +9,10 @@ fileignoreconfig:
99
checksum: bc4a53f96be6a10786e00133245c7bdc43c965c8a98b753e3879e1110cf9c601
1010
- filename: packages/contentstack-asset-management/test/unit/utils/cs-assets-api-adapter.test.ts
1111
checksum: 63c6bff4d51842d8fa3cce88545259d0a2c3cfe71df95d303d993f692cee883b
12-
- filename: packages/contentstack-audit/src/modules/assets.ts
13-
checksum: 3a2f3d338ab7d417ad24fbf3d8f25e51e3098a440857ebbc2156e72e208a284e
14-
- filename: packages/contentstack-audit/test/unit/modules/assets.test.ts
15-
checksum: ee776f2d485c89ecf67aec9e61e37350e5754988bfca4574bbaef3c732f014b1
16-
- filename: packages/contentstack-audit/test/unit/mock/am-contents/environments/environments.json
17-
checksum: ffaaee9269a6e833cd3dbe337ddc5c060cce948519873c7a2777754519a31b52
18-
- filename: packages/contentstack-audit/test/unit/mock/am-contents/stack/stack.json
19-
checksum: c4ac3ff80eb38b1c58eb0db21a232bd84f856d4647093542296199036945083c
20-
- filename: packages/contentstack-audit/test/unit/mock/am-contents/spaces/space_one/assets/chunk0-assets.json
21-
checksum: 9fc8b266ef58ecce8d3c4182632d1f7d088dfbce81b3fcea46b087cde3129b91
22-
- filename: packages/contentstack-audit/test/unit/mock/am-contents/spaces/space_clean/assets/chunk0-assets.json
23-
checksum: f02886c04ea1097ebc5965fa9a63a878adb9437d56a5b1797be1967fae35f9e4
12+
- filename: packages/contentstack-migration/src/commands/cm/stacks/migration.ts
13+
checksum: 529431473623cf6bbefb35bc898be2a60dcb453e0c9273ecb97923de658769b3
14+
- filename: packages/contentstack-seed/src/commands/cm/stacks/seed.ts
15+
checksum: 9a3fd45a57c6ebf63d6c154657f2ebf799befbbacb9a255944c2d89ae89b22ed
16+
- filename: packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts
17+
checksum: b54f572a614e5d80f7fa27a51e679147cb537fd76ac922eed981d1156a261e50
2418
version: '1.0'

‎packages/contentstack-audit/src/commands/cm/stacks/audit/fix.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ export default class AuditFix extends AuditBaseCommand {
2424
'report-path': Flags.string({
2525
description: auditMsg.REPORT_PATH,
2626
}),
27-
'reference-only': Flags.boolean({
28-
hidden: true,
29-
description: auditMsg.REFERENCE_ONLY,
30-
}),
3127
modules: Flags.string({
3228
multiple: true,
3329
options: config.modules,

‎packages/contentstack-audit/src/commands/cm/stacks/audit/index.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ export default class Audit extends AuditBaseCommand {
2020
'report-path': Flags.string({
2121
description: auditMsg.REPORT_PATH,
2222
}),
23-
'reference-only': Flags.boolean({
24-
hidden: true,
25-
description: auditMsg.REFERENCE_ONLY,
26-
}),
2723
modules: Flags.string({
2824
multiple: true,
2925
options: config.modules,

‎packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts‎

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import Bootstrap, { BootstrapOptions, SeedParams } from '../../bootstrap';
44
import {
55
inquireCloneDirectory,
66
inquireApp,
7-
inquireAppType,
87
inquireLivePreviewSupport,
98
inquireRunDevServer,
109
} from '../../bootstrap/interactive';
@@ -40,12 +39,6 @@ export default class BootstrapCommand extends Command {
4039
multiple: false,
4140
required: false,
4241
}),
43-
'app-type': flags.string({
44-
description: 'Sample or Starter app',
45-
multiple: false,
46-
required: false,
47-
hidden: true,
48-
}),
4942
'stack-api-key': flags.string({
5043
char: 'k',
5144
description: 'Provide stack API key to seed content',
@@ -98,23 +91,13 @@ export default class BootstrapCommand extends Command {
9891
});
9992

10093
// inquire user inputs
101-
let appType =
102-
(bootstrapCommandFlags.appType as string) || (bootstrapCommandFlags['app-type'] as string) || 'starterapp';
103-
if (!appType) {
104-
appType = await inquireAppType();
105-
}
94+
const appType = 'starterapp';
10695

10796
const selectedAppName =
10897
(bootstrapCommandFlags.appName as string) || (bootstrapCommandFlags['app-name'] as string);
10998
let selectedApp;
11099
if (!selectedAppName) {
111-
if (appType === 'sampleapp') {
112-
selectedApp = await inquireApp(config.sampleApps);
113-
} else if (appType === 'starterapp') {
114-
selectedApp = await inquireApp(config.starterApps);
115-
} else {
116-
this.error('Invalid app type provided: ' + appType, { exit: 1 });
117-
}
100+
selectedApp = await inquireApp(config.starterApps);
118101
}
119102

120103
if (!selectedAppName && !selectedApp) {

‎packages/contentstack-bootstrap/test/bootstrap-integration.test.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ describe('Bootstrap Integration Tests', () => {
2626
expect(BootstrapCommand.flags).to.have.property('stack-name');
2727
expect(BootstrapCommand.flags).to.have.property('yes');
2828
expect(BootstrapCommand.flags).to.have.property('alias');
29-
expect(BootstrapCommand.flags).to.have.property('app-type');
3029
});
3130

3231
it('should validate alias flag properties', () => {

‎packages/contentstack-bootstrap/test/bootstrap.test.js‎

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('Bootstrapping an app', () => {
152152
'app-name': 'kickstart-next',
153153
org: 'org-uid',
154154
'stack-name': 'test-bootstrap-cmd',
155-
'app-type': 'kickstart-next',
155+
156156
yes: true,
157157
};
158158
return Promise.resolve(responses[question.name] || {});
@@ -171,16 +171,6 @@ describe('Bootstrapping an app', () => {
171171
delete process.env.CONTENTSTACK_AUTH_TOKEN;
172172
});
173173

174-
it('should handle invalid app type gracefully', async () => {
175-
try {
176-
await runCommand(['cm:bootstrap', '--app-type', 'invalidtype']);
177-
} catch (error) {
178-
expect(error).to.exist;
179-
expect(error?.oclif?.exit).to.equal(1);
180-
expect(error.message).to.contain('Invalid app type provided invalidtype');
181-
}
182-
});
183-
184174
it('should bootstrap a Contentstack app with the correct flags', async () => {
185175
// Mock the BootstrapCommand class
186176
const MockBootstrapCommand = class extends Command {
@@ -351,7 +341,7 @@ describe('Bootstrapping an app', () => {
351341
flags: {
352342
alias: testAlias,
353343
'app-name': undefined,
354-
'app-type': undefined,
344+
355345
'project-dir': undefined,
356346
'stack-api-key': undefined,
357347
org: undefined,
@@ -428,7 +418,7 @@ describe('Bootstrapping an app', () => {
428418
flags: {
429419
alias: undefined,
430420
'app-name': undefined,
431-
'app-type': undefined,
421+
432422
'project-dir': undefined,
433423
'stack-api-key': undefined,
434424
org: undefined,
@@ -485,7 +475,7 @@ describe('Bootstrapping an app', () => {
485475
flags: {
486476
alias: undefined,
487477
'app-name': undefined,
488-
'app-type': undefined,
478+
489479
'project-dir': undefined,
490480
'stack-api-key': undefined,
491481
org: undefined,
@@ -542,7 +532,7 @@ describe('Bootstrapping an app', () => {
542532
flags: {
543533
alias: undefined,
544534
'app-name': undefined,
545-
'app-type': undefined,
535+
546536
'project-dir': undefined,
547537
'stack-api-key': undefined,
548538
org: undefined,
@@ -598,7 +588,6 @@ describe('Bootstrapping an app', () => {
598588
flags: {
599589
alias: undefined,
600590
'app-name': undefined,
601-
'app-type': 'sampleapp',
602591
'project-dir': undefined,
603592
'stack-api-key': undefined,
604593
org: undefined,
@@ -613,11 +602,11 @@ describe('Bootstrapping an app', () => {
613602
command.cmaHost = mock.region.cma;
614603

615604
await command.run();
616-
// Verify that appType is set correctly
605+
// Verify that appType is hardcoded to 'starterapp'
617606
expect(bootstrapOptions).to.not.be.null;
618-
expect(bootstrapOptions.appType).to.equal('sampleapp');
619-
// Verify that inquireApp was called with sampleApps (config.default in compiled CJS)
620-
expect(interactive.inquireApp.calledWith(config.default.sampleApps)).to.be.true;
607+
expect(bootstrapOptions.appType).to.equal('starterapp');
608+
// Verify that inquireApp was called with starterApps
609+
expect(interactive.inquireApp.calledWith(config.default.starterApps)).to.be.true;
621610
});
622611

623612
it('should handle app-name flag correctly', async () => {
@@ -655,7 +644,7 @@ describe('Bootstrapping an app', () => {
655644
flags: {
656645
alias: undefined,
657646
'app-name': 'kickstart-next',
658-
'app-type': undefined,
647+
659648
'project-dir': undefined,
660649
'stack-api-key': undefined,
661650
org: undefined,

‎packages/contentstack-import/src/import/modules/assets.ts‎

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,48 @@ export default class ImportAssets extends BaseClass {
273273
);
274274
}
275275

276+
/**
277+
* Groups already-filtered `publish_details` into publish payloads that preserve the source
278+
* env↔locale pairing. Entries are grouped by environment, then environments sharing an
279+
* identical locale set are coalesced into one payload. Each returned group is a rectangle
280+
* (its environments × its locales), so the CMA env×locale cross-product reproduces exactly
281+
* the source pairs — never a phantom combination (the DX-9772 over-publish).
282+
*
283+
* A fully-rectangular input (every env published to the same locales) collapses to a single
284+
* group, so behavior is unchanged for the common case; a ragged input fans out into one group
285+
* per distinct locale set. Only environments present in `this.environments` are kept, so the
286+
* env-name lookup is always safe — this preserves the DX-1656 invalid-environment guard.
287+
* Callers apply any further scoping (e.g. AM's `api_key`) before calling.
288+
*/
289+
private buildPublishGroups(
290+
publishDetails: Record<string, any>[],
291+
): { environments: string[]; locales: string[] }[] {
292+
const localesByEnv = new Map<string, Set<string>>();
293+
for (const { environment, locale } of publishDetails || []) {
294+
if (!locale || !this.environments?.hasOwnProperty(environment)) continue;
295+
let set = localesByEnv.get(environment);
296+
if (!set) {
297+
set = new Set<string>();
298+
localesByEnv.set(environment, set);
299+
}
300+
set.add(locale);
301+
}
302+
303+
// Coalesce environments with an identical locale set into a single payload.
304+
const groups = new Map<string, { environments: string[]; locales: string[] }>();
305+
for (const [envUid, localeSet] of localesByEnv) {
306+
const locales = [...localeSet].sort();
307+
const signature = locales.join(' ');
308+
const existing = groups.get(signature);
309+
if (existing) {
310+
existing.environments.push(this.environments[envUid].name);
311+
} else {
312+
groups.set(signature, { environments: [this.environments[envUid].name], locales });
313+
}
314+
}
315+
return [...groups.values()];
316+
}
317+
276318
/**
277319
* Publishes imported AM (Contentstack Assets / spaces) assets, mirroring the legacy `publish()`
278320
* but re-pointed at each space's chunk store under `spaces/{oldSpaceUid}/assets`.
@@ -330,9 +372,13 @@ export default class ImportAssets extends BaseClass {
330372
const fsUtil = new FsUtility({ basePath: assetsDir, indexFileName: assetsFileName });
331373
for (const _ of values(fsUtil.indexFileContent)) {
332374
const chunkData = await fsUtil.readChunkFiles.next().catch(() => ({}));
333-
publishableCount += filter(values(chunkData as Record<string, any>[]), (asset) =>
334-
this.isAmAssetPublishable(asset, sourceStack),
335-
).length;
375+
for (const asset of values(chunkData as Record<string, any>[])) {
376+
if (!this.isAmAssetPublishable(asset, sourceStack)) continue;
377+
// Count publish calls (one per env-locale-set group), not assets, so ticks stay 1:1.
378+
publishableCount += this.buildPublishGroups(
379+
filter(asset.publish_details, (pd: any) => pd?.api_key === sourceStack),
380+
).length;
381+
}
336382
}
337383
}
338384

@@ -362,37 +408,15 @@ export default class ImportAssets extends BaseClass {
362408
handleAndLogError(error, { ...this.importConfig.context, uid, title });
363409
};
364410

411+
// apiData is a pre-expanded sub-item ({ uid, title, publishDetails }); one per env-locale-set
412+
// group (see Pass 2). Pairing is already preserved, so this only resolves the destination UID.
365413
const serializeData = (apiOptions: ApiOptions) => {
366-
const { apiData: asset } = apiOptions;
367-
const publishDetails = filter(
368-
asset.publish_details,
369-
(pd: any) => pd?.api_key === sourceStack && this.environments?.hasOwnProperty(pd?.environment),
370-
);
371-
372-
if (!publishDetails.length) {
373-
apiOptions.entity = undefined;
374-
return apiOptions;
375-
}
376-
377-
const environments = uniq(map(publishDetails, ({ environment }) => this.environments[environment].name));
378-
const locales = uniq(map(publishDetails, 'locale'));
379-
380-
if (environments.length === 0 || locales.length === 0) {
381-
log.debug(`Skipping publish for asset ${asset.uid} - no valid environments/locales`, this.importConfig.context);
382-
apiOptions.entity = undefined;
383-
return apiOptions;
384-
}
385-
386-
asset.locales = locales;
387-
asset.environments = environments;
388-
apiOptions.apiData.publishDetails = { locales, environments };
389-
390-
apiOptions.uid = this.assetsUidMap[asset.uid] as string;
414+
const { apiData } = apiOptions;
415+
apiOptions.uid = this.assetsUidMap[apiData.uid] as string;
391416
if (!apiOptions.uid) {
392-
log.debug(`Skipping publish for asset ${asset.uid} - no UID mapping found`, this.importConfig.context);
417+
log.debug(`Skipping publish for asset ${apiData.uid} - no UID mapping found`, this.importConfig.context);
393418
apiOptions.entity = undefined;
394419
}
395-
396420
return apiOptions;
397421
};
398422

@@ -404,10 +428,15 @@ export default class ImportAssets extends BaseClass {
404428
const indexerCount = values(indexer).length;
405429

406430
for (const index in indexer) {
407-
const apiContent = filter(values(await fsUtil.readChunkFiles.next()), (asset) =>
408-
this.isAmAssetPublishable(asset, sourceStack),
409-
);
410-
log.debug(`Found ${apiContent.length} publishable CS Assets in chunk ${index}`, this.importConfig.context);
431+
// Expand each publishable asset into one sub-item per env-locale-set group, so each
432+
// makeConcurrentCall item is a single-rectangle publish (preserves env↔locale pairing).
433+
const apiContent = values(await fsUtil.readChunkFiles.next()).flatMap((asset: Record<string, any>) => {
434+
if (!this.isAmAssetPublishable(asset, sourceStack)) return [];
435+
return this.buildPublishGroups(
436+
filter(asset.publish_details, (pd: any) => pd?.api_key === sourceStack),
437+
).map((publishDetails) => ({ uid: asset.uid, title: asset.title, publishDetails }));
438+
});
439+
log.debug(`Found ${apiContent.length} CS Asset publish calls in chunk ${index}`, this.importConfig.context);
411440

412441
await this.makeConcurrentCall({
413442
apiContent,

‎packages/contentstack-import/test/unit/import/modules/assets.test.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,67 @@ describe('ImportAssets', () => {
770770
});
771771
});
772772

773+
describe('buildPublishGroups() method', () => {
774+
it('collapses a RECTANGULAR asset to a single publish call (behavior-preserving)', () => {
775+
importAssets['environments'] = {
776+
'e1': { name: 'production' },
777+
'e2': { name: 'preview' },
778+
'e3': { name: 'development' },
779+
};
780+
const pd = ['e1', 'e2', 'e3'].flatMap((environment) => [
781+
{ environment, locale: 'en-us' },
782+
{ environment, locale: 'fr-fr' },
783+
]);
784+
785+
const groups = (importAssets as any).buildPublishGroups(pd);
786+
787+
expect(groups).to.have.lengthOf(1);
788+
expect(groups[0].locales).to.deep.equal(['en-us', 'fr-fr']);
789+
expect(groups[0].environments).to.have.members(['production', 'preview', 'development']);
790+
});
791+
792+
it('fans a RAGGED asset out into one group per distinct locale set (DX-9772)', () => {
793+
importAssets['environments'] = { 'e1': { name: 'new' }, 'e2': { name: 'blt5795' } };
794+
const pd = [
795+
{ environment: 'e2', locale: 'en-us' },
796+
{ environment: 'e1', locale: 'en-us' },
797+
{ environment: 'e1', locale: 'ar' },
798+
];
799+
800+
const groups = (importAssets as any).buildPublishGroups(pd);
801+
802+
expect(groups).to.have.lengthOf(2);
803+
expect(groups).to.deep.include.members([
804+
{ environments: ['new'], locales: ['ar', 'en-us'] },
805+
{ environments: ['blt5795'], locales: ['en-us'] },
806+
]);
807+
// blt5795 must never be paired with ar (the phantom the old flatten produced).
808+
const blt5795 = groups.find((g: any) => g.environments.includes('blt5795'));
809+
expect(blt5795.locales).to.not.include('ar');
810+
});
811+
812+
it('handles a single env-locale pair (1×1) as one group', () => {
813+
importAssets['environments'] = { 'e1': { name: 'production' } };
814+
const groups = (importAssets as any).buildPublishGroups([{ environment: 'e1', locale: 'en-us' }]);
815+
expect(groups).to.deep.equal([{ environments: ['production'], locales: ['en-us'] }]);
816+
});
817+
818+
it('drops environments absent from the destination (preserves DX-1656 guard)', () => {
819+
importAssets['environments'] = { 'e1': { name: 'production' } };
820+
const groups = (importAssets as any).buildPublishGroups([
821+
{ environment: 'e1', locale: 'en-us' },
822+
{ environment: 'gone', locale: 'en-us' },
823+
]);
824+
expect(groups).to.deep.equal([{ environments: ['production'], locales: ['en-us'] }]);
825+
});
826+
827+
it('returns [] for empty publish_details', () => {
828+
importAssets['environments'] = { 'e1': { name: 'production' } };
829+
expect((importAssets as any).buildPublishGroups([])).to.deep.equal([]);
830+
expect((importAssets as any).buildPublishGroups(undefined)).to.deep.equal([]);
831+
});
832+
});
833+
773834
describe('constructFolderImportOrder() method', () => {
774835
it('should order folders with null parent_uid first', () => {
775836
const folders = [

0 commit comments

Comments
 (0)