New Command for Bootstrapping a project for ISV debugging (#282)

@W-4543894@
This commit is contained in:
Gunnar Wagenknecht 2018-02-05 13:36:21 +01:00 коммит произвёл GitHub
Родитель 74f153f69d
Коммит 3ae33f4cdd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 358 добавлений и 4 удалений

Просмотреть файл

@ -111,6 +111,7 @@ function registerCommands(): vscode.Disposable {
session.customRequest(EXCEPTION_BREAKPOINT_REQUEST, args);
});
});
return vscode.Disposable.from(
customEventHandler,
exceptionBreakpointCmd,

Просмотреть файл

@ -61,7 +61,8 @@
},
"activationEvents": [
"workspaceContains:sfdx-project.json",
"onCommand:sfdx.force.project.create"
"onCommand:sfdx.force.project.create",
"onCommand:sfdx.debug.isv.bootstrap"
],
"main": "./out/src",
"contributes": {
@ -253,6 +254,10 @@
{
"command": "sfdx.force.stop.apex.debug.logging",
"when": "sfdx:project_opened && sfdx:replay_debugger_extension"
},
{
"command": "sfdx.debug.isv.bootstrap",
"when": "config.salesforcedx-vscode-core.isv-debugger.enabled"
}
]
},
@ -388,6 +393,10 @@
{
"command": "sfdx.force.stop.apex.debug.logging",
"title": "%force_stop_apex_debug_logging%"
},
{
"command": "sfdx.debug.isv.bootstrap",
"title": "%isv_bootstrap_command_text%"
}
],
"configuration": {
@ -398,6 +407,11 @@
"type": ["boolean"],
"default": true,
"description": "%show_cli_success_msg_description%"
},
"salesforcedx-vscode-core.isv-debugger.enabled": {
"type": "boolean",
"default": false,
"description": "%isv_debugger_enabled_description%"
}
}
}

Просмотреть файл

@ -50,5 +50,10 @@
"force_start_apex_debug_logging":
"SFDX: Turn on Apex Debug Log for Replay Debugger",
"force_stop_apex_debug_logging":
"SFDX: Turn off Apex Debug Log for Replay Debugger"
"SFDX: Turn off Apex Debug Log for Replay Debugger",
"isv_bootstrap_command_text":
"SFDX: Create and Set Up Project for ISV Debugging",
"isv_debugger_enabled_description":
"Set to true to enable a feature preview of the ISV Debugger."
}

Просмотреть файл

