Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a command to install eslint plugin #62

Merged
merged 18 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
"command": "salesforcedx-vscode-offline-app.onboardingWizard",
"title": "%extension.commands.config-wizard.title%",
"category": "%extension.commands.config-wizard.category%"
},
{
"command": "salesforcedx-vscode-offline-app.configureLintingTools",
"title": "%extension.commands.config-linting-tools.title%",
"category": "%extension.commands.config-wizard.category%",
"when": "sfdx_project_opened"
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
}
]
},
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extension.commands.config-wizard.title": "Configuration Wizard",
"extension.commands.config-wizard.category": "Offline Starter Kit",
"extension.commands.config-linting-tools.title": "Configure Linting Tools",
"extension.displayName": "Salesforce Mobile Extensions for Visual Studio Code",
"extension.description": "Tools to help developers create their Salesforce Mobile experiences in a VSCode development environment."
}
202 changes: 202 additions & 0 deletions src/commands/lint/configureLintingToolsCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { commands, l10n, window, ExtensionContext } from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { WorkspaceUtils } from '../../utils/workspaceUtils';
import { TAB_SPACES } from '../../utils/constants';

const configureLintingToolsCommand =
'salesforcedx-vscode-offline-app.configureLintingTools';
const eslintDependencies = [
['@salesforce/eslint-plugin-lwc-graph-analyzer', '^0.9.0'],
Copy link
Collaborator

Choose a reason for hiding this comment

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

These are configuration items where at least the version values would be better represented in contributes.configuration settings. That way a) we take hard-coded data out of the code, and b) users can update these values in their own personal configuration settings, to override the versions we select.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

image

['eslint', '^8.47.0']
];
const lwcGraphAnalyzerRecommended: string =
'plugin:@salesforce/lwc-graph-analyzer/recommended';
const eslintRecommended = 'eslint:recommended';

interface PackageJson {
devDependencies?: Record<string, string>;
}

enum MessageType {
Error,
InformationYesNo,
InformationOk
}

export function onActivate(context: ExtensionContext) {
commands.executeCommand(
'setContext',
'sfdx_project_opened',
WorkspaceUtils.isSfdxProjectOpened()
);
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
}

export class ConfigureLintingToolsCommand {
static async configure(): Promise<boolean> {
try {
if (!WorkspaceUtils.lwcFolderExists()) {
await this.showMessage('LWC folder does not exist.');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do these messages need any further context? I don't know what the message title/header will look like, if any. But on their own, these messages seem a little sparse/ambiguous.

return Promise.resolve(false);
}

if (!WorkspaceUtils.packageJsonExists()) {
await this.showMessage(
'The project does not contain package.json.'
);
return Promise.resolve(false);
}

// Ask user to add eslint plugin
const result = await this.showMessage(
'Do you want to add eslint plugin for LWC data graph anaylsis to your package.json?',
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
MessageType.InformationYesNo
);

if (!result || result.title === l10n.t('No')) {
return Promise.resolve(false);
} else {
let modified = false;
try {
modified = this.updateDevDependencies();
} catch (error) {
await this.showMessage(
`Error updating package.json: ${error}`
);
return Promise.resolve(false);
}

try {
this.updateEslintrc();
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should be tracking whether this file was updated or not too, and reporting to the user accordingly—even if the report is built into a single message. We could be updating this but not the devDependencies, and vice versa. We should be clear to the user, exactly what's being updated and what is not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Showing 3 messages now at a maximum, one for updating devdependencies, one for updating .eslintrc.json, and one for running npm. I thought maybe I should mash the three. But localizing string was going to be tricky so instead went with three.

image

} catch (error) {
await this.showMessage(
`Error updating .eslintrc.json: ${error}`
);
return Promise.resolve(false);
}

if (modified) {
await this.showMessage(
`Updated developer dependency in package.json. Run package manager such as npmr/yarn/pnpm to update node modules.`,
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
MessageType.InformationOk
);
} else {
await this.showMessage(
`No update was made in package.json. It already includes eslint plugin for LWC data graph analysis.`,
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
MessageType.InformationOk
);
}

return Promise.resolve(true);
}
} catch (error) {
await this.showMessage(
`There was an error trying to update developer dependency in package.json: ${error}`
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
);
return Promise.resolve(false);
}
}

static updateDevDependencies(): boolean {
const packageJson: PackageJson = WorkspaceUtils.getPackageJson();
const devDependencies = packageJson.devDependencies;
let modified = false;

if (devDependencies) {
eslintDependencies.forEach((nameValuePair) => {
const [name, value] = nameValuePair;
if (!devDependencies[name]) {
devDependencies[name] = value;
modified = true;
}
});
}

if (modified) {
// Save json only if the content was modified.
WorkspaceUtils.setPackageJson(packageJson);
}

return modified;
}

static updateEslintrc() {
const eslintrcPath = path.join(
WorkspaceUtils.getWorkspaceDir(),
WorkspaceUtils.LWC_PATH,
'.eslintrc.json'
);

if (fs.existsSync(eslintrcPath)) {
const eslintrc = JSON.parse(fs.readFileSync(eslintrcPath, 'utf-8'));
const eslintrcExtends = eslintrc.extends as Array<string>;

if (eslintrc && eslintrcExtends) {
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
let modified = false;

if (!eslintrcExtends.includes(eslintRecommended)) {
eslintrcExtends.push(eslintRecommended);
modified = true;
}

if (!eslintrcExtends.includes(lwcGraphAnalyzerRecommended)) {
eslintrc.extends.push(lwcGraphAnalyzerRecommended);
modified = true;
}

if (modified) {
// Save json only if the content was modified.
fs.writeFileSync(
eslintrcPath,
JSON.stringify(eslintrc, null, TAB_SPACES)
);
}
}
} else {
// Create eslintrc
fs.writeFileSync(
eslintrcPath,
`{"extends": ["${eslintRecommended}", "${lwcGraphAnalyzerRecommended}"]}`
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
);
}
}

static async showMessage(
message: string,
messageType: MessageType = MessageType.Error
): Promise<{ title: string } | undefined> {
const localizedMessage = l10n.t(message);
switch (messageType) {
case MessageType.Error:
return await window.showErrorMessage(localizedMessage, {
title: l10n.t('OK')
});
case MessageType.InformationYesNo:
return await window.showInformationMessage(
localizedMessage,
{ title: l10n.t('Yes') },
{ title: l10n.t('No') }
);
case MessageType.InformationOk:
return await await window.showInformationMessage(
sfdctaka marked this conversation as resolved.
Show resolved Hide resolved
localizedMessage,
{
title: l10n.t('OK')
}
);
}
}
}

