Skip to content

Commit 2033071

Browse files
chore: merge main, resolve conflicts
2 parents b5cf5ab + 25d309c commit 2033071

22 files changed

Lines changed: 1961 additions & 3302 deletions

CHANGELOG.md

Lines changed: 657 additions & 2343 deletions
Large diffs are not rendered by default.

METADATA_SUPPORT.md

Lines changed: 881 additions & 866 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@salesforce/source-deploy-retrieve",
3-
"version": "12.36.7",
3+
"version": "12.37.2",
44
"description": "JavaScript library to run Salesforce metadata deploys and retrieves",
55
"main": "lib/src/index.js",
66
"author": "Salesforce",
@@ -25,7 +25,7 @@
2525
"node": ">=18.0.0"
2626
},
2727
"dependencies": {
28-
"@salesforce/core": "^8.31.4",
28+
"@salesforce/core": "^8.32.3",
2929
"@salesforce/kit": "^3.2.4",
3030
"@salesforce/ts-types": "^2.0.12",
3131
"@salesforce/types": "^1.6.0",
@@ -41,7 +41,7 @@
4141
"yaml": "^2.9.0"
4242
},
4343
"devDependencies": {
44-
"@jsforce/jsforce-node": "^3.10.17",
44+
"@jsforce/jsforce-node": "^3.10.19",
4545
"@salesforce/cli-plugins-testkit": "^5.3.58",
4646
"@salesforce/dev-scripts": "^11.0.4",
4747
"@types/deep-equal-in-any-order": "^1.0.1",

src/client/metadataApiDeploy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ export class MetadataApiDeploy extends MetadataTransfer<
193193
await connection.metadata.cancelDeploy(this.id);
194194
}
195195

