Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5114116
feat: add TransportProvider abstraction and DeployPipeline scaffolding
WillieRuemmele Sep 24, 2026
3cc0e85
refactor: rename DeployPipeline to TransportPipeline
WillieRuemmele Sep 24, 2026
2026007
chore: bump core
WillieRuemmele Sep 25, 2026
7b11514
feat: wire transport pipeline into retrieve path and add skipTranspor…
WillieRuemmele Sep 25, 2026
b4ef525
feat: add TransportProvider abstraction and DeployPipeline scaffolding
WillieRuemmele Sep 24, 2026
bfaf07b
refactor: rename DeployPipeline to TransportPipeline
WillieRuemmele Sep 24, 2026
f73d931
feat: wire transport pipeline into retrieve path and add skipTranspor…
WillieRuemmele Sep 25, 2026
cf3b9b0
refactor: coordinate transports outside metadata api
WillieRuemmele Oct 7, 2026
b7eae9f
feat: add ConnectApiTransport for Heroku Compute (PlatformComputeApp)
WillieRuemmele Sep 24, 2026
ac55ee1
refactor: lazy transport pipeline discovery and improved test coverage
WillieRuemmele Sep 30, 2026
5df5fec
fix: invalidate transport pipeline cache when transport-eligible comp…
WillieRuemmele Sep 30, 2026
a551da0
refactor: rename ConnectApiTransport to HerokuComputeTransport
WillieRuemmele Oct 6, 2026
8284d7d
refactor: remove registerTransport, use constructor injection in tests
WillieRuemmele Oct 6, 2026
bd880f3
feat: transport lifecycle events, retrieve flow fix, and improved err…
WillieRuemmele Oct 7, 2026
79e4285
refactor: require registry transport routes
WillieRuemmele Oct 7, 2026
dbbbcd9
refactor: keep transport internals private
WillieRuemmele Oct 7, 2026
dd0f0f0
refactor: constrain transport names
WillieRuemmele Oct 7, 2026
30cc02b
Merge branch 'wr/transport-scaffolding' of github.com:forcedotcom/sou…
WillieRuemmele Oct 7, 2026
e380c72
chore: bump core
WillieRuemmele Oct 7, 2026
19ca950
fix: harden transport execution
WillieRuemmele Oct 8, 2026
3f1b741
fix: bound transport resources
WillieRuemmele Oct 8, 2026
cd3971e
test: support Windows transport paths
WillieRuemmele Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,806 changes: 1,010 additions & 2,796 deletions CHANGELOG.md

Large diffs are not rendered by default.

