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  icon next to your Lambda function.
+ - To remotely invoke a function, select the  icon next to your Lambda function.
-## Advanced Features
+## Advanced features
- To convert to a Lambda function to an AWS SAM application, select the  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"
+}