diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 051d2d47961..2d536ac79b9 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -1330,26 +1330,40 @@ "fontCharacter": "\\f1de" } }, - "aws-schemas-registry": { + "aws-sagemaker-code-editor": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1df" } }, - "aws-schemas-schema": { + "aws-sagemaker-jupyter-lab": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e0" } }, - "aws-stepfunctions-preview": { + "aws-schemas-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e1" } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e2" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e3" + } } }, "walkthroughs": [ diff --git a/packages/core/resources/icons/vscode/light/cloud-upload.svg b/packages/core/resources/icons/vscode/light/cloud-upload.svg new file mode 100644 index 00000000000..8d4bc7722a8 --- /dev/null +++ b/packages/core/resources/icons/vscode/light/cloud-upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/icons/vscode/light/run.svg b/packages/core/resources/icons/vscode/light/run.svg new file mode 100644 index 00000000000..8b0a58eca9b --- /dev/null +++ b/packages/core/resources/icons/vscode/light/run.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/markdown/lambdaEdit.md b/packages/core/resources/markdown/lambdaEdit.md index c8842cf19ec..31733fb441e 100644 --- a/packages/core/resources/markdown/lambdaEdit.md +++ b/packages/core/resources/markdown/lambdaEdit.md @@ -1,6 +1,6 @@ -# Welcome to Lambda Local Development +# Welcome to Lambda local development -Learn how to view your Lambda Function locally, iterate, and deploy changes to the AWS Cloud. +Learn how to view your Lambda function locally, iterate, and deploy changes to the AWS Cloud. ## Edit your Lambda function @@ -9,11 +9,11 @@ Learn how to view your Lambda Function locally, iterate, and deploy changes to t ## Manage your Lambda functions -- Select the AWS icon in the left sidebar and select **EXPLORER** +- Select the AWS Toolkit icon in the left sidebar and select **EXPLORER** - In your desired region, select the Lambda dropdown menu: - - To save and deploy a previously edited Lambda function, select the cloud deploy icon next to your Lambda function. - - To remotely invoke a function, select the play icon next to your Lambda function. + - To save and deploy a previously edited Lambda function, select the ![deploy](./deploy.svg) icon next to your Lambda function. + - To remotely invoke a function, select the ![invoke](./invoke.svg) icon next to your Lambda function. -## Advanced Features +## Advanced features - To convert to a Lambda function to an AWS SAM application, select the ![createStack](./create-stack.svg) icon next to your Lambda function. For details on what AWS SAM is and how it can help you, see the [AWS Serverless Application Model Developer Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html). diff --git a/packages/core/src/lambda/activation.ts b/packages/core/src/lambda/activation.ts index 1bb91a737b3..9873371d40f 100644 --- a/packages/core/src/lambda/activation.ts +++ b/packages/core/src/lambda/activation.ts @@ -17,7 +17,7 @@ import { ExtContext } from '../shared/extensions' import { invokeRemoteLambda } from './vue/remoteInvoke/invokeLambda' import { registerSamDebugInvokeVueCommand, registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend' import { Commands } from '../shared/vscode/commands2' -import { DefaultLambdaClient, getFunctionWithCredentials } from '../shared/clients/lambdaClient' +import { DefaultLambdaClient } from '../shared/clients/lambdaClient' import { copyLambdaUrl } from './commands/copyLambdaUrl' import { ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode' import { isTreeNode, TreeNode } from '../shared/treeview/resourceTreeDataProvider' @@ -29,47 +29,125 @@ import { ToolkitError, isError } from '../shared/errors' import { LogStreamFilterResponse } from '../awsService/cloudWatchLogs/wizard/liveTailLogStreamSubmenu' import { tempDirPath } from '../shared/filesystemUtilities' import fs from '../shared/fs/fs' -import { deployFromTemp, editLambda, getReadme, openLambdaFolderForEdit } from './commands/editLambda' -import { getTempLocation } from './utils' +import { + confirmOutdatedChanges, + deleteFilesInFolder, + deployFromTemp, + getReadme, + openLambdaFolderForEdit, + watchForUpdates, +} from './commands/editLambda' +import { compareCodeSha, getFunctionInfo, getTempLocation, setFunctionInfo } from './utils' import { registerLambdaUriHandler } from './uriHandlers' +import globals from '../shared/extensionGlobals' const localize = nls.loadMessageBundle() -/** - * Activates Lambda components. - */ -export async function activate(context: ExtContext): Promise { - try { - if (vscode.workspace.workspaceFolders) { - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - // Making the comparison case insensitive because Windows can have `C\` or `c\` - const workspacePath = workspaceFolder.uri.fsPath.toLowerCase() - const tempPath = path.join(tempDirPath, 'lambda').toLowerCase() - if (workspacePath.startsWith(tempPath)) { - const name = path.basename(workspaceFolder.uri.fsPath) - const region = path.basename(path.dirname(workspaceFolder.uri.fsPath)) - const getFunctionOutput = await getFunctionWithCredentials(region, name) - const configuration = getFunctionOutput.Configuration - await editLambda( - { - name, - region, - // Configuration as any due to the difference in types between sdkV2 and sdkV3 - configuration: configuration as any, - }, - true +async function openReadme() { + const readmeUri = vscode.Uri.file(await getReadme()) + // We only want to do it if there's not a readme already + const isPreviewOpen = vscode.window.tabGroups.all.some((group) => + group.tabs.some((tab) => tab.label.includes('README')) + ) + if (!isPreviewOpen) { + await vscode.commands.executeCommand('markdown.showPreviewToSide', readmeUri) + } +} + +async function quickEditActivation() { + if (vscode.workspace.workspaceFolders) { + for (const workspaceFolder of vscode.workspace.workspaceFolders) { + // Making the comparison case insensitive because Windows can have `C\` or `c\` + const workspacePath = workspaceFolder.uri.fsPath.toLowerCase() + const tempPath = path.join(tempDirPath, 'lambda').toLowerCase() + if (workspacePath.includes(tempPath)) { + const name = path.basename(workspaceFolder.uri.fsPath) + const region = path.basename(path.dirname(workspaceFolder.uri.fsPath)) + + const lambda = { name, region, configuration: undefined } + + watchForUpdates(lambda, vscode.Uri.file(workspacePath)) + + await openReadme() + + // Open handler function + try { + const handler = await getFunctionInfo(lambda, 'handlerFile') + const lambdaLocation = path.join(workspacePath, handler) + await openLambdaFile(lambdaLocation, vscode.ViewColumn.One) + } catch (e) { + void vscode.window.showWarningMessage( + localize('AWS.lambda.openFile.failure', `Failed to determine handler location: ${e}`) ) + } - const readmeUri = vscode.Uri.file(await getReadme()) - await vscode.commands.executeCommand('markdown.showPreview', readmeUri, vscode.ViewColumn.Two) + // Check if there are changes that need overwritten + try { + // Checking if there are changes that need to be overwritten + const prompt = localize( + 'AWS.lambda.download.confirmOutdatedSync', + 'There are changes to your function in the cloud since you last edited locally, do you want to overwrite your local changes?' + ) + + // Adding delay to give the authentication time to catch up + await new Promise((resolve) => globals.clock.setTimeout(resolve, 1000)) + + const overwriteChanges = !(await compareCodeSha(lambda)) + ? await confirmOutdatedChanges(prompt) + : false + if (overwriteChanges) { + // Close all open tabs from this workspace + const workspaceUri = vscode.Uri.file(workspacePath) + for (const tabGroup of vscode.window.tabGroups.all) { + const tabsToClose = tabGroup.tabs.filter( + (tab) => + tab.input instanceof vscode.TabInputText && + tab.input.uri.fsPath.startsWith(workspaceUri.fsPath) + ) + if (tabsToClose.length > 0) { + await vscode.window.tabGroups.close(tabsToClose) + } + } + + // Delete all files in the directory + await deleteFilesInFolder(workspacePath) + + // Show message to user about next steps + void vscode.window.showInformationMessage( + localize( + 'AWS.lambda.refresh.complete', + 'Local workspace cleared. Navigate to the Toolkit explorer to get fresh code from the cloud.' + ) + ) + + await setFunctionInfo(lambda, { undeployed: false }) + + // Remove workspace folder + const workspaceIndex = vscode.workspace.workspaceFolders?.findIndex( + (folder) => folder.uri.fsPath.toLowerCase() === workspacePath + ) + if (workspaceIndex !== undefined && workspaceIndex >= 0) { + vscode.workspace.updateWorkspaceFolders(workspaceIndex, 1) + } + } + } catch (e) { + void vscode.window.showWarningMessage( + localize( + 'AWS.lambda.pull.failure', + `Failed to pull latest changes from the cloud, you can still edit locally: ${e}` + ) + ) } } } - } catch (e) { - void vscode.window.showWarningMessage( - localize('AWS.lambda.open.failure', `Unable to edit Lambda Function locally: ${e}`) - ) } +} + +/** + * Activates Lambda components. + */ +export async function activate(context: ExtContext): Promise { + void quickEditActivation() context.extensionContext.subscriptions.push( Commands.register('aws.deleteLambda', async (node: LambdaFunctionNode | TreeNode) => { diff --git a/packages/core/src/lambda/commands/downloadLambda.ts b/packages/core/src/lambda/commands/downloadLambda.ts index 80932a34a76..bc189b54c81 100644 --- a/packages/core/src/lambda/commands/downloadLambda.ts +++ b/packages/core/src/lambda/commands/downloadLambda.ts @@ -194,7 +194,7 @@ async function downloadAndUnzipLambda( } } -export async function openLambdaFile(lambdaLocation: string): Promise { +export async function openLambdaFile(lambdaLocation: string, viewColumn?: vscode.ViewColumn): Promise { if (!(await fs.exists(lambdaLocation))) { const warning = localize( 'AWS.lambda.download.fileNotFound', @@ -206,7 +206,7 @@ export async function openLambdaFile(lambdaLocation: string): Promise { throw new Error() } const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(lambdaLocation)) - await vscode.window.showTextDocument(doc) + await vscode.window.showTextDocument(doc, viewColumn) } async function addLaunchConfigEntry( diff --git a/packages/core/src/lambda/commands/editLambda.ts b/packages/core/src/lambda/commands/editLambda.ts index b0b2498b528..5ea8169d280 100644 --- a/packages/core/src/lambda/commands/editLambda.ts +++ b/packages/core/src/lambda/commands/editLambda.ts @@ -22,6 +22,8 @@ import { LambdaFunctionNodeDecorationProvider } from '../explorer/lambdaFunction import path from 'path' import { telemetry } from '../../shared/telemetry/telemetry' import { ToolkitError } from '../../shared/errors' +import { getFunctionWithCredentials } from '../../shared/clients/lambdaClient' +import { getLogger } from '../../shared/logger/logger' const localize = nls.loadMessageBundle() @@ -45,9 +47,17 @@ export function watchForUpdates(lambda: LambdaFunction, projectUri: vscode.Uri): }) watcher.onDidDelete(async (fileUri) => { - // We don't want to sync if the whole directory has been deleted + // We don't want to sync if the whole directory has been deleted or emptied if (fileUri.fsPath !== projectUri.fsPath) { - await promptForSync(lambda, projectUri, fileUri) + // Check if directory is empty before prompting for sync + try { + const entries = await fs.readdir(projectUri.fsPath) + if (entries.length > 0) { + await promptForSync(lambda, projectUri, fileUri) + } + } catch (err) { + getLogger().debug(`Failed to check Lambda directory contents: ${err}`) + } } }) } @@ -84,7 +94,7 @@ export async function promptForSync(lambda: LambdaFunction, projectUri: vscode.U } } -async function confirmOutdatedChanges(prompt: string): Promise { +export async function confirmOutdatedChanges(prompt: string): Promise { return await showConfirmationMessage({ prompt, confirm: localize('AWS.lambda.upload.overwrite', 'Overwrite'), @@ -128,28 +138,62 @@ export async function deployFromTemp(lambda: LambdaFunction, projectUri: vscode. }) } +export async function deleteFilesInFolder(location: string) { + const entries = await fs.readdir(location) + await Promise.all( + entries.map((entry) => fs.delete(path.join(location, entry[0]), { recursive: true, force: true })) + ) +} + export async function editLambdaCommand(functionNode: LambdaFunctionNode) { const region = functionNode.regionCode const functionName = functionNode.configuration.FunctionName! - return await editLambda({ name: functionName, region, configuration: functionNode.configuration }) + return await editLambda({ name: functionName, region, configuration: functionNode.configuration }, 'explorer') +} + +export async function overwriteChangesForEdit(lambda: LambdaFunction, downloadLocation: string) { + try { + // Clear directory contents instead of deleting to avoid Windows EBUSY errors + if (await fs.existsDir(downloadLocation)) { + await deleteFilesInFolder(downloadLocation) + } else { + await fs.mkdir(downloadLocation) + } + + await downloadLambdaInLocation(lambda, 'local', downloadLocation) + + // Watching for updates, then setting info, then removing the badges must be done in this order + // This is because the files creating can throw the watcher, which sometimes leads to changes being marked as undeployed + watchForUpdates(lambda, vscode.Uri.file(downloadLocation)) + + await setFunctionInfo(lambda, { + lastDeployed: globals.clock.Date.now(), + undeployed: false, + sha: lambda.configuration!.CodeSha256, + handlerFile: getLambdaDetails(lambda.configuration!).fileName, + }) + await LambdaFunctionNodeDecorationProvider.getInstance().removeBadge( + vscode.Uri.file(downloadLocation), + vscode.Uri.from({ scheme: 'lambda', path: `${lambda.region}/${lambda.name}` }) + ) + } catch { + throw new ToolkitError('Failed to download Lambda function', { code: 'failedDownload' }) + } } -export async function editLambda(lambda: LambdaFunction, onActivation?: boolean) { +export async function editLambda(lambda: LambdaFunction, source?: 'workspace' | 'explorer') { return await telemetry.lambda_quickEditFunction.run(async () => { - telemetry.record({ source: onActivation ? 'workspace' : 'explorer' }) + telemetry.record({ source }) const downloadLocation = getTempLocation(lambda.name, lambda.region) - const downloadLocationName = vscode.workspace.asRelativePath(downloadLocation, true) // We don't want to do anything if the folder already exists as a workspace folder, it means it's already being edited - if ( - vscode.workspace.workspaceFolders?.some((folder) => folder.uri.fsPath === downloadLocation && !onActivation) - ) { + if (vscode.workspace.workspaceFolders?.some((folder) => folder.uri.fsPath === downloadLocation)) { return downloadLocation } const prompt = localize( 'AWS.lambda.download.confirmOutdatedSync', - 'There are changes to your Function in the cloud since you last edited locally, do you want to overwrite your local changes?' + 'There are changes to your function in the cloud since you last edited locally, do you want to overwrite your local changes?' ) // We want to overwrite changes in the following cases: @@ -159,41 +203,19 @@ export async function editLambda(lambda: LambdaFunction, onActivation?: boolean) // This record tells us if they're attempting to edit a function they've edited before telemetry.record({ action: localExists ? 'existingEdit' : 'newEdit' }) + const isDirectoryEmpty = (await fs.existsDir(downloadLocation)) + ? (await fs.readdir(downloadLocation)).length === 0 + : true + const overwriteChanges = - !localExists || (!(await compareCodeSha(lambda)) ? await confirmOutdatedChanges(prompt) : false) + !localExists || + isDirectoryEmpty || + (!(await compareCodeSha(lambda)) ? await confirmOutdatedChanges(prompt) : false) if (overwriteChanges) { - try { - // Clear directory contents instead of deleting to avoid Windows EBUSY errors - if (await fs.existsDir(downloadLocation)) { - const entries = await fs.readdir(downloadLocation) - await Promise.all( - entries.map((entry) => - fs.delete(path.join(downloadLocation, entry[0]), { recursive: true, force: true }) - ) - ) - } else { - await fs.mkdir(downloadLocation) - } - - await downloadLambdaInLocation(lambda, downloadLocationName, downloadLocation) - - // Watching for updates, then setting info, then removing the badges must be done in this order - // This is because the files creating can throw the watcher, which sometimes leads to changes being marked as undeployed - watchForUpdates(lambda, vscode.Uri.file(downloadLocation)) - await setFunctionInfo(lambda, { - lastDeployed: globals.clock.Date.now(), - undeployed: false, - sha: lambda.configuration!.CodeSha256, - }) - await LambdaFunctionNodeDecorationProvider.getInstance().removeBadge( - vscode.Uri.file(downloadLocation), - vscode.Uri.from({ scheme: 'lambda', path: `${lambda.region}/${lambda.name}` }) - ) - } catch { - throw new ToolkitError('Failed to download Lambda function', { code: 'failedDownload' }) - } - } else { + await overwriteChangesForEdit(lambda, downloadLocation) + } else if (source === 'explorer') { + // If the source is the explorer, we want to open, otherwise we just wait to open in the workspace const lambdaLocation = path.join(downloadLocation, getLambdaDetails(lambda.configuration!).fileName) await openLambdaFile(lambdaLocation) watchForUpdates(lambda, vscode.Uri.file(downloadLocation)) @@ -206,34 +228,58 @@ export async function editLambda(lambda: LambdaFunction, onActivation?: boolean) export async function openLambdaFolderForEdit(name: string, region: string) { const downloadLocation = getTempLocation(name, region) - if ( - vscode.workspace.workspaceFolders?.some((workspaceFolder) => - workspaceFolder.uri.fsPath.toLowerCase().startsWith(downloadLocation.toLowerCase()) - ) - ) { - // If the folder already exists in the workspace, show that folder - await vscode.commands.executeCommand('workbench.action.focusSideBar') - await vscode.commands.executeCommand('workbench.view.explorer') - } else { - await fs.mkdir(downloadLocation) + // Do all authentication work before opening workspace to avoid race condition + const getFunctionOutput = await getFunctionWithCredentials(region, name) + const configuration = getFunctionOutput.Configuration + // Download and set up Lambda code before opening workspace + await editLambda( + { + name, + region, + configuration: configuration as any, + }, + 'workspace' + ) + + try { await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(downloadLocation), { newWindow: true, noRecentEntry: true, }) + } catch (e) { + throw new ToolkitError(`Failed to open your function as a workspace: ${e}`, { code: 'folderOpenFailure' }) } } export async function getReadme(): Promise { - const readmeSource = 'resources/markdown/lambdaEdit.md' + const readmeSource = path.join('resources', 'markdown', 'lambdaEdit.md') const readmeDestination = path.join(lambdaTempPath, 'README.md') - const readmeContent = await fs.readFileText(globals.context.asAbsolutePath(readmeSource)) - await fs.writeFile(readmeDestination, readmeContent) + try { + const readmeContent = await fs.readFileText(globals.context.asAbsolutePath(readmeSource)) + await fs.writeFile(readmeDestination, readmeContent) + } catch (e) { + getLogger().info(`Failed to copy content for Lambda README: ${e}`) + } + + try { + const createStackIconSource = path.join('resources', 'icons', 'aws', 'lambda', 'create-stack-light.svg') + const createStackIconDestination = path.join(lambdaTempPath, 'create-stack.svg') + await fs.copy(globals.context.asAbsolutePath(createStackIconSource), createStackIconDestination) - // Put cloud deploy icon in the readme - const createStackIconSource = 'resources/icons/aws/lambda/create-stack-light.svg' - const createStackIconDestination = path.join(lambdaTempPath, 'create-stack.svg') - await fs.copy(globals.context.asAbsolutePath(createStackIconSource), createStackIconDestination) + // Copy VS Code built-in icons + const vscodeIconPath = path.join('resources', 'icons', 'vscode', 'light') + + const invokeIconSource = path.join(vscodeIconPath, 'run.svg') + const invokeIconDestination = path.join(lambdaTempPath, 'invoke.svg') + await fs.copy(globals.context.asAbsolutePath(invokeIconSource), invokeIconDestination) + + const deployIconSource = path.join(vscodeIconPath, 'cloud-upload.svg') + const deployIconDestination = path.join(lambdaTempPath, 'deploy.svg') + await fs.copy(globals.context.asAbsolutePath(deployIconSource), deployIconDestination) + } catch (e) { + getLogger().info(`Failed to copy content for Lambda README: ${e}`) + } return readmeDestination } diff --git a/packages/core/src/lambda/uriHandlers.ts b/packages/core/src/lambda/uriHandlers.ts index b5d6b4d6661..8ae1d7b8c35 100644 --- a/packages/core/src/lambda/uriHandlers.ts +++ b/packages/core/src/lambda/uriHandlers.ts @@ -10,7 +10,6 @@ import { SearchParams } from '../shared/vscode/uriHandler' import { openLambdaFolderForEdit } from './commands/editLambda' import { showConfirmationMessage } from '../shared/utilities/messages' import globals from '../shared/extensionGlobals' -import { getFunctionWithCredentials } from '../shared/clients/lambdaClient' import { telemetry } from '../shared/telemetry/telemetry' import { ToolkitError } from '../shared/errors' @@ -20,9 +19,6 @@ export function registerLambdaUriHandler() { async function openFunctionHandler(params: ReturnType) { await telemetry.lambda_uriHandler.run(async () => { try { - // We just want to be able to get the function - if it fails we abort and throw the error - await getFunctionWithCredentials(params.region, params.functionName) - if (params.isCfn === 'true') { const response = await showConfirmationMessage({ prompt: localize( diff --git a/packages/core/src/lambda/utils.ts b/packages/core/src/lambda/utils.ts index e4c2d929680..eeea6451342 100644 --- a/packages/core/src/lambda/utils.ts +++ b/packages/core/src/lambda/utils.ts @@ -166,7 +166,14 @@ export async function compareCodeSha(lambda: LambdaFunction): Promise { return local === remote } -export async function getFunctionInfo(lambda: LambdaFunction, field?: 'lastDeployed' | 'undeployed' | 'sha') { +export interface FunctionInfo { + lastDeployed?: number + undeployed?: boolean + sha?: string + handlerFile?: string +} + +export async function getFunctionInfo(lambda: LambdaFunction, field?: K) { try { const data = JSON.parse(await fs.readFileText(getInfoLocation(lambda))) getLogger().debug('Data returned from getFunctionInfo for %s: %O', lambda.name, data) @@ -176,16 +183,14 @@ export async function getFunctionInfo(lambda: LambdaFunction, field?: 'lastDeplo } } -export async function setFunctionInfo( - lambda: LambdaFunction, - info: { lastDeployed?: number; undeployed?: boolean; sha?: string } -) { +export async function setFunctionInfo(lambda: LambdaFunction, info: Partial) { try { const existing = await getFunctionInfo(lambda) - const updated = { + const updated: FunctionInfo = { lastDeployed: info.lastDeployed ?? existing.lastDeployed, undeployed: info.undeployed ?? true, sha: info.sha ?? (await getCodeShaLive(lambda)), + handlerFile: info.handlerFile ?? existing.handlerFile, } await fs.writeFile(getInfoLocation(lambda), JSON.stringify(updated)) } catch (err) { diff --git a/packages/core/src/test/lambda/commands/editLambda.test.ts b/packages/core/src/test/lambda/commands/editLambda.test.ts index b6b1f88d623..44d874c14fe 100644 --- a/packages/core/src/test/lambda/commands/editLambda.test.ts +++ b/packages/core/src/test/lambda/commands/editLambda.test.ts @@ -11,7 +11,9 @@ import { watchForUpdates, promptForSync, deployFromTemp, - openLambdaFolderForEdit, + getReadme, + deleteFilesInFolder, + overwriteChangesForEdit, } from '../../../lambda/commands/editLambda' import { LambdaFunction } from '../../../lambda/commands/uploadLambda' import * as downloadLambda from '../../../lambda/commands/downloadLambda' @@ -21,6 +23,8 @@ import * as messages from '../../../shared/utilities/messages' import fs from '../../../shared/fs/fs' import { LambdaFunctionNodeDecorationProvider } from '../../../lambda/explorer/lambdaFunctionNodeDecorationProvider' import path from 'path' +import globals from '../../../shared/extensionGlobals' +import { lambdaTempPath } from '../../../lambda/utils' describe('editLambda', function () { let mockLambda: LambdaFunction @@ -36,10 +40,15 @@ describe('editLambda', function () { let runUploadDirectoryStub: sinon.SinonStub let showConfirmationMessageStub: sinon.SinonStub let createFileSystemWatcherStub: sinon.SinonStub - let executeCommandStub: sinon.SinonStub let existsDirStub: sinon.SinonStub let mkdirStub: sinon.SinonStub let promptDeployStub: sinon.SinonStub + let readdirStub: sinon.SinonStub + let readFileTextStub: sinon.SinonStub + let writeFileStub: sinon.SinonStub + let copyStub: sinon.SinonStub + let asAbsolutePathStub: sinon.SinonStub + let deleteStub: sinon.SinonStub beforeEach(function () { mockLambda = { @@ -68,16 +77,19 @@ describe('editLambda', function () { onDidDelete: sinon.stub(), dispose: sinon.stub(), } as any) - executeCommandStub = sinon.stub(vscode.commands, 'executeCommand').resolves() existsDirStub = sinon.stub(fs, 'existsDir').resolves(true) mkdirStub = sinon.stub(fs, 'mkdir').resolves() + readdirStub = sinon.stub(fs, 'readdir').resolves([['file', vscode.FileType.File]]) promptDeployStub = sinon.stub().resolves(true) sinon.replace(require('../../../lambda/commands/editLambda'), 'promptDeploy', promptDeployStub) + readFileTextStub = sinon.stub(fs, 'readFileText').resolves('# Lambda Edit README') + writeFileStub = sinon.stub(fs, 'writeFile').resolves() + copyStub = sinon.stub(fs, 'copy').resolves() + asAbsolutePathStub = sinon.stub(globals.context, 'asAbsolutePath').callsFake((p) => `/absolute/${p}`) + deleteStub = sinon.stub(fs, 'delete').resolves() // Other stubs sinon.stub(utils, 'getLambdaDetails').returns({ fileName: 'index.js', functionName: 'test-function' }) - sinon.stub(fs, 'readdir').resolves([]) - sinon.stub(fs, 'delete').resolves() sinon.stub(fs, 'stat').resolves({ ctime: Date.now() } as any) sinon.stub(vscode.workspace, 'saveAll').resolves(true) sinon.stub(LambdaFunctionNodeDecorationProvider.prototype, 'addBadge').resolves() @@ -121,11 +133,32 @@ describe('editLambda', function () { compareCodeShaStub.resolves(false) showConfirmationMessageStub.resolves(false) - await editLambda(mockLambda) + // Specify that it's from the explorer because otherwise there's no need to open + await editLambda(mockLambda, 'explorer') assert(openLambdaFileStub.calledOnce) }) + it('downloads lambda when directory exists but is empty', async function () { + getFunctionInfoStub.resolves('old-sha') + readdirStub.resolves([]) + + await editLambda(mockLambda) + + assert(downloadLambdaStub.calledOnce) + assert(showConfirmationMessageStub.notCalled) + }) + + it('downloads lambda when directory does not exist', async function () { + getFunctionInfoStub.resolves('old-sha') + existsDirStub.resolves(false) + + await editLambda(mockLambda) + + assert(downloadLambdaStub.calledOnce) + assert(showConfirmationMessageStub.notCalled) + }) + it('sets up file watcher after download', async function () { const watcherStub = { onDidChange: sinon.stub(), @@ -222,29 +255,53 @@ describe('editLambda', function () { }) }) - describe('openLambdaFolderForEdit', function () { - it('focuses existing workspace folder if already open', async function () { - const subfolderPath = path.normalize(path.join(mockTemp, 'subfolder')) - sinon.stub(vscode.workspace, 'workspaceFolders').value([{ uri: vscode.Uri.file(subfolderPath) }]) + describe('deleteFilesInFolder', function () { + it('deletes all files in the specified folder', async function () { + readdirStub.resolves([ + ['file1.js', vscode.FileType.File], + ['file2.js', vscode.FileType.File], + ]) - await openLambdaFolderForEdit('test-function', 'us-east-1') + await deleteFilesInFolder(path.join('test', 'folder')) - assert(executeCommandStub.calledWith('workbench.action.focusSideBar')) - assert(executeCommandStub.calledWith('workbench.view.explorer')) + assert(deleteStub.calledTwice) + assert(deleteStub.calledWith(path.join('test', 'folder', 'file1.js'), { recursive: true, force: true })) + assert(deleteStub.calledWith(path.join('test', 'folder', 'file2.js'), { recursive: true, force: true })) }) + }) - it('opens new folder when not in workspace', async function () { - sinon.stub(vscode.workspace, 'workspaceFolders').value([]) + describe('overwriteChangesForEdit', function () { + it('clears directory and downloads lambda code', async function () { + await overwriteChangesForEdit(mockLambda, mockTemp) - await openLambdaFolderForEdit('test-function', 'us-east-1') + assert(readdirStub.calledWith(mockTemp)) + assert(downloadLambdaStub.calledWith(mockLambda, 'local', mockTemp)) + assert(setFunctionInfoStub.calledWith(mockLambda, sinon.match.object)) + }) - assert(mkdirStub.calledOnce) - assert( - executeCommandStub.calledWith('vscode.openFolder', sinon.match.any, { - newWindow: true, - noRecentEntry: true, - }) - ) + it('creates directory if it does not exist', async function () { + existsDirStub.resolves(false) + + await overwriteChangesForEdit(mockLambda, mockTemp) + + assert(mkdirStub.calledWith(mockTemp)) + }) + }) + + describe('getReadme', function () { + it('reads markdown file and writes README.md to temp path', async function () { + const result = await getReadme() + + assert(readFileTextStub.calledOnce) + assert(asAbsolutePathStub.calledWith(path.join('resources', 'markdown', 'lambdaEdit.md'))) + assert(writeFileStub.calledWith(path.join(lambdaTempPath, 'README.md'), '# Lambda Edit README')) + assert.strictEqual(result, path.join(lambdaTempPath, 'README.md')) + }) + + it('copies all required icon files', async function () { + await getReadme() + + assert.strictEqual(copyStub.callCount, 3) }) }) }) diff --git a/packages/core/src/test/lambda/utils.test.ts b/packages/core/src/test/lambda/utils.test.ts index 9ea5eaf21cd..bc430a2e20d 100644 --- a/packages/core/src/test/lambda/utils.test.ts +++ b/packages/core/src/test/lambda/utils.test.ts @@ -132,7 +132,7 @@ describe('lambda utils', function () { }) it('merges with existing data', async function () { - const existingData = { lastDeployed: 123456, undeployed: true, sha: 'old-sha' } + const existingData = { lastDeployed: 123456, undeployed: true, sha: 'old-sha', handlerFile: 'index.js' } sinon.stub(fs, 'readFileText').resolves(JSON.stringify(existingData)) const writeStub = sinon.stub(fs, 'writeFile').resolves() sinon.stub(DefaultLambdaClient.prototype, 'getFunction').resolves({ @@ -146,6 +146,7 @@ describe('lambda utils', function () { assert.strictEqual(writtenData.lastDeployed, 123456) assert.strictEqual(writtenData.undeployed, false) assert.strictEqual(writtenData.sha, 'new-sha') + assert.strictEqual(writtenData.handlerFile, 'index.js') }) }) diff --git a/packages/toolkit/.changes/next-release/Bug Fix-7be7d120-d44b-493d-85d1-9ab9260c958f.json b/packages/toolkit/.changes/next-release/Bug Fix-7be7d120-d44b-493d-85d1-9ab9260c958f.json new file mode 100644 index 00000000000..da18c361957 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-7be7d120-d44b-493d-85d1-9ab9260c958f.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Toolkit fails to recognize it's logged in when editing Lambda function" +}