Skip to content

Commit 3efa388

Browse files
feat: add --ignored-only flag to 'project deploy preview'
1 parent 848b90f commit 3efa388

File tree

6 files changed

+126
-10
lines changed

6 files changed

+126
-10
lines changed

command-snapshot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
{
1515
"command": "project:deploy:preview",
1616
"plugin": "@salesforce/plugin-deploy-retrieve",
17-
"flags": ["ignore-conflicts", "json", "manifest", "metadata", "source-dir", "target-org"],
17+
"flags": ["ignore-conflicts", "json", "manifest", "metadata", "only-ignored", "source-dir", "target-org"],
1818
"alias": ["deploy:metadata:preview"]
1919
},
2020
{

dreamhouse-lwc

Lines changed: 0 additions & 1 deletion
This file was deleted.

messages/deploy.metadata.preview.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ To preview the deployment of multiple metadata components, either set multiple -
3636

3737
Login username or alias for the target org.
3838

39+
# flags.only-ignored.summary
40+
41+
Preview files to be ignored in a deployment.
42+
3943
# flags.target-org.description
4044

4145
Overrides your default org.

src/commands/project/deploy/preview.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7+
import * as fs from 'fs';
8+
import * as path from 'path';
79
import { Messages } from '@salesforce/core';
810
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
911
import { SourceTracking } from '@salesforce/source-tracking';
10-
import { ForceIgnore } from '@salesforce/source-deploy-retrieve';
12+
import { ForceIgnore, MetadataResolver, NodeFSTreeContainer, RegistryAccess } from '@salesforce/source-deploy-retrieve';
1113
import { buildComponentSet } from '../../../utils/deploy';
12-
import { PreviewResult, printTables, compileResults, getConflictFiles } from '../../../utils/previewOutput';
14+
import {
15+
PreviewResult,
16+
printTables,
17+
compileResults,
18+
getConflictFiles,
19+
printIgnoredTable,
20+
PreviewFile,
21+
} from '../../../utils/previewOutput';
1322

1423
Messages.importMessagesDirectory(__dirname);
1524
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata.preview');
@@ -32,6 +41,11 @@ export default class DeployMetadataPreview extends SfCommand<PreviewResult> {
3241
description: messages.getMessage('flags.ignore-conflicts.description'),
3342
default: false,
3443
}),
44+
'only-ignored': Flags.boolean({
45+
char: 'i',
46+
summary: messages.getMessage('flags.only-ignored.summary'),
47+
exclusive: ['manifest', 'metadata', 'ignore-conflicts'],
48+
}),
3549
manifest: Flags.file({
3650
char: 'x',
3751
description: messages.getMessage('flags.manifest.description'),
@@ -60,9 +74,17 @@ export default class DeployMetadataPreview extends SfCommand<PreviewResult> {
6074
}),
6175
};
6276

77+
public forceIgnore!: ForceIgnore;
78+
6379
public async run(): Promise<PreviewResult> {
6480
const { flags } = await this.parse(DeployMetadataPreview);
6581
const deploySpecified = [flags.manifest, flags.metadata, flags['source-dir']].some((f) => f !== undefined);
82+
const defaultPackagePath = this.project.getDefaultPackage().path;
83+
this.forceIgnore = ForceIgnore.findAndCreate(defaultPackagePath);
84+
85+
if (flags['only-ignored']) {
86+
return this.calculateAndPrintForceIgnoredFiles({ sourceDir: flags['source-dir'], defaultPackagePath });
87+
}
6688

6789
// we'll need STL both to check conflicts and to get the list of local changes if no flags are provided
6890
const stl =
@@ -73,8 +95,6 @@ export default class DeployMetadataPreview extends SfCommand<PreviewResult> {
7395
project: this.project,
7496
});
7597

76-
const forceIgnore = ForceIgnore.findAndCreate(this.project.getDefaultPackage().path);
77-
7898
const [componentSet, filesWithConflicts] = await Promise.all([
7999
buildComponentSet({ ...flags, 'target-org': flags['target-org'].getUsername() }, stl),
80100
getConflictFiles(stl, flags['ignore-conflicts']),
@@ -84,7 +104,7 @@ export default class DeployMetadataPreview extends SfCommand<PreviewResult> {
84104
componentSet,
85105
projectPath: this.project.getPath(),
86106
filesWithConflicts,
87-
forceIgnore,
107+
forceIgnore: this.forceIgnore,
88108
baseOperation: 'deploy',
89109
});
90110

@@ -93,4 +113,49 @@ export default class DeployMetadataPreview extends SfCommand<PreviewResult> {
93113
}
94114
return output;
95115
}
116+
117+
private async calculateAndPrintForceIgnoredFiles(options: {
118+
sourceDir?: string[];
119+
defaultPackagePath: string;
120+
}): Promise<PreviewResult> {
121+
// the third parameter makes the resolver use the default ForceIgnore entries, which will allow us to .getComponentsFromPath of a .forceignored path
122+
const mdr = new MetadataResolver(new RegistryAccess(), new NodeFSTreeContainer(), false);
123+
124+
const ignoredFiles: PreviewFile[] = (
125+
await Promise.all((options.sourceDir ?? [options.defaultPackagePath]).map((sp) => this.statIgnored(sp.trim())))
126+
)
127+
.flat()
128+
.map((entry) => {
129+
try {
130+
const component = mdr.getComponentsFromPath(path.resolve(entry))[0];
131+
return {
132+
projectRelativePath: entry,
133+
fullName: component?.fullName,
134+
type: component?.type.name,
135+
ignored: true,
136+
conflict: false,
137+
};
138+
} catch (e) {
139+
// some file paths, such as aura/.eslintrc.json will cause issues when getComponentsFromPath(), so catch the error and continue without type information
140+
return { projectRelativePath: entry, ignored: true, conflict: false } as PreviewFile;
141+
}
142+
});
143+
if (!this.jsonEnabled()) printIgnoredTable(ignoredFiles, 'deploy');
144+
return { ignored: ignoredFiles, conflicts: [], toDeploy: [], toDelete: [], toRetrieve: [] };
145+
}
146+
147+
// Stat the filepath. Test if a file, recurse if a directory.
148+
private async statIgnored(filepath: string): Promise<string[]> {
149+
const stats = await fs.promises.stat(filepath);
150+
if (stats.isDirectory()) {
151+
return (await Promise.all(await this.findIgnored(filepath))).flat();
152+
} else {
153+
return this.forceIgnore.denies(filepath) ? [filepath] : [];
154+
}
155+
}
156+
157+
// Recursively search a directory for source files to test.
158+
private async findIgnored(dir: string): Promise<Array<Promise<string[]>>> {
159+
return (await fs.promises.readdir(dir)).map((filename) => this.statIgnored(path.join(dir, filename)));
160+
}
96161
}