@ -0,0 +1,186 @@
/*
* Copyright (c) 2017, 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 {
CliCommandExecutor,
Command,
SfdxCommandBuilder
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import {
CancelResponse,
ContinueResponse,
ParametersGatherer
} from '@salesforce/salesforcedx-utils-vscode/out/src/types';
import * as path from 'path';
import * as querystring from 'querystring';
import { Observable } from 'rxjs/Observable';
import * as vscode from 'vscode';
import { Uri } from 'vscode';
import { CommandExecution } from '../../../../salesforcedx-utils-vscode/out/src/cli/commandExecutor';
import { channelService } from '../../channels';
import { nls } from '../../messages';
import { notificationService } from '../../notifications';
import { CancellableStatusBar, taskViewService } from '../../statuses';
import {
CompositeParametersGatherer,
EmptyPreChecker,
SfdxCommandlet,
SfdxCommandletExecutor
} from '../commands';
import {
PathExistsChecker,
ProjectNameAndPath,
SelectProjectFolder,
SelectProjectName
} from '../forceProjectCreate';
export class IsvDebugBootstrapExecutor extends SfdxCommandletExecutor<{}> {
public build(data: {}): Command {
throw new Error('not in use');
}
public buildCreateProjectCommand(data: IsvDebugBootstrapConfig): Command {
return new SfdxCommandBuilder()
.withDescription(nls.localize('isv_debug_bootstrap_step1_create_project'))
.withArg('force:project:create')
.withFlag('--projectname', data.projectName)
.withFlag('--outputdir', data.projectUri)
.build();
}
public buildConfigureProjectCommand(data: IsvDebugBootstrapConfig): Command {
return new SfdxCommandBuilder()
.withDescription(
nls.localize('isv_debug_bootstrap_step1_configure_project')
)
.withArg('force:config:set')
.withArg(`isvDebuggerSid=${data.sessionId}`)
.withArg(`isvDebuggerUrl=${data.loginUrl}`)
.withArg(`instanceUrl=${data.loginUrl}`)
.build();
}
public execute(response: ContinueResponse<IsvDebugBootstrapConfig>): void {
const cancellationTokenSource = new vscode.CancellationTokenSource();
const cancellationToken = cancellationTokenSource.token;
const projectParentPath = response.data.projectUri;
const projectPath = path.join(projectParentPath, response.data.projectName);
const createProjectExecution = new CliCommandExecutor(
this.buildCreateProjectCommand(response.data),
{
cwd: projectParentPath
}
).execute(cancellationToken);
createProjectExecution.processExitSubject.subscribe(async data => {
if (data != undefined && data.toString() === '0') {
const configureProjectExecution = new CliCommandExecutor(
this.buildConfigureProjectCommand(response.data),
{
cwd: projectPath
}
).execute(cancellationToken);
configureProjectExecution.processExitSubject.subscribe(async data2 => {
if (data2 != undefined && data2.toString() === '0') {
// last step is open the folder
await vscode.commands.executeCommand(
'vscode.openFolder',
vscode.Uri.parse(projectPath)
);
}
});
this.attachExecution(
configureProjectExecution,
cancellationTokenSource,
cancellationToken
);
}
});
this.attachExecution(
createProjectExecution,
cancellationTokenSource,
cancellationToken
);
}
protected attachExecution(
execution: CommandExecution,
cancellationTokenSource: vscode.CancellationTokenSource,
cancellationToken: vscode.CancellationToken
) {
channelService.streamCommandOutput(execution);
channelService.showChannelOutput();
notificationService.reportExecutionError(
execution.command.toString(),
(execution.stderrSubject as any) as Observable<Error | undefined>
);
CancellableStatusBar.show(execution, cancellationTokenSource);
taskViewService.addCommandExecution(execution, cancellationTokenSource);
}
}
export type IsvDebugBootstrapConfig = ProjectNameAndPath & ForceIdeUri;
export interface ForceIdeUri {
loginUrl: string;
sessionId: string;
}
export class EnterForceIdeUri implements ParametersGatherer<ForceIdeUri> {
public async gather(): Promise<
CancelResponse | ContinueResponse<ForceIdeUri>
> {
const forceIdeUri = await vscode.window.showInputBox({
prompt: nls.localize('parameter_gatherer_paste_forceide_url')
});
if (forceIdeUri) {
const url = Uri.parse(forceIdeUri);
const parameter = querystring.parse(url.query);
if (parameter.url && parameter.sessionId) {
return {
type: 'CONTINUE',
data: {
loginUrl: parameter.url,
sessionId: parameter.sessionId
}
};
}
vscode.window.showErrorMessage(
nls.localize('parameter_gatherer_invalid_forceide_url')
);
}
return { type: 'CANCEL' };
}
}
const workspaceChecker = new EmptyPreChecker();
const parameterGatherer = new CompositeParametersGatherer(
new EnterForceIdeUri(),
new SelectProjectName(),
new SelectProjectFolder()
);
const pathExistsChecker = new PathExistsChecker();
const executor = new IsvDebugBootstrapExecutor();
const commandlet = new SfdxCommandlet(
workspaceChecker,
parameterGatherer,
executor,
pathExistsChecker
);
export function isvDebugBootstrap() {
commandlet.run();
}

Просмотреть файл

@ -43,6 +43,7 @@ import {
SfdxWorkspaceChecker
} from './commands';
import { restoreDebugLevels } from './commands/forceStopApexDebugLogging';
import { isvDebugBootstrap } from './commands/isvdebugging/bootstrapCmd';
import {
CLIENT_ID,
SFDX_CLIENT_ENV_VAR,
@ -203,6 +204,11 @@ function registerCommands(): vscode.Disposable {
forceStopApexDebugLogging
);
const isvDebugBootstrapCmd = vscode.commands.registerCommand(
'sfdx.debug.isv.bootstrap',
isvDebugBootstrap
);
// Internal commands
const internalCancelCommandExecution = vscode.commands.registerCommand(
CANCEL_EXECUTION_COMMAND,
@ -243,6 +249,7 @@ function registerCommands(): vscode.Disposable {
forceApexTriggerCreateCmd,
forceStartApexDebugLoggingCmd,
forceStopApexDebugLoggingCmd,
isvDebugBootstrapCmd,
internalCancelCommandExecution
);
}
@ -287,6 +294,15 @@ export async function activate(context: vscode.ExtensionContext) {
sfdxProjectOpened
);
const sfdxApexDebuggerExtension = vscode.extensions.getExtension(
'salesforce.salesforcedx-vscode-apex-debugger'
);
vscode.commands.executeCommand(
'setContext',
'sfdx:apex_debug_extension_installed',
sfdxApexDebuggerExtension && sfdxApexDebuggerExtension.id
);
// Commands
const commands = registerCommands();
context.subscriptions.push(commands);

Просмотреть файл

@ -49,6 +49,9 @@ export const messages = {
parameter_gatherer_enter_alias_name:
'Enter a scratch org alias or use default alias',
parameter_gatherer_enter_project_name: 'Enter project name',
parameter_gatherer_paste_forceide_url: 'Paste forceide:// URL from Setup',
parameter_gatherer_invalid_forceide_url:
"The forceide:// URL is invalid. From your subscriber's org, copy and paste the forceide:// URL shown on the Apex Debugger page in Setup.",
force_org_create_default_scratch_org_text:
'SFDX: Create a Default Scratch Org...',
@ -116,5 +119,10 @@ export const messages = {
'SFDX: Turn on Apex Debug Log for Replay Debugger',
force_apex_debug_log_status_bar_text: 'Logging ends at %s',
force_stop_apex_debug_logging:
'SFDX: Turn off Apex Debug Log for Replay Debugger'
'SFDX: Turn off Apex Debug Log for Replay Debugger',
isv_debug_bootstrap_step1_create_project:
'SFDX: ISV Debugger Setup, Step 1 of 2: Create Project',
isv_debug_bootstrap_step1_configure_project:
'SFDX: ISV Debugger Setup, Step 2 of 2: Configure Project'
};

Просмотреть файл

@ -0,0 +1,124 @@
import { expect } from 'chai';
import * as path from 'path';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import {
EnterForceIdeUri,
IsvDebugBootstrapExecutor
} from '../../../src/commands/isvdebugging/bootstrapCmd';
import { nls } from '../../../src/messages';
// tslint:disable:no-unused-expression
describe('ISV Debugging Project Bootstrap Command', () => {
const LOGIN_URL = 'a.b.c';
const SESSION_ID = '0x123';
const PROJECT_NAME = 'sfdx-simple';
const WORKSPACE_PATH = path.join(vscode.workspace.rootPath!, '..');
const PROJECT_DIR: vscode.Uri[] = [vscode.Uri.parse(WORKSPACE_PATH)];
describe('EnterForceIdeUri Gatherer', () => {
let inputBoxSpy: sinon.SinonStub;
let showErrorMessageSpy: sinon.SinonStub;
before(() => {
inputBoxSpy = sinon.stub(vscode.window, 'showInputBox');
inputBoxSpy.onCall(0).returns(undefined);
inputBoxSpy.onCall(1).returns('');
inputBoxSpy
.onCall(2)
.returns(`forceide://abc?url=${LOGIN_URL}&sessionId=${SESSION_ID}`);
inputBoxSpy.onCall(3).returns(`forceide://abc?url=${LOGIN_URL}`);
inputBoxSpy.onCall(4).returns(`forceide://abc?sessionId=${SESSION_ID}`);
showErrorMessageSpy = sinon.stub(vscode.window, 'showErrorMessage');
});
after(() => {
inputBoxSpy.restore();
showErrorMessageSpy.restore();
});
it('Should return cancel if forceide url is undefined', async () => {
const gatherer = new EnterForceIdeUri();
const response = await gatherer.gather();
expect(inputBoxSpy.calledOnce).to.be.true;
expect(response.type).to.equal('CANCEL');
expect(showErrorMessageSpy.notCalled).to.be.true;
});
it('Should return cancel if user input is empty string', async () => {
const gatherer = new EnterForceIdeUri();
const response = await gatherer.gather();
expect(inputBoxSpy.calledTwice).to.be.true;
expect(response.type).to.equal('CANCEL');
expect(showErrorMessageSpy.notCalled).to.be.true;
});
it('Should return Continue with inputted url if not undefined or empty', async () => {
const gatherer = new EnterForceIdeUri();
const response = await gatherer.gather();
expect(inputBoxSpy.calledThrice).to.be.true;
if (response.type === 'CONTINUE') {
expect(response.data.loginUrl).to.equal(LOGIN_URL);
expect(response.data.sessionId).to.equal(SESSION_ID);
} else {
expect.fail('Response should be of type ContinueResponse');
}
});
it('Should return cancel and show error if forceide url is missing sessionId', async () => {
expect(showErrorMessageSpy.calledOnce).to.be.false;
const gatherer = new EnterForceIdeUri();
const response = await gatherer.gather();
expect(inputBoxSpy.callCount).equal(4);
expect(response.type).to.equal('CANCEL');
expect(showErrorMessageSpy.calledOnce).to.be.true;
});
it('Should return cancel and show error if forceide url is missing login address', async () => {
expect(showErrorMessageSpy.calledTwice).to.be.false;
const gatherer = new EnterForceIdeUri();
const response = await gatherer.gather();
expect(inputBoxSpy.callCount).equal(5);
expect(response.type).to.equal('CANCEL');
expect(showErrorMessageSpy.calledTwice).to.be.true;
});
});
describe('CLI Builder', () => {
it('Should build the project create command', async () => {
const forceProjectCreateBuilder = new IsvDebugBootstrapExecutor();
const createCommand = forceProjectCreateBuilder.buildCreateProjectCommand(
{
loginUrl: LOGIN_URL,
sessionId: SESSION_ID,
projectName: PROJECT_NAME,
projectUri: PROJECT_DIR[0].fsPath
}
);
expect(createCommand.toCommand()).to.equal(
`sfdx force:project:create --projectname ${PROJECT_NAME} --outputdir ${PROJECT_DIR[0]
.fsPath}`
);
expect(createCommand.description).to.equal(
nls.localize('isv_debug_bootstrap_step1_create_project')
);
});
it('Should build the project configure command', async () => {
const forceProjectConfigBuilder = new IsvDebugBootstrapExecutor();
const configureCommand = forceProjectConfigBuilder.buildConfigureProjectCommand(
{
loginUrl: LOGIN_URL,
sessionId: SESSION_ID,
projectName: PROJECT_NAME,
projectUri: PROJECT_DIR[0].fsPath
}
);
expect(configureCommand.toCommand()).to.equal(
`sfdx force:config:set isvDebuggerSid=${SESSION_ID} isvDebuggerUrl=${LOGIN_URL} instanceUrl=${LOGIN_URL}`
);
expect(configureCommand.description).to.equal(
nls.localize('isv_debug_bootstrap_step1_configure_project')
);
});
});
});

Просмотреть файл

@ -280,6 +280,6 @@ describe('Empty VSCode workspace', () => {
await common.type('>SFDX:');
await app.wait();
const quickOpenText = await common.getQuickOpenElementsText();
expect(quickOpenText).to.equal('SFDX: Create Project');
expect(quickOpenText).to.contain('SFDX: Create Project');
});
});