196+
protected getZipSize(): number | undefined {
197+
return this.zipSize;
198+
}
199+
196200
protected async pre(): Promise<AsyncResult> {
197201
const LifecycleInstance = Lifecycle.getInstance();
198202
const connection = await this.getConnection();

src/client/metadataTransfer.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export abstract class MetadataTransfer<
8989

9090
/**
9191
* Poll for the status of the metadata transfer request.
92-
* Default frequency is 100 ms.
92+
* Default frequency is 1000ms minimum, scaling with deploy size (zip bytes or component count).
9393
* Default timeout is 60 minutes.
9494
*
9595
* @param options Polling options; frequency, timeout, polling function.
@@ -98,7 +98,7 @@ export abstract class MetadataTransfer<
9898
public async pollStatus(options?: Partial<PollingClient.Options>): Promise<Result>;
9999
/**
100100
* Poll for the status of the metadata transfer request.
101-
* Default frequency is based on the number of SourceComponents, n, in the transfer, it ranges from 100ms -> n
101+
* Default frequency is 1000ms minimum, scaling with deploy size (zip bytes or component count, capped at 10s).
102102
* Default timeout is 60 minutes.
103103
*
104104
* @param frequency Polling frequency in milliseconds.
@@ -110,7 +110,12 @@ export abstract class MetadataTransfer<
110110
frequencyOrOptions?: number | Partial<PollingClient.Options>,
111111
timeout?: number
112112
): Promise<Result | undefined> {
113-
const normalizedOptions = normalizePollingInputs(frequencyOrOptions, timeout, sizeOfComponentSet(this.components));
113+
const normalizedOptions = normalizePollingInputs(
114+
frequencyOrOptions,
115+
timeout,
116+
sizeOfComponentSet(this.components),
117+
this.getZipSize()
118+
);
114119
// Initialize error retry tracking
115120
this.consecutiveErrorRetries = 0;
116121
this.errorRetryLimit = calculateErrorRetryLimit(this.logger);
@@ -204,6 +209,11 @@ export abstract class MetadataTransfer<
204209
this.event.on('error', subscriber);
205210
}
206211

212+
// eslint-disable-next-line class-methods-use-this
213+
protected getZipSize(): number | undefined {
214+
return undefined;
215+
}
216+
207217
protected async maybeSaveTempDirectory(target: SfdxFileFormat, cs?: ComponentSet): Promise<void> {
208218
if (this.mdapiTempDir) {
209219
await Lifecycle.getInstance().emitWarning(
@@ -367,10 +377,11 @@ const getConnectionNoHigherThanOrgAllows = async (conn: Connection, requestedVer
367377
export const normalizePollingInputs = (
368378
frequencyOrOptions?: number | Partial<PollingClient.Options>,
369379
timeout?: number,
370-
componentSetSize = 0
380+
componentSetSize = 0,
381+
zipSize?: number
371382
): Pick<PollingClient.Options, 'frequency' | 'timeout'> => {
372383
let pollingOptions: Pick<PollingClient.Options, 'frequency' | 'timeout'> = {
373-
frequency: Duration.milliseconds(calculatePollingFrequency(componentSetSize)),
384+
frequency: Duration.milliseconds(calculatePollingFrequency(componentSetSize, zipSize)),
374385
timeout: Duration.minutes(60),
375386
};
376387
if (isNumber(frequencyOrOptions)) {
@@ -383,7 +394,7 @@ export const normalizePollingInputs = (
383394
}
384395
// from the overloaded methods, there's a possibility frequency/timeout isn't set
385396
// guarantee frequency and timeout are set
386-
pollingOptions.frequency ??= Duration.milliseconds(calculatePollingFrequency(componentSetSize));
397+
pollingOptions.frequency ??= Duration.milliseconds(calculatePollingFrequency(componentSetSize, zipSize));
387398
pollingOptions.timeout ??= Duration.minutes(60);
388399

389400
return pollingOptions;
@@ -392,21 +403,25 @@ export const normalizePollingInputs = (
392403
/** yeah, there's a size property on CS. But we want the actual number of source components */
393404
const sizeOfComponentSet = (cs?: ComponentSet): number => cs?.getSourceComponents().toArray().length ?? 0;
394405

395-
/** based on the size of the components, pick a reasonable polling frequency */
396-
export const calculatePollingFrequency = (size: number): number => {
397-
if (size === 0) {
398-
// no component set size is possible for retrieve
399-
return 1000;
400-
} else if (size <= 10) {
401-
return 100;
402-
} else if (size <= 50) {
403-
return 250;
404-
} else if (size <= 100) {
405-
return 500;
406-
} else if (size <= 1000) {
406+
/** based on the size of the deploy (zip bytes for large deploys, component count for small ones), pick a reasonable polling frequency */
407+
export const calculatePollingFrequency = (componentCount: number, zipSize?: number): number => {
408+
// Use zip size only for large payloads where component count alone underestimates deploy time
409+
if (zipSize !== undefined && zipSize > 1_000_000) {
410+
const ONE_MB = 1_000_000;
411+
if (zipSize <= 5 * ONE_MB) {
412+
return 2000;
413+
} else if (zipSize <= 15 * ONE_MB) {
414+
return 5000;
415+
} else {
416+
// large deploys (15MB+ up to 39MB limit) — poll every 10s
417+
return 10_000;
418+
}
419+
}
420+
421+
if (componentCount <= 1000) {
407422
return 1000;
408423
} else {
409-
return size;
424+
return Math.min(componentCount, 10_000);
410425
}
411426
};
412427

src/convert/transformers/metadataTransformerFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { NonDecomposedMetadataTransformer } from './nonDecomposedMetadataTransfo
2525
import { LabelMetadataTransformer, LabelsMetadataTransformer } from './decomposeLabelsTransformer';
2626
import { DecomposedPermissionSetTransformer } from './decomposedPermissionSetTransformer';
2727
import { DecomposeExternalServiceRegistrationTransformer } from './decomposeExternalServiceRegistrationTransformer';
28+
import { UiBundleMetadataTransformer } from './uiBundleMetadataTransformer';
2829

2930
Messages.importMessagesDirectory(__dirname);
3031
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
@@ -57,6 +58,8 @@ export class MetadataTransformerFactory {
5758
: new LabelMetadataTransformer(this.registry, this.context);
5859
case 'decomposeExternalServiceRegistration':
5960
return new DecomposeExternalServiceRegistrationTransformer(this.registry, this.context);
61+
case 'uiBundle':
62+
return new UiBundleMetadataTransformer(this.registry, this.context);
6063
default:
6164
throw messages.createError('error_missing_transformer', [type.name, transformerId]);
6265
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { SourceComponent } from '../../resolve/sourceComponent';
17+
import { validateUiBundleForDeploy } from '../../resolve/adapters/uiBundleValidation';
18+
import { WriteInfo } from '../types';
19+
import { DefaultMetadataTransformer } from './defaultMetadataTransformer';
20+
21+
/**
22+
* Transformer for UIBundle components.
23+
*
24+
* Behaves like the default transformer, but validates the bundle's ui-bundle.json descriptor
25+
* when converting to metadata format for deploy. Retrieve (toSourceFormat) skips validation.
26+
*/
27+
export class UiBundleMetadataTransformer extends DefaultMetadataTransformer {
28+
public async toMetadataFormat(component: SourceComponent): Promise<WriteInfo[]> {
29+
if (component.content) {
30+
validateUiBundleForDeploy(component.content, component.tree, component.getForceIgnore());
31+
}
32+
return super.toMetadataFormat(component);
33+
}
34+
}

src/registry/metadataRegistry.json

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"waveTemplates": "wavetemplatebundle",
7272
"appTemplates": "appframeworktemplatebundle",
7373
"lightningTypes": "lightningtypebundle",
74-
"uiBundles": "uibundle"
74+
"uiBundles": "uibundle",
75+
"uiWidgets": "uiwidgetbundle"
7576
},
7677
"suffixes": {
7778
"Canvas": "canvasmetadata",
@@ -602,7 +603,8 @@
602603
"cnfgItemTypeIdentRule": "cnfgitemtypeidentrule",
603604
"cnfgItemTypeRelationDef": "cnfgitemtyperelationdef",
604605
"cnfgMgmtRelationTypeDef": "cnfgmgmtrelationtypedef",
605-
"meetingPlaybookDefinition": "meetingplaybookdefinition"
606+
"meetingPlaybookDefinition": "meetingplaybookdefinition",
607+
"idpConfiguration": "idpconfiguration"
606608
},
607609
"types": {
608610
"accesscontrolpolicy": {
@@ -4743,7 +4745,8 @@
47434745
"name": "UIBundle",
47444746
"suffix": "uibundle",
47454747
"strategies": {
4746-
"adapter": "uiBundles"
4748+
"adapter": "uiBundles",
4749+
"transformer": "uiBundle"
47474750
},
47484751
"strictDirectoryName": true,
47494752
"supportsPartialDelete": true
@@ -5371,6 +5374,26 @@
53715374
"directoryName": "policyRuleDefinitionSets",
53725375
"inFolder": false,
53735376
"strictDirectoryName": false
5377+
},
5378+
"idpconfiguration": {
5379+
"id": "idpconfiguration",
5380+
"name": "IdpConfiguration",
5381+
"suffix": "idpConfiguration",
5382+
"directoryName": "idpConfigurations",
5383+
"inFolder": false,
5384+
"strictDirectoryName": false
5385+
},
5386+
"uiwidgetbundle": {
5387+
"id": "uiwidgetbundle",
5388+
"name": "UiWidgetBundle",
5389+
"directoryName": "uiWidgets",
5390+
"inFolder": false,
5391+
"strictDirectoryName": true,
5392+
"metaFileSuffix": "uiwidget-meta.xml",
5393+
"strategies": {
5394+
"adapter": "bundle"
5395+
},
5396+
"supportsPartialDelete": true
53745397
}
53755398
}
53765399
}

src/registry/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ export type MetadataType = {
164164
| 'standard'
165165
| 'decomposedLabels'
166166
| 'decomposedPermissionSet'
167-
| 'decomposeExternalServiceRegistration';
167+
| 'decomposeExternalServiceRegistration'
168+
| 'uiBundle';
168169
decomposition?: 'topLevel' | 'folderPerType';
169170
recomposition?: 'startEmpty';
170171
};

src/resolve/adapters/uiBundleValidation.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
import { join, resolve, sep } from 'node:path';
1717
import { Messages } from '@salesforce/core/messages';
1818
import { SfError } from '@salesforce/core/sfError';
19-
import type { TreeContainer } from '../treeContainers';
19+
import { NodeFSTreeContainer, type TreeContainer } from '../treeContainers';
20+
import type { ForceIgnore } from '../forceIgnore';
2021
import { SourcePath } from '../../common/types';
2122

2223
Messages.importMessagesDirectory(__dirname);
@@ -146,6 +147,25 @@ function createFileError(message: string, actions?: string[]): SfError {
146147
return new SfError(message, 'ExpectedSourceFilesError', actions);
147148
}
148149

150+
/** Validate a UI bundle's `ui-bundle.json` descriptor (if present) at deploy time. Only runs against a real filesystem. */
151+
export function validateUiBundleForDeploy(
152+
contentPath: SourcePath,
153+
tree: TreeContainer,
154+
forceIgnore: ForceIgnore
155+
): void {
156+
if (!(tree instanceof NodeFSTreeContainer)) {
157+
return;
158+
}
159+
160+
const descriptorPath = join(contentPath, 'ui-bundle.json');
161+
if (!tree.exists(descriptorPath) || forceIgnore.denies(descriptorPath)) {
162+
return;
163+
}
164+
165+
const raw = tree.readFileSync(descriptorPath);
166+
validateUiBundleJson(raw, descriptorPath, contentPath, tree);
167+
}
168+
149169
/** Validate ui-bundle.json contents. Checks structure first, then schema, then file existence. */
150170
export function validateUiBundleJson(
151171
raw: Buffer,

0 commit comments

Comments
 (0)