Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ workflows:
- windows
command:
- 'yarn test:nuts:convert'
- 'yarn test:nuts:commands:other'
- 'yarn test:nuts:delete'
- 'yarn test:nuts:deploy'
- 'yarn test:nuts:deploy:async'
Expand Down
15 changes: 10 additions & 5 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "verbose", "wait"]
},
{
"command": "force:mdapi:deploy:cancel",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"]
},
{
"command": "force:mdapi:beta:retrieve",
"plugin": "@salesforce/plugin-source",
Expand Down Expand Up @@ -71,6 +66,11 @@
"zipfilename"
]
},
{
"command": "force:mdapi:deploy:cancel",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"]
},
{
"command": "force:mdapi:describemetadata",
"plugin": "@salesforce/plugin-source",
Expand Down Expand Up @@ -162,6 +162,11 @@
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "verbose", "wait"]
},
{
"command": "force:source:ignored:list",
"plugin": "@salesforce/plugin-source",
"flags": ["json", "loglevel", "sourcepath"]
},
{
"command": "force:source:manifest:create",
"plugin": "@salesforce/plugin-source",
Expand Down
8 changes: 8 additions & 0 deletions messages/ignored_list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"description": "check your local project package directories for forceignored files",
"examples": ["$ sfdx force:source:ignored", "$ sfdx force:source:ignored --sourcepath force-app"],
"flags": {
"sourcepath": "file or directory of files that the command checks for foreceignored files"
},
"invalidSourcePath": "File or directory '%s' doesn't exist in your project. Specify one that exists and rerun the command."
Copy link
Contributor

Choose a reason for hiding this comment

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

is this message used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call...not anymore. SDR is gonna handle that

}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
"test:command-reference": "./bin/run commandreference:generate --erroronwarnings",
"test:deprecation-policy": "./bin/run snapshot:compare",
"test:nuts": "ts-node ./test/nuts/generateNuts.ts && nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
"test:nuts:commands:other": "mocha \"test/nuts/open.nut.ts\" \"test/nuts/ignored_list.nut.ts\" --slow 4500 --timeout 600000 --retries 0 --parallel",
"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",
"test:nuts:delete": "mocha \"test/nuts/delete.nut.ts\" --slow 4500 --timeout 600000 --retries 0",
"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",
Expand Down
92 changes: 92 additions & 0 deletions src/commands/force/source/ignored/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as path from 'path';
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
import { fs as fsCore, Messages, SfdxError } from '@salesforce/core';
import { ForceIgnore } from '@salesforce/source-deploy-retrieve';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-source', 'ignored_list');

export type SourceIgnoredResults = {
ignoredFiles: string[];
};

export class SourceIgnoredCommand extends SfdxCommand {
public static readonly description = messages.getMessage('description');
public static readonly requiresProject = true;

public static readonly flagsConfig: FlagsConfig = {
sourcepath: flags.filepath({
char: 'p',
description: messages.getMessage('flags.sourcepath'),
}),
};

private forceIgnore: ForceIgnore;
/**
* Outputs all forceignored files from package directories of a project,
* or based on a sourcepath param that points to a specific file or directory.
*/
// eslint-disable-next-line @typescript-eslint/require-await
public async run(): Promise<SourceIgnoredResults> {
try {
this.forceIgnore = ForceIgnore.findAndCreate(this.project.getPath());
const sourcepaths = this.flags.sourcepath
? [this.flags.sourcepath as string]
: this.project.getUniquePackageDirectories().map((pDir) => pDir.path);

const ignoredFiles = (await Promise.all(sourcepaths.map((sp) => this.statIgnored(sp.trim())))).flat();

// Command output
if (ignoredFiles.length) {
this.ux.log('Found the following ignored files:');
ignoredFiles.forEach((filepath) => this.ux.log(filepath));
} else {
this.ux.log('No ignored files found in paths:');
sourcepaths.forEach((sp) => this.ux.log(sp));
}

return { ignoredFiles };
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (err.code === 'ENOENT') {
throw SfdxError.create('@salesforce/plugin-source', 'ignored_list', 'invalidSourcePath', [
this.flags.sourcepath,
]);
}
throw SfdxError.wrap(err);
}
}

// Stat the filepath. Test if a file, recurse if a directory.
private async statIgnored(filepath: string): Promise<string[]> {
const stats = await fsCore.stat(filepath);
if (stats.isDirectory()) {
return (await Promise.all(await this.findIgnored(filepath))).flat();
} else {
return this.isIgnored(filepath) ? [filepath] : [];
}
}

// Recursively search a directory for source files to test.
private async findIgnored(dir: string): Promise<Array<Promise<string[]>>> {
this.logger.debug(`Searching dir: ${dir}`);
return (await fsCore.readdir(dir)).map((filename) => this.statIgnored(path.join(dir, filename)));
}

// Test if a source file is denied, adding any ignored files to
// the ignoredFiles array for output.
private isIgnored(filepath: string): boolean {
if (this.forceIgnore.denies(filepath)) {
this.logger.debug(`[DENIED]: ${filepath}`);
return true;
}
this.logger.debug(`[ACCEPTED]: ${filepath}`);
return false;
}
}
155 changes: 155 additions & 0 deletions test/nuts/ignored_list.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
import { expect } from 'chai';
import { AuthStrategy } from '@salesforce/cli-plugins-testkit/lib/hubAuth';
import { SourceIgnoredResults } from '../../src/commands/force/source/ignored/list';

