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

feat: rename lightning component bundles #3923

Merged
merged 28 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a415e55
feat: rename lightning component
floralan Mar 7, 2022
c441da1
chore: utilize path
floralan Mar 8, 2022
6b31049
chore(forcerenamelightningcomponent): add telemetry
floralan Mar 9, 2022
00f58f9
chore: add regular expression to check the file name
floralan Mar 9, 2022
e77dc20
chore: add unit test
floralan Mar 16, 2022
9946fd4
feat: rename web component bundles
floralan Mar 16, 2022
a73e931
chore: delete useless export
floralan Mar 16, 2022
eca0e57
chore: update the dup error
floralan Mar 16, 2022
7677870
chore: show error when input is empty
floralan Mar 16, 2022
a6ca470
chore: revise code based on comments
floralan Mar 17, 2022
8033b28
chore: add comment
floralan Mar 17, 2022
c373a98
chore: revise the naming for a variable
floralan Mar 17, 2022
2a666f4
chore: minor updates in forceRenameLightningComponent.ts
floralan Mar 18, 2022
8c1144e
fix: isDuplicate should be case-sensitive
floralan Mar 18, 2022
ae399f9
Merge branch 'develop' into fl/rename_lightning_component
floralan Mar 18, 2022
1b616ae
chore: add parameterGatherer and refill component name
floralan Mar 18, 2022
c140c0a
chore: convert sync function to async
floralan Mar 19, 2022
8c9003d
chore: add test for isNameMatch & keep sync functions
floralan Mar 19, 2022
7759f04
fix: change command text
floralan Mar 24, 2022
14100bf
fix: use async functions
floralan Mar 24, 2022
7a1c779
fix: edit i18n for command text
floralan Mar 24, 2022
3329033
fix: trim user input
floralan Mar 24, 2022
8027eae
test: add test for trim input
floralan Mar 24, 2022
1bc9904
chore: return logic
floralan Mar 24, 2022
fa6d72d
chore: existSync to async
floralan Mar 24, 2022
5235c6f
Merge branch 'develop' into fl/rename_lightning_component
floralan Mar 24, 2022
64d4416
fix: showWarningMessageSpy
floralan Mar 24, 2022
b18b4fa
Merge branch 'fl/rename_lightning_component' of github.com:forcedotco…
floralan Mar 24, 2022
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
12 changes: 12 additions & 0 deletions packages/salesforcedx-vscode-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@
{
"command": "sfdx.create.manifest",
"when": "sfdx:project_opened"
},
{
"command": "sfdx.lightning.rename",
"when": "sfdx:project_opened && resource =~ /.*/(lwc|aura)/[^/]+(/[^/]+\\.(html|css|js|xml|svg|cmp|app|design|auradoc))?$/"
jeffb-sfdc marked this conversation as resolved.
Show resolved Hide resolved
}
],
"commandPalette": [
Expand Down Expand Up @@ -447,6 +451,10 @@
"command": "sfdx.create.manifest",
"when": "false"
},
{
"command": "sfdx.lightning.rename",
"when": "false"
},
{
"command": "sfdx.force.apex.trigger.create",
"when": "sfdx:project_opened"
Expand Down Expand Up @@ -937,6 +945,10 @@
{
"command": "sfdx.force.launch.apex.replay.debugger.with.current.file",
"title": "%force_launch_apex_replay_debugger_with_current_file%"
},
{
"command": "sfdx.lightning.rename",
"title": "%force_lightning_rename_component_text%"
}
],
"configuration": {
Expand Down
1 change: 1 addition & 0 deletions packages/salesforcedx-vscode-core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"force_lightning_interface_create_text": "SFDX: Create Aura Interface",
"force_lightning_lwc_create_text": "SFDX: Create Lightning Web Component",
"force_lightning_lwc_test_create_text": "SFDX: Create Lightning Web Component Test",
"force_lightning_rename_component_text": "SFDX: Rename Component (Files Only)",
floralan marked this conversation as resolved.
Show resolved Hide resolved
"force_source_status_local_text": "SFDX: View Local Changes",
"force_source_status_remote_text": "SFDX: View Changes in Default Scratch Org",
"force_debugger_stop_text": "SFDX: Stop Apex Debugger Session",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
gbockus-sf marked this conversation as resolved.
Show resolved Hide resolved
* 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 { LibraryCommandletExecutor } from '@salesforce/salesforcedx-utils-vscode/out/src';
import { notificationService } from '@salesforce/salesforcedx-utils-vscode/out/src/commands';
import { ContinueResponse } from '@salesforce/salesforcedx-utils-vscode/src/types';
import * as fs from 'fs';
import * as path from 'path';
import { format } from 'util';
import * as vscode from 'vscode';
import { OUTPUT_CHANNEL } from '../channels';
import { nls } from '../messages';
import { FilePathGatherer, SfdxCommandlet, SfdxWorkspaceChecker } from './util';

const RENAME_LIGHTNING_COMPONENT_EXECUTOR = 'force_rename_lightning_component';
const RENAME_INPUT_PLACEHOLDER = 'rename_component_input_placeholder';
const RENAME_INPUT_PROMPT = 'rename_component_input_prompt';
const RENAME_INPUT_DUP_ERROR = 'rename_component_input_dup_error';
const RENAME_WARNING = 'rename_component_warning';
floralan marked this conversation as resolved.
Show resolved Hide resolved

export class RenameLwcComponentExecutor extends LibraryCommandletExecutor<string> {
private sourceFsPath: string;
private responseText: string | undefined;
floralan marked this conversation as resolved.
Show resolved Hide resolved
constructor(sourceFsPath: string, responseText: string | undefined) {
super(
nls.localize(RENAME_LIGHTNING_COMPONENT_EXECUTOR),
RENAME_LIGHTNING_COMPONENT_EXECUTOR,
OUTPUT_CHANNEL
);
this.sourceFsPath = sourceFsPath;
this.responseText = responseText;
}

public async run(
response: ContinueResponse<string>,
progress?: vscode.Progress<{ message?: string | undefined; increment?: number | undefined; }>,
token?: vscode.CancellationToken
): Promise<boolean> {
if (this.sourceFsPath) {
if (this.responseText) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: you can combine these into a single if statement

Copy link
Contributor

@gbockus-sf gbockus-sf Mar 18, 2022

Choose a reason for hiding this comment

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

Should we add a log or telemetry for when we encounter the case where either of these values is falsy?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gbockus-sf Does these conditions are already includes into telemetry since this class extends LibraryCommandletExecutor?

renameComponent(this.sourceFsPath, this.responseText);
return true;
}
}
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

swinging back around to the telemetry. We do get a ping about this command being called b/c of the LibraryCommandletExecutor extends...however we don't know if we actually do it as determined by this logic. It's fine to push that off for now as we need a plan, but I was just calling it out previously.

}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I still think everything below here should probably be in a service/util class...but get have have existing patterns we should break away from.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gbockus-sf This can be Dev Choice problem that we can discuss within the team later, yes, I followed the existing pattern.

export async function forceRenameLightningComponent(sourceUri: vscode.Uri) {
const sourceFsPath = sourceUri.fsPath;
const inputOptions = {
placeHolder: nls.localize(RENAME_INPUT_PLACEHOLDER),
promopt: nls.localize(RENAME_INPUT_PROMPT)
} as vscode.InputBoxOptions;
floralan marked this conversation as resolved.
Show resolved Hide resolved

const responseText = await vscode.window.showInputBox(inputOptions);
if (sourceFsPath) {
floralan marked this conversation as resolved.
Show resolved Hide resolved
const commandlet = new SfdxCommandlet(
new SfdxWorkspaceChecker(),
new FilePathGatherer(sourceUri),
new RenameLwcComponentExecutor(sourceFsPath, responseText)
);
await commandlet.run();
}
}

function renameComponent(sourceFsPath: string, newName: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This smells like a service method vs something that should live in the command. The command code should only really concern itself with command execution. The check for existing and actual renaming should live elsewhere. Maybe under a services directory in core or utils. It looks like we have some one off services here and there but nothing organizing the logic. @jag-sfdc any thoughts or collections of services that I'm unaware of in the codebase?

const componentPath = getComponentPath(sourceFsPath);
const componentName = path.basename(componentPath);
checkForDuplicateName(componentPath, newName);
const items = fs.readdirSync(componentPath);
for (const item of items) {
// only rename the file that has same name with component
if (isNameMatch(item, componentName, componentPath)) {
const newItem = item.replace(componentName, newName);
fs.renameSync(
floralan marked this conversation as resolved.
Show resolved Hide resolved
path.join(componentPath, item),
path.join(componentPath, newItem)
);
}
}
const newComponentPath = path.join(path.dirname(componentPath), newName);
fs.renameSync(
componentPath,
newComponentPath
);
notificationService.showWarningMessage(nls.localize(RENAME_WARNING));
floralan marked this conversation as resolved.
Show resolved Hide resolved
}

function getComponentPath(sourceFsPath: string): string {
const stats = fs.statSync(sourceFsPath);
floralan marked this conversation as resolved.
Show resolved Hide resolved
return stats.isFile() ? path.dirname(sourceFsPath) : sourceFsPath;
}

function checkForDuplicateName(componentPath: string, newName: string) {
if (isDuplicate(componentPath, newName)) {
const errorMessage = nls.localize(RENAME_INPUT_DUP_ERROR);
notificationService.showErrorMessage(errorMessage);
throw new Error(format(errorMessage));
}
}

function isDuplicate(componentPath: string, newName: string): boolean {
// A LWC component can't share the same name as a Aura component
floralan marked this conversation as resolved.
Show resolved Hide resolved
const componentPathDirName = path.dirname(componentPath);
let lwcPath: string;
let auraPath: string;
if (isLwcComponent(componentPath)) {
lwcPath = componentPathDirName;
auraPath = path.join(path.dirname(componentPathDirName), 'aura');
} else {
lwcPath = path.join(path.dirname(componentPathDirName), 'lwc');
floralan marked this conversation as resolved.
Show resolved Hide resolved
auraPath = componentPathDirName;
}
const allLwcComponents = fs.readdirSync(lwcPath);
const allAuraComponents = fs.readdirSync(auraPath);
floralan marked this conversation as resolved.
Show resolved Hide resolved
if (allLwcComponents.includes(newName) || allAuraComponents.includes(newName)) {
floralan marked this conversation as resolved.
Show resolved Hide resolved
return true;
}
return false;
jeffb-sfdc marked this conversation as resolved.
Show resolved Hide resolved
}

function isNameMatch(item: string, componentName: string, componentPath: string) {
floralan marked this conversation as resolved.
Show resolved Hide resolved
const isLwc = isLwcComponent(componentPath);
let regularExp: RegExp;
if (isLwc) {
regularExp = new RegExp(`${componentName}\.(html|js|js-meta.xml|css|svg)`);
} else {
regularExp = new RegExp(`${componentName}(((Controller|Renderer|Helper)?\.js)|(\.(cmp|app|css|design|auradoc|svg)))`);
}
return item.match(regularExp) ? true : false;
floralan marked this conversation as resolved.
Show resolved Hide resolved
}

function isLwcComponent(componentPath: string): boolean {
return path.basename(path.dirname(componentPath)) === 'lwc' ? true : false;
jeffb-sfdc marked this conversation as resolved.
Show resolved Hide resolved
floralan marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 3 additions & 0 deletions packages/salesforcedx-vscode-core/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,6 @@ export {
forceRefreshSObjects,
initSObjectDefinitions
} from './forceRefreshSObjects';
export {
forceRenameLightningComponent
} from './forceRenameLightningComponent';
6 changes: 6 additions & 0 deletions packages/salesforcedx-vscode-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
forcePackageInstall,
forceProjectWithManifestCreate,
forceRefreshSObjects,
forceRenameLightningComponent,
forceSfdxProjectCreate,
forceSourceDelete,
forceSourceDeployManifest,
Expand Down Expand Up @@ -426,6 +427,11 @@ function registerCommands(
forceRefreshSObjects
);

const forceRenameComponentCmd = vscode.commands.registerCommand(
'sfdx.lightning.rename',
forceRenameLightningComponent
);

return vscode.Disposable.from(
forceAuthAccessTokenCmd,
forceAuthWebLoginCmd,
Expand Down
7 changes: 6 additions & 1 deletion packages/salesforcedx-vscode-core/src/messages/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,5 +621,10 @@ export const messages = {
force_sobjects_refresh: 'SFDX: Refresh SObject Definitions',
sobject_refresh_all: 'All SObjects',
sobject_refresh_custom: 'Custom SObjects',
sobject_refresh_standard: 'Standard SObjects'
sobject_refresh_standard: 'Standard SObjects',
force_rename_lightning_component: 'SFDX: Rename Component (Files Only)',
rename_component_input_dup_error: 'Component name is already in use in LWC or Aura',
rename_component_input_placeholder: 'Enter a unique component name',
rename_component_input_prompt: 'Press Enter to confirm your input or Escape to cancel',
rename_component_warning: 'Warning: References to the old name will not be updated. Update manually and redeploy once all changes have been made.'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { notificationService } from '@salesforce/salesforcedx-utils-vscode/out/src/commands';
import { expect } from 'chai';
import * as fs from 'fs';
import * as path from 'path';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import {RenameLwcComponentExecutor} from '../../../src/commands/forceRenameLightningComponent';

const lwcPath = vscode.Uri.parse('/force-app/main/default/lwc');
const auraPath = vscode.Uri.parse('/force-app/main/default/aura/');
const lwcComponent = 'hero';
const auraComponent = 'page';
const itemsInHero = ['_test_', 'hero.css', 'hero.html', 'hero.js', 'hero.js-meta.xml', 'templateOne.html'];
const itemsInPage = ['_test_', 'page.auradoc', 'page.cmp', 'page.cmp-meta.xml', 'page.css', 'page.design', 'page.svg', 'pageController.js', 'pageHelper.js', 'pageRenderer.js', 'templateOne.css'];

const env = sinon.createSandbox();
let renameStub: sinon.SinonStub;
let statStub: sinon.SinonStub;

describe('Force Rename Lightning Component', () => {
describe('Happy Path Unit Test', () => {
beforeEach(() => {
renameStub = env.stub(fs, 'renameSync').returns(undefined);
statStub = env.stub(fs, 'statSync').returns({
isFile: () => {
return false;
}
});
});

afterEach(() => {
env.restore();
});

it('should rename the files and folder with new name under the same path', async () => {
const sourceUri = vscode.Uri.joinPath(lwcPath, lwcComponent);
env.stub(fs, 'readdirSync')
.onFirstCall().returns([])
.onSecondCall().returns([])
.onThirdCall().returns([itemsInHero[1]]);
const executor = new RenameLwcComponentExecutor(sourceUri.fsPath, 'hero1');
await executor.run({
type: 'CONTINUE',
data: ''
});
const oldFilePath = path.join(sourceUri.fsPath, 'hero.css');
const newFilePath = path.join(sourceUri.fsPath, 'hero1.css');
const newFolderPath = path.join(lwcPath.fsPath, 'hero1');
expect(renameStub.callCount).to.equal(2);
expect(renameStub.calledWith(oldFilePath, newFilePath)).to.equal(true);
expect(renameStub.calledWith(sourceUri.fsPath, newFolderPath)).to.equal(true);
});

it('should only rename the files and folder that have same name with LWC component', async () => {
const sourceUri = vscode.Uri.joinPath(lwcPath, lwcComponent);
env.stub(fs, 'readdirSync')
.onFirstCall().returns([])
.onSecondCall().returns([])
.onThirdCall().returns(itemsInHero);
const executor = new RenameLwcComponentExecutor(sourceUri.fsPath, 'hero1');
await executor.run({
type: 'CONTINUE',
data: ''
});
expect(renameStub.callCount).to.equal(5);
Copy link
Contributor

Choose a reason for hiding this comment

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

dumb q: why is the count 5 here? Might be worth a code comment to call out what things we expect to be renamed if we are going to verify the call arguments.

Copy link
Contributor Author

@floralan floralan Mar 24, 2022

Choose a reason for hiding this comment

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

@gbockus-sf The previous test focus on the call arguments, this test is intended to make sure it only rename the files and folder that have same name with LWC component. Here in this lwc component directory, it has 6 files and only 4 files match the component name, so rename should be called 5 times (plus renaming the folder rename).

});

it('should only rename the files and folder that have same name with Aura component', async () => {
const sourceUri = vscode.Uri.joinPath(auraPath, auraComponent);
env.stub(fs, 'readdirSync')
.onFirstCall().returns([])
.onSecondCall().returns([])
.onThirdCall().returns(itemsInPage);
const executor = new RenameLwcComponentExecutor(sourceUri.fsPath, 'page1');
await executor.run({
type: 'CONTINUE',
data: ''
});
expect(renameStub.callCount).to.equal(10);
});

it('should show the warning message once rename is done', async () => {
const sourceUri = vscode.Uri.joinPath(lwcPath, lwcComponent);
env.stub(fs, 'readdirSync')
.onFirstCall().returns([])
.onSecondCall().returns([])
.onThirdCall().returns([itemsInHero[1]]);
const warningSpy = env.spy(notificationService, 'showWarningMessage');
floralan marked this conversation as resolved.
Show resolved Hide resolved
const executor = new RenameLwcComponentExecutor(sourceUri.fsPath, 'hero1');
await executor.run({
type: 'CONTINUE',
data: ''
});
expect(warningSpy.callCount).to.equal(1);
});
});

describe('Exception handling', () => {
beforeEach(() => {
renameStub = env.stub(fs, 'renameSync').returns(undefined);
statStub = env.stub(fs, 'statSync').returns({
isFile: () => {
return false;
}
});
});

afterEach(() => {
env.restore();
});

it('should not rename when input text is empty', async () => {
floralan marked this conversation as resolved.
Show resolved Hide resolved
const sourceUri = vscode.Uri.joinPath(lwcPath, lwcComponent);
env.stub(fs, 'readdirSync')
.onFirstCall().returns([])
.onSecondCall().returns([])
.onThirdCall().returns([itemsInHero[1]]);
const executor = new RenameLwcComponentExecutor(sourceUri.fsPath, undefined);
await executor.run({
type: 'CONTINUE',
data: ''
});
expect(renameStub.callCount).to.equal(0);
});

it('should not show warning message when input text is empty', async () => {
const sourceUri = vscode.Uri.joinPath(lwcPath, lwcComponent);
env.stub(fs, 'readdirSync')
.onFirstCall().returns([])
.onSecondCall().returns([])
.onThirdCall().returns([itemsInHero[1]]);
const warningSpy = env.spy(notificationService, 'showWarningMessage');
const executor = new RenameLwcComponentExecutor(sourceUri.fsPath, undefined);
await executor.run({
type: 'CONTINUE',
data: ''
});
expect(warningSpy.callCount).to.equal(0);
});

it('should enforce unique component name under LWC and Aura and show error message for duplicate name', async () => {
const sourceUri = vscode.Uri.joinPath(lwcPath, lwcComponent);
env.stub(fs, 'readdirSync')
.onFirstCall().returns([lwcComponent])
.onSecondCall().returns([]);
let exceptionThrown = false;
try {
const executor = new RenameLwcComponentExecutor(sourceUri.fsPath, 'hero');
await executor.run({
type: 'CONTINUE',
data: ''
});
} catch (e) {
exceptionThrown = true;
}
expect(exceptionThrown).to.equal(true);
expect(renameStub.callCount).to.equal(0);
});
});
});