export function registerCommand(context: ExtensionContext) {
commands.registerCommand(configureLintingToolsCommand, async () => {
await ConfigureLintingToolsCommand.configure();
});
}
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import * as onboardingWizard from './commands/wizard/onboardingWizard';
import * as configureLintingToolsCommand from './commands/lint/configureLintingToolsCommand';
import { CoreExtensionService } from './services/CoreExtensionService';

export function activate(context: vscode.ExtensionContext) {
Expand All @@ -28,6 +29,9 @@ export function activate(context: vscode.ExtensionContext) {

onboardingWizard.registerCommand(context);
onboardingWizard.onActivate(context);

configureLintingToolsCommand.registerCommand(context);
configureLintingToolsCommand.onActivate(context);
}

// This method is called when your extension is deactivated
Expand Down
157 changes: 157 additions & 0 deletions src/test/suite/commands/lint/configureLintingToolsCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import * as assert from 'assert';
import * as sinon from 'sinon';
import * as path from 'path';
import * as fs from 'fs';
import { window } from 'vscode';
import { afterEach } from 'mocha';
import { ConfigureLintingToolsCommand } from '../../../../commands/lint/configureLintingToolsCommand';
import { WorkspaceUtils } from '../../../../utils/workspaceUtils';
import {
TempProjectDirManager,
setupTempWorkspaceDirectoryStub
} from '../../../TestHelper';

suite('Configure Linting Tools Command Test Suite', () => {
afterEach(function () {
sinon.restore();
});

test('Configure linting cancelled because LWC folder does not exist', async () => {
sinon.stub(WorkspaceUtils, 'lwcFolderExists').returns(false);
const showErrorMessageStub = sinon.stub(window, 'showErrorMessage');
showErrorMessageStub.onCall(0).resolves({ title: 'OK' });
const result = await ConfigureLintingToolsCommand.configure();
assert.equal(result, false);
});

test('Configure linting cancelled because package.json does not exist', async () => {
sinon.stub(WorkspaceUtils, 'lwcFolderExists').returns(true);
sinon.stub(WorkspaceUtils, 'packageJsonExists').returns(false);
const showErrorMessageStub = sinon.stub(window, 'showErrorMessage');
showErrorMessageStub.onCall(0).resolves({ title: 'OK' });
const result = await ConfigureLintingToolsCommand.configure();
assert.equal(result, false);
});

test('Configure linting cancelled by the user', async () => {
sinon.stub(WorkspaceUtils, 'lwcFolderExists').returns(true);
sinon.stub(WorkspaceUtils, 'packageJsonExists').returns(true);
const showInformationMessageStub = sinon.stub(
window,
'showInformationMessage'
);
showInformationMessageStub.onCall(0).resolves({ title: 'No' });
const result = await ConfigureLintingToolsCommand.configure();
assert.equal(result, false);
});

test('Configure linting cancelled because updating pacakge.json failed', async () => {
sinon.stub(WorkspaceUtils, 'lwcFolderExists').returns(true);
sinon.stub(WorkspaceUtils, 'packageJsonExists').returns(true);
const showInformationMessageStub = sinon.stub(
window,
'showInformationMessage'
);
showInformationMessageStub.onCall(0).resolves({ title: 'Yes' });
sinon
.stub(ConfigureLintingToolsCommand, 'updateDevDependencies')
.throws('error');
const showErrorMessageStub = sinon.stub(window, 'showErrorMessage');
showErrorMessageStub.onCall(0).resolves({ title: 'OK' });
const result = await ConfigureLintingToolsCommand.configure();
assert.equal(result, false);
});

test('Configure linting cancelled because updating .eslintrc.json failed', async () => {
sinon.stub(WorkspaceUtils, 'lwcFolderExists').returns(true);
sinon.stub(WorkspaceUtils, 'packageJsonExists').returns(true);
const showInformationMessageStub = sinon.stub(
window,
'showInformationMessage'
);
showInformationMessageStub.onCall(0).resolves({ title: 'Yes' });
sinon
.stub(ConfigureLintingToolsCommand, 'updateDevDependencies')
.returns(true);
sinon
.stub(ConfigureLintingToolsCommand, 'updateEslintrc')
.throws('error');
const showErrorMessageStub = sinon.stub(window, 'showErrorMessage');
showErrorMessageStub.onCall(0).resolves({ title: 'OK' });
const result = await ConfigureLintingToolsCommand.configure();
assert.equal(result, false);
});

test('Configure linting did not update package.json because plugin was already included in the dev dependency', async () => {
sinon.stub(WorkspaceUtils, 'lwcFolderExists').returns(true);
sinon.stub(WorkspaceUtils, 'packageJsonExists').returns(true);
let showInformationMessageStub = sinon.stub(
window,
'showInformationMessage'
);
showInformationMessageStub.onCall(0).resolves({ title: 'Yes' });
sinon
.stub(ConfigureLintingToolsCommand, 'updateDevDependencies')
.returns(false);
sinon.stub(ConfigureLintingToolsCommand, 'updateEslintrc').returns();
showInformationMessageStub = sinon.stub(window, 'showErrorMessage');
showInformationMessageStub.onCall(0).resolves({ title: 'OK' });
const result = await ConfigureLintingToolsCommand.configure();
assert.equal(result, true);
});

test('Configure linting updated package.json successfully', async () => {
let getWorkspaceDirStub: sinon.SinonStub<[], string>;
let tempProjectDirManager: TempProjectDirManager;
tempProjectDirManager =
await TempProjectDirManager.createTempProjectDir();
getWorkspaceDirStub = setupTempWorkspaceDirectoryStub(
tempProjectDirManager
);
const packageJson = { devDependencies: { lwc: '1.2.3' } };
WorkspaceUtils.setPackageJson(packageJson);

sinon.stub(WorkspaceUtils, 'lwcFolderExists').returns(true);
let showInformationMessageStub = sinon.stub(
window,
'showInformationMessage'
);
showInformationMessageStub.onCall(0).resolves({ title: 'Yes' });
showInformationMessageStub.onCall(1).resolves({ title: 'Yes' });

try {
// Creating directories recursively
const lwcPath = path.join(
WorkspaceUtils.getWorkspaceDir(),
WorkspaceUtils.LWC_PATH
);
fs.mkdirSync(lwcPath, { recursive: true });
} catch (error) {
console.error('Error creating directories:', error);
}

const result = await ConfigureLintingToolsCommand.configure();
assert.equal(result, true);

const content = WorkspaceUtils.getPackageJson();
const updatedPackageJson = {
devDependencies: {
lwc: '1.2.3',
// eslint-disable-next-line @typescript-eslint/naming-convention
'@salesforce/eslint-plugin-lwc-graph-analyzer': '^0.9.0',
eslint: '^8.47.0'
}
};
assert.equal(
JSON.stringify(updatedPackageJson),
JSON.stringify(content)
);
});
});
Loading
Loading