src/utils/previewOutput.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ const printConflictsTable = (files: PreviewFile[]): void => {
243243
}
244244
};
245245

246-
const printIgnoredTable = (files: PreviewFile[], baseOperation: BaseOperation): void => {
246+
export const printIgnoredTable = (files: PreviewFile[], baseOperation: BaseOperation): void => {
247247
ux.log();
248248
if (files.length === 0) {
249249
ux.log(dim(messages.getMessage('ignored.none')));

test/nuts/tracking/forceIgnore.nut.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('forceignore changes', () => {
6060
await session?.clean();
6161
});
6262

63-
describe('local', () => {
63+
describe.only('local', () => {
6464
it('will not push a file that was created, then ignored', async () => {
6565
// setup a forceIgnore with some file. forceignore uses posix style paths
6666
const newForceIgnore = originalForceIgnore + '\n' + `${classdir}/IgnoreTest.cls*`;
@@ -72,6 +72,54 @@ describe('forceignore changes', () => {
7272
expect(output?.message).to.equal(deployMessages.getMessage('error.nothingToDeploy'));
7373
});
7474

75+
it('will list the forceignored files in a certain path', () => {
76+
const classDir = path.join('force-app', 'main', 'default', 'classes');
77+
const output = execCmd<PreviewResult>(`deploy:metadata:preview --only-ignored --source-dir ${classdir} --json`, {
78+
ensureExitCode: 0,
79+
}).jsonOutput?.result;
80+
81+
expect(output?.conflicts).to.deep.equal([]);
82+
expect(output?.toDeploy).to.deep.equal([]);
83+
expect(output?.toRetrieve).to.deep.equal([]);
84+
expect(output?.toDelete).to.deep.equal([]);
85+
expect(output?.ignored?.length).to.equal(2);
86+
expect(output?.ignored).to.be.deep.equal([
87+
{
88+
projectRelativePath: `${path.join(classDir, 'IgnoreTest.cls')}`,
89+
fullName: 'IgnoreTest',
90+
type: 'ApexClass',
91+
ignored: true,
92+
conflict: false,
93+
},
94+
{
95+
projectRelativePath: `${path.join(classDir, 'IgnoreTest.cls-meta.xml')}`,
96+
fullName: 'IgnoreTest',
97+
type: 'ApexClass',
98+
ignored: true,
99+
conflict: false,
100+
},
101+
]);
102+
});
103+
104+
it('will list the forceignored files', () => {
105+
const output = execCmd<PreviewResult>('deploy:metadata:preview --only-ignored --json', {
106+
ensureExitCode: 0,
107+
}).jsonOutput?.result;
108+
109+
expect(output?.conflicts).to.deep.equal([]);
110+
expect(output?.toDeploy).to.deep.equal([]);
111+
expect(output?.toRetrieve).to.deep.equal([]);
112+
expect(output?.toDelete).to.deep.equal([]);
113+
expect(output?.ignored?.length).to.equal(4);
114+
expect(
115+
output?.ignored.find((entry) => entry.projectRelativePath?.includes(path.join('aura', '.eslintrc.json')))
116+
).to.deep.equal({
117+
projectRelativePath: path.join('force-app', 'main', 'default', 'aura', '.eslintrc.json'),
118+
ignored: true,
119+
conflict: false,
120+
});
121+
});
122+
75123
it('shows the file in status as ignored', () => {
76124
const output = execCmd<StatusResult>('force:source:status --json', {
77125
ensureExitCode: 0,
@@ -169,7 +217,7 @@ describe('forceignore changes', () => {
169217
ensureExitCode: 0,
170218
}).jsonOutput?.result;
171219
expect(
172-
response?.ignored.some((c) => c.fullName === 'CreatedClass' && c.type === 'ApexClass' && c.ignored === true),
220+
response?.ignored.some((c) => c.fullName === 'CreatedClass' && c.type === 'ApexClass' && c.ignored),
173221
JSON.stringify(response)
174222
).to.equal(true);
175223
});

0 commit comments

Comments
 (0)