Skip to content

Commit 232bca9

Browse files
ignored:list (#391)
* feat: ignored list * chore: snapshot update * refactor: use SDR compSet * test: nut for ignored:list * test: ci for unstable org signup destinations * chore: bump stl/sdr * Revert "test: ci for unstable org signup destinations" This reverts commit ec24217. * test: pr feedback test ideas * ci: run the misc command NUTs * refactor: revert to walking all files for ignored * test: spell nut path correctly * test: forceignore doesn't use windows paths Co-authored-by: Willie Ruemmele <[email protected]>
1 parent a6b7c10 commit 232bca9

File tree

6 files changed

+267
-5
lines changed

6 files changed

+267
-5
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ workflows:
7171
- windows
7272
command:
7373
- 'yarn test:nuts:convert'
74+
- 'yarn test:nuts:commands:other'
7475
- 'yarn test:nuts:delete'
7576
- 'yarn test:nuts:deploy'
7677
- 'yarn test:nuts:deploy:async'

command-snapshot.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@
3131
"plugin": "@salesforce/plugin-source",
3232
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "verbose", "wait"]
3333
},
34-
{
35-
"command": "force:mdapi:deploy:cancel",
36-
"plugin": "@salesforce/plugin-source",
37-
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"]
38-
},
3934
{
4035
"command": "force:mdapi:beta:retrieve",
4136
"plugin": "@salesforce/plugin-source",
@@ -71,6 +66,11 @@
7166
"zipfilename"
7267
]
7368
},
69+
{
70+
"command": "force:mdapi:deploy:cancel",
71+
"plugin": "@salesforce/plugin-source",
72+
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"]
73+
},
7474
{
7575
"command": "force:mdapi:describemetadata",
7676
"plugin": "@salesforce/plugin-source",
@@ -162,6 +162,11 @@
162162
"plugin": "@salesforce/plugin-source",
163163
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "verbose", "wait"]
164164
},
165+
{
166+
"command": "force:source:ignored:list",
167+
"plugin": "@salesforce/plugin-source",
168+
"flags": ["json", "loglevel", "sourcepath"]
169+
},
165170
{
166171
"command": "force:source:manifest:create",
167172
"plugin": "@salesforce/plugin-source",

messages/ignored_list.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"description": "check your local project package directories for forceignored files",
3+
"examples": ["$ sfdx force:source:ignored", "$ sfdx force:source:ignored --sourcepath force-app"],
4+
"flags": {
5+
"sourcepath": "file or directory of files that the command checks for foreceignored files"
6+
},
7+
"invalidSourcePath": "File or directory '%s' doesn't exist in your project. Specify one that exists and rerun the command."
8+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"test:command-reference": "./bin/run commandreference:generate --erroronwarnings",
159159
"test:deprecation-policy": "./bin/run snapshot:compare",
160160
"test:nuts": "ts-node ./test/nuts/generateNuts.ts && nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
161+
"test:nuts:commands:other": "mocha \"test/nuts/open.nut.ts\" \"test/nuts/ignored_list.nut.ts\" --slow 4500 --timeout 600000 --retries 0 --parallel",
161162
"test:nuts:convert": "cross-env PLUGIN_SOURCE_SEED_FILTER=convert ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
162163
"test:nuts:delete": "mocha \"test/nuts/delete.nut.ts\" --slow 4500 --timeout 600000 --retries 0",
163164
"test:nuts:deploy": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy PLUGIN_SOURCE_SEED_EXCLUDE=async ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) 2022, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import * as path from 'path';
8+
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
9+
import { fs as fsCore, Messages, SfdxError } from '@salesforce/core';
10+
import { ForceIgnore } from '@salesforce/source-deploy-retrieve';
11+
12+
Messages.importMessagesDirectory(__dirname);
13+
const messages = Messages.loadMessages('@salesforce/plugin-source', 'ignored_list');
14+
15+
export type SourceIgnoredResults = {
16+
ignoredFiles: string[];
17+
};
18+
19+
export class SourceIgnoredCommand extends SfdxCommand {
20+
public static readonly description = messages.getMessage('description');
21+
public static readonly requiresProject = true;
22+
23+
public static readonly flagsConfig: FlagsConfig = {
24+
sourcepath: flags.filepath({
25+
char: 'p',
26+
description: messages.getMessage('flags.sourcepath'),
27+
}),
28+
};
29+
30+
private forceIgnore: ForceIgnore;
31+
/**
32+
* Outputs all forceignored files from package directories of a project,
33+
* or based on a sourcepath param that points to a specific file or directory.
34+
*/
35+
// eslint-disable-next-line @typescript-eslint/require-await
36+
public async run(): Promise<SourceIgnoredResults> {
37+
try {
38+
this.forceIgnore = ForceIgnore.findAndCreate(this.project.getPath());
39+
const sourcepaths = this.flags.sourcepath
40+
? [this.flags.sourcepath as string]
41+
: this.project.getUniquePackageDirectories().map((pDir) => pDir.path);
42+
43+
const ignoredFiles = (await Promise.all(sourcepaths.map((sp) => this.statIgnored(sp.trim())))).flat();
44+
45+
// Command output
46+
if (ignoredFiles.length) {
47+
this.ux.log('Found the following ignored files:');
48+
ignoredFiles.forEach((filepath) => this.ux.log(filepath));
49+
} else {
50+
this.ux.log('No ignored files found in paths:');
51+
sourcepaths.forEach((sp) => this.ux.log(sp));
52+
}
53+
54+
return { ignoredFiles };
55+
} catch (err) {
56+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
57+
if (err.code === 'ENOENT') {
58+
throw SfdxError.create('@salesforce/plugin-source', 'ignored_list', 'invalidSourcePath', [
59+
this.flags.sourcepath,
60+
]);
61+
}
62+
throw SfdxError.wrap(err);
63+
}
64+
}
65+
66+
// Stat the filepath. Test if a file, recurse if a directory.
67+
private async statIgnored(filepath: string): Promise<string[]> {
68+
const stats = await fsCore.stat(filepath);
69+
if (stats.isDirectory()) {
70+
return (await Promise.all(await this.findIgnored(filepath))).flat();
71+
} else {
72+
return this.isIgnored(filepath) ? [filepath] : [];
73+
}
74+
}
75+
76+
// Recursively search a directory for source files to test.
77+
private async findIgnored(dir: string): Promise<Array<Promise<string[]>>> {
78+
this.logger.debug(`Searching dir: ${dir}`);
79+
return (await fsCore.readdir(dir)).map((filename) => this.statIgnored(path.join(dir, filename)));
80+
}
81+
82+
// Test if a source file is denied, adding any ignored files to
83+
// the ignoredFiles array for output.
84+
private isIgnored(filepath: string): boolean {
85+
if (this.forceIgnore.denies(filepath)) {
86+
this.logger.debug(`[DENIED]: ${filepath}`);
87+
return true;
88+
}
89+
this.logger.debug(`[ACCEPTED]: ${filepath}`);
90+
return false;
91+
}
92+
}

