From 5d9ece592adcf1c65da1732036b1bc02e7f1f333 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 05:54:32 +0000 Subject: [PATCH 1/3] Initial plan From de9cf4d819f43e7aa6d37a1cc2dd21a305437a8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 06:03:29 +0000 Subject: [PATCH 2/3] Fix Power Pages commands registration on UsGov systems - Commands now register even if generator installation fails - Added proper exit code checking for npm install - Added retry mechanism with user-friendly error messages - Added test coverage for command registration behavior - Improved error handling and telemetry logging Co-authored-by: amitjoshi438 <54068463+amitjoshi438@users.noreply.github.com> --- src/client/lib/GeneratorAcquisition.ts | 11 +- .../create/CreateCommandWrapper.ts | 56 ++++++++- .../Integration/CreateCommandWrapper.test.ts | 115 ++++++++++++++++++ 3 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 src/client/test/Integration/CreateCommandWrapper.test.ts diff --git a/src/client/lib/GeneratorAcquisition.ts b/src/client/lib/GeneratorAcquisition.ts index 1e79d3441..9269e37ee 100644 --- a/src/client/lib/GeneratorAcquisition.ts +++ b/src/client/lib/GeneratorAcquisition.ts @@ -104,17 +104,18 @@ export class GeneratorAcquisition implements IDisposable { fs.writeFileSync(path.join(this._ppagesGlobalPath, "package.json"), JSON.stringify(packageJson), 'utf-8'); const child = this.npm(['install']); - if (child.error) { + if (child.error || child.status !== 0) { + const errorMessage = child.error ? String(child.error) : `npm install exited with code ${child.status}`; oneDSLoggerWrapper.getLogger().traceError( 'PowerPagesGeneratorInstallComplete', - String(child.error), - { name: 'PowerPagesGeneratorInstallComplete', message: String(child.error) } as Error, - { cliVersion: this._generatorVersion }, {} + errorMessage, + { name: 'PowerPagesGeneratorInstallComplete', message: errorMessage } as Error, + { cliVersion: this._generatorVersion, exitCode: child.status || -1 }, {} ); this._context.showErrorMessage(vscode.l10n.t({ message: "Cannot install Power Pages generator: {0}", - args: [String(child.error)], + args: [errorMessage], comment: ["{0} represents the error message returned from the exception"] })); } else { diff --git a/src/client/power-pages/create/CreateCommandWrapper.ts b/src/client/power-pages/create/CreateCommandWrapper.ts index 8fb859d7c..28710f292 100644 --- a/src/client/power-pages/create/CreateCommandWrapper.ts +++ b/src/client/power-pages/create/CreateCommandWrapper.ts @@ -15,6 +15,23 @@ import { createWebpage } from "./Webpage"; import { createWebTemplate } from "./WebTemplate"; const activeEditor = vscode.window.activeTextEditor; +function showGeneratorNotAvailableError(generator: GeneratorAcquisition): void { + const retryAction = vscode.l10n.t('Retry Installation'); + vscode.window.showErrorMessage( + vscode.l10n.t('Power Pages generator is not available. Please ensure npm is installed and try again.'), + retryAction + ).then(selection => { + if (selection === retryAction) { + const result = generator.ensureInstalled(); + if (result) { + vscode.window.showInformationMessage( + vscode.l10n.t('Power Pages generator installed successfully. Please reload the window to use the commands.') + ); + } + } + }); +} + export function initializeGenerator( context: vscode.ExtensionContext, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -24,8 +41,12 @@ export function initializeGenerator( generator.ensureInstalled(); context.subscriptions.push(generator); const yoCommandPath = generator.yoCommandPath; + + // Always register commands, even if generator installation failed + // Commands will show appropriate error messages if generator is not available + registerCreateCommands(context, yoCommandPath, generator); + if (yoCommandPath) { - registerCreateCommands(context, yoCommandPath); vscode.workspace .getConfiguration("powerPlatform") .update("generatorInstalled", true, true); @@ -34,13 +55,20 @@ export function initializeGenerator( function registerCreateCommands( context: vscode.ExtensionContext, - yoCommandPath: string + yoCommandPath: string | null, + generator: GeneratorAcquisition ) { vscode.commands.registerCommand( "microsoft-powerapps-portals.contentsnippet", async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: CONTENT_SNIPPET, triggerPoint: triggerPoint }); + + if (!yoCommandPath) { + showGeneratorNotAvailableError(generator); + return; + } + const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -58,6 +86,12 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: Tables.WEBTEMPLATE, triggerPoint: triggerPoint }); + + if (!yoCommandPath) { + showGeneratorNotAvailableError(generator); + return; + } + const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -75,6 +109,12 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: Tables.WEBPAGE, triggerPoint: triggerPoint }); + + if (!yoCommandPath) { + showGeneratorNotAvailableError(generator); + return; + } + const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -92,6 +132,12 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: Tables.PAGETEMPLATE, triggerPoint: triggerPoint }); + + if (!yoCommandPath) { + showGeneratorNotAvailableError(generator); + return; + } + const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -109,6 +155,12 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: WEBFILE, triggerPoint: triggerPoint }); + + if (!yoCommandPath) { + showGeneratorNotAvailableError(generator); + return; + } + const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, diff --git a/src/client/test/Integration/CreateCommandWrapper.test.ts b/src/client/test/Integration/CreateCommandWrapper.test.ts new file mode 100644 index 000000000..1748cac0e --- /dev/null +++ b/src/client/test/Integration/CreateCommandWrapper.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from 'vscode'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { initializeGenerator } from '../../power-pages/create/CreateCommandWrapper'; +import { GeneratorAcquisition } from '../../lib/GeneratorAcquisition'; +import { ICliAcquisitionContext } from '../../lib/CliAcquisitionContext'; + +describe('CreateCommandWrapper', () => { + let context: vscode.ExtensionContext; + let cliContext: ICliAcquisitionContext; + let registerCommandStub: sinon.SinonStub; + let generatorStub: sinon.SinonStubbedInstance; + + beforeEach(() => { + registerCommandStub = sinon.stub(vscode.commands, 'registerCommand'); + + context = { + subscriptions: [], + globalState: { + get: sinon.stub(), + update: sinon.stub(), + setKeysForSync: sinon.stub() + }, + workspaceState: { + get: sinon.stub(), + update: sinon.stub(), + keys: sinon.stub().returns([]) + } + } as unknown as vscode.ExtensionContext; + + cliContext = { + extensionPath: '/mock/path', + globalStorageLocalPath: '/mock/storage', + showInformationMessage: sinon.stub(), + showErrorMessage: sinon.stub(), + showCliPreparingMessage: sinon.stub(), + showCliReadyMessage: sinon.stub(), + showCliInstallFailedError: sinon.stub(), + locDotnetNotInstalledOrInsufficient: sinon.stub() + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should register commands even when generator installation fails', () => { + // Stub GeneratorAcquisition to simulate failed installation + sinon.stub(GeneratorAcquisition.prototype, 'ensureInstalled').returns(null); + sinon.stub(GeneratorAcquisition.prototype, 'yoCommandPath').get(() => null); + + initializeGenerator(context, cliContext); + + // Verify that all Power Pages commands were registered despite installation failure + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.contentsnippet')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webtemplate')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webpage')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.pagetemplate')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webfile')).to.be.true; + }); + + it('should register commands when generator installation succeeds', () => { + // Stub GeneratorAcquisition to simulate successful installation + sinon.stub(GeneratorAcquisition.prototype, 'ensureInstalled').returns('/mock/yo/path'); + sinon.stub(GeneratorAcquisition.prototype, 'yoCommandPath').get(() => '/mock/yo/path'); + + const updateStub = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + update: updateStub + } as unknown as vscode.WorkspaceConfiguration); + + initializeGenerator(context, cliContext); + + // Verify that all Power Pages commands were registered + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.contentsnippet')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webtemplate')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webpage')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.pagetemplate')).to.be.true; + expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webfile')).to.be.true; + + // Verify that the configuration was updated to indicate generator is installed + expect(updateStub.calledWith('generatorInstalled', true, true)).to.be.true; + }); + + it('should show error message when command is invoked without generator installed', async () => { + // Stub GeneratorAcquisition to simulate failed installation + sinon.stub(GeneratorAcquisition.prototype, 'ensureInstalled').returns(null); + sinon.stub(GeneratorAcquisition.prototype, 'yoCommandPath').get(() => null); + + const showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined); + + initializeGenerator(context, cliContext); + + // Get the registered command handler for webpage + const webpageCommand = registerCommandStub.getCalls().find( + call => call.args[0] === 'microsoft-powerapps-portals.webpage' + ); + expect(webpageCommand).to.not.be.undefined; + + // Execute the command handler + if (webpageCommand) { + await webpageCommand.args[1](); + + // Verify that an error message was shown + expect(showErrorMessageStub.called).to.be.true; + const errorMessage = showErrorMessageStub.getCall(0).args[0]; + expect(errorMessage).to.contain('Power Pages generator is not available'); + } + }); +}); From d03eaa4ffad86e8690872d8fe8b57c6a6a42d04b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:48:37 +0000 Subject: [PATCH 3/3] Revert command registration changes - keep only error detection fix Per feedback, commands should NOT register if generator installation fails. Reverted CreateCommandWrapper.ts to original behavior where commands only register when generator is successfully installed. Kept the improved error detection in GeneratorAcquisition.ts which properly checks npm exit codes (child.status !== 0) in addition to child.error. This ensures installation failures are properly detected and reported on restricted networks like UsGov systems. Co-authored-by: amitjoshi438 <54068463+amitjoshi438@users.noreply.github.com> --- .../create/CreateCommandWrapper.ts | 56 +-------- .../Integration/CreateCommandWrapper.test.ts | 115 ------------------ 2 files changed, 2 insertions(+), 169 deletions(-) delete mode 100644 src/client/test/Integration/CreateCommandWrapper.test.ts diff --git a/src/client/power-pages/create/CreateCommandWrapper.ts b/src/client/power-pages/create/CreateCommandWrapper.ts index 28710f292..8fb859d7c 100644 --- a/src/client/power-pages/create/CreateCommandWrapper.ts +++ b/src/client/power-pages/create/CreateCommandWrapper.ts @@ -15,23 +15,6 @@ import { createWebpage } from "./Webpage"; import { createWebTemplate } from "./WebTemplate"; const activeEditor = vscode.window.activeTextEditor; -function showGeneratorNotAvailableError(generator: GeneratorAcquisition): void { - const retryAction = vscode.l10n.t('Retry Installation'); - vscode.window.showErrorMessage( - vscode.l10n.t('Power Pages generator is not available. Please ensure npm is installed and try again.'), - retryAction - ).then(selection => { - if (selection === retryAction) { - const result = generator.ensureInstalled(); - if (result) { - vscode.window.showInformationMessage( - vscode.l10n.t('Power Pages generator installed successfully. Please reload the window to use the commands.') - ); - } - } - }); -} - export function initializeGenerator( context: vscode.ExtensionContext, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -41,12 +24,8 @@ export function initializeGenerator( generator.ensureInstalled(); context.subscriptions.push(generator); const yoCommandPath = generator.yoCommandPath; - - // Always register commands, even if generator installation failed - // Commands will show appropriate error messages if generator is not available - registerCreateCommands(context, yoCommandPath, generator); - if (yoCommandPath) { + registerCreateCommands(context, yoCommandPath); vscode.workspace .getConfiguration("powerPlatform") .update("generatorInstalled", true, true); @@ -55,20 +34,13 @@ export function initializeGenerator( function registerCreateCommands( context: vscode.ExtensionContext, - yoCommandPath: string | null, - generator: GeneratorAcquisition + yoCommandPath: string ) { vscode.commands.registerCommand( "microsoft-powerapps-portals.contentsnippet", async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: CONTENT_SNIPPET, triggerPoint: triggerPoint }); - - if (!yoCommandPath) { - showGeneratorNotAvailableError(generator); - return; - } - const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -86,12 +58,6 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: Tables.WEBTEMPLATE, triggerPoint: triggerPoint }); - - if (!yoCommandPath) { - showGeneratorNotAvailableError(generator); - return; - } - const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -109,12 +75,6 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: Tables.WEBPAGE, triggerPoint: triggerPoint }); - - if (!yoCommandPath) { - showGeneratorNotAvailableError(generator); - return; - } - const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -132,12 +92,6 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: Tables.PAGETEMPLATE, triggerPoint: triggerPoint }); - - if (!yoCommandPath) { - showGeneratorNotAvailableError(generator); - return; - } - const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, @@ -155,12 +109,6 @@ function registerCreateCommands( async (uri) => { const triggerPoint = uri ? TriggerPoint.CONTEXT_MENU : TriggerPoint.COMMAND_PALETTE; sendTelemetryEvent({ methodName: registerCreateCommands.name, eventName: UserFileCreateEvent, fileEntityType: WEBFILE, triggerPoint: triggerPoint }); - - if (!yoCommandPath) { - showGeneratorNotAvailableError(generator); - return; - } - const selectedWorkspaceFolder = await getSelectedWorkspaceFolder( uri, activeEditor, diff --git a/src/client/test/Integration/CreateCommandWrapper.test.ts b/src/client/test/Integration/CreateCommandWrapper.test.ts deleted file mode 100644 index 1748cac0e..000000000 --- a/src/client/test/Integration/CreateCommandWrapper.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -import * as vscode from 'vscode'; -import * as sinon from 'sinon'; -import { expect } from 'chai'; -import { initializeGenerator } from '../../power-pages/create/CreateCommandWrapper'; -import { GeneratorAcquisition } from '../../lib/GeneratorAcquisition'; -import { ICliAcquisitionContext } from '../../lib/CliAcquisitionContext'; - -describe('CreateCommandWrapper', () => { - let context: vscode.ExtensionContext; - let cliContext: ICliAcquisitionContext; - let registerCommandStub: sinon.SinonStub; - let generatorStub: sinon.SinonStubbedInstance; - - beforeEach(() => { - registerCommandStub = sinon.stub(vscode.commands, 'registerCommand'); - - context = { - subscriptions: [], - globalState: { - get: sinon.stub(), - update: sinon.stub(), - setKeysForSync: sinon.stub() - }, - workspaceState: { - get: sinon.stub(), - update: sinon.stub(), - keys: sinon.stub().returns([]) - } - } as unknown as vscode.ExtensionContext; - - cliContext = { - extensionPath: '/mock/path', - globalStorageLocalPath: '/mock/storage', - showInformationMessage: sinon.stub(), - showErrorMessage: sinon.stub(), - showCliPreparingMessage: sinon.stub(), - showCliReadyMessage: sinon.stub(), - showCliInstallFailedError: sinon.stub(), - locDotnetNotInstalledOrInsufficient: sinon.stub() - }; - }); - - afterEach(() => { - sinon.restore(); - }); - - it('should register commands even when generator installation fails', () => { - // Stub GeneratorAcquisition to simulate failed installation - sinon.stub(GeneratorAcquisition.prototype, 'ensureInstalled').returns(null); - sinon.stub(GeneratorAcquisition.prototype, 'yoCommandPath').get(() => null); - - initializeGenerator(context, cliContext); - - // Verify that all Power Pages commands were registered despite installation failure - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.contentsnippet')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webtemplate')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webpage')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.pagetemplate')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webfile')).to.be.true; - }); - - it('should register commands when generator installation succeeds', () => { - // Stub GeneratorAcquisition to simulate successful installation - sinon.stub(GeneratorAcquisition.prototype, 'ensureInstalled').returns('/mock/yo/path'); - sinon.stub(GeneratorAcquisition.prototype, 'yoCommandPath').get(() => '/mock/yo/path'); - - const updateStub = sinon.stub(); - sinon.stub(vscode.workspace, 'getConfiguration').returns({ - update: updateStub - } as unknown as vscode.WorkspaceConfiguration); - - initializeGenerator(context, cliContext); - - // Verify that all Power Pages commands were registered - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.contentsnippet')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webtemplate')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webpage')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.pagetemplate')).to.be.true; - expect(registerCommandStub.calledWith('microsoft-powerapps-portals.webfile')).to.be.true; - - // Verify that the configuration was updated to indicate generator is installed - expect(updateStub.calledWith('generatorInstalled', true, true)).to.be.true; - }); - - it('should show error message when command is invoked without generator installed', async () => { - // Stub GeneratorAcquisition to simulate failed installation - sinon.stub(GeneratorAcquisition.prototype, 'ensureInstalled').returns(null); - sinon.stub(GeneratorAcquisition.prototype, 'yoCommandPath').get(() => null); - - const showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined); - - initializeGenerator(context, cliContext); - - // Get the registered command handler for webpage - const webpageCommand = registerCommandStub.getCalls().find( - call => call.args[0] === 'microsoft-powerapps-portals.webpage' - ); - expect(webpageCommand).to.not.be.undefined; - - // Execute the command handler - if (webpageCommand) { - await webpageCommand.args[1](); - - // Verify that an error message was shown - expect(showErrorMessageStub.called).to.be.true; - const errorMessage = showErrorMessageStub.getCall(0).args[0]; - expect(errorMessage).to.contain('Power Pages generator is not available'); - } - }); -});