1,904 changes: 951 additions & 953 deletions METADATA_SUPPORT.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,21 @@
"node": ">=22.0.0"
},
"dependencies": {
"@salesforce/core": "^9.2.2",
"@salesforce/core": "^9.3.2",
"@salesforce/kit": "^4.0.0",
"@salesforce/ts-types": "^3.2.0",
"@salesforce/types": "^1.6.0",
"fast-levenshtein": "^3.0.0",
"fast-xml-parser": "^5.7.3",
"form-data": "^4.0.6",
"got": "^11.8.6",
"graceful-fs": "^4.2.11",
"ignore": "^5.3.2",
"jszip": "^3.10.1",
"mime": "2.6.0",
"minimatch": "^9.0.9",
"proxy-agent": "^6.5.0",
"tar-stream": "^3.2.1",
"yaml": "^2.9.0"
},
"devDependencies": {
Expand All @@ -53,6 +55,7 @@
"@types/mocha": "^10.0.10",
"@types/node": "^18",
"@types/sinon": "^10.0.20",
"@types/tar-stream": "^3.1.4",
"deep-equal-in-any-order": "^1.1.19",
"esbuild": "^0.28.0",
"eslint": "^10.4.0",
Expand Down
1 change: 1 addition & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
ScopedPostDeploy,
} from './metadataApiDeploy';
export { MetadataApiRetrieve, RetrieveResult, ScopedPostRetrieve, ScopedPreRetrieve } from './metadataApiRetrieve';
export type { TransportStageEvent } from './transports';
export {
ComponentDeployment,
ComponentRetrieval,
Expand Down
29 changes: 21 additions & 8 deletions src/client/metadataApiDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ReplacementEvent } from '../convert/types';
import { MetadataConverter } from '../convert';
import { ComponentSet } from '../collections';
import { MetadataTransfer, MetadataTransferOptions } from './metadataTransfer';
import { AsyncTransportHandle } from './transports/types';
import {
AsyncResult,
ComponentStatus,
Expand Down Expand Up @@ -56,6 +57,8 @@ const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sd
// TODO: (NEXT MAJOR) this should just be a readonly object and not a class.
export class DeployResult implements MetadataTransferResult {
private fileResponses?: FileResponse[];
private transportFileResponses: FileResponse[] = [];
private asyncTransportHandles: AsyncTransportHandle[] = [];

public constructor(
public readonly response: MetadataApiDeployStatus,
Expand All @@ -69,19 +72,29 @@ export class DeployResult implements MetadataTransferResult {
if (!this.fileResponses) {
this.fileResponses = [
...new Map(
(this.components
? buildFileResponsesFromComponentSet(this.components)(this.response)
: buildFileResponses(this.response)
).map((v) => {
const base = `${v.type}#${v.fullName}#${v.filePath ?? ''}#${v.state}`;
const key = 'error' in v ? `${base}#${v.error}#${String(v.lineNumber)}#${String(v.columnNumber)}` : base;
return [key, v] as const;
})
// MDAPI and required transport steps can report the same source file.
// A serialized structural key preserves one response without adding a dependency.
[
...(this.components
? buildFileResponsesFromComponentSet(this.components)(this.response)
: buildFileResponses(this.response)),
...this.transportFileResponses,
].map((v) => [JSON.stringify(v), v])
).values(),
];
}
return this.fileResponses;
}

public addTransportResults(fileResponses: FileResponse[], asyncHandles: AsyncTransportHandle[] = []): void {
this.transportFileResponses.push(...fileResponses);
this.asyncTransportHandles.push(...asyncHandles);
this.fileResponses = undefined;
}

public getAsyncTransportHandles(): AsyncTransportHandle[] {
return this.asyncTransportHandles;
}
}

export type MetadataApiDeployOptions = {
Expand Down
31 changes: 25 additions & 6 deletions src/client/metadataApiRetrieve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { extract } from './retrieveExtract';
import { getPackageOptions } from './retrieveExtract';
import { MetadataApiRetrieveOptions } from './types';
import { AsyncTransportHandle } from './transports/types';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
Expand All @@ -48,6 +49,8 @@ export class RetrieveResult implements MetadataTransferResult {
// rather than "Changed".
private localComponents: ComponentSet;
private fileResponses?: FileResponse[];
private transportFileResponses: FileResponse[] = [];
private asyncTransportHandles: AsyncTransportHandle[] = [];

/**
* @param response The metadata retrieve response from the server
Expand All @@ -69,7 +72,7 @@ export class RetrieveResult implements MetadataTransferResult {
return this.fileResponses;
}

this.fileResponses = [];
const responses: FileResponse[] = [];

// construct failures
if (this.response.messages) {
Expand All @@ -80,15 +83,15 @@ export class RetrieveResult implements MetadataTransferResult {
const matches = new RegExp(/.+'(.+)'.+'(.+)'/).exec(message.problem);
if (matches) {
const [typeName, fullName] = matches.slice(1);
this.fileResponses.push({
responses.push({
fullName,
type: typeName,
state: ComponentStatus.Failed,
error: message.problem,
problemType: 'Error',
});
} else {
this.fileResponses.push({
responses.push({
fullName: '',
type: '',
problemType: 'Error',
Expand All @@ -110,21 +113,37 @@ export class RetrieveResult implements MetadataTransferResult {

if (!type.children || Object.values(type.children.types).some((t) => t.unaddressableWithoutParent)) {
for (const filePath of retrievedComponent.walkContent()) {
this.fileResponses.push({ ...baseResponse, filePath } satisfies FileResponseSuccess);
responses.push({ ...baseResponse, filePath } satisfies FileResponseSuccess);
}
}

if (xml) {
this.fileResponses.push({ ...baseResponse, filePath: xml } satisfies FileResponseSuccess);
responses.push({ ...baseResponse, filePath: xml } satisfies FileResponseSuccess);
}
}

// Add file responses for components that support partial delete (e.g., DigitalExperience)
// where pieces of the component were deleted in the org, then retrieved.
this.fileResponses.push(...(this.partialDeleteFileResponses ?? []));
responses.push(...(this.partialDeleteFileResponses ?? []));

responses.push(...this.transportFileResponses);

// MDAPI extraction and required transport retrieval can report the same source file.
// A serialized structural key preserves one response without adding a dependency.
this.fileResponses = [...new Map(responses.map((v) => [JSON.stringify(v), v])).values()];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is another pattern Effect simplifies (hashSet for basic object equality).

but also, why would there be duplicates between the main normal fileResponses and the transport ones?


return this.fileResponses;
}

public addTransportResults(fileResponses: FileResponse[], asyncHandles: AsyncTransportHandle[] = []): void {
this.transportFileResponses.push(...fileResponses);
this.asyncTransportHandles.push(...asyncHandles);
this.fileResponses = undefined;
}

public getAsyncTransportHandles(): AsyncTransportHandle[] {
return this.asyncTransportHandles;
}
}

export class MetadataApiRetrieve extends MetadataTransfer<
Expand Down
13 changes: 12 additions & 1 deletion src/client/metadataTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export abstract class MetadataTransfer<
protected mdapiTempDir?: string;
private transferId: Options['id'];
private event = new EventEmitter();
private readonly resultProcessors: Array<(result: Result, status: Status) => Promise<void>> = [];
private usernameOrConnection: string | Connection;
private apiVersion?: string;
private consecutiveErrorRetries = 0;
Expand Down Expand Up @@ -148,6 +149,11 @@ export abstract class MetadataTransfer<
}

const result = await this.post(completedMdapiStatus);
for (const processor of this.resultProcessors) {
// Processors extend the operation result before it is returned to callers.
// eslint-disable-next-line no-await-in-loop
await processor(result, completedMdapiStatus);
}
if (completedMdapiStatus.status === RequestStatus.Canceled) {
this.event.emit('cancel', completedMdapiStatus);
} else {
Expand Down Expand Up @@ -176,7 +182,7 @@ export abstract class MetadataTransfer<
// this keeps SfError data for failures in post deploy/retrieve.
error.setData({
id: this.id,
causeErrorData: error.data,
causeErrorData: err.data as AnyJson,
});

error.actions = err.actions;
Expand All @@ -201,6 +207,11 @@ export abstract class MetadataTransfer<
this.event.on('finish', subscriber);
}

/** @internal Register work that must complete before pollStatus returns its result. */
public addResultProcessor(processor: (result: Result, status: Status) => Promise<void>): void {
this.resultProcessors.push(processor);
}

public onCancel(subscriber: (result: Status | undefined) => void): void {
this.event.on('cancel', subscriber);
}
Expand Down
Loading
Loading