describe('force:source:ignored:list', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

should we test the "no ignored files" possibility?

let session: TestSession;
let forceIgnorePath: string;
let originalForceIgnore;

const pathToIgnoredFile1 = path.join('foo-bar', 'app', 'classes', 'FooBar.cls');
const pathToIgnoredFile2 = path.join('foo-bar', 'app', 'classes', 'FooBar.cls-meta.xml');

before(async () => {
session = await TestSession.create({
project: {
gitClone: 'https://github.com/salesforcecli/sample-project-multiple-packages',
},
authStrategy: AuthStrategy.NONE,
});
forceIgnorePath = path.join(session.project.dir, '.forceignore');
originalForceIgnore = await fs.promises.readFile(forceIgnorePath, 'utf8');
});

after(async () => {
await session?.clean();
});

describe('no forceignore', () => {
before(async () => {
await fs.promises.rm(forceIgnorePath);
});
after(async () => {
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
});
it('default PkgDir', () => {
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
.result;
expect(result.ignoredFiles).to.deep.equal([]);
});
it('specified sourcePath', () => {
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
ensureExitCode: 0,
}).jsonOutput.result;
expect(result2.ignoredFiles).to.deep.equal([]);
});
});

describe('no files are ignored (empty forceignore)', () => {
before(async () => {
await fs.promises.writeFile(forceIgnorePath, '');
});
after(async () => {
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
});
it('default PkgDir', () => {
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
.result;
expect(result.ignoredFiles).to.deep.equal([]);
});
it('specified sourcePath', () => {
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
ensureExitCode: 0,
}).jsonOutput.result;
expect(result2.ignoredFiles).to.deep.equal([]);
});
});

describe('returns an ignored class using specified path in forceignore', () => {
before(async () => {
// forceignore uses a library that wants ignore rules in posix format.
await fs.promises.appendFile(
forceIgnorePath,
`${path.normalize(pathToIgnoredFile1).split(path.sep).join(path.posix.sep)}${os.EOL}`
);
await fs.promises.appendFile(
forceIgnorePath,
`${path.normalize(pathToIgnoredFile2).split(path.sep).join(path.posix.sep)}${os.EOL}`
);
});
after(async () => {
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
});
it('default PkgDir', () => {
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
.result;
expect(result.ignoredFiles).to.include(pathToIgnoredFile1);
expect(result.ignoredFiles).to.include(pathToIgnoredFile2);
});
it('specified sourcePath', () => {
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
ensureExitCode: 0,
}).jsonOutput.result;
expect(result2.ignoredFiles).to.include(pathToIgnoredFile1);
expect(result2.ignoredFiles).to.include(pathToIgnoredFile2);
});
});

describe('returns an ignored class using wildcards', () => {
before(async () => {
await fs.promises.appendFile(forceIgnorePath, '**/FooBar.*');
});
after(async () => {
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
});

it('default PkgDir', () => {
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
.result;
expect(result.ignoredFiles).to.include(pathToIgnoredFile1);
expect(result.ignoredFiles).to.include(pathToIgnoredFile2);
});
it('specified sourcePath', () => {
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
ensureExitCode: 0,
}).jsonOutput.result;
expect(result2.ignoredFiles).to.include(pathToIgnoredFile1);
expect(result2.ignoredFiles).to.include(pathToIgnoredFile2);
});
});

describe('returns an ignored non-metadata component', () => {
const lwcDir = path.join('foo-bar', 'app', 'lwc');
const lwcConfigPath = path.join(lwcDir, 'jsconfig.json');

before(async () => {
await fs.promises.mkdir(path.join(session.project.dir, lwcDir), { recursive: true });
await fs.promises.writeFile(path.join(session.project.dir, lwcConfigPath), '{}');
});
after(async () => {
await fs.promises.writeFile(forceIgnorePath, originalForceIgnore);
});

it('default PkgDir', () => {
const result = execCmd<SourceIgnoredResults>('force:source:ignored:list --json', { ensureExitCode: 0 }).jsonOutput
.result;
expect(result.ignoredFiles).to.include(lwcConfigPath);
});
it('specified sourcePath', () => {
const result2 = execCmd<SourceIgnoredResults>('force:source:ignored:list --json -p foo-bar', {
ensureExitCode: 0,
}).jsonOutput.result;
expect(result2.ignoredFiles).to.include(lwcConfigPath);
});
});
});