test/nuts/ignored_list.nut.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import * as fs from 'fs';
8+
import * as os from 'os';
9+
import * as path from 'path';
10+
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
11+
import { expect } from 'chai';
12+
import { AuthStrategy } from '@salesforce/cli-plugins-testkit/lib/hubAuth';
13+
import { SourceIgnoredResults } from '../../src/commands/force/source/ignored/list';
14+
15+
describe('force:source:ignored:list', () => {
16+
let session: TestSession;
17+
let forceIgnorePath: string;
18+
let originalForceIgnore;
19+
20+
const pathToIgnoredFile1 = path.join('foo-bar', 'app', 'classes', 'FooBar.cls');
21+
const pathToIgnoredFile2 = path.join('foo-bar', 'app', 'classes', 'FooBar.cls-meta.xml');
22+
23+
before(async () => {
24+
session = await TestSession.create({
25+
project: {
26+
gitClone: 'https://github.com/salesforcecli/sample-project-multiple-packages',
27+
},
28+
authStrategy: AuthStrategy.NONE,
29+
});
30+
forceIgnorePath = path.join(session.project.dir, '.forceignore');
31+
originalForceIgnore = await fs.promises.readFile(forceIgnorePath, 'utf8');
32+
});
33+
34+
after(async () => {
35+
await session?.clean();
36+
});
37+
38+
describe('no forceignore', () => {
39+
before(async () => {
40+
await fs.promises.rm(forceIgnorePath);
41+
});
42+
after(async () => {
43+
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
44+
});
45+
it('default PkgDir', () => {
46+
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
47+
.result;
48+
expect(result.ignoredFiles).to.deep.equal([]);
49+
});
50+
it('specified sourcePath', () => {
51+
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
52+
ensureExitCode: 0,
53+
}).jsonOutput.result;
54+
expect(result2.ignoredFiles).to.deep.equal([]);
55+
});
56+
});
57+
58+
describe('no files are ignored (empty forceignore)', () => {
59+
before(async () => {
60+
await fs.promises.writeFile(forceIgnorePath, '');
61+
});
62+
after(async () => {
63+
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
64+
});
65+
it('default PkgDir', () => {
66+
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
67+
.result;
68+
expect(result.ignoredFiles).to.deep.equal([]);
69+
});
70+
it('specified sourcePath', () => {
71+
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
72+
ensureExitCode: 0,
73+
}).jsonOutput.result;
74+
expect(result2.ignoredFiles).to.deep.equal([]);
75+
});
76+
});
77+
78+
describe('returns an ignored class using specified path in forceignore', () => {
79+
before(async () => {
80+
// forceignore uses a library that wants ignore rules in posix format.
81+
await fs.promises.appendFile(
82+
forceIgnorePath,
83+
`${path.normalize(pathToIgnoredFile1).split(path.sep).join(path.posix.sep)}${os.EOL}`
84+
);
85+
await fs.promises.appendFile(
86+
forceIgnorePath,
87+
`${path.normalize(pathToIgnoredFile2).split(path.sep).join(path.posix.sep)}${os.EOL}`
88+
);
89+
});
90+
after(async () => {
91+
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
92+
});
93+
it('default PkgDir', () => {
94+
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
95+
.result;
96+
expect(result.ignoredFiles).to.include(pathToIgnoredFile1);
97+
expect(result.ignoredFiles).to.include(pathToIgnoredFile2);
98+
});
99+
it('specified sourcePath', () => {
100+
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
101+
ensureExitCode: 0,
102+
}).jsonOutput.result;
103+
expect(result2.ignoredFiles).to.include(pathToIgnoredFile1);
104+
expect(result2.ignoredFiles).to.include(pathToIgnoredFile2);
105+
});
106+
});
107+
108+
describe('returns an ignored class using wildcards', () => {
109+
before(async () => {
110+
await fs.promises.appendFile(forceIgnorePath, '**/FooBar.*');
111+
});
112+
after(async () => {
113+
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
114+
});
115+
116+
it('default PkgDir', () => {
117+
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
118+
.result;
119+
expect(result.ignoredFiles).to.include(pathToIgnoredFile1);
120+
expect(result.ignoredFiles).to.include(pathToIgnoredFile2);
121+
});
122+
it('specified sourcePath', () => {
123+
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
124+
ensureExitCode: 0,
125+
}).jsonOutput.result;
126+
expect(result2.ignoredFiles).to.include(pathToIgnoredFile1);
127+
expect(result2.ignoredFiles).to.include(pathToIgnoredFile2);
128+
});
129+
});
130+
131+
describe('returns an ignored non-metadata component', () => {
132+
const lwcDir = path.join('foo-bar', 'app', 'lwc');
133+
const lwcConfigPath = path.join(lwcDir, 'jsconfig.json');
134+
135+
before(async () => {
136+
await fs.promises.mkdir(path.join(session.project.dir, lwcDir), { recursive: true });
137+
await fs.promises.writeFile(path.join(session.project.dir, lwcConfigPath), '{}');
138+
});
139+
after(async () => {
140+
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
141+
});
142+
143+
it('default PkgDir', () => {
144+
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
145+
.result;
146+
expect(result.ignoredFiles).to.include(lwcConfigPath);
147+
});
148+
it('specified sourcePath', () => {
149+
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
150+
ensureExitCode: 0,
151+
}).jsonOutput.result;
152+
expect(result2.ignoredFiles).to.include(lwcConfigPath);
153+
});
154+
});
155+
});

0 commit comments

Comments
 (0)